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:
co-authored by
Claude Opus 4.8
parent
4b7e0eca76
commit
5efcc834a8
@@ -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) {
|
||||||
|
|||||||
@@ -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' }),
|
||||||
|
|||||||
Reference in New Issue
Block a user