From 5efcc834a84776223e4ec909c3cdd4032a151f03 Mon Sep 17 00:00:00 2001 From: Wassim SAMAD Date: Wed, 17 Jun 2026 10:41:30 -0400 Subject: [PATCH] feat(editor): make snapshot capture camera-only Snapshot/preset capture is for framing a thumbnail, not editing, so it should only move the camera: - gate the selection manager, transform handles, floating menus, and the tool manager (which renders the site boundary flags) behind isCaptureMode, so clicking the scene no longer selects items or shows editing controls - hide zone meshes and the HTML zone tags while capturing - force 3D on entry (the 2D/split floorplan panes render nothing useful for a thumbnail) and restore the prior view mode on exit Co-Authored-By: Claude Opus 4.8 (1M context) --- .../editor/src/components/editor/index.tsx | 10 ++++-- .../components/systems/zone/zone-system.tsx | 18 +++++++--- packages/editor/src/store/use-editor.tsx | 35 ++++++++++++++++++- 3 files changed, 55 insertions(+), 8 deletions(-) diff --git a/packages/editor/src/components/editor/index.tsx b/packages/editor/src/components/editor/index.tsx index ca6dde06..3c8b1d59 100644 --- a/packages/editor/src/components/editor/index.tsx +++ b/packages/editor/src/components/editor/index.tsx @@ -569,7 +569,7 @@ function PaintCursorBadge({ alt="" aria-hidden="true" className="h-5 w-5 object-contain drop-shadow-[0_2px_4px_rgba(0,0,0,0.5)]" - src="/icons/paint.png" + src="/icons/paint.webp" /> @@ -601,10 +601,14 @@ const ViewerSceneContent = memo(function ViewerSceneContent({ }) { // Studio mode is a clean render/snapshot surface — no selection or editing // affordances. It mirrors version-preview's chrome gating on the canvas. - const noEditing = isVersionPreviewMode || isFirstPersonMode || isStudioMode + // Capture (snapshot) mode is camera-only for the same reason: suppress + // selection, editing handles, and the tool manager (which mounts the site + // boundary flags) so the framed shot stays clean. + const isCaptureMode = useEditor((s) => s.isCaptureMode) + const noEditing = isVersionPreviewMode || isFirstPersonMode || isStudioMode || isCaptureMode return ( <> - {!(isFirstPersonMode || isStudioMode) && } + {!(isFirstPersonMode || isStudioMode || isCaptureMode) && } {!noEditing && } {!noEditing && } {!noEditing && } diff --git a/packages/editor/src/components/systems/zone/zone-system.tsx b/packages/editor/src/components/systems/zone/zone-system.tsx index e42ac385..98e68b6c 100644 --- a/packages/editor/src/components/systems/zone/zone-system.tsx +++ b/packages/editor/src/components/systems/zone/zone-system.tsx @@ -16,6 +16,9 @@ export const ZoneSystem = () => { const selectedLevelId = useViewer.getState().selection.levelId const selectedZoneId = useViewer.getState().selection.zoneId const hoveredId = useViewer.getState().hoveredId + // Snapshot capture is a clean, camera-only surface — never show zone + // geometry or the HTML zone tags in the framed shot. + const isCaptureMode = useEditor.getState().isCaptureMode const zoneGeometryVisible = structureLayer === 'zones' const zones = sceneRegistry.byType.zone || new Set() @@ -35,8 +38,14 @@ export const ZoneSystem = () => { // Keep group visible (so labels stay active), hide/show meshes only. // Show meshes when: in zone mode, selected, or delete-hovered. if (!obj.visible) obj.visible = true - const meshVisible = zoneGeometryVisible || isSelected || isDeleteHovered - const targetOpacity = isSelected || isDeleteHovered ? 1 : zoneGeometryVisible ? 1 : 0 + const meshVisible = !isCaptureMode && (zoneGeometryVisible || isSelected || isDeleteHovered) + const targetOpacity = isCaptureMode + ? 0 + : isSelected || isDeleteHovered + ? 1 + : zoneGeometryVisible + ? 1 + : 0 const walls = (obj as Group).getObjectByName('walls') as Mesh | undefined if (walls) { @@ -73,8 +82,9 @@ export const ZoneSystem = () => { obj.userData.__raycastDisabled = true } - // Labels: always visible on the current level (regardless of mode) - const showLabel = !!selectedLevelId && isOnSelectedLevel + // Labels: visible on the current level (regardless of mode), but never + // during snapshot capture. + const showLabel = !isCaptureMode && !!selectedLevelId && isOnSelectedLevel const labelOpacity = showLabel ? '1' : '0' const labelEl = document.getElementById(`${zoneId}-label`) if (labelEl && labelEl.style.opacity !== labelOpacity) { diff --git a/packages/editor/src/store/use-editor.tsx b/packages/editor/src/store/use-editor.tsx index 96337af4..19543f62 100644 --- a/packages/editor/src/store/use-editor.tsx +++ b/packages/editor/src/store/use-editor.tsx @@ -688,6 +688,11 @@ export function selectSiteFloorplanContext() { }) } +// Stashes the view mode the user was in before entering capture, so we can +// restore it on exit. Snapshot capture always frames in 3D — the 2D/split +// floorplan panes render nothing meaningful for a thumbnail. +let viewModeBeforeCapture: ViewMode | null = null + const useEditor = create()( persist( (set, get) => ({ @@ -947,7 +952,35 @@ const useEditor = create()( setCaptureMode: (next) => { const resolved: CaptureMode = typeof next === 'boolean' ? { mode: next ? 'standard' : 'idle' } : next - set({ captureMode: resolved, isCaptureMode: resolved.mode !== 'idle' }) + const entering = resolved.mode !== 'idle' + set((state) => { + if (entering) { + // Force 3D for the shot. Remember the prior mode only on the first + // entry (viewMode is already '3d' on re-entry), so we restore the + // user's real choice — not the forced '3d' — when capture ends. + if (state.viewMode !== '3d') { + viewModeBeforeCapture = state.viewMode + return { + captureMode: resolved, + isCaptureMode: true, + viewMode: '3d', + isFloorplanOpen: false, + } + } + return { captureMode: resolved, isCaptureMode: true } + } + const restore = viewModeBeforeCapture + viewModeBeforeCapture = null + if (restore && restore !== '3d') { + return { + captureMode: resolved, + isCaptureMode: false, + viewMode: restore, + isFloorplanOpen: true, + } + } + return { captureMode: resolved, isCaptureMode: false } + }) }, viewMode: DEFAULT_PERSISTED_EDITOR_UI_STATE.viewMode, setViewMode: (mode) => set({ viewMode: mode, isFloorplanOpen: mode !== '3d' }),