@@ -369,10 +369,15 @@ export const CustomCameraControls = () => {
369369 const lastPublishedNavigationSync = useRef < NavigationCameraPoseSnapshot | null > ( null )
370370 const pendingFloorplanNavigationPose = useRef < PendingNavigationCameraPoseSnapshot | null > ( null )
371371 const lastApplied2dNavigationRevision = useRef ( 0 )
372+ const savedSmoothTimeRef = useRef < number | null > ( null )
372373 const maxPolarAngle =
373374 ! isPreviewMode && allowUndergroundCamera ? DEBUG_MAX_POLAR_ANGLE : DEFAULT_MAX_POLAR_ANGLE
374375 const clearPendingFloorplanNavigationPose = useCallback ( ( ) => {
375376 pendingFloorplanNavigationPose . current = null
377+ if ( savedSmoothTimeRef . current !== null && controls . current ) {
378+ controls . current . smoothTime = savedSmoothTimeRef . current
379+ savedSmoothTimeRef . current = null
380+ }
376381 } , [ ] )
377382
378383 const camera = useThree ( ( state ) => state . camera )
@@ -478,6 +483,13 @@ export const CustomCameraControls = () => {
478483 Math . abs ( viewWidthUpdate . viewWidth - pose . viewWidth ) >=
479484 NAVIGATION_SYNC_VIEW_WIDTH_EPSILON ,
480485 }
486+ // Match 3D settle time to 2D exponential decay (τ=90ms). SmoothDamp's
487+ // effective time constant is smoothTime/2, so smoothTime=0.18 gives
488+ // τ≈90ms and visual convergence in ~350-400ms, matching the 2D panel.
489+ if ( savedSmoothTimeRef . current === null ) {
490+ savedSmoothTimeRef . current = control . smoothTime
491+ }
492+ control . smoothTime = 0.18
481493 control . moveTo ( pose . target [ 0 ] , pose . target [ 1 ] , pose . target [ 2 ] , true )
482494 control . rotateTo ( targetAzimuth , control . polarAngle , true )
483495 applyCameraViewWidth ( control , viewWidthUpdate )
@@ -499,7 +511,7 @@ export const CustomCameraControls = () => {
499511 isCameraAtNavigationPose ( pendingFloorplanPose , syncTarget , syncSpherical . theta , viewWidth )
500512 ) {
501513 lastPublishedNavigationSync . current = pendingFloorplanPose
502- pendingFloorplanNavigationPose . current = null
514+ clearPendingFloorplanNavigationPose ( )
503515 if ( pendingFloorplanPose . publishOnComplete ) {
504516 useEditor . getState ( ) . publishNavigationSyncPose ( {
505517 source : '3d' ,
@@ -540,7 +552,7 @@ export const CustomCameraControls = () => {
540552 azimuth : syncSpherical . theta ,
541553 viewWidth,
542554 } )
543- } , [ camera , isFirstPersonMode , viewportSize ] )
555+ } , [ camera , clearPendingFloorplanNavigationPose , isFirstPersonMode , viewportSize ] )
544556
545557 useEffect ( ( ) => {
546558 if ( isFirstPersonMode || ( ! isFloorplanOpen && currentLevelId === null ) ) return
@@ -573,7 +585,7 @@ export const CustomCameraControls = () => {
573585 )
574586 const step = ( speed * Math . min ( delta , 0.05 ) ) / Math . hypot ( horizontal , vertical )
575587
576- pendingFloorplanNavigationPose . current = null
588+ clearPendingFloorplanNavigationPose ( )
577589 if ( horizontal !== 0 ) control . truck ( horizontal * step , 0 , true )
578590 if ( vertical !== 0 ) control . forward ( vertical * step , true )
579591 } )
@@ -725,7 +737,7 @@ export const CustomCameraControls = () => {
725737 ! isEditableKeyboardTarget ( event . target )
726738 ) {
727739 setKeyboardPanKey ( keyboardPanKeys . current , event . code , true )
728- pendingFloorplanNavigationPose . current = null
740+ clearPendingFloorplanNavigationPose ( )
729741 event . preventDefault ( )
730742 event . stopPropagation ( )
731743 }
@@ -788,7 +800,7 @@ export const CustomCameraControls = () => {
788800
789801 const onPointerDown = ( event : PointerEvent ) => {
790802 if ( ! ( event . target instanceof Node ) || ! gl . domElement . contains ( event . target ) ) return
791- pendingFloorplanNavigationPose . current = null
803+ clearPendingFloorplanNavigationPose ( )
792804 if ( event . button !== 1 && ! ( event . button === 0 && keyState . space ) ) return
793805
794806 panPointerId = event . pointerId
@@ -797,7 +809,7 @@ export const CustomCameraControls = () => {
797809 }
798810
799811 const onWheel = ( ) => {
800- pendingFloorplanNavigationPose . current = null
812+ clearPendingFloorplanNavigationPose ( )
801813 }
802814
803815 const onPointerUp = ( event : PointerEvent ) => {
@@ -839,7 +851,25 @@ export const CustomCameraControls = () => {
839851 clearKeyboardPanKeys ( )
840852 clearNavigationCursor ( )
841853 }
842- } , [ 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 ] )
843873
844874 // Preview mode: auto-navigate camera to selected node (viewer behavior)
845875 const previewTargetNodeId = isPreviewMode
0 commit comments