Skip to content

Commit d4f1053

Browse files
Aymericrclaude
andcommitted
fix(editor): compass sync review fixes
Route all pending-pose cancellations through clearPendingFloorplanNavigationPose() so savedSmoothTime is always restored. Add a controlstart listener to catch right-click orbit and other pointer drags that weren't handled. Guard the rotation-degree ref mirror to prevent React re-renders from clobbering the imperative 3D-owned value when the panel is hidden, and add a useLayoutEffect to keep the needle DOM in sync. Make the catch-up effect re-run directly on panel reopen. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent cb5cfc7 commit d4f1053

2 files changed

Lines changed: 43 additions & 16 deletions

File tree

packages/editor/src/components/editor/custom-camera-controls.tsx

Lines changed: 25 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -511,11 +511,7 @@ export const CustomCameraControls = () => {
511511
isCameraAtNavigationPose(pendingFloorplanPose, syncTarget, syncSpherical.theta, viewWidth)
512512
) {
513513
lastPublishedNavigationSync.current = pendingFloorplanPose
514-
pendingFloorplanNavigationPose.current = null
515-
if (savedSmoothTimeRef.current !== null && controls.current) {
516-
controls.current.smoothTime = savedSmoothTimeRef.current
517-
savedSmoothTimeRef.current = null
518-
}
514+
clearPendingFloorplanNavigationPose()
519515
if (pendingFloorplanPose.publishOnComplete) {
520516
useEditor.getState().publishNavigationSyncPose({
521517
source: '3d',
@@ -556,7 +552,7 @@ export const CustomCameraControls = () => {
556552
azimuth: syncSpherical.theta,
557553
viewWidth,
558554
})
559-
}, [camera, isFirstPersonMode, viewportSize])
555+
}, [camera, clearPendingFloorplanNavigationPose, isFirstPersonMode, viewportSize])
560556

561557
useEffect(() => {
562558
if (isFirstPersonMode || (!isFloorplanOpen && currentLevelId === null)) return
@@ -589,7 +585,7 @@ export const CustomCameraControls = () => {
589585
)
590586
const step = (speed * Math.min(delta, 0.05)) / Math.hypot(horizontal, vertical)
591587

592-
pendingFloorplanNavigationPose.current = null
588+
clearPendingFloorplanNavigationPose()
593589
if (horizontal !== 0) control.truck(horizontal * step, 0, true)
594590
if (vertical !== 0) control.forward(vertical * step, true)
595591
})
@@ -741,7 +737,7 @@ export const CustomCameraControls = () => {
741737
!isEditableKeyboardTarget(event.target)
742738
) {
743739
setKeyboardPanKey(keyboardPanKeys.current, event.code, true)
744-
pendingFloorplanNavigationPose.current = null
740+
clearPendingFloorplanNavigationPose()
745741
event.preventDefault()
746742
event.stopPropagation()
747743
}
@@ -804,7 +800,7 @@ export const CustomCameraControls = () => {
804800

805801
const onPointerDown = (event: PointerEvent) => {
806802
if (!(event.target instanceof Node) || !gl.domElement.contains(event.target)) return
807-
pendingFloorplanNavigationPose.current = null
803+
clearPendingFloorplanNavigationPose()
808804
if (event.button !== 1 && !(event.button === 0 && keyState.space)) return
809805

810806
panPointerId = event.pointerId
@@ -813,7 +809,7 @@ export const CustomCameraControls = () => {
813809
}
814810

815811
const onWheel = () => {
816-
pendingFloorplanNavigationPose.current = null
812+
clearPendingFloorplanNavigationPose()
817813
}
818814

819815
const onPointerUp = (event: PointerEvent) => {
@@ -855,7 +851,25 @@ export const CustomCameraControls = () => {
855851
clearKeyboardPanKeys()
856852
clearNavigationCursor()
857853
}
858-
}, [cameraMode, gl, isPreviewMode, isFirstPersonMode])
854+
}, [cameraMode, gl, isPreviewMode, isFirstPersonMode, clearPendingFloorplanNavigationPose])
855+
856+
// Cancel any in-progress 2D-origin navigation pose when the user starts
857+
// dragging (right-click orbit, middle-click pan, touch). `controlstart`
858+
// fires only for user pointer interactions — not for programmatic
859+
// moveTo/rotateTo which emit `transitionstart` instead.
860+
useEffect(() => {
861+
if (isFirstPersonMode) return
862+
const control = controls.current
863+
if (!control) return
864+
865+
const onControlStart = () => {
866+
clearPendingFloorplanNavigationPose()
867+
}
868+
control.addEventListener('controlstart', onControlStart)
869+
return () => {
870+
control.removeEventListener('controlstart', onControlStart)
871+
}
872+
}, [isFirstPersonMode, clearPendingFloorplanNavigationPose])
859873

860874
// Preview mode: auto-navigate camera to selected node (viewer behavior)
861875
const previewTargetNodeId = isPreviewMode

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

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5181,7 +5181,11 @@ export function FloorplanPanel({
51815181
const buildingRotationDeg = (buildingRotationY * 180) / Math.PI
51825182
const floorplanSceneRotationDeg =
51835183
FLOORPLAN_VIEW_ROTATION_DEG + floorplanUserRotationDeg - buildingRotationDeg
5184-
latestFloorplanUserRotationDegRef.current = floorplanUserRotationDeg
5184+
// Only sync ref from state when floorplan is open (state is source of truth).
5185+
// When hidden, the imperative 3D path owns the ref and must not be clobbered.
5186+
if (isFloorplanOpenRef.current) {
5187+
latestFloorplanUserRotationDegRef.current = floorplanUserRotationDeg
5188+
}
51855189

51865190
// Draft START points stay in panel state (set per click). The live END points
51875191
// are the per-move hot values — they live in `useFloorplanDraftPreview` so a
@@ -6543,19 +6547,18 @@ export function FloorplanPanel({
65436547
)
65446548

65456549
useEffect(() => {
6550+
if (!isFloorplanOpen) return
6551+
65466552
const pose = useEditor.getState().navigationSyncPose
65476553
if (!pose) {
65486554
return
65496555
}
65506556

65516557
latestNavigationSyncPoseRef.current = pose
65526558
if (pose.source === '3d') {
6553-
// Re-runs when the panel reopens (`isFloorplanOpen`) so the viewport
6554-
// catches up to the camera after the per-frame sync was skipped while
6555-
// hidden; a no-op while closed (the sync early-returns).
65566559
syncFloorplanViewportToNavigationPose(pose)
65576560
}
6558-
}, [syncFloorplanViewportToNavigationPose])
6561+
}, [syncFloorplanViewportToNavigationPose, isFloorplanOpen])
65596562

65606563
useEffect(() => {
65616564
return useEditor.subscribe((state) => {
@@ -6586,6 +6589,16 @@ export function FloorplanPanel({
65866589
})
65876590
}, [syncFloorplanViewportToNavigationPose])
65886591

6592+
// When the panel is hidden the imperative path owns the compass needle.
6593+
// React re-renders can overwrite the needle's inline transform with stale
6594+
// state; this layout effect restores the authoritative ref value before
6595+
// the browser paints so the needle never visibly snaps to a stale angle.
6596+
useLayoutEffect(() => {
6597+
if (!isFloorplanOpen && compassNeedleRef.current) {
6598+
compassNeedleRef.current.style.transform = `rotate(${latestFloorplanUserRotationDegRef.current}deg)`
6599+
}
6600+
})
6601+
65896602
useEffect(() => {
65906603
const host = viewportHostRef.current
65916604
if (!host) {

0 commit comments

Comments
 (0)