@pascal-app/editor 1.0.4 → 1.0.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (309) hide show
  1. package/README.md +106 -0
  2. package/package.json +40 -38
  3. package/src/components/editor/__bench__/room-label-placement.bench.ts +108 -0
  4. package/src/components/editor/bake-thumbnail.tsx +6 -15
  5. package/src/components/editor/construction-reveal-audio.test.tsx +150 -0
  6. package/src/components/editor/construction-reveal-audio.tsx +40 -0
  7. package/src/components/editor/custom-camera-controls.tsx +81 -8
  8. package/src/components/editor/editor-layout-mobile.tsx +1 -1
  9. package/src/components/editor/editor-layout-v2.tsx +1 -1
  10. package/src/components/editor/export-manager.tsx +14 -19
  11. package/src/components/editor/first-person/build-collider-world.test.ts +269 -0
  12. package/src/components/editor/first-person/build-collider-world.ts +22 -5
  13. package/src/components/editor/first-person/ground-holes.test.ts +128 -0
  14. package/src/components/editor/first-person/ground-holes.ts +130 -0
  15. package/src/components/editor/first-person/mechanism-targets.test.ts +89 -2
  16. package/src/components/editor/first-person/mechanism-targets.ts +52 -0
  17. package/src/components/editor/first-person-controls.tsx +19 -11
  18. package/src/components/editor/floor-aware-camera-controls.test.ts +24 -0
  19. package/src/components/editor/floorplan-mode-coordinator.tsx +2 -2
  20. package/src/components/editor/floorplan-panel.tsx +72 -23
  21. package/src/components/editor/follow-pascal.test.tsx +912 -0
  22. package/src/components/editor/follow-pascal.tsx +354 -0
  23. package/src/components/editor/group-actions.ts +2 -0
  24. package/src/components/editor/group-move-3d.ts +2 -0
  25. package/src/components/editor/group-rotate-handle.tsx +2 -0
  26. package/src/components/editor/group-transform-shared.test.ts +460 -0
  27. package/src/components/editor/index.tsx +166 -79
  28. package/src/components/editor/riser-diagram-panel.tsx +14 -7
  29. package/src/components/editor/room-highlight.tsx +13 -5
  30. package/src/components/editor/room-labels.test.ts +621 -128
  31. package/src/components/editor/room-labels.tsx +1090 -190
  32. package/src/components/editor/selection-manager.tsx +38 -117
  33. package/src/components/editor/slab-hole-highlights.tsx +2 -2
  34. package/src/components/editor/snapshot-capture-overlay.tsx +3 -2
  35. package/src/components/editor/snapshot-capture.test.ts +69 -58
  36. package/src/components/editor/snapshot-capture.ts +7 -1
  37. package/src/components/editor/thumbnail-generator.test.ts +350 -4
  38. package/src/components/editor/thumbnail-generator.tsx +128 -54
  39. package/src/components/editor/use-floorplan-background-placement.ts +3 -1
  40. package/src/components/editor/views/plugins-view.tsx +1 -8
  41. package/src/components/editor/views/view-bar.test.tsx +54 -0
  42. package/src/components/editor/views/view-bar.tsx +140 -17
  43. package/src/components/editor/wall-snap-beacon-layer.tsx +5 -4
  44. package/src/components/editor-2d/floorplan-group-move.tsx +3 -0
  45. package/src/components/editor-2d/floorplan-registered-tool-layer.tsx +2 -2
  46. package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +3 -4
  47. package/src/components/editor-2d/renderers/floorplan-dimension-edit-overlay.test.ts +1 -1
  48. package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +29 -7
  49. package/src/components/editor-2d/renderers/floorplan-reveal.test.tsx +254 -0
  50. package/src/components/editor-2d/renderers/floorplan-reveal.ts +141 -0
  51. package/src/components/point-ask/bubble.tsx +538 -0
  52. package/src/components/point-ask/hover-chip.tsx +91 -0
  53. package/src/components/point-ask/mode-pill.tsx +92 -0
  54. package/src/components/point-ask/optional-motion.ts +22 -0
  55. package/src/components/point-ask/outline-corners.tsx +93 -0
  56. package/src/components/point-ask/pins.tsx +711 -0
  57. package/src/components/point-ask/point-ask-layer.tsx +83 -0
  58. package/src/components/point-ask/point-ask-styles.ts +105 -0
  59. package/src/components/point-ask/region-marquee-styles.ts +15 -0
  60. package/src/components/point-ask/region-marquee.tsx +64 -0
  61. package/src/components/systems/ceiling/ceiling-bracket-batch.test.ts +12 -2
  62. package/src/components/systems/roof/roof-edit-system.tsx +1 -0
  63. package/src/components/tools/distribution/connection-compatibility.test.ts +34 -0
  64. package/src/components/tools/distribution/connection-compatibility.ts +43 -0
  65. package/src/components/tools/distribution/distribution-public.ts +9 -0
  66. package/src/components/tools/distribution/distribution-run-contract.test.ts +26 -0
  67. package/src/components/tools/distribution/distribution-run-contract.ts +94 -0
  68. package/src/components/tools/distribution/distribution-run-tool.test.ts +241 -0
  69. package/src/components/tools/distribution/distribution-run-tool.test.tsx +125 -0
  70. package/src/components/tools/distribution/distribution-run-tool.tsx +1323 -0
  71. package/src/components/tools/distribution/draw-alignment.ts +35 -0
  72. package/src/components/tools/distribution/ports.test.ts +56 -0
  73. package/src/components/tools/distribution/ports.ts +378 -0
  74. package/src/components/tools/distribution/run-cursor.test.ts +135 -0
  75. package/src/components/tools/distribution/run-cursor.ts +36 -0
  76. package/src/components/tools/distribution/run-direction-feedback.test.ts +60 -0
  77. package/src/components/tools/distribution/run-direction-feedback.tsx +251 -0
  78. package/src/components/tools/distribution/run-port-snap.test.ts +79 -0
  79. package/src/components/tools/distribution/run-port-snap.ts +25 -0
  80. package/src/components/tools/distribution/run-screen-direction.test.ts +23 -0
  81. package/src/components/tools/distribution/run-screen-direction.ts +30 -0
  82. package/src/components/tools/elevator/elevator-tool.tsx +3 -6
  83. package/src/components/tools/elevator/move-elevator-tool.tsx +2 -3
  84. package/src/components/tools/item/item-surface-strategy-resolver.test.ts +67 -68
  85. package/src/components/tools/item/placement-history.test.tsx +38 -0
  86. package/src/components/tools/item/placement-strategies.test.ts +46 -1
  87. package/src/components/tools/item/placement-strategies.ts +3 -6
  88. package/src/components/tools/item/shelf-surface-strategy-resolver.test.ts +56 -56
  89. package/src/components/tools/item/use-draft-node.test.tsx +94 -94
  90. package/src/components/tools/item/use-placement-coordinator.tsx +55 -49
  91. package/src/components/tools/registry/item-surface-move.test.ts +62 -63
  92. package/src/components/tools/registry/item-surface-move.ts +2 -0
  93. package/src/components/tools/registry/item-surface-resolver.test.ts +26 -26
  94. package/src/components/tools/registry/move-registry-node-tool.test.ts +268 -1
  95. package/src/components/tools/registry/move-registry-node-tool.tsx +63 -29
  96. package/src/components/tools/tool-manager.tsx +14 -5
  97. package/src/components/tools/wall/wall-drafting.test.ts +170 -57
  98. package/src/components/ui/action-menu/action-button.tsx +6 -1
  99. package/src/components/ui/action-menu/camera-actions.tsx +7 -3
  100. package/src/components/ui/action-menu/control-modes.tsx +13 -7
  101. package/src/components/ui/action-menu/follow-pascal-pill.tsx +32 -0
  102. package/src/components/ui/action-menu/follow-pascal-toggle.tsx +36 -0
  103. package/src/components/ui/action-menu/index.tsx +56 -33
  104. package/src/components/ui/action-menu/measurement-control.tsx +11 -9
  105. package/src/components/ui/action-menu/placement.ts +20 -0
  106. package/src/components/ui/action-menu/point-ask-button.tsx +52 -0
  107. package/src/components/ui/action-menu/view-toggles.tsx +47 -42
  108. package/src/components/ui/build-panel/build-panel.tsx +10 -3
  109. package/src/components/ui/command-palette/editor-commands.tsx +3 -12
  110. package/src/components/ui/command-palette/index.tsx +45 -15
  111. package/src/components/ui/command-palette/ranked-groups.test.ts +31 -0
  112. package/src/components/ui/controls/action-button.tsx +5 -2
  113. package/src/components/ui/controls/metric-control.tsx +1 -1
  114. package/src/components/ui/controls/segmented-control.tsx +21 -8
  115. package/src/components/ui/controls/select-control.tsx +1 -1
  116. package/src/components/ui/controls/slider-control.tsx +1 -1
  117. package/src/components/ui/controls/toggle-control.tsx +3 -3
  118. package/src/components/ui/controls/tool-options-panel.tsx +1 -1
  119. package/src/components/ui/floating-level-selector.tsx +150 -45
  120. package/src/components/ui/helpers/contextual-helper-panel.tsx +32 -7
  121. package/src/components/ui/helpers/helper-manager.test.tsx +54 -2
  122. package/src/components/ui/helpers/helper-manager.tsx +7 -1
  123. package/src/components/ui/helpers/item-helper.tsx +9 -1
  124. package/src/components/ui/panels/collections/collections-popover.tsx +8 -8
  125. package/src/components/ui/panels/mobile-panel-sheet.tsx +2 -2
  126. package/src/components/ui/panels/mobile-selection-bar.tsx +6 -3
  127. package/src/components/ui/panels/multi-selection-panel.tsx +120 -3
  128. package/src/components/ui/panels/panel-wrapper.tsx +53 -27
  129. package/src/components/ui/panels/parametric-field-control.tsx +2 -2
  130. package/src/components/ui/panels/parametric-inspector.tsx +3 -3
  131. package/src/components/ui/panels/room-panel.tsx +65 -0
  132. package/src/components/ui/primitives/badge.tsx +8 -0
  133. package/src/components/ui/primitives/sidebar.tsx +1 -1
  134. package/src/components/ui/right-stack.tsx +21 -2
  135. package/src/components/ui/sidebar/app-sidebar.tsx +1 -1
  136. package/src/components/ui/sidebar/icon-rail.tsx +3 -3
  137. package/src/components/ui/sidebar/panels/items-panel/function-tree-panel.tsx +1 -1
  138. package/src/components/ui/sidebar/panels/items-panel/index.tsx +6 -6
  139. package/src/components/ui/sidebar/panels/settings-panel/animation-speed-setting.tsx +40 -0
  140. package/src/components/ui/sidebar/panels/settings-panel/audio-settings-dialog.tsx +7 -8
  141. package/src/components/ui/sidebar/panels/settings-panel/construction-animation-setting.test.tsx +170 -0
  142. package/src/components/ui/sidebar/panels/settings-panel/construction-animation-setting.tsx +92 -0
  143. package/src/components/ui/sidebar/panels/settings-panel/index.tsx +107 -23
  144. package/src/components/ui/sidebar/panels/settings-panel/interface-theme.test.tsx +20 -0
  145. package/src/components/ui/sidebar/panels/settings-panel/print-export-button.tsx +4 -10
  146. package/src/components/ui/sidebar/panels/site-panel/building-tree-node.tsx +3 -11
  147. package/src/components/ui/sidebar/panels/site-panel/index.tsx +5 -12
  148. package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +13 -10
  149. package/src/components/ui/sidebar/panels/zone-panel/index.tsx +2 -2
  150. package/src/components/ui/sidebar/tab-bar.tsx +4 -4
  151. package/src/components/viewer/viewer-controls-bar.tsx +18 -6
  152. package/src/components/viewer/viewer-rooms.tsx +16 -0
  153. package/src/hooks/use-auto-save.integration.test.tsx +96 -38
  154. package/src/hooks/use-auto-save.ts +26 -1
  155. package/src/hooks/use-ceiling-events.test.ts +123 -118
  156. package/src/hooks/use-grid-events.test.ts +105 -105
  157. package/src/hooks/use-keyboard.test.ts +65 -16
  158. package/src/hooks/use-keyboard.ts +29 -12
  159. package/src/hooks/use-placement-notice.test.ts +24 -1
  160. package/src/hooks/use-placement-notice.ts +37 -6
  161. package/src/index.tsx +71 -24
  162. package/src/lib/__fixtures__/owner-victor-seattle-parking.json +1 -0
  163. package/src/lib/__fixtures__/owner-villa-aurelia-basement.json +1 -0
  164. package/src/lib/add-storey.test.ts +177 -0
  165. package/src/lib/add-storey.ts +59 -0
  166. package/src/lib/below-grade.test.tsx +171 -0
  167. package/src/lib/below-grade.ts +133 -0
  168. package/src/lib/camera-facing.test.ts +24 -0
  169. package/src/lib/camera-facing.ts +24 -0
  170. package/src/lib/direct-manipulation.ts +2 -0
  171. package/src/lib/download-project-file.ts +17 -0
  172. package/src/lib/editor-level-display.ts +9 -2
  173. package/src/lib/editor-views.ts +35 -1
  174. package/src/lib/export-texture-utils.ts +2 -4
  175. package/src/lib/floor-foundation-support.test.ts +1 -0
  176. package/src/lib/floorplan/floorplan-pdfkit-document.ts +31 -16
  177. package/src/lib/floorplan/floorplan-pdfkit-renderer.test.ts +39 -0
  178. package/src/lib/floorplan/floorplan-pdfkit-renderer.ts +4 -0
  179. package/src/lib/floorplan/pdfkit-standard-fonts.d.ts +5 -0
  180. package/src/lib/floorplan/sheet-export.test.ts +24 -0
  181. package/src/lib/floorplan/site-plan/build-site-plan-drawing.ts +34 -45
  182. package/src/lib/floorplan/site-plan/coverage.ts +2 -4
  183. package/src/lib/floorplan/site-plan/front-edge.ts +2 -1
  184. package/src/lib/floorplan/site-plan/geometry.test.ts +9 -15
  185. package/src/lib/floorplan/site-plan/geometry.ts +1 -416
  186. package/src/lib/floorplan/site-plan/index.ts +0 -16
  187. package/src/lib/floorplan/site-plan/site-annotations.test.ts +117 -0
  188. package/src/lib/floorplan/site-plan/site-annotations.ts +3 -2
  189. package/src/lib/floorplan/site-plan/site-parts.ts +14 -106
  190. package/src/lib/floorplan/site-plan/yards.test.ts +2 -1
  191. package/src/lib/follow-pascal-exterior.test-fixture.ts +17 -0
  192. package/src/lib/follow-pascal-exterior.test.ts +171 -0
  193. package/src/lib/follow-pascal-exterior.ts +39 -0
  194. package/src/lib/follow-pascal-presentation.test.ts +203 -0
  195. package/src/lib/follow-pascal-presentation.ts +129 -0
  196. package/src/lib/follow-pascal-sight.ts +84 -0
  197. package/src/lib/follow-pascal-targets.test.ts +406 -0
  198. package/src/lib/follow-pascal-targets.ts +198 -0
  199. package/src/lib/follow-pascal.test.ts +350 -0
  200. package/src/lib/follow-pascal.ts +284 -0
  201. package/src/lib/fresh-planar-placement.test.ts +112 -0
  202. package/src/lib/fresh-planar-placement.ts +57 -3
  203. package/src/lib/geometry-script/author.ts +6 -1
  204. package/src/lib/geometry-script/client.ts +8 -1
  205. package/src/lib/geometry-script/finish-scripts.ts +127 -0
  206. package/src/lib/geometry-script/protocol.ts +1 -0
  207. package/src/lib/glb-export.test.ts +187 -162
  208. package/src/lib/glb-export.ts +4 -1
  209. package/src/lib/history.test.ts +12 -7
  210. package/src/lib/interaction/scope.ts +4 -0
  211. package/src/lib/level-selection.test.ts +57 -0
  212. package/src/lib/level-selection.ts +16 -1
  213. package/src/lib/lot/clean-ring.test.ts +2 -1
  214. package/src/lib/lot/drop-in.ts +9 -24
  215. package/src/lib/lot/lot-patch.test.ts +1 -1
  216. package/src/lib/lot/lot-patch.ts +2 -2
  217. package/src/lib/measurement-kind.test.ts +6 -8
  218. package/src/lib/measurements.test.ts +23 -0
  219. package/src/lib/orbit-floor-clearance.test.ts +36 -0
  220. package/src/lib/orbit-floor-clearance.ts +48 -0
  221. package/src/lib/paint-pass-through.test.ts +11 -0
  222. package/src/lib/paint-pass-through.ts +4 -2
  223. package/src/lib/paint-scope.ts +1 -0
  224. package/src/lib/parametric-node-update.ts +16 -7
  225. package/src/lib/plugin-chat.ts +15 -0
  226. package/src/lib/point-ask/__fixtures__/consumer-browser.tsx +76 -0
  227. package/src/lib/point-ask/__fixtures__/house.ts +81 -0
  228. package/src/lib/point-ask/ask-created.test.ts +195 -0
  229. package/src/lib/point-ask/ask-flow.test.ts +380 -0
  230. package/src/lib/point-ask/build-context.test.ts +173 -0
  231. package/src/lib/point-ask/build-context.ts +94 -0
  232. package/src/lib/point-ask/capture-crop.ts +129 -0
  233. package/src/lib/point-ask/choreography.ts +69 -0
  234. package/src/lib/point-ask/compare.test.ts +145 -0
  235. package/src/lib/point-ask/compare.ts +73 -0
  236. package/src/lib/point-ask/describe-target.test.ts +165 -0
  237. package/src/lib/point-ask/describe-target.ts +234 -0
  238. package/src/lib/point-ask/dictation.ts +79 -0
  239. package/src/lib/point-ask/edge-marker.test.ts +61 -0
  240. package/src/lib/point-ask/edge-marker.ts +27 -0
  241. package/src/lib/point-ask/frame-loop.ts +25 -0
  242. package/src/lib/point-ask/history-marks.test.ts +94 -0
  243. package/src/lib/point-ask/history-marks.ts +39 -0
  244. package/src/lib/point-ask/host-api.ts +32 -0
  245. package/src/lib/point-ask/outline-box.test.ts +30 -0
  246. package/src/lib/point-ask/outline-box.ts +11 -0
  247. package/src/lib/point-ask/overlay-metrics.ts +6 -0
  248. package/src/lib/point-ask/pick.test.ts +220 -0
  249. package/src/lib/point-ask/pick.ts +121 -0
  250. package/src/lib/point-ask/pin-machine.test.ts +494 -0
  251. package/src/lib/point-ask/pin-machine.ts +217 -0
  252. package/src/lib/point-ask/placement.test.ts +203 -0
  253. package/src/lib/point-ask/placement.ts +141 -0
  254. package/src/lib/point-ask/pointer-tracker.ts +34 -0
  255. package/src/lib/point-ask/projector.ts +83 -0
  256. package/src/lib/point-ask/region.test.ts +157 -0
  257. package/src/lib/point-ask/region.ts +136 -0
  258. package/src/lib/point-ask/retry.test.ts +217 -0
  259. package/src/lib/point-ask/retry.ts +37 -0
  260. package/src/lib/point-ask/scene-commit.test.ts +312 -0
  261. package/src/lib/point-ask/scene-commit.ts +32 -0
  262. package/src/lib/point-ask/session.ts +1391 -0
  263. package/src/lib/point-ask/spring-keyframes.test.ts +45 -0
  264. package/src/lib/point-ask/spring-keyframes.ts +38 -0
  265. package/src/lib/point-ask/spring.test.ts +63 -0
  266. package/src/lib/point-ask/spring.ts +107 -0
  267. package/src/lib/point-ask/suggestions.test.ts +80 -0
  268. package/src/lib/point-ask/suggestions.ts +125 -0
  269. package/src/lib/point-ask/target-scope.test.ts +622 -0
  270. package/src/lib/point-ask/target-scope.ts +338 -0
  271. package/src/lib/point-ask/types.ts +48 -0
  272. package/src/lib/portable-export.test.ts +34 -1
  273. package/src/lib/portable-export.ts +119 -53
  274. package/src/lib/print-feature-thickness.ts +10 -1
  275. package/src/lib/print-roof-solids.test.ts +39 -1
  276. package/src/lib/print-roof-solids.ts +43 -10
  277. package/src/lib/print-shell-compiler-manifold-core.ts +1 -1
  278. package/src/lib/room-assembly-overlay.ts +2 -3
  279. package/src/lib/room-selection.test.ts +45 -1
  280. package/src/lib/scene-clipboard.test.ts +246 -1
  281. package/src/lib/scene-clipboard.ts +44 -14
  282. package/src/lib/selection-routing.ts +1 -1
  283. package/src/lib/sfx-bus.ts +13 -0
  284. package/src/lib/sfx-player.test.ts +70 -9
  285. package/src/lib/sfx-player.ts +6 -0
  286. package/src/lib/stair-levels.test.ts +25 -0
  287. package/src/lib/stair-levels.ts +22 -3
  288. package/src/lib/terrace-draft.ts +2 -1
  289. package/src/lib/viewer-selection.ts +22 -11
  290. package/src/lib/wall-placement-pointer.test.ts +42 -0
  291. package/src/lib/wall-placement-pointer.ts +57 -0
  292. package/src/lib/window-changes.ts +35 -0
  293. package/src/store/use-audio.test.ts +146 -0
  294. package/src/store/use-audio.tsx +127 -3
  295. package/src/store/use-construction-reveal.test.ts +173 -0
  296. package/src/store/use-construction-reveal.ts +145 -0
  297. package/src/store/use-corner-snap-hint.test.ts +24 -0
  298. package/src/store/use-corner-snap-hint.ts +47 -0
  299. package/src/store/use-follow-pascal.test.ts +62 -0
  300. package/src/store/use-follow-pascal.ts +53 -0
  301. package/src/store/use-point-ask.ts +247 -0
  302. package/src/store/use-stair-build-preview.ts +25 -5
  303. package/src/store/view-layout.test.ts +27 -1
  304. package/src/store/view-layout.ts +4 -1
  305. package/src/test-utils/render-current-markup.tsx +16 -0
  306. package/src/components/tools/stair/stair-click-guard.test.ts +0 -58
  307. package/src/components/tools/stair/stair-click-guard.ts +0 -72
  308. package/src/components/tools/stair/stair-defaults.ts +0 -15
  309. package/src/components/tools/stair/stair-tool.tsx +0 -682
