|
|
|
|
@@ -227,6 +227,7 @@ interface CameraTweenState {
|
|
|
|
|
toTarget: THREE.Vector3
|
|
|
|
|
startedAt: number
|
|
|
|
|
durationMs: number
|
|
|
|
|
reason?: 'poi-focus'
|
|
|
|
|
onComplete?: () => void
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@@ -344,26 +345,11 @@ interface PoiMarkerCacheEntry {
|
|
|
|
|
pois: RenderPoi[]
|
|
|
|
|
group: THREE.Group
|
|
|
|
|
markerSize: number
|
|
|
|
|
displayOffsetY: number
|
|
|
|
|
rawPoiCount?: number
|
|
|
|
|
rawPositionedPoiCount?: number
|
|
|
|
|
filteredCategoryCounts?: Record<string, number>
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
interface PoiYDistribution {
|
|
|
|
|
count: number
|
|
|
|
|
min: number
|
|
|
|
|
max: number
|
|
|
|
|
median: number
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
interface ModelYBounds {
|
|
|
|
|
min: number
|
|
|
|
|
max: number
|
|
|
|
|
center: number
|
|
|
|
|
height: number
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
interface PreparedFloorScene {
|
|
|
|
|
floor: FloorIndexItem
|
|
|
|
|
model: THREE.Object3D
|
|
|
|
|
@@ -481,8 +467,7 @@ let activeRoutePreviewSignature = ''
|
|
|
|
|
const poiDataCache = new Map<string, RenderPoi[]>()
|
|
|
|
|
const poiMarkerGroupCache = new Map<string, PoiMarkerCacheEntry>()
|
|
|
|
|
const preparedFloorModelCache = new Map<string, PreparedFloorModelCacheEntry>()
|
|
|
|
|
const poiDisplayOffsetYCache = new Map<string, number>()
|
|
|
|
|
const poiDisplayDiagnosticsKeys = new Set<string>()
|
|
|
|
|
const poiCoordinateDiagnosticsKeys = new Set<string>()
|
|
|
|
|
let activeFocusLabelSprite: THREE.Sprite | null = null
|
|
|
|
|
let activeFocusPulseSprite: THREE.Sprite | null = null
|
|
|
|
|
let activeFocusBaseSprite: THREE.Sprite | null = null
|
|
|
|
|
@@ -523,6 +508,8 @@ let activeAutoSwitchInputSource: GuideAutoSwitchInputSource = 'gesture'
|
|
|
|
|
let isProgrammaticCameraChange = false
|
|
|
|
|
let programmaticCameraTimer: ReturnType<typeof setTimeout> | null = null
|
|
|
|
|
let cameraTween: CameraTweenState | null = null
|
|
|
|
|
let poiFocusCameraAnimationCount = 0
|
|
|
|
|
let cameraSnapshotRestoreCount = 0
|
|
|
|
|
let adjacentPreloadSeq = 0
|
|
|
|
|
let defaultFloorPreloadSeq = 0
|
|
|
|
|
let firstModelLoadStartedAt = 0
|
|
|
|
|
@@ -545,11 +532,6 @@ const adjacentPreloadDelayMs = 900
|
|
|
|
|
const manualAutoSwitchPauseMs = 1500
|
|
|
|
|
const autoSwitchExitDistanceRatio = 0.2
|
|
|
|
|
const autoSwitchExitDistanceMarginRatio = 0.08
|
|
|
|
|
const poiMarkerVisualLiftFactor = 0.92
|
|
|
|
|
const poiAmbientLabelLiftFactor = 2.1
|
|
|
|
|
const poiFocusLabelLiftFactor = 2.72
|
|
|
|
|
const poiFocusBaseLiftFactor = 0.18
|
|
|
|
|
const poiFocusPulseLiftFactor = 0.34
|
|
|
|
|
|
|
|
|
|
type ThreeMapDiagnosticEvent =
|
|
|
|
|
| 'init-start'
|
|
|
|
|
@@ -570,6 +552,7 @@ type ThreeMapDiagnosticEvent =
|
|
|
|
|
| 'poi-background-start'
|
|
|
|
|
| 'poi-background-ready'
|
|
|
|
|
| 'poi-filter-diagnostics'
|
|
|
|
|
| 'poi-coordinate-diagnostics'
|
|
|
|
|
| 'poi-marker-render-diagnostics'
|
|
|
|
|
| 'poi-hit-diagnostics'
|
|
|
|
|
| 'adjacent-preload-skip'
|
|
|
|
|
@@ -1127,131 +1110,69 @@ const countPoiMarkerObjects = (group: THREE.Group) => {
|
|
|
|
|
return counts
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const summarizePoiYDistribution = (pois: RenderPoi[]): PoiYDistribution | null => {
|
|
|
|
|
const values = pois
|
|
|
|
|
.map((poi) => poi.positionGltf?.[1])
|
|
|
|
|
.filter((value): value is number => Number.isFinite(value))
|
|
|
|
|
.sort((left, right) => left - right)
|
|
|
|
|
|
|
|
|
|
if (!values.length) return null
|
|
|
|
|
|
|
|
|
|
const middle = Math.floor(values.length / 2)
|
|
|
|
|
const median = values.length % 2
|
|
|
|
|
? values[middle]
|
|
|
|
|
: (values[middle - 1] + values[middle]) / 2
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
count: values.length,
|
|
|
|
|
min: values[0],
|
|
|
|
|
max: values[values.length - 1],
|
|
|
|
|
median
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const getModelYBounds = (model: THREE.Object3D): ModelYBounds => {
|
|
|
|
|
const box = getObjectBox(model)
|
|
|
|
|
const height = Math.max(box.max.y - box.min.y, 0)
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
min: box.min.y,
|
|
|
|
|
max: box.max.y,
|
|
|
|
|
center: (box.min.y + box.max.y) / 2,
|
|
|
|
|
height
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const roundDiagnosticNumber = (value: number) => Math.round(value * 1000) / 1000
|
|
|
|
|
|
|
|
|
|
const getPoiDisplayOffsetCacheKey = (floorId: string) => floorId
|
|
|
|
|
|
|
|
|
|
const getCachedPoiDisplayOffsetY = (floorId: string) => (
|
|
|
|
|
poiDisplayOffsetYCache.get(getPoiDisplayOffsetCacheKey(floorId)) || 0
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
const getPoiDisplayPosition = (poi: RenderPoi, floorId = poi.floorId) => {
|
|
|
|
|
const getPoiDisplayPosition = (poi: RenderPoi) => {
|
|
|
|
|
if (!poi.positionGltf) return null
|
|
|
|
|
|
|
|
|
|
const [x, y, z] = poi.positionGltf
|
|
|
|
|
return new THREE.Vector3(x, y + getCachedPoiDisplayOffsetY(floorId), z)
|
|
|
|
|
return new THREE.Vector3(x, y, z)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const getPoiLiftedPosition = (
|
|
|
|
|
displayPosition: THREE.Vector3,
|
|
|
|
|
markerSize: number,
|
|
|
|
|
liftFactor: number
|
|
|
|
|
) => new THREE.Vector3(
|
|
|
|
|
displayPosition.x,
|
|
|
|
|
displayPosition.y + markerSize * liftFactor,
|
|
|
|
|
displayPosition.z
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
const getPoiMarkerPosition = (displayPosition: THREE.Vector3, markerSize: number) => (
|
|
|
|
|
getPoiLiftedPosition(displayPosition, markerSize, poiMarkerVisualLiftFactor)
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
const getPoiAmbientLabelPosition = (displayPosition: THREE.Vector3, markerSize: number) => (
|
|
|
|
|
getPoiLiftedPosition(displayPosition, markerSize, poiAmbientLabelLiftFactor)
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
const resolvePoiDisplayOffsetY = (
|
|
|
|
|
const recordPoiCoordinateDiagnostics = (
|
|
|
|
|
floorId: string,
|
|
|
|
|
model: THREE.Object3D,
|
|
|
|
|
pois: RenderPoi[]
|
|
|
|
|
) => {
|
|
|
|
|
const modelY = getModelYBounds(model)
|
|
|
|
|
const poiY = summarizePoiYDistribution(pois)
|
|
|
|
|
let offsetY = 0
|
|
|
|
|
let reason = 'no-positioned-poi'
|
|
|
|
|
if (!import.meta.env.DEV) return
|
|
|
|
|
|
|
|
|
|
if (poiY) {
|
|
|
|
|
const modelTolerance = Math.max(2, modelY.height * 0.35)
|
|
|
|
|
const overlapsModelRange = poiY.max >= modelY.min - modelTolerance && poiY.min <= modelY.max + modelTolerance
|
|
|
|
|
const box = getObjectBox(model)
|
|
|
|
|
const positionedPois = pois.filter((poi): poi is RenderPoi & { positionGltf: [number, number, number] } => (
|
|
|
|
|
Boolean(poi.positionGltf)
|
|
|
|
|
))
|
|
|
|
|
const coordinateErrors = positionedPois.map((poi) => {
|
|
|
|
|
const [x, y, z] = poi.positionGltf
|
|
|
|
|
const error = Math.hypot(
|
|
|
|
|
Math.max(box.min.x - x, 0, x - box.max.x),
|
|
|
|
|
Math.max(box.min.y - y, 0, y - box.max.y),
|
|
|
|
|
Math.max(box.min.z - z, 0, z - box.max.z)
|
|
|
|
|
)
|
|
|
|
|
return { poi, error }
|
|
|
|
|
})
|
|
|
|
|
const maxCoordinateError = Math.max(0, ...coordinateErrors.map(({ error }) => error))
|
|
|
|
|
const diagnosticsKey = [
|
|
|
|
|
floorId,
|
|
|
|
|
positionedPois.length,
|
|
|
|
|
roundDiagnosticNumber(maxCoordinateError),
|
|
|
|
|
roundDiagnosticNumber(box.min.x),
|
|
|
|
|
roundDiagnosticNumber(box.min.y),
|
|
|
|
|
roundDiagnosticNumber(box.min.z),
|
|
|
|
|
roundDiagnosticNumber(box.max.x),
|
|
|
|
|
roundDiagnosticNumber(box.max.y),
|
|
|
|
|
roundDiagnosticNumber(box.max.z)
|
|
|
|
|
].join(':')
|
|
|
|
|
|
|
|
|
|
if (overlapsModelRange) {
|
|
|
|
|
reason = 'poi-y-overlaps-model-y'
|
|
|
|
|
} else {
|
|
|
|
|
offsetY = modelY.center - poiY.median
|
|
|
|
|
reason = 'align-poi-median-to-model-center'
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (poiCoordinateDiagnosticsKeys.has(diagnosticsKey)) return
|
|
|
|
|
|
|
|
|
|
poiDisplayOffsetYCache.set(getPoiDisplayOffsetCacheKey(floorId), offsetY)
|
|
|
|
|
|
|
|
|
|
if (import.meta.env.DEV) {
|
|
|
|
|
const diagnosticsKey = [
|
|
|
|
|
floorId,
|
|
|
|
|
roundDiagnosticNumber(modelY.min),
|
|
|
|
|
roundDiagnosticNumber(modelY.max),
|
|
|
|
|
poiY ? roundDiagnosticNumber(poiY.min) : 'none',
|
|
|
|
|
poiY ? roundDiagnosticNumber(poiY.max) : 'none',
|
|
|
|
|
roundDiagnosticNumber(offsetY)
|
|
|
|
|
].join(':')
|
|
|
|
|
|
|
|
|
|
if (!poiDisplayDiagnosticsKeys.has(diagnosticsKey)) {
|
|
|
|
|
poiDisplayDiagnosticsKeys.add(diagnosticsKey)
|
|
|
|
|
console.info('[ThreeMap] floor POI display coordinate diagnostics', {
|
|
|
|
|
floorId,
|
|
|
|
|
modelY: {
|
|
|
|
|
min: roundDiagnosticNumber(modelY.min),
|
|
|
|
|
max: roundDiagnosticNumber(modelY.max),
|
|
|
|
|
center: roundDiagnosticNumber(modelY.center),
|
|
|
|
|
height: roundDiagnosticNumber(modelY.height)
|
|
|
|
|
},
|
|
|
|
|
poiY: poiY
|
|
|
|
|
? {
|
|
|
|
|
count: poiY.count,
|
|
|
|
|
min: roundDiagnosticNumber(poiY.min),
|
|
|
|
|
max: roundDiagnosticNumber(poiY.max),
|
|
|
|
|
median: roundDiagnosticNumber(poiY.median)
|
|
|
|
|
}
|
|
|
|
|
: null,
|
|
|
|
|
displayOffsetY: roundDiagnosticNumber(offsetY),
|
|
|
|
|
reason
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return offsetY
|
|
|
|
|
poiCoordinateDiagnosticsKeys.add(diagnosticsKey)
|
|
|
|
|
logThreeMapDiagnostic('poi-coordinate-diagnostics', {
|
|
|
|
|
floorId,
|
|
|
|
|
coordinateSpace: 'GLB_METER',
|
|
|
|
|
modelBounds: {
|
|
|
|
|
min: serializeVector3(box.min),
|
|
|
|
|
max: serializeVector3(box.max)
|
|
|
|
|
},
|
|
|
|
|
positionedPoiCount: positionedPois.length,
|
|
|
|
|
maxCoordinateError: roundDiagnosticNumber(maxCoordinateError),
|
|
|
|
|
mismatchedPois: coordinateErrors
|
|
|
|
|
.filter(({ error }) => error > 1e-6)
|
|
|
|
|
.slice(0, 12)
|
|
|
|
|
.map(({ poi, error }) => ({
|
|
|
|
|
poiId: poi.id,
|
|
|
|
|
positionGltf: poi.positionGltf,
|
|
|
|
|
coordinateError: roundDiagnosticNumber(error)
|
|
|
|
|
}))
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const getPoiGlyph = (poi: RenderPoi) => (
|
|
|
|
|
@@ -1449,7 +1370,6 @@ const updateAmbientPoiLabels = () => {
|
|
|
|
|
|
|
|
|
|
const densityTier = getPoiLabelDensityTier()
|
|
|
|
|
const acceptedBounds: Array<ReturnType<typeof getAmbientLabelBounds>> = []
|
|
|
|
|
const markerSize = getPoiMarkerSize()
|
|
|
|
|
const candidates = getPoiSprites()
|
|
|
|
|
.map((sprite) => {
|
|
|
|
|
const poi = sprite.userData.poi as RenderPoi | undefined
|
|
|
|
|
@@ -1481,44 +1401,19 @@ const updateAmbientPoiLabels = () => {
|
|
|
|
|
|
|
|
|
|
updateAmbientLabelScale(labelSprite, poi)
|
|
|
|
|
|
|
|
|
|
const baseOffset = markerSize * (isHallPoi(poi) ? poiAmbientLabelLiftFactor : 1.82)
|
|
|
|
|
const offsetAttempts = [
|
|
|
|
|
0,
|
|
|
|
|
markerSize * 0.82,
|
|
|
|
|
-markerSize * 0.62,
|
|
|
|
|
markerSize * 1.48,
|
|
|
|
|
-markerSize * 1.08
|
|
|
|
|
]
|
|
|
|
|
const spacing = isHallPoi(poi) ? poiAmbientLabelHallSpacingPx : poiAmbientLabelServiceSpacingPx
|
|
|
|
|
let accepted = false
|
|
|
|
|
|
|
|
|
|
for (const offset of offsetAttempts) {
|
|
|
|
|
labelSprite.position.set(displayPosition.x, displayPosition.y + baseOffset + offset, displayPosition.z)
|
|
|
|
|
const screenPosition = getProjectedScreenPosition(labelSprite)
|
|
|
|
|
if (!screenPosition) continue
|
|
|
|
|
|
|
|
|
|
const bounds = getAmbientLabelBounds(screenPosition, poi)
|
|
|
|
|
const overlaps = acceptedBounds.some((acceptedBound) => labelBoundsOverlap(bounds, acceptedBound, spacing))
|
|
|
|
|
if (overlaps) continue
|
|
|
|
|
|
|
|
|
|
labelSprite.visible = true
|
|
|
|
|
acceptedBounds.push(bounds)
|
|
|
|
|
accepted = true
|
|
|
|
|
break
|
|
|
|
|
labelSprite.position.copy(displayPosition)
|
|
|
|
|
const screenPosition = getProjectedScreenPosition(labelSprite)
|
|
|
|
|
if (!screenPosition) {
|
|
|
|
|
labelSprite.visible = false
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!accepted) {
|
|
|
|
|
if (isHallPoi(poi)) {
|
|
|
|
|
const fallbackOffset = offsetAttempts[offsetAttempts.length - 1]
|
|
|
|
|
labelSprite.position.set(
|
|
|
|
|
displayPosition.x,
|
|
|
|
|
displayPosition.y + baseOffset + fallbackOffset,
|
|
|
|
|
displayPosition.z
|
|
|
|
|
)
|
|
|
|
|
labelSprite.visible = true
|
|
|
|
|
} else {
|
|
|
|
|
labelSprite.visible = false
|
|
|
|
|
}
|
|
|
|
|
const bounds = getAmbientLabelBounds(screenPosition, poi)
|
|
|
|
|
const overlaps = acceptedBounds.some((acceptedBound) => labelBoundsOverlap(bounds, acceptedBound, spacing))
|
|
|
|
|
labelSprite.visible = !overlaps
|
|
|
|
|
if (!overlaps) {
|
|
|
|
|
acceptedBounds.push(bounds)
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
@@ -1662,12 +1557,20 @@ const cancelCameraTween = (options: { manual?: boolean } = {}) => {
|
|
|
|
|
const moveCameraTo = (
|
|
|
|
|
position: THREE.Vector3,
|
|
|
|
|
target: THREE.Vector3,
|
|
|
|
|
options: { durationMs?: number; immediate?: boolean; onComplete?: () => void } = {}
|
|
|
|
|
options: {
|
|
|
|
|
durationMs?: number
|
|
|
|
|
immediate?: boolean
|
|
|
|
|
reason?: 'poi-focus'
|
|
|
|
|
onComplete?: () => void
|
|
|
|
|
} = {}
|
|
|
|
|
) => {
|
|
|
|
|
if (!camera || !controls) return
|
|
|
|
|
|
|
|
|
|
const toPosition = position.clone()
|
|
|
|
|
const toTarget = target.clone()
|
|
|
|
|
if (options.reason === 'poi-focus') {
|
|
|
|
|
poiFocusCameraAnimationCount += 1
|
|
|
|
|
}
|
|
|
|
|
isProgrammaticCameraChange = true
|
|
|
|
|
clearProgrammaticCameraTimer()
|
|
|
|
|
|
|
|
|
|
@@ -1688,6 +1591,7 @@ const moveCameraTo = (
|
|
|
|
|
toTarget,
|
|
|
|
|
startedAt: performance.now(),
|
|
|
|
|
durationMs: options.durationMs ?? cameraTweenDurationMs,
|
|
|
|
|
reason: options.reason,
|
|
|
|
|
onComplete: options.onComplete
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
@@ -2358,8 +2262,7 @@ const disposePoiMarkerCache = () => {
|
|
|
|
|
})
|
|
|
|
|
poiMarkerGroupCache.clear()
|
|
|
|
|
poiDataCache.clear()
|
|
|
|
|
poiDisplayOffsetYCache.clear()
|
|
|
|
|
poiDisplayDiagnosticsKeys.clear()
|
|
|
|
|
poiCoordinateDiagnosticsKeys.clear()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const clearPoiGroupChildren = () => {
|
|
|
|
|
@@ -2572,22 +2475,8 @@ const clearRoutePreview = () => {
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const getRouteFloorOffsetY = (floorId: string) => {
|
|
|
|
|
if (activeView.value !== 'multi' || !activeModel) return 0
|
|
|
|
|
|
|
|
|
|
let offsetY = 0
|
|
|
|
|
activeModel.traverse((child) => {
|
|
|
|
|
if (offsetY) return
|
|
|
|
|
if (child.userData.floorId === floorId && typeof child.userData.multiFloorOffsetY === 'number') {
|
|
|
|
|
offsetY = child.userData.multiFloorOffsetY
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
return offsetY
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const routePointToVector = (position: [number, number, number], floorId: string, lift = 0.45) => (
|
|
|
|
|
new THREE.Vector3(position[0], position[1] + getRouteFloorOffsetY(floorId) + lift, position[2])
|
|
|
|
|
const routePointToVector = (position: [number, number, number]) => (
|
|
|
|
|
new THREE.Vector3(position[0], position[1], position[2])
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
const getVisibleRouteSegments = (route: GuideRouteResult): GuideRouteFloorSegment[] => {
|
|
|
|
|
@@ -2677,7 +2566,7 @@ const renderRouteEndpoint = (
|
|
|
|
|
if (!routeGroup || !shouldShowRouteEndpoint(endpoint)) return
|
|
|
|
|
|
|
|
|
|
const marker = createRouteMarkerSprite(label, color, markerSize)
|
|
|
|
|
marker.position.copy(routePointToVector(endpoint.position, endpoint.floorId, markerSize * 0.45))
|
|
|
|
|
marker.position.copy(routePointToVector(endpoint.position))
|
|
|
|
|
routeGroup.add(marker)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@@ -2688,7 +2577,7 @@ const renderRouteConnectorMarkers = (route: GuideRouteResult, markerSize: number
|
|
|
|
|
.filter((point) => activeView.value !== 'floor' || point.floorId === currentFloor.value)
|
|
|
|
|
.forEach((point) => {
|
|
|
|
|
const marker = createRouteMarkerSprite('换', '#8a5cf6', markerSize * 0.72)
|
|
|
|
|
marker.position.copy(routePointToVector(point.position, point.floorId, markerSize * 0.25))
|
|
|
|
|
marker.position.copy(routePointToVector(point.position))
|
|
|
|
|
routeGroup!.add(marker)
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
@@ -2746,7 +2635,7 @@ const renderRoutePreview = () => {
|
|
|
|
|
if (segment.points.length < 2) return
|
|
|
|
|
|
|
|
|
|
targetRouteGroup.add(createRouteLine(
|
|
|
|
|
segment.points.map((point) => routePointToVector(point.position, point.floorId)),
|
|
|
|
|
segment.points.map((point) => routePointToVector(point.position)),
|
|
|
|
|
activeView.value === 'floor' ? 1 : 0.88
|
|
|
|
|
))
|
|
|
|
|
})
|
|
|
|
|
@@ -3477,6 +3366,7 @@ const captureCameraSnapshot = (): CameraSnapshot => {
|
|
|
|
|
const restoreCameraSnapshot = (snapshot: CameraSnapshot) => {
|
|
|
|
|
if (!camera || !controls) return
|
|
|
|
|
|
|
|
|
|
cameraSnapshotRestoreCount += 1
|
|
|
|
|
cancelCameraTween()
|
|
|
|
|
clearProgrammaticCameraTimer()
|
|
|
|
|
isProgrammaticCameraChange = false
|
|
|
|
|
@@ -3609,13 +3499,30 @@ const getProjectedBuildingAnchors = () => {
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const getVisualFocusReport = () => {
|
|
|
|
|
if (!selectedPOI.value?.positionGltf) return null
|
|
|
|
|
|
|
|
|
|
const marker = findPoiSprite(selectedPOI.value.id)
|
|
|
|
|
return {
|
|
|
|
|
poiId: selectedPOI.value.id,
|
|
|
|
|
floorId: selectedPOI.value.floorId,
|
|
|
|
|
positionGltf: [...selectedPOI.value.positionGltf] as [number, number, number],
|
|
|
|
|
markerPosition: marker
|
|
|
|
|
? { x: marker.position.x, y: marker.position.y, z: marker.position.z }
|
|
|
|
|
: null
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const getVisualStabilityReport = () => ({
|
|
|
|
|
camera: serializeCameraSnapshot(captureCameraSnapshot()),
|
|
|
|
|
modelRoot: getModelRootTransformAudit(activeModel),
|
|
|
|
|
anchors: getProjectedBuildingAnchors(),
|
|
|
|
|
activeView: activeView.value,
|
|
|
|
|
floorId: currentFloor.value,
|
|
|
|
|
isCameraTweening: Boolean(cameraTween)
|
|
|
|
|
isCameraTweening: Boolean(cameraTween),
|
|
|
|
|
poiFocusCameraAnimationCount,
|
|
|
|
|
cameraSnapshotRestoreCount,
|
|
|
|
|
focus: getVisualFocusReport()
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
const installVisualStabilityDiagnostics = () => {
|
|
|
|
|
@@ -3633,7 +3540,25 @@ const installVisualStabilityDiagnostics = () => {
|
|
|
|
|
})),
|
|
|
|
|
switchFloor: handleFloorChange,
|
|
|
|
|
showOverview,
|
|
|
|
|
resetCamera
|
|
|
|
|
resetCamera,
|
|
|
|
|
getFloorPois: async (floorId: string) => {
|
|
|
|
|
const floor = floorIndex.value.find((item) => item.floorId === floorId)
|
|
|
|
|
if (!floor) return []
|
|
|
|
|
const pois = poiDataCache.get(floorId) || await props.modelSource.loadFloorPois(floorId)
|
|
|
|
|
return pois
|
|
|
|
|
.filter((poi) => Boolean(poi.positionGltf))
|
|
|
|
|
.map((poi) => ({
|
|
|
|
|
poiId: poi.id,
|
|
|
|
|
floorId: poi.floorId,
|
|
|
|
|
name: poi.name,
|
|
|
|
|
positionGltf: poi.positionGltf!
|
|
|
|
|
}))
|
|
|
|
|
},
|
|
|
|
|
focusTargetPoi: async (request: TargetPoiFocusRequest) => {
|
|
|
|
|
queueTargetFocus(request)
|
|
|
|
|
return targetFocusQueue
|
|
|
|
|
},
|
|
|
|
|
clearTargetFocus
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@@ -4629,7 +4554,7 @@ const showFocusPoiLabel = (poi: RenderPoi) => {
|
|
|
|
|
const markerSize = getPoiMarkerSize()
|
|
|
|
|
activeFocusLabelSprite = createPoiLabelSprite(poi, markerSize)
|
|
|
|
|
activeFocusLabelSprite.frustumCulled = false
|
|
|
|
|
activeFocusLabelSprite.position.copy(getPoiLiftedPosition(displayPosition, markerSize, poiFocusLabelLiftFactor))
|
|
|
|
|
activeFocusLabelSprite.position.copy(displayPosition)
|
|
|
|
|
updateFocusLabelScale()
|
|
|
|
|
poiGroup.add(activeFocusLabelSprite)
|
|
|
|
|
}
|
|
|
|
|
@@ -4643,7 +4568,7 @@ const showFocusPoiBase = (poi: RenderPoi) => {
|
|
|
|
|
const markerSize = getPoiMarkerSize()
|
|
|
|
|
activeFocusBaseSprite = createPoiBaseSprite(poi, markerSize)
|
|
|
|
|
activeFocusBaseSprite.frustumCulled = false
|
|
|
|
|
activeFocusBaseSprite.position.copy(getPoiLiftedPosition(displayPosition, markerSize, poiFocusBaseLiftFactor))
|
|
|
|
|
activeFocusBaseSprite.position.copy(displayPosition)
|
|
|
|
|
poiGroup.add(activeFocusBaseSprite)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@@ -4656,7 +4581,7 @@ const showFocusPoiPulse = (poi: RenderPoi) => {
|
|
|
|
|
const markerSize = getPoiMarkerSize()
|
|
|
|
|
activeFocusPulseSprite = createPoiPulseSprite(poi, markerSize)
|
|
|
|
|
activeFocusPulseSprite.frustumCulled = false
|
|
|
|
|
activeFocusPulseSprite.position.copy(getPoiLiftedPosition(displayPosition, markerSize, poiFocusPulseLiftFactor))
|
|
|
|
|
activeFocusPulseSprite.position.copy(displayPosition)
|
|
|
|
|
poiGroup.add(activeFocusPulseSprite)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@@ -4800,13 +4725,13 @@ const addFocusMarkerFromRequest = (request: TargetPoiFocusRequest) => {
|
|
|
|
|
if (!poiGroup || findPoiSprite(request.poiId)) return findLoadedPoi(request.poiId) || null
|
|
|
|
|
|
|
|
|
|
const poi = createPoiFromFocusRequest(request)
|
|
|
|
|
const displayPosition = poi ? getPoiDisplayPosition(poi, request.floorId) : null
|
|
|
|
|
const displayPosition = poi ? getPoiDisplayPosition(poi) : null
|
|
|
|
|
if (!poi || !displayPosition) return null
|
|
|
|
|
|
|
|
|
|
const markerSize = getPoiMarkerSize()
|
|
|
|
|
const { sprite, hitTarget } = createPoiMarkerSprites(poi, markerSize)
|
|
|
|
|
sprite.position.copy(getPoiMarkerPosition(displayPosition, markerSize))
|
|
|
|
|
hitTarget.position.copy(sprite.position)
|
|
|
|
|
sprite.position.copy(displayPosition)
|
|
|
|
|
hitTarget.position.copy(displayPosition)
|
|
|
|
|
poiGroup.add(sprite)
|
|
|
|
|
poiGroup.add(hitTarget)
|
|
|
|
|
|
|
|
|
|
@@ -4826,14 +4751,14 @@ const createPoiMarkerGroup = (
|
|
|
|
|
group.userData.displayMode = displayMode
|
|
|
|
|
|
|
|
|
|
pois.forEach((poi) => {
|
|
|
|
|
const displayPosition = getPoiDisplayPosition(poi, floorId)
|
|
|
|
|
const displayPosition = getPoiDisplayPosition(poi)
|
|
|
|
|
if (!displayPosition) return
|
|
|
|
|
|
|
|
|
|
const { sprite, hitTarget, labelSprite } = createPoiMarkerSprites(poi, markerSize)
|
|
|
|
|
sprite.position.copy(getPoiMarkerPosition(displayPosition, markerSize))
|
|
|
|
|
hitTarget.position.copy(sprite.position)
|
|
|
|
|
sprite.position.copy(displayPosition)
|
|
|
|
|
hitTarget.position.copy(displayPosition)
|
|
|
|
|
if (labelSprite) {
|
|
|
|
|
labelSprite.position.copy(getPoiAmbientLabelPosition(displayPosition, markerSize))
|
|
|
|
|
labelSprite.position.copy(displayPosition)
|
|
|
|
|
labelSprite.visible = false
|
|
|
|
|
}
|
|
|
|
|
sprite.visible = isPoiIncludedByVisibleFilter(poi)
|
|
|
|
|
@@ -4877,8 +4802,7 @@ const attachPoiMarkerGroup = (entry: PoiMarkerCacheEntry) => {
|
|
|
|
|
poiGroupCurrentFloor: poiGroup.userData.currentFloor,
|
|
|
|
|
selectedPoi: summarizeSelectedPoiForDiagnostics(selectedPOI.value),
|
|
|
|
|
markerSize: roundDiagnosticNumber(entry.markerSize),
|
|
|
|
|
displayOffsetY: roundDiagnosticNumber(entry.displayOffsetY),
|
|
|
|
|
visualLiftY: roundDiagnosticNumber(entry.markerSize * poiMarkerVisualLiftFactor)
|
|
|
|
|
coordinateSpace: 'GLB_METER'
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@@ -4945,9 +4869,9 @@ const prepareFloorPOIs = async (
|
|
|
|
|
renderPoiCategoryCount: validPois.filter((poi) => poi.primaryCategory === 'poi').length,
|
|
|
|
|
filteredCategoryCounts: countPoisByCategory(filteredPois)
|
|
|
|
|
})
|
|
|
|
|
const displayOffsetY = markerModel
|
|
|
|
|
? resolvePoiDisplayOffsetY(floor.floorId, markerModel, validPois)
|
|
|
|
|
: getCachedPoiDisplayOffsetY(floor.floorId)
|
|
|
|
|
if (markerModel) {
|
|
|
|
|
recordPoiCoordinateDiagnostics(floor.floorId, markerModel, validPois)
|
|
|
|
|
}
|
|
|
|
|
const markerSize = getPoiMarkerSizeForModel(markerModel)
|
|
|
|
|
const markerGroup = createPoiMarkerGroup(floor.floorId, displayMode, validPois, markerSize)
|
|
|
|
|
const entry: PoiMarkerCacheEntry = {
|
|
|
|
|
@@ -4956,7 +4880,6 @@ const prepareFloorPOIs = async (
|
|
|
|
|
pois: validPois,
|
|
|
|
|
group: markerGroup,
|
|
|
|
|
markerSize,
|
|
|
|
|
displayOffsetY,
|
|
|
|
|
rawPoiCount: floorPois.length,
|
|
|
|
|
rawPositionedPoiCount: floorPois.filter((poi) => Boolean(poi.positionGltf)).length,
|
|
|
|
|
filteredCategoryCounts: countPoisByCategory(filteredPois)
|
|
|
|
|
@@ -5232,30 +5155,25 @@ const isSceneReadyForTargetFocus = () => (
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
const focusCameraOnPoi = (poi: RenderPoi) => {
|
|
|
|
|
const displayPosition = getPoiDisplayPosition(poi)
|
|
|
|
|
if (!camera || !controls || !displayPosition) return
|
|
|
|
|
const target = getPoiDisplayPosition(poi)
|
|
|
|
|
if (!camera || !controls || !target) return
|
|
|
|
|
|
|
|
|
|
const target = displayPosition.clone()
|
|
|
|
|
const modelSize = activeModel
|
|
|
|
|
? 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 currentOffset = camera.position.clone().sub(controls.target)
|
|
|
|
|
const currentDistance = currentOffset.length()
|
|
|
|
|
const direction = currentDistance > 1e-6
|
|
|
|
|
? currentOffset.multiplyScalar(1 / currentDistance)
|
|
|
|
|
: new THREE.Vector3(0.72, 0.58, 1).normalize()
|
|
|
|
|
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 minimumDistance = Math.max(controls.minDistance, 1)
|
|
|
|
|
const maximumDistance = Math.max(minimumDistance, controls.maxDistance)
|
|
|
|
|
const distance = THREE.MathUtils.clamp(
|
|
|
|
|
currentDistance * focusDistanceFactor,
|
|
|
|
|
minimumDistance,
|
|
|
|
|
maximumDistance
|
|
|
|
|
)
|
|
|
|
|
const direction = new THREE.Vector3(0.72, 0.58, 1).normalize()
|
|
|
|
|
const nextPosition = target.clone().add(direction.multiplyScalar(distance))
|
|
|
|
|
|
|
|
|
|
camera.near = Math.max(SGS_VISUAL_RENDER_CONFIG.camera.near, maxDim / 1200)
|
|
|
|
|
camera.far = Math.max(SGS_VISUAL_RENDER_CONFIG.camera.far, maxDim * 20)
|
|
|
|
|
camera.updateProjectionMatrix()
|
|
|
|
|
|
|
|
|
|
controls.minDistance = Math.max(SGS_VISUAL_RENDER_CONFIG.controls.minDistance, maxDim * 0.025)
|
|
|
|
|
controls.maxDistance = Math.max(SGS_VISUAL_RENDER_CONFIG.controls.maxDistance, maxDim * 6)
|
|
|
|
|
moveCameraTo(nextPosition, target)
|
|
|
|
|
moveCameraTo(nextPosition, target, { reason: 'poi-focus' })
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const focusTargetPoi = async (request: TargetPoiFocusRequest) => {
|
|
|
|
|
@@ -5387,6 +5305,8 @@ const init3DScene = async () => {
|
|
|
|
|
firstModelLoadStartedAt = getNow()
|
|
|
|
|
firstModelVisibleReported = false
|
|
|
|
|
initialModelSettled = false
|
|
|
|
|
poiFocusCameraAnimationCount = 0
|
|
|
|
|
cameraSnapshotRestoreCount = 0
|
|
|
|
|
isLoading.value = true
|
|
|
|
|
loadError.value = false
|
|
|
|
|
selectedPOI.value = null
|
|
|
|
|
@@ -5570,6 +5490,8 @@ const disposeScene = () => {
|
|
|
|
|
clearModelAdjustReportTimer()
|
|
|
|
|
hasPendingManualModelAdjustment = false
|
|
|
|
|
cameraTween = null
|
|
|
|
|
poiFocusCameraAnimationCount = 0
|
|
|
|
|
cameraSnapshotRestoreCount = 0
|
|
|
|
|
isProgrammaticCameraChange = false
|
|
|
|
|
activeAutoSwitchInputSource = 'gesture'
|
|
|
|
|
resetInteractionGateState()
|
|
|
|
|
|