@pascal-app/editor 1.0.4 → 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 +39 -37
- 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
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import type { WallMode } from '@pascal-app/viewer'
|
|
2
|
+
import type { RoomKey } from './room-selection'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Follow Pascal changes how the building is shown so the build it follows is in view: the walls cut
|
|
6
|
+
* away when no eye sees its target, the floor it works on selected, the floors above it hidden. This
|
|
7
|
+
* keeps what it changed, and what the person had before, so that when Follow ends it puts back exactly
|
|
8
|
+
* that: a setting it changed and the person has not touched since goes back to what the person had.
|
|
9
|
+
*
|
|
10
|
+
* A setting the person changes while Follow is on is theirs: it is not put back, and Follow does not
|
|
11
|
+
* change it again until it has ended. Everything here is arithmetic on a port, so it is tested without
|
|
12
|
+
* a viewer; the component that owns the port is `FollowPascal`.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
export type LevelMode = 'stacked' | 'exploded' | 'solo' | 'manual'
|
|
16
|
+
|
|
17
|
+
/** The floor as the person has it: the selection and the room picked in the editor. */
|
|
18
|
+
export type Floor = {
|
|
19
|
+
buildingId: string | null
|
|
20
|
+
levelId: string | null
|
|
21
|
+
zoneId: string | null
|
|
22
|
+
selectedIds: readonly string[]
|
|
23
|
+
room: RoomKey | null
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export type ViewPresentation = {
|
|
27
|
+
wall: WallMode
|
|
28
|
+
levels: LevelMode
|
|
29
|
+
/** The upper floors are hidden, above the selected one. */
|
|
30
|
+
hideAbove: boolean
|
|
31
|
+
floor: Floor
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** What it needs of the viewer and the editor: read the view, and write one part of it. */
|
|
35
|
+
export type PresentationPort = {
|
|
36
|
+
read(): ViewPresentation
|
|
37
|
+
setWall(mode: WallMode): void
|
|
38
|
+
setLevels(mode: LevelMode): void
|
|
39
|
+
setHideAbove(hide: boolean): void
|
|
40
|
+
/** Selects a floor in its building. The room and what was picked on the floor before are dropped. */
|
|
41
|
+
setFloor(floor: { buildingId: string | null; levelId: string }): void
|
|
42
|
+
/** Puts a floor back as the person had it; what is no longer in the scene is dropped. */
|
|
43
|
+
putFloor(floor: Floor): void
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
type Part = keyof ViewPresentation
|
|
47
|
+
|
|
48
|
+
function sameFloor(a: Floor, b: Floor): boolean {
|
|
49
|
+
return (
|
|
50
|
+
a.buildingId === b.buildingId &&
|
|
51
|
+
a.levelId === b.levelId &&
|
|
52
|
+
a.zoneId === b.zoneId &&
|
|
53
|
+
a.selectedIds.length === b.selectedIds.length &&
|
|
54
|
+
a.selectedIds.every((id, index) => id === b.selectedIds[index]) &&
|
|
55
|
+
(a.room === b.room ||
|
|
56
|
+
(!!a.room &&
|
|
57
|
+
!!b.room &&
|
|
58
|
+
a.room.levelId === b.room.levelId &&
|
|
59
|
+
a.room.zoneId === b.room.zoneId))
|
|
60
|
+
)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function same<K extends Part>(part: K, a: ViewPresentation[K], b: ViewPresentation[K]): boolean {
|
|
64
|
+
return part === 'floor' ? sameFloor(a as Floor, b as Floor) : a === b
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export class FollowPresentation {
|
|
68
|
+
/** For each part Follow changed: what the person had, and what Follow left it as. */
|
|
69
|
+
private held: { [K in Part]?: { before: ViewPresentation[K]; set: ViewPresentation[K] } } = {}
|
|
70
|
+
/** Parts the person changed after Follow did: theirs until Follow ends. */
|
|
71
|
+
private theirs = new Set<Part>()
|
|
72
|
+
|
|
73
|
+
constructor(private port: PresentationPort) {}
|
|
74
|
+
|
|
75
|
+
/** The walls as `mode` shows them (cutaway, when no eye sees the target). */
|
|
76
|
+
wall(mode: WallMode): void {
|
|
77
|
+
this.change('wall', mode === this.port.read().wall, () => this.port.setWall(mode))
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
levels(mode: LevelMode): void {
|
|
81
|
+
this.change('levels', mode === this.port.read().levels, () => this.port.setLevels(mode))
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** The floors above the selected one hidden, or shown. */
|
|
85
|
+
hideAbove(hide: boolean): void {
|
|
86
|
+
this.change('hideAbove', hide === this.port.read().hideAbove, () =>
|
|
87
|
+
this.port.setHideAbove(hide),
|
|
88
|
+
)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** The floor a build works on, selected. Nothing changes when it is the selected one. */
|
|
92
|
+
floor(target: { buildingId: string | null; levelId: string }): void {
|
|
93
|
+
this.change('floor', this.port.read().floor.levelId === target.levelId, () =>
|
|
94
|
+
this.port.setFloor(target),
|
|
95
|
+
)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Follow ends: each part it changed goes back to what the person had, unless they have changed it
|
|
100
|
+
* since. A floor goes back only when the selection is as Follow left it: a room or an element
|
|
101
|
+
* picked on the floor is the person's choice of that floor.
|
|
102
|
+
*/
|
|
103
|
+
restore(): void {
|
|
104
|
+
const now = this.port.read()
|
|
105
|
+
const { wall, levels, hideAbove, floor } = this.held
|
|
106
|
+
this.held = {}
|
|
107
|
+
this.theirs.clear()
|
|
108
|
+
if (wall && now.wall === wall.set) this.port.setWall(wall.before)
|
|
109
|
+
if (levels && now.levels === levels.set) this.port.setLevels(levels.before)
|
|
110
|
+
if (hideAbove && now.hideAbove === hideAbove.set) this.port.setHideAbove(hideAbove.before)
|
|
111
|
+
if (floor && sameFloor(now.floor, floor.set)) this.port.putFloor(floor.before)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
private change<K extends Part>(part: K, already: boolean, write: () => void): void {
|
|
115
|
+
if (this.theirs.has(part)) return
|
|
116
|
+
const held = this.held[part]
|
|
117
|
+
const now = this.port.read()[part]
|
|
118
|
+
if (held && !same(part, now, held.set as ViewPresentation[K])) {
|
|
119
|
+
// Changed since Follow wrote it: the person's, and not Follow's to put back or to write over.
|
|
120
|
+
delete this.held[part]
|
|
121
|
+
this.theirs.add(part)
|
|
122
|
+
return
|
|
123
|
+
}
|
|
124
|
+
if (already) return
|
|
125
|
+
write()
|
|
126
|
+
const set = this.port.read()[part]
|
|
127
|
+
;(this.held as Record<Part, unknown>)[part] = { before: held ? held.before : now, set }
|
|
128
|
+
}
|
|
129
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type AnyNodeId,
|
|
3
|
+
type CameraPose,
|
|
4
|
+
findLevelAncestorId,
|
|
5
|
+
sceneRegistry,
|
|
6
|
+
useScene,
|
|
7
|
+
} from '@pascal-app/core'
|
|
8
|
+
import { BATCHED_LAYER, SCENE_LAYER, setSurfaceRaycastLayers, useViewer } from '@pascal-app/viewer'
|
|
9
|
+
import { type Intersection, type Mesh, type Object3D, Raycaster, Vector3 } from 'three'
|
|
10
|
+
import type { FollowWant } from './follow-pascal'
|
|
11
|
+
|
|
12
|
+
const DRAWN_LAYERS = (1 << SCENE_LAYER) | (1 << BATCHED_LAYER)
|
|
13
|
+
|
|
14
|
+
/** Query modeled surfaces, including batch sources, but not editor overlays or shadow-only floors. */
|
|
15
|
+
export class FollowSight {
|
|
16
|
+
private readonly ray = new Raycaster()
|
|
17
|
+
private readonly eye = new Vector3()
|
|
18
|
+
private readonly direction = new Vector3()
|
|
19
|
+
private readonly hits: Intersection[] = []
|
|
20
|
+
private meshes: Mesh[] = []
|
|
21
|
+
|
|
22
|
+
constructor(private scene: Object3D) {
|
|
23
|
+
setSurfaceRaycastLayers(this.ray.layers)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
refresh(want: FollowWant): void {
|
|
27
|
+
this.meshes = []
|
|
28
|
+
const root = this.scene.getObjectByName('scene-renderer')
|
|
29
|
+
if (!root) return
|
|
30
|
+
const targets = new Set(
|
|
31
|
+
want.nodeIds?.flatMap((id) => {
|
|
32
|
+
const object = sceneRegistry.nodes.get(id)
|
|
33
|
+
return object ? [object] : []
|
|
34
|
+
}),
|
|
35
|
+
)
|
|
36
|
+
const owners = new Map([...sceneRegistry.nodes].map(([id, object]) => [object, id]))
|
|
37
|
+
const { nodes } = useScene.getState()
|
|
38
|
+
const viewer = useViewer.getState()
|
|
39
|
+
const selected = viewer.selection.levelId ? nodes[viewer.selection.levelId] : undefined
|
|
40
|
+
root.updateWorldMatrix(true, true)
|
|
41
|
+
root.traverseVisible((object) => {
|
|
42
|
+
const mesh = object as Mesh
|
|
43
|
+
if (!mesh.isMesh || (mesh.layers.mask & DRAWN_LAYERS) === 0) return
|
|
44
|
+
for (let ancestor: Object3D | null = object; ancestor; ancestor = ancestor.parent) {
|
|
45
|
+
if (targets.has(ancestor)) return
|
|
46
|
+
const id = owners.get(ancestor)
|
|
47
|
+
if (!id) continue
|
|
48
|
+
if (nodes[id as AnyNodeId]?.visible === false) return
|
|
49
|
+
// A batched source can retain its batch layer while its floor is shadow-only.
|
|
50
|
+
if (
|
|
51
|
+
mesh.layers.isEnabled(BATCHED_LAYER) &&
|
|
52
|
+
viewer.hideLevelsAboveSelection &&
|
|
53
|
+
selected?.type === 'level'
|
|
54
|
+
) {
|
|
55
|
+
const levelId = findLevelAncestorId(id as AnyNodeId, nodes)
|
|
56
|
+
const level = levelId ? nodes[levelId as AnyNodeId] : undefined
|
|
57
|
+
if (
|
|
58
|
+
level?.type === 'level' &&
|
|
59
|
+
level.level > selected.level &&
|
|
60
|
+
!viewer.levelsRevealing.includes(level.id)
|
|
61
|
+
)
|
|
62
|
+
return
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
this.meshes.push(mesh)
|
|
66
|
+
})
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
clear(pose: CameraPose): boolean {
|
|
70
|
+
this.eye.set(...pose.position)
|
|
71
|
+
this.direction.set(...pose.target).sub(this.eye)
|
|
72
|
+
this.ray.far = this.direction.length() - 0.02
|
|
73
|
+
this.ray.set(this.eye, this.direction.normalize())
|
|
74
|
+
this.hits.length = 0
|
|
75
|
+
this.ray.intersectObjects(this.meshes, false, this.hits)
|
|
76
|
+
return !this.hits.some((hit) => {
|
|
77
|
+
const mesh = hit.object as Mesh
|
|
78
|
+
const material = Array.isArray(mesh.material)
|
|
79
|
+
? mesh.material[hit.face?.materialIndex ?? 0]
|
|
80
|
+
: mesh.material
|
|
81
|
+
return material?.visible && material.opacity > 0.1 && material.colorWrite
|
|
82
|
+
})
|
|
83
|
+
}
|
|
84
|
+
}
|
|
@@ -0,0 +1,406 @@
|
|
|
1
|
+
import { afterAll, beforeAll, describe, expect, test } from 'bun:test'
|
|
2
|
+
import {
|
|
3
|
+
type AnyNode,
|
|
4
|
+
type AnyNodeDefinition,
|
|
5
|
+
BuildingNode,
|
|
6
|
+
DoorNode,
|
|
7
|
+
ItemNode,
|
|
8
|
+
LevelNode,
|
|
9
|
+
loadPlugin,
|
|
10
|
+
nodeRegistry,
|
|
11
|
+
RoofNode,
|
|
12
|
+
RoofSegmentNode,
|
|
13
|
+
WallNode,
|
|
14
|
+
} from '@pascal-app/core'
|
|
15
|
+
import type { RevealEvent } from '@pascal-app/viewer'
|
|
16
|
+
import { openingExterior, wallExterior } from './follow-pascal-exterior.test-fixture'
|
|
17
|
+
import { stayOnLevel, wantFromEvent } from './follow-pascal-targets'
|
|
18
|
+
|
|
19
|
+
// What the camera looks at for each thing the reveal says: a floor when its walls rise, a window when
|
|
20
|
+
// it is cut in, an item when it drops, the roof from above as it assembles, and the whole house when
|
|
21
|
+
// the build is finished.
|
|
22
|
+
|
|
23
|
+
function house(at: [number, number, number] = [0, 0, 0]) {
|
|
24
|
+
const building = BuildingNode.parse({ id: 'building_h', position: at })
|
|
25
|
+
const level = LevelNode.parse({ id: 'level_h', parentId: building.id, level: 0, height: 2.7 })
|
|
26
|
+
const roofLevel = LevelNode.parse({ id: 'level_roof', parentId: building.id, level: 1 })
|
|
27
|
+
const corners: [number, number][] = [
|
|
28
|
+
[0, 0],
|
|
29
|
+
[10, 0],
|
|
30
|
+
[10, 8],
|
|
31
|
+
[0, 8],
|
|
32
|
+
]
|
|
33
|
+
const walls = corners.map((start, index) =>
|
|
34
|
+
WallNode.parse({
|
|
35
|
+
id: `wall_${index}`,
|
|
36
|
+
parentId: level.id,
|
|
37
|
+
start,
|
|
38
|
+
end: corners[(index + 1) % 4]!,
|
|
39
|
+
frontSide: 'interior',
|
|
40
|
+
backSide: 'exterior',
|
|
41
|
+
height: 2.7,
|
|
42
|
+
}),
|
|
43
|
+
)
|
|
44
|
+
const door = DoorNode.parse({
|
|
45
|
+
id: 'door_h',
|
|
46
|
+
parentId: walls[0]!.id,
|
|
47
|
+
wallId: walls[0]!.id,
|
|
48
|
+
position: [3, 1.05, 0],
|
|
49
|
+
width: 0.9,
|
|
50
|
+
height: 2.1,
|
|
51
|
+
})
|
|
52
|
+
const item = ItemNode.parse({
|
|
53
|
+
id: 'item_h',
|
|
54
|
+
parentId: level.id,
|
|
55
|
+
position: [4, 0, 3],
|
|
56
|
+
asset: {
|
|
57
|
+
id: 'chair',
|
|
58
|
+
category: 'furniture',
|
|
59
|
+
name: 'Chair',
|
|
60
|
+
thumbnail: '',
|
|
61
|
+
src: '/items/chair/model.glb',
|
|
62
|
+
dimensions: [0.6, 0.9, 0.6],
|
|
63
|
+
},
|
|
64
|
+
})
|
|
65
|
+
const segment = RoofSegmentNode.parse({
|
|
66
|
+
id: 'rseg_h',
|
|
67
|
+
parentId: 'roof_h',
|
|
68
|
+
roofType: 'gable',
|
|
69
|
+
width: 10,
|
|
70
|
+
depth: 8,
|
|
71
|
+
pitch: 30,
|
|
72
|
+
wallHeight: 0,
|
|
73
|
+
})
|
|
74
|
+
const roof = RoofNode.parse({
|
|
75
|
+
id: 'roof_h',
|
|
76
|
+
parentId: roofLevel.id,
|
|
77
|
+
position: [5, 0, 4],
|
|
78
|
+
children: [segment.id],
|
|
79
|
+
})
|
|
80
|
+
const list = [building, level, roofLevel, ...walls, door, item, roof, segment] as AnyNode[]
|
|
81
|
+
const nodes = Object.fromEntries(list.map((node) => [node.id, { ...node }])) as Record<
|
|
82
|
+
string,
|
|
83
|
+
any
|
|
84
|
+
>
|
|
85
|
+
nodes.building_h.children = [level.id, roofLevel.id]
|
|
86
|
+
nodes.level_h.children = [...walls.map((wall) => wall.id), item.id]
|
|
87
|
+
nodes.level_roof.children = [roof.id]
|
|
88
|
+
nodes.wall_0.children = [door.id]
|
|
89
|
+
return nodes as Record<string, AnyNode>
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const group = (
|
|
93
|
+
type: 'start' | 'land' | 'settle',
|
|
94
|
+
phase: string,
|
|
95
|
+
ids: string[],
|
|
96
|
+
levelId: string | null = 'level_h',
|
|
97
|
+
) =>
|
|
98
|
+
({ type, phase, levelId, nodeIds: ids, source: 'agent', instant: false, atMs: 0 }) as RevealEvent
|
|
99
|
+
const started = (id: string, phase: string, style = 'drop') =>
|
|
100
|
+
({
|
|
101
|
+
type: 'node-start',
|
|
102
|
+
id,
|
|
103
|
+
phase,
|
|
104
|
+
levelId: 'level_h',
|
|
105
|
+
style,
|
|
106
|
+
source: 'agent',
|
|
107
|
+
atMs: 0,
|
|
108
|
+
}) as RevealEvent
|
|
109
|
+
|
|
110
|
+
// The roof declares that it lifts for the furnishing; the kinds are the nodes package's, so a
|
|
111
|
+
// fixture of the same kind stands in for them here.
|
|
112
|
+
let restoreRegistry: () => void = () => {}
|
|
113
|
+
beforeAll(async () => {
|
|
114
|
+
restoreRegistry = nodeRegistry._snapshot()
|
|
115
|
+
nodeRegistry._reset()
|
|
116
|
+
await loadPlugin({
|
|
117
|
+
id: 'fixture:follow',
|
|
118
|
+
apiVersion: 1,
|
|
119
|
+
nodes: [
|
|
120
|
+
{
|
|
121
|
+
kind: 'roof',
|
|
122
|
+
schemaVersion: 1,
|
|
123
|
+
schema: RoofNode,
|
|
124
|
+
category: 'structure',
|
|
125
|
+
defaults: () => ({}),
|
|
126
|
+
capabilities: {
|
|
127
|
+
reveal: {
|
|
128
|
+
phase: 'roof',
|
|
129
|
+
style: 'assemble',
|
|
130
|
+
height: 4,
|
|
131
|
+
clears: { for: 'furnishing', height: 2.4 },
|
|
132
|
+
},
|
|
133
|
+
exterior: true,
|
|
134
|
+
},
|
|
135
|
+
geometry: () => null,
|
|
136
|
+
} as unknown as AnyNodeDefinition,
|
|
137
|
+
{
|
|
138
|
+
kind: 'wall',
|
|
139
|
+
schemaVersion: 1,
|
|
140
|
+
schema: WallNode,
|
|
141
|
+
category: 'structure',
|
|
142
|
+
defaults: () => ({}),
|
|
143
|
+
capabilities: {
|
|
144
|
+
reveal: { phase: 'structure', style: 'rise' },
|
|
145
|
+
revealOnEdit: ['start', 'end', 'curveOffset'],
|
|
146
|
+
exterior: wallExterior,
|
|
147
|
+
},
|
|
148
|
+
geometry: () => null,
|
|
149
|
+
} as unknown as AnyNodeDefinition,
|
|
150
|
+
{
|
|
151
|
+
kind: 'door',
|
|
152
|
+
schemaVersion: 1,
|
|
153
|
+
schema: DoorNode,
|
|
154
|
+
category: 'structure',
|
|
155
|
+
defaults: () => ({}),
|
|
156
|
+
capabilities: {
|
|
157
|
+
reveal: { phase: 'openings', style: 'cut' },
|
|
158
|
+
revealOnEdit: ['position'],
|
|
159
|
+
exterior: openingExterior,
|
|
160
|
+
},
|
|
161
|
+
geometry: () => null,
|
|
162
|
+
} as unknown as AnyNodeDefinition,
|
|
163
|
+
],
|
|
164
|
+
})
|
|
165
|
+
})
|
|
166
|
+
afterAll(() => restoreRegistry())
|
|
167
|
+
|
|
168
|
+
describe('what the camera looks at', () => {
|
|
169
|
+
test('a floor, whole, when its slabs and walls start', () => {
|
|
170
|
+
for (const phase of ['foundation', 'structure', 'circulation']) {
|
|
171
|
+
const want = wantFromEvent(group('start', phase, ['wall_0']), house(), 1)!
|
|
172
|
+
expect(want.kind).toBe('floor')
|
|
173
|
+
expect(want.box.min[0]).toBeLessThanOrEqual(0)
|
|
174
|
+
expect(want.box.max[0]).toBeGreaterThanOrEqual(10)
|
|
175
|
+
expect(want.box.max[2]).toBeGreaterThanOrEqual(8)
|
|
176
|
+
expect(want.at).toBe(1)
|
|
177
|
+
}
|
|
178
|
+
})
|
|
179
|
+
|
|
180
|
+
test('a wall the agent edited is looked at, close up: it is not a floor being built', () => {
|
|
181
|
+
const nodes = house()
|
|
182
|
+
const edited = { ...started('wall_0', 'structure', 'rise'), changed: true } as RevealEvent
|
|
183
|
+
const want = wantFromEvent(edited, nodes, 0)
|
|
184
|
+
expect(want?.kind).toBe('detail')
|
|
185
|
+
expect(want?.levelId).toBe('level_h')
|
|
186
|
+
})
|
|
187
|
+
|
|
188
|
+
test('the walls themselves are not targets: the floor already is', () => {
|
|
189
|
+
expect(wantFromEvent(started('wall_1', 'structure', 'rise'), house(), 0)).toBeNull()
|
|
190
|
+
})
|
|
191
|
+
|
|
192
|
+
test('a window or a door close up, square to the wall it is in, from outside', () => {
|
|
193
|
+
const want = wantFromEvent(started('door_h', 'openings', 'cut'), house(), 2)!
|
|
194
|
+
expect(want.kind).toBe('detail')
|
|
195
|
+
expect(want.box.max[1] - want.box.min[1]).toBeLessThan(2.5)
|
|
196
|
+
expect(want.normal).toEqual([0, -1])
|
|
197
|
+
})
|
|
198
|
+
|
|
199
|
+
test('an item as furniture, from above', () => {
|
|
200
|
+
const want = wantFromEvent(started('item_h', 'furnishing'), house(), 3)!
|
|
201
|
+
expect(want.kind).toBe('furnish')
|
|
202
|
+
expect(want.normal).toBeUndefined()
|
|
203
|
+
})
|
|
204
|
+
|
|
205
|
+
test('furniture is seen from above the roof that lifts for it', () => {
|
|
206
|
+
const want = wantFromEvent(started('item_h', 'furnishing'), house(), 3)!
|
|
207
|
+
// The roof's top is above 2.7 m; it lifts 2.4 m for the furniture, and the eye clears that.
|
|
208
|
+
expect(want.eyeAbove).toBeGreaterThan(2.7 + 2.4)
|
|
209
|
+
// A house with no roof has nothing to clear.
|
|
210
|
+
const bare = house()
|
|
211
|
+
delete (bare as Record<string, unknown>).roof_h
|
|
212
|
+
delete (bare as Record<string, unknown>).rseg_h
|
|
213
|
+
expect(wantFromEvent(started('item_h', 'furnishing'), bare, 3)!.eyeAbove).toBeUndefined()
|
|
214
|
+
})
|
|
215
|
+
|
|
216
|
+
test('the whole house once the furniture has landed, before the roof comes back down', () => {
|
|
217
|
+
const want = wantFromEvent(group('settle', 'furnishing', ['item_h']), house(), 6)!
|
|
218
|
+
expect(want.kind).toBe('group')
|
|
219
|
+
expect(want.box.max[1]).toBeGreaterThan(3)
|
|
220
|
+
expect(want.box.max[0]).toBeGreaterThanOrEqual(10)
|
|
221
|
+
// Still looked down through the lifted roof, not from within it.
|
|
222
|
+
expect(want.eyeAbove).toBeGreaterThan(2.7 + 2.4)
|
|
223
|
+
// The walls settling say nothing about where to look.
|
|
224
|
+
expect(wantFromEvent(group('settle', 'structure', ['wall_0']), house(), 6)).toBeNull()
|
|
225
|
+
})
|
|
226
|
+
|
|
227
|
+
test('the whole house, for a person who stays on one level: that level whole, with no roof to clear and no floor of its own to change', () => {
|
|
228
|
+
const nodes = house()
|
|
229
|
+
const whole = wantFromEvent(group('settle', 'furnishing', ['item_h']), nodes, 6)!
|
|
230
|
+
expect(whole.levelId).toBeUndefined()
|
|
231
|
+
const stay = stayOnLevel(whole, nodes, 'level_h')
|
|
232
|
+
expect(stay.kind).toBe('floor')
|
|
233
|
+
expect(stay.levelId).toBe('level_h')
|
|
234
|
+
expect(stay.eyeAbove).toBeUndefined()
|
|
235
|
+
expect(stay.at).toBe(6)
|
|
236
|
+
// The level's own box, not the house's: the roof level over it is not in it.
|
|
237
|
+
expect(stay.box.max[1]).toBeLessThan(whole.box.max[1])
|
|
238
|
+
// A level with nothing to frame still stays where it is.
|
|
239
|
+
expect(stayOnLevel(whole, nodes, 'level_nowhere').levelId).toBe('level_nowhere')
|
|
240
|
+
})
|
|
241
|
+
|
|
242
|
+
test('a piece the agent moved is looked at like one it placed, and its settling does not pull back to the whole house', () => {
|
|
243
|
+
const moved = { ...started('item_h', 'furnishing', 'glide'), changed: true } as RevealEvent
|
|
244
|
+
const want = wantFromEvent(moved, house(), 3)!
|
|
245
|
+
expect(want.kind).toBe('furnish')
|
|
246
|
+
expect(want.eyeAbove).toBeGreaterThan(2.7 + 2.4)
|
|
247
|
+
// The build around it is not finishing: the camera stays where the piece is.
|
|
248
|
+
const settled = { ...group('settle', 'furnishing', ['item_h']), changed: true } as RevealEvent
|
|
249
|
+
expect(wantFromEvent(settled, house(), 6)).toBeNull()
|
|
250
|
+
})
|
|
251
|
+
|
|
252
|
+
test('the roof, from the group that assembles it', () => {
|
|
253
|
+
const want = wantFromEvent(
|
|
254
|
+
group('start', 'roof', ['roof_h', 'rseg_h'], 'level_roof'),
|
|
255
|
+
house(),
|
|
256
|
+
4,
|
|
257
|
+
)!
|
|
258
|
+
expect(want.kind).toBe('roof')
|
|
259
|
+
expect(want.box.max[1]).toBeGreaterThan(2.7)
|
|
260
|
+
})
|
|
261
|
+
|
|
262
|
+
test('where the building stands, not at the origin', () => {
|
|
263
|
+
const want = wantFromEvent(started('door_h', 'openings', 'cut'), house([20, 0, 30]), 0)!
|
|
264
|
+
expect(want.box.min[0]).toBeGreaterThan(20)
|
|
265
|
+
expect(want.box.min[2]).toBeGreaterThan(29)
|
|
266
|
+
})
|
|
267
|
+
|
|
268
|
+
test('the whole house, roof and all, when the build is finished', () => {
|
|
269
|
+
const want = wantFromEvent(
|
|
270
|
+
{
|
|
271
|
+
type: 'finale',
|
|
272
|
+
id: 'roof_h',
|
|
273
|
+
phase: 'roof',
|
|
274
|
+
levelId: 'level_roof',
|
|
275
|
+
source: 'agent',
|
|
276
|
+
atMs: 0,
|
|
277
|
+
} as RevealEvent,
|
|
278
|
+
house(),
|
|
279
|
+
5,
|
|
280
|
+
)!
|
|
281
|
+
expect(want.kind).toBe('group')
|
|
282
|
+
expect(want.box.max[1]).toBeGreaterThan(3)
|
|
283
|
+
expect(want.box.max[0]).toBeGreaterThanOrEqual(10)
|
|
284
|
+
})
|
|
285
|
+
|
|
286
|
+
test('an undo is followed: what goes first is looked at, group by group', () => {
|
|
287
|
+
const undone = (phase: string, nodeIds: string[], levelId: string | null = 'level_h') =>
|
|
288
|
+
({
|
|
289
|
+
type: 'retract-start',
|
|
290
|
+
phase,
|
|
291
|
+
levelId,
|
|
292
|
+
nodeIds,
|
|
293
|
+
source: null,
|
|
294
|
+
atMs: 0,
|
|
295
|
+
}) as RevealEvent
|
|
296
|
+
const furniture = wantFromEvent(undone('furnishing', ['item_h']), house(), 1)!
|
|
297
|
+
expect(furniture.kind).toBe('furnish')
|
|
298
|
+
// Seen from above the roof that is still up for it.
|
|
299
|
+
expect(furniture.eyeAbove).toBeGreaterThan(2.7 + 2.4)
|
|
300
|
+
const roof = wantFromEvent(undone('roof', ['roof_h', 'rseg_h'], 'level_roof'), house(), 2)!
|
|
301
|
+
expect(roof.kind).toBe('roof')
|
|
302
|
+
expect(roof.box.max[1]).toBeGreaterThan(2.7)
|
|
303
|
+
const openings = wantFromEvent(undone('openings', ['door_h']), house(), 3)!
|
|
304
|
+
expect(openings.kind).toBe('group')
|
|
305
|
+
expect(openings.box.max[1]).toBeLessThan(2.5)
|
|
306
|
+
// The walls going: the floor they stand on is what is looked at, by the walls themselves.
|
|
307
|
+
const walls = wantFromEvent(undone('structure', ['wall_0', 'wall_2']), house(), 4)!
|
|
308
|
+
expect(walls.kind).toBe('floor')
|
|
309
|
+
expect(walls.box.max[0]).toBeGreaterThanOrEqual(10)
|
|
310
|
+
// A group whose nodes are no longer in the scene says nothing about where to look.
|
|
311
|
+
expect(wantFromEvent(undone('structure', ['wall_gone']), house(), 5)).toBeNull()
|
|
312
|
+
// The end of a group says nothing.
|
|
313
|
+
expect(
|
|
314
|
+
wantFromEvent(
|
|
315
|
+
{ ...undone('structure', ['wall_0']), type: 'retract-end' } as RevealEvent,
|
|
316
|
+
house(),
|
|
317
|
+
6,
|
|
318
|
+
),
|
|
319
|
+
).toBeNull()
|
|
320
|
+
})
|
|
321
|
+
|
|
322
|
+
test('nothing to look at for what says nothing about the scene', () => {
|
|
323
|
+
expect(wantFromEvent(group('land', 'structure', ['wall_0']), house(), 0)).toBeNull()
|
|
324
|
+
expect(wantFromEvent(group('settle', 'structure', ['wall_0']), house(), 0)).toBeNull()
|
|
325
|
+
expect(
|
|
326
|
+
wantFromEvent(
|
|
327
|
+
{
|
|
328
|
+
type: 'complete',
|
|
329
|
+
nodeCount: 1,
|
|
330
|
+
durationMs: 1,
|
|
331
|
+
source: null,
|
|
332
|
+
instant: false,
|
|
333
|
+
atMs: 0,
|
|
334
|
+
} as RevealEvent,
|
|
335
|
+
house(),
|
|
336
|
+
0,
|
|
337
|
+
),
|
|
338
|
+
).toBeNull()
|
|
339
|
+
// A node that is gone is no target either.
|
|
340
|
+
expect(wantFromEvent(started('item_gone', 'furnishing'), house(), 0)).toBeNull()
|
|
341
|
+
})
|
|
342
|
+
})
|
|
343
|
+
|
|
344
|
+
// The owner, 15 October: the cutaway hid the exterior wall the agent was bending. A change seen from
|
|
345
|
+
// outside is looked at from outdoors, with the walls up; the rest keeps the cutaway.
|
|
346
|
+
describe('what is seen from outside', () => {
|
|
347
|
+
const changed = (id: string, phase: string, style = 'rise') =>
|
|
348
|
+
({ ...started(id, phase, style), changed: true }) as RevealEvent
|
|
349
|
+
|
|
350
|
+
test('an exterior wall the agent bent is an outside step, framed from its exterior face', () => {
|
|
351
|
+
const want = wantFromEvent(changed('wall_0', 'structure'), house(), 0)!
|
|
352
|
+
expect(want.exterior).toBe(true)
|
|
353
|
+
// wall_0 runs along +x with its exterior on the back side: it faces -z.
|
|
354
|
+
expect(want.normal).toEqual([0, -1])
|
|
355
|
+
})
|
|
356
|
+
|
|
357
|
+
test('a window moved on an exterior wall is an outside step; a chair moved is not', () => {
|
|
358
|
+
const nodes = house()
|
|
359
|
+
expect(wantFromEvent(changed('door_h', 'openings', 'cut'), nodes, 0)!.exterior).toBe(true)
|
|
360
|
+
expect(
|
|
361
|
+
wantFromEvent(changed('item_h', 'furnishing', 'drop'), nodes, 0)!.exterior,
|
|
362
|
+
).toBeUndefined()
|
|
363
|
+
})
|
|
364
|
+
|
|
365
|
+
test('an interior wall the agent bent keeps the cutaway', () => {
|
|
366
|
+
const nodes = house() as Record<string, any>
|
|
367
|
+
nodes.wall_1 = { ...nodes.wall_1, frontSide: 'interior', backSide: 'interior' }
|
|
368
|
+
const want = wantFromEvent(changed('wall_1', 'structure'), nodes, 0)!
|
|
369
|
+
expect(want.exterior).toBeUndefined()
|
|
370
|
+
expect(want.normal).toBeUndefined()
|
|
371
|
+
})
|
|
372
|
+
|
|
373
|
+
test('an opening on an interior wall keeps the cutaway', () => {
|
|
374
|
+
const nodes = house() as Record<string, any>
|
|
375
|
+
nodes.wall_0 = { ...nodes.wall_0, frontSide: 'interior', backSide: 'interior' }
|
|
376
|
+
expect(wantFromEvent(changed('door_h', 'openings', 'cut'), nodes, 0)!.exterior).toBeUndefined()
|
|
377
|
+
})
|
|
378
|
+
|
|
379
|
+
// The owner, 15 Oct: the outside view is for edits and moves only; a build is followed as it was.
|
|
380
|
+
test('a build says nothing of outside: a roof assembling, an opening coming in, a floor of walls', () => {
|
|
381
|
+
const nodes = house()
|
|
382
|
+
for (const event of [
|
|
383
|
+
group('start', 'roof', ['roof_h'], 'level_roof'),
|
|
384
|
+
group('start', 'openings', ['door_h']),
|
|
385
|
+
group('start', 'structure', ['wall_0', 'wall_1']),
|
|
386
|
+
started('door_h', 'openings', 'cut'),
|
|
387
|
+
started('item_h', 'furnishing'),
|
|
388
|
+
{ ...group('start', 'roof', ['roof_h'], 'level_roof'), type: 'retract-start' } as RevealEvent,
|
|
389
|
+
])
|
|
390
|
+
expect(wantFromEvent(event, nodes, 0)?.exterior).toBeUndefined()
|
|
391
|
+
})
|
|
392
|
+
|
|
393
|
+
test('the same pieces edited or moved are outside steps', () => {
|
|
394
|
+
const nodes = house()
|
|
395
|
+
const edited = (id: string, phase: string) =>
|
|
396
|
+
({ ...started(id, phase), changed: true }) as RevealEvent
|
|
397
|
+
expect(wantFromEvent(edited('door_h', 'openings'), nodes, 0)!.exterior).toBe(true)
|
|
398
|
+
expect(
|
|
399
|
+
wantFromEvent(
|
|
400
|
+
{ ...group('start', 'roof', ['roof_h'], 'level_roof'), changed: true } as RevealEvent,
|
|
401
|
+
nodes,
|
|
402
|
+
0,
|
|
403
|
+
)!.exterior,
|
|
404
|
+
).toBe(true)
|
|
405
|
+
})
|
|
406
|
+
})
|