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