Merge pull request #487 from pascalorg/fix/compass-follow-hidden-align-north
fix(editor): compass needle follows align-north in 3D-only view
This commit is contained in:
@@ -5146,6 +5146,7 @@ export function FloorplanPanel({
|
||||
useEditor.getState().navigationSyncPose,
|
||||
)
|
||||
const compassNeedleRef = useRef<SVGSVGElement | null>(null)
|
||||
const hiddenCompassAnimationRef = useRef<number | null>(null)
|
||||
const levelId = useViewer((state) => state.selection.levelId)
|
||||
const buildingId = useViewer((state) => state.selection.buildingId)
|
||||
const selectedZoneId = useViewer((state) => state.selection.zoneId)
|
||||
@@ -6624,8 +6625,50 @@ export function FloorplanPanel({
|
||||
}
|
||||
}, [syncFloorplanViewportToNavigationPose, isFloorplanOpen])
|
||||
|
||||
const cancelHiddenCompassAnimation = useCallback(() => {
|
||||
if (hiddenCompassAnimationRef.current !== null) {
|
||||
cancelAnimationFrame(hiddenCompassAnimationRef.current)
|
||||
hiddenCompassAnimationRef.current = null
|
||||
}
|
||||
}, [])
|
||||
|
||||
// Align-north while the panel is hidden publishes a single '2d' pose that
|
||||
// the 3D camera applies through the echo-suppressed pending-pose path — it
|
||||
// never publishes '3d' frames back, so the needle must animate itself.
|
||||
// Same time constant as the 2D view animation and the camera's effective
|
||||
// smoothTime, so all three stay visually in step.
|
||||
const animateHiddenCompassNeedle = useCallback(
|
||||
(targetDeg: number) => {
|
||||
cancelHiddenCompassAnimation()
|
||||
let last = performance.now()
|
||||
const tick = (now: number) => {
|
||||
hiddenCompassAnimationRef.current = null
|
||||
if (isFloorplanOpenRef.current) {
|
||||
return
|
||||
}
|
||||
const deltaMs = now - last
|
||||
last = now
|
||||
const currentDeg = latestFloorplanUserRotationDegRef.current
|
||||
const decay = Math.exp(-deltaMs / FLOORPLAN_VIEW_ANIMATION_TIME_CONSTANT_MS)
|
||||
let nextDeg = targetDeg - (targetDeg - currentDeg) * decay
|
||||
if (Math.abs(targetDeg - nextDeg) < 0.05) {
|
||||
nextDeg = targetDeg
|
||||
}
|
||||
latestFloorplanUserRotationDegRef.current = nextDeg
|
||||
if (compassNeedleRef.current) {
|
||||
compassNeedleRef.current.style.transform = `rotate(${nextDeg}deg)`
|
||||
}
|
||||
if (nextDeg !== targetDeg) {
|
||||
hiddenCompassAnimationRef.current = requestAnimationFrame(tick)
|
||||
}
|
||||
}
|
||||
hiddenCompassAnimationRef.current = requestAnimationFrame(tick)
|
||||
},
|
||||
[cancelHiddenCompassAnimation],
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
return useEditor.subscribe((state) => {
|
||||
const unsubscribe = useEditor.subscribe((state) => {
|
||||
const pose = state.navigationSyncPose
|
||||
if (!pose || latestNavigationSyncPoseRef.current?.revision === pose.revision) {
|
||||
return
|
||||
@@ -6633,25 +6676,40 @@ export function FloorplanPanel({
|
||||
|
||||
latestNavigationSyncPoseRef.current = pose
|
||||
|
||||
if (pose.source === '3d') {
|
||||
if (!isFloorplanOpenRef.current) {
|
||||
// Panel hidden — drive the compass needle imperatively without
|
||||
// triggering React state (setViewport) that would re-render the
|
||||
// full floorplan SVG every camera frame.
|
||||
const nextDeg = floorplanRotationFromCameraAzimuth(
|
||||
pose.azimuth,
|
||||
latestFloorplanUserRotationDegRef.current,
|
||||
)
|
||||
if (pose.source === '3d') {
|
||||
// Panel hidden — drive the compass needle imperatively without
|
||||
// triggering React state (setViewport) that would re-render the
|
||||
// full floorplan SVG every camera frame. The live camera stream
|
||||
// owns the needle, so any local animation yields to it.
|
||||
cancelHiddenCompassAnimation()
|
||||
latestFloorplanUserRotationDegRef.current = nextDeg
|
||||
if (compassNeedleRef.current) {
|
||||
compassNeedleRef.current.style.transform = `rotate(${nextDeg}deg)`
|
||||
}
|
||||
} else {
|
||||
animateHiddenCompassNeedle(nextDeg)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if (pose.source === '3d') {
|
||||
syncFloorplanViewportToNavigationPose(pose)
|
||||
}
|
||||
})
|
||||
}, [syncFloorplanViewportToNavigationPose])
|
||||
return () => {
|
||||
unsubscribe()
|
||||
cancelHiddenCompassAnimation()
|
||||
}
|
||||
}, [
|
||||
syncFloorplanViewportToNavigationPose,
|
||||
animateHiddenCompassNeedle,
|
||||
cancelHiddenCompassAnimation,
|
||||
])
|
||||
|
||||
// When the panel is hidden the imperative path owns the compass needle.
|
||||
// React re-renders can overwrite the needle's inline transform with stale
|
||||
@@ -7442,8 +7500,8 @@ export function FloorplanPanel({
|
||||
const alignFloorplanViewToNorth = useCallback(() => {
|
||||
if (!isFloorplanOpenRef.current) {
|
||||
// Panel hidden — derive from the live 3D camera pose and publish
|
||||
// directly. The compass animates via the imperative subscription as
|
||||
// the 3D camera transitions.
|
||||
// directly. The pose subscription picks this '2d' pose up and animates
|
||||
// the needle locally (the camera transition suppresses '3d' echoes).
|
||||
const pose = latestNavigationSyncPoseRef.current
|
||||
if (!pose) return
|
||||
const currentRotation = latestFloorplanUserRotationDegRef.current
|
||||
|
||||
Reference in New Issue
Block a user