-
Notifications
You must be signed in to change notification settings - Fork 2.4k
fix(editor): compass needle follows align-north in 3D-only view #487
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -5082,6 +5082,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) | ||
|
|
@@ -6560,34 +6561,91 @@ 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 | ||
| } | ||
|
|
||
| latestNavigationSyncPoseRef.current = pose | ||
|
|
||
| if (pose.source === '3d') { | ||
| if (!isFloorplanOpenRef.current) { | ||
| if (!isFloorplanOpenRef.current) { | ||
| 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. | ||
| const nextDeg = floorplanRotationFromCameraAzimuth( | ||
| pose.azimuth, | ||
| latestFloorplanUserRotationDegRef.current, | ||
| ) | ||
| // 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)` | ||
| } | ||
| return | ||
| } else { | ||
| animateHiddenCompassNeedle(nextDeg) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Orbit cannot cancel needle animationMedium Severity After align-north with the floorplan hidden, Reviewed by Cursor Bugbot for commit cbcc705. Configure here. |
||
| } | ||
| 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 | ||
|
|
@@ -7378,8 +7436,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 | ||
|
|
||


There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Stale compass state after animation
Medium Severity
Hidden-panel align-north drives the compass via
animateHiddenCompassNeedle, which updateslatestFloorplanUserRotationDegRefand the DOM but neverfloorplanUserRotationDeg. After the animation finishes, reopening the floorplan copies stale state into the ref and the compassnorthRotationDegprop, so the needle can jump away from the heading shown in 3D-only mode. The reopen catch-up effect still syncs only3dposes, so a lingering2dnavigation pose after hidden align does not refresh the floorplan view.Additional Locations (1)
packages/editor/src/components/editor/floorplan-panel.tsx#L6558-L6561Reviewed by Cursor Bugbot for commit cbcc705. Configure here.