diff --git a/src/components/map/ThreeMap.vue b/src/components/map/ThreeMap.vue index 13ee5f5..4b92f95 100644 --- a/src/components/map/ThreeMap.vue +++ b/src/components/map/ThreeMap.vue @@ -132,6 +132,7 @@ interface ReusableModelResources { interface PoiMarkerCacheEntry { floorId: string + displayMode: PoiDisplayMode pois: RenderPoi[] group: THREE.Group markerSize: number @@ -393,20 +394,25 @@ const isExteriorModelNode = (object: THREE.Object3D) => ( )) ) -const getKnownFloorIdsBySpecificity = () => ( +const getKnownFloorMatchEntriesBySpecificity = () => ( floorIndex.value - .map((floor) => floor.floorId) - .sort((a, b) => b.length - a.length) + .flatMap((floor) => [floor.floorId, floor.label, ...(floor.modelMatchKeys || [])] + .filter((value): value is string => Boolean(value)) + .map((key) => ({ key, floorId: floor.floorId }))) + .sort((a, b) => b.key.length - a.key.length) ) const getModelNodeFloorIdByName = (name: string) => { if (!name) return '' - return getKnownFloorIdsBySpecificity().find((floorId) => ( - name === floorId - || name.startsWith(`${floorId}_`) - || name.startsWith(`${floorId}中心`) - )) || '' + const normalizedName = normalizeModelMatchKey(name) + return getKnownFloorMatchEntriesBySpecificity().find(({ key }) => ( + name === key + || name.startsWith(`${key}_`) + || name.startsWith(`${key}中心`) + || normalizedName === normalizeModelMatchKey(key) + || normalizedName.startsWith(normalizeModelMatchKey(key)) + ))?.floorId || '' } const getModelNodeFloorId = (object: THREE.Object3D) => ( @@ -489,6 +495,8 @@ const getPoiDisplayMode = (): PoiDisplayMode => { return 'detail' } +const getPoiMarkerCacheKey = (floorId: string, displayMode = getPoiDisplayMode()) => `${floorId}:${displayMode}` + const getPoiGlyph = (poi: RenderPoi) => ( poiGlyphMap[poi.iconType] || poi.name.charAt(0) || '点' ) @@ -521,6 +529,7 @@ const getActiveModelSpan = () => { } const getPoiVisibilityTier = (): PoiVisibilityTier => { + if (activeView.value === 'floor') return 'full' if (!controls || !activeModel) return 'full' const ratio = controls.getDistance() / getActiveModelSpan() @@ -2339,10 +2348,16 @@ const addFocusMarkerFromRequest = (request: TargetPoiFocusRequest) => { return poi } -const createPoiMarkerGroup = (floorId: string, pois: RenderPoi[], markerSize: number) => { +const createPoiMarkerGroup = ( + floorId: string, + displayMode: PoiDisplayMode, + pois: RenderPoi[], + markerSize: number +) => { const group = new THREE.Group() - group.name = `GuideModelPOI_${floorId}` + group.name = `GuideModelPOI_${floorId}_${displayMode}` group.userData.floorId = floorId + group.userData.displayMode = displayMode pois.forEach((poi) => { const [x, y, z] = poi.positionGltf! @@ -2374,22 +2389,25 @@ const loadFloorPOIs = async (floor: FloorIndexItem, loadToken?: number) => { if (loadToken !== undefined && !isCurrentModelLoad(loadToken)) return - const cachedEntry = poiMarkerGroupCache.get(floor.floorId) + const displayMode = getPoiDisplayMode() + const markerCacheKey = getPoiMarkerCacheKey(floor.floorId, displayMode) + const cachedEntry = poiMarkerGroupCache.get(markerCacheKey) if (cachedEntry) { attachPoiMarkerGroup(cachedEntry) return } const cachedPois = poiDataCache.get(floor.floorId) - const validPois = cachedPois || (await props.modelSource.loadFloorPois(floor.floorId)) - .filter((poi) => shouldShowPoiInCurrentMode(poi)) + const floorPois = cachedPois || await props.modelSource.loadFloorPois(floor.floorId) if (loadToken !== undefined && !isCurrentModelLoad(loadToken)) return - poiDataCache.set(floor.floorId, validPois) + poiDataCache.set(floor.floorId, floorPois) + const validPois = floorPois.filter((poi) => shouldShowPoiInCurrentMode(poi)) const markerSize = getPoiMarkerSize() - const markerGroup = createPoiMarkerGroup(floor.floorId, validPois, markerSize) + const markerGroup = createPoiMarkerGroup(floor.floorId, displayMode, validPois, markerSize) const entry: PoiMarkerCacheEntry = { floorId: floor.floorId, + displayMode, pois: validPois, group: markerGroup, markerSize @@ -2400,7 +2418,7 @@ const loadFloorPOIs = async (floor: FloorIndexItem, loadToken?: number) => { return } - poiMarkerGroupCache.set(floor.floorId, entry) + poiMarkerGroupCache.set(markerCacheKey, entry) attachPoiMarkerGroup(entry) } diff --git a/src/domain/guideFloor.ts b/src/domain/guideFloor.ts new file mode 100644 index 0000000..9fb08a1 --- /dev/null +++ b/src/domain/guideFloor.ts @@ -0,0 +1,70 @@ +import type { + MuseumFloor, + MuseumPoi +} from '@/domain/museum' + +interface FloorLike { + id?: string | number | null + label?: string | null + floorId?: string | number | null + floorCode?: string | null + floorName?: string | null + name?: string | null + modelGroup?: string | null +} + +const indoorFloorCodePattern = /^L-?\d+(?:\.\d+)?$/i +const indoorFloorLabelPattern = /^(?:B\d+(?:\.\d+)?|\d+(?:\.\d+)?F)$/i +const rawNumericIdPattern = /^\d{6,}$/ +const nonNavigableFloorPattern = /(EXTERIOR|OUTDOOR|BUILDING|FACADE|EXTERNAL|外立面|建筑外观|建筑外立面|室外模型|室外场景|室外底图|室外建筑|室外外观|室外)$/i + +const normalizeValue = (value: unknown) => ( + value === null || typeof value === 'undefined' ? '' : String(value).trim() +) + +export const isNonNavigableFloorToken = (value: unknown) => { + const normalized = normalizeValue(value) + return Boolean(normalized && nonNavigableFloorPattern.test(normalized)) +} + +export const isIndoorFloorToken = (value: unknown) => { + const normalized = normalizeValue(value) + if (!normalized || isNonNavigableFloorToken(normalized)) return false + + return indoorFloorCodePattern.test(normalized) + || indoorFloorLabelPattern.test(normalized) + || rawNumericIdPattern.test(normalized) +} + +export const isIndoorNavigableFloor = (floor: FloorLike | null | undefined) => { + if (!floor) return false + + const tokens = [ + floor.id, + floor.label, + floor.floorId, + floor.floorCode, + floor.floorName, + floor.name, + floor.modelGroup + ] + + if (tokens.some(isNonNavigableFloorToken)) return false + + return tokens.some(isIndoorFloorToken) +} + +export const isPoiOnIndoorNavigableFloor = (poi: Pick) => ( + isIndoorNavigableFloor({ + floorId: poi.floorId, + label: poi.floorLabel + }) +) + +export const filterIndoorNavigableFloors = (floors: T[]) => ( + floors.filter((floor) => isIndoorNavigableFloor(floor)) +) + +export const filterPoisOnIndoorNavigableFloors = >(pois: T[]) => ( + pois.filter(isPoiOnIndoorNavigableFloor) +) diff --git a/src/domain/guideModel.ts b/src/domain/guideModel.ts index 71d9179..988765a 100644 --- a/src/domain/guideModel.ts +++ b/src/domain/guideModel.ts @@ -15,6 +15,7 @@ export interface GuideModelFloorAsset { order: number modelUrl: string sharedModelAsset?: boolean + modelMatchKeys?: string[] } export interface GuideModelRenderPackage { diff --git a/src/repositories/GuideModelRepository.ts b/src/repositories/GuideModelRepository.ts index d295222..bb1d680 100644 --- a/src/repositories/GuideModelRepository.ts +++ b/src/repositories/GuideModelRepository.ts @@ -89,6 +89,26 @@ const resolveSgsAssetUrl = (url?: string | null) => { const sortSgsFloors = (floors: SgsSdkFloorSummaryPayload[]) => [...floors] .sort((a, b) => Number(a.sortOrder || 0) - Number(b.sortOrder || 0)) +const isSharedSgsModelAsset = ( + modelUrl: string, + overviewModelUrl: string, + allModelUrls: string[] +) => { + if (!modelUrl) return false + + return modelUrl === overviewModelUrl || allModelUrls.filter((url) => url === modelUrl).length > 1 +} + +const getSgsFloorModelMatchKeys = (floor: SgsSdkFloorSummaryPayload, label: string) => ( + [ + String(floor.floorId), + floor.floorCode, + floor.floorName, + label + ] + .filter((value): value is string => Boolean(value)) +) + export interface GuideModelRepository extends GuideModelSource {} export class StaticGuideModelRepository implements GuideModelRepository { @@ -144,17 +164,18 @@ export class SgsSdkGuideModelRepository implements GuideModelRepository { floors.map((floor) => this.provider.getFloorBundle(String(floor.floorId))) ) - const floorAssets: GuideModelFloorAsset[] = floors + const floorModelItems = floors .map((floor, index) => { const bundle = bundles[index] const modelUrl = resolveSgsAssetUrl(bundle.model?.modelUrl || bundle.model?.fallbackModelUrl) + const label = formatSgsFloorLabel(floor.floorCode, floor.floorName) return { floorId: String(floor.floorId), - label: formatSgsFloorLabel(floor.floorCode, floor.floorName), + label, order: Number(floor.sortOrder || 0), modelUrl, - sharedModelAsset: false + modelMatchKeys: getSgsFloorModelMatchKeys(floor, label) } }) .filter((asset) => asset.modelUrl) @@ -165,7 +186,12 @@ export class SgsSdkGuideModelRepository implements GuideModelRepository { : null const overviewModelUrl = resolveSgsAssetUrl( overviewBundle?.model?.modelUrl || overviewBundle?.model?.fallbackModelUrl - ) || floorAssets[0]?.modelUrl || '' + ) || floorModelItems[0]?.modelUrl || '' + const floorModelUrls = floorModelItems.map((asset) => asset.modelUrl) + const floorAssets: GuideModelFloorAsset[] = floorModelItems.map((asset) => ({ + ...asset, + sharedModelAsset: isSharedSgsModelAsset(asset.modelUrl, overviewModelUrl, floorModelUrls) + })) return { overviewModelUrl,