Skip to content

Commit 7d6879c

Browse files
authored
Merge pull request #487 from pascalorg/fix/compass-follow-hidden-align-north
fix(editor): compass needle follows align-north in 3D-only view
2 parents 5e5b62b + cbcc705 commit 7d6879c

1 file changed

Lines changed: 70 additions & 12 deletions

File tree

packages/editor/src/components/editor/floorplan-panel.tsx

Lines changed: 70 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -5146,6 +5146,7 @@ export function FloorplanPanel({
51465146
useEditor.getState().navigationSyncPose,
51475147
)
51485148
const compassNeedleRef = useRef<SVGSVGElement | null>(null)
5149+
const hiddenCompassAnimationRef = useRef<number | null>(null)
51495150
const levelId = useViewer((state) => state.selection.levelId)
51505151
const buildingId = useViewer((state) => state.selection.buildingId)
51515152
const selectedZoneId = useViewer((state) => state.selection.zoneId)
@@ -6624,34 +6625,91 @@ export function FloorplanPanel({
66246625
}
66256626
}, [syncFloorplanViewportToNavigationPose, isFloorplanOpen])
66266627

6628+
const cancelHiddenCompassAnimation = useCallback(() => {
6629+
if (hiddenCompassAnimationRef.current !== null) {
6630+
cancelAnimationFrame(hiddenCompassAnimationRef.current)
6631+
hiddenCompassAnimationRef.current = null
6632+
}
6633+
}, [])
6634+
6635+
// Align-north while the panel is hidden publishes a single '2d' pose that
6636+
// the 3D camera applies through the echo-suppressed pending-pose path — it
6637+
// never publishes '3d' frames back, so the needle must animate itself.
6638+
// Same time constant as the 2D view animation and the camera's effective
6639+
// smoothTime, so all three stay visually in step.
6640+
const animateHiddenCompassNeedle = useCallback(
6641+
(targetDeg: number) => {
6642+
cancelHiddenCompassAnimation()
6643+
let last = performance.now()
6644+
const tick = (now: number) => {
6645+
hiddenCompassAnimationRef.current = null
6646+
if (isFloorplanOpenRef.current) {
6647+
return
6648+
}
6649+
const deltaMs = now - last
6650+
last = now
6651+
const currentDeg = latestFloorplanUserRotationDegRef.current
6652+
const decay = Math.exp(-deltaMs / FLOORPLAN_VIEW_ANIMATION_TIME_CONSTANT_MS)
6653+
let nextDeg = targetDeg - (targetDeg - currentDeg) * decay
6654+
if (Math.abs(targetDeg - nextDeg) < 0.05) {
6655+
nextDeg = targetDeg
6656+
}
6657+
latestFloorplanUserRotationDegRef.current = nextDeg
6658+
if (compassNeedleRef.current) {
6659+
compassNeedleRef.current.style.transform = `rotate(${nextDeg}deg)`
6660+
}
6661+
if (nextDeg !== targetDeg) {
6662+
hiddenCompassAnimationRef.current = requestAnimationFrame(tick)
6663+
}
6664+
}
6665+
hiddenCompassAnimationRef.current = requestAnimationFrame(tick)
6666+
},
6667+
[cancelHiddenCompassAnimation],
6668+
)
6669+
66276670
useEffect(() => {
6628-
return useEditor.subscribe((state) => {
6671+
const unsubscribe = useEditor.subscribe((state) => {
66296672
const pose = state.navigationSyncPose
66306673
if (!pose || latestNavigationSyncPoseRef.current?.revision === pose.revision) {
66316674
return
66326675
}
66336676

66346677
latestNavigationSyncPoseRef.current = pose
66356678

6636-
if (pose.source === '3d') {
6637-
if (!isFloorplanOpenRef.current) {
6679+
if (!isFloorplanOpenRef.current) {
6680+
const nextDeg = floorplanRotationFromCameraAzimuth(
6681+
pose.azimuth,
6682+
latestFloorplanUserRotationDegRef.current,
6683+
)
6684+
if (pose.source === '3d') {
66386685
// Panel hidden — drive the compass needle imperatively without
66396686
// triggering React state (setViewport) that would re-render the
6640-
// full floorplan SVG every camera frame.
6641-
const nextDeg = floorplanRotationFromCameraAzimuth(
6642-
pose.azimuth,
6643-
latestFloorplanUserRotationDegRef.current,
6644-
)
6687+
// full floorplan SVG every camera frame. The live camera stream
6688+
// owns the needle, so any local animation yields to it.
6689+
cancelHiddenCompassAnimation()
66456690
latestFloorplanUserRotationDegRef.current = nextDeg
66466691
if (compassNeedleRef.current) {
66476692
compassNeedleRef.current.style.transform = `rotate(${nextDeg}deg)`
66486693
}
6649-
return
6694+
} else {
6695+
animateHiddenCompassNeedle(nextDeg)
66506696
}
6697+
return
6698+
}
6699+
6700+
if (pose.source === '3d') {
66516701
syncFloorplanViewportToNavigationPose(pose)
66526702
}
66536703
})
6654-
}, [syncFloorplanViewportToNavigationPose])
6704+
return () => {
6705+
unsubscribe()
6706+
cancelHiddenCompassAnimation()
6707+
}
6708+
}, [
6709+
syncFloorplanViewportToNavigationPose,
6710+
animateHiddenCompassNeedle,
6711+
cancelHiddenCompassAnimation,
6712+
])
66556713

66566714
// When the panel is hidden the imperative path owns the compass needle.
66576715
// React re-renders can overwrite the needle's inline transform with stale
@@ -7442,8 +7500,8 @@ export function FloorplanPanel({
74427500
const alignFloorplanViewToNorth = useCallback(() => {
74437501
if (!isFloorplanOpenRef.current) {
74447502
// Panel hidden — derive from the live 3D camera pose and publish
7445-
// directly. The compass animates via the imperative subscription as
7446-
// the 3D camera transitions.
7503+
// directly. The pose subscription picks this '2d' pose up and animates
7504+
// the needle locally (the camera transition suppresses '3d' echoes).
74477505
const pose = latestNavigationSyncPoseRef.current
74487506
if (!pose) return
74497507
const currentRotation = latestFloorplanUserRotationDegRef.current

0 commit comments

Comments
 (0)