perf(editor): skip floorplan viewport sync while the 2D panel is hidden

Camera-zoom hitch: the FloorplanPanel stays mounted (display:none in 3D mode),
so its navigation-pose subscriber fired every camera onUpdate. During zoom the
view-width changes continuously, so the epsilon guard never short-circuited and
`syncFloorplanViewportToNavigationPose` ran each frame → setViewport/
setFloorplanUserRotationDeg → a full re-render of the ~10k-line floorplan SVG,
even though nothing is visible (React reconciles display:none subtrees).

Gate the viewport sync on `isFloorplanOpen` via a ref the per-frame subscriber
reads, and re-run the mount catch-up effect when the panel reopens so the
viewport snaps to the current camera. The compass is unaffected — it's portaled
to the always-visible viewer area and still receives the pose; only the panel's
own viewport sync is skipped while hidden.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Wassim SAMAD
2026-06-27 17:34:42 -04:00
co-authored by Claude Opus 4.8
parent bbe5b9a8eb
commit b384bc8178
@@ -5046,6 +5046,11 @@ export function FloorplanPanel({
// the user closes and re-opens the 2D editor instead of restoring the // the user closes and re-opens the 2D editor instead of restoring the
// stale viewport from before they closed it. // stale viewport from before they closed it.
const isFloorplanOpen = useEditor((state) => state.isFloorplanOpen) const isFloorplanOpen = useEditor((state) => state.isFloorplanOpen)
// Mirror for callbacks that fire outside React's render (the per-frame
// navigation-pose subscriber): when the 2D panel is hidden (`display:none` in
// 3D mode) it must NOT re-render on every camera-zoom frame.
const isFloorplanOpenRef = useRef(isFloorplanOpen)
isFloorplanOpenRef.current = isFloorplanOpen
const selectedReferenceId = useEditor((state) => state.selectedReferenceId) const selectedReferenceId = useEditor((state) => state.selectedReferenceId)
const setSelectedReferenceId = useEditor((state) => state.setSelectedReferenceId) const setSelectedReferenceId = useEditor((state) => state.setSelectedReferenceId)
const setMode = useEditor((state) => state.setMode) const setMode = useEditor((state) => state.setMode)
@@ -6441,6 +6446,13 @@ export function FloorplanPanel({
const syncFloorplanViewportToNavigationPose = useCallback( const syncFloorplanViewportToNavigationPose = useCallback(
(pose: NavigationSyncPose) => { (pose: NavigationSyncPose) => {
// Skip the viewport sync while the 2D panel is hidden (3D mode). It writes
// React state (`setViewport`) that re-renders the whole floorplan SVG, so
// doing it every camera-zoom frame for an invisible panel was a needless
// per-frame stall. The catch-up effect below re-syncs on reopen.
if (!isFloorplanOpenRef.current) {
return
}
if (floorplanRotationStateRef.current) { if (floorplanRotationStateRef.current) {
return return
} }
@@ -6471,9 +6483,12 @@ export function FloorplanPanel({
latestNavigationSyncPoseRef.current = pose latestNavigationSyncPoseRef.current = pose
if (pose.source === '3d') { if (pose.source === '3d') {
// Re-runs when the panel reopens (`isFloorplanOpen`) so the viewport
// catches up to the camera after the per-frame sync was skipped while
// hidden; a no-op while closed (the sync early-returns).
syncFloorplanViewportToNavigationPose(pose) syncFloorplanViewportToNavigationPose(pose)
} }
}, [syncFloorplanViewportToNavigationPose]) }, [isFloorplanOpen, syncFloorplanViewportToNavigationPose])
useEffect(() => { useEffect(() => {
return useEditor.subscribe((state) => { return useEditor.subscribe((state) => {