Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
82 changes: 70 additions & 12 deletions packages/editor/src/components/editor/floorplan-panel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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)
}

Copy link
Copy Markdown

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 updates latestFloorplanUserRotationDegRef and the DOM but never floorplanUserRotationDeg. After the animation finishes, reopening the floorplan copies stale state into the ref and the compass northRotationDeg prop, so the needle can jump away from the heading shown in 3D-only mode. The reopen catch-up effect still syncs only 3d poses, so a lingering 2d navigation pose after hidden align does not refresh the floorplan view.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit cbcc705. Configure here.

}
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)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Orbit cannot cancel needle animation

Medium Severity

After align-north with the floorplan hidden, animateHiddenCompassNeedle runs on the '2d' pose, but yielding to the camera relies on later '3d' poses calling cancelHiddenCompassAnimation. While the camera is still damping toward that '2d' target, publishCurrentNavigationPose keeps pendingFloorplanNavigationPose set and returns without publishing '3d', so orbiting mid-transition never cancels the local animation and the needle can keep moving toward north while the live camera diverges.

Fix in Cursor Fix in Web

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
Expand Down Expand Up @@ -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
Expand Down
Loading