@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
|
@@ -10,14 +10,16 @@ import {
|
|
|
10
10
|
type AnyNode,
|
|
11
11
|
type AnyNodeId,
|
|
12
12
|
type BuildingNode,
|
|
13
|
-
|
|
14
|
-
getWallPlanFootprint,
|
|
13
|
+
getRoofSegmentDeckReach,
|
|
15
14
|
type LevelNode,
|
|
16
15
|
migrateSiteMetadata,
|
|
16
|
+
type RoofSegmentNode,
|
|
17
17
|
type SceneSnapshot,
|
|
18
18
|
type SiteNode,
|
|
19
|
+
siteFrame,
|
|
20
|
+
turnPlanPoint,
|
|
19
21
|
unionPolygons,
|
|
20
|
-
|
|
22
|
+
yawOf,
|
|
21
23
|
} from '@pascal-app/core'
|
|
22
24
|
import { type Pt, pointInPolygon, polygonArea, polygonBounds } from './geometry'
|
|
23
25
|
|
|
@@ -69,98 +71,9 @@ export function findLowestLevel(
|
|
|
69
71
|
}
|
|
70
72
|
|
|
71
73
|
/**
|
|
72
|
-
*
|
|
73
|
-
*
|
|
74
|
-
*
|
|
75
|
-
*/
|
|
76
|
-
export function levelFootprintLoops(
|
|
77
|
-
scene: SceneSnapshot,
|
|
78
|
-
level: LevelNode | null,
|
|
79
|
-
building: BuildingNode | null,
|
|
80
|
-
): Pt[][] {
|
|
81
|
-
if (!level) return []
|
|
82
|
-
const walls: WallNode[] = []
|
|
83
|
-
for (const childId of level.children ?? []) {
|
|
84
|
-
const child = scene.nodes[childId as AnyNodeId] as AnyNode | undefined
|
|
85
|
-
if (child?.type === 'wall') walls.push(child as WallNode)
|
|
86
|
-
}
|
|
87
|
-
if (walls.length === 0) return []
|
|
88
|
-
|
|
89
|
-
const miters = calculateLevelMiters(walls)
|
|
90
|
-
const yaw = building?.rotation?.[1] ?? 0
|
|
91
|
-
const cos = Math.cos(yaw)
|
|
92
|
-
const sin = Math.sin(yaw)
|
|
93
|
-
const ox = building?.position?.[0] ?? 0
|
|
94
|
-
const oz = building?.position?.[2] ?? 0
|
|
95
|
-
|
|
96
|
-
const loops: Pt[][] = []
|
|
97
|
-
for (const wall of walls) {
|
|
98
|
-
const poly = getWallPlanFootprint(wall, miters)
|
|
99
|
-
if (poly.length < 3) continue
|
|
100
|
-
// three.js Y rotation, the convention BuildingRenderer applies:
|
|
101
|
-
// world = (cos·lx + sin·lz, −sin·lx + cos·lz) + position
|
|
102
|
-
loops.push(poly.map((p) => [ox + p.x * cos + p.y * sin, oz - p.x * sin + p.y * cos] as Pt))
|
|
103
|
-
}
|
|
104
|
-
return loops
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
/**
|
|
108
|
-
* The outer ring(s) of a set of wall bands: their union, minus any ring that
|
|
109
|
-
* lies inside another (a room enclosed by partitions is a hole in the union,
|
|
110
|
-
* not a second building). Falls back to the bands themselves when the union
|
|
111
|
-
* yields nothing.
|
|
112
|
-
*/
|
|
113
|
-
export function footprintOutline(loops: readonly Pt[][]): Pt[][] {
|
|
114
|
-
const rings = unionPolygons(loops.map((loop) => loop.map((p) => [p[0], p[1]]))) as Pt[][]
|
|
115
|
-
if (rings.length === 0) return [...loops]
|
|
116
|
-
const inside = (p: Pt, ring: Pt[]): boolean => {
|
|
117
|
-
let hit = false
|
|
118
|
-
for (let i = 0, j = ring.length - 1; i < ring.length; j = i++) {
|
|
119
|
-
const a = ring[i] as Pt
|
|
120
|
-
const b = ring[j] as Pt
|
|
121
|
-
if (
|
|
122
|
-
a[1] > p[1] !== b[1] > p[1] &&
|
|
123
|
-
p[0] < ((b[0] - a[0]) * (p[1] - a[1])) / (b[1] - a[1]) + a[0]
|
|
124
|
-
) {
|
|
125
|
-
hit = !hit
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
return hit
|
|
129
|
-
}
|
|
130
|
-
return rings.filter(
|
|
131
|
-
(ring, i) =>
|
|
132
|
-
!rings.some((other, j) => j !== i && ring[0] !== undefined && inside(ring[0], other)),
|
|
133
|
-
)
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
/** A three.js Y rotation of a plan point: local (x, z) turned by `yaw`. */
|
|
137
|
-
export function turn(x: number, z: number, yaw: number): Pt {
|
|
138
|
-
const c = Math.cos(yaw)
|
|
139
|
-
const s = Math.sin(yaw)
|
|
140
|
-
return [c * x + s * z, -s * x + c * z]
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
export function yawOf(rotation: unknown): number {
|
|
144
|
-
if (typeof rotation === 'number') return rotation
|
|
145
|
-
if (Array.isArray(rotation)) return Number(rotation[1] ?? 0) || 0
|
|
146
|
-
return 0
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
/** Level-local plan (x, z) → site metres, through the building's placement (see `levelFootprintLoops`). */
|
|
150
|
-
export function siteFrame(building: BuildingNode | null): (x: number, z: number) => Pt {
|
|
151
|
-
const yaw = building?.rotation?.[1] ?? 0
|
|
152
|
-
const ox = building?.position?.[0] ?? 0
|
|
153
|
-
const oz = building?.position?.[2] ?? 0
|
|
154
|
-
return (x, z) => {
|
|
155
|
-
const [tx, tz] = turn(x, z, yaw)
|
|
156
|
-
return [ox + tx, oz + tz]
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
/**
|
|
161
|
-
* The roof's outline on the lot — every roof segment's plan rectangle plus
|
|
162
|
-
* its overhang, through the segment's, the roof's and the building's turns,
|
|
163
|
-
* unioned into the outer ring(s). Dashed on the site plan: what the eye sees
|
|
74
|
+
* The roof's outline on the lot — every roof segment's plan rectangle out to
|
|
75
|
+
* its deck's reach at its eaves and rakes, through the segment's, the roof's
|
|
76
|
+
* and the building's turns, unioned into the outer ring(s). Dashed on the site plan: what the eye sees
|
|
164
77
|
* from above is the roof, and the setback is measured to the wall under it.
|
|
165
78
|
*/
|
|
166
79
|
export function roofOutlineRings(
|
|
@@ -182,15 +95,10 @@ export function roofOutlineRings(
|
|
|
182
95
|
if (node.type !== 'roof-segment') continue
|
|
183
96
|
const roof = roofs.get(String((node as { parentId?: unknown }).parentId))
|
|
184
97
|
if (!roof) continue
|
|
185
|
-
const seg = node as
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
depth?: number
|
|
190
|
-
overhang?: number
|
|
191
|
-
}
|
|
192
|
-
const w = (seg.width ?? 0) / 2 + (seg.overhang ?? 0)
|
|
193
|
-
const d = (seg.depth ?? 0) / 2 + (seg.overhang ?? 0)
|
|
98
|
+
const seg = node as RoofSegmentNode
|
|
99
|
+
const reach = getRoofSegmentDeckReach(seg)
|
|
100
|
+
const w = (seg.width ?? 0) / 2 + reach.x
|
|
101
|
+
const d = (seg.depth ?? 0) / 2 + reach.z
|
|
194
102
|
if (w <= 0 || d <= 0) continue
|
|
195
103
|
const segYaw = yawOf(seg.rotation)
|
|
196
104
|
const roofYaw = yawOf(roof.rotation)
|
|
@@ -206,8 +114,8 @@ export function roofOutlineRings(
|
|
|
206
114
|
]
|
|
207
115
|
rects.push(
|
|
208
116
|
local.map(([lx, lz]) => {
|
|
209
|
-
const [ax, az] =
|
|
210
|
-
const [bx, bz] =
|
|
117
|
+
const [ax, az] = turnPlanPoint(lx, lz, segYaw)
|
|
118
|
+
const [bx, bz] = turnPlanPoint(ax + sx, az + sz, roofYaw)
|
|
211
119
|
return toSite(bx + rx, bz + rz)
|
|
212
120
|
}),
|
|
213
121
|
)
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { describe, expect, test } from 'bun:test'
|
|
2
|
-
import {
|
|
2
|
+
import { castYardDimensionsOriented } from '@pascal-app/core'
|
|
3
|
+
import { castYardDimensions, type Pt } from './geometry'
|
|
3
4
|
|
|
4
5
|
// 20 m × 30 m lot centred on the origin, y south
|
|
5
6
|
const LOT: Pt[] = [
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { AnyNode } from '@pascal-app/core'
|
|
2
|
+
|
|
3
|
+
// What the nodes package declares as `capabilities.exterior` for its wall, door and window, written
|
|
4
|
+
// out for the tests that register their own throw-away kinds (`packages/editor` does not import
|
|
5
|
+
// `@pascal-app/nodes`).
|
|
6
|
+
|
|
7
|
+
/** A wall with an outdoor face. */
|
|
8
|
+
export const wallExterior = (node: AnyNode) =>
|
|
9
|
+
node.type === 'wall' && (node.frontSide === 'exterior' || node.backSide === 'exterior')
|
|
10
|
+
|
|
11
|
+
/** An opening in a roof segment or in such a wall. */
|
|
12
|
+
export const openingExterior = (node: AnyNode, nodes: Readonly<Record<string, AnyNode>>) => {
|
|
13
|
+
if (node.type !== 'door' && node.type !== 'window') return false
|
|
14
|
+
if (node.roofSegmentId) return true
|
|
15
|
+
const host = nodes[node.wallId ?? node.parentId ?? '']
|
|
16
|
+
return host ? wallExterior(host) : false
|
|
17
|
+
}
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import { afterAll, beforeAll, describe, expect, test } from 'bun:test'
|
|
2
|
+
import {
|
|
3
|
+
type AnyNode,
|
|
4
|
+
type AnyNodeDefinition,
|
|
5
|
+
BuildingNode,
|
|
6
|
+
type Capabilities,
|
|
7
|
+
DoorNode,
|
|
8
|
+
ItemNode,
|
|
9
|
+
LevelNode,
|
|
10
|
+
loadPlugin,
|
|
11
|
+
nodeRegistry,
|
|
12
|
+
RoofNode,
|
|
13
|
+
RoofSegmentNode,
|
|
14
|
+
SiteNode,
|
|
15
|
+
WallNode,
|
|
16
|
+
} from '@pascal-app/core'
|
|
17
|
+
import { anyExterior, isExteriorNode } from './follow-pascal-exterior'
|
|
18
|
+
import { openingExterior, wallExterior } from './follow-pascal-exterior.test-fixture'
|
|
19
|
+
|
|
20
|
+
// The kinds are the nodes package's (it declares `capabilities.exterior` on them); fixtures of the
|
|
21
|
+
// same kind stand in for them here, and plugin-style kinds that declare the same fields are held to
|
|
22
|
+
// the same rules.
|
|
23
|
+
const kind = (type: string, schema: unknown, exterior?: Capabilities['exterior']) =>
|
|
24
|
+
({
|
|
25
|
+
kind: type,
|
|
26
|
+
schemaVersion: 1,
|
|
27
|
+
schema,
|
|
28
|
+
category: 'structure',
|
|
29
|
+
defaults: () => ({}),
|
|
30
|
+
capabilities: exterior === undefined ? {} : { exterior },
|
|
31
|
+
geometry: () => null,
|
|
32
|
+
}) as unknown as AnyNodeDefinition
|
|
33
|
+
|
|
34
|
+
let restoreRegistry: () => void = () => {}
|
|
35
|
+
beforeAll(async () => {
|
|
36
|
+
restoreRegistry = nodeRegistry._snapshot()
|
|
37
|
+
nodeRegistry._reset()
|
|
38
|
+
await loadPlugin({
|
|
39
|
+
id: 'fixture:exterior',
|
|
40
|
+
apiVersion: 1,
|
|
41
|
+
nodes: [
|
|
42
|
+
kind('site', SiteNode, true),
|
|
43
|
+
kind('building', BuildingNode, false),
|
|
44
|
+
kind('level', LevelNode, false),
|
|
45
|
+
kind('roof', RoofNode, true),
|
|
46
|
+
kind('roof-segment', RoofSegmentNode, true),
|
|
47
|
+
kind('wall', WallNode, wallExterior),
|
|
48
|
+
kind('door', DoorNode, openingExterior),
|
|
49
|
+
kind('item', ItemNode),
|
|
50
|
+
// A plugin's own kinds: outdoors, outdoors by a field, and saying nothing.
|
|
51
|
+
kind('fixture:deck', ItemNode, true),
|
|
52
|
+
kind('fixture:cover', ItemNode, (node) => (node.name === 'seen' ? true : undefined)),
|
|
53
|
+
kind('fixture:cable', ItemNode),
|
|
54
|
+
],
|
|
55
|
+
})
|
|
56
|
+
})
|
|
57
|
+
afterAll(() => restoreRegistry())
|
|
58
|
+
|
|
59
|
+
function scene() {
|
|
60
|
+
const site = SiteNode.parse({ id: 'site_x' })
|
|
61
|
+
const building = BuildingNode.parse({ id: 'building_x', parentId: site.id })
|
|
62
|
+
const level = LevelNode.parse({ id: 'level_x', parentId: building.id, level: 0 })
|
|
63
|
+
const outer = WallNode.parse({
|
|
64
|
+
id: 'wall_outer',
|
|
65
|
+
parentId: level.id,
|
|
66
|
+
start: [0, 0],
|
|
67
|
+
end: [4, 0],
|
|
68
|
+
frontSide: 'interior',
|
|
69
|
+
backSide: 'exterior',
|
|
70
|
+
})
|
|
71
|
+
const inner = WallNode.parse({
|
|
72
|
+
id: 'wall_inner',
|
|
73
|
+
parentId: level.id,
|
|
74
|
+
start: [0, 2],
|
|
75
|
+
end: [4, 2],
|
|
76
|
+
frontSide: 'interior',
|
|
77
|
+
backSide: 'interior',
|
|
78
|
+
})
|
|
79
|
+
const onOuter = DoorNode.parse({ id: 'door_outer', parentId: outer.id, wallId: outer.id })
|
|
80
|
+
const onInner = DoorNode.parse({ id: 'door_inner', parentId: inner.id, wallId: inner.id })
|
|
81
|
+
const sofa = ItemNode.parse({
|
|
82
|
+
id: 'item_sofa',
|
|
83
|
+
parentId: level.id,
|
|
84
|
+
asset: {
|
|
85
|
+
id: 'sofa',
|
|
86
|
+
category: 'furniture',
|
|
87
|
+
name: 'Sofa',
|
|
88
|
+
thumbnail: '',
|
|
89
|
+
src: '/sofa.glb',
|
|
90
|
+
dimensions: [2, 1, 1],
|
|
91
|
+
},
|
|
92
|
+
})
|
|
93
|
+
const bench = ItemNode.parse({ ...sofa, id: 'item_bench', parentId: site.id })
|
|
94
|
+
const segment = RoofSegmentNode.parse({ id: 'rseg_x', parentId: 'roof_x' })
|
|
95
|
+
const roof = RoofNode.parse({ id: 'roof_x', parentId: level.id, children: [segment.id] })
|
|
96
|
+
// Only their kind, name and parent matter here.
|
|
97
|
+
const plugin = (id: string, type: string, parentId: string, name?: string) =>
|
|
98
|
+
({ id, type, parentId, name }) as unknown as AnyNode
|
|
99
|
+
const deck = plugin('deck_x', 'fixture:deck', site.id)
|
|
100
|
+
const deckOnLevel = plugin('deck_level', 'fixture:deck', level.id)
|
|
101
|
+
const underDeck = plugin('cable_deck', 'fixture:cable', deckOnLevel.id)
|
|
102
|
+
const underRoof = plugin('cable_roof', 'fixture:cable', roof.id)
|
|
103
|
+
const cableInRoom = plugin('cable_room', 'fixture:cable', level.id)
|
|
104
|
+
const seen = plugin('cover_seen', 'fixture:cover', level.id, 'seen')
|
|
105
|
+
const unseen = plugin('cover_unseen', 'fixture:cover', level.id)
|
|
106
|
+
const coverOnSite = plugin('cover_site', 'fixture:cover', site.id)
|
|
107
|
+
return Object.fromEntries(
|
|
108
|
+
[
|
|
109
|
+
site,
|
|
110
|
+
building,
|
|
111
|
+
level,
|
|
112
|
+
outer,
|
|
113
|
+
inner,
|
|
114
|
+
onOuter,
|
|
115
|
+
onInner,
|
|
116
|
+
sofa,
|
|
117
|
+
bench,
|
|
118
|
+
roof,
|
|
119
|
+
segment,
|
|
120
|
+
deck,
|
|
121
|
+
deckOnLevel,
|
|
122
|
+
underDeck,
|
|
123
|
+
underRoof,
|
|
124
|
+
cableInRoom,
|
|
125
|
+
seen,
|
|
126
|
+
unseen,
|
|
127
|
+
coverOnSite,
|
|
128
|
+
].map((node) => [node.id, node as AnyNode]),
|
|
129
|
+
)
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
describe('what a change is seen from', () => {
|
|
133
|
+
const nodes = scene()
|
|
134
|
+
|
|
135
|
+
test('a wall with an exterior face, and an opening in it, are seen from outside', () => {
|
|
136
|
+
expect(isExteriorNode(nodes, 'wall_outer')).toBe(true)
|
|
137
|
+
expect(isExteriorNode(nodes, 'door_outer')).toBe(true)
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
test('an interior wall, an opening in it and furniture are looked at through a cutaway', () => {
|
|
141
|
+
expect(isExteriorNode(nodes, 'wall_inner')).toBe(false)
|
|
142
|
+
expect(isExteriorNode(nodes, 'door_inner')).toBe(false)
|
|
143
|
+
expect(isExteriorNode(nodes, 'item_sofa')).toBe(false)
|
|
144
|
+
})
|
|
145
|
+
|
|
146
|
+
test('a roof and what stands on it, the ground, and furnishing standing in no storey are outside', () => {
|
|
147
|
+
for (const id of ['roof_x', 'rseg_x', 'deck_x', 'item_bench'])
|
|
148
|
+
expect(isExteriorNode(nodes, id)).toBe(true)
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
// A kind that declares what the built-ins declare is treated as the built-in is.
|
|
152
|
+
test('a plugin kind is outdoors as its definition says: always, by a field, or by what holds it', () => {
|
|
153
|
+
expect(isExteriorNode(nodes, 'deck_level')).toBe(true)
|
|
154
|
+
expect(isExteriorNode(nodes, 'cover_seen')).toBe(true)
|
|
155
|
+
expect(isExteriorNode(nodes, 'cover_unseen')).toBe(false)
|
|
156
|
+
expect(isExteriorNode(nodes, 'cable_deck')).toBe(true)
|
|
157
|
+
expect(isExteriorNode(nodes, 'cable_roof')).toBe(true)
|
|
158
|
+
expect(isExteriorNode(nodes, 'cable_room')).toBe(false)
|
|
159
|
+
expect(isExteriorNode(nodes, 'cover_site')).toBe(true)
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
test('a node that is not in the scene is nothing', () => {
|
|
163
|
+
expect(isExteriorNode(nodes, 'wall_gone')).toBe(false)
|
|
164
|
+
})
|
|
165
|
+
|
|
166
|
+
test('a group is outside when any of its nodes is', () => {
|
|
167
|
+
expect(anyExterior(nodes, ['item_sofa', 'door_outer'])).toBe(true)
|
|
168
|
+
expect(anyExterior(nodes, ['item_sofa', 'wall_inner'])).toBe(false)
|
|
169
|
+
expect(anyExterior(nodes, [])).toBe(false)
|
|
170
|
+
})
|
|
171
|
+
})
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { type AnyNode, type AnyNodeId, findLevelAncestorId, nodeRegistry } from '@pascal-app/core'
|
|
2
|
+
|
|
3
|
+
type Nodes = Readonly<Record<string, AnyNode>>
|
|
4
|
+
|
|
5
|
+
/** What a kind says about a node (`capabilities.exterior`): outdoors, never, or nothing. */
|
|
6
|
+
function declaredExterior(node: AnyNode, nodes: Nodes): boolean | undefined {
|
|
7
|
+
const exterior = nodeRegistry.get(node.type)?.capabilities?.exterior
|
|
8
|
+
return typeof exterior === 'function' ? exterior(node, nodes) : exterior
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Whether a change to this node is seen from outside, as its kind declares (`capabilities.exterior`:
|
|
13
|
+
* a roof, a wall with an exterior face and an opening in it, a fence). A node whose kind says nothing
|
|
14
|
+
* is outdoors when what holds it is (a chimney's cap), or when it stands in no building's storey
|
|
15
|
+
* (outdoor furnishing, plants). A sofa, an interior wall and a finish inside are not: they are looked
|
|
16
|
+
* at through a cutaway.
|
|
17
|
+
*/
|
|
18
|
+
export function isExteriorNode(nodes: Nodes, id: string): boolean {
|
|
19
|
+
const node = nodes[id]
|
|
20
|
+
if (!node) return false
|
|
21
|
+
const own = declaredExterior(node, nodes)
|
|
22
|
+
if (own !== undefined) return own
|
|
23
|
+
// Standing under a node that is outdoors, up to the first one that never is (a storey).
|
|
24
|
+
for (
|
|
25
|
+
let at = node.parentId ? nodes[node.parentId] : undefined, guard = 0;
|
|
26
|
+
at && guard < 16;
|
|
27
|
+
at = at.parentId ? nodes[at.parentId] : undefined, guard += 1
|
|
28
|
+
) {
|
|
29
|
+
const held = nodeRegistry.get(at.type)?.capabilities?.exterior
|
|
30
|
+
if (held === true) return true
|
|
31
|
+
if (held === false) break
|
|
32
|
+
}
|
|
33
|
+
return findLevelAncestorId(id as AnyNodeId, nodes as never) === null
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Whether any of `ids` is a change seen from outside. */
|
|
37
|
+
export function anyExterior(nodes: Nodes, ids: readonly string[]): boolean {
|
|
38
|
+
return ids.some((id) => isExteriorNode(nodes, id))
|
|
39
|
+
}
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test'
|
|
2
|
+
import {
|
|
3
|
+
type Floor,
|
|
4
|
+
FollowPresentation,
|
|
5
|
+
type PresentationPort,
|
|
6
|
+
type ViewPresentation,
|
|
7
|
+
} from './follow-pascal-presentation'
|
|
8
|
+
|
|
9
|
+
// The owner, 12 October, on 3114: Follow Pascal left the view in cutaway with the upper floors hidden
|
|
10
|
+
// after it stopped, and the walls looked invisible. Follow changes the presentation to see what a build
|
|
11
|
+
// works on; when it ends it puts back what it changed, and only that.
|
|
12
|
+
|
|
13
|
+
const GROUND: Floor = {
|
|
14
|
+
buildingId: 'b',
|
|
15
|
+
levelId: 'ground',
|
|
16
|
+
zoneId: null,
|
|
17
|
+
selectedIds: [],
|
|
18
|
+
room: null,
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** A view and a record of every write made to it. */
|
|
22
|
+
function view(initial: Partial<ViewPresentation> = {}) {
|
|
23
|
+
const state: ViewPresentation = {
|
|
24
|
+
wall: 'up',
|
|
25
|
+
levels: 'stacked',
|
|
26
|
+
hideAbove: false,
|
|
27
|
+
floor: { ...GROUND },
|
|
28
|
+
...initial,
|
|
29
|
+
}
|
|
30
|
+
const writes: string[] = []
|
|
31
|
+
const port: PresentationPort = {
|
|
32
|
+
read: () => ({ ...state, floor: { ...state.floor } }),
|
|
33
|
+
setWall: (mode) => {
|
|
34
|
+
writes.push(`wall:${mode}`)
|
|
35
|
+
state.wall = mode
|
|
36
|
+
},
|
|
37
|
+
setLevels: (mode) => {
|
|
38
|
+
writes.push(`levels:${mode}`)
|
|
39
|
+
state.levels = mode
|
|
40
|
+
},
|
|
41
|
+
setHideAbove: (hide) => {
|
|
42
|
+
writes.push(`hideAbove:${hide}`)
|
|
43
|
+
state.hideAbove = hide
|
|
44
|
+
},
|
|
45
|
+
// A selected floor drops the room and what was picked, as the viewer's selection does.
|
|
46
|
+
setFloor: ({ buildingId, levelId }) => {
|
|
47
|
+
writes.push(`floor:${levelId}`)
|
|
48
|
+
state.floor = {
|
|
49
|
+
buildingId: buildingId ?? state.floor.buildingId,
|
|
50
|
+
levelId,
|
|
51
|
+
zoneId: null,
|
|
52
|
+
selectedIds: [],
|
|
53
|
+
room: null,
|
|
54
|
+
}
|
|
55
|
+
},
|
|
56
|
+
putFloor: (floor) => {
|
|
57
|
+
writes.push(`put:${floor.levelId}`)
|
|
58
|
+
state.floor = { ...floor }
|
|
59
|
+
},
|
|
60
|
+
}
|
|
61
|
+
return { state, writes, port }
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
describe('what Follow changes, it puts back', () => {
|
|
65
|
+
test('the cutaway it switched to', () => {
|
|
66
|
+
const { state, port } = view({ wall: 'up' })
|
|
67
|
+
const follow = new FollowPresentation(port)
|
|
68
|
+
follow.wall('cutaway')
|
|
69
|
+
expect(state.wall).toBe('cutaway')
|
|
70
|
+
follow.restore()
|
|
71
|
+
expect(state.wall).toBe('up')
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
test('the floor it selected, with the room and the picks the person had', () => {
|
|
75
|
+
const had: Floor = {
|
|
76
|
+
buildingId: 'b',
|
|
77
|
+
levelId: 'upper',
|
|
78
|
+
zoneId: 'zone_kitchen',
|
|
79
|
+
selectedIds: ['item_a', 'item_b'],
|
|
80
|
+
room: { levelId: 'upper', zoneId: 'zone_kitchen' },
|
|
81
|
+
}
|
|
82
|
+
const { state, port } = view({ floor: had })
|
|
83
|
+
const follow = new FollowPresentation(port)
|
|
84
|
+
follow.floor({ buildingId: 'b', levelId: 'ground' })
|
|
85
|
+
expect(state.floor.levelId).toBe('ground')
|
|
86
|
+
expect(state.floor.room).toBeNull()
|
|
87
|
+
follow.restore()
|
|
88
|
+
expect(state.floor).toEqual(had)
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
test('the upper floors it hid, and the level display it set', () => {
|
|
92
|
+
const { state, port } = view({ hideAbove: false, levels: 'solo' })
|
|
93
|
+
const follow = new FollowPresentation(port)
|
|
94
|
+
follow.hideAbove(true)
|
|
95
|
+
follow.levels('stacked')
|
|
96
|
+
expect(state).toMatchObject({ hideAbove: true, levels: 'stacked' })
|
|
97
|
+
follow.restore()
|
|
98
|
+
expect(state).toMatchObject({ hideAbove: false, levels: 'solo' })
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
test('to what the person had before the first change, however many floors it visited', () => {
|
|
102
|
+
const { state, port } = view()
|
|
103
|
+
const follow = new FollowPresentation(port)
|
|
104
|
+
follow.floor({ buildingId: 'b', levelId: 'upper' })
|
|
105
|
+
follow.floor({ buildingId: 'b', levelId: 'roof' })
|
|
106
|
+
follow.wall('cutaway')
|
|
107
|
+
follow.wall('cutaway')
|
|
108
|
+
follow.restore()
|
|
109
|
+
expect(state.floor.levelId).toBe('ground')
|
|
110
|
+
expect(state.wall).toBe('up')
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
test('nothing it did not change: a setting already as Follow wants it is not written', () => {
|
|
114
|
+
const { writes, port } = view({ wall: 'cutaway', hideAbove: true, levels: 'stacked' })
|
|
115
|
+
const follow = new FollowPresentation(port)
|
|
116
|
+
follow.wall('cutaway')
|
|
117
|
+
follow.hideAbove(true)
|
|
118
|
+
follow.levels('stacked')
|
|
119
|
+
follow.floor({ buildingId: 'b', levelId: 'ground' })
|
|
120
|
+
follow.restore()
|
|
121
|
+
expect(writes).toEqual([])
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
test('once: ending again, with nothing held, writes nothing', () => {
|
|
125
|
+
const { writes, port } = view()
|
|
126
|
+
const follow = new FollowPresentation(port)
|
|
127
|
+
follow.wall('cutaway')
|
|
128
|
+
follow.restore()
|
|
129
|
+
const count = writes.length
|
|
130
|
+
follow.restore()
|
|
131
|
+
expect(writes).toHaveLength(count)
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
test('a later session starts from what the person has then', () => {
|
|
135
|
+
const { state, port } = view()
|
|
136
|
+
const follow = new FollowPresentation(port)
|
|
137
|
+
follow.wall('cutaway')
|
|
138
|
+
follow.restore()
|
|
139
|
+
state.wall = 'down'
|
|
140
|
+
follow.wall('cutaway')
|
|
141
|
+
follow.restore()
|
|
142
|
+
expect(state.wall).toBe('down')
|
|
143
|
+
})
|
|
144
|
+
})
|
|
145
|
+
|
|
146
|
+
describe("the person's own choices stand", () => {
|
|
147
|
+
test('a wall mode picked while Follow is on is kept, and Follow does not take it back', () => {
|
|
148
|
+
const { state, writes, port } = view({ wall: 'up' })
|
|
149
|
+
const follow = new FollowPresentation(port)
|
|
150
|
+
follow.wall('cutaway')
|
|
151
|
+
state.wall = 'translucent'
|
|
152
|
+
follow.wall('cutaway')
|
|
153
|
+
expect(state.wall).toBe('translucent')
|
|
154
|
+
follow.restore()
|
|
155
|
+
expect(state.wall).toBe('translucent')
|
|
156
|
+
expect(writes).toEqual(['wall:cutaway'])
|
|
157
|
+
})
|
|
158
|
+
|
|
159
|
+
test('a floor picked while Follow is on is kept, and Follow does not select over it', () => {
|
|
160
|
+
const { state, writes, port } = view()
|
|
161
|
+
const follow = new FollowPresentation(port)
|
|
162
|
+
follow.floor({ buildingId: 'b', levelId: 'upper' })
|
|
163
|
+
state.floor = { ...state.floor, levelId: 'roof' }
|
|
164
|
+
follow.floor({ buildingId: 'b', levelId: 'upper' })
|
|
165
|
+
expect(state.floor.levelId).toBe('roof')
|
|
166
|
+
follow.restore()
|
|
167
|
+
expect(state.floor.levelId).toBe('roof')
|
|
168
|
+
expect(writes).toEqual(['floor:upper'])
|
|
169
|
+
})
|
|
170
|
+
|
|
171
|
+
test('the floor stays when the person picked something on it', () => {
|
|
172
|
+
const { state, writes, port } = view()
|
|
173
|
+
const follow = new FollowPresentation(port)
|
|
174
|
+
follow.floor({ buildingId: 'b', levelId: 'upper' })
|
|
175
|
+
state.floor = { ...state.floor, selectedIds: ['item_x'] }
|
|
176
|
+
follow.restore()
|
|
177
|
+
expect(state.floor).toMatchObject({ levelId: 'upper', selectedIds: ['item_x'] })
|
|
178
|
+
expect(writes).toEqual(['floor:upper'])
|
|
179
|
+
})
|
|
180
|
+
|
|
181
|
+
test('the settings are independent: a wall mode of their own, the floor still goes back', () => {
|
|
182
|
+
const { state, port } = view()
|
|
183
|
+
const follow = new FollowPresentation(port)
|
|
184
|
+
follow.wall('cutaway')
|
|
185
|
+
follow.floor({ buildingId: 'b', levelId: 'upper' })
|
|
186
|
+
state.wall = 'down'
|
|
187
|
+
follow.restore()
|
|
188
|
+
expect(state.wall).toBe('down')
|
|
189
|
+
expect(state.floor.levelId).toBe('ground')
|
|
190
|
+
})
|
|
191
|
+
|
|
192
|
+
test("the person's choice is theirs for the session, then Follow may change it again", () => {
|
|
193
|
+
const { state, port } = view()
|
|
194
|
+
const follow = new FollowPresentation(port)
|
|
195
|
+
follow.wall('cutaway')
|
|
196
|
+
state.wall = 'down'
|
|
197
|
+
follow.restore()
|
|
198
|
+
follow.wall('cutaway')
|
|
199
|
+
expect(port.read().wall).toBe('cutaway')
|
|
200
|
+
follow.restore()
|
|
201
|
+
expect(port.read().wall).toBe('down')
|
|
202
|
+
})
|
|
203
|
+
})
|