@pascal-app/editor 1.0.3 → 1.0.5
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/README.md +106 -0
- package/package.json +42 -38
- package/src/components/editor/__bench__/room-label-placement.bench.ts +108 -0
- package/src/components/editor/action-menu-button.tsx +83 -0
- package/src/components/editor/bake-thumbnail.tsx +307 -5
- package/src/components/editor/capture-level-picker.tsx +72 -0
- package/src/components/editor/construction-reveal-audio.test.tsx +150 -0
- package/src/components/editor/construction-reveal-audio.tsx +40 -0
- package/src/components/editor/custom-camera-controls.tsx +275 -57
- package/src/components/editor/delete-confirmation-dialog.tsx +297 -7
- package/src/components/editor/duplicate-node.ts +244 -0
- package/src/components/editor/editor-layout-mobile.tsx +37 -26
- package/src/components/editor/editor-layout-v2.tsx +132 -59
- package/src/components/editor/export-manager.tsx +47 -26
- package/src/components/editor/first-person/build-collider-world.test.ts +320 -0
- package/src/components/editor/first-person/build-collider-world.ts +28 -7
- package/src/components/editor/first-person/ground-holes.test.ts +128 -0
- package/src/components/editor/first-person/ground-holes.ts +130 -0
- package/src/components/editor/first-person/mechanism-targets.test.ts +162 -0
- package/src/components/editor/first-person/mechanism-targets.ts +88 -0
- package/src/components/editor/first-person-controls.tsx +220 -9
- package/src/components/editor/floating-action-menu.tsx +63 -207
- package/src/components/editor/floor-aware-camera-controls.test.ts +466 -0
- package/src/components/editor/floor-aware-camera-controls.ts +324 -0
- package/src/components/editor/floor-openings-overlay.tsx +243 -0
- package/src/components/editor/floor-region-controls.tsx +846 -0
- package/src/components/editor/floor-region-webgpu-materials.test.ts +12 -0
- package/src/components/editor/floor-surface.test.ts +165 -0
- package/src/components/editor/floor-surface.ts +182 -0
- package/src/components/editor/floorplan-background-selection.test.ts +11 -2
- package/src/components/editor/floorplan-background-selection.ts +1 -3
- package/src/components/editor/floorplan-mode-coordinator.tsx +2 -2
- package/src/components/editor/floorplan-panel.tsx +414 -363
- package/src/components/editor/follow-pascal.test.tsx +912 -0
- package/src/components/editor/follow-pascal.tsx +354 -0
- package/src/components/editor/footprint-height-handle.tsx +204 -0
- package/src/components/editor/group-actions.ts +56 -2
- package/src/components/editor/group-move-3d.ts +2 -0
- package/src/components/editor/group-rotate-handle.tsx +2 -0
- package/src/components/editor/group-transform-shared.test.ts +460 -0
- package/src/components/editor/handles/handle-hit-priority.test.tsx +63 -0
- package/src/components/editor/handles/handle-hit-priority.tsx +37 -0
- package/src/components/editor/handles/swallow-next-click.test.ts +28 -0
- package/src/components/editor/handles/use-handle-drag.ts +269 -197
- package/src/components/editor/index.tsx +409 -197
- package/src/components/editor/join-walls-pill.tsx +44 -0
- package/src/components/editor/node-action-menu.tsx +68 -70
- package/src/components/editor/node-arrow-handles.tsx +25 -6
- package/src/components/editor/open-wall-ends-3d-layer.tsx +273 -0
- package/src/components/editor/open-wall-ends-hint.tsx +89 -0
- package/src/components/editor/quick-measurement-hud.tsx +3 -2
- package/src/components/editor/registry-action-contributions.test.tsx +63 -0
- package/src/components/editor/registry-action-contributions.tsx +80 -1
- package/src/components/editor/riser-diagram-panel.tsx +14 -7
- package/src/components/editor/room-controls.tsx +847 -0
- package/src/components/editor/room-floor-highlight.tsx +128 -0
- package/src/components/editor/room-handles-2d.tsx +354 -0
- package/src/components/editor/room-handles.tsx +675 -0
- package/src/components/editor/room-highlight.tsx +233 -0
- package/src/components/editor/room-labels.test.ts +850 -0
- package/src/components/editor/room-labels.tsx +1890 -0
- package/src/components/editor/room-transform-ghost.tsx +256 -0
- package/src/components/editor/room-webgpu-materials.test.ts +27 -0
- package/src/components/editor/selection-manager.tsx +474 -419
- package/src/components/editor/slab-hole-highlights.tsx +5 -2
- package/src/components/editor/snapshot-capture-overlay.tsx +7 -2
- package/src/components/editor/snapshot-capture.test.ts +119 -60
- package/src/components/editor/snapshot-capture.ts +36 -5
- package/src/components/editor/thumbnail-generator.test.ts +654 -0
- package/src/components/editor/thumbnail-generator.tsx +1103 -223
- package/src/components/editor/use-floorplan-background-placement.ts +34 -12
- package/src/components/editor/vector-edge-extractor.tsx +121 -0
- package/src/components/editor/views/host-view.tsx +61 -0
- package/src/components/editor/views/plugins-view.tsx +663 -0
- package/src/components/editor/views/scene-region.tsx +59 -0
- package/src/components/editor/views/use-editor-views.ts +57 -0
- package/src/components/editor/views/view-bar.test.tsx +54 -0
- package/src/components/editor/views/view-bar.tsx +343 -0
- package/src/components/editor/wall-justification.test.ts +140 -0
- package/src/components/editor/wall-move-side-handles.tsx +43 -142
- package/src/components/editor/wall-push-arrow.tsx +92 -0
- package/src/components/editor/wall-region-handles.tsx +509 -0
- package/src/components/editor/wall-snap-beacon-layer.tsx +8 -4
- package/src/components/editor-2d/floorplan-cursor-indicator-overlay.tsx +8 -4
- package/src/components/editor-2d/floorplan-group-move.tsx +3 -0
- package/src/components/editor-2d/floorplan-open-wall-ends-layer.test.tsx +86 -0
- package/src/components/editor-2d/floorplan-open-wall-ends-layer.tsx +290 -0
- package/src/components/editor-2d/floorplan-registered-tool-layer.tsx +2 -2
- package/src/components/editor-2d/floorplan-registry-action-menu.tsx +11 -1
- package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +116 -106
- package/src/components/editor-2d/renderers/floorplan-dimension-edit-overlay.test.ts +156 -0
- package/src/components/editor-2d/renderers/floorplan-dimension-edit-overlay.tsx +278 -0
- package/src/components/editor-2d/renderers/floorplan-dimension-renderer.tsx +77 -0
- package/src/components/editor-2d/renderers/{floorplan-draft-wall-measurement.tsx → floorplan-draft-measurement.tsx} +8 -8
- package/src/components/editor-2d/renderers/floorplan-geometry-renderer.tsx +4 -1
- package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +38 -8
- package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +319 -233
- package/src/components/editor-2d/renderers/floorplan-reveal.test.tsx +254 -0
- package/src/components/editor-2d/renderers/floorplan-reveal.ts +141 -0
- package/src/components/editor-2d/svg-paths.ts +12 -15
- package/src/components/point-ask/bubble.tsx +538 -0
- package/src/components/point-ask/hover-chip.tsx +91 -0
- package/src/components/point-ask/mode-pill.tsx +92 -0
- package/src/components/point-ask/optional-motion.ts +22 -0
- package/src/components/point-ask/outline-corners.tsx +93 -0
- package/src/components/point-ask/pins.tsx +711 -0
- package/src/components/point-ask/point-ask-layer.tsx +83 -0
- package/src/components/point-ask/point-ask-styles.ts +105 -0
- package/src/components/point-ask/region-marquee-styles.ts +15 -0
- package/src/components/point-ask/region-marquee.tsx +64 -0
- package/src/components/procedural-items/inspector.tsx +40 -15
- package/src/components/systems/ceiling/ceiling-bracket-batch.test.ts +12 -2
- package/src/components/systems/ceiling/ceiling-selection-affordance-system.test.ts +97 -14
- package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +18 -18
- package/src/components/systems/roof/roof-edit-system.tsx +1 -0
- package/src/components/systems/zone/zone-system.test.ts +47 -0
- package/src/components/systems/zone/zone-system.tsx +29 -68
- package/src/components/tools/distribution/connection-compatibility.test.ts +34 -0
- package/src/components/tools/distribution/connection-compatibility.ts +43 -0
- package/src/components/tools/distribution/distribution-public.ts +9 -0
- package/src/components/tools/distribution/distribution-run-contract.test.ts +26 -0
- package/src/components/tools/distribution/distribution-run-contract.ts +94 -0
- package/src/components/tools/distribution/distribution-run-tool.test.ts +241 -0
- package/src/components/tools/distribution/distribution-run-tool.test.tsx +125 -0
- package/src/components/tools/distribution/distribution-run-tool.tsx +1323 -0
- package/src/components/tools/distribution/draw-alignment.ts +35 -0
- package/src/components/tools/distribution/ports.test.ts +56 -0
- package/src/components/tools/distribution/ports.ts +378 -0
- package/src/components/tools/distribution/run-cursor.test.ts +135 -0
- package/src/components/tools/distribution/run-cursor.ts +36 -0
- package/src/components/tools/distribution/run-direction-feedback.test.ts +60 -0
- package/src/components/tools/distribution/run-direction-feedback.tsx +251 -0
- package/src/components/tools/distribution/run-port-snap.test.ts +79 -0
- package/src/components/tools/distribution/run-port-snap.ts +25 -0
- package/src/components/tools/distribution/run-screen-direction.test.ts +23 -0
- package/src/components/tools/distribution/run-screen-direction.ts +30 -0
- package/src/components/tools/elevator/elevator-tool.tsx +3 -6
- package/src/components/tools/elevator/move-elevator-tool.tsx +2 -3
- package/src/components/tools/item/item-surface-strategy-resolver.test.ts +67 -68
- package/src/components/tools/item/placement-history.test.tsx +512 -0
- package/src/components/tools/item/placement-strategies.test.ts +46 -1
- package/src/components/tools/item/placement-strategies.ts +11 -10
- package/src/components/tools/item/shelf-surface-strategy-resolver.test.ts +56 -56
- package/src/components/tools/item/use-draft-node.test.tsx +432 -1
- package/src/components/tools/item/use-draft-node.ts +166 -95
- package/src/components/tools/item/use-placement-coordinator.tsx +148 -95
- package/src/components/tools/paint-region/use-paint-tint.ts +37 -0
- package/src/components/tools/paint-region/wall-paint-region-tool.tsx +466 -0
- package/src/components/tools/registry/item-surface-move.test.ts +62 -63
- package/src/components/tools/registry/item-surface-move.ts +2 -0
- package/src/components/tools/registry/item-surface-resolver.test.ts +26 -26
- package/src/components/tools/registry/move-registry-node-tool.test.ts +268 -1
- package/src/components/tools/registry/move-registry-node-tool.tsx +66 -30
- package/src/components/tools/select/box-select-tool.tsx +2 -12
- package/src/components/tools/select/plane-box-select-tool.test.tsx +79 -0
- package/src/components/tools/select/plane-box-select-tool.tsx +35 -22
- package/src/components/tools/select/select-candidates.test.ts +37 -4
- package/src/components/tools/select/select-candidates.ts +15 -11
- package/src/components/tools/shared/cursor-sphere.tsx +36 -12
- package/src/components/tools/site/site-boundary-editor.tsx +1 -2
- package/src/components/tools/tool-manager.tsx +59 -24
- package/src/components/tools/wall/wall-drafting.test.ts +175 -47
- package/src/components/tools/zone/zone-tool.tsx +4 -12
- package/src/components/ui/action-menu/action-button.tsx +9 -1
- package/src/components/ui/action-menu/camera-actions.tsx +9 -4
- package/src/components/ui/action-menu/control-modes.tsx +15 -42
- package/src/components/ui/action-menu/follow-pascal-pill.tsx +32 -0
- package/src/components/ui/action-menu/follow-pascal-toggle.tsx +36 -0
- package/src/components/ui/action-menu/index.tsx +56 -33
- package/src/components/ui/action-menu/measurement-control.tsx +14 -19
- package/src/components/ui/action-menu/placement.ts +20 -0
- package/src/components/ui/action-menu/point-ask-button.tsx +52 -0
- package/src/components/ui/action-menu/view-toggles.tsx +47 -42
- package/src/components/ui/build-panel/build-panel.tsx +242 -0
- package/src/components/ui/command-palette/editor-commands.tsx +7 -42
- package/src/components/ui/command-palette/index.tsx +50 -39
- package/src/components/ui/command-palette/ranked-groups.test.ts +31 -0
- package/src/components/ui/controls/action-button.tsx +5 -2
- package/src/components/ui/controls/finish-swatch.tsx +110 -0
- package/src/components/ui/controls/material-paint-panel.tsx +97 -26
- package/src/components/ui/controls/material-picker.tsx +4 -1
- package/src/components/ui/controls/metric-control.tsx +1 -1
- package/src/components/ui/controls/scene-material-list.tsx +4 -32
- package/src/components/ui/controls/segmented-control.tsx +21 -8
- package/src/components/ui/controls/select-control.tsx +65 -0
- package/src/components/ui/controls/slider-control.tsx +1 -1
- package/src/components/ui/controls/toggle-control.tsx +3 -3
- package/src/components/ui/controls/tool-options-panel.tsx +1 -1
- package/src/components/ui/floating-level-selector.tsx +159 -59
- package/src/components/ui/helpers/building-helper.tsx +4 -1
- package/src/components/ui/helpers/contextual-helper-panel.tsx +181 -72
- package/src/components/ui/helpers/helper-manager.test.tsx +121 -0
- package/src/components/ui/helpers/helper-manager.tsx +331 -14
- package/src/components/ui/helpers/item-helper.tsx +17 -1
- package/src/components/ui/helpers/registered-tool-helper.tsx +8 -1
- package/src/components/ui/icon-ref.tsx +45 -1
- package/src/components/ui/level-duplicate-dialog.tsx +1 -1
- package/src/components/ui/node-icon-image.tsx +16 -0
- package/src/components/ui/panels/collections/collections-popover.tsx +28 -22
- package/src/components/ui/panels/floor-foundation-panel.tsx +551 -0
- package/src/components/ui/panels/floor-opening-panel.tsx +143 -0
- package/src/components/ui/panels/mobile-panel-sheet.tsx +4 -4
- package/src/components/ui/panels/mobile-selection-bar.tsx +8 -5
- package/src/components/ui/panels/multi-field-value.test.ts +26 -3
- package/src/components/ui/panels/multi-field-value.ts +6 -3
- package/src/components/ui/panels/multi-parametric-inspector.tsx +52 -2
- package/src/components/ui/panels/multi-selection-panel.tsx +161 -5
- package/src/components/ui/panels/node-display.ts +8 -3
- package/src/components/ui/panels/panel-manager.tsx +103 -68
- package/src/components/ui/panels/panel-wrapper.tsx +130 -62
- package/src/components/ui/panels/parametric-field-control.tsx +3 -3
- package/src/components/ui/panels/parametric-inspector.tsx +7 -7
- package/src/components/ui/panels/reference-panel.tsx +2 -3
- package/src/components/ui/panels/room-construction-rows.tsx +774 -0
- package/src/components/ui/panels/room-name-options.test.ts +55 -0
- package/src/components/ui/panels/room-name-options.ts +47 -0
- package/src/components/ui/panels/room-openings-field.tsx +110 -0
- package/src/components/ui/panels/room-panel.tsx +790 -0
- package/src/components/ui/panels/shape-choice.tsx +58 -0
- package/src/components/ui/panels/wall-region-list.tsx +109 -0
- package/src/components/ui/primitives/badge.tsx +8 -0
- package/src/components/ui/primitives/color-dot.tsx +36 -24
- package/src/components/ui/primitives/sidebar.tsx +3 -1
- package/src/components/ui/right-stack.test.ts +29 -0
- package/src/components/ui/right-stack.tsx +164 -0
- package/src/components/ui/sidebar/app-sidebar.tsx +1 -1
- package/src/components/ui/sidebar/icon-rail.tsx +8 -3
- package/src/components/ui/sidebar/panels/items-panel/function-tree-panel.tsx +1 -1
- package/src/components/ui/sidebar/panels/items-panel/index.tsx +6 -6
- package/src/components/ui/sidebar/panels/settings-panel/animation-speed-setting.tsx +40 -0
- package/src/components/ui/sidebar/panels/settings-panel/audio-settings-dialog.tsx +7 -8
- package/src/components/ui/sidebar/panels/settings-panel/construction-animation-setting.test.tsx +170 -0
- package/src/components/ui/sidebar/panels/settings-panel/construction-animation-setting.tsx +92 -0
- package/src/components/ui/sidebar/panels/settings-panel/index.tsx +138 -27
- package/src/components/ui/sidebar/panels/settings-panel/interface-theme.test.tsx +20 -0
- package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +5 -5
- package/src/components/ui/sidebar/panels/settings-panel/print-export-button.tsx +4 -10
- package/src/components/ui/sidebar/panels/site-panel/building-tree-node.tsx +3 -11
- package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +4 -4
- package/src/components/ui/sidebar/panels/site-panel/chimney-tree-node.tsx +1 -5
- package/src/components/ui/sidebar/panels/site-panel/collection-tree-node.tsx +197 -0
- package/src/components/ui/sidebar/panels/site-panel/column-tree-node.tsx +1 -5
- package/src/components/ui/sidebar/panels/site-panel/door-tree-node.tsx +1 -5
- package/src/components/ui/sidebar/panels/site-panel/dormer-tree-node.tsx +1 -5
- package/src/components/ui/sidebar/panels/site-panel/elevator-tree-node.tsx +1 -5
- package/src/components/ui/sidebar/panels/site-panel/fence-tree-node.tsx +24 -11
- package/src/components/ui/sidebar/panels/site-panel/gutter-tree-node.tsx +1 -5
- package/src/components/ui/sidebar/panels/site-panel/index.tsx +181 -223
- package/src/components/ui/sidebar/panels/site-panel/inline-rename-input.tsx +30 -8
- package/src/components/ui/sidebar/panels/site-panel/item-tree-node.tsx +1 -3
- package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +4 -1
- package/src/components/ui/sidebar/panels/site-panel/registry-tree-node.tsx +1 -3
- package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +1 -4
- package/src/components/ui/sidebar/panels/site-panel/shelf-tree-node.tsx +1 -3
- package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +21 -13
- package/src/components/ui/sidebar/panels/site-panel/solar-panel-tree-node.tsx +1 -5
- package/src/components/ui/sidebar/panels/site-panel/spawn-tree-node.tsx +1 -5
- package/src/components/ui/sidebar/panels/site-panel/stair-tree-node.tsx +1 -5
- package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +13 -37
- package/src/components/ui/sidebar/panels/site-panel/unit-tree-node.tsx +2 -3
- package/src/components/ui/sidebar/panels/site-panel/wall-tree-node.tsx +1 -5
- package/src/components/ui/sidebar/panels/site-panel/window-tree-node.tsx +1 -5
- package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +9 -4
- package/src/components/ui/sidebar/panels/zone-panel/index.tsx +2 -4
- package/src/components/ui/sidebar/tab-bar.tsx +14 -4
- package/src/components/ui/sidebar/use-plugin-panels.tsx +15 -5
- package/src/components/viewer/floorplan-compass-button.tsx +9 -1
- package/src/components/viewer/floorplan-preview.tsx +5 -6
- package/src/components/viewer/viewer-controls-bar.tsx +18 -6
- package/src/components/viewer/viewer-hover-label.tsx +34 -0
- package/src/components/viewer/viewer-rooms.tsx +191 -0
- package/src/components/viewer/viewer-scene-header.tsx +181 -145
- package/src/components/viewer/viewer-selection-manager.tsx +124 -0
- package/src/components/viewer/viewer-stage.tsx +2 -2
- package/src/components/viewer-zone-system.tsx +3 -8
- package/src/hooks/use-auto-save.integration.test.tsx +215 -43
- package/src/hooks/use-auto-save.ts +60 -9
- package/src/hooks/use-ceiling-events.test.ts +123 -118
- package/src/hooks/use-grid-events.test.ts +105 -105
- package/src/hooks/use-keyboard.test.ts +243 -19
- package/src/hooks/use-keyboard.ts +118 -83
- package/src/hooks/use-open-wall-ends.test.ts +99 -0
- package/src/hooks/use-open-wall-ends.ts +238 -0
- package/src/hooks/use-placement-notice.test.ts +71 -0
- package/src/hooks/use-placement-notice.ts +73 -0
- package/src/hooks/use-selected-room.ts +143 -0
- package/src/index.tsx +312 -58
- package/src/lib/__fixtures__/owner-victor-seattle-parking.json +1 -0
- package/src/lib/__fixtures__/owner-villa-aurelia-basement.json +1 -0
- package/src/lib/add-storey.test.ts +177 -0
- package/src/lib/add-storey.ts +59 -0
- package/src/lib/below-grade.test.tsx +171 -0
- package/src/lib/below-grade.ts +133 -0
- package/src/lib/camera-facing.test.ts +24 -0
- package/src/lib/camera-facing.ts +24 -0
- package/src/lib/ceiling-edit-session.test.ts +321 -0
- package/src/lib/ceiling-edit-session.ts +206 -0
- package/src/lib/ceiling-region-session.test.ts +179 -0
- package/src/lib/continuation.test.ts +19 -1
- package/src/lib/continuation.ts +25 -4
- package/src/lib/direct-manipulation.ts +2 -0
- package/src/lib/door-interaction.ts +14 -0
- package/src/lib/download-project-file.ts +17 -0
- package/src/lib/editor-level-display.test.ts +96 -0
- package/src/lib/editor-level-display.ts +55 -0
- package/src/lib/editor-views.ts +178 -0
- package/src/lib/export-geometry-wait.test.ts +90 -0
- package/src/lib/export-texture-utils.ts +2 -4
- package/src/lib/floor-edit-session.ts +153 -0
- package/src/lib/floor-footprints.test.ts +563 -0
- package/src/lib/floor-footprints.ts +395 -0
- package/src/lib/floor-foundation-support.test.ts +284 -0
- package/src/lib/floor-opening-draft.ts +218 -0
- package/src/lib/floor-openings.test.ts +216 -0
- package/src/lib/floor-region-geometry.ts +191 -0
- package/src/lib/floor-region-session.test.ts +485 -0
- package/src/lib/floor-region-session.ts +589 -0
- package/src/lib/floor-region-snap.ts +101 -0
- package/src/lib/floor-step-hover.test.ts +32 -0
- package/src/lib/floorplan/annotation-visibility.ts +17 -0
- package/src/lib/floorplan/dimension-drive.test.ts +360 -0
- package/src/lib/floorplan/dimension-drive.ts +479 -0
- package/src/lib/floorplan/floorplan-export.test.ts +12 -8
- package/src/lib/floorplan/floorplan-export.tsx +32 -8
- package/src/lib/floorplan/floorplan-extension.ts +32 -0
- package/src/lib/floorplan/floorplan-mode.test.ts +4 -0
- package/src/lib/floorplan/floorplan-mode.ts +2 -0
- package/src/lib/floorplan/floorplan-pdfkit-document.ts +363 -27
- package/src/lib/floorplan/floorplan-pdfkit-renderer.test.ts +177 -8
- package/src/lib/floorplan/floorplan-pdfkit-renderer.ts +60 -25
- package/src/lib/floorplan/floorplan-plugin-install.test.tsx +138 -0
- package/src/lib/floorplan/floorplan-readonly.ts +6 -0
- package/src/lib/floorplan/geometry.ts +13 -6
- package/src/lib/floorplan/index.ts +0 -9
- package/src/lib/floorplan/items.ts +5 -2
- package/src/lib/floorplan/marks.test.ts +213 -0
- package/src/lib/floorplan/marks.ts +238 -0
- package/src/lib/floorplan/open-wall-ends.test.ts +268 -0
- package/src/lib/floorplan/open-wall-ends.ts +162 -0
- package/src/lib/floorplan/pdf-fonts/GeistMono-Bold.ttf +0 -0
- package/src/lib/floorplan/pdf-fonts/GeistMono-Regular.ttf +0 -0
- package/src/lib/floorplan/pdf-fonts/LICENSE-GeistMono.txt +92 -0
- package/src/lib/floorplan/pdf-fonts/LICENSE-LiberationSans.txt +102 -0
- package/src/lib/floorplan/pdf-fonts/LiberationSans-Bold.ttf +0 -0
- package/src/lib/floorplan/pdf-fonts/LiberationSans-Regular.ttf +0 -0
- package/src/lib/floorplan/pdfkit-standard-fonts.d.ts +5 -0
- package/src/lib/floorplan/schedules.test.ts +189 -0
- package/src/lib/floorplan/schedules.ts +339 -0
- package/src/lib/floorplan/sheet-export.test.ts +124 -0
- package/src/lib/floorplan/sheet-export.ts +318 -0
- package/src/lib/floorplan/site-plan/build-site-plan-drawing.ts +768 -0
- package/src/lib/floorplan/site-plan/contours.test.ts +43 -0
- package/src/lib/floorplan/site-plan/contributors.ts +97 -0
- package/src/lib/floorplan/site-plan/coverage.ts +230 -0
- package/src/lib/floorplan/site-plan/drawing-type-switch.test.ts +15 -0
- package/src/lib/floorplan/site-plan/drawing-type-switch.tsx +86 -0
- package/src/lib/floorplan/site-plan/front-edge.test.ts +103 -0
- package/src/lib/floorplan/site-plan/front-edge.ts +157 -0
- package/src/lib/floorplan/site-plan/geometry.test.ts +269 -0
- package/src/lib/floorplan/site-plan/geometry.ts +142 -0
- package/src/lib/floorplan/site-plan/index.ts +55 -0
- package/src/lib/floorplan/site-plan/site-annotations.test.ts +483 -0
- package/src/lib/floorplan/site-plan/site-annotations.ts +1084 -0
- package/src/lib/floorplan/site-plan/site-parts.ts +287 -0
- package/src/lib/floorplan/site-plan/site-plan-layer.tsx +138 -0
- package/src/lib/floorplan/site-plan/street-name.test.ts +69 -0
- package/src/lib/floorplan/site-plan/street-name.ts +302 -0
- package/src/lib/floorplan/site-plan/yards.test.ts +70 -0
- package/src/lib/floorplan/types.ts +1 -27
- package/src/lib/follow-pascal-exterior.test-fixture.ts +17 -0
- package/src/lib/follow-pascal-exterior.test.ts +171 -0
- package/src/lib/follow-pascal-exterior.ts +39 -0
- package/src/lib/follow-pascal-presentation.test.ts +203 -0
- package/src/lib/follow-pascal-presentation.ts +129 -0
- package/src/lib/follow-pascal-sight.ts +84 -0
- package/src/lib/follow-pascal-targets.test.ts +406 -0
- package/src/lib/follow-pascal-targets.ts +198 -0
- package/src/lib/follow-pascal.test.ts +350 -0
- package/src/lib/follow-pascal.ts +284 -0
- package/src/lib/fresh-planar-placement.test.ts +148 -0
- package/src/lib/fresh-planar-placement.ts +114 -37
- package/src/lib/geometry-script/author.ts +157 -0
- package/src/lib/geometry-script/client.ts +159 -0
- package/src/lib/geometry-script/finish-scripts.ts +127 -0
- package/src/lib/geometry-script/protocol.ts +13 -0
- package/src/lib/gesture-lifecycle.test.ts +595 -0
- package/src/lib/gesture-lifecycle.ts +290 -0
- package/src/lib/glb-export-zone-holes.test.ts +41 -0
- package/src/lib/glb-export.test.ts +636 -126
- package/src/lib/glb-export.ts +241 -120
- package/src/lib/history.test.ts +18 -7
- package/src/lib/history.ts +7 -0
- package/src/lib/hud-title.ts +109 -0
- package/src/lib/hydration-gated-storage.ts +49 -0
- package/src/lib/ifc-export.test.ts +174 -0
- package/src/lib/ifc-export.ts +201 -0
- package/src/lib/ifc-parts.ts +15 -0
- package/src/lib/inspector-expanded.test.ts +44 -0
- package/src/lib/inspector-expanded.ts +36 -0
- package/src/lib/interaction/scope.ts +22 -0
- package/src/lib/keyboard-pan.test.ts +129 -7
- package/src/lib/keyboard-pan.ts +161 -12
- package/src/lib/level-selection.test.ts +57 -0
- package/src/lib/level-selection.ts +16 -1
- package/src/lib/live-gesture-round-trip.test.ts +360 -0
- package/src/lib/lot/__fixtures__/map-dossier-miami-shores.json +1 -0
- package/src/lib/lot/__fixtures__/map-dossier-stpete.json +1 -0
- package/src/lib/lot/clean-ring.test.ts +187 -0
- package/src/lib/lot/clean-ring.ts +217 -0
- package/src/lib/lot/dossier.test.ts +202 -0
- package/src/lib/lot/dossier.ts +498 -0
- package/src/lib/lot/drop-in.test.ts +326 -0
- package/src/lib/lot/drop-in.ts +392 -0
- package/src/lib/lot/index.ts +43 -0
- package/src/lib/lot/lot-patch.test.ts +340 -0
- package/src/lib/lot/lot-patch.ts +394 -0
- package/src/lib/lot/parcel-provider.ts +43 -0
- package/src/lib/lot/terrain.test.ts +161 -0
- package/src/lib/lot/terrain.ts +266 -0
- package/src/lib/material-catalog-model.ts +3 -1
- package/src/lib/material-paint-panel-model.ts +9 -5
- package/src/lib/material-paint.ts +4 -0
- package/src/lib/measurement-kind.test.ts +6 -8
- package/src/lib/measurements.test.ts +23 -0
- package/src/lib/mezzanine-actions.test.ts +342 -0
- package/src/lib/mezzanine-draft.test.ts +501 -0
- package/src/lib/mezzanine-draft.ts +278 -0
- package/src/lib/mezzanine-messages.ts +26 -0
- package/src/lib/mezzanine-selection.test.ts +207 -0
- package/src/lib/mezzanine-stairs.ts +46 -0
- package/src/lib/model-export.ts +3 -1
- package/src/lib/node-action-movement.ts +17 -6
- package/src/lib/node-display-label.ts +37 -0
- package/src/lib/orbit-floor-clearance.test.ts +201 -0
- package/src/lib/orbit-floor-clearance.ts +154 -0
- package/src/lib/paint-commit-route.test.ts +37 -0
- package/src/lib/paint-commit-route.ts +17 -0
- package/src/lib/paint-eyedropper.test.tsx +165 -0
- package/src/lib/paint-eyedropper.ts +99 -0
- package/src/lib/paint-pass-through.test.ts +122 -0
- package/src/lib/paint-pass-through.ts +78 -0
- package/src/lib/paint-region-hover.ts +25 -0
- package/src/lib/paint-region-mode.ts +154 -0
- package/src/lib/paint-regions.test.ts +166 -0
- package/src/lib/paint-regions.ts +192 -0
- package/src/lib/paint-scope.test.ts +317 -35
- package/src/lib/paint-scope.ts +267 -25
- package/src/lib/panel-rows.ts +21 -0
- package/src/lib/parametric-node-update.ts +16 -7
- package/src/lib/plate-paint-affected.test.ts +300 -0
- package/src/lib/plate-paint-affected.ts +177 -0
- package/src/lib/plugin-chat.ts +65 -0
- package/src/lib/plugin-panels.test.ts +25 -0
- package/src/lib/plugin-panels.ts +94 -0
- package/src/lib/point-ask/__fixtures__/consumer-browser.tsx +76 -0
- package/src/lib/point-ask/__fixtures__/house.ts +81 -0
- package/src/lib/point-ask/ask-created.test.ts +195 -0
- package/src/lib/point-ask/ask-flow.test.ts +380 -0
- package/src/lib/point-ask/build-context.test.ts +173 -0
- package/src/lib/point-ask/build-context.ts +94 -0
- package/src/lib/point-ask/capture-crop.ts +129 -0
- package/src/lib/point-ask/choreography.ts +69 -0
- package/src/lib/point-ask/compare.test.ts +145 -0
- package/src/lib/point-ask/compare.ts +73 -0
- package/src/lib/point-ask/describe-target.test.ts +165 -0
- package/src/lib/point-ask/describe-target.ts +234 -0
- package/src/lib/point-ask/dictation.ts +79 -0
- package/src/lib/point-ask/edge-marker.test.ts +61 -0
- package/src/lib/point-ask/edge-marker.ts +27 -0
- package/src/lib/point-ask/frame-loop.ts +25 -0
- package/src/lib/point-ask/history-marks.test.ts +94 -0
- package/src/lib/point-ask/history-marks.ts +39 -0
- package/src/lib/point-ask/host-api.ts +32 -0
- package/src/lib/point-ask/outline-box.test.ts +30 -0
- package/src/lib/point-ask/outline-box.ts +11 -0
- package/src/lib/point-ask/overlay-metrics.ts +6 -0
- package/src/lib/point-ask/pick.test.ts +220 -0
- package/src/lib/point-ask/pick.ts +121 -0
- package/src/lib/point-ask/pin-machine.test.ts +494 -0
- package/src/lib/point-ask/pin-machine.ts +217 -0
- package/src/lib/point-ask/placement.test.ts +203 -0
- package/src/lib/point-ask/placement.ts +141 -0
- package/src/lib/point-ask/pointer-tracker.ts +34 -0
- package/src/lib/point-ask/projector.ts +83 -0
- package/src/lib/point-ask/region.test.ts +157 -0
- package/src/lib/point-ask/region.ts +136 -0
- package/src/lib/point-ask/retry.test.ts +217 -0
- package/src/lib/point-ask/retry.ts +37 -0
- package/src/lib/point-ask/scene-commit.test.ts +312 -0
- package/src/lib/point-ask/scene-commit.ts +32 -0
- package/src/lib/point-ask/session.ts +1391 -0
- package/src/lib/point-ask/spring-keyframes.test.ts +45 -0
- package/src/lib/point-ask/spring-keyframes.ts +38 -0
- package/src/lib/point-ask/spring.test.ts +63 -0
- package/src/lib/point-ask/spring.ts +107 -0
- package/src/lib/point-ask/suggestions.test.ts +80 -0
- package/src/lib/point-ask/suggestions.ts +125 -0
- package/src/lib/point-ask/target-scope.test.ts +622 -0
- package/src/lib/point-ask/target-scope.ts +338 -0
- package/src/lib/point-ask/types.ts +48 -0
- package/src/lib/portable-export.test.ts +34 -1
- package/src/lib/portable-export.ts +119 -53
- package/src/lib/print-feature-thickness.ts +10 -1
- package/src/lib/print-roof-solids.test.ts +39 -1
- package/src/lib/print-roof-solids.ts +43 -10
- package/src/lib/print-shell-compiler-manifold-core.ts +1 -1
- package/src/lib/print-shell-compiler.ts +1 -0
- package/src/lib/registered-draft-snap.ts +12 -0
- package/src/lib/roof-duplication.ts +10 -2
- package/src/lib/room-assembly-overlay.test.ts +275 -0
- package/src/lib/room-assembly-overlay.ts +265 -0
- package/src/lib/room-built-on.test.ts +379 -0
- package/src/lib/room-built-on.ts +138 -0
- package/src/lib/room-construction-commands.ts +712 -0
- package/src/lib/room-construction.test.ts +746 -0
- package/src/lib/room-construction.ts +385 -0
- package/src/lib/room-controls-phase6.test.ts +630 -0
- package/src/lib/room-divide-session.ts +523 -0
- package/src/lib/room-handle-drag.ts +1068 -0
- package/src/lib/room-name-catalog.test.ts +90 -0
- package/src/lib/room-name-catalog.ts +168 -0
- package/src/lib/room-push-dimensions.test.ts +311 -0
- package/src/lib/room-push-dimensions.ts +251 -0
- package/src/lib/room-selection-commands.ts +62 -0
- package/src/lib/room-selection.test.ts +1017 -0
- package/src/lib/room-selection.ts +417 -0
- package/src/lib/room-structure-commands.ts +94 -0
- package/src/lib/room-structure.test.ts +921 -0
- package/src/lib/room-transform-session.ts +570 -0
- package/src/lib/room-zone-routing.test.ts +294 -0
- package/src/lib/room-zone-routing.ts +197 -0
- package/src/lib/scene-clipboard.test.ts +610 -1
- package/src/lib/scene-clipboard.ts +269 -58
- package/src/lib/scene-material-refs.test.ts +79 -0
- package/src/lib/scene-material-refs.ts +79 -0
- package/src/lib/scene.test.ts +24 -36
- package/src/lib/scene.ts +33 -57
- package/src/lib/selection-routing.test.ts +129 -45
- package/src/lib/selection-routing.ts +54 -75
- package/src/lib/session-writes.test.ts +120 -0
- package/src/lib/session-writes.ts +205 -0
- package/src/lib/sfx-bus.ts +13 -0
- package/src/lib/sfx-player.test.ts +70 -9
- package/src/lib/sfx-player.ts +6 -0
- package/src/lib/sidebar-panel.ts +62 -0
- package/src/lib/site-boundary.test.ts +4 -6
- package/src/lib/site-boundary.ts +1 -1
- package/src/lib/snapping-mode.test.ts +4 -0
- package/src/lib/snapping-mode.ts +8 -1
- package/src/lib/stair-levels.test.ts +25 -0
- package/src/lib/stair-levels.ts +22 -3
- package/src/lib/terrace-draft.test.ts +145 -0
- package/src/lib/terrace-draft.ts +214 -0
- package/src/lib/tool-cursor-icon.test.ts +16 -0
- package/src/lib/tool-cursor-icon.ts +11 -0
- package/src/lib/typing-target.ts +9 -0
- package/src/lib/units.ts +77 -39
- package/src/lib/vector-edges.test.ts +58 -0
- package/src/lib/vector-edges.ts +400 -0
- package/src/lib/viewer-selection.test.ts +577 -0
- package/src/lib/viewer-selection.ts +580 -0
- package/src/lib/wall-draw-variant.test.ts +71 -0
- package/src/lib/wall-draw-variant.ts +91 -0
- package/src/lib/wall-paint-region-session.ts +246 -0
- package/src/lib/wall-paint-region.test.ts +373 -0
- package/src/lib/wall-placement-pointer.test.ts +42 -0
- package/src/lib/wall-placement-pointer.ts +57 -0
- package/src/lib/wall-polygon-draft.test.ts +262 -0
- package/src/lib/wall-polygon-draft.ts +286 -0
- package/src/lib/wall-push.test.ts +453 -0
- package/src/lib/wall-region-face.ts +352 -0
- package/src/lib/wall-region-handles.test.ts +304 -0
- package/src/lib/wall-region-handles.ts +368 -0
- package/src/lib/wall-region-list.test.ts +119 -0
- package/src/lib/wall-region-list.ts +136 -0
- package/src/lib/wall-region-snap.ts +110 -0
- package/src/lib/window-changes.ts +35 -0
- package/src/lib/window-interaction.ts +14 -0
- package/src/lib/zone-content.ts +4 -1
- package/src/lib/zone-name.test.ts +17 -0
- package/src/lib/zone-name.ts +17 -0
- package/src/store/opens-in-select-mode.test.ts +223 -0
- package/src/store/paint-mode.test.ts +188 -0
- package/src/store/terrain-sculpt-mode.test.ts +19 -21
- package/src/store/tool-mode.test.ts +137 -6
- package/src/store/use-audio.test.ts +146 -0
- package/src/store/use-audio.tsx +127 -3
- package/src/store/use-construction-reveal.test.ts +173 -0
- package/src/store/use-construction-reveal.ts +145 -0
- package/src/store/use-corner-snap-hint.test.ts +24 -0
- package/src/store/use-corner-snap-hint.ts +47 -0
- package/src/store/use-delete-confirmation.ts +28 -1
- package/src/store/use-drawing-view.test.ts +24 -3
- package/src/store/use-drawing-view.ts +29 -1
- package/src/store/use-editor.tsx +432 -321
- package/src/store/use-floorplan-draft-preview.ts +13 -0
- package/src/store/use-floorplan-marquee.ts +12 -2
- package/src/store/use-follow-pascal.test.ts +62 -0
- package/src/store/use-follow-pascal.ts +53 -0
- package/src/store/use-point-ask.ts +247 -0
- package/src/store/use-quick-measurement-hud.ts +3 -3
- package/src/store/use-stair-build-preview.ts +33 -5
- package/src/store/use-wall-move-ghosts.ts +4 -2
- package/src/store/use-wall-region-handles-selection.test.ts +222 -0
- package/src/store/use-wall-region-handles-selection.ts +139 -0
- package/src/store/view-layout.test.ts +201 -0
- package/src/store/view-layout.ts +199 -0
- package/src/test-utils/immediate-animation-frames.ts +16 -0
- package/src/test-utils/live-gesture-round-trip.ts +224 -0
- package/src/test-utils/render-current-markup.tsx +16 -0
- package/src/test-utils/selection-harness.ts +82 -0
- package/src/components/editor-2d/renderers/floorplan-stair-layer.tsx +0 -499
- package/src/components/systems/zone/zone-label-editor-system.tsx +0 -333
- package/src/components/tools/fence/fence-drafting.test.ts +0 -100
- package/src/components/tools/fence/fence-drafting.ts +0 -266
- package/src/components/tools/stair/stair-click-guard.test.ts +0 -58
- package/src/components/tools/stair/stair-click-guard.ts +0 -72
- package/src/components/tools/stair/stair-defaults.ts +0 -18
- package/src/components/tools/stair/stair-tool.tsx +0 -634
- package/src/components/ui/sidebar/panels/plugins-panel.tsx +0 -218
- package/src/components/viewer/viewer-units-panel.tsx +0 -116
- package/src/lib/floorplan/stairs.ts +0 -489
- package/src/lib/level-duplication.test.ts +0 -144
- package/src/lib/level-duplication.ts +0 -195
|
@@ -1,18 +1,23 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
3
|
import {
|
|
4
|
-
type AnyNodeId,
|
|
5
4
|
emitter,
|
|
5
|
+
isCaptureSessionOn,
|
|
6
|
+
levelGradeStanding,
|
|
6
7
|
sceneRegistry,
|
|
8
|
+
subscribeCaptureSessions,
|
|
7
9
|
type ThumbnailGenerateEvent,
|
|
8
10
|
useScene,
|
|
9
11
|
} from '@pascal-app/core'
|
|
12
|
+
import { sceneViewInterior } from '@pascal-app/core/agent-operations'
|
|
10
13
|
import {
|
|
11
14
|
computeHeroFraming,
|
|
12
15
|
createSnapshotPipeline,
|
|
13
16
|
GRID_LAYER,
|
|
14
|
-
getVisibleWallMaterials,
|
|
15
17
|
heroCameraPose,
|
|
18
|
+
holdLiveFrame,
|
|
19
|
+
pendingSceneBuildCount,
|
|
20
|
+
presentLevelsForCapture,
|
|
16
21
|
refreshIsolation,
|
|
17
22
|
SNAPSHOT_MAX_EDGE,
|
|
18
23
|
SNAPSHOT_MIME,
|
|
@@ -21,8 +26,12 @@ import {
|
|
|
21
26
|
snapLevelsToTruePositions,
|
|
22
27
|
THUMBNAIL_HEIGHT,
|
|
23
28
|
THUMBNAIL_WIDTH,
|
|
29
|
+
temporarilyGroundWindowForCapture,
|
|
24
30
|
temporarilyHideNodeTypes,
|
|
31
|
+
temporarilyShowPresentation,
|
|
25
32
|
temporarilyShowShadowOnly,
|
|
33
|
+
temporarilyVeilGround,
|
|
34
|
+
temporarilyWithoutAmbiance,
|
|
26
35
|
useSceneAtmosphere,
|
|
27
36
|
useViewer,
|
|
28
37
|
} from '@pascal-app/viewer'
|
|
@@ -30,12 +39,13 @@ import type { CameraControls } from '@react-three/drei'
|
|
|
30
39
|
import { useThree } from '@react-three/fiber'
|
|
31
40
|
import { useCallback, useEffect, useRef } from 'react'
|
|
32
41
|
import * as THREE from 'three'
|
|
33
|
-
import type
|
|
42
|
+
import { CanvasTarget, ClippingGroup, type WebGPURenderer } from 'three/webgpu'
|
|
34
43
|
import { EDITOR_LAYER } from '../../lib/constants'
|
|
35
44
|
import {
|
|
36
45
|
applySnapshotCapturePose,
|
|
37
46
|
captureSnapshotScene,
|
|
38
47
|
createSnapshotQueue,
|
|
48
|
+
deliverSnapshot,
|
|
39
49
|
enqueueSnapshotCapture,
|
|
40
50
|
runSnapshotCapture,
|
|
41
51
|
} from './snapshot-capture'
|
|
@@ -50,6 +60,7 @@ export interface SnapshotCameraData {
|
|
|
50
60
|
zoom?: number
|
|
51
61
|
captureMode?: 'standard' | 'viewport' | 'area'
|
|
52
62
|
resolution?: { w: number; h: number }
|
|
63
|
+
presentation?: ThumbnailGenerateEvent['presentation']
|
|
53
64
|
}
|
|
54
65
|
|
|
55
66
|
interface ThumbnailGeneratorProps {
|
|
@@ -59,6 +70,150 @@ interface ThumbnailGeneratorProps {
|
|
|
59
70
|
/** Metres ahead of a controls-less camera to place the stored snapshot target. */
|
|
60
71
|
const FIRST_PERSON_TARGET_DISTANCE = 8
|
|
61
72
|
|
|
73
|
+
/** The long edge, in pixels, of a supersampled capture's picture at most (it may render in tiles). */
|
|
74
|
+
const MAX_CAPTURE_EDGE = 8192
|
|
75
|
+
/** The long edge of one render pass at most, whatever the GPU allows: the post-processing targets scale with it. */
|
|
76
|
+
const MAX_PASS_EDGE = 4096
|
|
77
|
+
/** A supersampled picture's pixels at most on a GPU, and on a software renderer (SwiftShader, llvmpipe) — its time budget. */
|
|
78
|
+
const MAX_CAPTURE_PIXELS = 24_000_000
|
|
79
|
+
const MAX_SOFTWARE_CAPTURE_PIXELS = 16_000_000
|
|
80
|
+
/** Pixels a tile renders past its edge on each inner side, cropped off: the AO and the ink sample across it. */
|
|
81
|
+
const TILE_OVERLAP = 96
|
|
82
|
+
|
|
83
|
+
type CaptureLimits = { passEdge: number; maxPixels: number; software: boolean }
|
|
84
|
+
|
|
85
|
+
export function captureDimensions(
|
|
86
|
+
canvasSize: readonly [number, number],
|
|
87
|
+
event: Pick<ThumbnailGenerateEvent, 'ortho' | 'perspective' | 'standardSize' | 'supersample'>,
|
|
88
|
+
maxPixels: number,
|
|
89
|
+
): [number, number] {
|
|
90
|
+
const supersample = event.supersample ?? 1
|
|
91
|
+
// A requested camera must not inherit the editor pane's aspect or be upscaled from its crop.
|
|
92
|
+
const size =
|
|
93
|
+
(event.ortho || event.perspective) && event.standardSize && supersample === 1
|
|
94
|
+
? [event.standardSize.w, event.standardSize.h]
|
|
95
|
+
: canvasSize
|
|
96
|
+
const scale = Math.min(
|
|
97
|
+
Math.max(1, supersample),
|
|
98
|
+
MAX_CAPTURE_EDGE / Math.max(size[0], size[1]),
|
|
99
|
+
Math.sqrt(maxPixels / (size[0] * size[1])),
|
|
100
|
+
)
|
|
101
|
+
return [Math.round(size[0] * scale), Math.round(size[1] * scale)]
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* What one capture may ask of this GPU: a pass no larger than its biggest
|
|
106
|
+
* texture (and MAX_PASS_EDGE), and — on a software renderer, where a big
|
|
107
|
+
* frame takes minutes — fewer pixels.
|
|
108
|
+
*/
|
|
109
|
+
function captureLimits(renderer: unknown): CaptureLimits {
|
|
110
|
+
const backend = (
|
|
111
|
+
renderer as {
|
|
112
|
+
backend?: {
|
|
113
|
+
device?: {
|
|
114
|
+
limits?: { maxTextureDimension2D?: number }
|
|
115
|
+
adapterInfo?: { architecture?: string; description?: string; vendor?: string }
|
|
116
|
+
}
|
|
117
|
+
gl?: WebGL2RenderingContext
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
).backend
|
|
121
|
+
let gpuEdge = backend?.device?.limits?.maxTextureDimension2D ?? 0
|
|
122
|
+
let rendererName = [
|
|
123
|
+
backend?.device?.adapterInfo?.vendor,
|
|
124
|
+
backend?.device?.adapterInfo?.architecture,
|
|
125
|
+
backend?.device?.adapterInfo?.description,
|
|
126
|
+
].join(' ')
|
|
127
|
+
const gl = backend?.gl
|
|
128
|
+
if (gl) {
|
|
129
|
+
gpuEdge = Math.min(
|
|
130
|
+
gl.getParameter(gl.MAX_TEXTURE_SIZE) as number,
|
|
131
|
+
gl.getParameter(gl.MAX_RENDERBUFFER_SIZE) as number,
|
|
132
|
+
)
|
|
133
|
+
const info = gl.getExtension('WEBGL_debug_renderer_info')
|
|
134
|
+
rendererName = String(gl.getParameter(info ? info.UNMASKED_RENDERER_WEBGL : gl.RENDERER))
|
|
135
|
+
}
|
|
136
|
+
const software = /swiftshader|llvmpipe|software|softpipe/i.test(rendererName)
|
|
137
|
+
const limits = {
|
|
138
|
+
passEdge: Math.min(MAX_PASS_EDGE, gpuEdge || MAX_PASS_EDGE),
|
|
139
|
+
maxPixels: software ? MAX_SOFTWARE_CAPTURE_PIXELS : MAX_CAPTURE_PIXELS,
|
|
140
|
+
software,
|
|
141
|
+
}
|
|
142
|
+
// dev: `window.__pascalCaptureLimits = { passEdge: 2048 }` forces tiling (or a budget) for a probe
|
|
143
|
+
if (process.env.NODE_ENV === 'production') return limits
|
|
144
|
+
return {
|
|
145
|
+
...limits,
|
|
146
|
+
...(window as unknown as { __pascalCaptureLimits?: Partial<CaptureLimits> })
|
|
147
|
+
.__pascalCaptureLimits,
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
type Tile = {
|
|
152
|
+
/** The pixels the tile renders, overlap included. */
|
|
153
|
+
x: number
|
|
154
|
+
y: number
|
|
155
|
+
w: number
|
|
156
|
+
h: number
|
|
157
|
+
/** The pixels it contributes to the picture. */
|
|
158
|
+
inner: { x: number; y: number; w: number; h: number }
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* A `width × height` picture split into equal tiles no larger than
|
|
163
|
+
* `passEdge`, each rendered TILE_OVERLAP past its inner edges so the
|
|
164
|
+
* screen-space passes (AO, ink, FXAA) have their neighbourhood at a seam.
|
|
165
|
+
* One tile when the picture fits a pass.
|
|
166
|
+
*/
|
|
167
|
+
export function tileGrid(width: number, height: number, passEdge: number): Tile[] {
|
|
168
|
+
const room = passEdge - 2 * TILE_OVERLAP
|
|
169
|
+
const cols = width <= passEdge ? 1 : Math.ceil(width / room)
|
|
170
|
+
const rows = height <= passEdge ? 1 : Math.ceil(height / room)
|
|
171
|
+
const tiles: Tile[] = []
|
|
172
|
+
for (let row = 0; row < rows; row++) {
|
|
173
|
+
for (let col = 0; col < cols; col++) {
|
|
174
|
+
const ix = Math.round((col * width) / cols)
|
|
175
|
+
const iy = Math.round((row * height) / rows)
|
|
176
|
+
const iw = Math.round(((col + 1) * width) / cols) - ix
|
|
177
|
+
const ih = Math.round(((row + 1) * height) / rows) - iy
|
|
178
|
+
const x = Math.max(0, ix - TILE_OVERLAP)
|
|
179
|
+
const y = Math.max(0, iy - TILE_OVERLAP)
|
|
180
|
+
const w = Math.min(width, ix + iw + TILE_OVERLAP) - x
|
|
181
|
+
const h = Math.min(height, iy + ih + TILE_OVERLAP) - y
|
|
182
|
+
tiles.push({ x, y, w, h, inner: { x: ix, y: iy, w: iw, h: ih } })
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
return tiles
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* `render` with the renderer's canvas target swapped for a detached one of
|
|
190
|
+
* `width × height`. Every pass of a capture pipeline sizes itself from the
|
|
191
|
+
* renderer's drawing buffer, so a supersampled capture renders at its own
|
|
192
|
+
* size into its own targets while the live canvas — and the live pipeline's
|
|
193
|
+
* targets — keep theirs. Only for renders into a render target: nothing may
|
|
194
|
+
* draw to the screen while the stand-in is current.
|
|
195
|
+
*/
|
|
196
|
+
export function atCaptureSize<T>(
|
|
197
|
+
renderer: WebGPURenderer,
|
|
198
|
+
width: number,
|
|
199
|
+
height: number,
|
|
200
|
+
render: () => T,
|
|
201
|
+
): T {
|
|
202
|
+
const live = renderer.getCanvasTarget()
|
|
203
|
+
if (live.domElement.width === width && live.domElement.height === height) return render()
|
|
204
|
+
const stand = document.createElement('canvas')
|
|
205
|
+
stand.width = width
|
|
206
|
+
stand.height = height
|
|
207
|
+
const target = new CanvasTarget(stand)
|
|
208
|
+
renderer.setCanvasTarget(target)
|
|
209
|
+
try {
|
|
210
|
+
return render()
|
|
211
|
+
} finally {
|
|
212
|
+
renderer.setCanvasTarget(live)
|
|
213
|
+
target.dispose()
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
62
217
|
function clampSnapshotSize(width: number, height: number): { w: number; h: number } {
|
|
63
218
|
const maxEdge = Math.max(width, height)
|
|
64
219
|
if (maxEdge <= SNAPSHOT_MAX_EDGE) return { w: width, h: height }
|
|
@@ -67,11 +222,402 @@ function clampSnapshotSize(width: number, height: number): { w: number; h: numbe
|
|
|
67
222
|
return { w: Math.round(width * scale), h: Math.round(height * scale) }
|
|
68
223
|
}
|
|
69
224
|
|
|
225
|
+
/**
|
|
226
|
+
* Every directional light re-aimed at the face a capture looks at — from the
|
|
227
|
+
* camera's side of it, 35° above the horizon, a quarter to the camera's
|
|
228
|
+
* left so returns still read — and brightened a little; the returned
|
|
229
|
+
* function puts them back. The lights' own drift toward their theme config
|
|
230
|
+
* (lights.tsx lerps per frame) resumes afterwards.
|
|
231
|
+
*/
|
|
232
|
+
function aimLightsAtFace(
|
|
233
|
+
scene: THREE.Scene,
|
|
234
|
+
position: readonly [number, number, number],
|
|
235
|
+
target: readonly [number, number, number],
|
|
236
|
+
): () => void {
|
|
237
|
+
const aim = new THREE.Vector3(target[0], target[1], target[2])
|
|
238
|
+
const toCamera = new THREE.Vector3(position[0], position[1], position[2]).sub(aim)
|
|
239
|
+
toCamera.y = 0
|
|
240
|
+
if (toCamera.lengthSq() < 1e-9) return () => {}
|
|
241
|
+
toCamera.normalize()
|
|
242
|
+
const left = new THREE.Vector3(0, 1, 0).cross(toCamera).normalize()
|
|
243
|
+
const up = Math.sin((35 * Math.PI) / 180)
|
|
244
|
+
const along = Math.cos((35 * Math.PI) / 180)
|
|
245
|
+
const direction = toCamera
|
|
246
|
+
.clone()
|
|
247
|
+
.multiplyScalar(along)
|
|
248
|
+
.add(new THREE.Vector3(0, up, 0))
|
|
249
|
+
.add(left.multiplyScalar(0.25))
|
|
250
|
+
.normalize()
|
|
251
|
+
const restores: (() => void)[] = []
|
|
252
|
+
scene.traverse((object) => {
|
|
253
|
+
const light = object as THREE.DirectionalLight
|
|
254
|
+
if (!light.isDirectionalLight) return
|
|
255
|
+
const savedPosition = light.position.clone()
|
|
256
|
+
const savedTarget = light.target.position.clone()
|
|
257
|
+
const savedIntensity = light.intensity
|
|
258
|
+
light.position.copy(aim.clone().add(direction.clone().multiplyScalar(150)))
|
|
259
|
+
light.target.position.copy(aim)
|
|
260
|
+
light.target.updateMatrixWorld()
|
|
261
|
+
light.updateMatrixWorld()
|
|
262
|
+
light.intensity = savedIntensity * 1.15
|
|
263
|
+
restores.push(() => {
|
|
264
|
+
light.position.copy(savedPosition)
|
|
265
|
+
light.target.position.copy(savedTarget)
|
|
266
|
+
light.target.updateMatrixWorld()
|
|
267
|
+
light.updateMatrixWorld()
|
|
268
|
+
light.intensity = savedIntensity
|
|
269
|
+
})
|
|
270
|
+
})
|
|
271
|
+
return () => {
|
|
272
|
+
for (const restore of restores) restore()
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/** The step, in seconds of R3F's clock, a frame run by hand takes (FrameLimiter's own `kick`). */
|
|
277
|
+
const PUMPED_FRAME_SECONDS = 1 / 1000
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Run the frame loop `n` times by hand — the useFrame subscribers and the
|
|
281
|
+
* render — a macrotask apart so React's commits land between the frames.
|
|
282
|
+
*
|
|
283
|
+
* With `frameloop="never"` R3F takes each frame's delta as `timestamp −
|
|
284
|
+
* clock.elapsedTime`, and the viewer's FrameLimiter keeps that clock in
|
|
285
|
+
* SECONDS since it mounted. A pumped frame steps it by a millisecond and puts
|
|
286
|
+
* it back, so the next real frame gets exactly its own delta. (Pumping it with
|
|
287
|
+
* `performance.now()` — milliseconds — handed the next real frame a delta of
|
|
288
|
+
* minus hundreds of thousands of seconds: every delta-scaled lerp blew up, the
|
|
289
|
+
* lights and the backdrop to ±1e6, and the 3D view went black, then colours,
|
|
290
|
+
* while the next capture came back black or blown out white — 2026-09-22.)
|
|
291
|
+
*/
|
|
292
|
+
export async function pumpFrames(
|
|
293
|
+
advance: (timestamp: number, runGlobalEffects?: boolean) => void,
|
|
294
|
+
clock: { elapsedTime: number; oldTime: number },
|
|
295
|
+
n: number,
|
|
296
|
+
): Promise<void> {
|
|
297
|
+
for (let i = 0; i < n; i++) {
|
|
298
|
+
const { elapsedTime, oldTime } = clock
|
|
299
|
+
try {
|
|
300
|
+
advance(elapsedTime + PUMPED_FRAME_SECONDS, true)
|
|
301
|
+
} finally {
|
|
302
|
+
clock.elapsedTime = elapsedTime
|
|
303
|
+
clock.oldTime = oldTime
|
|
304
|
+
}
|
|
305
|
+
await macrotask()
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* Objects a renderer tagged `userData.excludeFromCapture` (a plugin's site
|
|
311
|
+
* context, e.g. a utility pole and its drop) hidden for a sheet's capture; the
|
|
312
|
+
* returned function shows them again.
|
|
313
|
+
*/
|
|
314
|
+
export function hideCaptureExcluded(scene: THREE.Scene): () => void {
|
|
315
|
+
const hidden: THREE.Object3D[] = []
|
|
316
|
+
scene.traverse((object) => {
|
|
317
|
+
if (
|
|
318
|
+
(object.userData as { excludeFromCapture?: unknown }).excludeFromCapture !== true ||
|
|
319
|
+
!object.visible
|
|
320
|
+
)
|
|
321
|
+
return
|
|
322
|
+
object.visible = false
|
|
323
|
+
hidden.push(object)
|
|
324
|
+
})
|
|
325
|
+
return () => {
|
|
326
|
+
for (const object of hidden) object.visible = true
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* A section looks INTO the house, and what it sees there — the attic under
|
|
332
|
+
* the roof, a porch roof's underside — faces away from the sun re-aimed at
|
|
333
|
+
* the cut: it read as a solid dark mass. For the frame the unshadowed fill
|
|
334
|
+
* lights shine from the camera and the hemisphere's ground takes the sky's
|
|
335
|
+
* colour, so the attic reads as a light void with its roof and ceiling
|
|
336
|
+
* lines; the returned function puts the lights back.
|
|
337
|
+
*/
|
|
338
|
+
function lightInterior(scene: THREE.Scene, camera: THREE.Camera): () => void {
|
|
339
|
+
const look = camera.getWorldDirection(new THREE.Vector3())
|
|
340
|
+
const eye = camera.getWorldPosition(new THREE.Vector3())
|
|
341
|
+
const restores: (() => void)[] = []
|
|
342
|
+
scene.traverse((object) => {
|
|
343
|
+
const directional = object as THREE.DirectionalLight
|
|
344
|
+
if (directional.isDirectionalLight && !directional.castShadow) {
|
|
345
|
+
const savedPosition = directional.position.clone()
|
|
346
|
+
const savedTarget = directional.target.position.clone()
|
|
347
|
+
const savedIntensity = directional.intensity
|
|
348
|
+
directional.position.copy(eye)
|
|
349
|
+
directional.target.position.copy(eye.clone().add(look))
|
|
350
|
+
directional.target.updateMatrixWorld()
|
|
351
|
+
directional.updateMatrixWorld()
|
|
352
|
+
directional.intensity = Math.max(savedIntensity, INTERIOR_FILL_INTENSITY)
|
|
353
|
+
restores.push(() => {
|
|
354
|
+
directional.position.copy(savedPosition)
|
|
355
|
+
directional.target.position.copy(savedTarget)
|
|
356
|
+
directional.target.updateMatrixWorld()
|
|
357
|
+
directional.updateMatrixWorld()
|
|
358
|
+
directional.intensity = savedIntensity
|
|
359
|
+
})
|
|
360
|
+
return
|
|
361
|
+
}
|
|
362
|
+
const hemisphere = object as THREE.HemisphereLight
|
|
363
|
+
if (hemisphere.isHemisphereLight) {
|
|
364
|
+
const savedGround = hemisphere.groundColor.clone()
|
|
365
|
+
hemisphere.groundColor.copy(hemisphere.color)
|
|
366
|
+
restores.push(() => hemisphere.groundColor.copy(savedGround))
|
|
367
|
+
}
|
|
368
|
+
})
|
|
369
|
+
return () => {
|
|
370
|
+
for (const restore of restores) restore()
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
/** The fill a section's interior gets from the camera's side (the key light is the re-aimed sun). */
|
|
375
|
+
const INTERIOR_FILL_INTENSITY = 2
|
|
376
|
+
|
|
377
|
+
/** Why a sheet's capture refuses to run in a background tab — the words reach the Architect. */
|
|
378
|
+
const HIDDEN_TAB =
|
|
379
|
+
'The Pascal tab is in the background, where the 3D viewer draws no frames — bring it to the front.'
|
|
380
|
+
|
|
381
|
+
function isTabHidden(): boolean {
|
|
382
|
+
return typeof document !== 'undefined' && document.visibilityState === 'hidden'
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
/** A scene that stops settling for this long holds a node no system will ever build. */
|
|
386
|
+
const SCENE_BUILD_STALL_MS = 15_000
|
|
387
|
+
/** However busy the scene, a capture waits no longer than this for it. */
|
|
388
|
+
const SCENE_BUILD_MAX_MS = 120_000
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* Until the scene has built what the picture shows — walls, roofs, openings,
|
|
392
|
+
* every item's model (a loading placeholder draws through the roof) — the
|
|
393
|
+
* viewer's own scene-ready test. Progress-based: a slow GPU loading a big
|
|
394
|
+
* house keeps it waiting while the pending count moves; a count that stops
|
|
395
|
+
* moving (a node no system settles) lets the capture go. Returns the count
|
|
396
|
+
* left.
|
|
397
|
+
*/
|
|
398
|
+
async function untilSceneBuilt(): Promise<number> {
|
|
399
|
+
const started = performance.now()
|
|
400
|
+
let pending = pendingSceneBuildCount()
|
|
401
|
+
let changedAt = started
|
|
402
|
+
while (pending > 0) {
|
|
403
|
+
if (isTabHidden()) throw new Error(HIDDEN_TAB)
|
|
404
|
+
const now = performance.now()
|
|
405
|
+
if (now - changedAt > SCENE_BUILD_STALL_MS || now - started > SCENE_BUILD_MAX_MS) break
|
|
406
|
+
await new Promise((resolve) => setTimeout(resolve, 100))
|
|
407
|
+
const next = pendingSceneBuildCount()
|
|
408
|
+
if (next !== pending) {
|
|
409
|
+
pending = next
|
|
410
|
+
changedAt = performance.now()
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
return pending
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
/** `promise`, or a rejection with `message` after `ms` — a wait that must not hold the capture queue for good. */
|
|
417
|
+
function within<T>(promise: Promise<T>, ms: number, message: string): Promise<T> {
|
|
418
|
+
let timer: ReturnType<typeof setTimeout> | undefined
|
|
419
|
+
return Promise.race([
|
|
420
|
+
promise,
|
|
421
|
+
new Promise<never>((_, reject) => {
|
|
422
|
+
timer = setTimeout(() => reject(new Error(message)), ms)
|
|
423
|
+
}),
|
|
424
|
+
]).finally(() => clearTimeout(timer))
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
/** A readback or a host callback that takes longer than this is abandoned so the next capture runs. */
|
|
428
|
+
const CAPTURE_SETTLE_MS = 120_000
|
|
429
|
+
|
|
430
|
+
/** After a capture session puts the user's view back, the viewport resumes once the restored materials are in. */
|
|
431
|
+
const SESSION_RESUME_MS = 250
|
|
432
|
+
|
|
433
|
+
/** The longest a capture keeps the viewport on its last frame, should its release never come. */
|
|
434
|
+
const HOLD_MAX_MS = 300_000
|
|
435
|
+
|
|
436
|
+
/**
|
|
437
|
+
* What the sheets' pictures are drawn in, whatever the user's view: full-
|
|
438
|
+
* height walls, the storeys stacked at their true heights, the materials'
|
|
439
|
+
* own textures and colours (a white or blueprint palette is a view, not the
|
|
440
|
+
* house). The shading stays the user's: the capture brings its own AO.
|
|
441
|
+
*/
|
|
442
|
+
const FINISHED_PRESENTATION = {
|
|
443
|
+
wallMode: 'up',
|
|
444
|
+
levelMode: 'stacked',
|
|
445
|
+
textures: true,
|
|
446
|
+
colorPreset: 'clay',
|
|
447
|
+
} as const
|
|
448
|
+
|
|
449
|
+
/** The viewer switched to FINISHED_PRESENTATION; the returned function puts the user's view back. */
|
|
450
|
+
export function presentFinished(): () => void {
|
|
451
|
+
const viewer = useViewer.getState()
|
|
452
|
+
const restores: (() => void)[] = []
|
|
453
|
+
if (viewer.wallMode !== FINISHED_PRESENTATION.wallMode) {
|
|
454
|
+
const wallMode = viewer.wallMode
|
|
455
|
+
viewer.setWallMode(FINISHED_PRESENTATION.wallMode)
|
|
456
|
+
restores.push(() => useViewer.getState().setWallMode(wallMode))
|
|
457
|
+
}
|
|
458
|
+
if (viewer.levelMode !== FINISHED_PRESENTATION.levelMode) {
|
|
459
|
+
const levelMode = viewer.levelMode
|
|
460
|
+
viewer.setLevelMode(FINISHED_PRESENTATION.levelMode)
|
|
461
|
+
restores.push(() => useViewer.getState().setLevelMode(levelMode))
|
|
462
|
+
}
|
|
463
|
+
if (viewer.textures !== FINISHED_PRESENTATION.textures) {
|
|
464
|
+
const textures = viewer.textures
|
|
465
|
+
viewer.setTextures(FINISHED_PRESENTATION.textures)
|
|
466
|
+
restores.push(() => useViewer.getState().setTextures(textures))
|
|
467
|
+
}
|
|
468
|
+
if (viewer.colorPreset !== FINISHED_PRESENTATION.colorPreset) {
|
|
469
|
+
const colorPreset = viewer.colorPreset
|
|
470
|
+
viewer.setColorPreset(FINISHED_PRESENTATION.colorPreset)
|
|
471
|
+
restores.push(() => useViewer.getState().setColorPreset(colorPreset))
|
|
472
|
+
}
|
|
473
|
+
return () => {
|
|
474
|
+
for (const restore of restores.reverse()) restore()
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
/**
|
|
479
|
+
* The pictures' scene while a capture session is on (core's `isCaptureSessionOn`, runs that overlap
|
|
480
|
+
* counted once): `present` when the first starts, and what it returns, which puts the user's view
|
|
481
|
+
* back, when the last ends. Returns the stop, which puts the view back if a session is still on.
|
|
482
|
+
*/
|
|
483
|
+
export function presentWhileCapturing(present: () => () => void): () => void {
|
|
484
|
+
let restore: (() => void) | null = null
|
|
485
|
+
const follow = () => {
|
|
486
|
+
if (isCaptureSessionOn() && !restore) restore = present()
|
|
487
|
+
else if (!isCaptureSessionOn() && restore) {
|
|
488
|
+
restore()
|
|
489
|
+
restore = null
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
follow()
|
|
493
|
+
const unsubscribe = subscribeCaptureSessions(follow)
|
|
494
|
+
return () => {
|
|
495
|
+
unsubscribe()
|
|
496
|
+
restore?.()
|
|
497
|
+
restore = null
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
/** A yield to the next macrotask through a MessageChannel — unlike a timer, never throttled in a hidden tab. */
|
|
502
|
+
function macrotask(): Promise<void> {
|
|
503
|
+
return new Promise((resolve) => {
|
|
504
|
+
const channel = new MessageChannel()
|
|
505
|
+
channel.port1.onmessage = () => {
|
|
506
|
+
channel.port1.close()
|
|
507
|
+
resolve()
|
|
508
|
+
}
|
|
509
|
+
channel.port2.postMessage(null)
|
|
510
|
+
})
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
/**
|
|
514
|
+
* The children of `root` (the scene, or one floor) gathered under one
|
|
515
|
+
* ClippingGroup carrying `planes` (world space) for the length of a render;
|
|
516
|
+
* the returned function puts them back. The WebGPU renderer clips only
|
|
517
|
+
* through such groups (there is no renderer-level plane list), and a group
|
|
518
|
+
* added and removed inside one synchronous render never reaches the React
|
|
519
|
+
* tree. `clipShadows` cuts the shadows too, so light reaches past the cut.
|
|
520
|
+
*/
|
|
521
|
+
function clipSceneFor(
|
|
522
|
+
root: THREE.Object3D,
|
|
523
|
+
planes: readonly { normal: [number, number, number]; constant: number }[],
|
|
524
|
+
clipShadows = false,
|
|
525
|
+
): () => void {
|
|
526
|
+
const group = new ClippingGroup()
|
|
527
|
+
group.clippingPlanes = planes.map(
|
|
528
|
+
(p) =>
|
|
529
|
+
new THREE.Plane(
|
|
530
|
+
new THREE.Vector3(p.normal[0], p.normal[1], p.normal[2]).normalize(),
|
|
531
|
+
p.constant,
|
|
532
|
+
),
|
|
533
|
+
)
|
|
534
|
+
group.enabled = true
|
|
535
|
+
group.clipShadows = clipShadows
|
|
536
|
+
const children = [...root.children]
|
|
537
|
+
for (const child of children) group.add(child)
|
|
538
|
+
root.add(group)
|
|
539
|
+
return () => {
|
|
540
|
+
for (const child of children) root.add(child)
|
|
541
|
+
root.remove(group)
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
/**
|
|
546
|
+
* An interior view of one floor, for one render (view_scene): the other
|
|
547
|
+
* floors and this floor's roofs and ceilings out of the picture, everything
|
|
548
|
+
* on it cut where core says (sceneViewInterior, from the scene as this editor
|
|
549
|
+
* has it) and its shadows with it, and whatever the user's solo or isolation
|
|
550
|
+
* holds back on it shown. The site's ground follows this floor, not the
|
|
551
|
+
* user's: veiled wholesale over a floor below grade; over one only partly
|
|
552
|
+
* below grade, or at grade behind a hillside, veiled only where it stands
|
|
553
|
+
* between this frame's camera and the floor (the ground window).
|
|
554
|
+
*/
|
|
555
|
+
export function interiorCutaway(
|
|
556
|
+
levelId: string,
|
|
557
|
+
restore: (callback: () => void) => void,
|
|
558
|
+
camera?: THREE.Camera,
|
|
559
|
+
) {
|
|
560
|
+
const { nodes } = useScene.getState()
|
|
561
|
+
const { cutHeight, hiddenIds } = sceneViewInterior(nodes, levelId)
|
|
562
|
+
const floor = sceneRegistry.nodes.get(levelId)
|
|
563
|
+
if (!floor) throw new Error('The floor to look inside is not built yet. Try again.')
|
|
564
|
+
for (const id of hiddenIds) {
|
|
565
|
+
const object = sceneRegistry.nodes.get(id)
|
|
566
|
+
if (!object?.visible) continue
|
|
567
|
+
object.visible = false
|
|
568
|
+
restore(() => {
|
|
569
|
+
object.visible = true
|
|
570
|
+
})
|
|
571
|
+
}
|
|
572
|
+
restore(temporarilyShowPresentation(floor))
|
|
573
|
+
restore(clipSceneFor(floor, [{ normal: [0, -1, 0], constant: cutHeight }], true))
|
|
574
|
+
const standing = levelGradeStanding(nodes, levelId)
|
|
575
|
+
if (standing === 'below-grade' || !camera) {
|
|
576
|
+
restore(temporarilyVeilGround(standing !== 'at-grade'))
|
|
577
|
+
} else {
|
|
578
|
+
restore(temporarilyVeilGround(false))
|
|
579
|
+
restore(temporarilyGroundWindowForCapture(camera, nodes, levelId))
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
/**
|
|
584
|
+
* The ground of an exterior picture, for one render: solid, whatever level the person is
|
|
585
|
+
* on — but a posed frame (an authored snapshot pose, or the caller's own perspective or
|
|
586
|
+
* orthographic view, which is how both agent lanes ask) decides its own ground window from
|
|
587
|
+
* its own camera, never inheriting the person's. An unposed frame (the person's viewport,
|
|
588
|
+
* the hero shot) makes no such decision: the window is the person's to hold.
|
|
589
|
+
*/
|
|
590
|
+
export function exteriorGroundForCapture(
|
|
591
|
+
restore: (callback: () => void) => void,
|
|
592
|
+
posed: boolean,
|
|
593
|
+
camera: THREE.Camera,
|
|
594
|
+
) {
|
|
595
|
+
restore(temporarilyVeilGround(false))
|
|
596
|
+
if (posed) restore(temporarilyGroundWindowForCapture(camera, useScene.getState().nodes))
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
/**
|
|
600
|
+
* A caller's own orthographic view, or with no view of its own an
|
|
601
|
+
* orthographic main camera, captures orthographically; a caller's perspective
|
|
602
|
+
* view (view_scene's), the auto-save hero shot and an authored snapshot pose
|
|
603
|
+
* stay perspective.
|
|
604
|
+
*/
|
|
605
|
+
export function capturesOrthographically(
|
|
606
|
+
event: Pick<ThumbnailGenerateEvent, 'snapLevels' | 'cameraPose' | 'ortho' | 'perspective'>,
|
|
607
|
+
mainCamera: THREE.Camera,
|
|
608
|
+
): boolean {
|
|
609
|
+
if (event.snapLevels || event.cameraPose) return false
|
|
610
|
+
if (event.ortho) return true
|
|
611
|
+
return !event.perspective && mainCamera instanceof THREE.OrthographicCamera
|
|
612
|
+
}
|
|
613
|
+
|
|
70
614
|
export const ThumbnailGenerator = ({ onThumbnailCapture }: ThumbnailGeneratorProps) => {
|
|
71
615
|
const gl = useThree((state) => state.gl)
|
|
72
616
|
const scene = useThree((state) => state.scene)
|
|
73
617
|
const atmosphere = useSceneAtmosphere()
|
|
74
618
|
const getThree = useThree((state) => state.get)
|
|
619
|
+
const advance = useThree((state) => state.advance)
|
|
620
|
+
const clock = useThree((state) => state.clock)
|
|
75
621
|
const controls = useThree((state) => state.controls) as CameraControls | null
|
|
76
622
|
const isGenerating = useRef(false)
|
|
77
623
|
const captureQueue = useRef(createSnapshotQueue())
|
|
@@ -80,6 +626,12 @@ export const ThumbnailGenerator = ({ onThumbnailCapture }: ThumbnailGeneratorPro
|
|
|
80
626
|
const thumbnailCameraRef = useRef<THREE.PerspectiveCamera | null>(null)
|
|
81
627
|
const pipelineRef = useRef<SnapshotPipeline | null>(null)
|
|
82
628
|
const captureVersion = useRef(0)
|
|
629
|
+
// An ORTHOGRAPHIC capture (a sheet's elevation, 2026-09-10) needs its own
|
|
630
|
+
// camera and its own pass — the pipeline binds the camera it was built
|
|
631
|
+
// with — built on first use, kept until the atmosphere changes.
|
|
632
|
+
const orthoCameraRef = useRef<THREE.OrthographicCamera | null>(null)
|
|
633
|
+
const orthoPipelineRef = useRef<SnapshotPipeline | null>(null)
|
|
634
|
+
const orthoPipelineBuild = useRef<Promise<SnapshotPipeline | null> | null>(null)
|
|
83
635
|
|
|
84
636
|
useEffect(() => {
|
|
85
637
|
onThumbnailCaptureRef.current = onThumbnailCapture
|
|
@@ -117,16 +669,71 @@ export const ThumbnailGenerator = ({ onThumbnailCapture }: ThumbnailGeneratorPro
|
|
|
117
669
|
thumbnailCameraRef.current = null
|
|
118
670
|
pipelineRef.current?.dispose()
|
|
119
671
|
pipelineRef.current = null
|
|
672
|
+
orthoPipelineRef.current?.dispose()
|
|
673
|
+
orthoPipelineRef.current = null
|
|
674
|
+
orthoPipelineBuild.current = null
|
|
675
|
+
}
|
|
676
|
+
}, [gl, scene, atmosphere])
|
|
677
|
+
|
|
678
|
+
/** The orthographic capture camera and its pipeline, built once per atmosphere. */
|
|
679
|
+
const orthoPipeline = useCallback(async (): Promise<{
|
|
680
|
+
camera: THREE.OrthographicCamera
|
|
681
|
+
pipeline: SnapshotPipeline | null
|
|
682
|
+
}> => {
|
|
683
|
+
if (!orthoCameraRef.current) {
|
|
684
|
+
const cam = new THREE.OrthographicCamera(-1, 1, 1, -1, 0.1, 1000)
|
|
685
|
+
cam.layers.disable(EDITOR_LAYER)
|
|
686
|
+
cam.layers.disable(GRID_LAYER)
|
|
687
|
+
orthoCameraRef.current = cam
|
|
688
|
+
}
|
|
689
|
+
const camera = orthoCameraRef.current
|
|
690
|
+
if (!orthoPipelineRef.current) {
|
|
691
|
+
if (!orthoPipelineBuild.current) {
|
|
692
|
+
// the post-processed stack (AO + ink edges) for the orthographic camera
|
|
693
|
+
orthoPipelineBuild.current = createSnapshotPipeline({
|
|
694
|
+
renderer: gl as unknown as WebGPURenderer,
|
|
695
|
+
scene,
|
|
696
|
+
camera,
|
|
697
|
+
atmosphere,
|
|
698
|
+
})
|
|
699
|
+
}
|
|
700
|
+
orthoPipelineRef.current = await orthoPipelineBuild.current
|
|
120
701
|
}
|
|
702
|
+
return { camera, pipeline: orthoPipelineRef.current }
|
|
121
703
|
}, [gl, scene, atmosphere])
|
|
122
704
|
|
|
123
705
|
const generate = useCallback(
|
|
124
706
|
async (event: ThumbnailGenerateEvent) => {
|
|
125
|
-
const { captureMode, cropRegion, standardSize, cameraPose, requestId } = event
|
|
707
|
+
const { captureMode, cropRegion, standardSize, cameraPose, requestId, interior } = event
|
|
126
708
|
const snapLevels = event.snapLevels === true
|
|
127
709
|
const transparent = event.transparent === true
|
|
710
|
+
// A SHEET's capture asks for more than a
|
|
711
|
+
// thumbnail: its own ink edges, an orthographic or perspective view of
|
|
712
|
+
// its own (the user's camera never moves), node types hidden, the sun
|
|
713
|
+
// re-aimed at the face, the scene clipped at a section's cut, and the
|
|
714
|
+
// frame rendered at print scale.
|
|
715
|
+
const edgesOverride = event.edges
|
|
716
|
+
const ortho = event.ortho
|
|
717
|
+
const hideTypes = event.hideTypes ?? []
|
|
718
|
+
const perspective = event.perspective
|
|
719
|
+
const lightFace = event.lightFace === true
|
|
720
|
+
const clip = event.clip ?? []
|
|
721
|
+
const supersample = event.supersample ?? 1
|
|
128
722
|
const standardW = standardSize?.w ?? THUMBNAIL_WIDTH
|
|
129
723
|
const standardH = standardSize?.h ?? THUMBNAIL_HEIGHT
|
|
724
|
+
// dev: the capture handshake on `window.__pascalCaptureTrace`, for a probe
|
|
725
|
+
const trace = (step: string, data?: unknown) => {
|
|
726
|
+
if (process.env.NODE_ENV === 'production') return
|
|
727
|
+
const w = window as unknown as { __pascalCaptureTrace?: unknown[] }
|
|
728
|
+
w.__pascalCaptureTrace ??= []
|
|
729
|
+
w.__pascalCaptureTrace.push({ t: Math.round(performance.now()), step, data })
|
|
730
|
+
}
|
|
731
|
+
trace('generate', {
|
|
732
|
+
captureMode,
|
|
733
|
+
ortho: !!ortho,
|
|
734
|
+
busy: isGenerating.current,
|
|
735
|
+
callback: !!onThumbnailCaptureRef.current,
|
|
736
|
+
})
|
|
130
737
|
await runSnapshotCapture(
|
|
131
738
|
requestId,
|
|
132
739
|
isGenerating,
|
|
@@ -136,8 +743,8 @@ export const ThumbnailGenerator = ({ onThumbnailCapture }: ThumbnailGeneratorPro
|
|
|
136
743
|
if (!onCapture) throw new Error('Snapshot storage is unavailable')
|
|
137
744
|
if (cameraPose && event.projectId !== useViewer.getState().projectId)
|
|
138
745
|
throw new Error('The active project changed before capture')
|
|
139
|
-
const
|
|
140
|
-
if (!
|
|
746
|
+
const perspectiveCamera = thumbnailCameraRef.current
|
|
747
|
+
if (!perspectiveCamera) throw new Error('Snapshot camera is not ready')
|
|
141
748
|
const { camera: mainCamera, controls } = getThree()
|
|
142
749
|
if (cameraPose && (snapLevels || (captureMode && captureMode !== 'standard'))) {
|
|
143
750
|
throw new Error('An explicit snapshot camera requires standard capture mode')
|
|
@@ -146,244 +753,517 @@ export const ThumbnailGenerator = ({ onThumbnailCapture }: ThumbnailGeneratorPro
|
|
|
146
753
|
throw new Error('Snapshot renderer is not ready. Try again.')
|
|
147
754
|
}
|
|
148
755
|
|
|
149
|
-
//
|
|
150
|
-
//
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
if (
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
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 }),
|
|
215
|
-
}
|
|
756
|
+
// a supersampled frame renders at a multiple of the canvas' size into
|
|
757
|
+
// the capture's own targets; the live canvas is never resized
|
|
758
|
+
const canvasSize: [number, number] = [gl.domElement.width, gl.domElement.height]
|
|
759
|
+
const pose = ortho ?? perspective
|
|
760
|
+
const limits = captureLimits(gl)
|
|
761
|
+
const [width, height] = captureDimensions(canvasSize, event, limits.maxPixels)
|
|
762
|
+
// a supersampled picture is encoded at the size it could render (the
|
|
763
|
+
// caller asked for canvas × supersample, maybe past this GPU's limits)
|
|
764
|
+
const encodeSize = supersample > 1 ? { w: width, h: height } : standardSize
|
|
765
|
+
// a background tab draws no frames and (on the WebGL backend) never
|
|
766
|
+
// finishes a readback: a sheet's capture says so at once
|
|
767
|
+
if (pose && isTabHidden()) throw new Error(HIDDEN_TAB)
|
|
768
|
+
// a sheet's capture runs frames by hand in a scene set up for the
|
|
769
|
+
// picture: the viewport keeps its last frame meanwhile
|
|
770
|
+
const releaseLiveFrame = pose ? holdLiveFrame(HOLD_MAX_MS) : null
|
|
216
771
|
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
772
|
+
try {
|
|
773
|
+
const edges = edgesOverride ?? (transparent ? 'off' : useViewer.getState().edges)
|
|
774
|
+
|
|
775
|
+
const wantOrtho = capturesOrthographically(event, mainCamera)
|
|
776
|
+
let thumbnailCamera: THREE.PerspectiveCamera | THREE.OrthographicCamera =
|
|
777
|
+
perspectiveCamera
|
|
778
|
+
let pipeline: SnapshotPipeline | null = pipelineRef.current
|
|
779
|
+
if (wantOrtho) {
|
|
780
|
+
// the orthographic capture goes through its own post-processed
|
|
781
|
+
// pipeline (AO, ink edges) and target, on either backend — never
|
|
782
|
+
// through the live canvas
|
|
783
|
+
const built = await orthoPipeline()
|
|
784
|
+
const cam = built.camera
|
|
785
|
+
if (ortho) {
|
|
786
|
+
const halfW = ortho.viewWidth / 2
|
|
787
|
+
const halfH = halfW / (width / height)
|
|
788
|
+
cam.position.set(ortho.position[0], ortho.position[1], ortho.position[2])
|
|
789
|
+
cam.up.set(0, 1, 0)
|
|
790
|
+
cam.lookAt(ortho.target[0], ortho.target[1], ortho.target[2])
|
|
791
|
+
cam.left = -halfW
|
|
792
|
+
cam.right = halfW
|
|
793
|
+
cam.top = halfH
|
|
794
|
+
cam.bottom = -halfH
|
|
795
|
+
cam.zoom = 1
|
|
796
|
+
cam.near = 0.1
|
|
797
|
+
cam.far = 2000
|
|
798
|
+
} else {
|
|
799
|
+
const main = mainCamera as THREE.OrthographicCamera
|
|
800
|
+
cam.position.copy(main.position)
|
|
801
|
+
cam.quaternion.copy(main.quaternion)
|
|
802
|
+
cam.left = main.left
|
|
803
|
+
cam.right = main.right
|
|
804
|
+
cam.top = main.top
|
|
805
|
+
cam.bottom = main.bottom
|
|
806
|
+
cam.zoom = main.zoom
|
|
807
|
+
cam.near = main.near
|
|
808
|
+
cam.far = main.far
|
|
809
|
+
}
|
|
810
|
+
cam.updateProjectionMatrix()
|
|
811
|
+
cam.updateMatrixWorld()
|
|
812
|
+
thumbnailCamera = cam
|
|
813
|
+
pipeline = built.pipeline
|
|
814
|
+
} else {
|
|
815
|
+
if (perspective && !snapLevels && !cameraPose) {
|
|
816
|
+
// a sheet's cover view: the capture camera stands where the
|
|
817
|
+
// sheet asks, the user's camera never moves
|
|
818
|
+
perspectiveCamera.position.set(
|
|
819
|
+
perspective.position[0],
|
|
820
|
+
perspective.position[1],
|
|
821
|
+
perspective.position[2],
|
|
822
|
+
)
|
|
823
|
+
perspectiveCamera.up.set(0, 1, 0)
|
|
824
|
+
perspectiveCamera.lookAt(
|
|
825
|
+
perspective.target[0],
|
|
826
|
+
perspective.target[1],
|
|
827
|
+
perspective.target[2],
|
|
828
|
+
)
|
|
829
|
+
perspectiveCamera.fov = perspective.fov ?? 60
|
|
830
|
+
perspectiveCamera.near = 0.1
|
|
831
|
+
perspectiveCamera.far = 2000
|
|
832
|
+
} else {
|
|
833
|
+
// Copy the main camera's transform and projection so the thumbnail
|
|
834
|
+
// matches exactly what the user sees in the viewport.
|
|
835
|
+
perspectiveCamera.position.copy(mainCamera.position)
|
|
836
|
+
perspectiveCamera.quaternion.copy(mainCamera.quaternion)
|
|
837
|
+
if (mainCamera instanceof THREE.PerspectiveCamera) {
|
|
838
|
+
perspectiveCamera.fov = mainCamera.fov
|
|
839
|
+
perspectiveCamera.near = mainCamera.near
|
|
840
|
+
perspectiveCamera.far = mainCamera.far
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
perspectiveCamera.aspect = width / height
|
|
844
|
+
if (cameraPose) {
|
|
845
|
+
applySnapshotCapturePose(
|
|
846
|
+
perspectiveCamera,
|
|
847
|
+
cameraPose,
|
|
848
|
+
{ width, height },
|
|
849
|
+
{
|
|
850
|
+
w: standardW,
|
|
851
|
+
h: standardH,
|
|
852
|
+
},
|
|
853
|
+
)
|
|
223
854
|
}
|
|
224
|
-
|
|
855
|
+
perspectiveCamera.updateProjectionMatrix()
|
|
856
|
+
// The capture camera never joins the scene graph, so its matrixWorld
|
|
857
|
+
// is only refreshed by the render itself — too late for the backdrop
|
|
858
|
+
// uniforms below.
|
|
859
|
+
perspectiveCamera.updateMatrixWorld()
|
|
225
860
|
}
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
861
|
+
|
|
862
|
+
pipeline?.applyEnvironment({
|
|
863
|
+
theme: useViewer.getState().sceneAppearance,
|
|
864
|
+
transparent,
|
|
865
|
+
grade: useViewer.getState().shading === 'rendered',
|
|
866
|
+
edges,
|
|
867
|
+
camera: thumbnailCamera,
|
|
868
|
+
})
|
|
869
|
+
|
|
870
|
+
// Capture camera data for snapshot storage
|
|
871
|
+
const pos = cameraPose ? thumbnailCamera.position : mainCamera.position
|
|
872
|
+
let tgt: [number, number, number] | null = null
|
|
873
|
+
if (!cameraPose && controls && 'getTarget' in controls) {
|
|
874
|
+
const v = new THREE.Vector3()
|
|
875
|
+
;(controls as any).getTarget(v)
|
|
876
|
+
tgt = [v.x, v.y, v.z]
|
|
877
|
+
} else {
|
|
878
|
+
// Walk / drone captures run without orbit controls, so there is no orbit
|
|
879
|
+
// target to read. Synthesize one down the view axis — otherwise the
|
|
880
|
+
// saved snapshot carries no framing to return to.
|
|
881
|
+
const look = new THREE.Vector3(0, 0, -1)
|
|
882
|
+
.applyQuaternion(cameraPose ? thumbnailCamera.quaternion : mainCamera.quaternion)
|
|
883
|
+
.multiplyScalar(FIRST_PERSON_TARGET_DISTANCE)
|
|
884
|
+
.add(pos)
|
|
885
|
+
tgt = [look.x, look.y, look.z]
|
|
886
|
+
}
|
|
887
|
+
const isOrtho = !cameraPose && mainCamera instanceof THREE.OrthographicCamera
|
|
888
|
+
const cameraData: SnapshotCameraData = {
|
|
889
|
+
...(requestId && { requestId }),
|
|
890
|
+
position: [pos.x, pos.y, pos.z],
|
|
891
|
+
...(cameraPose && {
|
|
892
|
+
quaternion: [...cameraPose.quaternion] as [number, number, number, number],
|
|
893
|
+
fov: cameraPose.fov,
|
|
894
|
+
}),
|
|
895
|
+
target: tgt,
|
|
896
|
+
type: isOrtho ? 'orthographic' : 'perspective',
|
|
897
|
+
...(isOrtho && { zoom: (mainCamera as THREE.OrthographicCamera).zoom }),
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
if (pose) {
|
|
901
|
+
// a sheet's capture: the house as built — never a half-built scene
|
|
902
|
+
// or an item's loading placeholder (the cover once showed striped
|
|
903
|
+
// boxes through a white roof, 2026-09-23) — then the viewer's own
|
|
904
|
+
// systems (the wall materials, a plugin's batches) take two real
|
|
905
|
+
// frames — run by hand, never awaited from the animation loop: a
|
|
906
|
+
// hidden tab gets no animation frames at all, and a capture that
|
|
907
|
+
// waited on one hung the sheet's whole capture chain (2026-09-10)
|
|
908
|
+
const pending = await untilSceneBuilt()
|
|
909
|
+
trace('scene-built', { pending })
|
|
910
|
+
await pumpFrames(advance, clock, 2)
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
// the scene as the picture wants it, for one render; `restore` puts
|
|
914
|
+
// every change back right after it
|
|
915
|
+
const setUp = (restore: (callback: () => void) => void) => {
|
|
916
|
+
// A picture is read for its rooms, taken in the world's own light: the earth the
|
|
917
|
+
// person's view turns to under the ground (sky, fog, vignette, the sky's lights)
|
|
918
|
+
// is not in it.
|
|
919
|
+
restore(temporarilyWithoutAmbiance())
|
|
920
|
+
if (snapLevels) {
|
|
921
|
+
const prevMode = useViewer.getState().levelMode
|
|
922
|
+
if (prevMode !== 'stacked') {
|
|
923
|
+
restore(() => useViewer.getState().setLevelMode(prevMode))
|
|
924
|
+
useViewer.getState().setLevelMode('stacked')
|
|
925
|
+
}
|
|
926
|
+
restore(snapLevelsToTruePositions())
|
|
927
|
+
}
|
|
928
|
+
// Hide scan, guide, and spawn nodes (and what the caller asks) so
|
|
929
|
+
// they are excluded from the thumbnail regardless of whether the
|
|
930
|
+
// system listeners are registered.
|
|
931
|
+
restore(temporarilyHideNodeTypes(['scan', 'guide', 'spawn', ...hideTypes]))
|
|
932
|
+
// a sheet's picture is of the house: what a renderer excluded
|
|
933
|
+
// from captures stays out of it, and every storey stands at its
|
|
934
|
+
// true height
|
|
935
|
+
if (pose) {
|
|
936
|
+
restore(hideCaptureExcluded(scene))
|
|
937
|
+
restore(snapLevelsToTruePositions())
|
|
938
|
+
}
|
|
939
|
+
if (lightFace && pose) restore(aimLightsAtFace(scene, pose.position, pose.target))
|
|
940
|
+
if (clip.length > 0) {
|
|
941
|
+
restore(clipSceneFor(scene, clip))
|
|
942
|
+
restore(lightInterior(scene, thumbnailCamera))
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
// Auto-save uses the published hero framing. An authored shot keeps
|
|
946
|
+
// its own camera while sharing the same true level positions.
|
|
947
|
+
if (snapLevels) {
|
|
948
|
+
const framing = computeHeroFraming()
|
|
949
|
+
if (framing) {
|
|
950
|
+
const hero = heroCameraPose({
|
|
951
|
+
boxes: framing.boxes,
|
|
952
|
+
aim: framing.aim,
|
|
953
|
+
azimuthRad: framing.azimuthRad,
|
|
954
|
+
aspect: width / height,
|
|
955
|
+
})
|
|
956
|
+
thumbnailCamera.position.set(hero.position[0], hero.position[1], hero.position[2])
|
|
957
|
+
thumbnailCamera.lookAt(hero.target[0], hero.target[1], hero.target[2])
|
|
958
|
+
thumbnailCamera.updateMatrixWorld()
|
|
959
|
+
pipeline?.applyEnvironment({
|
|
960
|
+
theme: useViewer.getState().sceneAppearance,
|
|
961
|
+
transparent,
|
|
962
|
+
grade: useViewer.getState().shading === 'rendered',
|
|
963
|
+
edges,
|
|
964
|
+
camera: thumbnailCamera,
|
|
965
|
+
})
|
|
966
|
+
cameraData.position = hero.position
|
|
967
|
+
cameraData.target = hero.target
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
restore(() => emitter.emit('thumbnail:after-capture', undefined))
|
|
972
|
+
// Walls whole, as built, whatever the user's wall mode: an
|
|
973
|
+
// authored pose is not the user's view, an interior is cut below.
|
|
974
|
+
emitter.emit(
|
|
975
|
+
'thumbnail:before-capture',
|
|
976
|
+
cameraPose || interior ? { wholeWalls: true } : undefined,
|
|
977
|
+
)
|
|
978
|
+
if (cameraPose) {
|
|
979
|
+
restore(snapLevelsToTruePositions())
|
|
980
|
+
restore(temporarilyShowShadowOnly(scene))
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
// Geometry and presentation systems may have mounted new meshes
|
|
984
|
+
// since the viewport's last frame. Apply the same isolation to
|
|
985
|
+
// those meshes before either snapshot render path submits them,
|
|
986
|
+
// and restore fog afterwards so the atmosphere never leaks in.
|
|
987
|
+
restore(refreshIsolation(scene))
|
|
988
|
+
if (event.presentation) {
|
|
989
|
+
restore(presentLevelsForCapture(event.presentation))
|
|
990
|
+
cameraData.presentation = event.presentation
|
|
991
|
+
}
|
|
992
|
+
if (interior) {
|
|
993
|
+
interiorCutaway(interior.levelId, restore, thumbnailCamera)
|
|
994
|
+
restore(lightInterior(scene, thumbnailCamera))
|
|
995
|
+
} else if (snapLevels || pose || cameraPose) {
|
|
996
|
+
exteriorGroundForCapture(restore, Boolean(cameraPose || pose), thumbnailCamera)
|
|
251
997
|
}
|
|
252
998
|
}
|
|
999
|
+
// The scene pass is a FRAME-updated node: it renders once per node
|
|
1000
|
+
// frame, and only the animation loop advances that frame. With the
|
|
1001
|
+
// viewer idle behind the Sheets overlay two captures share one
|
|
1002
|
+
// frame and the second returns the first's picture (2026-09-10:
|
|
1003
|
+
// the north elevation came back as the east). Advance it by hand.
|
|
1004
|
+
const advanceNodeFrame = () =>
|
|
1005
|
+
(
|
|
1006
|
+
gl as unknown as { _nodes?: { nodeFrame?: { update?: () => void } } }
|
|
1007
|
+
)._nodes?.nodeFrame?.update?.()
|
|
1008
|
+
// a sheet's picture goes to the sheets lossless: they pick its print encoding
|
|
1009
|
+
const mime = pose ? 'image/png' : undefined
|
|
1010
|
+
const tiles = pose && pipeline ? tileGrid(width, height, limits.passEdge) : []
|
|
253
1011
|
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
1012
|
+
if (tiles.length > 1 && pipeline) {
|
|
1013
|
+
// Past one pass (the GPU's biggest texture, or MAX_PASS_EDGE) the
|
|
1014
|
+
// picture renders tile by tile — each a view offset of the same
|
|
1015
|
+
// camera, the scene set up afresh for it (the frame loop runs
|
|
1016
|
+
// between the readbacks) — and the tiles land on one canvas.
|
|
1017
|
+
trace('capture:tiles', {
|
|
1018
|
+
width,
|
|
1019
|
+
height,
|
|
1020
|
+
tiles: tiles.length,
|
|
1021
|
+
software: limits.software,
|
|
262
1022
|
})
|
|
263
|
-
const
|
|
264
|
-
const
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
const
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
1023
|
+
const picture = new OffscreenCanvas(width, height)
|
|
1024
|
+
const context = picture.getContext('2d')
|
|
1025
|
+
if (!context) throw new Error('The picture canvas could not be created')
|
|
1026
|
+
for (const tile of tiles) {
|
|
1027
|
+
const shot = captureSnapshotScene((restore) => {
|
|
1028
|
+
setUp(restore)
|
|
1029
|
+
thumbnailCamera.setViewOffset(width, height, tile.x, tile.y, tile.w, tile.h)
|
|
1030
|
+
restore(() => thumbnailCamera.clearViewOffset())
|
|
1031
|
+
thumbnailCamera.updateMatrixWorld()
|
|
1032
|
+
pipeline.applyEnvironment({
|
|
1033
|
+
theme: useViewer.getState().sceneAppearance,
|
|
1034
|
+
transparent,
|
|
1035
|
+
grade: useViewer.getState().shading === 'rendered',
|
|
1036
|
+
edges,
|
|
1037
|
+
camera: thumbnailCamera,
|
|
1038
|
+
})
|
|
1039
|
+
advanceNodeFrame()
|
|
1040
|
+
return atCaptureSize(gl as unknown as WebGPURenderer, tile.w, tile.h, () =>
|
|
1041
|
+
pipeline.capture({
|
|
1042
|
+
captureMode: 'standard',
|
|
1043
|
+
standardSize: { w: tile.w, h: tile.h },
|
|
1044
|
+
mime,
|
|
1045
|
+
}),
|
|
1046
|
+
)
|
|
1047
|
+
})
|
|
1048
|
+
const result = await within(
|
|
1049
|
+
Promise.resolve(shot),
|
|
1050
|
+
CAPTURE_SETTLE_MS,
|
|
1051
|
+
'The GPU readback of the captured frame never came back.',
|
|
1052
|
+
)
|
|
1053
|
+
const bitmap = await createImageBitmap(result.blob)
|
|
1054
|
+
const { inner } = tile
|
|
1055
|
+
context.drawImage(
|
|
1056
|
+
bitmap,
|
|
1057
|
+
inner.x - tile.x,
|
|
1058
|
+
inner.y - tile.y,
|
|
1059
|
+
inner.w,
|
|
1060
|
+
inner.h,
|
|
1061
|
+
inner.x,
|
|
1062
|
+
inner.y,
|
|
1063
|
+
inner.w,
|
|
1064
|
+
inner.h,
|
|
277
1065
|
)
|
|
1066
|
+
bitmap.close()
|
|
278
1067
|
}
|
|
1068
|
+
const blob = await picture.convertToBlob({ type: 'image/png' })
|
|
1069
|
+
trace('capture:done', { w: width, h: height, bytes: blob.size, tiles: tiles.length })
|
|
1070
|
+
if (captureMode !== undefined) cameraData.captureMode = captureMode
|
|
1071
|
+
cameraData.resolution = { w: width, h: height }
|
|
1072
|
+
if (
|
|
1073
|
+
version !== captureVersion.current ||
|
|
1074
|
+
perspectiveCamera !== thumbnailCameraRef.current
|
|
1075
|
+
) {
|
|
1076
|
+
throw new Error('The scene changed during capture. Try again.')
|
|
1077
|
+
}
|
|
1078
|
+
trace('callback', { bytes: blob.size })
|
|
1079
|
+
await within(
|
|
1080
|
+
Promise.resolve(deliverSnapshot(event, blob, cameraData, onCapture)),
|
|
1081
|
+
CAPTURE_SETTLE_MS,
|
|
1082
|
+
'The snapshot host never took the frame.',
|
|
1083
|
+
)
|
|
1084
|
+
return
|
|
279
1085
|
}
|
|
280
1086
|
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
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
|
-
),
|
|
1087
|
+
const capturePromise = captureSnapshotScene((restore) => {
|
|
1088
|
+
setUp(restore)
|
|
1089
|
+
if (pipeline) {
|
|
1090
|
+
trace('capture:start', { ortho: wantOrtho, pipeline: true, width, height })
|
|
1091
|
+
advanceNodeFrame()
|
|
1092
|
+
return atCaptureSize(gl as unknown as WebGPURenderer, width, height, () =>
|
|
1093
|
+
pipeline.capture({ captureMode, cropRegion, standardSize: encodeSize, mime }),
|
|
1094
|
+
)
|
|
1095
|
+
}
|
|
1096
|
+
// a sheet's picture (or a supersampled one) is never drawn into
|
|
1097
|
+
// the live canvas the viewport shows
|
|
1098
|
+
if (pose || width !== canvasSize[0] || height !== canvasSize[1])
|
|
1099
|
+
throw new Error('Snapshot renderer is not ready. Try again.')
|
|
1100
|
+
// Fallback: plain render directly to the canvas
|
|
1101
|
+
if (transparent) {
|
|
1102
|
+
const clearColor = (
|
|
1103
|
+
gl as unknown as { getClearColor: (t: THREE.Color) => THREE.Color }
|
|
1104
|
+
).getClearColor(new THREE.Color())
|
|
1105
|
+
const clearAlpha = gl.getClearAlpha()
|
|
1106
|
+
const sceneBackground = scene.background
|
|
1107
|
+
restore(() => {
|
|
1108
|
+
scene.background = sceneBackground
|
|
1109
|
+
gl.setClearColor(clearColor, clearAlpha)
|
|
1110
|
+
})
|
|
1111
|
+
scene.background = null
|
|
1112
|
+
gl.setClearColor(new THREE.Color('#ffffff'), 0)
|
|
1113
|
+
}
|
|
1114
|
+
trace('canvas:render', { ortho: wantOrtho })
|
|
1115
|
+
gl.render(scene, thumbnailCamera)
|
|
1116
|
+
return undefined
|
|
1117
|
+
})
|
|
1118
|
+
|
|
1119
|
+
let blob: Blob
|
|
1120
|
+
if (pipeline) {
|
|
1121
|
+
// the frame is rendered and the scene restored: a readback that
|
|
1122
|
+
// never returns must not hold every later capture in the queue
|
|
1123
|
+
const result = await within(
|
|
1124
|
+
Promise.resolve(capturePromise),
|
|
1125
|
+
CAPTURE_SETTLE_MS,
|
|
1126
|
+
'The GPU readback of the captured frame never came back.',
|
|
336
1127
|
)
|
|
1128
|
+
if (!result) throw new Error('Snapshot capture produced no image')
|
|
1129
|
+
trace('capture:done', { w: result.outW, h: result.outH, bytes: result.blob.size })
|
|
1130
|
+
blob = result.blob
|
|
1131
|
+
if (captureMode !== undefined) cameraData.captureMode = captureMode
|
|
1132
|
+
cameraData.resolution = { w: result.outW, h: result.outH }
|
|
337
1133
|
} else {
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
let
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
1134
|
+
await capturePromise
|
|
1135
|
+
let outW: number
|
|
1136
|
+
let outH: number
|
|
1137
|
+
|
|
1138
|
+
if (captureMode === 'viewport') {
|
|
1139
|
+
;({ w: outW, h: outH } = clampSnapshotSize(width, height))
|
|
1140
|
+
const offscreen = document.createElement('canvas')
|
|
1141
|
+
offscreen.width = outW
|
|
1142
|
+
offscreen.height = outH
|
|
1143
|
+
const ctx = offscreen.getContext('2d')!
|
|
1144
|
+
if (outW !== width || outH !== height) ctx.imageSmoothingQuality = 'high'
|
|
1145
|
+
ctx.drawImage(gl.domElement, 0, 0, width, height, 0, 0, outW, outH)
|
|
1146
|
+
blob = await new Promise<Blob>((resolve, reject) =>
|
|
1147
|
+
offscreen.toBlob(
|
|
1148
|
+
(b) => (b ? resolve(b) : reject(new Error('Canvas capture failed'))),
|
|
1149
|
+
SNAPSHOT_MIME,
|
|
1150
|
+
SNAPSHOT_QUALITY,
|
|
1151
|
+
),
|
|
1152
|
+
)
|
|
1153
|
+
} else if (captureMode === 'area' && cropRegion) {
|
|
1154
|
+
const sx = Math.round(cropRegion.x * width)
|
|
1155
|
+
const sy = Math.round(cropRegion.y * height)
|
|
1156
|
+
const sourceW = Math.round(cropRegion.width * width)
|
|
1157
|
+
const sourceH = Math.round(cropRegion.height * height)
|
|
1158
|
+
;({ w: outW, h: outH } = clampSnapshotSize(sourceW, sourceH))
|
|
1159
|
+
const offscreen = document.createElement('canvas')
|
|
1160
|
+
offscreen.width = outW
|
|
1161
|
+
offscreen.height = outH
|
|
1162
|
+
const ctx = offscreen.getContext('2d')!
|
|
1163
|
+
if (outW !== sourceW || outH !== sourceH) ctx.imageSmoothingQuality = 'high'
|
|
1164
|
+
ctx.drawImage(gl.domElement, sx, sy, sourceW, sourceH, 0, 0, outW, outH)
|
|
1165
|
+
blob = await new Promise<Blob>((resolve, reject) =>
|
|
1166
|
+
offscreen.toBlob(
|
|
1167
|
+
(b) => (b ? resolve(b) : reject(new Error('Canvas capture failed'))),
|
|
1168
|
+
SNAPSHOT_MIME,
|
|
1169
|
+
SNAPSHOT_QUALITY,
|
|
1170
|
+
),
|
|
1171
|
+
)
|
|
1172
|
+
} else {
|
|
1173
|
+
const srcAspect = width / height
|
|
1174
|
+
const dstAspect = standardW / standardH
|
|
1175
|
+
let sx = 0,
|
|
1176
|
+
sy = 0,
|
|
1177
|
+
sWidth = width,
|
|
1178
|
+
sHeight = height
|
|
1179
|
+
if (srcAspect > dstAspect) {
|
|
1180
|
+
sWidth = Math.round(height * dstAspect)
|
|
1181
|
+
sx = Math.round((width - sWidth) / 2)
|
|
1182
|
+
} else if (srcAspect < dstAspect) {
|
|
1183
|
+
sHeight = Math.round(width / dstAspect)
|
|
1184
|
+
sy = Math.round((height - sHeight) / 2)
|
|
1185
|
+
}
|
|
1186
|
+
outW = standardW
|
|
1187
|
+
outH = standardH
|
|
1188
|
+
const offscreen = document.createElement('canvas')
|
|
1189
|
+
offscreen.width = outW
|
|
1190
|
+
offscreen.height = outH
|
|
1191
|
+
offscreen
|
|
1192
|
+
.getContext('2d')!
|
|
1193
|
+
.drawImage(gl.domElement, sx, sy, sWidth, sHeight, 0, 0, outW, outH)
|
|
1194
|
+
blob = await new Promise<Blob>((resolve, reject) =>
|
|
1195
|
+
offscreen.toBlob(
|
|
1196
|
+
(b) => (b ? resolve(b) : reject(new Error('Canvas capture failed'))),
|
|
1197
|
+
SNAPSHOT_MIME,
|
|
1198
|
+
SNAPSHOT_QUALITY,
|
|
1199
|
+
),
|
|
1200
|
+
)
|
|
350
1201
|
}
|
|
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
|
-
)
|
|
366
|
-
}
|
|
367
1202
|
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
1203
|
+
if (captureMode !== undefined) cameraData.captureMode = captureMode
|
|
1204
|
+
cameraData.resolution = { w: outW, h: outH }
|
|
1205
|
+
}
|
|
371
1206
|
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
1207
|
+
if (
|
|
1208
|
+
version !== captureVersion.current ||
|
|
1209
|
+
perspectiveCamera !== thumbnailCameraRef.current
|
|
1210
|
+
) {
|
|
1211
|
+
throw new Error('The scene changed during capture. Try again.')
|
|
1212
|
+
}
|
|
1213
|
+
if (cameraPose && event.projectId !== useViewer.getState().projectId) {
|
|
1214
|
+
throw new Error('The active project changed during capture')
|
|
1215
|
+
}
|
|
1216
|
+
trace('callback', { bytes: blob.size })
|
|
1217
|
+
await within(
|
|
1218
|
+
Promise.resolve(deliverSnapshot(event, blob, cameraData, onCapture)),
|
|
1219
|
+
CAPTURE_SETTLE_MS,
|
|
1220
|
+
'The snapshot host never took the frame.',
|
|
1221
|
+
)
|
|
1222
|
+
} finally {
|
|
1223
|
+
releaseLiveFrame?.()
|
|
1224
|
+
// a sheet capture (transparent ground, ink edges, an orthographic
|
|
1225
|
+
// camera) sets the environment the live viewer shares; put the
|
|
1226
|
+
// viewer's own look back whatever happened above, or the live 3D
|
|
1227
|
+
// view keeps the capture's tinted (sepia-looking) environment
|
|
1228
|
+
if (transparent || edgesOverride !== undefined || ortho !== undefined) {
|
|
1229
|
+
try {
|
|
1230
|
+
pipelineRef.current?.applyEnvironment({
|
|
1231
|
+
theme: useViewer.getState().sceneAppearance,
|
|
1232
|
+
transparent: false,
|
|
1233
|
+
grade: useViewer.getState().shading === 'rendered',
|
|
1234
|
+
edges: useViewer.getState().edges,
|
|
1235
|
+
camera: mainCamera,
|
|
1236
|
+
})
|
|
1237
|
+
} catch (error) {
|
|
1238
|
+
trace('restore-environment', String(error))
|
|
1239
|
+
}
|
|
1240
|
+
}
|
|
380
1241
|
}
|
|
381
|
-
await onCapture(blob, cameraData)
|
|
382
1242
|
},
|
|
383
1243
|
(failure) => emitter.emit('snapshot:capture-failed', failure),
|
|
384
1244
|
)
|
|
385
1245
|
},
|
|
386
|
-
[gl, scene, getThree],
|
|
1246
|
+
[gl, scene, getThree, orthoPipeline, advance, clock],
|
|
1247
|
+
)
|
|
1248
|
+
|
|
1249
|
+
// A run of captures (a sheet's pictures, an agent's view) sets the shared
|
|
1250
|
+
// scene up for its pictures — the finished presentation, the ground hidden —
|
|
1251
|
+
// so while any run is on the viewport keeps its last frame and the viewer
|
|
1252
|
+
// shows the finished house whatever view the user was in; the last run's
|
|
1253
|
+
// end puts the user's view back, then (a few commits later) the viewport
|
|
1254
|
+
// resumes. Runs overlap (a sheet and an agent's view): the user's view is
|
|
1255
|
+
// the one before the first, never a run's finished presentation.
|
|
1256
|
+
useEffect(
|
|
1257
|
+
() =>
|
|
1258
|
+
presentWhileCapturing(() => {
|
|
1259
|
+
const releaseLiveFrame = holdLiveFrame(HOLD_MAX_MS)
|
|
1260
|
+
const restorePresentation = presentFinished()
|
|
1261
|
+
return () => {
|
|
1262
|
+
restorePresentation()
|
|
1263
|
+
setTimeout(releaseLiveFrame, SESSION_RESUME_MS)
|
|
1264
|
+
}
|
|
1265
|
+
}),
|
|
1266
|
+
[],
|
|
387
1267
|
)
|
|
388
1268
|
|
|
389
1269
|
// Thumbnail request via emitter. Two call shapes:
|