diff --git a/src/components/explain/ExplainHallSelect.vue b/src/components/explain/ExplainHallSelect.vue
new file mode 100644
index 0000000..decdcff
--- /dev/null
+++ b/src/components/explain/ExplainHallSelect.vue
@@ -0,0 +1,339 @@
+
+
+
+
+
+
+ 正在加载展厅讲解
+ 稍后将展示可选择的展厅。
+
+
+
+ 讲解内容加载失败
+ {{ error }}
+
+
+
+
+ 请选择展厅
+ {{ filteredHalls.length }} 个展厅
+
+
+
+
+
+
+ {{ hallIconText(hall.name) }}
+
+
+
+ {{ hall.name }}
+
+
+ {{ hall.floorLabel || '楼层待补充' }}
+
+ {{ hall.explainCount > 0 ? `${hall.explainCount} 条讲解` : '暂无讲解' }}
+
+
+
+ ›
+
+
+
+
+ 未找到相关展厅
+ 暂无可选择的展厅讲解。
+
+
+
+
+
+
+
+
+
diff --git a/src/components/map/ThreeMap.vue b/src/components/map/ThreeMap.vue
index 5245ceb..a633500 100644
--- a/src/components/map/ThreeMap.vue
+++ b/src/components/map/ThreeMap.vue
@@ -64,6 +64,7 @@ import type {
type ViewMode = 'overview' | 'floor' | 'multi'
type CameraPreset = 'top' | 'oblique'
+type TouchGestureMode = 'orbit' | 'pan'
type FloorIndexItem = GuideModelFloorAsset
@@ -79,12 +80,23 @@ type PoiVisibilityTier = 'tight' | 'balanced' | 'full'
interface PoiSpriteUserData {
poi?: RenderPoi
baseScale?: number
+ labelBaseScaleX?: number
+ labelBaseScaleY?: number
isPoiLabel?: boolean
isPoiPulse?: boolean
isPoiBase?: boolean
isCorePoi?: boolean
}
+interface FocusHallMaterialState {
+ material: THREE.Material
+ color?: THREE.Color
+ emissive?: THREE.Color
+ emissiveIntensity?: number
+ opacity: number
+ transparent: boolean
+}
+
interface TargetPoiFocusRequest {
requestId: number | string
poiId: string
@@ -93,6 +105,7 @@ interface TargetPoiFocusRequest {
floorLabel?: string
primaryCategoryZh?: string
positionGltf?: [number, number, number]
+ sourceObjectName?: string
}
interface TargetPoiFocusResult {
@@ -120,6 +133,8 @@ const props = withDefaults(defineProps<{
showControls?: boolean
showPoi?: boolean
targetFocus?: TargetPoiFocusRequest | null
+ touchGestureMode?: TouchGestureMode
+ targetFocusDistanceFactor?: number
routePreview?: GuideRouteResult | null
showRoute?: boolean
autoSwitch?: boolean
@@ -133,6 +148,8 @@ const props = withDefaults(defineProps<{
showControls: true,
showPoi: false,
targetFocus: null,
+ touchGestureMode: 'orbit',
+ targetFocusDistanceFactor: 0.22,
routePreview: null,
showRoute: false,
autoSwitch: true,
@@ -183,6 +200,8 @@ let routeGroup: THREE.Group | null = null
let activeFocusLabelSprite: THREE.Sprite | null = null
let activeFocusPulseSprite: THREE.Sprite | null = null
let activeFocusBaseSprite: THREE.Sprite | null = null
+let activeFocusHallGlowMesh: THREE.Mesh | null = null
+let activeFocusHallMaterialStates: FocusHallMaterialState[] = []
let animationId = 0
let resizeObserver: ResizeObserver | null = null
let isDisposed = false
@@ -201,6 +220,15 @@ let autoSwitchDisableTimer: ReturnType | null = null
let isProgrammaticCameraChange = false
let programmaticCameraTimer: ReturnType | null = null
+const syncControlInteractionOptions = () => {
+ if (!controls) return
+
+ const shouldEnablePan = props.showControls || props.touchGestureMode === 'pan'
+ controls.enablePan = shouldEnablePan
+ controls.touches.ONE = props.touchGestureMode === 'pan' ? THREE.TOUCH.PAN : THREE.TOUCH.ROTATE
+ controls.touches.TWO = THREE.TOUCH.DOLLY_PAN
+}
+
const corePoiCategories = new Set([
'basic_service_facility',
'transport_circulation',
@@ -276,6 +304,61 @@ const getModelNodeNames = (object: THREE.Object3D) => {
return names
}
+const normalizeModelMatchKey = (value: string) => (
+ value
+ .trim()
+ .toLowerCase()
+ .replace(/[\s_\-./\\]/g, '')
+)
+
+const getPoiModelMatchKeys = (poi: RenderPoi) => (
+ [
+ poi.sourceObjectName,
+ `${poi.floorId}_${poi.name}`,
+ poi.name
+ ]
+ .filter((value): value is string => Boolean(value))
+ .map(normalizeModelMatchKey)
+ .filter(Boolean)
+)
+
+const isPoiModelNodeMatch = (object: THREE.Object3D, poi: RenderPoi) => {
+ const keys = getPoiModelMatchKeys(poi)
+ if (!keys.length) return false
+
+ return getModelNodeNames(object)
+ .map(normalizeModelMatchKey)
+ .some((name) => keys.some((key) => name === key || name.includes(key)))
+}
+
+const findPoiModelRoot = (poi: RenderPoi): THREE.Object3D | null => {
+ if (!activeModel) return null
+
+ let matchedRoot: THREE.Object3D | null = null
+
+ activeModel.traverse((child) => {
+ if (matchedRoot || child === activeModel) return
+
+ const childName = child.name ? normalizeModelMatchKey(child.name) : ''
+ const keys = getPoiModelMatchKeys(poi)
+ if (childName && keys.some((key) => childName === key || childName.includes(key))) {
+ matchedRoot = child
+ }
+ })
+
+ if (matchedRoot) return matchedRoot
+
+ activeModel.traverse((child) => {
+ if (matchedRoot || !(child instanceof THREE.Mesh) || !child.visible) return
+
+ if (isPoiModelNodeMatch(child, poi)) {
+ matchedRoot = child.parent && child.parent !== activeModel ? child.parent : child
+ }
+ })
+
+ return matchedRoot
+}
+
const isExteriorModelNode = (object: THREE.Object3D) => (
getModelNodeNames(object).some((name) => (
floorExteriorNameKeywords.some((keyword) => name.includes(keyword))
@@ -591,12 +674,12 @@ const initThree = async () => {
controls = new OrbitControls(camera, renderer.domElement)
controls.enableDamping = true
controls.dampingFactor = 0.08
- controls.enablePan = props.showControls
controls.enableZoom = true
controls.minDistance = 6
controls.maxDistance = 1200
controls.minPolarAngle = 0.08
controls.maxPolarAngle = Math.PI * 0.48
+ syncControlInteractionOptions()
loader = new GLTFLoader()
poiGroup = new THREE.Group()
@@ -636,6 +719,7 @@ const startRenderLoop = () => {
if (isDisposed || !renderer || !scene || !camera) return
controls?.update()
+ updateFocusLabelScale()
renderer.render(scene, camera)
animationId = window.requestAnimationFrame(render)
}
@@ -675,6 +759,174 @@ const disposeObject = (object: THREE.Object3D) => {
})
}
+const getMaterialColor = (material: THREE.Material) => {
+ const candidate = material as THREE.Material & { color?: unknown }
+ return candidate.color instanceof THREE.Color ? candidate.color : undefined
+}
+
+const getMaterialEmissive = (material: THREE.Material) => {
+ const candidate = material as THREE.Material & { emissive?: unknown }
+ return candidate.emissive instanceof THREE.Color ? candidate.emissive : undefined
+}
+
+const getMaterialEmissiveIntensity = (material: THREE.Material) => {
+ const candidate = material as THREE.Material & { emissiveIntensity?: unknown }
+ return typeof candidate.emissiveIntensity === 'number'
+ ? candidate.emissiveIntensity
+ : undefined
+}
+
+const setMaterialEmissiveIntensity = (material: THREE.Material, value: number) => {
+ const candidate = material as THREE.Material & { emissiveIntensity?: number }
+ if (typeof candidate.emissiveIntensity === 'number') {
+ candidate.emissiveIntensity = value
+ }
+}
+
+const clearFocusHallHighlight = () => {
+ activeFocusHallMaterialStates.forEach((state) => {
+ const color = getMaterialColor(state.material)
+ const emissive = getMaterialEmissive(state.material)
+
+ if (color && state.color) {
+ color.copy(state.color)
+ }
+
+ if (emissive && state.emissive) {
+ emissive.copy(state.emissive)
+ }
+
+ if (typeof state.emissiveIntensity === 'number') {
+ setMaterialEmissiveIntensity(state.material, state.emissiveIntensity)
+ }
+
+ state.material.opacity = state.opacity
+ state.material.transparent = state.transparent
+ state.material.needsUpdate = true
+ })
+
+ activeFocusHallMaterialStates = []
+
+ if (activeFocusHallGlowMesh) {
+ activeFocusHallGlowMesh.parent?.remove(activeFocusHallGlowMesh)
+ const material = activeFocusHallGlowMesh.material
+ if (!Array.isArray(material)) {
+ const glowMaterial = material as THREE.MeshBasicMaterial
+ glowMaterial.map?.dispose()
+ }
+ disposeObject(activeFocusHallGlowMesh)
+ activeFocusHallGlowMesh = null
+ }
+}
+
+const applyFocusHallMaterial = (material: THREE.Material, seenMaterials: Set) => {
+ if (seenMaterials.has(material)) return
+ seenMaterials.add(material)
+
+ const color = getMaterialColor(material)
+ const emissive = getMaterialEmissive(material)
+ const emissiveIntensity = getMaterialEmissiveIntensity(material)
+
+ activeFocusHallMaterialStates.push({
+ material,
+ color: color?.clone(),
+ emissive: emissive?.clone(),
+ emissiveIntensity,
+ opacity: material.opacity,
+ transparent: material.transparent
+ })
+
+ const glowColor = new THREE.Color('#e0df00')
+
+ if (color) {
+ color.lerp(glowColor, 0.34)
+ }
+
+ if (emissive) {
+ emissive.copy(glowColor)
+ setMaterialEmissiveIntensity(material, Math.max(emissiveIntensity || 0, 0.76))
+ }
+
+ material.opacity = Math.max(material.opacity, 0.96)
+ material.transparent = true
+ material.needsUpdate = true
+}
+
+const createFocusHallGlowMesh = (poi: RenderPoi) => {
+ if (!poi.positionGltf) return null
+
+ const canvas = document.createElement('canvas')
+ canvas.width = 256
+ canvas.height = 256
+ const context = canvas.getContext('2d')
+
+ if (context) {
+ const gradient = context.createRadialGradient(128, 128, 16, 128, 128, 118)
+ gradient.addColorStop(0, 'rgba(224, 223, 0, 0.52)')
+ gradient.addColorStop(0.45, 'rgba(224, 223, 0, 0.34)')
+ gradient.addColorStop(0.78, 'rgba(224, 223, 0, 0.16)')
+ gradient.addColorStop(1, 'rgba(224, 223, 0, 0)')
+
+ context.clearRect(0, 0, canvas.width, canvas.height)
+ context.fillStyle = gradient
+ context.fillRect(0, 0, canvas.width, canvas.height)
+ }
+
+ const texture = new THREE.CanvasTexture(canvas)
+ texture.colorSpace = THREE.SRGBColorSpace
+ const material = new THREE.MeshBasicMaterial({
+ map: texture,
+ transparent: true,
+ opacity: 0.78,
+ depthTest: false,
+ depthWrite: false,
+ side: THREE.DoubleSide
+ })
+ const hallGlowSize = Math.max(getPoiMarkerSize() * 14, getActiveModelSpan() * 0.14)
+ const geometry = new THREE.PlaneGeometry(hallGlowSize, hallGlowSize * 0.72)
+ const mesh = new THREE.Mesh(geometry, material)
+ const [x, y, z] = poi.positionGltf
+
+ mesh.name = 'GuideFocusHallGlow'
+ mesh.position.set(x, y + 0.08, z)
+ mesh.rotation.x = -Math.PI / 2
+ mesh.renderOrder = 7
+
+ return mesh
+}
+
+const showFocusHallHighlight = (poi: RenderPoi) => {
+ clearFocusHallHighlight()
+ if (!activeModel) return
+
+ activeFocusHallGlowMesh = createFocusHallGlowMesh(poi)
+ if (activeFocusHallGlowMesh) {
+ poiGroup?.add(activeFocusHallGlowMesh)
+ }
+
+ const modelRoot = findPoiModelRoot(poi)
+ if (!modelRoot) return
+
+ const seenMaterials = new Set()
+ let matchedMeshCount = 0
+
+ modelRoot.traverse((child) => {
+ if (!(child instanceof THREE.Mesh) || !child.visible) return
+
+ matchedMeshCount += 1
+ const materials = Array.isArray(child.material) ? child.material : [child.material]
+ materials.forEach((material) => {
+ if (material) {
+ applyFocusHallMaterial(material, seenMaterials)
+ }
+ })
+ })
+
+ if (!matchedMeshCount) {
+ clearFocusHallHighlight()
+ }
+}
+
const disposeCachedOverviewModel = () => {
if (!cachedOverviewModel) return
@@ -840,6 +1092,7 @@ const renderRoutePreview = () => {
const clearSceneData = () => {
if (activeModel && scene) {
+ clearFocusHallHighlight()
scene.remove(activeModel)
if (activeModel === cachedOverviewModel) {
cachedOverviewModel.visible = false
@@ -1390,8 +1643,10 @@ const createPoiLabelSprite = (poi: RenderPoi, markerSize: number) => {
depthWrite: false
}))
sprite.userData.isPoiLabel = true
+ sprite.userData.labelBaseScaleX = markerSize * 5.6
+ sprite.userData.labelBaseScaleY = markerSize * 1.68
sprite.renderOrder = 30
- sprite.scale.set(markerSize * 5.6, markerSize * 1.68, 1)
+ sprite.scale.set(sprite.userData.labelBaseScaleX, sprite.userData.labelBaseScaleY, 1)
return sprite
}
@@ -1501,6 +1756,7 @@ const showFocusPoiLabel = (poi: RenderPoi) => {
const [x, y, z] = poi.positionGltf
activeFocusLabelSprite = createPoiLabelSprite(poi, markerSize)
activeFocusLabelSprite.position.set(x, y + markerSize * 2.35, z)
+ updateFocusLabelScale()
poiGroup.add(activeFocusLabelSprite)
}
@@ -1529,6 +1785,7 @@ const showFocusPoiPulse = (poi: RenderPoi) => {
}
const showFocusPoiAffordances = (poi: RenderPoi) => {
+ showFocusHallHighlight(poi)
showFocusPoiBase(poi)
showFocusPoiPulse(poi)
showFocusPoiLabel(poi)
@@ -1552,6 +1809,28 @@ const getPoiMarkerSize = () => (
: 3
)
+const getFocusLabelScaleBoost = () => {
+ if (!controls || !activeModel) return 1
+
+ const distanceRatio = controls.getDistance() / getActiveModelSpan()
+ return Math.min(2.25, Math.max(1, distanceRatio / 0.36))
+}
+
+const updateFocusLabelScale = () => {
+ if (!activeFocusLabelSprite) return
+
+ const userData = getPoiSpriteUserData(activeFocusLabelSprite)
+ const baseScaleX = typeof userData.labelBaseScaleX === 'number'
+ ? userData.labelBaseScaleX
+ : activeFocusLabelSprite.scale.x
+ const baseScaleY = typeof userData.labelBaseScaleY === 'number'
+ ? userData.labelBaseScaleY
+ : activeFocusLabelSprite.scale.y
+ const scaleBoost = getFocusLabelScaleBoost()
+
+ activeFocusLabelSprite.scale.set(baseScaleX * scaleBoost, baseScaleY * scaleBoost, 1)
+}
+
const setPoiSpriteFocusStyle = (sprite: THREE.Sprite, focused: boolean) => {
const userData = getPoiSpriteUserData(sprite)
const baseScale = typeof userData.baseScale === 'number'
@@ -1603,7 +1882,8 @@ const createPoiFromFocusRequest = (request: TargetPoiFocusRequest): RenderPoi |
primaryCategory: 'target_preview',
primaryCategoryZh: request.primaryCategoryZh || '位置预览',
iconType: 'target_preview',
- positionGltf: request.positionGltf
+ positionGltf: request.positionGltf,
+ sourceObjectName: request.sourceObjectName
}
}
@@ -1654,6 +1934,7 @@ const loadFloorPOIs = async (floor: FloorIndexItem, loadToken?: number) => {
const clearMapSelection = (shouldEmit = true) => {
activeFocusPoiId.value = ''
selectedPOI.value = null
+ clearFocusHallHighlight()
disposeFocusLabel()
disposeFocusPulse()
disposeFocusBase()
@@ -1779,7 +2060,12 @@ const focusCameraOnPoi = (poi: RenderPoi) => {
? new THREE.Box3().setFromObject(activeModel).getSize(new THREE.Vector3())
: new THREE.Vector3(80, 80, 80)
const maxDim = Math.max(modelSize.x, modelSize.y, modelSize.z, 1)
- const distance = Math.min(Math.max(maxDim * 0.22, 28), Math.max(maxDim * 0.55, 80))
+ const focusDistanceFactor = Math.max(props.targetFocusDistanceFactor, 0.1)
+ const focusMaxDistanceFactor = Math.max(0.55, focusDistanceFactor * 1.4)
+ const distance = Math.min(
+ Math.max(maxDim * focusDistanceFactor, 28),
+ Math.max(maxDim * focusMaxDistanceFactor, 80)
+ )
const direction = new THREE.Vector3(0.72, 0.58, 1).normalize()
camera.position.copy(target).add(direction.multiplyScalar(distance))
@@ -2036,6 +2322,10 @@ watch(() => props.modelSource, () => {
init3DScene()
})
+watch(() => [props.showControls, props.touchGestureMode], () => {
+ syncControlInteractionOptions()
+})
+
watch(() => props.targetFocus, (request) => {
queueTargetFocus(request || null)
}, {
diff --git a/src/components/navigation/GuideMapShell.vue b/src/components/navigation/GuideMapShell.vue
index f1631f4..fecdf80 100644
--- a/src/components/navigation/GuideMapShell.vue
+++ b/src/components/navigation/GuideMapShell.vue
@@ -25,6 +25,8 @@
:show-controls="false"
:show-poi="shouldShowIndoorPois"
:target-focus="targetFocusRequest"
+ :touch-gesture-mode="touchGestureMode"
+ :target-focus-distance-factor="targetFocusDistanceFactor"
:route-preview="routePreview"
:show-route="showRoute"
@floor-change="handleThreeFloorChange"
@@ -216,6 +218,7 @@ interface TargetPoiFocusRequest {
floorLabel?: string
primaryCategoryZh?: string
positionGltf?: [number, number, number]
+ sourceObjectName?: string
}
interface TargetPoiFocusResult {
@@ -228,6 +231,7 @@ interface TargetPoiFocusResult {
type IndoorViewMode = 'overview' | 'floor' | 'multi'
type LayerDisplayMode = 'single' | 'multi'
+type TouchGestureMode = 'orbit' | 'pan'
const props = withDefaults(defineProps<{
searchText?: string
@@ -267,6 +271,8 @@ const props = withDefaults(defineProps<{
normalizeFloorId?: (labelOrId: string) => string
dimmed?: boolean
targetFocusRequest?: TargetPoiFocusRequest | null
+ touchGestureMode?: TouchGestureMode
+ targetFocusDistanceFactor?: number
routePreview?: GuideRouteResult | null
showRoute?: boolean
}>(), {
@@ -307,6 +313,8 @@ const props = withDefaults(defineProps<{
normalizeFloorId: (labelOrId: string) => labelOrId,
dimmed: false,
targetFocusRequest: null,
+ touchGestureMode: 'orbit',
+ targetFocusDistanceFactor: 0.22,
routePreview: null,
showRoute: false
})
diff --git a/src/components/navigation/GuidePageFrame.vue b/src/components/navigation/GuidePageFrame.vue
index 1a2bb81..c5d7793 100644
--- a/src/components/navigation/GuidePageFrame.vue
+++ b/src/components/navigation/GuidePageFrame.vue
@@ -1,6 +1,10 @@
-
+
{
background: var(--museum-bg-light);
}
+.variant-static.no-top-tabs .guide-page-frame-body {
+ top: 0;
+}
+
.guide-page-frame-actions {
position: absolute;
top: 0;
diff --git a/src/data/adapters/navAssetsAdapter.ts b/src/data/adapters/navAssetsAdapter.ts
index 6bddfe9..57c1886 100644
--- a/src/data/adapters/navAssetsAdapter.ts
+++ b/src/data/adapters/navAssetsAdapter.ts
@@ -54,6 +54,7 @@ export const toMuseumPoi = (poi: StaticNavPoiPayload): MuseumPoi => ({
},
categories: (poi.categories || []).map(toCategory),
positionGltf: poi.positionGltf,
+ sourceObjectName: poi.sourceObjectName,
sourceConfidence: poi.sourceConfidence,
navigationReadiness: poi.navigationReadiness,
accessible: isPoiAccessible(poi)
diff --git a/src/data/providers/staticNavAssetsProvider.ts b/src/data/providers/staticNavAssetsProvider.ts
index e9d7563..ec67d77 100644
--- a/src/data/providers/staticNavAssetsProvider.ts
+++ b/src/data/providers/staticNavAssetsProvider.ts
@@ -24,6 +24,7 @@ export interface StaticNavPoiPayload {
categories?: StaticNavPoiCategoryPayload[]
iconType?: string
positionGltf?: [number, number, number]
+ sourceObjectName?: string
navigationReadiness?: string
sourceConfidence?: string
}
diff --git a/src/domain/guideModel.ts b/src/domain/guideModel.ts
index eb2efe8..71d9179 100644
--- a/src/domain/guideModel.ts
+++ b/src/domain/guideModel.ts
@@ -6,6 +6,7 @@ export interface GuideRenderPoi {
primaryCategoryZh: string
iconType: string
positionGltf?: [number, number, number]
+ sourceObjectName?: string
}
export interface GuideModelFloorAsset {
diff --git a/src/domain/museum.ts b/src/domain/museum.ts
index 854ebbd..5e7f768 100644
--- a/src/domain/museum.ts
+++ b/src/domain/museum.ts
@@ -18,6 +18,7 @@ export interface MuseumPoi {
primaryCategory: MuseumCategory
categories: MuseumCategory[]
positionGltf?: [number, number, number]
+ sourceObjectName?: string
sourceConfidence?: string
navigationReadiness?: string
accessible: boolean
@@ -85,6 +86,7 @@ export interface GuideLocationPreview {
floorLabel: string
primaryCategoryZh?: string
positionGltf?: [number, number, number]
+ sourceObjectName?: string
}
export interface GuideStartLocation {
diff --git a/src/pages/exhibit/detail.vue b/src/pages/exhibit/detail.vue
index 75b16a3..b1dee1f 100644
--- a/src/pages/exhibit/detail.vue
+++ b/src/pages/exhibit/detail.vue
@@ -2,147 +2,73 @@
-
-
-
-
-
- 讲解
-
-
-
-
-
-
- {{ audioStatusText }}
-
+
+
+
+
+
+ 讲解
-
- {{ contentTypeText }}
+
{{ exhibit.title }}
- {{ exhibit.subtitle }}
+ {{ detailMeta }}
-
-
- 内容类型
- {{ contentTypeText }}
+
+
+
+
-
- 所属展厅
- {{ exhibit.hallName }}
-
-
- 所在楼层
- {{ exhibit.floorLabel }}
-
-
- 讲解状态
- {{ audioStatusText }}
+ 00:00
+
+
+ {{ exhibit.audio.durationLabel || '音频讲解' }}
-
- {{ audioNoteTitle }}
-
- {{ exhibit.audio.unavailableReason || '当前仅提供图文讲解,正式音频接入后将显示播放入口。' }}
+
+ 图文讲解
+
+ {{ exhibit.audio.unavailableReason || '该讲解暂无已发布音频,当前提供图文讲解。' }}
- 讲解摘要
- {{ exhibit.summary }}
-
-
-
- 讲解介绍
- {{ exhibit.body }}
-
-
-
-
-
-
- {{ chapter.title }}
-
- {{ formatChapterTime(chapter.startTime) }}
-
-
-
- 正式章节数据接入后将在这里展示。
-
-
-
-
-
- {{ exhibit.transcript || '正式文稿数据接入后将在这里展示。' }}
-
-
-
-
- 位置说明
- {{ locationDescription }}
+ 讲解内容
+ {{ exhibit.body || exhibit.summary }}
-
-
-
-
-
- 收藏
-
-
-
-
-
-
- 分享
+ 查看位置
@@ -174,12 +100,10 @@ import {
} from '@/usecases/guideUseCase'
import {
toExplainDetailPageViewModel,
- type ExplainContentType,
type ExplainDetailPageViewModel
} from '@/view-models/explainViewModels'
import {
isGuideTopTab,
- navigateToGuideTopTab,
type GuideTopTab
} from '@/utils/guideTopTabs'
@@ -188,12 +112,12 @@ const defaultDetail: ExplainDetailPageViewModel = {
title: '讲解内容',
subtitle: '位置待补充',
contentType: 'exhibit',
- coverImages: ['/static/exhibit-placeholder.jpg'],
+ coverImages: [],
summary: '该讲解内容待补充。',
body: '该讲解内容待补充。',
audio: {
status: 'unavailable',
- unavailableReason: '该展项暂无已发布音频,当前提供图文讲解。'
+ unavailableReason: '该讲解暂无已发布音频,当前提供图文讲解。'
},
chapters: [],
relatedItems: []
@@ -209,59 +133,28 @@ const audioPlayerRef = ref(null)
const showAudioPlayer = ref(false)
const currentAudio = ref(null)
const isPlaying = ref(false)
-const isCollected = ref(false)
-const activeTopTab = ref('guide')
+const activeTopTab = ref('explain')
const heroImage = computed(() => exhibit.value.coverImages[0])
-const audioStatusText = computed(() => {
- if (exhibit.value.audio.status === 'playable') {
- return exhibit.value.audio.durationLabel || '可播放'
- }
- if (exhibit.value.audio.status === 'none') {
- return '图文讲解'
- }
- return '图文讲解'
-})
-
-const audioNoteTitle = computed(() => (
- exhibit.value.audio.status === 'playable'
- ? '音频讲解'
- : '图文讲解'
-))
-
-const contentTypeText = computed(() => {
- const map: Record = {
- hall: '空间讲解',
- zone: '展区讲解',
- exhibit: '展品讲解',
- theme: '主题讲解'
- }
- return map[exhibit.value.contentType]
-})
-
-const locationDescription = computed(() => (
- exhibit.value.location?.note || '当前支持三维位置预览。'
-))
-
-onLoad(async (options: any) => {
- if (options.id) {
- const exhibitData = await explainUseCase.getExhibitById(options.id)
- if (exhibitData) {
- exhibit.value = toExplainDetailPageViewModel(exhibitData)
- }
- }
+const detailMeta = computed(() => [
+ exhibit.value.hallName,
+ exhibit.value.floorLabel
+].filter(Boolean).join(' · '))
+onLoad(async (options: any = {}) => {
const tab = Array.isArray(options.tab) ? options.tab[0] : options.tab
if (isGuideTopTab(tab)) {
activeTopTab.value = tab
}
-})
-const formatChapterTime = (seconds: number) => {
- const minute = Math.floor(seconds / 60)
- const second = Math.floor(seconds % 60)
- return `${minute}:${String(second).padStart(2, '0')}`
-}
+ const exhibitId = Array.isArray(options.id) ? options.id[0] : options.id
+ if (!exhibitId) return
+
+ const exhibitData = await explainUseCase.getExhibitById(exhibitId)
+ if (exhibitData) {
+ exhibit.value = toExplainDetailPageViewModel(exhibitData)
+ }
+})
const handlePlayAudio = async () => {
if (exhibit.value.audio.url) {
@@ -376,22 +269,6 @@ const handleNavigate = async () => {
})
}
-const handleCollect = () => {
- isCollected.value = !isCollected.value
- uni.showToast({
- title: isCollected.value ? '已加入本地收藏' : '已取消本地收藏',
- icon: 'none'
- })
-}
-
-const handleShare = () => {
- uni.showShareMenu()
-}
-
-const handleTopTabChange = (tab: GuideTopTab) => {
- navigateToGuideTopTab(tab)
-}
-
const handleBack = () => {
uni.navigateBack({
delta: 1,
@@ -405,285 +282,208 @@ const handleBack = () => {
diff --git a/src/pages/hall/detail.vue b/src/pages/hall/detail.vue
index f32f7e2..d2da82c 100644
--- a/src/pages/hall/detail.vue
+++ b/src/pages/hall/detail.vue
@@ -2,70 +2,83 @@
-
-
-
-
-
-
- {{ hall.floorLabel }}
+
+
+
+ {{ hall.name }}
+
+ {{ hall.floorLabel }} · {{ explainCountText }}
+
+
+
+
+
+ 查看展厅位置
+
-
-
- {{ hall.name }}
- {{ hall.description }}
+
+ 正在加载讲解列表
+ 稍后将展示该展厅下的讲解内容。
+
-
-
- {{ hall.exhibitCount }}
- 讲解内容
-
-
- {{ hall.floorLabel }}
- 所在楼层
-
-
+
+ 讲解列表加载失败
+ {{ error }}
+
-
-
- 空间讲解内容
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ exhibit.title }}
+ {{ itemMeta(exhibit) }}
+
+
+ ›
+
+
+ 暂无讲解内容
+ 该展厅暂未接入可展示的讲解条目。
+
-
-
-
-
- {{ hall.location?.actionText || '暂无位置' }}
-
-
diff --git a/src/pages/index/index.vue b/src/pages/index/index.vue
index 14fc199..ef5f703 100644
--- a/src/pages/index/index.vue
+++ b/src/pages/index/index.vue
@@ -1,9 +1,10 @@
-
-
-
-
-
-
@@ -227,9 +205,9 @@ import RoutePlannerPanel from '@/components/navigation/RoutePlannerPanel.vue'
import type {
RoutePointOption
} from '@/components/navigation/RoutePointPicker.vue'
-import ExplainList, { type Exhibit } from '@/components/explain/ExplainList.vue'
-import FloatingAudioButton from '@/components/audio/FloatingAudioButton.vue'
-import AudioPlayer, { type AudioItem } from '@/components/audio/AudioPlayer.vue'
+import ExplainHallSelect, {
+ type ExplainHallSelectItem
+} from '@/components/explain/ExplainHallSelect.vue'
import {
getLastGuideLocationPreviewUrl,
isGuideTopTab,
@@ -244,15 +222,13 @@ import {
import {
explainUseCase
} from '@/usecases/explainUseCase'
-import {
- toExplainCardViewModel
-} from '@/view-models/explainViewModels'
import type {
GuideRenderPoi
} from '@/domain/guideModel'
import type {
GuideRouteResult,
GuideRouteTarget,
+ MuseumExhibit,
MuseumFloor
} from '@/domain/museum'
@@ -355,11 +331,7 @@ const routeSimulationStepText = computed(() => {
return hasConnector ? '路线示意 · 跨楼层连接点' : '路线示意 · 同层位置关系'
})
-// 音频播放器状态
-const showAudioPlayer = ref(false)
-const isAudioPlaying = ref(false)
-const currentAudio = ref(null)
-const explainExhibits = ref([])
+const explainHallItems = ref([])
const explainLoading = ref(false)
const explainError = ref('')
let explainLoadPromise: Promise | null = null
@@ -469,7 +441,7 @@ const loadGuideFloors = async () => {
}
const loadExplainExhibits = async () => {
- if (explainExhibits.value.length) return
+ if (explainHallItems.value.length) return
if (explainLoadPromise) return explainLoadPromise
explainLoading.value = true
@@ -477,12 +449,15 @@ const loadExplainExhibits = async () => {
explainLoadPromise = (async () => {
try {
- const exhibits = await explainUseCase.listExplainExhibits()
- explainExhibits.value = exhibits.map(toExplainCardViewModel)
+ const [halls, exhibits] = await Promise.all([
+ explainUseCase.listHalls(),
+ explainUseCase.listExplainExhibits()
+ ])
+ explainHallItems.value = buildExplainHallItems(halls, exhibits)
} catch (error) {
console.error('加载讲解内容失败:', error)
explainError.value = '讲解内容加载失败,请稍后重试'
- explainExhibits.value = []
+ explainHallItems.value = []
} finally {
explainLoading.value = false
explainLoadPromise = null
@@ -492,6 +467,68 @@ const loadExplainExhibits = async () => {
return explainLoadPromise
}
+const normalizeExplainKeyword = (value?: string) => (value || '').trim().toLowerCase()
+
+const buildExplainHallItems = (
+ halls: Awaited>,
+ exhibits: MuseumExhibit[]
+): ExplainHallSelectItem[] => {
+ const countsByHallId = new Map()
+ const countsByHallName = new Map()
+ const keywordsByHallId = new Map>()
+ const keywordsByHallName = new Map>()
+
+ exhibits.forEach((exhibit) => {
+ const keywordParts = [
+ exhibit.name,
+ exhibit.hallName,
+ exhibit.floorLabel,
+ exhibit.description,
+ exhibit.guideTitle,
+ exhibit.guideText,
+ ...(exhibit.tags || [])
+ ].filter(Boolean) as string[]
+
+ if (exhibit.hallId) {
+ countsByHallId.set(exhibit.hallId, (countsByHallId.get(exhibit.hallId) || 0) + 1)
+ const keywords = keywordsByHallId.get(exhibit.hallId) || new Set()
+ keywordParts.forEach((part) => keywords.add(part))
+ keywordsByHallId.set(exhibit.hallId, keywords)
+ }
+
+ if (exhibit.hallName) {
+ countsByHallName.set(exhibit.hallName, (countsByHallName.get(exhibit.hallName) || 0) + 1)
+ const keywords = keywordsByHallName.get(exhibit.hallName) || new Set()
+ keywordParts.forEach((part) => keywords.add(part))
+ keywordsByHallName.set(exhibit.hallName, keywords)
+ }
+ })
+
+ return halls.map((hall) => {
+ const explainCount = countsByHallId.get(hall.id)
+ || countsByHallName.get(hall.name)
+ || hall.exhibitCount
+ || 0
+ const keywordParts = [
+ hall.name,
+ hall.floorLabel,
+ hall.description,
+ hall.area,
+ ...Array.from(keywordsByHallId.get(hall.id) || []),
+ ...Array.from(keywordsByHallName.get(hall.name) || [])
+ ].filter(Boolean) as string[]
+
+ return {
+ id: hall.id,
+ name: hall.name,
+ floorLabel: hall.floorLabel,
+ image: hall.image,
+ explainCount,
+ searchText: normalizeExplainKeyword(keywordParts.join(' '))
+ }
+ })
+}
+
const handleGuideSearchTap = () => {
uni.navigateTo({
url: '/pages/search/index'
@@ -843,131 +880,21 @@ const guideSearchText = computed(() => {
return '请输入地点进行搜索'
})
-// 音频相关处理
-const handleAudioClick = async (exhibit: Exhibit) => {
- if (exhibit.audioUrl) {
- currentAudio.value = {
- id: `media-${exhibit.id}`,
- name: exhibit.title,
- audioUrl: exhibit.audioUrl,
- image: exhibit.coverImage,
- duration: exhibit.audioDuration
- }
- showAudioPlayer.value = true
- return
- }
-
- const selection = await explainUseCase.selectAudioForExhibit(exhibit.id)
-
- if (!selection?.playable || !selection.media?.url) {
- uni.showToast({
- title: selection?.unavailableMessage || exhibit.audioStatusText || '该讲解暂无可播放音频',
- icon: 'none'
- })
- return
- }
-
- currentAudio.value = {
- id: selection.media.id,
- name: selection.track?.title || selection.exhibit.name,
- audioUrl: selection.media.url,
- image: selection.exhibit.image,
- duration: selection.media.duration
- }
- showAudioPlayer.value = true
-}
-
-const handleFloatingButtonClick = () => {
- if (showAudioPlayer.value) {
- showAudioPlayer.value = false
- } else if (currentAudio.value) {
- showAudioPlayer.value = true
- } else {
- uni.showToast({
- title: '请先选择一个讲解内容',
- icon: 'none'
- })
- }
-}
-
-const clearAudioState = () => {
- showAudioPlayer.value = false
- isAudioPlaying.value = false
- currentAudio.value = null
-}
-
-const handleAudioVisibleChange = (visible: boolean) => {
- showAudioPlayer.value = visible
- if (!visible) {
- isAudioPlaying.value = false
- currentAudio.value = null
- }
-}
-
-const handleAudioPlay = (audio: AudioItem) => {
- console.log('开始播放:', audio)
- isAudioPlaying.value = true
-}
-
-const handleAudioPause = (audio: AudioItem) => {
- console.log('暂停播放:', audio)
- isAudioPlaying.value = false
-}
-
-const handleAudioEnded = (audio: AudioItem) => {
- console.log('播放结束:', audio)
- clearAudioState()
-}
-
-const handleAudioError = (_audio: AudioItem | null, message: string) => {
- console.warn('讲解音频不可播放:', message)
- clearAudioState()
-}
-
// 讲解页面处理
-const handleExplainExhibitClick = (exhibit: Exhibit) => {
- uni.navigateTo({
- url: `/pages/exhibit/detail?id=${exhibit.id}&tab=explain`
- })
-}
-
-const handleExplainFeaturedClick = async (exhibit: Exhibit) => {
- await handleAudioClick(exhibit)
-}
-
const handleExplainHallClick = (hallId: string) => {
uni.navigateTo({
url: `/pages/hall/detail?id=${encodeURIComponent(hallId)}&tab=explain`
})
}
-const handleExplainBrowseAll = () => {
- uni.navigateTo({
- url: '/pages/explain/list'
- })
-}
-
-const handleExplainLocationClick = async (exhibit: Exhibit) => {
- if (!exhibit.poiId) {
- uni.showToast({
- title: '该讲解暂无三维位置数据',
- icon: 'none'
- })
- return
- }
-
- const matchedPoi = await guideUseCase.getPoiById(exhibit.poiId)
- if (!matchedPoi) {
- uni.showToast({
- title: '该讲解位置尚未映射到当前三维导览资源',
- icon: 'none'
- })
- return
- }
-
- uni.navigateTo({
- url: `/pages/route/detail?facilityId=${encodeURIComponent(exhibit.poiId)}&target=${encodeURIComponent(exhibit.title)}&state=preview`
- })
+const handleExplainBack = () => {
+ currentTab.value = 'guide'
+ syncTopTabToUrl('guide')
+ loadTopTabData('guide')
+ showGuideMoreMenu.value = false
+ showRoutePlanner.value = false
+ isSimulatingRoute.value = false
+ isPoiCardCollapsed.value = false
}
@@ -1187,7 +1114,7 @@ const handleExplainLocationClick = async (exhibit: Exhibit) => {
.route-sim-top-pill {
position: absolute;
- top: 154px;
+ top: 126px;
left: 50%;
max-width: calc(100% - 40px);
height: 34px;
@@ -1263,7 +1190,7 @@ const handleExplainLocationClick = async (exhibit: Exhibit) => {
.entrance-tip {
position: absolute;
- top: 154px;
+ top: 126px;
left: 30px;
width: 168px;
height: 42px;
diff --git a/src/pages/route/detail.vue b/src/pages/route/detail.vue
index a3ae922..5d8e512 100644
--- a/src/pages/route/detail.vue
+++ b/src/pages/route/detail.vue
@@ -94,15 +94,6 @@
-
-
室外地图预览:{{ route.target }}
@@ -126,7 +117,6 @@ import { computed, onMounted, onUnmounted, ref } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import GuidePageFrame from '@/components/navigation/GuidePageFrame.vue'
import GuideMapShell from '@/components/navigation/GuideMapShell.vue'
-import LocationPreview from '@/components/navigation/LocationPreview.vue'
import {
guideUseCase
} from '@/usecases/guideUseCase'
@@ -226,6 +216,7 @@ const shellConfig = computed(() => {
tools: [],
showSearch: true,
showFloor: false,
+ showModeRow: false,
modeTop: '104px',
modeLayout: 'full' as const,
mapType: 'indoor' as const
@@ -243,6 +234,7 @@ const shellConfig = computed(() => {
tools: [],
showSearch: true,
showFloor: false,
+ showModeRow: false,
modeTop: '104px',
modeLayout: 'full' as const,
mapType: 'indoor' as const,
@@ -261,6 +253,7 @@ const shellConfig = computed(() => {
tools: [],
showSearch: true,
showFloor: false,
+ showModeRow: false,
modeTop: '104px',
modeLayout: 'status' as const,
modeStatus: '室外参考',
@@ -279,9 +272,12 @@ const shellConfig = computed(() => {
tools: ['回正'],
showSearch: true,
showFloor: true,
+ showModeRow: false,
modeTop: '104px',
modeLayout: 'full' as const,
- mapType: 'indoor' as const
+ mapType: 'indoor' as const,
+ touchGestureMode: 'pan' as const,
+ targetFocusDistanceFactor: 0.86
}
})
@@ -360,7 +356,8 @@ const requestTargetFocus = (target: Partial | null, fallbac
floorId: target.floorId,
floorLabel,
primaryCategoryZh: target.primaryCategoryZh,
- positionGltf: target.positionGltf
+ positionGltf: target.positionGltf,
+ sourceObjectName: target.sourceObjectName
}
activeFloor.value = floorLabel
@@ -477,29 +474,6 @@ const handleSearchTap = () => {
})
}
-const handleViewOutdoorMap = () => {
- isManualLocationPanelOpen.value = false
- routeViewState.value = 'outdoor-preview'
- saveCurrentLocationPreviewUrl()
-}
-
-const handleShowTargetLocation = (target: Partial | null) => {
- const focused = requestTargetFocus(target || route.value.targetLocation)
-
- if (!focused) {
- uni.showToast({
- title: '目标暂无三维位置数据',
- icon: 'none'
- })
- return
- }
-
- uni.showToast({
- title: '已显示三维位置',
- icon: 'none'
- })
-}
-
const handleTargetFocus = (result: TargetPoiFocusResult) => {
if (result.status === 'focused') return
diff --git a/src/repositories/GuideModelRepository.ts b/src/repositories/GuideModelRepository.ts
index fa93bc2..60fc068 100644
--- a/src/repositories/GuideModelRepository.ts
+++ b/src/repositories/GuideModelRepository.ts
@@ -20,7 +20,8 @@ const toRenderPoi = (poi: StaticNavPoiPayload): GuideRenderPoi => ({
primaryCategory: poi.primaryCategory,
primaryCategoryZh: poi.primaryCategoryZh,
iconType: poi.iconType || poi.primaryCategory,
- positionGltf: poi.positionGltf
+ positionGltf: poi.positionGltf,
+ sourceObjectName: poi.sourceObjectName
})
export interface GuideModelRepository extends GuideModelSource {}
diff --git a/src/repositories/GuideRepository.ts b/src/repositories/GuideRepository.ts
index 5347048..654956a 100644
--- a/src/repositories/GuideRepository.ts
+++ b/src/repositories/GuideRepository.ts
@@ -47,7 +47,8 @@ const toLocationPreview = (poi: MuseumPoi): GuideLocationPreview => ({
floorId: poi.floorId,
floorLabel: poi.floorLabel,
primaryCategoryZh: poi.primaryCategory.label,
- positionGltf: poi.positionGltf
+ positionGltf: poi.positionGltf,
+ sourceObjectName: poi.sourceObjectName
})
export interface GuideRepository {
diff --git a/static/nav-assets/codex_nav_20260621_175310_museum_default_centerline_finalxy/app_nav_manifest.json b/static/nav-assets/codex_nav_20260621_175310_museum_default_centerline_finalxy/app_nav_manifest.json
index 04934f0..917ae05 100644
--- a/static/nav-assets/codex_nav_20260621_175310_museum_default_centerline_finalxy/app_nav_manifest.json
+++ b/static/nav-assets/codex_nav_20260621_175310_museum_default_centerline_finalxy/app_nav_manifest.json
@@ -45,57 +45,57 @@
},
"assets": {
"overviewModel": {
- "asset": "web_model/current_session.glb",
- "bytes": 32345596,
+ "asset": "web_model/current_session.display.glb",
+ "bytes": 14924720,
"sharedModelAsset": true
},
"floorModels": [
{
"floorId": "L-2",
"order": 0,
- "asset": "web_model/current_session.glb",
+ "asset": "web_model/current_session.display.glb",
"sharedModelAsset": true
},
{
"floorId": "L-1",
"order": 1,
- "asset": "web_model/current_session.glb",
+ "asset": "web_model/current_session.display.glb",
"sharedModelAsset": true
},
{
"floorId": "L1",
"order": 2,
- "asset": "web_model/current_session.glb",
+ "asset": "web_model/current_session.display.glb",
"sharedModelAsset": true
},
{
"floorId": "L1.5",
"order": 3,
- "asset": "web_model/current_session.glb",
+ "asset": "web_model/current_session.display.glb",
"sharedModelAsset": true
},
{
"floorId": "L2",
"order": 4,
- "asset": "web_model/current_session.glb",
+ "asset": "web_model/current_session.display.glb",
"sharedModelAsset": true
},
{
"floorId": "L3",
"order": 5,
- "asset": "web_model/current_session.glb",
+ "asset": "web_model/current_session.display.glb",
"sharedModelAsset": true
},
{
"floorId": "L4",
"order": 6,
- "asset": "web_model/current_session.glb",
+ "asset": "web_model/current_session.display.glb",
"sharedModelAsset": true
},
{
"floorId": "L5",
"order": 7,
- "asset": "web_model/current_session.glb",
+ "asset": "web_model/current_session.display.glb",
"sharedModelAsset": true
}
]
@@ -186,7 +186,7 @@
"notesZh": [
"该包由 2026-06-21 finalxy 中心线路网 run 生成,nav_data.json 与 route_graph.json 为权威数据源。",
"前端兼容 JSON 由 nav_data.buildingPois/navAnchors 派生,页面仍通过 Provider / Adapter / Repository 读取 domain models。",
- "current_session.glb 是本包唯一三维模型;楼层切换复用同一模型并按楼层加载 POI。",
+ "current_session.display.glb 是用户端展示模型,已移除中心线路网辅助网格;原始 current_session.glb 保留给 nav_data.json 与 route_graph.json 的数据生成/审计链路使用。",
"POI positionGltf 已按 [x, y, z] -> [x, z, -y] 转换到 Three.js/GLB 坐标。",
"connector_machine_verification 已记录 finalVerticalEndpointXYGate,通过的 102 个垂直连接候选才进入最终图。"
]
diff --git a/static/nav-assets/codex_nav_20260621_175310_museum_default_centerline_finalxy/data/floor_index.json b/static/nav-assets/codex_nav_20260621_175310_museum_default_centerline_finalxy/data/floor_index.json
index 164956a..1ca8046 100644
--- a/static/nav-assets/codex_nav_20260621_175310_museum_default_centerline_finalxy/data/floor_index.json
+++ b/static/nav-assets/codex_nav_20260621_175310_museum_default_centerline_finalxy/data/floor_index.json
@@ -11,7 +11,7 @@
"name": "L-2",
"label": "B2",
"zRepresentative": -14.244266,
- "modelAsset": "web_model/current_session.glb",
+ "modelAsset": "web_model/current_session.display.glb",
"sharedModelAsset": true,
"poiDataAsset": "data/poi_by_floor/L-2.json",
"poiCount": 59
@@ -22,7 +22,7 @@
"name": "L-1",
"label": "B1",
"zRepresentative": -10.673778,
- "modelAsset": "web_model/current_session.glb",
+ "modelAsset": "web_model/current_session.display.glb",
"sharedModelAsset": true,
"poiDataAsset": "data/poi_by_floor/L-1.json",
"poiCount": 47
@@ -33,7 +33,7 @@
"name": "L1",
"label": "1F",
"zRepresentative": 0.170108,
- "modelAsset": "web_model/current_session.glb",
+ "modelAsset": "web_model/current_session.display.glb",
"sharedModelAsset": true,
"poiDataAsset": "data/poi_by_floor/L1.json",
"poiCount": 74
@@ -44,7 +44,7 @@
"name": "L1.5",
"label": "1.5F",
"zRepresentative": 5.1561,
- "modelAsset": "web_model/current_session.glb",
+ "modelAsset": "web_model/current_session.display.glb",
"sharedModelAsset": true,
"poiDataAsset": "data/poi_by_floor/L1.5.json",
"poiCount": 21
@@ -55,7 +55,7 @@
"name": "L2",
"label": "2F",
"zRepresentative": 14.75636,
- "modelAsset": "web_model/current_session.glb",
+ "modelAsset": "web_model/current_session.display.glb",
"sharedModelAsset": true,
"poiDataAsset": "data/poi_by_floor/L2.json",
"poiCount": 46
@@ -66,7 +66,7 @@
"name": "L3",
"label": "3F",
"zRepresentative": 16.577412,
- "modelAsset": "web_model/current_session.glb",
+ "modelAsset": "web_model/current_session.display.glb",
"sharedModelAsset": true,
"poiDataAsset": "data/poi_by_floor/L3.json",
"poiCount": 15
@@ -77,7 +77,7 @@
"name": "L4",
"label": "4F",
"zRepresentative": 22.86525,
- "modelAsset": "web_model/current_session.glb",
+ "modelAsset": "web_model/current_session.display.glb",
"sharedModelAsset": true,
"poiDataAsset": "data/poi_by_floor/L4.json",
"poiCount": 25
@@ -88,7 +88,7 @@
"name": "L5",
"label": "5F",
"zRepresentative": 27.10755,
- "modelAsset": "web_model/current_session.glb",
+ "modelAsset": "web_model/current_session.display.glb",
"sharedModelAsset": true,
"poiDataAsset": "data/poi_by_floor/L5.json",
"poiCount": 11
diff --git a/static/nav-assets/codex_nav_20260621_175310_museum_default_centerline_finalxy/web_model/current_session.display.glb b/static/nav-assets/codex_nav_20260621_175310_museum_default_centerline_finalxy/web_model/current_session.display.glb
new file mode 100644
index 0000000..d46440f
Binary files /dev/null and b/static/nav-assets/codex_nav_20260621_175310_museum_default_centerline_finalxy/web_model/current_session.display.glb differ