@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,912 @@
|
|
|
1
|
+
import { afterEach, beforeEach, expect, test } from 'bun:test'
|
|
2
|
+
import {
|
|
3
|
+
type AnyNodeDefinition,
|
|
4
|
+
type AnyNodeId,
|
|
5
|
+
BuildingNode,
|
|
6
|
+
type CameraPose,
|
|
7
|
+
emitter,
|
|
8
|
+
ItemNode,
|
|
9
|
+
LevelNode,
|
|
10
|
+
loadPlugin,
|
|
11
|
+
nodeRegistry,
|
|
12
|
+
RoofNode,
|
|
13
|
+
runAsSceneCommitAuthor,
|
|
14
|
+
SlabNode,
|
|
15
|
+
sceneRegistry,
|
|
16
|
+
useScene,
|
|
17
|
+
WallNode,
|
|
18
|
+
WindowNode,
|
|
19
|
+
} from '@pascal-app/core'
|
|
20
|
+
import { isNodeRevealing, startConstructionReveal, useViewer } from '@pascal-app/viewer'
|
|
21
|
+
import { act, create } from '@react-three/test-renderer'
|
|
22
|
+
import { BoxGeometry, DoubleSide, Group, Mesh, MeshBasicMaterial, Raycaster, Vector3 } from 'three'
|
|
23
|
+
import { followBelowGrade } from '../../lib/below-grade'
|
|
24
|
+
import { openingExterior, wallExterior } from '../../lib/follow-pascal-exterior.test-fixture'
|
|
25
|
+
import useEditor from '../../store/use-editor'
|
|
26
|
+
import useFollowPascal from '../../store/use-follow-pascal'
|
|
27
|
+
import { usePointAsk } from '../../store/use-point-ask'
|
|
28
|
+
import { FollowPascal } from './follow-pascal'
|
|
29
|
+
|
|
30
|
+
;(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true
|
|
31
|
+
globalThis.requestAnimationFrame ??= () => 0
|
|
32
|
+
globalThis.cancelAnimationFrame ??= () => {}
|
|
33
|
+
|
|
34
|
+
let renderer: Awaited<ReturnType<typeof create>> | null = null
|
|
35
|
+
let driver: ReturnType<typeof startConstructionReveal> | null = null
|
|
36
|
+
let restoreRegistry = () => {}
|
|
37
|
+
const previousScene = useScene.getState()
|
|
38
|
+
const previousViewer = useViewer.getState()
|
|
39
|
+
const previousEditor = useEditor.getState()
|
|
40
|
+
const previousFollow = useFollowPascal.getState()
|
|
41
|
+
const previousAsk = usePointAsk.getState()
|
|
42
|
+
let shell: Mesh | null = null
|
|
43
|
+
let poses: CameraPose[] = []
|
|
44
|
+
const recordPose = (pose: CameraPose) => poses.push(pose)
|
|
45
|
+
|
|
46
|
+
beforeEach(async () => {
|
|
47
|
+
poses = []
|
|
48
|
+
emitter.on('camera-controls:apply-pose', recordPose)
|
|
49
|
+
restoreRegistry = nodeRegistry._snapshot()
|
|
50
|
+
nodeRegistry._reset()
|
|
51
|
+
await loadPlugin({
|
|
52
|
+
id: 'fixture:follow-camera',
|
|
53
|
+
apiVersion: 1,
|
|
54
|
+
nodes: [
|
|
55
|
+
{
|
|
56
|
+
kind: 'item',
|
|
57
|
+
schemaVersion: 1,
|
|
58
|
+
schema: ItemNode,
|
|
59
|
+
category: 'furnish',
|
|
60
|
+
defaults: () => ({}),
|
|
61
|
+
capabilities: { reveal: { phase: 'furnishing', style: 'drop', height: 0.3 } },
|
|
62
|
+
geometry: () => null,
|
|
63
|
+
} as unknown as AnyNodeDefinition,
|
|
64
|
+
{
|
|
65
|
+
kind: 'window',
|
|
66
|
+
schemaVersion: 1,
|
|
67
|
+
schema: WindowNode,
|
|
68
|
+
category: 'structure',
|
|
69
|
+
defaults: () => ({}),
|
|
70
|
+
capabilities: {
|
|
71
|
+
reveal: { phase: 'openings', style: 'cut' },
|
|
72
|
+
revealOnEdit: ['position'],
|
|
73
|
+
exterior: openingExterior,
|
|
74
|
+
},
|
|
75
|
+
geometry: () => null,
|
|
76
|
+
} as unknown as AnyNodeDefinition,
|
|
77
|
+
{
|
|
78
|
+
kind: 'roof',
|
|
79
|
+
schemaVersion: 1,
|
|
80
|
+
schema: RoofNode,
|
|
81
|
+
category: 'structure',
|
|
82
|
+
defaults: () => ({}),
|
|
83
|
+
capabilities: { reveal: { phase: 'roof', style: 'assemble', height: 4 }, exterior: true },
|
|
84
|
+
geometry: () => null,
|
|
85
|
+
} as unknown as AnyNodeDefinition,
|
|
86
|
+
{
|
|
87
|
+
kind: 'wall',
|
|
88
|
+
schemaVersion: 1,
|
|
89
|
+
schema: WallNode,
|
|
90
|
+
category: 'structure',
|
|
91
|
+
defaults: () => ({}),
|
|
92
|
+
capabilities: {
|
|
93
|
+
reveal: { phase: 'structure', style: 'rise' },
|
|
94
|
+
revealOnEdit: ['start', 'end', 'curveOffset'],
|
|
95
|
+
exterior: wallExterior,
|
|
96
|
+
},
|
|
97
|
+
geometry: () => null,
|
|
98
|
+
} as unknown as AnyNodeDefinition,
|
|
99
|
+
],
|
|
100
|
+
})
|
|
101
|
+
useFollowPascal.setState({ on: true, paused: false, building: false })
|
|
102
|
+
usePointAsk.setState({ busy: false })
|
|
103
|
+
useEditor.setState({ isFirstPersonMode: false })
|
|
104
|
+
useViewer.getState().setCameraMode('perspective')
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
afterEach(async () => {
|
|
108
|
+
emitter.off('camera-controls:apply-pose', recordPose)
|
|
109
|
+
driver?.stop()
|
|
110
|
+
driver = null
|
|
111
|
+
await renderer?.unmount()
|
|
112
|
+
renderer = null
|
|
113
|
+
restoreRegistry()
|
|
114
|
+
for (const id of [...sceneRegistry.nodes.keys()])
|
|
115
|
+
if (id.startsWith('item_') || ['window_east', 'wall_east', 'roof_east'].includes(id))
|
|
116
|
+
sceneRegistry.nodes.delete(id)
|
|
117
|
+
useScene.setState(previousScene)
|
|
118
|
+
useViewer.setState(previousViewer)
|
|
119
|
+
useEditor.setState(previousEditor)
|
|
120
|
+
useFollowPascal.setState(previousFollow)
|
|
121
|
+
usePointAsk.setState(previousAsk)
|
|
122
|
+
if (shell) {
|
|
123
|
+
shell.geometry.dispose()
|
|
124
|
+
;(shell.material as MeshBasicMaterial).dispose()
|
|
125
|
+
shell = null
|
|
126
|
+
}
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
/** The agent's turn is over and the reveal has played its last word. */
|
|
130
|
+
function turnOver() {
|
|
131
|
+
usePointAsk.getState().setBusy(false)
|
|
132
|
+
useFollowPascal.getState().endBuild()
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
type FollowSetup = {
|
|
136
|
+
/** Walls all round the target, so no eye sees it clear and Follow falls back to cutaway. */
|
|
137
|
+
enclosed?: boolean
|
|
138
|
+
hideAbove?: boolean
|
|
139
|
+
/** How long the reveal plays before the test looks (long enough to play the item in, by default). */
|
|
140
|
+
playMs?: number
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
async function followItem(
|
|
144
|
+
fromUpper = false,
|
|
145
|
+
mode: 'stacked' | 'solo' = 'stacked',
|
|
146
|
+
setup: FollowSetup = {},
|
|
147
|
+
) {
|
|
148
|
+
const building = BuildingNode.parse({ id: 'building_follow' })
|
|
149
|
+
const ground = LevelNode.parse({ id: 'level_ground', parentId: building.id, level: 0 })
|
|
150
|
+
const upper = LevelNode.parse({ id: 'level_upper', parentId: building.id, level: 1 })
|
|
151
|
+
const roof = LevelNode.parse({ id: 'level_roof', parentId: building.id, level: 2 })
|
|
152
|
+
building.children = [ground.id, upper.id, roof.id]
|
|
153
|
+
useScene
|
|
154
|
+
.getState()
|
|
155
|
+
.setScene(Object.fromEntries([building, ground, upper, roof].map((node) => [node.id, node])), [
|
|
156
|
+
building.id,
|
|
157
|
+
])
|
|
158
|
+
const target = fromUpper ? ground : upper
|
|
159
|
+
useViewer.getState().setSelection({
|
|
160
|
+
buildingId: building.id,
|
|
161
|
+
levelId: fromUpper ? upper.id : ground.id,
|
|
162
|
+
})
|
|
163
|
+
useViewer.setState({ levelMode: mode, hideLevelsAboveSelection: setup.hideAbove ?? true })
|
|
164
|
+
renderer = await create(
|
|
165
|
+
<>
|
|
166
|
+
<group name="scene-renderer" />
|
|
167
|
+
<FollowPascal />
|
|
168
|
+
</>,
|
|
169
|
+
)
|
|
170
|
+
if (setup.enclosed) {
|
|
171
|
+
shell = new Mesh(new BoxGeometry(6, 6, 6), new MeshBasicMaterial({ side: DoubleSide }))
|
|
172
|
+
shell.position.set(2, 0.5, 2)
|
|
173
|
+
renderer.scene.instance.getObjectByName('scene-renderer')!.add(shell)
|
|
174
|
+
shell.updateWorldMatrix(true, true)
|
|
175
|
+
}
|
|
176
|
+
driver = startConstructionReveal({ reveals: (commit) => commit.author === 'agent' })
|
|
177
|
+
await act(async () => {
|
|
178
|
+
runAsSceneCommitAuthor('agent', () =>
|
|
179
|
+
useScene.getState().createNode(
|
|
180
|
+
ItemNode.parse({
|
|
181
|
+
id: 'item_follow',
|
|
182
|
+
position: [2, 0, 2],
|
|
183
|
+
asset: {
|
|
184
|
+
id: 'chair',
|
|
185
|
+
category: 'furniture',
|
|
186
|
+
name: 'Chair',
|
|
187
|
+
thumbnail: '',
|
|
188
|
+
src: '/chair.glb',
|
|
189
|
+
dimensions: [1, 1, 1],
|
|
190
|
+
},
|
|
191
|
+
}),
|
|
192
|
+
target.id,
|
|
193
|
+
),
|
|
194
|
+
)
|
|
195
|
+
// The item's renderer has mounted its root, as it does once the reveal lets the item in.
|
|
196
|
+
sceneRegistry.nodes.set('item_follow', new Group())
|
|
197
|
+
// The reveal plays whatever floor the person has: its words come as each piece starts.
|
|
198
|
+
for (let at = 50; at <= (setup.playMs ?? 6000); at += 50) {
|
|
199
|
+
driver!.tick(at)
|
|
200
|
+
await renderer!.advanceFrames(1, 0.02)
|
|
201
|
+
}
|
|
202
|
+
await renderer!.advanceFrames(10, 0.1)
|
|
203
|
+
})
|
|
204
|
+
return { building, target }
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
test.each([
|
|
208
|
+
{ fromUpper: false, mode: 'stacked' as const },
|
|
209
|
+
{ fromUpper: true, mode: 'solo' as const },
|
|
210
|
+
])(
|
|
211
|
+
'following a build on another floor selects it from $mode (fromUpper: $fromUpper)',
|
|
212
|
+
async ({ fromUpper, mode }) => {
|
|
213
|
+
const { building, target } = await followItem(fromUpper, mode)
|
|
214
|
+
expect(useViewer.getState().selection).toMatchObject({
|
|
215
|
+
buildingId: building.id,
|
|
216
|
+
levelId: target.id,
|
|
217
|
+
})
|
|
218
|
+
expect(useViewer.getState().levelMode).toBe('stacked')
|
|
219
|
+
// The finished house is looked at whole: the floors above are shown, until Follow ends.
|
|
220
|
+
expect(useViewer.getState().hideLevelsAboveSelection).toBe(false)
|
|
221
|
+
// The build is over and no turn is running: the person has the floor they had.
|
|
222
|
+
useFollowPascal.getState().endBuild()
|
|
223
|
+
expect(useViewer.getState().selection).toMatchObject({
|
|
224
|
+
buildingId: building.id,
|
|
225
|
+
levelId: fromUpper ? 'level_upper' : 'level_ground',
|
|
226
|
+
})
|
|
227
|
+
},
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
// The owner, 15 October: a person on the ground floor with the floors above hidden did not see the
|
|
231
|
+
// animation of a build on the floor above. With Follow off the build still plays on show, and the
|
|
232
|
+
// person's own floor is not touched.
|
|
233
|
+
test('with Follow off, a build on the floor above plays on show and the person keeps their floor', async () => {
|
|
234
|
+
useFollowPascal.setState({ on: false })
|
|
235
|
+
const { target } = await followItem(false, 'stacked', { playMs: 200 })
|
|
236
|
+
expect(useViewer.getState().selection.levelId).toBe('level_ground')
|
|
237
|
+
expect(useViewer.getState().hideLevelsAboveSelection).toBe(true)
|
|
238
|
+
expect(useViewer.getState().levelsRevealing).toEqual([target.id])
|
|
239
|
+
expect(poses.length).toBe(0)
|
|
240
|
+
await act(async () => {
|
|
241
|
+
for (let at = 250; at <= 9000; at += 50) driver!.tick(at)
|
|
242
|
+
})
|
|
243
|
+
expect(useViewer.getState().levelsRevealing).toEqual([])
|
|
244
|
+
expect(useViewer.getState().selection.levelId).toBe('level_ground')
|
|
245
|
+
})
|
|
246
|
+
|
|
247
|
+
// The build is finished and the camera pulls back to the whole house: the floors above the one it
|
|
248
|
+
// worked on are not hidden then, or the finished house would be shown without its upper floors.
|
|
249
|
+
test('the pull-back to the finished house shows the floors above, and the person gets their display back', async () => {
|
|
250
|
+
usePointAsk.getState().setBusy(true)
|
|
251
|
+
await followItem(false, 'stacked')
|
|
252
|
+
expect(useViewer.getState().selection.levelId).toBe('level_upper')
|
|
253
|
+
expect(useViewer.getState().hideLevelsAboveSelection).toBe(false)
|
|
254
|
+
|
|
255
|
+
turnOver()
|
|
256
|
+
expect(useViewer.getState().selection.levelId).toBe('level_ground')
|
|
257
|
+
expect(useViewer.getState().hideLevelsAboveSelection).toBe(true)
|
|
258
|
+
})
|
|
259
|
+
|
|
260
|
+
test('an obstacle appearing after arrival causes one calm flight to a clear view', async () => {
|
|
261
|
+
await followItem()
|
|
262
|
+
await renderer!.advanceFrames(120, 0.05)
|
|
263
|
+
const before = poses.at(-1)!
|
|
264
|
+
const obstacle = new Mesh(new BoxGeometry(8, 5, 0.5), new MeshBasicMaterial({ side: DoubleSide }))
|
|
265
|
+
obstacle.position.set(before.target[0], before.target[1] + 2.5, before.target[2] + 2)
|
|
266
|
+
renderer!.scene.instance.getObjectByName('scene-renderer')!.add(obstacle)
|
|
267
|
+
obstacle.updateWorldMatrix(true, true)
|
|
268
|
+
const ray = new Raycaster()
|
|
269
|
+
const sees = (pose: CameraPose) => {
|
|
270
|
+
const eye = new Vector3(...pose.position)
|
|
271
|
+
const direction = new Vector3(...pose.target).sub(eye)
|
|
272
|
+
ray.set(eye, direction.clone().normalize())
|
|
273
|
+
ray.far = direction.length() - 0.02
|
|
274
|
+
return ray.intersectObject(obstacle).length === 0
|
|
275
|
+
}
|
|
276
|
+
expect(sees(before)).toBe(false)
|
|
277
|
+
await renderer!.advanceFrames(160, 0.05)
|
|
278
|
+
const after = poses.at(-1)!
|
|
279
|
+
expect(sees(after)).toBe(true)
|
|
280
|
+
expect(after.target[0]).toBeCloseTo(before.target[0], 2)
|
|
281
|
+
expect(after.target[2]).toBeCloseTo(before.target[2], 2)
|
|
282
|
+
const count = poses.length
|
|
283
|
+
await renderer!.advanceFrames(40, 0.05)
|
|
284
|
+
expect(poses.length).toBe(count)
|
|
285
|
+
obstacle.geometry.dispose()
|
|
286
|
+
;(obstacle.material as MeshBasicMaterial).dispose()
|
|
287
|
+
})
|
|
288
|
+
|
|
289
|
+
test.each(['camera-controls:interaction-start', 'camera-controls:orbit-cw'] as const)(
|
|
290
|
+
'$0 yields the camera, puts the floor back and does not select it again',
|
|
291
|
+
async (event) => {
|
|
292
|
+
await followItem()
|
|
293
|
+
expect(poses.length).toBeGreaterThan(0)
|
|
294
|
+
expect(useViewer.getState().selection.levelId).toBe('level_upper')
|
|
295
|
+
emitter.emit(event)
|
|
296
|
+
const count = poses.length
|
|
297
|
+
expect(useFollowPascal.getState().paused).toBe(true)
|
|
298
|
+
expect(useViewer.getState().selection.levelId).toBe('level_ground')
|
|
299
|
+
await act(async () => {
|
|
300
|
+
runAsSceneCommitAuthor('agent', () =>
|
|
301
|
+
useScene
|
|
302
|
+
.getState()
|
|
303
|
+
.createNode(
|
|
304
|
+
ItemNode.parse({ ...useScene.getState().nodes.item_follow, id: 'item_next' }),
|
|
305
|
+
'level_roof',
|
|
306
|
+
),
|
|
307
|
+
)
|
|
308
|
+
driver!.tick(40)
|
|
309
|
+
await renderer!.advanceFrames(100, 0.05)
|
|
310
|
+
})
|
|
311
|
+
expect(useFollowPascal.getState().paused).toBe(true)
|
|
312
|
+
expect(poses.length).toBe(count)
|
|
313
|
+
expect(useViewer.getState().selection.levelId).toBe('level_ground')
|
|
314
|
+
},
|
|
315
|
+
)
|
|
316
|
+
|
|
317
|
+
// The owner, 12 October, on 3114: after Follow Pascal stopped the walls looked invisible. It had
|
|
318
|
+
// fallen back to cutaway when no eye saw its target, selected the floor being built and hidden the
|
|
319
|
+
// floors above it, and nothing put them back.
|
|
320
|
+
const ENDS: { name: string; end: () => unknown }[] = [
|
|
321
|
+
{ name: 'it is switched off', end: () => useFollowPascal.getState().setOn(false) },
|
|
322
|
+
{
|
|
323
|
+
name: 'the person takes the camera',
|
|
324
|
+
end: () => emitter.emit('camera-controls:interaction-start'),
|
|
325
|
+
},
|
|
326
|
+
{
|
|
327
|
+
// The turn ended, and the reveal's last words are over (what it still plays is still followed).
|
|
328
|
+
name: 'the turn is over and the build is over',
|
|
329
|
+
end: turnOver,
|
|
330
|
+
},
|
|
331
|
+
]
|
|
332
|
+
|
|
333
|
+
// The person is on the upper floor in the solo display, with the floors above not hidden; the build
|
|
334
|
+
// is on the ground floor, which the solo display hides: it plays on show, and Follow takes the floor.
|
|
335
|
+
test.each(ENDS)(
|
|
336
|
+
'when $name, the cutaway, the floor and the hidden floors are put back',
|
|
337
|
+
async ({ end }) => {
|
|
338
|
+
usePointAsk.getState().setBusy(true)
|
|
339
|
+
const { building } = await followItem(true, 'solo', {
|
|
340
|
+
enclosed: true,
|
|
341
|
+
hideAbove: false,
|
|
342
|
+
playMs: 1500,
|
|
343
|
+
})
|
|
344
|
+
expect(useViewer.getState().wallMode).toBe('cutaway')
|
|
345
|
+
expect(useViewer.getState().levelMode).toBe('stacked')
|
|
346
|
+
expect(useViewer.getState().hideLevelsAboveSelection).toBe(true)
|
|
347
|
+
expect(useViewer.getState().selection.levelId).toBe('level_ground')
|
|
348
|
+
|
|
349
|
+
await act(async () => end())
|
|
350
|
+
|
|
351
|
+
expect(useViewer.getState().wallMode).toBe('up')
|
|
352
|
+
expect(useViewer.getState().levelMode).toBe('solo')
|
|
353
|
+
expect(useViewer.getState().hideLevelsAboveSelection).toBe(false)
|
|
354
|
+
expect(useViewer.getState().selection).toMatchObject({
|
|
355
|
+
buildingId: building.id,
|
|
356
|
+
levelId: 'level_upper',
|
|
357
|
+
})
|
|
358
|
+
},
|
|
359
|
+
)
|
|
360
|
+
|
|
361
|
+
test('leaving the editor puts them back', async () => {
|
|
362
|
+
await followItem(false, 'stacked', { enclosed: true })
|
|
363
|
+
expect(useViewer.getState().wallMode).toBe('cutaway')
|
|
364
|
+
await renderer!.unmount()
|
|
365
|
+
renderer = null
|
|
366
|
+
expect(useViewer.getState().wallMode).toBe('up')
|
|
367
|
+
expect(useViewer.getState().selection.levelId).toBe('level_ground')
|
|
368
|
+
})
|
|
369
|
+
|
|
370
|
+
test('a wall mode the person picks while Follow is on is theirs, and Follow does not take it back', async () => {
|
|
371
|
+
usePointAsk.getState().setBusy(true)
|
|
372
|
+
await followItem(false, 'stacked', { enclosed: true })
|
|
373
|
+
expect(useViewer.getState().wallMode).toBe('cutaway')
|
|
374
|
+
useViewer.getState().setWallMode('translucent')
|
|
375
|
+
await act(async () => {
|
|
376
|
+
await renderer!.advanceFrames(40, 0.05)
|
|
377
|
+
})
|
|
378
|
+
expect(useViewer.getState().wallMode).toBe('translucent')
|
|
379
|
+
turnOver()
|
|
380
|
+
expect(useViewer.getState().wallMode).toBe('translucent')
|
|
381
|
+
// The floor was Follow's: it goes back all the same.
|
|
382
|
+
expect(useViewer.getState().selection.levelId).toBe('level_ground')
|
|
383
|
+
})
|
|
384
|
+
|
|
385
|
+
test('a floor the person picks while Follow is on is theirs, and Follow does not select over it', async () => {
|
|
386
|
+
usePointAsk.getState().setBusy(true)
|
|
387
|
+
await followItem()
|
|
388
|
+
expect(useViewer.getState().selection.levelId).toBe('level_upper')
|
|
389
|
+
const count = poses.length
|
|
390
|
+
useViewer.getState().setSelection({ levelId: 'level_ground' })
|
|
391
|
+
// The next piece goes on the roof level: above the person's floor, so the reveal speaks at once.
|
|
392
|
+
await act(async () => {
|
|
393
|
+
runAsSceneCommitAuthor('agent', () =>
|
|
394
|
+
useScene
|
|
395
|
+
.getState()
|
|
396
|
+
.createNode(
|
|
397
|
+
ItemNode.parse({ ...useScene.getState().nodes.item_follow, id: 'item_roof' }),
|
|
398
|
+
'level_roof',
|
|
399
|
+
),
|
|
400
|
+
)
|
|
401
|
+
driver!.tick(40)
|
|
402
|
+
await renderer!.advanceFrames(100, 0.05)
|
|
403
|
+
})
|
|
404
|
+
// Follow went on to the new piece, and the floor stayed the person's.
|
|
405
|
+
expect(poses.length).toBeGreaterThan(count)
|
|
406
|
+
expect(useViewer.getState().selection.levelId).toBe('level_ground')
|
|
407
|
+
turnOver()
|
|
408
|
+
expect(useViewer.getState().selection.levelId).toBe('level_ground')
|
|
409
|
+
})
|
|
410
|
+
|
|
411
|
+
test('what Follow did not change is left as it was when it ends', async () => {
|
|
412
|
+
useViewer.getState().setWallMode('cutaway')
|
|
413
|
+
usePointAsk.getState().setBusy(true)
|
|
414
|
+
const { building } = await followItem(false, 'stacked', { enclosed: true })
|
|
415
|
+
expect(useViewer.getState().selection.levelId).toBe('level_upper')
|
|
416
|
+
turnOver()
|
|
417
|
+
expect(useViewer.getState().wallMode).toBe('cutaway')
|
|
418
|
+
expect(useViewer.getState().hideLevelsAboveSelection).toBe(true)
|
|
419
|
+
expect(useViewer.getState().selection).toMatchObject({
|
|
420
|
+
buildingId: building.id,
|
|
421
|
+
levelId: 'level_ground',
|
|
422
|
+
})
|
|
423
|
+
})
|
|
424
|
+
|
|
425
|
+
// A turn runs several programs, with the agent thinking between them: the build is over for a few
|
|
426
|
+
// seconds and then goes on. The view holds steady until the turn is over.
|
|
427
|
+
test('a build that is over keeps the view while the turn goes on', async () => {
|
|
428
|
+
usePointAsk.getState().setBusy(true)
|
|
429
|
+
await followItem(false, 'stacked', { enclosed: true })
|
|
430
|
+
useFollowPascal.getState().endBuild()
|
|
431
|
+
expect(useViewer.getState().wallMode).toBe('cutaway')
|
|
432
|
+
expect(useViewer.getState().selection.levelId).toBe('level_upper')
|
|
433
|
+
usePointAsk.getState().setBusy(false)
|
|
434
|
+
expect(useViewer.getState().wallMode).toBe('up')
|
|
435
|
+
expect(useViewer.getState().selection.levelId).toBe('level_ground')
|
|
436
|
+
})
|
|
437
|
+
|
|
438
|
+
// The reveal can play on after the agent's turn: it is still followed, and ends with the build.
|
|
439
|
+
test('a turn that ends while the reveal is still playing ends it with the reveal', async () => {
|
|
440
|
+
usePointAsk.getState().setBusy(true)
|
|
441
|
+
await followItem(false, 'stacked', { enclosed: true })
|
|
442
|
+
usePointAsk.getState().setBusy(false)
|
|
443
|
+
expect(useFollowPascal.getState().building).toBe(true)
|
|
444
|
+
expect(useViewer.getState().wallMode).toBe('cutaway')
|
|
445
|
+
useFollowPascal.getState().endBuild()
|
|
446
|
+
expect(useViewer.getState().wallMode).toBe('up')
|
|
447
|
+
expect(useViewer.getState().selection.levelId).toBe('level_ground')
|
|
448
|
+
})
|
|
449
|
+
|
|
450
|
+
// A host that does not say when a turn runs (a standalone editor, a build driven over MCP).
|
|
451
|
+
test('without a host that says a turn is running, the build being over ends it', async () => {
|
|
452
|
+
await followItem(false, 'stacked', { enclosed: true })
|
|
453
|
+
expect(useViewer.getState().wallMode).toBe('cutaway')
|
|
454
|
+
useFollowPascal.getState().endBuild()
|
|
455
|
+
expect(useViewer.getState().wallMode).toBe('up')
|
|
456
|
+
expect(useViewer.getState().selection.levelId).toBe('level_ground')
|
|
457
|
+
})
|
|
458
|
+
|
|
459
|
+
// The owner, 15 October: the agent bent an exterior wall and moved a window, and the cutaway hid
|
|
460
|
+
// exactly what moved. A change seen from outside is looked at from outdoors, with the walls up; an
|
|
461
|
+
// interior one keeps the cutaway; a commit with one of each takes the outside view. What the person
|
|
462
|
+
// had is theirs after.
|
|
463
|
+
async function followMoves(setup: {
|
|
464
|
+
moveWindow?: boolean
|
|
465
|
+
moveSofa?: boolean
|
|
466
|
+
bendWall?: boolean
|
|
467
|
+
personWall: 'up' | 'cutaway'
|
|
468
|
+
}) {
|
|
469
|
+
const building = BuildingNode.parse({ id: 'building_follow' })
|
|
470
|
+
const level = LevelNode.parse({ id: 'level_ground', parentId: building.id, level: 0 })
|
|
471
|
+
const wall = WallNode.parse({
|
|
472
|
+
id: 'wall_east',
|
|
473
|
+
parentId: level.id,
|
|
474
|
+
start: [4, 0],
|
|
475
|
+
end: [4, 4],
|
|
476
|
+
frontSide: 'interior',
|
|
477
|
+
backSide: 'exterior',
|
|
478
|
+
})
|
|
479
|
+
const window = WindowNode.parse({
|
|
480
|
+
id: 'window_east',
|
|
481
|
+
parentId: wall.id,
|
|
482
|
+
wallId: wall.id,
|
|
483
|
+
position: [1, 1.2, 0],
|
|
484
|
+
})
|
|
485
|
+
const sofa = ItemNode.parse({
|
|
486
|
+
id: 'item_sofa',
|
|
487
|
+
parentId: level.id,
|
|
488
|
+
position: [1, 0, 1],
|
|
489
|
+
asset: {
|
|
490
|
+
id: 'sofa',
|
|
491
|
+
category: 'furniture',
|
|
492
|
+
name: 'Sofa',
|
|
493
|
+
thumbnail: '',
|
|
494
|
+
src: '/sofa.glb',
|
|
495
|
+
dimensions: [2, 1, 1],
|
|
496
|
+
},
|
|
497
|
+
})
|
|
498
|
+
building.children = [level.id]
|
|
499
|
+
level.children = [wall.id, sofa.id]
|
|
500
|
+
wall.children = [window.id]
|
|
501
|
+
useScene
|
|
502
|
+
.getState()
|
|
503
|
+
.setScene(
|
|
504
|
+
Object.fromEntries([building, level, wall, window, sofa].map((node) => [node.id, node])),
|
|
505
|
+
[building.id],
|
|
506
|
+
)
|
|
507
|
+
useViewer.getState().setSelection({ buildingId: building.id, levelId: level.id })
|
|
508
|
+
useViewer.setState({ levelMode: 'stacked', hideLevelsAboveSelection: false })
|
|
509
|
+
useViewer.getState().setWallMode(setup.personWall)
|
|
510
|
+
renderer = await create(
|
|
511
|
+
<>
|
|
512
|
+
<group name="scene-renderer" />
|
|
513
|
+
<FollowPascal />
|
|
514
|
+
</>,
|
|
515
|
+
)
|
|
516
|
+
// Walls all round: no eye sees the target, so an interior step falls back to the cutaway.
|
|
517
|
+
shell = new Mesh(new BoxGeometry(6, 6, 6), new MeshBasicMaterial({ side: DoubleSide }))
|
|
518
|
+
shell.position.set(2, 0.5, 2)
|
|
519
|
+
renderer.scene.instance.getObjectByName('scene-renderer')!.add(shell)
|
|
520
|
+
shell.updateWorldMatrix(true, true)
|
|
521
|
+
for (const id of ['window_east', 'item_sofa', 'wall_east'])
|
|
522
|
+
sceneRegistry.nodes.set(id, new Group())
|
|
523
|
+
driver = startConstructionReveal({ reveals: (commit) => commit.author === 'agent' })
|
|
524
|
+
usePointAsk.getState().setBusy(true)
|
|
525
|
+
await act(async () => {
|
|
526
|
+
runAsSceneCommitAuthor('agent', () => {
|
|
527
|
+
if (setup.moveWindow)
|
|
528
|
+
useScene.getState().updateNode('window_east', { position: [2.5, 1.2, 0] } as never)
|
|
529
|
+
if (setup.moveSofa)
|
|
530
|
+
useScene.getState().updateNode('item_sofa', { position: [2.5, 0, 2.5] } as never)
|
|
531
|
+
if (setup.bendWall) useScene.getState().updateNode('wall_east', { curveOffset: 0.6 } as never)
|
|
532
|
+
})
|
|
533
|
+
for (let at = 50; at <= 4000; at += 50) {
|
|
534
|
+
driver!.tick(at)
|
|
535
|
+
await renderer!.advanceFrames(1, 0.02)
|
|
536
|
+
}
|
|
537
|
+
await renderer!.advanceFrames(10, 0.1)
|
|
538
|
+
})
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
test('a window moved on an exterior wall is looked at from outside, and the person keeps their cutaway', async () => {
|
|
542
|
+
await followMoves({ moveWindow: true, personWall: 'cutaway' })
|
|
543
|
+
expect(poses.length).toBeGreaterThan(0)
|
|
544
|
+
// The cutaway the person had is off while the change plays: it would hide the very wall.
|
|
545
|
+
expect(useViewer.getState().wallMode).toBe('up')
|
|
546
|
+
turnOver()
|
|
547
|
+
expect(useViewer.getState().wallMode).toBe('cutaway')
|
|
548
|
+
})
|
|
549
|
+
|
|
550
|
+
test('an exterior wall bent is looked at from outside too', async () => {
|
|
551
|
+
await followMoves({ bendWall: true, personWall: 'cutaway' })
|
|
552
|
+
expect(useViewer.getState().wallMode).toBe('up')
|
|
553
|
+
turnOver()
|
|
554
|
+
expect(useViewer.getState().wallMode).toBe('cutaway')
|
|
555
|
+
})
|
|
556
|
+
|
|
557
|
+
test('a sofa moved inside is looked at through the cutaway, and the person gets their walls back', async () => {
|
|
558
|
+
await followMoves({ moveSofa: true, personWall: 'up' })
|
|
559
|
+
expect(poses.length).toBeGreaterThan(0)
|
|
560
|
+
expect(useViewer.getState().wallMode).toBe('cutaway')
|
|
561
|
+
turnOver()
|
|
562
|
+
expect(useViewer.getState().wallMode).toBe('up')
|
|
563
|
+
})
|
|
564
|
+
|
|
565
|
+
test('a commit with a window and a sofa in it takes the outside view', async () => {
|
|
566
|
+
await followMoves({ moveWindow: true, moveSofa: true, personWall: 'cutaway' })
|
|
567
|
+
expect(useViewer.getState().wallMode).toBe('up')
|
|
568
|
+
turnOver()
|
|
569
|
+
expect(useViewer.getState().wallMode).toBe('cutaway')
|
|
570
|
+
})
|
|
571
|
+
|
|
572
|
+
// The owner, 15 October: the outside view is for edits and moves only. A build is followed as it
|
|
573
|
+
// always was: the cutaway once, when no eye sees the target, and never the walls up in between.
|
|
574
|
+
test.each([
|
|
575
|
+
{ person: 'up' as const, written: ['cutaway'] as string[] },
|
|
576
|
+
// The person already cuts the walls away: a build never puts them up in between.
|
|
577
|
+
{ person: 'cutaway' as const, written: [] as string[] },
|
|
578
|
+
])(
|
|
579
|
+
'a full build with Follow on (structure, openings, furnishing, roof) keeps the single cutaway, from $person',
|
|
580
|
+
async ({ person, written: expected }) => {
|
|
581
|
+
const building = BuildingNode.parse({ id: 'building_follow' })
|
|
582
|
+
const level = LevelNode.parse({ id: 'level_ground', parentId: building.id, level: 0 })
|
|
583
|
+
building.children = [level.id]
|
|
584
|
+
useScene
|
|
585
|
+
.getState()
|
|
586
|
+
.setScene(Object.fromEntries([building, level].map((node) => [node.id, node])), [building.id])
|
|
587
|
+
useViewer.getState().setSelection({ buildingId: building.id, levelId: level.id })
|
|
588
|
+
useViewer.setState({ levelMode: 'stacked', hideLevelsAboveSelection: false })
|
|
589
|
+
useViewer.getState().setWallMode(person)
|
|
590
|
+
renderer = await create(
|
|
591
|
+
<>
|
|
592
|
+
<group name="scene-renderer" />
|
|
593
|
+
<FollowPascal />
|
|
594
|
+
</>,
|
|
595
|
+
)
|
|
596
|
+
shell = new Mesh(new BoxGeometry(6, 6, 6), new MeshBasicMaterial({ side: DoubleSide }))
|
|
597
|
+
shell.position.set(2, 0.5, 2)
|
|
598
|
+
renderer.scene.instance.getObjectByName('scene-renderer')!.add(shell)
|
|
599
|
+
shell.updateWorldMatrix(true, true)
|
|
600
|
+
const written: string[] = []
|
|
601
|
+
const stop = useViewer.subscribe((state, before) => {
|
|
602
|
+
if (state.wallMode !== before.wallMode) written.push(state.wallMode)
|
|
603
|
+
})
|
|
604
|
+
driver = startConstructionReveal({ reveals: (commit) => commit.author === 'agent' })
|
|
605
|
+
usePointAsk.getState().setBusy(true)
|
|
606
|
+
const wall = WallNode.parse({
|
|
607
|
+
id: 'wall_east',
|
|
608
|
+
parentId: level.id,
|
|
609
|
+
start: [4, 0],
|
|
610
|
+
end: [4, 4],
|
|
611
|
+
frontSide: 'interior',
|
|
612
|
+
backSide: 'exterior',
|
|
613
|
+
})
|
|
614
|
+
const window = WindowNode.parse({
|
|
615
|
+
id: 'window_east',
|
|
616
|
+
parentId: wall.id,
|
|
617
|
+
wallId: wall.id,
|
|
618
|
+
position: [1, 1.2, 0],
|
|
619
|
+
})
|
|
620
|
+
const sofa = ItemNode.parse({
|
|
621
|
+
id: 'item_sofa',
|
|
622
|
+
parentId: level.id,
|
|
623
|
+
position: [1, 0, 1],
|
|
624
|
+
asset: {
|
|
625
|
+
id: 'sofa',
|
|
626
|
+
category: 'furniture',
|
|
627
|
+
name: 'Sofa',
|
|
628
|
+
thumbnail: '',
|
|
629
|
+
src: '/sofa.glb',
|
|
630
|
+
dimensions: [2, 1, 1],
|
|
631
|
+
},
|
|
632
|
+
})
|
|
633
|
+
const roof = RoofNode.parse({ id: 'roof_east', parentId: level.id })
|
|
634
|
+
await act(async () => {
|
|
635
|
+
runAsSceneCommitAuthor('agent', () => {
|
|
636
|
+
useScene.getState().createNode(wall, level.id)
|
|
637
|
+
useScene.getState().createNode(window, wall.id)
|
|
638
|
+
useScene.getState().createNode(sofa, level.id)
|
|
639
|
+
useScene.getState().createNode(roof, level.id)
|
|
640
|
+
})
|
|
641
|
+
for (const id of ['wall_east', 'window_east', 'item_sofa', 'roof_east'])
|
|
642
|
+
sceneRegistry.nodes.set(id, new Group())
|
|
643
|
+
for (let at = 50; at <= 20000; at += 50) {
|
|
644
|
+
driver!.tick(at)
|
|
645
|
+
await renderer!.advanceFrames(1, 0.02)
|
|
646
|
+
}
|
|
647
|
+
await renderer!.advanceFrames(10, 0.1)
|
|
648
|
+
})
|
|
649
|
+
stop()
|
|
650
|
+
// The build was followed (the camera moved) and the walls were cut away once, never put up between.
|
|
651
|
+
expect(poses.length).toBeGreaterThan(0)
|
|
652
|
+
expect(written).toEqual(expected)
|
|
653
|
+
expect(useViewer.getState().wallMode).toBe('cutaway')
|
|
654
|
+
turnOver()
|
|
655
|
+
expect(useViewer.getState().wallMode).toBe(person)
|
|
656
|
+
},
|
|
657
|
+
)
|
|
658
|
+
|
|
659
|
+
// A commit's outside view is its own: an exterior edit another commit queued behind a build, or an
|
|
660
|
+
// exterior commit before an interior one, does not take the build's walls up between its steps.
|
|
661
|
+
async function followBuildAmid(setup: {
|
|
662
|
+
first: 'build' | 'exterior'
|
|
663
|
+
next: 'exterior' | 'build'
|
|
664
|
+
/** When the second commit lands, on the reveal clock; with the first when absent. */
|
|
665
|
+
nextAtMs?: number
|
|
666
|
+
}) {
|
|
667
|
+
const building = BuildingNode.parse({ id: 'building_follow' })
|
|
668
|
+
const level = LevelNode.parse({ id: 'level_ground', parentId: building.id, level: 0 })
|
|
669
|
+
const wall = WallNode.parse({
|
|
670
|
+
id: 'wall_east',
|
|
671
|
+
parentId: level.id,
|
|
672
|
+
start: [4, 0],
|
|
673
|
+
end: [4, 4],
|
|
674
|
+
frontSide: 'interior',
|
|
675
|
+
backSide: 'exterior',
|
|
676
|
+
})
|
|
677
|
+
const window = WindowNode.parse({
|
|
678
|
+
id: 'window_east',
|
|
679
|
+
parentId: wall.id,
|
|
680
|
+
wallId: wall.id,
|
|
681
|
+
position: [1, 1.2, 0],
|
|
682
|
+
})
|
|
683
|
+
const resting = ItemNode.parse({
|
|
684
|
+
id: 'item_sofa_resting',
|
|
685
|
+
parentId: level.id,
|
|
686
|
+
position: [3, 0, 3],
|
|
687
|
+
asset: {
|
|
688
|
+
id: 'sofa',
|
|
689
|
+
category: 'furniture',
|
|
690
|
+
name: 'Sofa',
|
|
691
|
+
thumbnail: '',
|
|
692
|
+
src: '/sofa.glb',
|
|
693
|
+
dimensions: [2, 1, 1],
|
|
694
|
+
},
|
|
695
|
+
})
|
|
696
|
+
building.children = [level.id]
|
|
697
|
+
level.children = [wall.id, resting.id]
|
|
698
|
+
wall.children = [window.id]
|
|
699
|
+
useScene
|
|
700
|
+
.getState()
|
|
701
|
+
.setScene(
|
|
702
|
+
Object.fromEntries([building, level, wall, window, resting].map((node) => [node.id, node])),
|
|
703
|
+
[building.id],
|
|
704
|
+
)
|
|
705
|
+
useViewer.getState().setSelection({ buildingId: building.id, levelId: level.id })
|
|
706
|
+
useViewer.setState({ levelMode: 'stacked', hideLevelsAboveSelection: false })
|
|
707
|
+
useViewer.getState().setWallMode('cutaway')
|
|
708
|
+
renderer = await create(
|
|
709
|
+
<>
|
|
710
|
+
<group name="scene-renderer" />
|
|
711
|
+
<FollowPascal />
|
|
712
|
+
</>,
|
|
713
|
+
)
|
|
714
|
+
shell = new Mesh(new BoxGeometry(6, 6, 6), new MeshBasicMaterial({ side: DoubleSide }))
|
|
715
|
+
shell.position.set(2, 0.5, 2)
|
|
716
|
+
renderer.scene.instance.getObjectByName('scene-renderer')!.add(shell)
|
|
717
|
+
shell.updateWorldMatrix(true, true)
|
|
718
|
+
for (const id of ['window_east', 'wall_east', resting.id])
|
|
719
|
+
sceneRegistry.nodes.set(id, new Group())
|
|
720
|
+
const sofa = ItemNode.parse({
|
|
721
|
+
id: 'item_sofa',
|
|
722
|
+
parentId: level.id,
|
|
723
|
+
position: [1, 0, 1],
|
|
724
|
+
asset: {
|
|
725
|
+
id: 'sofa',
|
|
726
|
+
category: 'furniture',
|
|
727
|
+
name: 'Sofa',
|
|
728
|
+
thumbnail: '',
|
|
729
|
+
src: '/sofa.glb',
|
|
730
|
+
dimensions: [2, 1, 1],
|
|
731
|
+
},
|
|
732
|
+
})
|
|
733
|
+
// Enough furniture that the build plays for seconds, in several steps.
|
|
734
|
+
const buildSofa = () => {
|
|
735
|
+
const pieces = Array.from({ length: 12 }, (_, index) =>
|
|
736
|
+
ItemNode.parse({
|
|
737
|
+
...sofa,
|
|
738
|
+
id: `item_sofa${index === 0 ? '' : index}`,
|
|
739
|
+
position: [index * 0.3, 0, 1 + (index % 3)],
|
|
740
|
+
}),
|
|
741
|
+
)
|
|
742
|
+
for (const piece of pieces) {
|
|
743
|
+
useScene.getState().createNode(piece, level.id)
|
|
744
|
+
sceneRegistry.nodes.set(piece.id, new Group())
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
// A commit with a window and a sofa in it: the window takes it outdoors.
|
|
748
|
+
const moveWindow = () => {
|
|
749
|
+
useScene.getState().updateNode('window_east', { position: [2.5, 1.2, 0] } as never)
|
|
750
|
+
useScene.getState().updateNode(resting.id, { position: [2.5, 0, 2.5] } as never)
|
|
751
|
+
}
|
|
752
|
+
const playing = () =>
|
|
753
|
+
Array.from({ length: 12 }, (_, i) => `item_sofa${i === 0 ? '' : i}`).some((id) =>
|
|
754
|
+
isNodeRevealing(id),
|
|
755
|
+
)
|
|
756
|
+
const upDuringBuild: number[] = []
|
|
757
|
+
let at = 0
|
|
758
|
+
const stop = useViewer.subscribe((state, before) => {
|
|
759
|
+
if (state.wallMode === 'up' && before.wallMode !== 'up' && playing()) upDuringBuild.push(at)
|
|
760
|
+
})
|
|
761
|
+
driver = startConstructionReveal({ reveals: (commit) => commit.author === 'agent' })
|
|
762
|
+
usePointAsk.getState().setBusy(true)
|
|
763
|
+
await act(async () => {
|
|
764
|
+
const commit = (kind: 'build' | 'exterior') =>
|
|
765
|
+
runAsSceneCommitAuthor('agent', kind === 'build' ? buildSofa : moveWindow)
|
|
766
|
+
commit(setup.first)
|
|
767
|
+
if (setup.nextAtMs === undefined) commit(setup.next)
|
|
768
|
+
for (at = 50; at <= 12000; at += 50) {
|
|
769
|
+
if (at === setup.nextAtMs) commit(setup.next)
|
|
770
|
+
driver!.tick(at)
|
|
771
|
+
await renderer!.advanceFrames(1, 0.02)
|
|
772
|
+
}
|
|
773
|
+
await renderer!.advanceFrames(10, 0.1)
|
|
774
|
+
})
|
|
775
|
+
stop()
|
|
776
|
+
return { upDuringBuild }
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
test('an exterior edit queued behind a build does not put the walls up while the build plays', async () => {
|
|
780
|
+
const { upDuringBuild } = await followBuildAmid({ first: 'build', next: 'exterior' })
|
|
781
|
+
expect(poses.length).toBeGreaterThan(0)
|
|
782
|
+
expect(upDuringBuild).toEqual([])
|
|
783
|
+
// The edit itself is looked at from outside once it plays.
|
|
784
|
+
expect(useViewer.getState().wallMode).toBe('up')
|
|
785
|
+
})
|
|
786
|
+
|
|
787
|
+
test('an exterior edit that has played does not take the next build outdoors', async () => {
|
|
788
|
+
const { upDuringBuild } = await followBuildAmid({
|
|
789
|
+
first: 'exterior',
|
|
790
|
+
next: 'build',
|
|
791
|
+
nextAtMs: 3000,
|
|
792
|
+
})
|
|
793
|
+
expect(poses.length).toBeGreaterThan(0)
|
|
794
|
+
expect(upDuringBuild).toEqual([])
|
|
795
|
+
// The walls the edit put up are cut away again for the build, once.
|
|
796
|
+
expect(useViewer.getState().wallMode).toBe('cutaway')
|
|
797
|
+
})
|
|
798
|
+
|
|
799
|
+
// The owner, 20 October, on the underground PR: with Follow Pascal on, the basement's opening
|
|
800
|
+
// animation ran again and again while the agent worked. A level below grade is shown with the
|
|
801
|
+
// floors above it hidden and the ground open over it; every pull-back to the finished house
|
|
802
|
+
// showed the floors above and closed the ground, and the next piece on the basement opened it again.
|
|
803
|
+
const basementPiece = (id: string, level: string, at: number) =>
|
|
804
|
+
ItemNode.parse({
|
|
805
|
+
id,
|
|
806
|
+
parentId: level,
|
|
807
|
+
position: [at, 0, 2],
|
|
808
|
+
asset: {
|
|
809
|
+
id: 'chair',
|
|
810
|
+
category: 'furniture',
|
|
811
|
+
name: 'Chair',
|
|
812
|
+
thumbnail: '',
|
|
813
|
+
src: '/chair.glb',
|
|
814
|
+
dimensions: [1, 1, 1],
|
|
815
|
+
},
|
|
816
|
+
})
|
|
817
|
+
|
|
818
|
+
/**
|
|
819
|
+
* A house seated 2.9 m down: a basement (below grade) under a ground floor, the basement shown the
|
|
820
|
+
* way the level display shows it. `pieces` are agent commits, each at its own time (ms) on one of
|
|
821
|
+
* the two floors; the reveal plays them and Follow follows. What the ground did is returned:
|
|
822
|
+
* every time the level shown lay below grade, or stopped doing so.
|
|
823
|
+
*/
|
|
824
|
+
async function followBasement(pieces: Array<{ at: number; on: 'basement' | 'ground' }>) {
|
|
825
|
+
const square: Array<[number, number]> = [
|
|
826
|
+
[0, 0],
|
|
827
|
+
[6, 0],
|
|
828
|
+
[6, 6],
|
|
829
|
+
[0, 6],
|
|
830
|
+
]
|
|
831
|
+
const building = BuildingNode.parse({ id: 'building_follow', position: [0, -2.9, 0] })
|
|
832
|
+
const basement = LevelNode.parse({
|
|
833
|
+
id: 'level_basement',
|
|
834
|
+
parentId: building.id,
|
|
835
|
+
level: -1,
|
|
836
|
+
height: 2.9,
|
|
837
|
+
})
|
|
838
|
+
const ground = LevelNode.parse({ id: 'level_ground', parentId: building.id, level: 0 })
|
|
839
|
+
building.children = [basement.id, ground.id]
|
|
840
|
+
basement.children = ['slab_basement'] as never
|
|
841
|
+
ground.children = ['slab_ground'] as never
|
|
842
|
+
const slabs = [basement, ground].map((level) =>
|
|
843
|
+
SlabNode.parse({
|
|
844
|
+
id: level === basement ? 'slab_basement' : 'slab_ground',
|
|
845
|
+
parentId: level.id,
|
|
846
|
+
polygon: square,
|
|
847
|
+
}),
|
|
848
|
+
)
|
|
849
|
+
useScene
|
|
850
|
+
.getState()
|
|
851
|
+
.setScene(
|
|
852
|
+
Object.fromEntries([building, basement, ground, ...slabs].map((node) => [node.id, node])),
|
|
853
|
+
[building.id],
|
|
854
|
+
)
|
|
855
|
+
useViewer.getState().setSelection({ buildingId: building.id, levelId: basement.id })
|
|
856
|
+
useViewer.setState({ levelMode: 'stacked', hideLevelsAboveSelection: true })
|
|
857
|
+
const stopBelowGrade = followBelowGrade()
|
|
858
|
+
const standing: boolean[] = [useViewer.getState().belowGrade]
|
|
859
|
+
const stopWatching = useViewer.subscribe((state) => {
|
|
860
|
+
if (state.belowGrade !== standing.at(-1)) standing.push(state.belowGrade)
|
|
861
|
+
})
|
|
862
|
+
renderer = await create(
|
|
863
|
+
<>
|
|
864
|
+
<group name="scene-renderer" />
|
|
865
|
+
<FollowPascal />
|
|
866
|
+
</>,
|
|
867
|
+
)
|
|
868
|
+
driver = startConstructionReveal({ reveals: (commit) => commit.author === 'agent' })
|
|
869
|
+
usePointAsk.getState().setBusy(true)
|
|
870
|
+
const end = Math.max(...pieces.map((piece) => piece.at)) + 7000
|
|
871
|
+
await act(async () => {
|
|
872
|
+
for (let at = 0; at <= end; at += 50) {
|
|
873
|
+
for (const [index, piece] of pieces.entries()) {
|
|
874
|
+
if (piece.at !== at) continue
|
|
875
|
+
const node = basementPiece(`item_piece${index}`, `level_${piece.on}`, index * 0.4)
|
|
876
|
+
runAsSceneCommitAuthor('agent', () =>
|
|
877
|
+
useScene.getState().createNode(node, node.parentId as AnyNodeId),
|
|
878
|
+
)
|
|
879
|
+
sceneRegistry.nodes.set(node.id, new Group())
|
|
880
|
+
}
|
|
881
|
+
driver!.tick(at + 1)
|
|
882
|
+
await renderer!.advanceFrames(1, 0.02)
|
|
883
|
+
}
|
|
884
|
+
await renderer!.advanceFrames(10, 0.1)
|
|
885
|
+
})
|
|
886
|
+
stopWatching()
|
|
887
|
+
const result = { standing, shown: useViewer.getState().selection.levelId }
|
|
888
|
+
stopBelowGrade()
|
|
889
|
+
return result
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
test('following furnishing on a basement keeps the floors above hidden and the ground open', async () => {
|
|
893
|
+
const { standing, shown } = await followBasement([
|
|
894
|
+
{ at: 0, on: 'basement' },
|
|
895
|
+
{ at: 7000, on: 'basement' },
|
|
896
|
+
{ at: 14000, on: 'basement' },
|
|
897
|
+
])
|
|
898
|
+
expect(shown).toBe('level_basement')
|
|
899
|
+
// The basement is below grade from the first moment to the last: the ground is never asked to close.
|
|
900
|
+
expect(standing).toEqual([true])
|
|
901
|
+
expect(useViewer.getState().hideLevelsAboveSelection).toBe(true)
|
|
902
|
+
})
|
|
903
|
+
|
|
904
|
+
test('a piece on the ground floor between two on the basement leaves it once and comes back once', async () => {
|
|
905
|
+
const { standing } = await followBasement([
|
|
906
|
+
{ at: 0, on: 'basement' },
|
|
907
|
+
{ at: 7000, on: 'ground' },
|
|
908
|
+
{ at: 14000, on: 'basement' },
|
|
909
|
+
{ at: 21000, on: 'basement' },
|
|
910
|
+
])
|
|
911
|
+
expect(standing).toEqual([true, false, true])
|
|
912
|
+
})
|