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) <noreply@anthropic.com>
This commit is contained in:
Wassim SAMAD
2026-06-17 10:41:50 -04:00
co-authored by Claude Opus 4.8
parent 4b7e0eca76
commit 5efcc834a8
3 changed files with 55 additions and 8 deletions
@@ -569,7 +569,7 @@ function PaintCursorBadge({
alt="" alt=""
aria-hidden="true" aria-hidden="true"
className="h-5 w-5 object-contain drop-shadow-[0_2px_4px_rgba(0,0,0,0.5)]" 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"
/> />
</div> </div>
</div> </div>
@@ -601,10 +601,14 @@ const ViewerSceneContent = memo(function ViewerSceneContent({
}) { }) {
// Studio mode is a clean render/snapshot surface — no selection or editing // Studio mode is a clean render/snapshot surface — no selection or editing
// affordances. It mirrors version-preview's chrome gating on the canvas. // 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 ( return (
<> <>
{!(isFirstPersonMode || isStudioMode) && <SelectionManager />} {!(isFirstPersonMode || isStudioMode || isCaptureMode) && <SelectionManager />}
{!noEditing && <BoxSelectTool />} {!noEditing && <BoxSelectTool />}
{!noEditing && <NodeArrowHandles />} {!noEditing && <NodeArrowHandles />}
{!noEditing && <GroupRotateHandle />} {!noEditing && <GroupRotateHandle />}
@@ -16,6 +16,9 @@ export const ZoneSystem = () => {
const selectedLevelId = useViewer.getState().selection.levelId const selectedLevelId = useViewer.getState().selection.levelId
const selectedZoneId = useViewer.getState().selection.zoneId const selectedZoneId = useViewer.getState().selection.zoneId
const hoveredId = useViewer.getState().hoveredId 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 zoneGeometryVisible = structureLayer === 'zones'
const zones = sceneRegistry.byType.zone || new Set() const zones = sceneRegistry.byType.zone || new Set()
@@ -35,8 +38,14 @@ export const ZoneSystem = () => {
// Keep group visible (so <Html> labels stay active), hide/show meshes only. // Keep group visible (so <Html> labels stay active), hide/show meshes only.
// Show meshes when: in zone mode, selected, or delete-hovered. // Show meshes when: in zone mode, selected, or delete-hovered.
if (!obj.visible) obj.visible = true if (!obj.visible) obj.visible = true
const meshVisible = zoneGeometryVisible || isSelected || isDeleteHovered const meshVisible = !isCaptureMode && (zoneGeometryVisible || isSelected || isDeleteHovered)
const targetOpacity = isSelected || isDeleteHovered ? 1 : zoneGeometryVisible ? 1 : 0 const targetOpacity = isCaptureMode
? 0
: isSelected || isDeleteHovered
? 1
: zoneGeometryVisible
? 1
: 0
const walls = (obj as Group).getObjectByName('walls') as Mesh | undefined const walls = (obj as Group).getObjectByName('walls') as Mesh | undefined
if (walls) { if (walls) {
@@ -73,8 +82,9 @@ export const ZoneSystem = () => {
obj.userData.__raycastDisabled = true obj.userData.__raycastDisabled = true
} }
// Labels: always visible on the current level (regardless of mode) // Labels: visible on the current level (regardless of mode), but never
const showLabel = !!selectedLevelId && isOnSelectedLevel // during snapshot capture.
const showLabel = !isCaptureMode && !!selectedLevelId && isOnSelectedLevel
const labelOpacity = showLabel ? '1' : '0' const labelOpacity = showLabel ? '1' : '0'
const labelEl = document.getElementById(`${zoneId}-label`) const labelEl = document.getElementById(`${zoneId}-label`)
if (labelEl && labelEl.style.opacity !== labelOpacity) { if (labelEl && labelEl.style.opacity !== labelOpacity) {
+34 -1
View File
@@ -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<EditorState>()( const useEditor = create<EditorState>()(
persist( persist(
(set, get) => ({ (set, get) => ({
@@ -947,7 +952,35 @@ const useEditor = create<EditorState>()(
setCaptureMode: (next) => { setCaptureMode: (next) => {
const resolved: CaptureMode = const resolved: CaptureMode =
typeof next === 'boolean' ? { mode: next ? 'standard' : 'idle' } : next 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, viewMode: DEFAULT_PERSISTED_EDITOR_UI_STATE.viewMode,
setViewMode: (mode) => set({ viewMode: mode, isFloorplanOpen: mode !== '3d' }), setViewMode: (mode) => set({ viewMode: mode, isFloorplanOpen: mode !== '3d' }),