@pascal-app/editor 1.0.0-beta.4 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/bunfig.toml +4 -0
- package/package.json +15 -8
- package/scripts/generate-print-golden-house.ts +89 -0
- package/src/components/editor/alignment-3d-guide-layer.tsx +12 -3
- package/src/components/editor/bake-exporter.tsx +1 -0
- package/src/components/editor/camera-dragging-lifecycle.test.ts +41 -0
- package/src/components/editor/camera-dragging-lifecycle.ts +10 -2
- package/src/components/editor/capture-camera-rig.tsx +59 -0
- package/src/components/editor/custom-camera-controls.tsx +20 -11
- package/src/components/editor/editor-layout-mobile.tsx +6 -6
- package/src/components/editor/editor-layout-v2.tsx +17 -5
- package/src/components/editor/elevation-3d-guide-layer.tsx +3 -2
- package/src/components/editor/export-manager.tsx +175 -33
- package/src/components/editor/first-person/build-collider-world.test.ts +30 -0
- package/src/components/editor/first-person-controls.tsx +182 -57
- package/src/components/editor/floating-action-menu.tsx +55 -55
- package/src/components/editor/floorplan-background-selection.test.ts +31 -4
- package/src/components/editor/floorplan-background-selection.ts +27 -7
- package/src/components/editor/floorplan-panel.tsx +81 -57
- package/src/components/editor/grid.tsx +12 -7
- package/src/components/editor/group-actions.ts +34 -50
- package/src/components/editor/group-floating-action-menu.tsx +30 -15
- package/src/components/editor/group-move-3d.ts +51 -21
- package/src/components/editor/group-transform-shared.test.ts +78 -0
- package/src/components/editor/group-transform-shared.ts +74 -7
- package/src/components/editor/handles/handle-arrow-raycast.test.ts +86 -0
- package/src/components/editor/handles/handle-arrow.tsx +109 -48
- package/src/components/editor/handles/linear-resize-drag.ts +67 -0
- package/src/components/editor/handles/resize-snap.test.ts +46 -0
- package/src/components/editor/handles/resize-snap.ts +10 -1
- package/src/components/editor/handles/use-handle-drag.ts +18 -1
- package/src/components/editor/index.tsx +252 -46
- package/src/components/editor/measurement-pill.tsx +12 -12
- package/src/components/editor/node-action-menu.tsx +42 -1
- package/src/components/editor/node-arrow-handles.tsx +47 -58
- package/src/components/editor/opening-guides-3d-layer.tsx +13 -4
- package/src/components/editor/selection-manager.tsx +132 -47
- package/src/components/editor/site-edge-labels.tsx +2 -1
- package/src/components/editor/snapshot-capture-overlay.tsx +309 -41
- package/src/components/editor/snapshot-capture.test.ts +422 -0
- package/src/components/editor/snapshot-capture.ts +145 -0
- package/src/components/editor/thumbnail-generator.tsx +295 -232
- package/src/components/editor/use-floorplan-background-placement.ts +53 -5
- package/src/components/editor/wall-measurement-label.tsx +3 -2
- package/src/components/editor/wall-move-side-handles.tsx +299 -7
- package/src/components/editor-2d/floorplan-group-action-menu.tsx +23 -16
- package/src/components/editor-2d/floorplan-group-move.tsx +67 -33
- package/src/components/editor-2d/floorplan-registered-tool-layer.tsx +3 -1
- package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +129 -38
- package/src/components/editor-2d/renderers/floorplan-dimension-renderer.tsx +14 -1
- package/src/components/editor-2d/renderers/floorplan-geometry-renderer.test.tsx +16 -0
- package/src/components/editor-2d/renderers/floorplan-geometry-renderer.tsx +2 -0
- package/src/components/editor-2d/renderers/floorplan-placement-preview-layer.tsx +52 -3
- package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +196 -1
- package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +204 -114
- package/src/components/systems/ceiling/ceiling-bracket-batch.test.ts +402 -0
- package/src/components/systems/ceiling/ceiling-bracket-batch.ts +371 -0
- package/src/components/systems/ceiling/ceiling-selection-affordance-system.test.ts +347 -0
- package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +281 -305
- package/src/components/systems/roof/roof-edit-system.test.tsx +120 -0
- package/src/components/systems/roof/roof-edit-system.tsx +10 -19
- package/src/components/systems/selection-affordance-manager.tsx +59 -9
- package/src/components/systems/selection-affordance-services.ts +19 -0
- package/src/components/systems/stair/stair-edit-system.tsx +5 -2
- package/src/components/tools/fence/fence-drafting.test.ts +100 -0
- package/src/components/tools/fence/fence-drafting.ts +6 -25
- package/src/components/tools/item/draft-creation.test.ts +12 -0
- package/src/components/tools/item/draft-creation.ts +10 -0
- package/src/components/tools/item/face-host-commit.test.ts +65 -0
- package/src/components/tools/item/face-host-commit.ts +56 -0
- package/src/components/tools/item/face-host-preview.test.ts +77 -0
- package/src/components/tools/item/face-host-preview.ts +82 -0
- package/src/components/tools/item/move-tool.tsx +4 -3
- package/src/components/tools/item/placement-math.test.ts +11 -1
- package/src/components/tools/item/placement-math.ts +0 -20
- package/src/components/tools/item/placement-strategies.test.ts +494 -0
- package/src/components/tools/item/placement-strategies.ts +192 -24
- package/src/components/tools/item/placement-surface.test.ts +61 -0
- package/src/components/tools/item/placement-surface.ts +24 -0
- package/src/components/tools/item/placement-types.ts +6 -0
- package/src/components/tools/item/test-face-host.ts +121 -0
- package/src/components/tools/item/use-draft-node.test.tsx +336 -0
- package/src/components/tools/item/use-draft-node.ts +26 -4
- package/src/components/tools/item/use-placement-coordinator.tsx +447 -180
- package/src/components/tools/registered-tool-install-lifecycle.test.tsx +256 -0
- package/src/components/tools/registry/move-registry-node-tool.test.ts +10 -0
- package/src/components/tools/registry/move-registry-node-tool.tsx +306 -110
- package/src/components/tools/registry-tool-context.tsx +30 -0
- package/src/components/tools/select/box-select-tool.tsx +3 -1
- package/src/components/tools/select/marquee-footprint.test.ts +53 -0
- package/src/components/tools/select/marquee-footprint.ts +29 -0
- package/src/components/tools/shared/placement-box.tsx +63 -8
- package/src/components/tools/shared/placement-dimension-guides.tsx +121 -0
- package/src/components/tools/shared/pointer-support-cap.test.ts +319 -17
- package/src/components/tools/shared/pointer-support-cap.ts +45 -8
- package/src/components/tools/site/terrain-brush-cursor.tsx +2 -1
- package/src/components/tools/site/terrain-sculpt-grid.tsx +9 -5
- package/src/components/tools/site/terrain-sculpt-tool.tsx +1 -1
- package/src/components/tools/stair/stair-tool.tsx +127 -74
- package/src/components/tools/tool-manager.tsx +25 -8
- package/src/components/tools/wall/wall-drafting.test.ts +527 -4
- package/src/components/tools/wall/wall-drafting.ts +55 -462
- package/src/components/ui/action-menu/control-modes.tsx +9 -6
- package/src/components/ui/action-menu/index.tsx +11 -1
- package/src/components/ui/command-palette/editor-commands.tsx +12 -13
- package/src/components/ui/command-palette/index.tsx +7 -2
- package/src/components/ui/controls/material-paint-panel.tsx +6 -4
- package/src/components/ui/controls/material-picker.tsx +4 -4
- package/src/components/ui/controls/scene-material-list.tsx +6 -5
- package/src/components/ui/controls/segmented-control.tsx +8 -1
- package/src/components/ui/controls/slider-control.tsx +19 -3
- package/src/components/ui/controls/toggle-control.tsx +18 -5
- package/src/components/ui/controls/tool-options-panel.tsx +126 -0
- package/src/components/ui/floating-level-selector.tsx +23 -6
- package/src/components/ui/helpers/contextual-helper-panel.tsx +9 -0
- package/src/components/ui/helpers/helper-manager.tsx +47 -11
- package/src/components/ui/helpers/item-helper.tsx +1 -1
- package/src/components/ui/helpers/registered-tool-helper.tsx +21 -0
- package/src/components/ui/panels/homogeneous-selection.test.ts +80 -0
- package/src/components/ui/panels/homogeneous-selection.ts +47 -0
- package/src/components/ui/panels/mobile-selection-bar.tsx +11 -5
- package/src/components/ui/panels/multi-field-value.test.ts +209 -0
- package/src/components/ui/panels/multi-field-value.ts +163 -0
- package/src/components/ui/panels/multi-height-mode.tsx +185 -0
- package/src/components/ui/panels/multi-parametric-inspector.tsx +263 -0
- package/src/components/ui/panels/multi-selection-panel.tsx +78 -26
- package/src/components/ui/panels/node-display.ts +4 -0
- package/src/components/ui/panels/panel-manager.tsx +77 -3
- package/src/components/ui/panels/panel-wrapper.tsx +228 -12
- package/src/components/ui/panels/parametric-field-control.tsx +173 -0
- package/src/components/ui/panels/parametric-field-utils.ts +18 -0
- package/src/components/ui/panels/parametric-inspector.tsx +29 -162
- package/src/components/ui/panels/reference-panel.tsx +31 -1
- package/src/components/ui/primitives/shortcut-token.tsx +23 -1
- package/src/components/ui/scene-loader.tsx +35 -0
- package/src/components/ui/sidebar/panels/items-panel/function-tree-panel.tsx +7 -2
- package/src/components/ui/sidebar/panels/settings-panel/index.tsx +390 -36
- package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +31 -23
- package/src/components/ui/sidebar/panels/settings-panel/print-export-button.test.ts +140 -0
- package/src/components/ui/sidebar/panels/settings-panel/print-export-button.tsx +109 -0
- package/src/components/ui/sidebar/panels/site-panel/dormer-tree-node.tsx +19 -5
- package/src/components/ui/sidebar/panels/site-panel/index.tsx +128 -46
- package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +5 -2
- package/src/components/ui/sidebar/panels/site-panel/registry-tree-node.tsx +28 -11
- package/src/components/ui/sidebar/panels/site-panel/tree-node.test.ts +8 -0
- package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +22 -7
- package/src/components/ui/sidebar/tab-bar.tsx +9 -1
- package/src/components/ui/sidebar/use-plugin-panels.tsx +17 -2
- package/src/components/viewer/floorplan-compass-button.tsx +50 -0
- package/src/components/viewer/floorplan-preview-geometry.test.ts +107 -0
- package/src/components/viewer/floorplan-preview-geometry.ts +281 -0
- package/src/components/viewer/floorplan-preview-navigation.test.ts +43 -0
- package/src/components/viewer/floorplan-preview-navigation.ts +62 -0
- package/src/components/viewer/floorplan-preview.tsx +1069 -0
- package/src/components/viewer/use-viewer-camera-navigation-sync.ts +140 -0
- package/src/components/viewer/viewer-scene-header.tsx +2 -1
- package/src/components/viewer/viewer-stage-modes.test.ts +38 -0
- package/src/components/viewer/viewer-stage-modes.ts +49 -0
- package/src/components/viewer/viewer-stage-switcher.tsx +95 -0
- package/src/components/viewer/viewer-stage.test.tsx +47 -0
- package/src/components/viewer/viewer-stage.tsx +221 -0
- package/src/components/viewer-overlay.tsx +13 -30
- package/src/components/viewer-zone-system.tsx +6 -2
- package/src/components/walkthrough-hud.tsx +19 -10
- package/src/hooks/use-auto-save.test.ts +122 -1
- package/src/hooks/use-auto-save.ts +119 -13
- package/src/hooks/use-ceiling-events.test.ts +122 -0
- package/src/hooks/use-drag-action.ts +17 -3
- package/src/hooks/use-grid-events.test.ts +240 -0
- package/src/hooks/use-grid-events.ts +190 -11
- package/src/hooks/use-keyboard.test.ts +244 -0
- package/src/hooks/use-keyboard.ts +164 -63
- package/src/hooks/use-registered-tool-enabled.ts +20 -0
- package/src/hooks/use-save-shortcut.ts +33 -0
- package/src/index.tsx +109 -4
- package/src/lib/active-placement-surface.test.ts +16 -0
- package/src/lib/active-placement-surface.ts +14 -0
- package/src/lib/ceiling-surface-raycast.test.ts +64 -0
- package/src/lib/ceiling-surface-raycast.ts +12 -0
- package/src/lib/contextual-help-extension.ts +17 -0
- package/src/lib/contextual-help.test.ts +8 -0
- package/src/lib/contextual-help.ts +8 -0
- package/src/lib/continuation.test.ts +11 -0
- package/src/lib/continuation.ts +8 -1
- package/src/lib/direct-manipulation.test.ts +132 -0
- package/src/lib/direct-manipulation.ts +38 -1
- package/src/lib/editor-api.ts +6 -3
- package/src/lib/elevation-guides.test.ts +28 -1
- package/src/lib/elevation-guides.ts +22 -7
- package/src/lib/floating-menu-scale.test.ts +29 -0
- package/src/lib/floating-menu-scale.ts +14 -0
- package/src/lib/floorplan/apply-alignment.test.ts +47 -2
- package/src/lib/floorplan/apply-alignment.ts +10 -1
- package/src/lib/floorplan/floorplan-export.test.ts +397 -1
- package/src/lib/floorplan/floorplan-export.tsx +111 -16
- package/src/lib/floorplan/floorplan-pdfkit-renderer.test.ts +26 -0
- package/src/lib/floorplan/floorplan-pdfkit-renderer.ts +32 -24
- package/src/lib/floorplan/floorplan-readonly.test.ts +47 -0
- package/src/lib/floorplan/floorplan-readonly.ts +85 -0
- package/src/lib/floorplan-grid-event-point.test.ts +29 -0
- package/src/lib/floorplan-grid-event-point.ts +13 -0
- package/src/lib/glb-export.test.ts +1169 -21
- package/src/lib/glb-export.ts +629 -101
- package/src/lib/grid-event-presentation.ts +16 -0
- package/src/lib/history.test.ts +523 -77
- package/src/lib/history.ts +133 -14
- package/src/lib/host-tree-children.test.ts +27 -0
- package/src/lib/host-tree-children.ts +79 -0
- package/src/lib/inspector-card-mode.test.ts +92 -0
- package/src/lib/inspector-card-mode.ts +60 -0
- package/src/lib/interaction/hot-set.test.ts +10 -1
- package/src/lib/interaction/hot-set.ts +3 -1
- package/src/lib/interaction/overlay-policy.test.ts +33 -1
- package/src/lib/interaction/overlay-policy.ts +16 -0
- package/src/lib/interaction/registered-drafting.ts +36 -0
- package/src/lib/interaction/scope.ts +31 -0
- package/src/lib/level-print-export.test.ts +484 -0
- package/src/lib/level-print-export.ts +526 -0
- package/src/lib/local-project-presentation-persistence.test.ts +302 -0
- package/src/lib/local-project-presentation-persistence.ts +335 -0
- package/src/lib/material-paint.ts +2 -0
- package/src/lib/measurement-parser.ts +3 -0
- package/src/lib/measurements.test.ts +4 -0
- package/src/lib/model-export.ts +29 -0
- package/src/lib/paint-preview-owner.ts +71 -0
- package/src/lib/paint-scope.test.ts +58 -2
- package/src/lib/paint-scope.ts +8 -2
- package/src/lib/planar-cursor-placement.test.ts +178 -1
- package/src/lib/planar-cursor-placement.ts +61 -5
- package/src/lib/plugin-panels.test.ts +92 -1
- package/src/lib/plugin-panels.ts +48 -0
- package/src/lib/portable-export.test.ts +366 -0
- package/src/lib/portable-export.ts +969 -0
- package/src/lib/print-3mf.test.ts +86 -0
- package/src/lib/print-3mf.ts +161 -0
- package/src/lib/print-content-scope.test.ts +107 -0
- package/src/lib/print-content-scope.ts +90 -0
- package/src/lib/print-export.test.ts +294 -0
- package/src/lib/print-export.ts +735 -0
- package/src/lib/print-feature-thickness.test.ts +147 -0
- package/src/lib/print-feature-thickness.ts +180 -0
- package/src/lib/print-golden-house.test-fixture.ts +352 -0
- package/src/lib/print-golden-house.test.ts +249 -0
- package/src/lib/print-roof-solids.test.ts +133 -0
- package/src/lib/print-roof-solids.ts +630 -0
- package/src/lib/print-shell-compiler-baseline.test.ts +419 -0
- package/src/lib/print-shell-compiler-baseline.ts +225 -0
- package/src/lib/print-shell-compiler-manifold-core.ts +330 -0
- package/src/lib/print-shell-compiler-manifold-worker.ts +187 -0
- package/src/lib/print-shell-compiler-manifold.worker.ts +20 -0
- package/src/lib/print-shell-compiler-mesh-data.ts +32 -0
- package/src/lib/print-shell-compiler-protocol.ts +30 -0
- package/src/lib/print-shell-compiler.ts +273 -0
- package/src/lib/print-wall-solids.test.ts +194 -0
- package/src/lib/print-wall-solids.ts +328 -0
- package/src/lib/rigid-plan-svg-transform.test.ts +35 -0
- package/src/lib/rigid-plan-svg-transform.ts +24 -0
- package/src/lib/scene.ts +22 -8
- package/src/lib/selection-routing.test.ts +125 -6
- package/src/lib/selection-routing.ts +34 -1
- package/src/lib/session-groups.test.ts +88 -0
- package/src/lib/session-groups.ts +201 -0
- package/src/lib/sfx-bus.ts +4 -0
- package/src/lib/sfx-player.ts +8 -0
- package/src/lib/snapping-mode.test.ts +32 -4
- package/src/lib/snapping-mode.ts +11 -7
- package/src/lib/structured-clone-fallback.test.ts +45 -0
- package/src/lib/structured-clone-fallback.ts +24 -0
- package/src/lib/usdz-export.ts +38 -0
- package/src/lib/walkthrough-pointer-lock.ts +47 -0
- package/src/store/live-draft-preview-stores.test.ts +3 -0
- package/src/store/terrain-sculpt-mode.test.ts +3 -6
- package/src/store/tool-mode.test.ts +87 -0
- package/src/store/use-camera-hint-focus.ts +48 -0
- package/src/store/use-editor.tsx +299 -188
- package/src/store/use-floorplan-draft-preview.ts +9 -0
- package/src/store/use-interaction-scope.test.ts +23 -1
- package/src/store/use-interaction-scope.ts +63 -2
- package/src/store/use-placement-preview.ts +56 -3
- package/src/store/use-session-groups.ts +103 -0
- package/src/components/tools/roof/roof-tool.tsx +0 -656
- package/src/components/ui/helpers/roof-helper.tsx +0 -14
|
@@ -1,16 +1,27 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
type AnyNodeId,
|
|
5
|
+
emitter,
|
|
6
|
+
sceneRegistry,
|
|
7
|
+
type ThumbnailGenerateEvent,
|
|
8
|
+
useScene,
|
|
9
|
+
} from '@pascal-app/core'
|
|
4
10
|
import {
|
|
5
11
|
computeHeroFraming,
|
|
6
12
|
createSnapshotPipeline,
|
|
7
13
|
GRID_LAYER,
|
|
14
|
+
getVisibleWallMaterials,
|
|
8
15
|
heroCameraPose,
|
|
16
|
+
SNAPSHOT_MAX_EDGE,
|
|
17
|
+
SNAPSHOT_MIME,
|
|
18
|
+
SNAPSHOT_QUALITY,
|
|
9
19
|
type SnapshotPipeline,
|
|
10
20
|
snapLevelsToTruePositions,
|
|
11
21
|
THUMBNAIL_HEIGHT,
|
|
12
22
|
THUMBNAIL_WIDTH,
|
|
13
23
|
temporarilyHideNodeTypes,
|
|
24
|
+
temporarilyShowShadowOnly,
|
|
14
25
|
useViewer,
|
|
15
26
|
} from '@pascal-app/viewer'
|
|
16
27
|
import type { CameraControls } from '@react-three/drei'
|
|
@@ -19,9 +30,19 @@ import { useCallback, useEffect, useRef } from 'react'
|
|
|
19
30
|
import * as THREE from 'three'
|
|
20
31
|
import type { WebGPURenderer } from 'three/webgpu'
|
|
21
32
|
import { EDITOR_LAYER } from '../../lib/constants'
|
|
33
|
+
import {
|
|
34
|
+
applySnapshotCapturePose,
|
|
35
|
+
captureSnapshotScene,
|
|
36
|
+
createSnapshotQueue,
|
|
37
|
+
enqueueSnapshotCapture,
|
|
38
|
+
runSnapshotCapture,
|
|
39
|
+
} from './snapshot-capture'
|
|
22
40
|
|
|
23
41
|
export interface SnapshotCameraData {
|
|
42
|
+
requestId?: string
|
|
24
43
|
position: [number, number, number]
|
|
44
|
+
quaternion?: [number, number, number, number]
|
|
45
|
+
fov?: number
|
|
25
46
|
target: [number, number, number] | null
|
|
26
47
|
type?: 'perspective' | 'orthographic'
|
|
27
48
|
zoom?: number
|
|
@@ -33,16 +54,29 @@ interface ThumbnailGeneratorProps {
|
|
|
33
54
|
onThumbnailCapture?: (blob: Blob, cameraData: SnapshotCameraData) => void
|
|
34
55
|
}
|
|
35
56
|
|
|
57
|
+
/** Metres ahead of a controls-less camera to place the stored snapshot target. */
|
|
58
|
+
const FIRST_PERSON_TARGET_DISTANCE = 8
|
|
59
|
+
|
|
60
|
+
function clampSnapshotSize(width: number, height: number): { w: number; h: number } {
|
|
61
|
+
const maxEdge = Math.max(width, height)
|
|
62
|
+
if (maxEdge <= SNAPSHOT_MAX_EDGE) return { w: width, h: height }
|
|
63
|
+
|
|
64
|
+
const scale = SNAPSHOT_MAX_EDGE / maxEdge
|
|
65
|
+
return { w: Math.round(width * scale), h: Math.round(height * scale) }
|
|
66
|
+
}
|
|
67
|
+
|
|
36
68
|
export const ThumbnailGenerator = ({ onThumbnailCapture }: ThumbnailGeneratorProps) => {
|
|
37
69
|
const gl = useThree((state) => state.gl)
|
|
38
70
|
const scene = useThree((state) => state.scene)
|
|
39
|
-
const
|
|
71
|
+
const getThree = useThree((state) => state.get)
|
|
40
72
|
const controls = useThree((state) => state.controls) as CameraControls | null
|
|
41
73
|
const isGenerating = useRef(false)
|
|
74
|
+
const captureQueue = useRef(createSnapshotQueue())
|
|
42
75
|
const onThumbnailCaptureRef = useRef(onThumbnailCapture)
|
|
43
76
|
|
|
44
77
|
const thumbnailCameraRef = useRef<THREE.PerspectiveCamera | null>(null)
|
|
45
78
|
const pipelineRef = useRef<SnapshotPipeline | null>(null)
|
|
79
|
+
const captureVersion = useRef(0)
|
|
46
80
|
|
|
47
81
|
useEffect(() => {
|
|
48
82
|
onThumbnailCaptureRef.current = onThumbnailCapture
|
|
@@ -50,6 +84,7 @@ export const ThumbnailGenerator = ({ onThumbnailCapture }: ThumbnailGeneratorPro
|
|
|
50
84
|
|
|
51
85
|
// Build the thumbnail camera, SSGI pipeline, and render target once — reused on every capture.
|
|
52
86
|
useEffect(() => {
|
|
87
|
+
captureVersion.current += 1
|
|
53
88
|
const cam = new THREE.PerspectiveCamera(60, THUMBNAIL_WIDTH / THUMBNAIL_HEIGHT, 0.1, 1000)
|
|
54
89
|
cam.layers.disable(EDITOR_LAYER)
|
|
55
90
|
cam.layers.disable(GRID_LAYER)
|
|
@@ -74,237 +109,272 @@ export const ThumbnailGenerator = ({ onThumbnailCapture }: ThumbnailGeneratorPro
|
|
|
74
109
|
|
|
75
110
|
return () => {
|
|
76
111
|
mounted = false
|
|
112
|
+
captureVersion.current += 1
|
|
113
|
+
thumbnailCameraRef.current = null
|
|
77
114
|
pipelineRef.current?.dispose()
|
|
78
115
|
pipelineRef.current = null
|
|
79
116
|
}
|
|
80
117
|
}, [gl, scene])
|
|
81
118
|
|
|
82
119
|
const generate = useCallback(
|
|
83
|
-
async (
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
standardSize?: { w: number; h: number },
|
|
88
|
-
transparent = false,
|
|
89
|
-
) => {
|
|
120
|
+
async (event: ThumbnailGenerateEvent) => {
|
|
121
|
+
const { captureMode, cropRegion, standardSize, cameraPose, requestId } = event
|
|
122
|
+
const snapLevels = event.snapLevels === true
|
|
123
|
+
const transparent = event.transparent === true
|
|
90
124
|
const standardW = standardSize?.w ?? THUMBNAIL_WIDTH
|
|
91
125
|
const standardH = standardSize?.h ?? THUMBNAIL_HEIGHT
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
thumbnailCamera.fov = mainCamera.fov
|
|
107
|
-
thumbnailCamera.near = mainCamera.near
|
|
108
|
-
thumbnailCamera.far = mainCamera.far
|
|
109
|
-
}
|
|
110
|
-
const { width, height } = gl.domElement
|
|
111
|
-
thumbnailCamera.aspect = width / height
|
|
112
|
-
thumbnailCamera.updateProjectionMatrix()
|
|
113
|
-
// The capture camera never joins the scene graph, so its matrixWorld
|
|
114
|
-
// is only refreshed by the render itself — too late for the backdrop
|
|
115
|
-
// uniforms below.
|
|
116
|
-
thumbnailCamera.updateMatrixWorld()
|
|
117
|
-
|
|
118
|
-
const pipeline = pipelineRef.current
|
|
119
|
-
pipeline?.applyEnvironment({
|
|
120
|
-
theme: useViewer.getState().sceneTheme,
|
|
121
|
-
transparent,
|
|
122
|
-
grade: useViewer.getState().shading === 'rendered',
|
|
123
|
-
// Preset/item captures stay clean; scene captures mirror the canvas.
|
|
124
|
-
edges: transparent ? 'off' : useViewer.getState().edges,
|
|
125
|
-
camera: thumbnailCamera,
|
|
126
|
-
})
|
|
127
|
-
|
|
128
|
-
// Capture camera data for snapshot storage
|
|
129
|
-
const pos = mainCamera.position
|
|
130
|
-
let tgt: [number, number, number] | null = null
|
|
131
|
-
if (controls && 'getTarget' in controls) {
|
|
132
|
-
const v = new THREE.Vector3()
|
|
133
|
-
;(controls as any).getTarget(v)
|
|
134
|
-
tgt = [v.x, v.y, v.z]
|
|
135
|
-
}
|
|
136
|
-
const isOrtho = mainCamera instanceof THREE.OrthographicCamera
|
|
137
|
-
const cameraData: SnapshotCameraData = {
|
|
138
|
-
position: [pos.x, pos.y, pos.z],
|
|
139
|
-
target: tgt,
|
|
140
|
-
type: isOrtho ? 'orthographic' : 'perspective',
|
|
141
|
-
...(isOrtho && { zoom: (mainCamera as THREE.OrthographicCamera).zoom }),
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
// For auto-save: snap levels to stacked positions and reset levelMode
|
|
145
|
-
let restoreLevelMode: (() => void) | null = null
|
|
146
|
-
let restoreLevels: () => void = () => {}
|
|
147
|
-
if (snapLevels) {
|
|
148
|
-
const prevMode = useViewer.getState().levelMode
|
|
149
|
-
if (prevMode !== 'stacked') {
|
|
150
|
-
useViewer.getState().setLevelMode('stacked')
|
|
151
|
-
restoreLevelMode = () => useViewer.getState().setLevelMode(prevMode)
|
|
126
|
+
await runSnapshotCapture(
|
|
127
|
+
requestId,
|
|
128
|
+
isGenerating,
|
|
129
|
+
async () => {
|
|
130
|
+
const version = captureVersion.current
|
|
131
|
+
const onCapture = onThumbnailCaptureRef.current
|
|
132
|
+
if (!onCapture) throw new Error('Snapshot storage is unavailable')
|
|
133
|
+
if (cameraPose && event.projectId !== useViewer.getState().projectId)
|
|
134
|
+
throw new Error('The active project changed before capture')
|
|
135
|
+
const thumbnailCamera = thumbnailCameraRef.current
|
|
136
|
+
if (!thumbnailCamera) throw new Error('Snapshot camera is not ready')
|
|
137
|
+
const { camera: mainCamera, controls } = getThree()
|
|
138
|
+
if (cameraPose && (snapLevels || (captureMode && captureMode !== 'standard'))) {
|
|
139
|
+
throw new Error('An explicit snapshot camera requires standard capture mode')
|
|
152
140
|
}
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
// Hide scan, guide, and spawn nodes directly so they are excluded from
|
|
157
|
-
// the thumbnail regardless of whether ScanSystem/GuideSystem listeners
|
|
158
|
-
// are registered. Spawn renders on SCENE_LAYER for occlusion, so the
|
|
159
|
-
// thumbnail camera's layer mask can't filter it either. Returns a
|
|
160
|
-
// function that restores the original visibility.
|
|
161
|
-
const restoreNodeVisibility = temporarilyHideNodeTypes(['scan', 'guide', 'spawn'])
|
|
162
|
-
|
|
163
|
-
// Auto-save shots don't copy the user's mid-edit camera — they re-pose
|
|
164
|
-
// onto the same computed hero angle the published thumbnail uses, so a
|
|
165
|
-
// project's card never shows a half-zoomed working view. Measured after
|
|
166
|
-
// the level snap so stacked positions frame correctly. User-driven
|
|
167
|
-
// captures (captureMode set) keep the exact viewport pose.
|
|
168
|
-
if (snapLevels) {
|
|
169
|
-
const framing = computeHeroFraming()
|
|
170
|
-
if (framing) {
|
|
171
|
-
const pose = heroCameraPose({
|
|
172
|
-
boxes: framing.boxes,
|
|
173
|
-
aim: framing.aim,
|
|
174
|
-
azimuthRad: framing.azimuthRad,
|
|
175
|
-
aspect: width / height,
|
|
176
|
-
})
|
|
177
|
-
thumbnailCamera.position.set(pose.position[0], pose.position[1], pose.position[2])
|
|
178
|
-
thumbnailCamera.lookAt(pose.target[0], pose.target[1], pose.target[2])
|
|
179
|
-
thumbnailCamera.updateMatrixWorld()
|
|
180
|
-
pipeline?.applyEnvironment({
|
|
181
|
-
theme: useViewer.getState().sceneTheme,
|
|
182
|
-
transparent,
|
|
183
|
-
grade: useViewer.getState().shading === 'rendered',
|
|
184
|
-
edges: transparent ? 'off' : useViewer.getState().edges,
|
|
185
|
-
camera: thumbnailCamera,
|
|
186
|
-
})
|
|
187
|
-
cameraData.position = pose.position
|
|
188
|
-
cameraData.target = pose.target
|
|
141
|
+
if (cameraPose && !pipelineRef.current) {
|
|
142
|
+
throw new Error('Snapshot renderer is not ready. Try again.')
|
|
189
143
|
}
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
let blob: Blob
|
|
193
|
-
|
|
194
|
-
if (pipeline) {
|
|
195
|
-
let capturePromise: ReturnType<SnapshotPipeline['capture']>
|
|
196
144
|
|
|
197
|
-
//
|
|
198
|
-
//
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
145
|
+
// Copy the main camera's transform and projection so the thumbnail
|
|
146
|
+
// matches exactly what the user sees in the viewport.
|
|
147
|
+
thumbnailCamera.position.copy(mainCamera.position)
|
|
148
|
+
thumbnailCamera.quaternion.copy(mainCamera.quaternion)
|
|
149
|
+
if (mainCamera instanceof THREE.PerspectiveCamera) {
|
|
150
|
+
thumbnailCamera.fov = mainCamera.fov
|
|
151
|
+
thumbnailCamera.near = mainCamera.near
|
|
152
|
+
thumbnailCamera.far = mainCamera.far
|
|
153
|
+
}
|
|
154
|
+
const { width, height } = gl.domElement
|
|
155
|
+
thumbnailCamera.aspect = width / height
|
|
156
|
+
if (cameraPose) {
|
|
157
|
+
applySnapshotCapturePose(
|
|
158
|
+
thumbnailCamera,
|
|
159
|
+
cameraPose,
|
|
160
|
+
{ width, height },
|
|
161
|
+
{
|
|
162
|
+
w: standardW,
|
|
163
|
+
h: standardH,
|
|
164
|
+
},
|
|
165
|
+
)
|
|
166
|
+
}
|
|
167
|
+
thumbnailCamera.updateProjectionMatrix()
|
|
168
|
+
// The capture camera never joins the scene graph, so its matrixWorld
|
|
169
|
+
// is only refreshed by the render itself — too late for the backdrop
|
|
170
|
+
// uniforms below.
|
|
171
|
+
thumbnailCamera.updateMatrixWorld()
|
|
172
|
+
|
|
173
|
+
const pipeline = pipelineRef.current
|
|
174
|
+
pipeline?.applyEnvironment({
|
|
175
|
+
theme: useViewer.getState().sceneTheme,
|
|
176
|
+
transparent,
|
|
177
|
+
grade: useViewer.getState().shading === 'rendered',
|
|
178
|
+
// Preset/item captures stay clean; scene captures mirror the canvas.
|
|
179
|
+
edges: transparent ? 'off' : useViewer.getState().edges,
|
|
180
|
+
camera: thumbnailCamera,
|
|
181
|
+
})
|
|
182
|
+
|
|
183
|
+
// Capture camera data for snapshot storage
|
|
184
|
+
const pos = cameraPose ? thumbnailCamera.position : mainCamera.position
|
|
185
|
+
let tgt: [number, number, number] | null = null
|
|
186
|
+
if (!cameraPose && controls && 'getTarget' in controls) {
|
|
187
|
+
const v = new THREE.Vector3()
|
|
188
|
+
;(controls as any).getTarget(v)
|
|
189
|
+
tgt = [v.x, v.y, v.z]
|
|
190
|
+
} else {
|
|
191
|
+
// Walk / drone captures run without orbit controls, so there is no orbit
|
|
192
|
+
// target to read. Synthesize one down the view axis — otherwise the
|
|
193
|
+
// saved snapshot carries no framing to return to.
|
|
194
|
+
const look = new THREE.Vector3(0, 0, -1)
|
|
195
|
+
.applyQuaternion(cameraPose ? thumbnailCamera.quaternion : mainCamera.quaternion)
|
|
196
|
+
.multiplyScalar(FIRST_PERSON_TARGET_DISTANCE)
|
|
197
|
+
.add(pos)
|
|
198
|
+
tgt = [look.x, look.y, look.z]
|
|
199
|
+
}
|
|
200
|
+
const isOrtho = !cameraPose && mainCamera instanceof THREE.OrthographicCamera
|
|
201
|
+
const cameraData: SnapshotCameraData = {
|
|
202
|
+
...(requestId && { requestId }),
|
|
203
|
+
position: [pos.x, pos.y, pos.z],
|
|
204
|
+
...(cameraPose && {
|
|
205
|
+
quaternion: [...cameraPose.quaternion] as [number, number, number, number],
|
|
206
|
+
fov: cameraPose.fov,
|
|
207
|
+
}),
|
|
208
|
+
target: tgt,
|
|
209
|
+
type: isOrtho ? 'orthographic' : 'perspective',
|
|
210
|
+
...(isOrtho && { zoom: (mainCamera as THREE.OrthographicCamera).zoom }),
|
|
214
211
|
}
|
|
215
212
|
|
|
216
|
-
const
|
|
217
|
-
|
|
213
|
+
const capturePromise = captureSnapshotScene((restore) => {
|
|
214
|
+
if (snapLevels) {
|
|
215
|
+
const prevMode = useViewer.getState().levelMode
|
|
216
|
+
if (prevMode !== 'stacked') {
|
|
217
|
+
restore(() => useViewer.getState().setLevelMode(prevMode))
|
|
218
|
+
useViewer.getState().setLevelMode('stacked')
|
|
219
|
+
}
|
|
220
|
+
restore(snapLevelsToTruePositions())
|
|
221
|
+
}
|
|
222
|
+
restore(temporarilyHideNodeTypes(['scan', 'guide', 'spawn']))
|
|
223
|
+
|
|
224
|
+
// Auto-save uses the published hero framing. An authored shot keeps
|
|
225
|
+
// its own camera while sharing the same true level positions.
|
|
226
|
+
if (snapLevels) {
|
|
227
|
+
const framing = computeHeroFraming()
|
|
228
|
+
if (framing) {
|
|
229
|
+
const pose = heroCameraPose({
|
|
230
|
+
boxes: framing.boxes,
|
|
231
|
+
aim: framing.aim,
|
|
232
|
+
azimuthRad: framing.azimuthRad,
|
|
233
|
+
aspect: width / height,
|
|
234
|
+
})
|
|
235
|
+
thumbnailCamera.position.set(pose.position[0], pose.position[1], pose.position[2])
|
|
236
|
+
thumbnailCamera.lookAt(pose.target[0], pose.target[1], pose.target[2])
|
|
237
|
+
thumbnailCamera.updateMatrixWorld()
|
|
238
|
+
pipeline?.applyEnvironment({
|
|
239
|
+
theme: useViewer.getState().sceneTheme,
|
|
240
|
+
transparent,
|
|
241
|
+
grade: useViewer.getState().shading === 'rendered',
|
|
242
|
+
edges: transparent ? 'off' : useViewer.getState().edges,
|
|
243
|
+
camera: thumbnailCamera,
|
|
244
|
+
})
|
|
245
|
+
cameraData.position = pose.position
|
|
246
|
+
cameraData.target = pose.target
|
|
247
|
+
}
|
|
248
|
+
}
|
|
218
249
|
|
|
219
|
-
|
|
220
|
-
cameraData.resolution = { w: result.outW, h: result.outH }
|
|
221
|
-
} else {
|
|
222
|
-
// Fallback: plain render directly to the canvas
|
|
223
|
-
try {
|
|
250
|
+
restore(() => emitter.emit('thumbnail:after-capture', undefined))
|
|
224
251
|
emitter.emit('thumbnail:before-capture', undefined)
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
252
|
+
if (cameraPose) {
|
|
253
|
+
restore(snapLevelsToTruePositions())
|
|
254
|
+
restore(temporarilyShowShadowOnly(scene))
|
|
255
|
+
const wallMaterials = new Map<THREE.Mesh, THREE.Material | THREE.Material[]>()
|
|
256
|
+
restore(() => {
|
|
257
|
+
for (const [mesh, material] of wallMaterials) mesh.material = material
|
|
258
|
+
})
|
|
259
|
+
const state = useScene.getState()
|
|
260
|
+
const viewer = useViewer.getState()
|
|
261
|
+
for (const id of sceneRegistry.byType.wall ?? []) {
|
|
262
|
+
const node = state.nodes[id as AnyNodeId]
|
|
263
|
+
const mesh = sceneRegistry.nodes.get(id) as THREE.Mesh | undefined
|
|
264
|
+
if (node?.type !== 'wall' || !mesh?.isMesh) continue
|
|
265
|
+
wallMaterials.set(mesh, mesh.material)
|
|
266
|
+
mesh.material = getVisibleWallMaterials(
|
|
267
|
+
node,
|
|
268
|
+
viewer.shading,
|
|
269
|
+
viewer.textures,
|
|
270
|
+
viewer.colorPreset,
|
|
271
|
+
viewer.sceneTheme,
|
|
272
|
+
state.materials,
|
|
273
|
+
)
|
|
274
|
+
}
|
|
275
|
+
}
|
|
235
276
|
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
)
|
|
249
|
-
} else if (captureMode === 'area' && cropRegion) {
|
|
250
|
-
const sx = Math.round(cropRegion.x * width)
|
|
251
|
-
const sy = Math.round(cropRegion.y * height)
|
|
252
|
-
outW = Math.round(cropRegion.width * width)
|
|
253
|
-
outH = Math.round(cropRegion.height * height)
|
|
254
|
-
const offscreen = document.createElement('canvas')
|
|
255
|
-
offscreen.width = outW
|
|
256
|
-
offscreen.height = outH
|
|
257
|
-
offscreen
|
|
258
|
-
.getContext('2d')!
|
|
259
|
-
.drawImage(gl.domElement, sx, sy, outW, outH, 0, 0, outW, outH)
|
|
260
|
-
blob = await new Promise<Blob>((resolve, reject) =>
|
|
261
|
-
offscreen.toBlob(
|
|
262
|
-
(b) => (b ? resolve(b) : reject(new Error('Canvas capture failed'))),
|
|
263
|
-
'image/png',
|
|
264
|
-
),
|
|
265
|
-
)
|
|
277
|
+
if (pipeline) return pipeline.capture({ captureMode, cropRegion, standardSize })
|
|
278
|
+
gl.render(scene, thumbnailCamera)
|
|
279
|
+
return undefined
|
|
280
|
+
})
|
|
281
|
+
|
|
282
|
+
let blob: Blob
|
|
283
|
+
if (pipeline) {
|
|
284
|
+
const result = await capturePromise
|
|
285
|
+
if (!result) throw new Error('Snapshot capture produced no image')
|
|
286
|
+
blob = result.blob
|
|
287
|
+
if (captureMode !== undefined) cameraData.captureMode = captureMode
|
|
288
|
+
cameraData.resolution = { w: result.outW, h: result.outH }
|
|
266
289
|
} else {
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
let
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
290
|
+
await capturePromise
|
|
291
|
+
let outW: number
|
|
292
|
+
let outH: number
|
|
293
|
+
|
|
294
|
+
if (captureMode === 'viewport') {
|
|
295
|
+
;({ w: outW, h: outH } = clampSnapshotSize(width, height))
|
|
296
|
+
const offscreen = document.createElement('canvas')
|
|
297
|
+
offscreen.width = outW
|
|
298
|
+
offscreen.height = outH
|
|
299
|
+
const ctx = offscreen.getContext('2d')!
|
|
300
|
+
if (outW !== width || outH !== height) ctx.imageSmoothingQuality = 'high'
|
|
301
|
+
ctx.drawImage(gl.domElement, 0, 0, width, height, 0, 0, outW, outH)
|
|
302
|
+
blob = await new Promise<Blob>((resolve, reject) =>
|
|
303
|
+
offscreen.toBlob(
|
|
304
|
+
(b) => (b ? resolve(b) : reject(new Error('Canvas capture failed'))),
|
|
305
|
+
SNAPSHOT_MIME,
|
|
306
|
+
SNAPSHOT_QUALITY,
|
|
307
|
+
),
|
|
308
|
+
)
|
|
309
|
+
} else if (captureMode === 'area' && cropRegion) {
|
|
310
|
+
const sx = Math.round(cropRegion.x * width)
|
|
311
|
+
const sy = Math.round(cropRegion.y * height)
|
|
312
|
+
const sourceW = Math.round(cropRegion.width * width)
|
|
313
|
+
const sourceH = Math.round(cropRegion.height * height)
|
|
314
|
+
;({ w: outW, h: outH } = clampSnapshotSize(sourceW, sourceH))
|
|
315
|
+
const offscreen = document.createElement('canvas')
|
|
316
|
+
offscreen.width = outW
|
|
317
|
+
offscreen.height = outH
|
|
318
|
+
const ctx = offscreen.getContext('2d')!
|
|
319
|
+
if (outW !== sourceW || outH !== sourceH) ctx.imageSmoothingQuality = 'high'
|
|
320
|
+
ctx.drawImage(gl.domElement, sx, sy, sourceW, sourceH, 0, 0, outW, outH)
|
|
321
|
+
blob = await new Promise<Blob>((resolve, reject) =>
|
|
322
|
+
offscreen.toBlob(
|
|
323
|
+
(b) => (b ? resolve(b) : reject(new Error('Canvas capture failed'))),
|
|
324
|
+
SNAPSHOT_MIME,
|
|
325
|
+
SNAPSHOT_QUALITY,
|
|
326
|
+
),
|
|
327
|
+
)
|
|
328
|
+
} else {
|
|
329
|
+
const srcAspect = width / height
|
|
330
|
+
const dstAspect = standardW / standardH
|
|
331
|
+
let sx = 0,
|
|
332
|
+
sy = 0,
|
|
333
|
+
sWidth = width,
|
|
334
|
+
sHeight = height
|
|
335
|
+
if (srcAspect > dstAspect) {
|
|
336
|
+
sWidth = Math.round(height * dstAspect)
|
|
337
|
+
sx = Math.round((width - sWidth) / 2)
|
|
338
|
+
} else if (srcAspect < dstAspect) {
|
|
339
|
+
sHeight = Math.round(width / dstAspect)
|
|
340
|
+
sy = Math.round((height - sHeight) / 2)
|
|
341
|
+
}
|
|
342
|
+
outW = standardW
|
|
343
|
+
outH = standardH
|
|
344
|
+
const offscreen = document.createElement('canvas')
|
|
345
|
+
offscreen.width = outW
|
|
346
|
+
offscreen.height = outH
|
|
347
|
+
offscreen
|
|
348
|
+
.getContext('2d')!
|
|
349
|
+
.drawImage(gl.domElement, sx, sy, sWidth, sHeight, 0, 0, outW, outH)
|
|
350
|
+
blob = await new Promise<Blob>((resolve, reject) =>
|
|
351
|
+
offscreen.toBlob(
|
|
352
|
+
(b) => (b ? resolve(b) : reject(new Error('Canvas capture failed'))),
|
|
353
|
+
SNAPSHOT_MIME,
|
|
354
|
+
SNAPSHOT_QUALITY,
|
|
355
|
+
),
|
|
356
|
+
)
|
|
279
357
|
}
|
|
280
|
-
outW = standardW
|
|
281
|
-
outH = standardH
|
|
282
|
-
const offscreen = document.createElement('canvas')
|
|
283
|
-
offscreen.width = outW
|
|
284
|
-
offscreen.height = outH
|
|
285
|
-
offscreen
|
|
286
|
-
.getContext('2d')!
|
|
287
|
-
.drawImage(gl.domElement, sx, sy, sWidth, sHeight, 0, 0, outW, outH)
|
|
288
|
-
blob = await new Promise<Blob>((resolve, reject) =>
|
|
289
|
-
offscreen.toBlob(
|
|
290
|
-
(b) => (b ? resolve(b) : reject(new Error('Canvas capture failed'))),
|
|
291
|
-
'image/png',
|
|
292
|
-
),
|
|
293
|
-
)
|
|
294
|
-
}
|
|
295
358
|
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
359
|
+
if (captureMode !== undefined) cameraData.captureMode = captureMode
|
|
360
|
+
cameraData.resolution = { w: outW, h: outH }
|
|
361
|
+
}
|
|
299
362
|
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
363
|
+
if (
|
|
364
|
+
version !== captureVersion.current ||
|
|
365
|
+
thumbnailCamera !== thumbnailCameraRef.current
|
|
366
|
+
) {
|
|
367
|
+
throw new Error('The scene changed during capture. Try again.')
|
|
368
|
+
}
|
|
369
|
+
if (cameraPose && event.projectId !== useViewer.getState().projectId) {
|
|
370
|
+
throw new Error('The active project changed during capture')
|
|
371
|
+
}
|
|
372
|
+
await onCapture(blob, cameraData)
|
|
373
|
+
},
|
|
374
|
+
(failure) => emitter.emit('snapshot:capture-failed', failure),
|
|
375
|
+
)
|
|
306
376
|
},
|
|
307
|
-
[gl, scene,
|
|
377
|
+
[gl, scene, getThree],
|
|
308
378
|
)
|
|
309
379
|
|
|
310
380
|
// Thumbnail request via emitter. Two call shapes:
|
|
@@ -314,30 +384,23 @@ export const ThumbnailGenerator = ({ onThumbnailCapture }: ThumbnailGeneratorPro
|
|
|
314
384
|
// to their true positions first for a consistent auto-thumbnail angle.
|
|
315
385
|
// The caller owns policy (when to fire, whether the tab is visible).
|
|
316
386
|
useEffect(() => {
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
// and project thumbnails — composite the theme backdrop + sky.
|
|
327
|
-
transparent?: boolean
|
|
328
|
-
}) => {
|
|
329
|
-
await generate(
|
|
330
|
-
event.snapLevels === true,
|
|
331
|
-
event.captureMode,
|
|
332
|
-
event.cropRegion,
|
|
333
|
-
event.standardSize,
|
|
334
|
-
event.transparent === true,
|
|
387
|
+
const handleGenerateThumbnail = async (event: ThumbnailGenerateEvent) => {
|
|
388
|
+
// A saved-frame notification can enqueue the next shot frame before
|
|
389
|
+
// its predecessor's host callback returns and releases the renderer.
|
|
390
|
+
await enqueueSnapshotCapture(
|
|
391
|
+
captureQueue.current,
|
|
392
|
+
captureVersion,
|
|
393
|
+
event,
|
|
394
|
+
generate,
|
|
395
|
+
(failure) => emitter.emit('snapshot:capture-failed', failure),
|
|
335
396
|
)
|
|
336
397
|
}
|
|
337
398
|
|
|
338
399
|
emitter.on('camera-controls:generate-thumbnail', handleGenerateThumbnail)
|
|
339
|
-
return () =>
|
|
340
|
-
|
|
400
|
+
return () => {
|
|
401
|
+
emitter.off('camera-controls:generate-thumbnail', handleGenerateThumbnail)
|
|
402
|
+
}
|
|
403
|
+
}, [generate])
|
|
341
404
|
|
|
342
405
|
// Go-to-camera: animate camera to a saved snapshot position/target
|
|
343
406
|
useEffect(() => {
|