@pascal-app/editor 1.0.0-beta.5 → 1.0.1
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/bake-exporter.tsx +5 -0
- package/src/components/editor/bake-thumbnail.tsx +4 -1
- 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 +9 -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 +180 -12
- package/src/components/editor/floating-action-menu.tsx +55 -51
- package/src/components/editor/floorplan-panel.tsx +99 -15
- package/src/components/editor/grid.tsx +12 -7
- package/src/components/editor/group-actions.ts +34 -50
- 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/handle-placement.test.ts +111 -0
- package/src/components/editor/handles/handle-placement.ts +69 -0
- package/src/components/editor/handles/linear-resize-drag.ts +78 -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 +23 -3
- package/src/components/editor/index.tsx +151 -24
- package/src/components/editor/node-action-menu.tsx +14 -1
- package/src/components/editor/node-arrow-handles.tsx +67 -119
- package/src/components/editor/selection-manager.tsx +153 -42
- package/src/components/editor/snapshot-capture-overlay.tsx +298 -39
- 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 -239
- package/src/components/editor/use-floorplan-background-placement.ts +53 -5
- package/src/components/editor/wall-move-side-handles.tsx +299 -7
- 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-action-menu.tsx +12 -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 +226 -35
- package/src/components/procedural-items/inspector.tsx +223 -0
- 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/ceiling/ceiling-system.tsx +3 -1
- 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/item-surface-placement.test.ts +175 -0
- package/src/components/tools/item/item-surface-strategy-resolver.test.ts +210 -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 +331 -4
- package/src/components/tools/item/placement-strategies.ts +304 -171
- 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 +10 -0
- package/src/components/tools/item/shelf-surface-strategy-resolver.test.ts +257 -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 +116 -44
- package/src/components/tools/item/use-placement-coordinator.tsx +672 -256
- package/src/components/tools/registered-tool-install-lifecycle.test.tsx +256 -0
- package/src/components/tools/registry/item-surface-child-frame.test.ts +90 -0
- package/src/components/tools/registry/item-surface-move.test.ts +501 -0
- package/src/components/tools/registry/item-surface-move.ts +441 -0
- package/src/components/tools/registry/item-surface-resolver.test.ts +257 -0
- package/src/components/tools/registry/move-registry-node-tool.test.ts +10 -0
- package/src/components/tools/registry/move-registry-node-tool.tsx +505 -164
- 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 +57 -7
- 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 +56 -17
- package/src/components/tools/shared/shelf-stickiness.ts +54 -0
- package/src/components/tools/shared/surface-hit.test.ts +56 -0
- package/src/components/tools/shared/surface-hit.ts +23 -0
- package/src/components/tools/shared/surface-rejection.test.tsx +106 -0
- package/src/components/tools/shared/surface-rejection.tsx +29 -0
- 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 +320 -2
- package/src/components/tools/wall/wall-drafting.ts +23 -114
- package/src/components/tools/zone/zone-tool.tsx +13 -4
- 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 +63 -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/metric-control.tsx +29 -13
- 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 +44 -21
- 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 +103 -10
- 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 +266 -0
- package/src/components/ui/panels/multi-selection-panel.tsx +48 -37
- 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/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 +35 -164
- package/src/components/ui/panels/reference-panel.tsx +34 -4
- package/src/components/ui/primitives/checkbox.tsx +34 -0
- 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/settings-panel/index.tsx +502 -31
- package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +20 -23
- package/src/components/ui/sidebar/panels/settings-panel/print-export-button.test.ts +167 -0
- package/src/components/ui/sidebar/panels/settings-panel/print-export-button.tsx +128 -0
- package/src/components/ui/sidebar/panels/site-panel/building-tree-node.tsx +8 -1
- package/src/components/ui/sidebar/panels/site-panel/dormer-tree-node.tsx +19 -5
- package/src/components/ui/sidebar/panels/site-panel/index.tsx +222 -50
- package/src/components/ui/sidebar/panels/site-panel/item-tree-node.tsx +2 -7
- 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/shelf-tree-node.tsx +3 -8
- 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 +20 -7
- package/src/components/ui/sidebar/panels/site-panel/unit-tree-node.tsx +224 -0
- package/src/components/ui/sidebar/panels/site-panel/zone-membership-checkbox.tsx +22 -0
- package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +24 -7
- package/src/components/ui/sidebar/tab-bar.tsx +2 -0
- package/src/components/ui/sidebar/use-plugin-panels.tsx +17 -2
- package/src/components/ui/snap-target-badge.tsx +2 -0
- package/src/components/viewer/viewer-scene-header.tsx +5 -1
- package/src/components/viewer/viewer-stage.tsx +4 -1
- package/src/components/viewer/viewer-units-panel.tsx +116 -0
- package/src/components/viewer-overlay.tsx +1 -22
- 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 +73 -1
- package/src/hooks/use-auto-save.ts +76 -5
- package/src/hooks/use-ceiling-events.test.ts +144 -0
- package/src/hooks/use-ceiling-events.ts +5 -1
- 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 +132 -63
- package/src/hooks/use-registered-tool-enabled.ts +20 -0
- package/src/hooks/use-save-shortcut.ts +33 -0
- package/src/index.tsx +87 -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/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/export-texture-utils.test.ts +167 -0
- package/src/lib/export-texture-utils.ts +121 -0
- package/src/lib/export-user-data.test.ts +48 -0
- package/src/lib/export-user-data.ts +69 -0
- 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 +536 -2
- package/src/lib/floorplan/floorplan-export.tsx +120 -19
- 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.ts +5 -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/fresh-planar-placement.ts +13 -5
- package/src/lib/glb-export-serialize.test.ts +280 -0
- package/src/lib/glb-export.test.ts +1375 -21
- package/src/lib/glb-export.ts +781 -106
- package/src/lib/grid-event-presentation.ts +16 -0
- package/src/lib/history.test.ts +528 -147
- package/src/lib/history.ts +81 -7
- package/src/lib/host-tree-children.test.ts +27 -0
- package/src/lib/host-tree-children.ts +79 -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-duplication.test.ts +73 -0
- package/src/lib/level-duplication.ts +32 -2
- package/src/lib/level-print-export.test.ts +484 -0
- package/src/lib/level-print-export.ts +526 -0
- package/src/lib/linear-display.test.ts +59 -0
- package/src/lib/linear-display.ts +39 -0
- package/src/lib/local-guide-image.test.ts +24 -0
- package/src/lib/local-guide-image.ts +34 -1
- 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/model-export.ts +29 -0
- package/src/lib/node-action-movement.ts +21 -0
- package/src/lib/paint-preview-owner.ts +71 -0
- 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 +455 -0
- package/src/lib/portable-export.ts +1042 -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 +379 -0
- package/src/lib/print-shell-compiler-manifold-worker.ts +200 -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 +38 -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-clipboard.ts +1 -1
- package/src/lib/scene.ts +22 -8
- package/src/lib/selection-routing.test.ts +105 -1
- package/src/lib/selection-routing.ts +19 -0
- package/src/lib/send-to-app.test.ts +108 -0
- package/src/lib/send-to-app.ts +121 -0
- package/src/lib/sfx/movement-tick.ts +37 -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/surface-attachment.ts +77 -0
- package/src/lib/units.ts +254 -0
- package/src/lib/usdz-export.ts +65 -0
- package/src/lib/use-linear-display.ts +8 -32
- 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-handle-group.ts +4 -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-send-to-app.ts +21 -0
- package/src/components/tools/roof/roof-tool.tsx +0 -656
- package/src/components/ui/helpers/roof-helper.tsx +0 -14
|
@@ -1,11 +1,19 @@
|
|
|
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
|
+
refreshIsolation,
|
|
9
17
|
SNAPSHOT_MAX_EDGE,
|
|
10
18
|
SNAPSHOT_MIME,
|
|
11
19
|
SNAPSHOT_QUALITY,
|
|
@@ -14,6 +22,8 @@ import {
|
|
|
14
22
|
THUMBNAIL_HEIGHT,
|
|
15
23
|
THUMBNAIL_WIDTH,
|
|
16
24
|
temporarilyHideNodeTypes,
|
|
25
|
+
temporarilyShowShadowOnly,
|
|
26
|
+
useSceneAtmosphere,
|
|
17
27
|
useViewer,
|
|
18
28
|
} from '@pascal-app/viewer'
|
|
19
29
|
import type { CameraControls } from '@react-three/drei'
|
|
@@ -22,9 +32,19 @@ import { useCallback, useEffect, useRef } from 'react'
|
|
|
22
32
|
import * as THREE from 'three'
|
|
23
33
|
import type { WebGPURenderer } from 'three/webgpu'
|
|
24
34
|
import { EDITOR_LAYER } from '../../lib/constants'
|
|
35
|
+
import {
|
|
36
|
+
applySnapshotCapturePose,
|
|
37
|
+
captureSnapshotScene,
|
|
38
|
+
createSnapshotQueue,
|
|
39
|
+
enqueueSnapshotCapture,
|
|
40
|
+
runSnapshotCapture,
|
|
41
|
+
} from './snapshot-capture'
|
|
25
42
|
|
|
26
43
|
export interface SnapshotCameraData {
|
|
44
|
+
requestId?: string
|
|
27
45
|
position: [number, number, number]
|
|
46
|
+
quaternion?: [number, number, number, number]
|
|
47
|
+
fov?: number
|
|
28
48
|
target: [number, number, number] | null
|
|
29
49
|
type?: 'perspective' | 'orthographic'
|
|
30
50
|
zoom?: number
|
|
@@ -36,6 +56,9 @@ interface ThumbnailGeneratorProps {
|
|
|
36
56
|
onThumbnailCapture?: (blob: Blob, cameraData: SnapshotCameraData) => void
|
|
37
57
|
}
|
|
38
58
|
|
|
59
|
+
/** Metres ahead of a controls-less camera to place the stored snapshot target. */
|
|
60
|
+
const FIRST_PERSON_TARGET_DISTANCE = 8
|
|
61
|
+
|
|
39
62
|
function clampSnapshotSize(width: number, height: number): { w: number; h: number } {
|
|
40
63
|
const maxEdge = Math.max(width, height)
|
|
41
64
|
if (maxEdge <= SNAPSHOT_MAX_EDGE) return { w: width, h: height }
|
|
@@ -47,20 +70,24 @@ function clampSnapshotSize(width: number, height: number): { w: number; h: numbe
|
|
|
47
70
|
export const ThumbnailGenerator = ({ onThumbnailCapture }: ThumbnailGeneratorProps) => {
|
|
48
71
|
const gl = useThree((state) => state.gl)
|
|
49
72
|
const scene = useThree((state) => state.scene)
|
|
50
|
-
const
|
|
73
|
+
const atmosphere = useSceneAtmosphere()
|
|
74
|
+
const getThree = useThree((state) => state.get)
|
|
51
75
|
const controls = useThree((state) => state.controls) as CameraControls | null
|
|
52
76
|
const isGenerating = useRef(false)
|
|
77
|
+
const captureQueue = useRef(createSnapshotQueue())
|
|
53
78
|
const onThumbnailCaptureRef = useRef(onThumbnailCapture)
|
|
54
79
|
|
|
55
80
|
const thumbnailCameraRef = useRef<THREE.PerspectiveCamera | null>(null)
|
|
56
81
|
const pipelineRef = useRef<SnapshotPipeline | null>(null)
|
|
82
|
+
const captureVersion = useRef(0)
|
|
57
83
|
|
|
58
84
|
useEffect(() => {
|
|
59
85
|
onThumbnailCaptureRef.current = onThumbnailCapture
|
|
60
86
|
}, [onThumbnailCapture])
|
|
61
87
|
|
|
62
|
-
//
|
|
88
|
+
// Reuse the camera and snapshot graph until the active atmosphere source changes.
|
|
63
89
|
useEffect(() => {
|
|
90
|
+
captureVersion.current += 1
|
|
64
91
|
const cam = new THREE.PerspectiveCamera(60, THUMBNAIL_WIDTH / THUMBNAIL_HEIGHT, 0.1, 1000)
|
|
65
92
|
cam.layers.disable(EDITOR_LAYER)
|
|
66
93
|
cam.layers.disable(GRID_LAYER)
|
|
@@ -73,6 +100,7 @@ export const ThumbnailGenerator = ({ onThumbnailCapture }: ThumbnailGeneratorPro
|
|
|
73
100
|
renderer: gl as unknown as WebGPURenderer,
|
|
74
101
|
scene,
|
|
75
102
|
camera: cam,
|
|
103
|
+
atmosphere,
|
|
76
104
|
})
|
|
77
105
|
if (!mounted) {
|
|
78
106
|
pipeline?.dispose()
|
|
@@ -85,242 +113,277 @@ export const ThumbnailGenerator = ({ onThumbnailCapture }: ThumbnailGeneratorPro
|
|
|
85
113
|
|
|
86
114
|
return () => {
|
|
87
115
|
mounted = false
|
|
116
|
+
captureVersion.current += 1
|
|
117
|
+
thumbnailCameraRef.current = null
|
|
88
118
|
pipelineRef.current?.dispose()
|
|
89
119
|
pipelineRef.current = null
|
|
90
120
|
}
|
|
91
|
-
}, [gl, scene])
|
|
121
|
+
}, [gl, scene, atmosphere])
|
|
92
122
|
|
|
93
123
|
const generate = useCallback(
|
|
94
|
-
async (
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
standardSize?: { w: number; h: number },
|
|
99
|
-
transparent = false,
|
|
100
|
-
) => {
|
|
124
|
+
async (event: ThumbnailGenerateEvent) => {
|
|
125
|
+
const { captureMode, cropRegion, standardSize, cameraPose, requestId } = event
|
|
126
|
+
const snapLevels = event.snapLevels === true
|
|
127
|
+
const transparent = event.transparent === true
|
|
101
128
|
const standardW = standardSize?.w ?? THUMBNAIL_WIDTH
|
|
102
129
|
const standardH = standardSize?.h ?? THUMBNAIL_HEIGHT
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
thumbnailCamera.fov = mainCamera.fov
|
|
118
|
-
thumbnailCamera.near = mainCamera.near
|
|
119
|
-
thumbnailCamera.far = mainCamera.far
|
|
120
|
-
}
|
|
121
|
-
const { width, height } = gl.domElement
|
|
122
|
-
thumbnailCamera.aspect = width / height
|
|
123
|
-
thumbnailCamera.updateProjectionMatrix()
|
|
124
|
-
// The capture camera never joins the scene graph, so its matrixWorld
|
|
125
|
-
// is only refreshed by the render itself — too late for the backdrop
|
|
126
|
-
// uniforms below.
|
|
127
|
-
thumbnailCamera.updateMatrixWorld()
|
|
128
|
-
|
|
129
|
-
const pipeline = pipelineRef.current
|
|
130
|
-
pipeline?.applyEnvironment({
|
|
131
|
-
theme: useViewer.getState().sceneTheme,
|
|
132
|
-
transparent,
|
|
133
|
-
grade: useViewer.getState().shading === 'rendered',
|
|
134
|
-
// Preset/item captures stay clean; scene captures mirror the canvas.
|
|
135
|
-
edges: transparent ? 'off' : useViewer.getState().edges,
|
|
136
|
-
camera: thumbnailCamera,
|
|
137
|
-
})
|
|
138
|
-
|
|
139
|
-
// Capture camera data for snapshot storage
|
|
140
|
-
const pos = mainCamera.position
|
|
141
|
-
let tgt: [number, number, number] | null = null
|
|
142
|
-
if (controls && 'getTarget' in controls) {
|
|
143
|
-
const v = new THREE.Vector3()
|
|
144
|
-
;(controls as any).getTarget(v)
|
|
145
|
-
tgt = [v.x, v.y, v.z]
|
|
146
|
-
}
|
|
147
|
-
const isOrtho = mainCamera instanceof THREE.OrthographicCamera
|
|
148
|
-
const cameraData: SnapshotCameraData = {
|
|
149
|
-
position: [pos.x, pos.y, pos.z],
|
|
150
|
-
target: tgt,
|
|
151
|
-
type: isOrtho ? 'orthographic' : 'perspective',
|
|
152
|
-
...(isOrtho && { zoom: (mainCamera as THREE.OrthographicCamera).zoom }),
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
// For auto-save: snap levels to stacked positions and reset levelMode
|
|
156
|
-
let restoreLevelMode: (() => void) | null = null
|
|
157
|
-
let restoreLevels: () => void = () => {}
|
|
158
|
-
if (snapLevels) {
|
|
159
|
-
const prevMode = useViewer.getState().levelMode
|
|
160
|
-
if (prevMode !== 'stacked') {
|
|
161
|
-
useViewer.getState().setLevelMode('stacked')
|
|
162
|
-
restoreLevelMode = () => useViewer.getState().setLevelMode(prevMode)
|
|
130
|
+
await runSnapshotCapture(
|
|
131
|
+
requestId,
|
|
132
|
+
isGenerating,
|
|
133
|
+
async () => {
|
|
134
|
+
const version = captureVersion.current
|
|
135
|
+
const onCapture = onThumbnailCaptureRef.current
|
|
136
|
+
if (!onCapture) throw new Error('Snapshot storage is unavailable')
|
|
137
|
+
if (cameraPose && event.projectId !== useViewer.getState().projectId)
|
|
138
|
+
throw new Error('The active project changed before capture')
|
|
139
|
+
const thumbnailCamera = thumbnailCameraRef.current
|
|
140
|
+
if (!thumbnailCamera) throw new Error('Snapshot camera is not ready')
|
|
141
|
+
const { camera: mainCamera, controls } = getThree()
|
|
142
|
+
if (cameraPose && (snapLevels || (captureMode && captureMode !== 'standard'))) {
|
|
143
|
+
throw new Error('An explicit snapshot camera requires standard capture mode')
|
|
163
144
|
}
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
// Hide scan, guide, and spawn nodes directly so they are excluded from
|
|
168
|
-
// the thumbnail regardless of whether ScanSystem/GuideSystem listeners
|
|
169
|
-
// are registered. Spawn renders on SCENE_LAYER for occlusion, so the
|
|
170
|
-
// thumbnail camera's layer mask can't filter it either. Returns a
|
|
171
|
-
// function that restores the original visibility.
|
|
172
|
-
const restoreNodeVisibility = temporarilyHideNodeTypes(['scan', 'guide', 'spawn'])
|
|
173
|
-
|
|
174
|
-
// Auto-save shots don't copy the user's mid-edit camera — they re-pose
|
|
175
|
-
// onto the same computed hero angle the published thumbnail uses, so a
|
|
176
|
-
// project's card never shows a half-zoomed working view. Measured after
|
|
177
|
-
// the level snap so stacked positions frame correctly. User-driven
|
|
178
|
-
// captures (captureMode set) keep the exact viewport pose.
|
|
179
|
-
if (snapLevels) {
|
|
180
|
-
const framing = computeHeroFraming()
|
|
181
|
-
if (framing) {
|
|
182
|
-
const pose = heroCameraPose({
|
|
183
|
-
boxes: framing.boxes,
|
|
184
|
-
aim: framing.aim,
|
|
185
|
-
azimuthRad: framing.azimuthRad,
|
|
186
|
-
aspect: width / height,
|
|
187
|
-
})
|
|
188
|
-
thumbnailCamera.position.set(pose.position[0], pose.position[1], pose.position[2])
|
|
189
|
-
thumbnailCamera.lookAt(pose.target[0], pose.target[1], pose.target[2])
|
|
190
|
-
thumbnailCamera.updateMatrixWorld()
|
|
191
|
-
pipeline?.applyEnvironment({
|
|
192
|
-
theme: useViewer.getState().sceneTheme,
|
|
193
|
-
transparent,
|
|
194
|
-
grade: useViewer.getState().shading === 'rendered',
|
|
195
|
-
edges: transparent ? 'off' : useViewer.getState().edges,
|
|
196
|
-
camera: thumbnailCamera,
|
|
197
|
-
})
|
|
198
|
-
cameraData.position = pose.position
|
|
199
|
-
cameraData.target = pose.target
|
|
145
|
+
if (cameraPose && !pipelineRef.current) {
|
|
146
|
+
throw new Error('Snapshot renderer is not ready. Try again.')
|
|
200
147
|
}
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
let blob: Blob
|
|
204
|
-
|
|
205
|
-
if (pipeline) {
|
|
206
|
-
let capturePromise: ReturnType<SnapshotPipeline['capture']>
|
|
207
148
|
|
|
208
|
-
//
|
|
209
|
-
//
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
149
|
+
// Copy the main camera's transform and projection so the thumbnail
|
|
150
|
+
// matches exactly what the user sees in the viewport.
|
|
151
|
+
thumbnailCamera.position.copy(mainCamera.position)
|
|
152
|
+
thumbnailCamera.quaternion.copy(mainCamera.quaternion)
|
|
153
|
+
if (mainCamera instanceof THREE.PerspectiveCamera) {
|
|
154
|
+
thumbnailCamera.fov = mainCamera.fov
|
|
155
|
+
thumbnailCamera.near = mainCamera.near
|
|
156
|
+
thumbnailCamera.far = mainCamera.far
|
|
157
|
+
}
|
|
158
|
+
const { width, height } = gl.domElement
|
|
159
|
+
thumbnailCamera.aspect = width / height
|
|
160
|
+
if (cameraPose) {
|
|
161
|
+
applySnapshotCapturePose(
|
|
162
|
+
thumbnailCamera,
|
|
163
|
+
cameraPose,
|
|
164
|
+
{ width, height },
|
|
165
|
+
{
|
|
166
|
+
w: standardW,
|
|
167
|
+
h: standardH,
|
|
168
|
+
},
|
|
169
|
+
)
|
|
170
|
+
}
|
|
171
|
+
thumbnailCamera.updateProjectionMatrix()
|
|
172
|
+
// The capture camera never joins the scene graph, so its matrixWorld
|
|
173
|
+
// is only refreshed by the render itself — too late for the backdrop
|
|
174
|
+
// uniforms below.
|
|
175
|
+
thumbnailCamera.updateMatrixWorld()
|
|
176
|
+
|
|
177
|
+
const pipeline = pipelineRef.current
|
|
178
|
+
pipeline?.applyEnvironment({
|
|
179
|
+
theme: useViewer.getState().sceneTheme,
|
|
180
|
+
transparent,
|
|
181
|
+
grade: useViewer.getState().shading === 'rendered',
|
|
182
|
+
// Preset/item captures stay clean; scene captures mirror the canvas.
|
|
183
|
+
edges: transparent ? 'off' : useViewer.getState().edges,
|
|
184
|
+
camera: thumbnailCamera,
|
|
185
|
+
})
|
|
186
|
+
|
|
187
|
+
// Capture camera data for snapshot storage
|
|
188
|
+
const pos = cameraPose ? thumbnailCamera.position : mainCamera.position
|
|
189
|
+
let tgt: [number, number, number] | null = null
|
|
190
|
+
if (!cameraPose && controls && 'getTarget' in controls) {
|
|
191
|
+
const v = new THREE.Vector3()
|
|
192
|
+
;(controls as any).getTarget(v)
|
|
193
|
+
tgt = [v.x, v.y, v.z]
|
|
194
|
+
} else {
|
|
195
|
+
// Walk / drone captures run without orbit controls, so there is no orbit
|
|
196
|
+
// target to read. Synthesize one down the view axis — otherwise the
|
|
197
|
+
// saved snapshot carries no framing to return to.
|
|
198
|
+
const look = new THREE.Vector3(0, 0, -1)
|
|
199
|
+
.applyQuaternion(cameraPose ? thumbnailCamera.quaternion : mainCamera.quaternion)
|
|
200
|
+
.multiplyScalar(FIRST_PERSON_TARGET_DISTANCE)
|
|
201
|
+
.add(pos)
|
|
202
|
+
tgt = [look.x, look.y, look.z]
|
|
203
|
+
}
|
|
204
|
+
const isOrtho = !cameraPose && mainCamera instanceof THREE.OrthographicCamera
|
|
205
|
+
const cameraData: SnapshotCameraData = {
|
|
206
|
+
...(requestId && { requestId }),
|
|
207
|
+
position: [pos.x, pos.y, pos.z],
|
|
208
|
+
...(cameraPose && {
|
|
209
|
+
quaternion: [...cameraPose.quaternion] as [number, number, number, number],
|
|
210
|
+
fov: cameraPose.fov,
|
|
211
|
+
}),
|
|
212
|
+
target: tgt,
|
|
213
|
+
type: isOrtho ? 'orthographic' : 'perspective',
|
|
214
|
+
...(isOrtho && { zoom: (mainCamera as THREE.OrthographicCamera).zoom }),
|
|
225
215
|
}
|
|
226
216
|
|
|
227
|
-
const
|
|
228
|
-
|
|
217
|
+
const capturePromise = captureSnapshotScene((restore) => {
|
|
218
|
+
if (snapLevels) {
|
|
219
|
+
const prevMode = useViewer.getState().levelMode
|
|
220
|
+
if (prevMode !== 'stacked') {
|
|
221
|
+
restore(() => useViewer.getState().setLevelMode(prevMode))
|
|
222
|
+
useViewer.getState().setLevelMode('stacked')
|
|
223
|
+
}
|
|
224
|
+
restore(snapLevelsToTruePositions())
|
|
225
|
+
}
|
|
226
|
+
restore(temporarilyHideNodeTypes(['scan', 'guide', 'spawn']))
|
|
227
|
+
|
|
228
|
+
// Auto-save uses the published hero framing. An authored shot keeps
|
|
229
|
+
// its own camera while sharing the same true level positions.
|
|
230
|
+
if (snapLevels) {
|
|
231
|
+
const framing = computeHeroFraming()
|
|
232
|
+
if (framing) {
|
|
233
|
+
const pose = heroCameraPose({
|
|
234
|
+
boxes: framing.boxes,
|
|
235
|
+
aim: framing.aim,
|
|
236
|
+
azimuthRad: framing.azimuthRad,
|
|
237
|
+
aspect: width / height,
|
|
238
|
+
})
|
|
239
|
+
thumbnailCamera.position.set(pose.position[0], pose.position[1], pose.position[2])
|
|
240
|
+
thumbnailCamera.lookAt(pose.target[0], pose.target[1], pose.target[2])
|
|
241
|
+
thumbnailCamera.updateMatrixWorld()
|
|
242
|
+
pipeline?.applyEnvironment({
|
|
243
|
+
theme: useViewer.getState().sceneTheme,
|
|
244
|
+
transparent,
|
|
245
|
+
grade: useViewer.getState().shading === 'rendered',
|
|
246
|
+
edges: transparent ? 'off' : useViewer.getState().edges,
|
|
247
|
+
camera: thumbnailCamera,
|
|
248
|
+
})
|
|
249
|
+
cameraData.position = pose.position
|
|
250
|
+
cameraData.target = pose.target
|
|
251
|
+
}
|
|
252
|
+
}
|
|
229
253
|
|
|
230
|
-
|
|
231
|
-
cameraData.resolution = { w: result.outW, h: result.outH }
|
|
232
|
-
} else {
|
|
233
|
-
// Fallback: plain render directly to the canvas
|
|
234
|
-
try {
|
|
254
|
+
restore(() => emitter.emit('thumbnail:after-capture', undefined))
|
|
235
255
|
emitter.emit('thumbnail:before-capture', undefined)
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
256
|
+
if (cameraPose) {
|
|
257
|
+
restore(snapLevelsToTruePositions())
|
|
258
|
+
restore(temporarilyShowShadowOnly(scene))
|
|
259
|
+
const wallMaterials = new Map<THREE.Mesh, THREE.Material | THREE.Material[]>()
|
|
260
|
+
restore(() => {
|
|
261
|
+
for (const [mesh, material] of wallMaterials) mesh.material = material
|
|
262
|
+
})
|
|
263
|
+
const state = useScene.getState()
|
|
264
|
+
const viewer = useViewer.getState()
|
|
265
|
+
for (const id of sceneRegistry.byType.wall ?? []) {
|
|
266
|
+
const node = state.nodes[id as AnyNodeId]
|
|
267
|
+
const mesh = sceneRegistry.nodes.get(id) as THREE.Mesh | undefined
|
|
268
|
+
if (node?.type !== 'wall' || !mesh?.isMesh) continue
|
|
269
|
+
wallMaterials.set(mesh, mesh.material)
|
|
270
|
+
mesh.material = getVisibleWallMaterials(
|
|
271
|
+
node,
|
|
272
|
+
viewer.shading,
|
|
273
|
+
viewer.textures,
|
|
274
|
+
viewer.colorPreset,
|
|
275
|
+
viewer.sceneTheme,
|
|
276
|
+
state.materials,
|
|
277
|
+
)
|
|
278
|
+
}
|
|
279
|
+
}
|
|
246
280
|
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
const sy = Math.round(cropRegion.y * height)
|
|
265
|
-
const sourceW = Math.round(cropRegion.width * width)
|
|
266
|
-
const sourceH = Math.round(cropRegion.height * height)
|
|
267
|
-
;({ w: outW, h: outH } = clampSnapshotSize(sourceW, sourceH))
|
|
268
|
-
const offscreen = document.createElement('canvas')
|
|
269
|
-
offscreen.width = outW
|
|
270
|
-
offscreen.height = outH
|
|
271
|
-
const ctx = offscreen.getContext('2d')!
|
|
272
|
-
if (outW !== sourceW || outH !== sourceH) ctx.imageSmoothingQuality = 'high'
|
|
273
|
-
ctx.drawImage(gl.domElement, sx, sy, sourceW, sourceH, 0, 0, outW, outH)
|
|
274
|
-
blob = await new Promise<Blob>((resolve, reject) =>
|
|
275
|
-
offscreen.toBlob(
|
|
276
|
-
(b) => (b ? resolve(b) : reject(new Error('Canvas capture failed'))),
|
|
277
|
-
SNAPSHOT_MIME,
|
|
278
|
-
SNAPSHOT_QUALITY,
|
|
279
|
-
),
|
|
280
|
-
)
|
|
281
|
+
// Geometry and presentation systems may have mounted new meshes
|
|
282
|
+
// since the viewport's last frame. Apply the same isolation to
|
|
283
|
+
// those meshes before either snapshot render path submits them,
|
|
284
|
+
// and restore fog afterwards so the atmosphere never leaks in.
|
|
285
|
+
restore(refreshIsolation(scene))
|
|
286
|
+
if (pipeline) return pipeline.capture({ captureMode, cropRegion, standardSize })
|
|
287
|
+
gl.render(scene, thumbnailCamera)
|
|
288
|
+
return undefined
|
|
289
|
+
})
|
|
290
|
+
|
|
291
|
+
let blob: Blob
|
|
292
|
+
if (pipeline) {
|
|
293
|
+
const result = await capturePromise
|
|
294
|
+
if (!result) throw new Error('Snapshot capture produced no image')
|
|
295
|
+
blob = result.blob
|
|
296
|
+
if (captureMode !== undefined) cameraData.captureMode = captureMode
|
|
297
|
+
cameraData.resolution = { w: result.outW, h: result.outH }
|
|
281
298
|
} else {
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
let
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
299
|
+
await capturePromise
|
|
300
|
+
let outW: number
|
|
301
|
+
let outH: number
|
|
302
|
+
|
|
303
|
+
if (captureMode === 'viewport') {
|
|
304
|
+
;({ w: outW, h: outH } = clampSnapshotSize(width, height))
|
|
305
|
+
const offscreen = document.createElement('canvas')
|
|
306
|
+
offscreen.width = outW
|
|
307
|
+
offscreen.height = outH
|
|
308
|
+
const ctx = offscreen.getContext('2d')!
|
|
309
|
+
if (outW !== width || outH !== height) ctx.imageSmoothingQuality = 'high'
|
|
310
|
+
ctx.drawImage(gl.domElement, 0, 0, width, height, 0, 0, outW, outH)
|
|
311
|
+
blob = await new Promise<Blob>((resolve, reject) =>
|
|
312
|
+
offscreen.toBlob(
|
|
313
|
+
(b) => (b ? resolve(b) : reject(new Error('Canvas capture failed'))),
|
|
314
|
+
SNAPSHOT_MIME,
|
|
315
|
+
SNAPSHOT_QUALITY,
|
|
316
|
+
),
|
|
317
|
+
)
|
|
318
|
+
} else if (captureMode === 'area' && cropRegion) {
|
|
319
|
+
const sx = Math.round(cropRegion.x * width)
|
|
320
|
+
const sy = Math.round(cropRegion.y * height)
|
|
321
|
+
const sourceW = Math.round(cropRegion.width * width)
|
|
322
|
+
const sourceH = Math.round(cropRegion.height * height)
|
|
323
|
+
;({ w: outW, h: outH } = clampSnapshotSize(sourceW, sourceH))
|
|
324
|
+
const offscreen = document.createElement('canvas')
|
|
325
|
+
offscreen.width = outW
|
|
326
|
+
offscreen.height = outH
|
|
327
|
+
const ctx = offscreen.getContext('2d')!
|
|
328
|
+
if (outW !== sourceW || outH !== sourceH) ctx.imageSmoothingQuality = 'high'
|
|
329
|
+
ctx.drawImage(gl.domElement, sx, sy, sourceW, sourceH, 0, 0, outW, outH)
|
|
330
|
+
blob = await new Promise<Blob>((resolve, reject) =>
|
|
331
|
+
offscreen.toBlob(
|
|
332
|
+
(b) => (b ? resolve(b) : reject(new Error('Canvas capture failed'))),
|
|
333
|
+
SNAPSHOT_MIME,
|
|
334
|
+
SNAPSHOT_QUALITY,
|
|
335
|
+
),
|
|
336
|
+
)
|
|
337
|
+
} else {
|
|
338
|
+
const srcAspect = width / height
|
|
339
|
+
const dstAspect = standardW / standardH
|
|
340
|
+
let sx = 0,
|
|
341
|
+
sy = 0,
|
|
342
|
+
sWidth = width,
|
|
343
|
+
sHeight = height
|
|
344
|
+
if (srcAspect > dstAspect) {
|
|
345
|
+
sWidth = Math.round(height * dstAspect)
|
|
346
|
+
sx = Math.round((width - sWidth) / 2)
|
|
347
|
+
} else if (srcAspect < dstAspect) {
|
|
348
|
+
sHeight = Math.round(width / dstAspect)
|
|
349
|
+
sy = Math.round((height - sHeight) / 2)
|
|
350
|
+
}
|
|
351
|
+
outW = standardW
|
|
352
|
+
outH = standardH
|
|
353
|
+
const offscreen = document.createElement('canvas')
|
|
354
|
+
offscreen.width = outW
|
|
355
|
+
offscreen.height = outH
|
|
356
|
+
offscreen
|
|
357
|
+
.getContext('2d')!
|
|
358
|
+
.drawImage(gl.domElement, sx, sy, sWidth, sHeight, 0, 0, outW, outH)
|
|
359
|
+
blob = await new Promise<Blob>((resolve, reject) =>
|
|
360
|
+
offscreen.toBlob(
|
|
361
|
+
(b) => (b ? resolve(b) : reject(new Error('Canvas capture failed'))),
|
|
362
|
+
SNAPSHOT_MIME,
|
|
363
|
+
SNAPSHOT_QUALITY,
|
|
364
|
+
),
|
|
365
|
+
)
|
|
294
366
|
}
|
|
295
|
-
outW = standardW
|
|
296
|
-
outH = standardH
|
|
297
|
-
const offscreen = document.createElement('canvas')
|
|
298
|
-
offscreen.width = outW
|
|
299
|
-
offscreen.height = outH
|
|
300
|
-
offscreen
|
|
301
|
-
.getContext('2d')!
|
|
302
|
-
.drawImage(gl.domElement, sx, sy, sWidth, sHeight, 0, 0, outW, outH)
|
|
303
|
-
blob = await new Promise<Blob>((resolve, reject) =>
|
|
304
|
-
offscreen.toBlob(
|
|
305
|
-
(b) => (b ? resolve(b) : reject(new Error('Canvas capture failed'))),
|
|
306
|
-
SNAPSHOT_MIME,
|
|
307
|
-
SNAPSHOT_QUALITY,
|
|
308
|
-
),
|
|
309
|
-
)
|
|
310
|
-
}
|
|
311
367
|
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
368
|
+
if (captureMode !== undefined) cameraData.captureMode = captureMode
|
|
369
|
+
cameraData.resolution = { w: outW, h: outH }
|
|
370
|
+
}
|
|
315
371
|
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
372
|
+
if (
|
|
373
|
+
version !== captureVersion.current ||
|
|
374
|
+
thumbnailCamera !== thumbnailCameraRef.current
|
|
375
|
+
) {
|
|
376
|
+
throw new Error('The scene changed during capture. Try again.')
|
|
377
|
+
}
|
|
378
|
+
if (cameraPose && event.projectId !== useViewer.getState().projectId) {
|
|
379
|
+
throw new Error('The active project changed during capture')
|
|
380
|
+
}
|
|
381
|
+
await onCapture(blob, cameraData)
|
|
382
|
+
},
|
|
383
|
+
(failure) => emitter.emit('snapshot:capture-failed', failure),
|
|
384
|
+
)
|
|
322
385
|
},
|
|
323
|
-
[gl, scene,
|
|
386
|
+
[gl, scene, getThree],
|
|
324
387
|
)
|
|
325
388
|
|
|
326
389
|
// Thumbnail request via emitter. Two call shapes:
|
|
@@ -330,30 +393,23 @@ export const ThumbnailGenerator = ({ onThumbnailCapture }: ThumbnailGeneratorPro
|
|
|
330
393
|
// to their true positions first for a consistent auto-thumbnail angle.
|
|
331
394
|
// The caller owns policy (when to fire, whether the tab is visible).
|
|
332
395
|
useEffect(() => {
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
// and project thumbnails — composite the theme backdrop + sky.
|
|
343
|
-
transparent?: boolean
|
|
344
|
-
}) => {
|
|
345
|
-
await generate(
|
|
346
|
-
event.snapLevels === true,
|
|
347
|
-
event.captureMode,
|
|
348
|
-
event.cropRegion,
|
|
349
|
-
event.standardSize,
|
|
350
|
-
event.transparent === true,
|
|
396
|
+
const handleGenerateThumbnail = async (event: ThumbnailGenerateEvent) => {
|
|
397
|
+
// A saved-frame notification can enqueue the next shot frame before
|
|
398
|
+
// its predecessor's host callback returns and releases the renderer.
|
|
399
|
+
await enqueueSnapshotCapture(
|
|
400
|
+
captureQueue.current,
|
|
401
|
+
captureVersion,
|
|
402
|
+
event,
|
|
403
|
+
generate,
|
|
404
|
+
(failure) => emitter.emit('snapshot:capture-failed', failure),
|
|
351
405
|
)
|
|
352
406
|
}
|
|
353
407
|
|
|
354
408
|
emitter.on('camera-controls:generate-thumbnail', handleGenerateThumbnail)
|
|
355
|
-
return () =>
|
|
356
|
-
|
|
409
|
+
return () => {
|
|
410
|
+
emitter.off('camera-controls:generate-thumbnail', handleGenerateThumbnail)
|
|
411
|
+
}
|
|
412
|
+
}, [generate])
|
|
357
413
|
|
|
358
414
|
// Go-to-camera: animate camera to a saved snapshot position/target
|
|
359
415
|
useEffect(() => {
|