@pascal-app/editor 1.0.4 → 1.0.6
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 +40 -38
- package/src/components/editor/__bench__/room-label-placement.bench.ts +108 -0
- package/src/components/editor/bake-thumbnail.tsx +6 -15
- 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 +81 -8
- package/src/components/editor/editor-layout-mobile.tsx +1 -1
- package/src/components/editor/editor-layout-v2.tsx +1 -1
- package/src/components/editor/export-manager.tsx +14 -19
- package/src/components/editor/first-person/build-collider-world.test.ts +269 -0
- package/src/components/editor/first-person/build-collider-world.ts +22 -5
- 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 +89 -2
- package/src/components/editor/first-person/mechanism-targets.ts +52 -0
- package/src/components/editor/first-person-controls.tsx +19 -11
- package/src/components/editor/floor-aware-camera-controls.test.ts +24 -0
- package/src/components/editor/floorplan-mode-coordinator.tsx +2 -2
- package/src/components/editor/floorplan-panel.tsx +72 -23
- package/src/components/editor/follow-pascal.test.tsx +912 -0
- package/src/components/editor/follow-pascal.tsx +354 -0
- package/src/components/editor/group-actions.ts +2 -0
- 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/index.tsx +166 -79
- package/src/components/editor/riser-diagram-panel.tsx +14 -7
- package/src/components/editor/room-highlight.tsx +13 -5
- package/src/components/editor/room-labels.test.ts +621 -128
- package/src/components/editor/room-labels.tsx +1090 -190
- package/src/components/editor/selection-manager.tsx +38 -117
- package/src/components/editor/slab-hole-highlights.tsx +2 -2
- package/src/components/editor/snapshot-capture-overlay.tsx +3 -2
- package/src/components/editor/snapshot-capture.test.ts +69 -58
- package/src/components/editor/snapshot-capture.ts +7 -1
- package/src/components/editor/thumbnail-generator.test.ts +350 -4
- package/src/components/editor/thumbnail-generator.tsx +128 -54
- package/src/components/editor/use-floorplan-background-placement.ts +3 -1
- package/src/components/editor/views/plugins-view.tsx +1 -8
- package/src/components/editor/views/view-bar.test.tsx +54 -0
- package/src/components/editor/views/view-bar.tsx +140 -17
- package/src/components/editor/wall-snap-beacon-layer.tsx +5 -4
- package/src/components/editor-2d/floorplan-group-move.tsx +3 -0
- package/src/components/editor-2d/floorplan-registered-tool-layer.tsx +2 -2
- package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +3 -4
- package/src/components/editor-2d/renderers/floorplan-dimension-edit-overlay.test.ts +1 -1
- package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +29 -7
- 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/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/systems/ceiling/ceiling-bracket-batch.test.ts +12 -2
- package/src/components/systems/roof/roof-edit-system.tsx +1 -0
- 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 +38 -0
- package/src/components/tools/item/placement-strategies.test.ts +46 -1
- package/src/components/tools/item/placement-strategies.ts +3 -6
- package/src/components/tools/item/shelf-surface-strategy-resolver.test.ts +56 -56
- package/src/components/tools/item/use-draft-node.test.tsx +94 -94
- package/src/components/tools/item/use-placement-coordinator.tsx +55 -49
- 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 +63 -29
- package/src/components/tools/tool-manager.tsx +14 -5
- package/src/components/tools/wall/wall-drafting.test.ts +170 -57
- package/src/components/ui/action-menu/action-button.tsx +6 -1
- package/src/components/ui/action-menu/camera-actions.tsx +7 -3
- package/src/components/ui/action-menu/control-modes.tsx +13 -7
- 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 +11 -9
- 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 +10 -3
- package/src/components/ui/command-palette/editor-commands.tsx +3 -12
- package/src/components/ui/command-palette/index.tsx +45 -15
- 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/metric-control.tsx +1 -1
- package/src/components/ui/controls/segmented-control.tsx +21 -8
- package/src/components/ui/controls/select-control.tsx +1 -1
- 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 +150 -45
- package/src/components/ui/helpers/contextual-helper-panel.tsx +32 -7
- package/src/components/ui/helpers/helper-manager.test.tsx +54 -2
- package/src/components/ui/helpers/helper-manager.tsx +7 -1
- package/src/components/ui/helpers/item-helper.tsx +9 -1
- package/src/components/ui/panels/collections/collections-popover.tsx +8 -8
- package/src/components/ui/panels/mobile-panel-sheet.tsx +2 -2
- package/src/components/ui/panels/mobile-selection-bar.tsx +6 -3
- package/src/components/ui/panels/multi-selection-panel.tsx +120 -3
- package/src/components/ui/panels/panel-wrapper.tsx +53 -27
- package/src/components/ui/panels/parametric-field-control.tsx +2 -2
- package/src/components/ui/panels/parametric-inspector.tsx +3 -3
- package/src/components/ui/panels/room-panel.tsx +65 -0
- package/src/components/ui/primitives/badge.tsx +8 -0
- package/src/components/ui/primitives/sidebar.tsx +1 -1
- package/src/components/ui/right-stack.tsx +21 -2
- package/src/components/ui/sidebar/app-sidebar.tsx +1 -1
- package/src/components/ui/sidebar/icon-rail.tsx +3 -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 +107 -23
- package/src/components/ui/sidebar/panels/settings-panel/interface-theme.test.tsx +20 -0
- 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/index.tsx +5 -12
- package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +13 -10
- package/src/components/ui/sidebar/panels/zone-panel/index.tsx +2 -2
- package/src/components/ui/sidebar/tab-bar.tsx +4 -4
- package/src/components/viewer/viewer-controls-bar.tsx +18 -6
- package/src/components/viewer/viewer-rooms.tsx +16 -0
- package/src/hooks/use-auto-save.integration.test.tsx +96 -38
- package/src/hooks/use-auto-save.ts +26 -1
- 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 +65 -16
- package/src/hooks/use-keyboard.ts +29 -12
- package/src/hooks/use-placement-notice.test.ts +24 -1
- package/src/hooks/use-placement-notice.ts +37 -6
- package/src/index.tsx +71 -24
- 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/direct-manipulation.ts +2 -0
- package/src/lib/download-project-file.ts +17 -0
- package/src/lib/editor-level-display.ts +9 -2
- package/src/lib/editor-views.ts +35 -1
- package/src/lib/export-texture-utils.ts +2 -4
- package/src/lib/floor-foundation-support.test.ts +1 -0
- package/src/lib/floorplan/floorplan-pdfkit-document.ts +31 -16
- package/src/lib/floorplan/floorplan-pdfkit-renderer.test.ts +39 -0
- package/src/lib/floorplan/floorplan-pdfkit-renderer.ts +4 -0
- package/src/lib/floorplan/pdfkit-standard-fonts.d.ts +5 -0
- package/src/lib/floorplan/sheet-export.test.ts +24 -0
- package/src/lib/floorplan/site-plan/build-site-plan-drawing.ts +34 -45
- package/src/lib/floorplan/site-plan/coverage.ts +2 -4
- package/src/lib/floorplan/site-plan/front-edge.ts +2 -1
- package/src/lib/floorplan/site-plan/geometry.test.ts +9 -15
- package/src/lib/floorplan/site-plan/geometry.ts +1 -416
- package/src/lib/floorplan/site-plan/index.ts +0 -16
- package/src/lib/floorplan/site-plan/site-annotations.test.ts +117 -0
- package/src/lib/floorplan/site-plan/site-annotations.ts +3 -2
- package/src/lib/floorplan/site-plan/site-parts.ts +14 -106
- package/src/lib/floorplan/site-plan/yards.test.ts +2 -1
- 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 +112 -0
- package/src/lib/fresh-planar-placement.ts +57 -3
- package/src/lib/geometry-script/author.ts +6 -1
- package/src/lib/geometry-script/client.ts +8 -1
- package/src/lib/geometry-script/finish-scripts.ts +127 -0
- package/src/lib/geometry-script/protocol.ts +1 -0
- package/src/lib/glb-export.test.ts +187 -162
- package/src/lib/glb-export.ts +4 -1
- package/src/lib/history.test.ts +12 -7
- package/src/lib/interaction/scope.ts +4 -0
- package/src/lib/level-selection.test.ts +57 -0
- package/src/lib/level-selection.ts +16 -1
- package/src/lib/lot/clean-ring.test.ts +2 -1
- package/src/lib/lot/drop-in.ts +9 -24
- package/src/lib/lot/lot-patch.test.ts +1 -1
- package/src/lib/lot/lot-patch.ts +2 -2
- package/src/lib/measurement-kind.test.ts +6 -8
- package/src/lib/measurements.test.ts +23 -0
- package/src/lib/orbit-floor-clearance.test.ts +36 -0
- package/src/lib/orbit-floor-clearance.ts +48 -0
- package/src/lib/paint-pass-through.test.ts +11 -0
- package/src/lib/paint-pass-through.ts +4 -2
- package/src/lib/paint-scope.ts +1 -0
- package/src/lib/parametric-node-update.ts +16 -7
- package/src/lib/plugin-chat.ts +15 -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/room-assembly-overlay.ts +2 -3
- package/src/lib/room-selection.test.ts +45 -1
- package/src/lib/scene-clipboard.test.ts +246 -1
- package/src/lib/scene-clipboard.ts +44 -14
- package/src/lib/selection-routing.ts +1 -1
- 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/stair-levels.test.ts +25 -0
- package/src/lib/stair-levels.ts +22 -3
- package/src/lib/terrace-draft.ts +2 -1
- package/src/lib/viewer-selection.ts +22 -11
- package/src/lib/wall-placement-pointer.test.ts +42 -0
- package/src/lib/wall-placement-pointer.ts +57 -0
- package/src/lib/window-changes.ts +35 -0
- 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-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-stair-build-preview.ts +25 -5
- package/src/store/view-layout.test.ts +27 -1
- package/src/store/view-layout.ts +4 -1
- package/src/test-utils/render-current-markup.tsx +16 -0
- 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 -15
- package/src/components/tools/stair/stair-tool.tsx +0 -682
|
@@ -1,14 +1,56 @@
|
|
|
1
1
|
import { afterEach, beforeEach, describe, expect, test } from 'bun:test'
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
2
|
+
import {
|
|
3
|
+
type AnyNode,
|
|
4
|
+
type AnyNodeId,
|
|
5
|
+
createTerrainField,
|
|
6
|
+
emitter,
|
|
7
|
+
encodeTerrainField,
|
|
8
|
+
getLevelElevations,
|
|
9
|
+
sceneRegistry,
|
|
10
|
+
useScene,
|
|
11
|
+
} from '@pascal-app/core'
|
|
12
|
+
import { type SceneViewPose, sceneViewPlan } from '@pascal-app/core/agent-operations'
|
|
13
|
+
import {
|
|
14
|
+
BuildingNode,
|
|
15
|
+
CeilingNode,
|
|
16
|
+
LevelNode,
|
|
17
|
+
SiteNode,
|
|
18
|
+
SlabNode,
|
|
19
|
+
WallNode,
|
|
20
|
+
} from '@pascal-app/core/schema'
|
|
21
|
+
import {
|
|
22
|
+
groundWindowBoxNow,
|
|
23
|
+
hideFromScene,
|
|
24
|
+
SCENE_LAYER,
|
|
25
|
+
SITE_GROUND,
|
|
26
|
+
setGroundWindow,
|
|
27
|
+
useViewer,
|
|
28
|
+
} from '@pascal-app/viewer'
|
|
29
|
+
import {
|
|
30
|
+
Group,
|
|
31
|
+
Layers,
|
|
32
|
+
Mesh,
|
|
33
|
+
type Object3D,
|
|
34
|
+
OrthographicCamera,
|
|
35
|
+
PerspectiveCamera,
|
|
36
|
+
PlaneGeometry,
|
|
37
|
+
Raycaster,
|
|
38
|
+
Scene,
|
|
39
|
+
Vector2,
|
|
40
|
+
Vector3,
|
|
41
|
+
} from 'three'
|
|
6
42
|
import { CanvasTarget, ClippingGroup, type WebGPURenderer } from 'three/webgpu'
|
|
43
|
+
import hillside from '../../../../core/src/lib/__fixtures__/owner-hillside-grade.json'
|
|
44
|
+
import victorSeattle from '../../lib/__fixtures__/owner-victor-seattle-parking.json'
|
|
45
|
+
import villaAurelia from '../../lib/__fixtures__/owner-villa-aurelia-basement.json'
|
|
7
46
|
import {
|
|
8
47
|
atCaptureSize,
|
|
48
|
+
captureDimensions,
|
|
9
49
|
capturesOrthographically,
|
|
50
|
+
exteriorGroundForCapture,
|
|
10
51
|
interiorCutaway,
|
|
11
52
|
presentFinished,
|
|
53
|
+
presentWhileCapturing,
|
|
12
54
|
pumpFrames,
|
|
13
55
|
tileGrid,
|
|
14
56
|
} from './thumbnail-generator'
|
|
@@ -47,6 +89,34 @@ function fakeRenderer(canvas: FakeCanvas, pixelRatio: number) {
|
|
|
47
89
|
}
|
|
48
90
|
|
|
49
91
|
describe('supersampled capture', () => {
|
|
92
|
+
test('an explicit camera renders at the requested resolution and aspect without resizing the pane', () => {
|
|
93
|
+
const live: FakeCanvas = { width: 800, height: 1000, style: {} }
|
|
94
|
+
const renderer = fakeRenderer(live, 1)
|
|
95
|
+
const position: [number, number, number] = [2, 1, 3]
|
|
96
|
+
const target: [number, number, number] = [0, 0, 0]
|
|
97
|
+
for (const pose of [
|
|
98
|
+
{ perspective: { position, target, fov: 35 } },
|
|
99
|
+
{ ortho: { position, target, viewWidth: 4 } },
|
|
100
|
+
]) {
|
|
101
|
+
const size = captureDimensions(
|
|
102
|
+
[800, 1000],
|
|
103
|
+
{ ...pose, standardSize: { w: 1920, h: 1280 } },
|
|
104
|
+
24_000_000,
|
|
105
|
+
)
|
|
106
|
+
expect(size).toEqual([1920, 1280])
|
|
107
|
+
const buffer = atCaptureSize(renderer, ...size, () =>
|
|
108
|
+
renderer.getDrawingBufferSize(new Vector2()).toArray(),
|
|
109
|
+
)
|
|
110
|
+
expect(buffer).toEqual([1920, 1280])
|
|
111
|
+
expect([live.width, live.height]).toEqual([800, 1000])
|
|
112
|
+
}
|
|
113
|
+
expect(
|
|
114
|
+
captureDimensions([800, 1000], { standardSize: { w: 1920, h: 1280 } }, 24_000_000),
|
|
115
|
+
).toEqual([800, 1000])
|
|
116
|
+
expect(captureDimensions([800, 1000], { supersample: 2 }, 24_000_000)).toEqual([1600, 2000])
|
|
117
|
+
expect(captureDimensions([6000, 4000], { supersample: 2 }, 6_000_000)).toEqual([3000, 2000])
|
|
118
|
+
})
|
|
119
|
+
|
|
50
120
|
test('renders at its own size while the live canvas keeps its size', () => {
|
|
51
121
|
// a 1.5 dpr screen: the old capture resized the live canvas with drawing
|
|
52
122
|
// buffer pixels where CSS pixels were due — 3× during the capture, 1.5×
|
|
@@ -177,6 +247,46 @@ describe('a sheet capture session', () => {
|
|
|
177
247
|
unsubscribe()
|
|
178
248
|
expect(writes).toBe(0)
|
|
179
249
|
})
|
|
250
|
+
|
|
251
|
+
// A sheet's pictures and an agent's view can overlap: the user's view is the one before the
|
|
252
|
+
// first, never a run's finished presentation.
|
|
253
|
+
test('runs that overlap present once and put the view back after the last', () => {
|
|
254
|
+
const session = (owner: string, active: boolean) =>
|
|
255
|
+
emitter.emit('camera-controls:capture-session', { owner, active })
|
|
256
|
+
const steps: string[] = []
|
|
257
|
+
const stop = presentWhileCapturing(() => {
|
|
258
|
+
steps.push('present')
|
|
259
|
+
return () => steps.push('restore')
|
|
260
|
+
})
|
|
261
|
+
session('sheets-a', true)
|
|
262
|
+
session('view-b', true)
|
|
263
|
+
session('sheets-a', false)
|
|
264
|
+
expect(steps).toEqual(['present'])
|
|
265
|
+
session('nobody', false)
|
|
266
|
+
session('view-b', false)
|
|
267
|
+
expect(steps).toEqual(['present', 'restore'])
|
|
268
|
+
session('view-c', true)
|
|
269
|
+
stop()
|
|
270
|
+
expect(steps).toEqual(['present', 'restore', 'present', 'restore'])
|
|
271
|
+
session('view-c', false)
|
|
272
|
+
expect(steps).toHaveLength(4)
|
|
273
|
+
})
|
|
274
|
+
|
|
275
|
+
// The generator mounted while a sheet captures (a remount, the editor opened mid-run): the run's
|
|
276
|
+
// finished presentation from the first frame, the user's view back at its end.
|
|
277
|
+
test('a run already on when the generator mounts is presented at once', () => {
|
|
278
|
+
emitter.emit('camera-controls:capture-session', { owner: 'sheets-early', active: true })
|
|
279
|
+
const steps: string[] = []
|
|
280
|
+
const stop = presentWhileCapturing(() => {
|
|
281
|
+
steps.push('present')
|
|
282
|
+
return () => steps.push('restore')
|
|
283
|
+
})
|
|
284
|
+
expect(steps).toEqual(['present'])
|
|
285
|
+
emitter.emit('camera-controls:capture-session', { owner: 'sheets-early', active: false })
|
|
286
|
+
expect(steps).toEqual(['present', 'restore'])
|
|
287
|
+
stop()
|
|
288
|
+
expect(steps).toEqual(['present', 'restore'])
|
|
289
|
+
})
|
|
180
290
|
})
|
|
181
291
|
|
|
182
292
|
describe('a print-size picture past one render pass', () => {
|
|
@@ -289,6 +399,242 @@ describe('an interior view of a floor', () => {
|
|
|
289
399
|
})
|
|
290
400
|
})
|
|
291
401
|
|
|
402
|
+
// What a capture's own camera sees of the ground, on the owner's scenes (13 and 14 Oct, trimmed to
|
|
403
|
+
// their levels, slabs and walls) and a review's plate: the basement's floor and not the lawn, the
|
|
404
|
+
// window opened for the frame (and the person's own given back), and nothing opened from the
|
|
405
|
+
// garden side. Each test runs the cutaway as the capture does, looks, and restores.
|
|
406
|
+
describe('what a capture sees of the ground', () => {
|
|
407
|
+
let previousScene: ReturnType<typeof useScene.getState>
|
|
408
|
+
beforeEach(() => {
|
|
409
|
+
previousScene = useScene.getState()
|
|
410
|
+
})
|
|
411
|
+
afterEach(() => {
|
|
412
|
+
sceneRegistry.clear()
|
|
413
|
+
useScene.setState(previousScene, true)
|
|
414
|
+
})
|
|
415
|
+
|
|
416
|
+
/**
|
|
417
|
+
* The scene as the editor holds it: every level's object at its height, a floor in `basement`, and
|
|
418
|
+
* the site's lawn (wider than any frame) with its veil, hidden. The veil is never picked, as
|
|
419
|
+
* GroundVeil mounts it: the ray below sees through it.
|
|
420
|
+
*/
|
|
421
|
+
function editorScene(
|
|
422
|
+
nodes: Record<AnyNodeId, AnyNode>,
|
|
423
|
+
basement?: string,
|
|
424
|
+
siteId = 'site_owner',
|
|
425
|
+
) {
|
|
426
|
+
useScene.setState({ nodes } as never)
|
|
427
|
+
const scene = new Scene()
|
|
428
|
+
for (const [levelId, { baseY }] of getLevelElevations(nodes)) {
|
|
429
|
+
const level = new Group()
|
|
430
|
+
level.position.y = baseY
|
|
431
|
+
scene.add(level)
|
|
432
|
+
sceneRegistry.nodes.set(levelId, level)
|
|
433
|
+
}
|
|
434
|
+
const floor = new Mesh(new PlaneGeometry(400, 400).rotateX(-Math.PI / 2))
|
|
435
|
+
if (basement) sceneRegistry.nodes.get(basement)?.add(floor)
|
|
436
|
+
const ground = new Mesh(new PlaneGeometry(400, 400).rotateX(-Math.PI / 2))
|
|
437
|
+
ground.userData[SITE_GROUND] = 'solid'
|
|
438
|
+
const veil = new Mesh(ground.geometry)
|
|
439
|
+
veil.userData[SITE_GROUND] = 'veil'
|
|
440
|
+
veil.visible = false
|
|
441
|
+
veil.raycast = () => {}
|
|
442
|
+
const lotLine = new Mesh()
|
|
443
|
+
const site = new Group().add(ground, veil, lotLine)
|
|
444
|
+
scene.add(site)
|
|
445
|
+
sceneRegistry.nodes.set(siteId, site)
|
|
446
|
+
sceneRegistry.byType.site?.add(siteId)
|
|
447
|
+
return { scene, floor, ground, veil, lotLine }
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
/** Opens the capture's ground cutaway, runs `look`, restores in reverse; returns what `look` returns. */
|
|
451
|
+
function whileCut<T>(open: (add: (restore: () => void) => void) => void, look: () => T): T {
|
|
452
|
+
const restorers: (() => void)[] = []
|
|
453
|
+
open((restore) => restorers.push(restore))
|
|
454
|
+
try {
|
|
455
|
+
return look()
|
|
456
|
+
} finally {
|
|
457
|
+
for (const restore of restorers.reverse()) restore()
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
/** The first thing the capture camera draws on the line from its eye to its aim. */
|
|
462
|
+
function firstSeen(scene: Scene, pose: SceneViewPose): Object3D | undefined {
|
|
463
|
+
const eye = new Vector3(...pose.position)
|
|
464
|
+
const ray = new Raycaster(eye, new Vector3(...pose.target).sub(eye).normalize())
|
|
465
|
+
ray.layers.set(SCENE_LAYER)
|
|
466
|
+
scene.updateMatrixWorld(true)
|
|
467
|
+
const drawn = (object: Object3D | null): boolean =>
|
|
468
|
+
!object || (object.visible && drawn(object.parent))
|
|
469
|
+
return ray.intersectObject(scene, true).find((hit) => drawn(hit.object))?.object
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
const cameraAt = (position: [number, number, number]) => {
|
|
473
|
+
const camera = new PerspectiveCamera()
|
|
474
|
+
camera.position.set(...position)
|
|
475
|
+
return camera
|
|
476
|
+
}
|
|
477
|
+
const persons = {
|
|
478
|
+
min: [-6, 0, -7] as [number, number, number],
|
|
479
|
+
max: [6, 3, 7] as [number, number, number],
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
// The villa's Lower Ground 3.35 m under the lawn, the Victor's parking 2.61 m under it and past
|
|
483
|
+
// its west lot line.
|
|
484
|
+
test.each([
|
|
485
|
+
['the villa Aurelia', villaAurelia, 'level_68jf2zqaukfjof3m', 'level_ztimtvrgdzzd6c9f'],
|
|
486
|
+
['the Victor Seattle', victorSeattle, 'level_q209tmgwuzcahwzj', 'level_wj5tz664u03eolux'],
|
|
487
|
+
] as const)(
|
|
488
|
+
"the agent's picture of %s's basement sees its floor, not the lawn; a floor at grade is pictured on solid ground, even veiled",
|
|
489
|
+
(_, owner, basement, atGrade) => {
|
|
490
|
+
const nodes = owner.nodes as unknown as Record<AnyNodeId, AnyNode>
|
|
491
|
+
const plan = sceneViewPlan(nodes, { target: basement })
|
|
492
|
+
const { scene, floor, ground, veil, lotLine } = editorScene(nodes, basement)
|
|
493
|
+
expect(firstSeen(scene, plan.pose)).toBe(ground)
|
|
494
|
+
whileCut(
|
|
495
|
+
(add) => interiorCutaway(basement, add),
|
|
496
|
+
() => {
|
|
497
|
+
expect(firstSeen(scene, plan.pose)).toBe(floor)
|
|
498
|
+
expect(veil.visible).toBe(true)
|
|
499
|
+
expect(lotLine.layers.isEnabled(SCENE_LAYER)).toBe(true)
|
|
500
|
+
},
|
|
501
|
+
)
|
|
502
|
+
expect(firstSeen(scene, plan.pose)).toBe(ground)
|
|
503
|
+
expect(veil.visible).toBe(false)
|
|
504
|
+
|
|
505
|
+
// A floor at grade while the person has the ground veiled and aside.
|
|
506
|
+
hideFromScene(ground, 'ground-aside')
|
|
507
|
+
veil.visible = true
|
|
508
|
+
whileCut(
|
|
509
|
+
(add) => interiorCutaway(atGrade, add),
|
|
510
|
+
() => {
|
|
511
|
+
expect(ground.layers.isEnabled(SCENE_LAYER)).toBe(true)
|
|
512
|
+
expect(veil.visible).toBe(false)
|
|
513
|
+
},
|
|
514
|
+
)
|
|
515
|
+
expect(ground.layers.isEnabled(SCENE_LAYER)).toBe(false)
|
|
516
|
+
expect(veil.visible).toBe(true)
|
|
517
|
+
},
|
|
518
|
+
)
|
|
519
|
+
|
|
520
|
+
// The owner's hillside house (14 Oct): no floor buried, a 6 m plateau on the street side. A
|
|
521
|
+
// capture from the street sees the house through the ground window, from its own camera,
|
|
522
|
+
// whatever the person's view holds; from the garden side the ground stands solid.
|
|
523
|
+
describe('on the hillside house', () => {
|
|
524
|
+
const nodes = hillside.nodes as unknown as Record<AnyNodeId, AnyNode>
|
|
525
|
+
const garden = Object.values(nodes).find(
|
|
526
|
+
(node) => node.type === 'level' && node.name === 'Garden level · walk-out',
|
|
527
|
+
)
|
|
528
|
+
if (!garden) throw new Error('the hillside fixture is missing its garden level')
|
|
529
|
+
|
|
530
|
+
test('from the street side the window opens for the frame and closes after it, the solid ground staying drawn; from the garden side none opens', () => {
|
|
531
|
+
const { ground, veil } = editorScene(nodes, undefined, 'site_hillside')
|
|
532
|
+
whileCut(
|
|
533
|
+
(add) => interiorCutaway(garden.id, add, cameraAt([0, 7, -20])),
|
|
534
|
+
() => {
|
|
535
|
+
expect(groundWindowBoxNow()).not.toBeNull()
|
|
536
|
+
expect(veil.visible).toBe(true)
|
|
537
|
+
// Its window is cut per fragment, not by hiding it.
|
|
538
|
+
expect(ground.layers.isEnabled(SCENE_LAYER)).toBe(true)
|
|
539
|
+
},
|
|
540
|
+
)
|
|
541
|
+
expect(groundWindowBoxNow()).toBeNull()
|
|
542
|
+
expect(veil.visible).toBe(false)
|
|
543
|
+
whileCut(
|
|
544
|
+
(add) => interiorCutaway(garden.id, add, cameraAt([0, 3, 20])),
|
|
545
|
+
() => {
|
|
546
|
+
expect(groundWindowBoxNow()).toBeNull()
|
|
547
|
+
expect(veil.visible).toBe(false)
|
|
548
|
+
},
|
|
549
|
+
)
|
|
550
|
+
})
|
|
551
|
+
|
|
552
|
+
// Both agent lanes ask with a perspective/orthographic pose of their own, not a cameraPose:
|
|
553
|
+
// the posed exterior frame decides its window from its own eye, not the person's closed one.
|
|
554
|
+
test('a posed exterior capture decides its own window and gives the person’s back; an unposed one decides none', () => {
|
|
555
|
+
const { veil } = editorScene(nodes, undefined, 'site_hillside')
|
|
556
|
+
whileCut(
|
|
557
|
+
(add) => exteriorGroundForCapture(add, true, cameraAt([0, 7, -20])),
|
|
558
|
+
() => {
|
|
559
|
+
expect(groundWindowBoxNow()).not.toBeNull()
|
|
560
|
+
expect(veil.visible).toBe(true)
|
|
561
|
+
},
|
|
562
|
+
)
|
|
563
|
+
expect(groundWindowBoxNow()).toBeNull()
|
|
564
|
+
expect(veil.visible).toBe(false)
|
|
565
|
+
// The person's window, open for their eye, closes for a garden-side frame and returns; an
|
|
566
|
+
// unposed capture leaves it alone.
|
|
567
|
+
setGroundWindow(persons)
|
|
568
|
+
whileCut(
|
|
569
|
+
(add) => exteriorGroundForCapture(add, true, cameraAt([0, 3, 20])),
|
|
570
|
+
() => expect(groundWindowBoxNow()).toBeNull(),
|
|
571
|
+
)
|
|
572
|
+
expect(groundWindowBoxNow()).toBe(persons)
|
|
573
|
+
whileCut(
|
|
574
|
+
(add) => exteriorGroundForCapture(add, false, cameraAt([0, 3, 20])),
|
|
575
|
+
() => expect(groundWindowBoxNow()).toBe(persons),
|
|
576
|
+
)
|
|
577
|
+
expect(groundWindowBoxNow()).toBe(persons)
|
|
578
|
+
setGroundWindow(null)
|
|
579
|
+
})
|
|
580
|
+
})
|
|
581
|
+
|
|
582
|
+
// The review's plate (14 Oct): [-5,5] × [-4,4] at Y 0.2, terrain 2 m only inside x 0…4, z -3…3.
|
|
583
|
+
// An interior capture from overhead opens the window over the buried eastern rooms: partly below
|
|
584
|
+
// grade has no wholesale veil to fall back on.
|
|
585
|
+
test('from overhead the window opens over the buried rooms of a floor partly below grade, and closes after', () => {
|
|
586
|
+
const field = createTerrainField({ cols: 25, rows: 25, spacing: 1, origin: [-12, -12] })
|
|
587
|
+
const heights = new Int16Array(field.heights)
|
|
588
|
+
for (let row = 0; row < field.rows; row += 1)
|
|
589
|
+
for (let col = 0; col < field.cols; col += 1) {
|
|
590
|
+
const x = field.origin[0] + col * field.spacing
|
|
591
|
+
const z = field.origin[1] + row * field.spacing
|
|
592
|
+
if (x >= 0 && x <= 4 && z >= -3 && z <= 3) heights[row * field.cols + col] = 200
|
|
593
|
+
}
|
|
594
|
+
const list: AnyNode[] = [
|
|
595
|
+
SiteNode.parse({
|
|
596
|
+
id: 'site_a',
|
|
597
|
+
children: ['building_a'],
|
|
598
|
+
terrain: encodeTerrainField({ ...field, heights }),
|
|
599
|
+
}),
|
|
600
|
+
BuildingNode.parse({ id: 'building_a', parentId: 'site_a', children: ['level_0'] }),
|
|
601
|
+
LevelNode.parse({
|
|
602
|
+
id: 'level_0',
|
|
603
|
+
level: 0,
|
|
604
|
+
height: 3,
|
|
605
|
+
parentId: 'building_a',
|
|
606
|
+
children: ['slab_0'],
|
|
607
|
+
}),
|
|
608
|
+
SlabNode.parse({
|
|
609
|
+
id: 'slab_0',
|
|
610
|
+
parentId: 'level_0',
|
|
611
|
+
elevation: 0.2,
|
|
612
|
+
polygon: [
|
|
613
|
+
[-5, -4],
|
|
614
|
+
[5, -4],
|
|
615
|
+
[5, 4],
|
|
616
|
+
[-5, 4],
|
|
617
|
+
],
|
|
618
|
+
}),
|
|
619
|
+
]
|
|
620
|
+
const { ground, veil } = editorScene(
|
|
621
|
+
Object.fromEntries(list.map((node) => [node.id, node])) as Record<AnyNodeId, AnyNode>,
|
|
622
|
+
undefined,
|
|
623
|
+
'site_a',
|
|
624
|
+
)
|
|
625
|
+
whileCut(
|
|
626
|
+
(add) => interiorCutaway('level_0', add, cameraAt([0, 15, 0])),
|
|
627
|
+
() => {
|
|
628
|
+
expect(groundWindowBoxNow()).not.toBeNull()
|
|
629
|
+
expect(veil.visible).toBe(true)
|
|
630
|
+
expect(ground.layers.isEnabled(SCENE_LAYER)).toBe(true)
|
|
631
|
+
},
|
|
632
|
+
)
|
|
633
|
+
expect(groundWindowBoxNow()).toBeNull()
|
|
634
|
+
expect(veil.visible).toBe(false)
|
|
635
|
+
})
|
|
636
|
+
})
|
|
637
|
+
|
|
292
638
|
describe("a capture's projection", () => {
|
|
293
639
|
const user = { ortho: new OrthographicCamera(), perspective: new PerspectiveCamera() }
|
|
294
640
|
const view = { position: [8, 6, 8], target: [0, 1, 0] } as never
|
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
emitter,
|
|
5
|
+
isCaptureSessionOn,
|
|
6
|
+
levelGradeStanding,
|
|
7
|
+
sceneRegistry,
|
|
8
|
+
subscribeCaptureSessions,
|
|
9
|
+
type ThumbnailGenerateEvent,
|
|
10
|
+
useScene,
|
|
11
|
+
} from '@pascal-app/core'
|
|
4
12
|
import { sceneViewInterior } from '@pascal-app/core/agent-operations'
|
|
5
13
|
import {
|
|
6
14
|
computeHeroFraming,
|
|
@@ -9,6 +17,7 @@ import {
|
|
|
9
17
|
heroCameraPose,
|
|
10
18
|
holdLiveFrame,
|
|
11
19
|
pendingSceneBuildCount,
|
|
20
|
+
presentLevelsForCapture,
|
|
12
21
|
refreshIsolation,
|
|
13
22
|
SNAPSHOT_MAX_EDGE,
|
|
14
23
|
SNAPSHOT_MIME,
|
|
@@ -17,9 +26,12 @@ import {
|
|
|
17
26
|
snapLevelsToTruePositions,
|
|
18
27
|
THUMBNAIL_HEIGHT,
|
|
19
28
|
THUMBNAIL_WIDTH,
|
|
29
|
+
temporarilyGroundWindowForCapture,
|
|
20
30
|
temporarilyHideNodeTypes,
|
|
21
31
|
temporarilyShowPresentation,
|
|
22
32
|
temporarilyShowShadowOnly,
|
|
33
|
+
temporarilyVeilGround,
|
|
34
|
+
temporarilyWithoutAmbiance,
|
|
23
35
|
useSceneAtmosphere,
|
|
24
36
|
useViewer,
|
|
25
37
|
} from '@pascal-app/viewer'
|
|
@@ -48,6 +60,7 @@ export interface SnapshotCameraData {
|
|
|
48
60
|
zoom?: number
|
|
49
61
|
captureMode?: 'standard' | 'viewport' | 'area'
|
|
50
62
|
resolution?: { w: number; h: number }
|
|
63
|
+
presentation?: ThumbnailGenerateEvent['presentation']
|
|
51
64
|
}
|
|
52
65
|
|
|
53
66
|
interface ThumbnailGeneratorProps {
|
|
@@ -69,6 +82,25 @@ const TILE_OVERLAP = 96
|
|
|
69
82
|
|
|
70
83
|
type CaptureLimits = { passEdge: number; maxPixels: number; software: boolean }
|
|
71
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
|
+
|
|
72
104
|
/**
|
|
73
105
|
* What one capture may ask of this GPU: a pass no larger than its biggest
|
|
74
106
|
* texture (and MAX_PASS_EDGE), and — on a software renderer, where a big
|
|
@@ -443,6 +475,29 @@ export function presentFinished(): () => void {
|
|
|
443
475
|
}
|
|
444
476
|
}
|
|
445
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
|
+
|
|
446
501
|
/** A yield to the next macrotask through a MessageChannel — unlike a timer, never throttled in a hidden tab. */
|
|
447
502
|
function macrotask(): Promise<void> {
|
|
448
503
|
return new Promise((resolve) => {
|
|
@@ -492,10 +547,18 @@ function clipSceneFor(
|
|
|
492
547
|
* floors and this floor's roofs and ceilings out of the picture, everything
|
|
493
548
|
* on it cut where core says (sceneViewInterior, from the scene as this editor
|
|
494
549
|
* has it) and its shadows with it, and whatever the user's solo or isolation
|
|
495
|
-
* holds back on it shown.
|
|
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).
|
|
496
554
|
*/
|
|
497
|
-
export function interiorCutaway(
|
|
498
|
-
|
|
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)
|
|
499
562
|
const floor = sceneRegistry.nodes.get(levelId)
|
|
500
563
|
if (!floor) throw new Error('The floor to look inside is not built yet. Try again.')
|
|
501
564
|
for (const id of hiddenIds) {
|
|
@@ -508,6 +571,29 @@ export function interiorCutaway(levelId: string, restore: (callback: () => void)
|
|
|
508
571
|
}
|
|
509
572
|
restore(temporarilyShowPresentation(floor))
|
|
510
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))
|
|
511
597
|
}
|
|
512
598
|
|
|
513
599
|
/**
|
|
@@ -670,21 +756,12 @@ export const ThumbnailGenerator = ({ onThumbnailCapture }: ThumbnailGeneratorPro
|
|
|
670
756
|
// a supersampled frame renders at a multiple of the canvas' size into
|
|
671
757
|
// the capture's own targets; the live canvas is never resized
|
|
672
758
|
const canvasSize: [number, number] = [gl.domElement.width, gl.domElement.height]
|
|
759
|
+
const pose = ortho ?? perspective
|
|
673
760
|
const limits = captureLimits(gl)
|
|
674
|
-
const
|
|
675
|
-
1,
|
|
676
|
-
Math.min(
|
|
677
|
-
supersample,
|
|
678
|
-
MAX_CAPTURE_EDGE / Math.max(canvasSize[0], canvasSize[1]),
|
|
679
|
-
Math.sqrt(limits.maxPixels / (canvasSize[0] * canvasSize[1])),
|
|
680
|
-
),
|
|
681
|
-
)
|
|
682
|
-
const width = Math.round(canvasSize[0] * scale)
|
|
683
|
-
const height = Math.round(canvasSize[1] * scale)
|
|
761
|
+
const [width, height] = captureDimensions(canvasSize, event, limits.maxPixels)
|
|
684
762
|
// a supersampled picture is encoded at the size it could render (the
|
|
685
763
|
// caller asked for canvas × supersample, maybe past this GPU's limits)
|
|
686
764
|
const encodeSize = supersample > 1 ? { w: width, h: height } : standardSize
|
|
687
|
-
const pose = ortho ?? perspective
|
|
688
765
|
// a background tab draws no frames and (on the WebGL backend) never
|
|
689
766
|
// finishes a readback: a sheet's capture says so at once
|
|
690
767
|
if (pose && isTabHidden()) throw new Error(HIDDEN_TAB)
|
|
@@ -783,7 +860,7 @@ export const ThumbnailGenerator = ({ onThumbnailCapture }: ThumbnailGeneratorPro
|
|
|
783
860
|
}
|
|
784
861
|
|
|
785
862
|
pipeline?.applyEnvironment({
|
|
786
|
-
theme: useViewer.getState().
|
|
863
|
+
theme: useViewer.getState().sceneAppearance,
|
|
787
864
|
transparent,
|
|
788
865
|
grade: useViewer.getState().shading === 'rendered',
|
|
789
866
|
edges,
|
|
@@ -836,6 +913,10 @@ export const ThumbnailGenerator = ({ onThumbnailCapture }: ThumbnailGeneratorPro
|
|
|
836
913
|
// the scene as the picture wants it, for one render; `restore` puts
|
|
837
914
|
// every change back right after it
|
|
838
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())
|
|
839
920
|
if (snapLevels) {
|
|
840
921
|
const prevMode = useViewer.getState().levelMode
|
|
841
922
|
if (prevMode !== 'stacked') {
|
|
@@ -876,7 +957,7 @@ export const ThumbnailGenerator = ({ onThumbnailCapture }: ThumbnailGeneratorPro
|
|
|
876
957
|
thumbnailCamera.lookAt(hero.target[0], hero.target[1], hero.target[2])
|
|
877
958
|
thumbnailCamera.updateMatrixWorld()
|
|
878
959
|
pipeline?.applyEnvironment({
|
|
879
|
-
theme: useViewer.getState().
|
|
960
|
+
theme: useViewer.getState().sceneAppearance,
|
|
880
961
|
transparent,
|
|
881
962
|
grade: useViewer.getState().shading === 'rendered',
|
|
882
963
|
edges,
|
|
@@ -904,9 +985,15 @@ export const ThumbnailGenerator = ({ onThumbnailCapture }: ThumbnailGeneratorPro
|
|
|
904
985
|
// those meshes before either snapshot render path submits them,
|
|
905
986
|
// and restore fog afterwards so the atmosphere never leaks in.
|
|
906
987
|
restore(refreshIsolation(scene))
|
|
988
|
+
if (event.presentation) {
|
|
989
|
+
restore(presentLevelsForCapture(event.presentation))
|
|
990
|
+
cameraData.presentation = event.presentation
|
|
991
|
+
}
|
|
907
992
|
if (interior) {
|
|
908
|
-
interiorCutaway(interior.levelId, restore)
|
|
993
|
+
interiorCutaway(interior.levelId, restore, thumbnailCamera)
|
|
909
994
|
restore(lightInterior(scene, thumbnailCamera))
|
|
995
|
+
} else if (snapLevels || pose || cameraPose) {
|
|
996
|
+
exteriorGroundForCapture(restore, Boolean(cameraPose || pose), thumbnailCamera)
|
|
910
997
|
}
|
|
911
998
|
}
|
|
912
999
|
// The scene pass is a FRAME-updated node: it renders once per node
|
|
@@ -943,7 +1030,7 @@ export const ThumbnailGenerator = ({ onThumbnailCapture }: ThumbnailGeneratorPro
|
|
|
943
1030
|
restore(() => thumbnailCamera.clearViewOffset())
|
|
944
1031
|
thumbnailCamera.updateMatrixWorld()
|
|
945
1032
|
pipeline.applyEnvironment({
|
|
946
|
-
theme: useViewer.getState().
|
|
1033
|
+
theme: useViewer.getState().sceneAppearance,
|
|
947
1034
|
transparent,
|
|
948
1035
|
grade: useViewer.getState().shading === 'rendered',
|
|
949
1036
|
edges,
|
|
@@ -1008,7 +1095,8 @@ export const ThumbnailGenerator = ({ onThumbnailCapture }: ThumbnailGeneratorPro
|
|
|
1008
1095
|
}
|
|
1009
1096
|
// a sheet's picture (or a supersampled one) is never drawn into
|
|
1010
1097
|
// the live canvas the viewport shows
|
|
1011
|
-
if (pose ||
|
|
1098
|
+
if (pose || width !== canvasSize[0] || height !== canvasSize[1])
|
|
1099
|
+
throw new Error('Snapshot renderer is not ready. Try again.')
|
|
1012
1100
|
// Fallback: plain render directly to the canvas
|
|
1013
1101
|
if (transparent) {
|
|
1014
1102
|
const clearColor = (
|
|
@@ -1140,7 +1228,7 @@ export const ThumbnailGenerator = ({ onThumbnailCapture }: ThumbnailGeneratorPro
|
|
|
1140
1228
|
if (transparent || edgesOverride !== undefined || ortho !== undefined) {
|
|
1141
1229
|
try {
|
|
1142
1230
|
pipelineRef.current?.applyEnvironment({
|
|
1143
|
-
theme: useViewer.getState().
|
|
1231
|
+
theme: useViewer.getState().sceneAppearance,
|
|
1144
1232
|
transparent: false,
|
|
1145
1233
|
grade: useViewer.getState().shading === 'rendered',
|
|
1146
1234
|
edges: useViewer.getState().edges,
|
|
@@ -1158,39 +1246,25 @@ export const ThumbnailGenerator = ({ onThumbnailCapture }: ThumbnailGeneratorPro
|
|
|
1158
1246
|
[gl, scene, getThree, orthoPipeline, advance, clock],
|
|
1159
1247
|
)
|
|
1160
1248
|
|
|
1161
|
-
// A sheet's
|
|
1162
|
-
// up for its pictures — the finished presentation, the ground hidden —
|
|
1163
|
-
//
|
|
1164
|
-
// finished house whatever view the user was in;
|
|
1165
|
-
// user's view back, then (a few commits later) the viewport
|
|
1166
|
-
//
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
restorePresentation()
|
|
1181
|
-
setTimeout(releaseLiveFrame, SESSION_RESUME_MS)
|
|
1182
|
-
})
|
|
1183
|
-
}
|
|
1184
|
-
const bus = emitter as unknown as {
|
|
1185
|
-
on: (name: string, handler: typeof onSession) => void
|
|
1186
|
-
off: (name: string, handler: typeof onSession) => void
|
|
1187
|
-
}
|
|
1188
|
-
bus.on('camera-controls:capture-session', onSession)
|
|
1189
|
-
return () => {
|
|
1190
|
-
bus.off('camera-controls:capture-session', onSession)
|
|
1191
|
-
for (const owner of [...sessions.keys()]) end(owner)
|
|
1192
|
-
}
|
|
1193
|
-
}, [])
|
|
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
|
+
[],
|
|
1267
|
+
)
|
|
1194
1268
|
|
|
1195
1269
|
// Thumbnail request via emitter. Two call shapes:
|
|
1196
1270
|
// - user-driven capture: `{ projectId, captureMode, cropRegion }` — captures
|