@@ -0,0 +1,129 @@
1
+ import type { WallMode } from '@pascal-app/viewer'
2
+ import type { RoomKey } from './room-selection'
3
+
4
+ /**
5
+ * Follow Pascal changes how the building is shown so the build it follows is in view: the walls cut
6
+ * away when no eye sees its target, the floor it works on selected, the floors above it hidden. This
7
+ * keeps what it changed, and what the person had before, so that when Follow ends it puts back exactly
8
+ * that: a setting it changed and the person has not touched since goes back to what the person had.
9
+ *
10
+ * A setting the person changes while Follow is on is theirs: it is not put back, and Follow does not
11
+ * change it again until it has ended. Everything here is arithmetic on a port, so it is tested without
12
+ * a viewer; the component that owns the port is `FollowPascal`.
13
+ */
14
+
15
+ export type LevelMode = 'stacked' | 'exploded' | 'solo' | 'manual'
16
+
17
+ /** The floor as the person has it: the selection and the room picked in the editor. */
18
+ export type Floor = {
19
+ buildingId: string | null
20
+ levelId: string | null
21
+ zoneId: string | null
22
+ selectedIds: readonly string[]
23
+ room: RoomKey | null
24
+ }
25
+
26
+ export type ViewPresentation = {
27
+ wall: WallMode
28
+ levels: LevelMode
29
+ /** The upper floors are hidden, above the selected one. */
30
+ hideAbove: boolean
31
+ floor: Floor
32
+ }
33
+
34
+ /** What it needs of the viewer and the editor: read the view, and write one part of it. */
35
+ export type PresentationPort = {
36
+ read(): ViewPresentation
37
+ setWall(mode: WallMode): void
38
+ setLevels(mode: LevelMode): void
39
+ setHideAbove(hide: boolean): void
40
+ /** Selects a floor in its building. The room and what was picked on the floor before are dropped. */
41
+ setFloor(floor: { buildingId: string | null; levelId: string }): void
42
+ /** Puts a floor back as the person had it; what is no longer in the scene is dropped. */
43
+ putFloor(floor: Floor): void
44
+ }
45
+
46
+ type Part = keyof ViewPresentation
47
+
48
+ function sameFloor(a: Floor, b: Floor): boolean {
49
+ return (
50
+ a.buildingId === b.buildingId &&
51
+ a.levelId === b.levelId &&
52
+ a.zoneId === b.zoneId &&
53
+ a.selectedIds.length === b.selectedIds.length &&
54
+ a.selectedIds.every((id, index) => id === b.selectedIds[index]) &&
55
+ (a.room === b.room ||
56
+ (!!a.room &&
57
+ !!b.room &&
58
+ a.room.levelId === b.room.levelId &&
59
+ a.room.zoneId === b.room.zoneId))
60
+ )
61
+ }
62
+
63
+ function same<K extends Part>(part: K, a: ViewPresentation[K], b: ViewPresentation[K]): boolean {
64
+ return part === 'floor' ? sameFloor(a as Floor, b as Floor) : a === b
65
+ }
66
+
67
+ export class FollowPresentation {
68
+ /** For each part Follow changed: what the person had, and what Follow left it as. */
69
+ private held: { [K in Part]?: { before: ViewPresentation[K]; set: ViewPresentation[K] } } = {}
70
+ /** Parts the person changed after Follow did: theirs until Follow ends. */
71
+ private theirs = new Set<Part>()
72
+
73
+ constructor(private port: PresentationPort) {}
74
+
75
+ /** The walls as `mode` shows them (cutaway, when no eye sees the target). */
76
+ wall(mode: WallMode): void {
77
+ this.change('wall', mode === this.port.read().wall, () => this.port.setWall(mode))
78
+ }
79
+
80
+ levels(mode: LevelMode): void {
81
+ this.change('levels', mode === this.port.read().levels, () => this.port.setLevels(mode))
82
+ }
83
+
84
+ /** The floors above the selected one hidden, or shown. */
85
+ hideAbove(hide: boolean): void {
86
+ this.change('hideAbove', hide === this.port.read().hideAbove, () =>
87
+ this.port.setHideAbove(hide),
88
+ )
89
+ }
90
+
91
+ /** The floor a build works on, selected. Nothing changes when it is the selected one. */
92
+ floor(target: { buildingId: string | null; levelId: string }): void {
93
+ this.change('floor', this.port.read().floor.levelId === target.levelId, () =>
94
+ this.port.setFloor(target),
95
+ )
96
+ }
97
+
98
+ /**
99
+ * Follow ends: each part it changed goes back to what the person had, unless they have changed it
100
+ * since. A floor goes back only when the selection is as Follow left it: a room or an element
101
+ * picked on the floor is the person's choice of that floor.
102
+ */
103
+ restore(): void {
104
+ const now = this.port.read()
105
+ const { wall, levels, hideAbove, floor } = this.held
106
+ this.held = {}
107
+ this.theirs.clear()
108
+ if (wall && now.wall === wall.set) this.port.setWall(wall.before)
109
+ if (levels && now.levels === levels.set) this.port.setLevels(levels.before)
110
+ if (hideAbove && now.hideAbove === hideAbove.set) this.port.setHideAbove(hideAbove.before)
111
+ if (floor && sameFloor(now.floor, floor.set)) this.port.putFloor(floor.before)
112
+ }
113
+
114
+ private change<K extends Part>(part: K, already: boolean, write: () => void): void {
115
+ if (this.theirs.has(part)) return
116
+ const held = this.held[part]
117
+ const now = this.port.read()[part]
118
+ if (held && !same(part, now, held.set as ViewPresentation[K])) {
119
+ // Changed since Follow wrote it: the person's, and not Follow's to put back or to write over.
120
+ delete this.held[part]
121
+ this.theirs.add(part)
122
+ return
123
+ }
124
+ if (already) return
125
+ write()
126
+ const set = this.port.read()[part]
127
+ ;(this.held as Record<Part, unknown>)[part] = { before: held ? held.before : now, set }
128
+ }
129
+ }
@@ -0,0 +1,84 @@
1
+ import {
2
+ type AnyNodeId,
3
+ type CameraPose,
4
+ findLevelAncestorId,
5
+ sceneRegistry,
6
+ useScene,
7
+ } from '@pascal-app/core'
8
+ import { BATCHED_LAYER, SCENE_LAYER, setSurfaceRaycastLayers, useViewer } from '@pascal-app/viewer'
9
+ import { type Intersection, type Mesh, type Object3D, Raycaster, Vector3 } from 'three'
10
+ import type { FollowWant } from './follow-pascal'
11
+
12
+ const DRAWN_LAYERS = (1 << SCENE_LAYER) | (1 << BATCHED_LAYER)
13
+
14
+ /** Query modeled surfaces, including batch sources, but not editor overlays or shadow-only floors. */
15
+ export class FollowSight {
16
+ private readonly ray = new Raycaster()
17
+ private readonly eye = new Vector3()
18
+ private readonly direction = new Vector3()
19
+ private readonly hits: Intersection[] = []
20
+ private meshes: Mesh[] = []
21
+
22
+ constructor(private scene: Object3D) {
23
+ setSurfaceRaycastLayers(this.ray.layers)
24
+ }
25
+
26
+ refresh(want: FollowWant): void {
27
+ this.meshes = []
28
+ const root = this.scene.getObjectByName('scene-renderer')
29
+ if (!root) return
30
+ const targets = new Set(
31
+ want.nodeIds?.flatMap((id) => {
32
+ const object = sceneRegistry.nodes.get(id)
33
+ return object ? [object] : []
34
+ }),
35
+ )
36
+ const owners = new Map([...sceneRegistry.nodes].map(([id, object]) => [object, id]))
37
+ const { nodes } = useScene.getState()
38
+ const viewer = useViewer.getState()
39
+ const selected = viewer.selection.levelId ? nodes[viewer.selection.levelId] : undefined
40
+ root.updateWorldMatrix(true, true)
41
+ root.traverseVisible((object) => {
42
+ const mesh = object as Mesh
43
+ if (!mesh.isMesh || (mesh.layers.mask & DRAWN_LAYERS) === 0) return
44
+ for (let ancestor: Object3D | null = object; ancestor; ancestor = ancestor.parent) {
45
+ if (targets.has(ancestor)) return
46
+ const id = owners.get(ancestor)
47
+ if (!id) continue
48
+ if (nodes[id as AnyNodeId]?.visible === false) return
49
+ // A batched source can retain its batch layer while its floor is shadow-only.
50
+ if (
51
+ mesh.layers.isEnabled(BATCHED_LAYER) &&
52
+ viewer.hideLevelsAboveSelection &&
53
+ selected?.type === 'level'
54
+ ) {
55
+ const levelId = findLevelAncestorId(id as AnyNodeId, nodes)
56
+ const level = levelId ? nodes[levelId as AnyNodeId] : undefined
57
+ if (
58
+ level?.type === 'level' &&
59
+ level.level > selected.level &&
60
+ !viewer.levelsRevealing.includes(level.id)
61
+ )
62
+ return
63
+ }
64
+ }
65
+ this.meshes.push(mesh)
66
+ })
67
+ }
68
+
69
+ clear(pose: CameraPose): boolean {
70
+ this.eye.set(...pose.position)
71
+ this.direction.set(...pose.target).sub(this.eye)
72
+ this.ray.far = this.direction.length() - 0.02
73
+ this.ray.set(this.eye, this.direction.normalize())
74
+ this.hits.length = 0
75
+ this.ray.intersectObjects(this.meshes, false, this.hits)
76
+ return !this.hits.some((hit) => {
77
+ const mesh = hit.object as Mesh
78
+ const material = Array.isArray(mesh.material)
79
+ ? mesh.material[hit.face?.materialIndex ?? 0]
80
+ : mesh.material
81
+ return material?.visible && material.opacity > 0.1 && material.colorWrite
82
+ })
83
+ }
84
+ }
@@ -0,0 +1,406 @@
1
+ import { afterAll, beforeAll, describe, expect, test } from 'bun:test'
2
+ import {
3
+ type AnyNode,
4
+ type AnyNodeDefinition,
5
+ BuildingNode,
6
+ DoorNode,
7
+ ItemNode,
8
+ LevelNode,
9
+ loadPlugin,
10
+ nodeRegistry,
11
+ RoofNode,
12
+ RoofSegmentNode,
13
+ WallNode,
14
+ } from '@pascal-app/core'
15
+ import type { RevealEvent } from '@pascal-app/viewer'
16
+ import { openingExterior, wallExterior } from './follow-pascal-exterior.test-fixture'
17
+ import { stayOnLevel, wantFromEvent } from './follow-pascal-targets'
18
+
19
+ // What the camera looks at for each thing the reveal says: a floor when its walls rise, a window when
20
+ // it is cut in, an item when it drops, the roof from above as it assembles, and the whole house when
21
+ // the build is finished.
22
+
23
+ function house(at: [number, number, number] = [0, 0, 0]) {
24
+ const building = BuildingNode.parse({ id: 'building_h', position: at })
25
+ const level = LevelNode.parse({ id: 'level_h', parentId: building.id, level: 0, height: 2.7 })
26
+ const roofLevel = LevelNode.parse({ id: 'level_roof', parentId: building.id, level: 1 })
27
+ const corners: [number, number][] = [
28
+ [0, 0],
29
+ [10, 0],
30
+ [10, 8],
31
+ [0, 8],
32
+ ]
33
+ const walls = corners.map((start, index) =>
34
+ WallNode.parse({
35
+ id: `wall_${index}`,
36
+ parentId: level.id,
37
+ start,
38
+ end: corners[(index + 1) % 4]!,
39
+ frontSide: 'interior',
40
+ backSide: 'exterior',
41
+ height: 2.7,
42
+ }),
43
+ )
44
+ const door = DoorNode.parse({
45
+ id: 'door_h',
46
+ parentId: walls[0]!.id,
47
+ wallId: walls[0]!.id,
48
+ position: [3, 1.05, 0],
49
+ width: 0.9,
50
+ height: 2.1,
51
+ })
52
+ const item = ItemNode.parse({
53
+ id: 'item_h',
54
+ parentId: level.id,
55
+ position: [4, 0, 3],
56
+ asset: {
57
+ id: 'chair',
58
+ category: 'furniture',
59
+ name: 'Chair',
60
+ thumbnail: '',
61
+ src: '/items/chair/model.glb',
62
+ dimensions: [0.6, 0.9, 0.6],
63
+ },
64
+ })
65
+ const segment = RoofSegmentNode.parse({
66
+ id: 'rseg_h',
67
+ parentId: 'roof_h',
68
+ roofType: 'gable',
69
+ width: 10,
70
+ depth: 8,
71
+ pitch: 30,
72
+ wallHeight: 0,
73
+ })
74
+ const roof = RoofNode.parse({
75
+ id: 'roof_h',
76
+ parentId: roofLevel.id,
77
+ position: [5, 0, 4],
78
+ children: [segment.id],
79
+ })
80
+ const list = [building, level, roofLevel, ...walls, door, item, roof, segment] as AnyNode[]
81
+ const nodes = Object.fromEntries(list.map((node) => [node.id, { ...node }])) as Record<
82
+ string,
83
+ any
84
+ >
85
+ nodes.building_h.children = [level.id, roofLevel.id]
86
+ nodes.level_h.children = [...walls.map((wall) => wall.id), item.id]
87
+ nodes.level_roof.children = [roof.id]
88
+ nodes.wall_0.children = [door.id]
89
+ return nodes as Record<string, AnyNode>
90
+ }
91
+
92
+ const group = (
93
+ type: 'start' | 'land' | 'settle',
94
+ phase: string,
95
+ ids: string[],
96
+ levelId: string | null = 'level_h',
97
+ ) =>
98
+ ({ type, phase, levelId, nodeIds: ids, source: 'agent', instant: false, atMs: 0 }) as RevealEvent
99
+ const started = (id: string, phase: string, style = 'drop') =>
100
+ ({
101
+ type: 'node-start',
102
+ id,
103
+ phase,
104
+ levelId: 'level_h',
105
+ style,
106
+ source: 'agent',
107
+ atMs: 0,
108
+ }) as RevealEvent
109
+
110
+ // The roof declares that it lifts for the furnishing; the kinds are the nodes package's, so a
111
+ // fixture of the same kind stands in for them here.
112
+ let restoreRegistry: () => void = () => {}
113
+ beforeAll(async () => {
114
+ restoreRegistry = nodeRegistry._snapshot()
115
+ nodeRegistry._reset()
116
+ await loadPlugin({
117
+ id: 'fixture:follow',
118
+ apiVersion: 1,
119
+ nodes: [
120
+ {
121
+ kind: 'roof',
122
+ schemaVersion: 1,
123
+ schema: RoofNode,
124
+ category: 'structure',
125
+ defaults: () => ({}),
126
+ capabilities: {
127
+ reveal: {
128
+ phase: 'roof',
129
+ style: 'assemble',
130
+ height: 4,
131
+ clears: { for: 'furnishing', height: 2.4 },
132
+ },
133
+ exterior: true,
134
+ },
135
+ geometry: () => null,
136
+ } as unknown as AnyNodeDefinition,
137
+ {
138
+ kind: 'wall',
139
+ schemaVersion: 1,
140
+ schema: WallNode,
141
+ category: 'structure',
142
+ defaults: () => ({}),
143
+ capabilities: {
144
+ reveal: { phase: 'structure', style: 'rise' },
145
+ revealOnEdit: ['start', 'end', 'curveOffset'],
146
+ exterior: wallExterior,
147
+ },
148
+ geometry: () => null,
149
+ } as unknown as AnyNodeDefinition,
150
+ {
151
+ kind: 'door',
152
+ schemaVersion: 1,
153
+ schema: DoorNode,
154
+ category: 'structure',
155
+ defaults: () => ({}),
156
+ capabilities: {
157
+ reveal: { phase: 'openings', style: 'cut' },
158
+ revealOnEdit: ['position'],
159
+ exterior: openingExterior,
160
+ },
161
+ geometry: () => null,
162
+ } as unknown as AnyNodeDefinition,
163
+ ],
164
+ })
165
+ })
166
+ afterAll(() => restoreRegistry())
167
+
168
+ describe('what the camera looks at', () => {
169
+ test('a floor, whole, when its slabs and walls start', () => {
170
+ for (const phase of ['foundation', 'structure', 'circulation']) {
171
+ const want = wantFromEvent(group('start', phase, ['wall_0']), house(), 1)!
172
+ expect(want.kind).toBe('floor')
173
+ expect(want.box.min[0]).toBeLessThanOrEqual(0)
174
+ expect(want.box.max[0]).toBeGreaterThanOrEqual(10)
175
+ expect(want.box.max[2]).toBeGreaterThanOrEqual(8)
176
+ expect(want.at).toBe(1)
177
+ }
178
+ })
179
+
180
+ test('a wall the agent edited is looked at, close up: it is not a floor being built', () => {
181
+ const nodes = house()
182
+ const edited = { ...started('wall_0', 'structure', 'rise'), changed: true } as RevealEvent
183
+ const want = wantFromEvent(edited, nodes, 0)
184
+ expect(want?.kind).toBe('detail')
185
+ expect(want?.levelId).toBe('level_h')
186
+ })
187
+
188
+ test('the walls themselves are not targets: the floor already is', () => {
189
+ expect(wantFromEvent(started('wall_1', 'structure', 'rise'), house(), 0)).toBeNull()
190
+ })
191
+
192
+ test('a window or a door close up, square to the wall it is in, from outside', () => {
193
+ const want = wantFromEvent(started('door_h', 'openings', 'cut'), house(), 2)!
194
+ expect(want.kind).toBe('detail')
195
+ expect(want.box.max[1] - want.box.min[1]).toBeLessThan(2.5)
196
+ expect(want.normal).toEqual([0, -1])
197
+ })
198
+
199
+ test('an item as furniture, from above', () => {
200
+ const want = wantFromEvent(started('item_h', 'furnishing'), house(), 3)!
201
+ expect(want.kind).toBe('furnish')
202
+ expect(want.normal).toBeUndefined()
203
+ })
204
+
205
+ test('furniture is seen from above the roof that lifts for it', () => {
206
+ const want = wantFromEvent(started('item_h', 'furnishing'), house(), 3)!
207
+ // The roof's top is above 2.7 m; it lifts 2.4 m for the furniture, and the eye clears that.
208
+ expect(want.eyeAbove).toBeGreaterThan(2.7 + 2.4)
209
+ // A house with no roof has nothing to clear.
210
+ const bare = house()
211
+ delete (bare as Record<string, unknown>).roof_h
212
+ delete (bare as Record<string, unknown>).rseg_h
213
+ expect(wantFromEvent(started('item_h', 'furnishing'), bare, 3)!.eyeAbove).toBeUndefined()
214
+ })
215
+
216
+ test('the whole house once the furniture has landed, before the roof comes back down', () => {
217
+ const want = wantFromEvent(group('settle', 'furnishing', ['item_h']), house(), 6)!
218
+ expect(want.kind).toBe('group')
219
+ expect(want.box.max[1]).toBeGreaterThan(3)
220
+ expect(want.box.max[0]).toBeGreaterThanOrEqual(10)
221
+ // Still looked down through the lifted roof, not from within it.
222
+ expect(want.eyeAbove).toBeGreaterThan(2.7 + 2.4)
223
+ // The walls settling say nothing about where to look.
224
+ expect(wantFromEvent(group('settle', 'structure', ['wall_0']), house(), 6)).toBeNull()
225
+ })
226
+
227
+ test('the whole house, for a person who stays on one level: that level whole, with no roof to clear and no floor of its own to change', () => {
228
+ const nodes = house()
229
+ const whole = wantFromEvent(group('settle', 'furnishing', ['item_h']), nodes, 6)!
230
+ expect(whole.levelId).toBeUndefined()
231
+ const stay = stayOnLevel(whole, nodes, 'level_h')
232
+ expect(stay.kind).toBe('floor')
233
+ expect(stay.levelId).toBe('level_h')
234
+ expect(stay.eyeAbove).toBeUndefined()
235
+ expect(stay.at).toBe(6)
236
+ // The level's own box, not the house's: the roof level over it is not in it.
237
+ expect(stay.box.max[1]).toBeLessThan(whole.box.max[1])
238
+ // A level with nothing to frame still stays where it is.
239
+ expect(stayOnLevel(whole, nodes, 'level_nowhere').levelId).toBe('level_nowhere')
240
+ })
241
+
242
+ test('a piece the agent moved is looked at like one it placed, and its settling does not pull back to the whole house', () => {
243
+ const moved = { ...started('item_h', 'furnishing', 'glide'), changed: true } as RevealEvent
244
+ const want = wantFromEvent(moved, house(), 3)!
245
+ expect(want.kind).toBe('furnish')
246
+ expect(want.eyeAbove).toBeGreaterThan(2.7 + 2.4)
247
+ // The build around it is not finishing: the camera stays where the piece is.
248
+ const settled = { ...group('settle', 'furnishing', ['item_h']), changed: true } as RevealEvent
249
+ expect(wantFromEvent(settled, house(), 6)).toBeNull()
250
+ })
251
+
252
+ test('the roof, from the group that assembles it', () => {
253
+ const want = wantFromEvent(
254
+ group('start', 'roof', ['roof_h', 'rseg_h'], 'level_roof'),
255
+ house(),
256
+ 4,
257
+ )!
258
+ expect(want.kind).toBe('roof')
259
+ expect(want.box.max[1]).toBeGreaterThan(2.7)
260
+ })
261
+
262
+ test('where the building stands, not at the origin', () => {
263
+ const want = wantFromEvent(started('door_h', 'openings', 'cut'), house([20, 0, 30]), 0)!
264
+ expect(want.box.min[0]).toBeGreaterThan(20)
265
+ expect(want.box.min[2]).toBeGreaterThan(29)
266
+ })
267
+
268
+ test('the whole house, roof and all, when the build is finished', () => {
269
+ const want = wantFromEvent(
270
+ {
271
+ type: 'finale',
272
+ id: 'roof_h',
273
+ phase: 'roof',
274
+ levelId: 'level_roof',
275
+ source: 'agent',
276
+ atMs: 0,
277
+ } as RevealEvent,
278
+ house(),
279
+ 5,
280
+ )!
281
+ expect(want.kind).toBe('group')
282
+ expect(want.box.max[1]).toBeGreaterThan(3)
283
+ expect(want.box.max[0]).toBeGreaterThanOrEqual(10)
284
+ })
285
+
286
+ test('an undo is followed: what goes first is looked at, group by group', () => {
287
+ const undone = (phase: string, nodeIds: string[], levelId: string | null = 'level_h') =>
288
+ ({
289
+ type: 'retract-start',
290
+ phase,
291
+ levelId,
292
+ nodeIds,
293
+ source: null,
294
+ atMs: 0,
295
+ }) as RevealEvent
296
+ const furniture = wantFromEvent(undone('furnishing', ['item_h']), house(), 1)!
297
+ expect(furniture.kind).toBe('furnish')
298
+ // Seen from above the roof that is still up for it.
299
+ expect(furniture.eyeAbove).toBeGreaterThan(2.7 + 2.4)
300
+ const roof = wantFromEvent(undone('roof', ['roof_h', 'rseg_h'], 'level_roof'), house(), 2)!
301
+ expect(roof.kind).toBe('roof')
302
+ expect(roof.box.max[1]).toBeGreaterThan(2.7)
303
+ const openings = wantFromEvent(undone('openings', ['door_h']), house(), 3)!
304
+ expect(openings.kind).toBe('group')
305
+ expect(openings.box.max[1]).toBeLessThan(2.5)
306
+ // The walls going: the floor they stand on is what is looked at, by the walls themselves.
307
+ const walls = wantFromEvent(undone('structure', ['wall_0', 'wall_2']), house(), 4)!
308
+ expect(walls.kind).toBe('floor')
309
+ expect(walls.box.max[0]).toBeGreaterThanOrEqual(10)
310
+ // A group whose nodes are no longer in the scene says nothing about where to look.
311
+ expect(wantFromEvent(undone('structure', ['wall_gone']), house(), 5)).toBeNull()
312
+ // The end of a group says nothing.
313
+ expect(
314
+ wantFromEvent(
315
+ { ...undone('structure', ['wall_0']), type: 'retract-end' } as RevealEvent,
316
+ house(),
317
+ 6,
318
+ ),
319
+ ).toBeNull()
320
+ })
321
+
322
+ test('nothing to look at for what says nothing about the scene', () => {
323
+ expect(wantFromEvent(group('land', 'structure', ['wall_0']), house(), 0)).toBeNull()
324
+ expect(wantFromEvent(group('settle', 'structure', ['wall_0']), house(), 0)).toBeNull()
325
+ expect(
326
+ wantFromEvent(
327
+ {
328
+ type: 'complete',
329
+ nodeCount: 1,
330
+ durationMs: 1,
331
+ source: null,
332
+ instant: false,
333
+ atMs: 0,
334
+ } as RevealEvent,
335
+ house(),
336
+ 0,
337
+ ),
338
+ ).toBeNull()
339
+ // A node that is gone is no target either.
340
+ expect(wantFromEvent(started('item_gone', 'furnishing'), house(), 0)).toBeNull()
341
+ })
342
+ })
343
+
344
+ // The owner, 15 October: the cutaway hid the exterior wall the agent was bending. A change seen from
345
+ // outside is looked at from outdoors, with the walls up; the rest keeps the cutaway.
346
+ describe('what is seen from outside', () => {
347
+ const changed = (id: string, phase: string, style = 'rise') =>
348
+ ({ ...started(id, phase, style), changed: true }) as RevealEvent
349
+
350
+ test('an exterior wall the agent bent is an outside step, framed from its exterior face', () => {
351
+ const want = wantFromEvent(changed('wall_0', 'structure'), house(), 0)!
352
+ expect(want.exterior).toBe(true)
353
+ // wall_0 runs along +x with its exterior on the back side: it faces -z.
354
+ expect(want.normal).toEqual([0, -1])
355
+ })
356
+
357
+ test('a window moved on an exterior wall is an outside step; a chair moved is not', () => {
358
+ const nodes = house()
359
+ expect(wantFromEvent(changed('door_h', 'openings', 'cut'), nodes, 0)!.exterior).toBe(true)
360
+ expect(
361
+ wantFromEvent(changed('item_h', 'furnishing', 'drop'), nodes, 0)!.exterior,
362
+ ).toBeUndefined()
363
+ })
364
+
365
+ test('an interior wall the agent bent keeps the cutaway', () => {
366
+ const nodes = house() as Record<string, any>
367
+ nodes.wall_1 = { ...nodes.wall_1, frontSide: 'interior', backSide: 'interior' }
368
+ const want = wantFromEvent(changed('wall_1', 'structure'), nodes, 0)!
369
+ expect(want.exterior).toBeUndefined()
370
+ expect(want.normal).toBeUndefined()
371
+ })
372
+
373
+ test('an opening on an interior wall keeps the cutaway', () => {
374
+ const nodes = house() as Record<string, any>
375
+ nodes.wall_0 = { ...nodes.wall_0, frontSide: 'interior', backSide: 'interior' }
376
+ expect(wantFromEvent(changed('door_h', 'openings', 'cut'), nodes, 0)!.exterior).toBeUndefined()
377
+ })
378
+
379
+ // The owner, 15 Oct: the outside view is for edits and moves only; a build is followed as it was.
380
+ test('a build says nothing of outside: a roof assembling, an opening coming in, a floor of walls', () => {
381
+ const nodes = house()
382
+ for (const event of [
383
+ group('start', 'roof', ['roof_h'], 'level_roof'),
384
+ group('start', 'openings', ['door_h']),
385
+ group('start', 'structure', ['wall_0', 'wall_1']),
386
+ started('door_h', 'openings', 'cut'),
387
+ started('item_h', 'furnishing'),
388
+ { ...group('start', 'roof', ['roof_h'], 'level_roof'), type: 'retract-start' } as RevealEvent,
389
+ ])
390
+ expect(wantFromEvent(event, nodes, 0)?.exterior).toBeUndefined()
391
+ })
392
+
393
+ test('the same pieces edited or moved are outside steps', () => {
394
+ const nodes = house()
395
+ const edited = (id: string, phase: string) =>
396
+ ({ ...started(id, phase), changed: true }) as RevealEvent
397
+ expect(wantFromEvent(edited('door_h', 'openings'), nodes, 0)!.exterior).toBe(true)
398
+ expect(
399
+ wantFromEvent(
400
+ { ...group('start', 'roof', ['roof_h'], 'level_roof'), changed: true } as RevealEvent,
401
+ nodes,
402
+ 0,
403
+ )!.exterior,
404
+ ).toBe(true)
405
+ })
406
+ })