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