@pascal-app/editor 1.0.3 → 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.
Files changed (624) hide show
  1. package/README.md +106 -0
  2. package/package.json +42 -38
  3. package/src/components/editor/__bench__/room-label-placement.bench.ts +108 -0
  4. package/src/components/editor/action-menu-button.tsx +83 -0
  5. package/src/components/editor/bake-thumbnail.tsx +307 -5
  6. package/src/components/editor/capture-level-picker.tsx +72 -0
  7. package/src/components/editor/construction-reveal-audio.test.tsx +150 -0
  8. package/src/components/editor/construction-reveal-audio.tsx +40 -0
  9. package/src/components/editor/custom-camera-controls.tsx +275 -57
  10. package/src/components/editor/delete-confirmation-dialog.tsx +297 -7
  11. package/src/components/editor/duplicate-node.ts +244 -0
  12. package/src/components/editor/editor-layout-mobile.tsx +37 -26
  13. package/src/components/editor/editor-layout-v2.tsx +132 -59
  14. package/src/components/editor/export-manager.tsx +47 -26
  15. package/src/components/editor/first-person/build-collider-world.test.ts +320 -0
  16. package/src/components/editor/first-person/build-collider-world.ts +28 -7
  17. package/src/components/editor/first-person/ground-holes.test.ts +128 -0
  18. package/src/components/editor/first-person/ground-holes.ts +130 -0
  19. package/src/components/editor/first-person/mechanism-targets.test.ts +162 -0
  20. package/src/components/editor/first-person/mechanism-targets.ts +88 -0
  21. package/src/components/editor/first-person-controls.tsx +220 -9
  22. package/src/components/editor/floating-action-menu.tsx +63 -207
  23. package/src/components/editor/floor-aware-camera-controls.test.ts +466 -0
  24. package/src/components/editor/floor-aware-camera-controls.ts +324 -0
  25. package/src/components/editor/floor-openings-overlay.tsx +243 -0
  26. package/src/components/editor/floor-region-controls.tsx +846 -0
  27. package/src/components/editor/floor-region-webgpu-materials.test.ts +12 -0
  28. package/src/components/editor/floor-surface.test.ts +165 -0
  29. package/src/components/editor/floor-surface.ts +182 -0
  30. package/src/components/editor/floorplan-background-selection.test.ts +11 -2
  31. package/src/components/editor/floorplan-background-selection.ts +1 -3
  32. package/src/components/editor/floorplan-mode-coordinator.tsx +2 -2
  33. package/src/components/editor/floorplan-panel.tsx +414 -363
  34. package/src/components/editor/follow-pascal.test.tsx +912 -0
  35. package/src/components/editor/follow-pascal.tsx +354 -0
  36. package/src/components/editor/footprint-height-handle.tsx +204 -0
  37. package/src/components/editor/group-actions.ts +56 -2
  38. package/src/components/editor/group-move-3d.ts +2 -0
  39. package/src/components/editor/group-rotate-handle.tsx +2 -0
  40. package/src/components/editor/group-transform-shared.test.ts +460 -0
  41. package/src/components/editor/handles/handle-hit-priority.test.tsx +63 -0
  42. package/src/components/editor/handles/handle-hit-priority.tsx +37 -0
  43. package/src/components/editor/handles/swallow-next-click.test.ts +28 -0
  44. package/src/components/editor/handles/use-handle-drag.ts +269 -197
  45. package/src/components/editor/index.tsx +409 -197
  46. package/src/components/editor/join-walls-pill.tsx +44 -0
  47. package/src/components/editor/node-action-menu.tsx +68 -70
  48. package/src/components/editor/node-arrow-handles.tsx +25 -6
  49. package/src/components/editor/open-wall-ends-3d-layer.tsx +273 -0
  50. package/src/components/editor/open-wall-ends-hint.tsx +89 -0
  51. package/src/components/editor/quick-measurement-hud.tsx +3 -2
  52. package/src/components/editor/registry-action-contributions.test.tsx +63 -0
  53. package/src/components/editor/registry-action-contributions.tsx +80 -1
  54. package/src/components/editor/riser-diagram-panel.tsx +14 -7
  55. package/src/components/editor/room-controls.tsx +847 -0
  56. package/src/components/editor/room-floor-highlight.tsx +128 -0
  57. package/src/components/editor/room-handles-2d.tsx +354 -0
  58. package/src/components/editor/room-handles.tsx +675 -0
  59. package/src/components/editor/room-highlight.tsx +233 -0
  60. package/src/components/editor/room-labels.test.ts +850 -0
  61. package/src/components/editor/room-labels.tsx +1890 -0
  62. package/src/components/editor/room-transform-ghost.tsx +256 -0
  63. package/src/components/editor/room-webgpu-materials.test.ts +27 -0
  64. package/src/components/editor/selection-manager.tsx +474 -419
  65. package/src/components/editor/slab-hole-highlights.tsx +5 -2
  66. package/src/components/editor/snapshot-capture-overlay.tsx +7 -2
  67. package/src/components/editor/snapshot-capture.test.ts +119 -60
  68. package/src/components/editor/snapshot-capture.ts +36 -5
  69. package/src/components/editor/thumbnail-generator.test.ts +654 -0
  70. package/src/components/editor/thumbnail-generator.tsx +1103 -223
  71. package/src/components/editor/use-floorplan-background-placement.ts +34 -12
  72. package/src/components/editor/vector-edge-extractor.tsx +121 -0
  73. package/src/components/editor/views/host-view.tsx +61 -0
  74. package/src/components/editor/views/plugins-view.tsx +663 -0
  75. package/src/components/editor/views/scene-region.tsx +59 -0
  76. package/src/components/editor/views/use-editor-views.ts +57 -0
  77. package/src/components/editor/views/view-bar.test.tsx +54 -0
  78. package/src/components/editor/views/view-bar.tsx +343 -0
  79. package/src/components/editor/wall-justification.test.ts +140 -0
  80. package/src/components/editor/wall-move-side-handles.tsx +43 -142
  81. package/src/components/editor/wall-push-arrow.tsx +92 -0
  82. package/src/components/editor/wall-region-handles.tsx +509 -0
  83. package/src/components/editor/wall-snap-beacon-layer.tsx +8 -4
  84. package/src/components/editor-2d/floorplan-cursor-indicator-overlay.tsx +8 -4
  85. package/src/components/editor-2d/floorplan-group-move.tsx +3 -0
  86. package/src/components/editor-2d/floorplan-open-wall-ends-layer.test.tsx +86 -0
  87. package/src/components/editor-2d/floorplan-open-wall-ends-layer.tsx +290 -0
  88. package/src/components/editor-2d/floorplan-registered-tool-layer.tsx +2 -2
  89. package/src/components/editor-2d/floorplan-registry-action-menu.tsx +11 -1
  90. package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +116 -106
  91. package/src/components/editor-2d/renderers/floorplan-dimension-edit-overlay.test.ts +156 -0
  92. package/src/components/editor-2d/renderers/floorplan-dimension-edit-overlay.tsx +278 -0
  93. package/src/components/editor-2d/renderers/floorplan-dimension-renderer.tsx +77 -0
  94. package/src/components/editor-2d/renderers/{floorplan-draft-wall-measurement.tsx → floorplan-draft-measurement.tsx} +8 -8
  95. package/src/components/editor-2d/renderers/floorplan-geometry-renderer.tsx +4 -1
  96. package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +38 -8
  97. package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +319 -233
  98. package/src/components/editor-2d/renderers/floorplan-reveal.test.tsx +254 -0
  99. package/src/components/editor-2d/renderers/floorplan-reveal.ts +141 -0
  100. package/src/components/editor-2d/svg-paths.ts +12 -15
  101. package/src/components/point-ask/bubble.tsx +538 -0
  102. package/src/components/point-ask/hover-chip.tsx +91 -0
  103. package/src/components/point-ask/mode-pill.tsx +92 -0
  104. package/src/components/point-ask/optional-motion.ts +22 -0
  105. package/src/components/point-ask/outline-corners.tsx +93 -0
  106. package/src/components/point-ask/pins.tsx +711 -0
  107. package/src/components/point-ask/point-ask-layer.tsx +83 -0
  108. package/src/components/point-ask/point-ask-styles.ts +105 -0
  109. package/src/components/point-ask/region-marquee-styles.ts +15 -0
  110. package/src/components/point-ask/region-marquee.tsx +64 -0
  111. package/src/components/procedural-items/inspector.tsx +40 -15
  112. package/src/components/systems/ceiling/ceiling-bracket-batch.test.ts +12 -2
  113. package/src/components/systems/ceiling/ceiling-selection-affordance-system.test.ts +97 -14
  114. package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +18 -18
  115. package/src/components/systems/roof/roof-edit-system.tsx +1 -0
  116. package/src/components/systems/zone/zone-system.test.ts +47 -0
  117. package/src/components/systems/zone/zone-system.tsx +29 -68
  118. package/src/components/tools/distribution/connection-compatibility.test.ts +34 -0
  119. package/src/components/tools/distribution/connection-compatibility.ts +43 -0
  120. package/src/components/tools/distribution/distribution-public.ts +9 -0
  121. package/src/components/tools/distribution/distribution-run-contract.test.ts +26 -0
  122. package/src/components/tools/distribution/distribution-run-contract.ts +94 -0
  123. package/src/components/tools/distribution/distribution-run-tool.test.ts +241 -0
  124. package/src/components/tools/distribution/distribution-run-tool.test.tsx +125 -0
  125. package/src/components/tools/distribution/distribution-run-tool.tsx +1323 -0
  126. package/src/components/tools/distribution/draw-alignment.ts +35 -0
  127. package/src/components/tools/distribution/ports.test.ts +56 -0
  128. package/src/components/tools/distribution/ports.ts +378 -0
  129. package/src/components/tools/distribution/run-cursor.test.ts +135 -0
  130. package/src/components/tools/distribution/run-cursor.ts +36 -0
  131. package/src/components/tools/distribution/run-direction-feedback.test.ts +60 -0
  132. package/src/components/tools/distribution/run-direction-feedback.tsx +251 -0
  133. package/src/components/tools/distribution/run-port-snap.test.ts +79 -0
  134. package/src/components/tools/distribution/run-port-snap.ts +25 -0
  135. package/src/components/tools/distribution/run-screen-direction.test.ts +23 -0
  136. package/src/components/tools/distribution/run-screen-direction.ts +30 -0
  137. package/src/components/tools/elevator/elevator-tool.tsx +3 -6
  138. package/src/components/tools/elevator/move-elevator-tool.tsx +2 -3
  139. package/src/components/tools/item/item-surface-strategy-resolver.test.ts +67 -68
  140. package/src/components/tools/item/placement-history.test.tsx +512 -0
  141. package/src/components/tools/item/placement-strategies.test.ts +46 -1
  142. package/src/components/tools/item/placement-strategies.ts +11 -10
  143. package/src/components/tools/item/shelf-surface-strategy-resolver.test.ts +56 -56
  144. package/src/components/tools/item/use-draft-node.test.tsx +432 -1
  145. package/src/components/tools/item/use-draft-node.ts +166 -95
  146. package/src/components/tools/item/use-placement-coordinator.tsx +148 -95
  147. package/src/components/tools/paint-region/use-paint-tint.ts +37 -0
  148. package/src/components/tools/paint-region/wall-paint-region-tool.tsx +466 -0
  149. package/src/components/tools/registry/item-surface-move.test.ts +62 -63
  150. package/src/components/tools/registry/item-surface-move.ts +2 -0
  151. package/src/components/tools/registry/item-surface-resolver.test.ts +26 -26
  152. package/src/components/tools/registry/move-registry-node-tool.test.ts +268 -1
  153. package/src/components/tools/registry/move-registry-node-tool.tsx +66 -30
  154. package/src/components/tools/select/box-select-tool.tsx +2 -12
  155. package/src/components/tools/select/plane-box-select-tool.test.tsx +79 -0
  156. package/src/components/tools/select/plane-box-select-tool.tsx +35 -22
  157. package/src/components/tools/select/select-candidates.test.ts +37 -4
  158. package/src/components/tools/select/select-candidates.ts +15 -11
  159. package/src/components/tools/shared/cursor-sphere.tsx +36 -12
  160. package/src/components/tools/site/site-boundary-editor.tsx +1 -2
  161. package/src/components/tools/tool-manager.tsx +59 -24
  162. package/src/components/tools/wall/wall-drafting.test.ts +175 -47
  163. package/src/components/tools/zone/zone-tool.tsx +4 -12
  164. package/src/components/ui/action-menu/action-button.tsx +9 -1
  165. package/src/components/ui/action-menu/camera-actions.tsx +9 -4
  166. package/src/components/ui/action-menu/control-modes.tsx +15 -42
  167. package/src/components/ui/action-menu/follow-pascal-pill.tsx +32 -0
  168. package/src/components/ui/action-menu/follow-pascal-toggle.tsx +36 -0
  169. package/src/components/ui/action-menu/index.tsx +56 -33
  170. package/src/components/ui/action-menu/measurement-control.tsx +14 -19
  171. package/src/components/ui/action-menu/placement.ts +20 -0
  172. package/src/components/ui/action-menu/point-ask-button.tsx +52 -0
  173. package/src/components/ui/action-menu/view-toggles.tsx +47 -42
  174. package/src/components/ui/build-panel/build-panel.tsx +242 -0
  175. package/src/components/ui/command-palette/editor-commands.tsx +7 -42
  176. package/src/components/ui/command-palette/index.tsx +50 -39
  177. package/src/components/ui/command-palette/ranked-groups.test.ts +31 -0
  178. package/src/components/ui/controls/action-button.tsx +5 -2
  179. package/src/components/ui/controls/finish-swatch.tsx +110 -0
  180. package/src/components/ui/controls/material-paint-panel.tsx +97 -26
  181. package/src/components/ui/controls/material-picker.tsx +4 -1
  182. package/src/components/ui/controls/metric-control.tsx +1 -1
  183. package/src/components/ui/controls/scene-material-list.tsx +4 -32
  184. package/src/components/ui/controls/segmented-control.tsx +21 -8
  185. package/src/components/ui/controls/select-control.tsx +65 -0
  186. package/src/components/ui/controls/slider-control.tsx +1 -1
  187. package/src/components/ui/controls/toggle-control.tsx +3 -3
  188. package/src/components/ui/controls/tool-options-panel.tsx +1 -1
  189. package/src/components/ui/floating-level-selector.tsx +159 -59
  190. package/src/components/ui/helpers/building-helper.tsx +4 -1
  191. package/src/components/ui/helpers/contextual-helper-panel.tsx +181 -72
  192. package/src/components/ui/helpers/helper-manager.test.tsx +121 -0
  193. package/src/components/ui/helpers/helper-manager.tsx +331 -14
  194. package/src/components/ui/helpers/item-helper.tsx +17 -1
  195. package/src/components/ui/helpers/registered-tool-helper.tsx +8 -1
  196. package/src/components/ui/icon-ref.tsx +45 -1
  197. package/src/components/ui/level-duplicate-dialog.tsx +1 -1
  198. package/src/components/ui/node-icon-image.tsx +16 -0
  199. package/src/components/ui/panels/collections/collections-popover.tsx +28 -22
  200. package/src/components/ui/panels/floor-foundation-panel.tsx +551 -0
  201. package/src/components/ui/panels/floor-opening-panel.tsx +143 -0
  202. package/src/components/ui/panels/mobile-panel-sheet.tsx +4 -4
  203. package/src/components/ui/panels/mobile-selection-bar.tsx +8 -5
  204. package/src/components/ui/panels/multi-field-value.test.ts +26 -3
  205. package/src/components/ui/panels/multi-field-value.ts +6 -3
  206. package/src/components/ui/panels/multi-parametric-inspector.tsx +52 -2
  207. package/src/components/ui/panels/multi-selection-panel.tsx +161 -5
  208. package/src/components/ui/panels/node-display.ts +8 -3
  209. package/src/components/ui/panels/panel-manager.tsx +103 -68
  210. package/src/components/ui/panels/panel-wrapper.tsx +130 -62
  211. package/src/components/ui/panels/parametric-field-control.tsx +3 -3
  212. package/src/components/ui/panels/parametric-inspector.tsx +7 -7
  213. package/src/components/ui/panels/reference-panel.tsx +2 -3
  214. package/src/components/ui/panels/room-construction-rows.tsx +774 -0
  215. package/src/components/ui/panels/room-name-options.test.ts +55 -0
  216. package/src/components/ui/panels/room-name-options.ts +47 -0
  217. package/src/components/ui/panels/room-openings-field.tsx +110 -0
  218. package/src/components/ui/panels/room-panel.tsx +790 -0
  219. package/src/components/ui/panels/shape-choice.tsx +58 -0
  220. package/src/components/ui/panels/wall-region-list.tsx +109 -0
  221. package/src/components/ui/primitives/badge.tsx +8 -0
  222. package/src/components/ui/primitives/color-dot.tsx +36 -24
  223. package/src/components/ui/primitives/sidebar.tsx +3 -1
  224. package/src/components/ui/right-stack.test.ts +29 -0
  225. package/src/components/ui/right-stack.tsx +164 -0
  226. package/src/components/ui/sidebar/app-sidebar.tsx +1 -1
  227. package/src/components/ui/sidebar/icon-rail.tsx +8 -3
  228. package/src/components/ui/sidebar/panels/items-panel/function-tree-panel.tsx +1 -1
  229. package/src/components/ui/sidebar/panels/items-panel/index.tsx +6 -6
  230. package/src/components/ui/sidebar/panels/settings-panel/animation-speed-setting.tsx +40 -0
  231. package/src/components/ui/sidebar/panels/settings-panel/audio-settings-dialog.tsx +7 -8
  232. package/src/components/ui/sidebar/panels/settings-panel/construction-animation-setting.test.tsx +170 -0
  233. package/src/components/ui/sidebar/panels/settings-panel/construction-animation-setting.tsx +92 -0
  234. package/src/components/ui/sidebar/panels/settings-panel/index.tsx +138 -27
  235. package/src/components/ui/sidebar/panels/settings-panel/interface-theme.test.tsx +20 -0
  236. package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +5 -5
  237. package/src/components/ui/sidebar/panels/settings-panel/print-export-button.tsx +4 -10
  238. package/src/components/ui/sidebar/panels/site-panel/building-tree-node.tsx +3 -11
  239. package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +4 -4
  240. package/src/components/ui/sidebar/panels/site-panel/chimney-tree-node.tsx +1 -5
  241. package/src/components/ui/sidebar/panels/site-panel/collection-tree-node.tsx +197 -0
  242. package/src/components/ui/sidebar/panels/site-panel/column-tree-node.tsx +1 -5
  243. package/src/components/ui/sidebar/panels/site-panel/door-tree-node.tsx +1 -5
  244. package/src/components/ui/sidebar/panels/site-panel/dormer-tree-node.tsx +1 -5
  245. package/src/components/ui/sidebar/panels/site-panel/elevator-tree-node.tsx +1 -5
  246. package/src/components/ui/sidebar/panels/site-panel/fence-tree-node.tsx +24 -11
  247. package/src/components/ui/sidebar/panels/site-panel/gutter-tree-node.tsx +1 -5
  248. package/src/components/ui/sidebar/panels/site-panel/index.tsx +181 -223
  249. package/src/components/ui/sidebar/panels/site-panel/inline-rename-input.tsx +30 -8
  250. package/src/components/ui/sidebar/panels/site-panel/item-tree-node.tsx +1 -3
  251. package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +4 -1
  252. package/src/components/ui/sidebar/panels/site-panel/registry-tree-node.tsx +1 -3
  253. package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +1 -4
  254. package/src/components/ui/sidebar/panels/site-panel/shelf-tree-node.tsx +1 -3
  255. package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +21 -13
  256. package/src/components/ui/sidebar/panels/site-panel/solar-panel-tree-node.tsx +1 -5
  257. package/src/components/ui/sidebar/panels/site-panel/spawn-tree-node.tsx +1 -5
  258. package/src/components/ui/sidebar/panels/site-panel/stair-tree-node.tsx +1 -5
  259. package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +13 -37
  260. package/src/components/ui/sidebar/panels/site-panel/unit-tree-node.tsx +2 -3
  261. package/src/components/ui/sidebar/panels/site-panel/wall-tree-node.tsx +1 -5
  262. package/src/components/ui/sidebar/panels/site-panel/window-tree-node.tsx +1 -5
  263. package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +9 -4
  264. package/src/components/ui/sidebar/panels/zone-panel/index.tsx +2 -4
  265. package/src/components/ui/sidebar/tab-bar.tsx +14 -4
  266. package/src/components/ui/sidebar/use-plugin-panels.tsx +15 -5
  267. package/src/components/viewer/floorplan-compass-button.tsx +9 -1
  268. package/src/components/viewer/floorplan-preview.tsx +5 -6
  269. package/src/components/viewer/viewer-controls-bar.tsx +18 -6
  270. package/src/components/viewer/viewer-hover-label.tsx +34 -0
  271. package/src/components/viewer/viewer-rooms.tsx +191 -0
  272. package/src/components/viewer/viewer-scene-header.tsx +181 -145
  273. package/src/components/viewer/viewer-selection-manager.tsx +124 -0
  274. package/src/components/viewer/viewer-stage.tsx +2 -2
  275. package/src/components/viewer-zone-system.tsx +3 -8
  276. package/src/hooks/use-auto-save.integration.test.tsx +215 -43
  277. package/src/hooks/use-auto-save.ts +60 -9
  278. package/src/hooks/use-ceiling-events.test.ts +123 -118
  279. package/src/hooks/use-grid-events.test.ts +105 -105
  280. package/src/hooks/use-keyboard.test.ts +243 -19
  281. package/src/hooks/use-keyboard.ts +118 -83
  282. package/src/hooks/use-open-wall-ends.test.ts +99 -0
  283. package/src/hooks/use-open-wall-ends.ts +238 -0
  284. package/src/hooks/use-placement-notice.test.ts +71 -0
  285. package/src/hooks/use-placement-notice.ts +73 -0
  286. package/src/hooks/use-selected-room.ts +143 -0
  287. package/src/index.tsx +312 -58
  288. package/src/lib/__fixtures__/owner-victor-seattle-parking.json +1 -0
  289. package/src/lib/__fixtures__/owner-villa-aurelia-basement.json +1 -0
  290. package/src/lib/add-storey.test.ts +177 -0
  291. package/src/lib/add-storey.ts +59 -0
  292. package/src/lib/below-grade.test.tsx +171 -0
  293. package/src/lib/below-grade.ts +133 -0
  294. package/src/lib/camera-facing.test.ts +24 -0
  295. package/src/lib/camera-facing.ts +24 -0
  296. package/src/lib/ceiling-edit-session.test.ts +321 -0
  297. package/src/lib/ceiling-edit-session.ts +206 -0
  298. package/src/lib/ceiling-region-session.test.ts +179 -0
  299. package/src/lib/continuation.test.ts +19 -1
  300. package/src/lib/continuation.ts +25 -4
  301. package/src/lib/direct-manipulation.ts +2 -0
  302. package/src/lib/door-interaction.ts +14 -0
  303. package/src/lib/download-project-file.ts +17 -0
  304. package/src/lib/editor-level-display.test.ts +96 -0
  305. package/src/lib/editor-level-display.ts +55 -0
  306. package/src/lib/editor-views.ts +178 -0
  307. package/src/lib/export-geometry-wait.test.ts +90 -0
  308. package/src/lib/export-texture-utils.ts +2 -4
  309. package/src/lib/floor-edit-session.ts +153 -0
  310. package/src/lib/floor-footprints.test.ts +563 -0
  311. package/src/lib/floor-footprints.ts +395 -0
  312. package/src/lib/floor-foundation-support.test.ts +284 -0
  313. package/src/lib/floor-opening-draft.ts +218 -0
  314. package/src/lib/floor-openings.test.ts +216 -0
  315. package/src/lib/floor-region-geometry.ts +191 -0
  316. package/src/lib/floor-region-session.test.ts +485 -0
  317. package/src/lib/floor-region-session.ts +589 -0
  318. package/src/lib/floor-region-snap.ts +101 -0
  319. package/src/lib/floor-step-hover.test.ts +32 -0
  320. package/src/lib/floorplan/annotation-visibility.ts +17 -0
  321. package/src/lib/floorplan/dimension-drive.test.ts +360 -0
  322. package/src/lib/floorplan/dimension-drive.ts +479 -0
  323. package/src/lib/floorplan/floorplan-export.test.ts +12 -8
  324. package/src/lib/floorplan/floorplan-export.tsx +32 -8
  325. package/src/lib/floorplan/floorplan-extension.ts +32 -0
  326. package/src/lib/floorplan/floorplan-mode.test.ts +4 -0
  327. package/src/lib/floorplan/floorplan-mode.ts +2 -0
  328. package/src/lib/floorplan/floorplan-pdfkit-document.ts +363 -27
  329. package/src/lib/floorplan/floorplan-pdfkit-renderer.test.ts +177 -8
  330. package/src/lib/floorplan/floorplan-pdfkit-renderer.ts +60 -25
  331. package/src/lib/floorplan/floorplan-plugin-install.test.tsx +138 -0
  332. package/src/lib/floorplan/floorplan-readonly.ts +6 -0
  333. package/src/lib/floorplan/geometry.ts +13 -6
  334. package/src/lib/floorplan/index.ts +0 -9
  335. package/src/lib/floorplan/items.ts +5 -2
  336. package/src/lib/floorplan/marks.test.ts +213 -0
  337. package/src/lib/floorplan/marks.ts +238 -0
  338. package/src/lib/floorplan/open-wall-ends.test.ts +268 -0
  339. package/src/lib/floorplan/open-wall-ends.ts +162 -0
  340. package/src/lib/floorplan/pdf-fonts/GeistMono-Bold.ttf +0 -0
  341. package/src/lib/floorplan/pdf-fonts/GeistMono-Regular.ttf +0 -0
  342. package/src/lib/floorplan/pdf-fonts/LICENSE-GeistMono.txt +92 -0
  343. package/src/lib/floorplan/pdf-fonts/LICENSE-LiberationSans.txt +102 -0
  344. package/src/lib/floorplan/pdf-fonts/LiberationSans-Bold.ttf +0 -0
  345. package/src/lib/floorplan/pdf-fonts/LiberationSans-Regular.ttf +0 -0
  346. package/src/lib/floorplan/pdfkit-standard-fonts.d.ts +5 -0
  347. package/src/lib/floorplan/schedules.test.ts +189 -0
  348. package/src/lib/floorplan/schedules.ts +339 -0
  349. package/src/lib/floorplan/sheet-export.test.ts +124 -0
  350. package/src/lib/floorplan/sheet-export.ts +318 -0
  351. package/src/lib/floorplan/site-plan/build-site-plan-drawing.ts +768 -0
  352. package/src/lib/floorplan/site-plan/contours.test.ts +43 -0
  353. package/src/lib/floorplan/site-plan/contributors.ts +97 -0
  354. package/src/lib/floorplan/site-plan/coverage.ts +230 -0
  355. package/src/lib/floorplan/site-plan/drawing-type-switch.test.ts +15 -0
  356. package/src/lib/floorplan/site-plan/drawing-type-switch.tsx +86 -0
  357. package/src/lib/floorplan/site-plan/front-edge.test.ts +103 -0
  358. package/src/lib/floorplan/site-plan/front-edge.ts +157 -0
  359. package/src/lib/floorplan/site-plan/geometry.test.ts +269 -0
  360. package/src/lib/floorplan/site-plan/geometry.ts +142 -0
  361. package/src/lib/floorplan/site-plan/index.ts +55 -0
  362. package/src/lib/floorplan/site-plan/site-annotations.test.ts +483 -0
  363. package/src/lib/floorplan/site-plan/site-annotations.ts +1084 -0
  364. package/src/lib/floorplan/site-plan/site-parts.ts +287 -0
  365. package/src/lib/floorplan/site-plan/site-plan-layer.tsx +138 -0
  366. package/src/lib/floorplan/site-plan/street-name.test.ts +69 -0
  367. package/src/lib/floorplan/site-plan/street-name.ts +302 -0
  368. package/src/lib/floorplan/site-plan/yards.test.ts +70 -0
  369. package/src/lib/floorplan/types.ts +1 -27
  370. package/src/lib/follow-pascal-exterior.test-fixture.ts +17 -0
  371. package/src/lib/follow-pascal-exterior.test.ts +171 -0
  372. package/src/lib/follow-pascal-exterior.ts +39 -0
  373. package/src/lib/follow-pascal-presentation.test.ts +203 -0
  374. package/src/lib/follow-pascal-presentation.ts +129 -0
  375. package/src/lib/follow-pascal-sight.ts +84 -0
  376. package/src/lib/follow-pascal-targets.test.ts +406 -0
  377. package/src/lib/follow-pascal-targets.ts +198 -0
  378. package/src/lib/follow-pascal.test.ts +350 -0
  379. package/src/lib/follow-pascal.ts +284 -0
  380. package/src/lib/fresh-planar-placement.test.ts +148 -0
  381. package/src/lib/fresh-planar-placement.ts +114 -37
  382. package/src/lib/geometry-script/author.ts +157 -0
  383. package/src/lib/geometry-script/client.ts +159 -0
  384. package/src/lib/geometry-script/finish-scripts.ts +127 -0
  385. package/src/lib/geometry-script/protocol.ts +13 -0
  386. package/src/lib/gesture-lifecycle.test.ts +595 -0
  387. package/src/lib/gesture-lifecycle.ts +290 -0
  388. package/src/lib/glb-export-zone-holes.test.ts +41 -0
  389. package/src/lib/glb-export.test.ts +636 -126
  390. package/src/lib/glb-export.ts +241 -120
  391. package/src/lib/history.test.ts +18 -7
  392. package/src/lib/history.ts +7 -0
  393. package/src/lib/hud-title.ts +109 -0
  394. package/src/lib/hydration-gated-storage.ts +49 -0
  395. package/src/lib/ifc-export.test.ts +174 -0
  396. package/src/lib/ifc-export.ts +201 -0
  397. package/src/lib/ifc-parts.ts +15 -0
  398. package/src/lib/inspector-expanded.test.ts +44 -0
  399. package/src/lib/inspector-expanded.ts +36 -0
  400. package/src/lib/interaction/scope.ts +22 -0
  401. package/src/lib/keyboard-pan.test.ts +129 -7
  402. package/src/lib/keyboard-pan.ts +161 -12
  403. package/src/lib/level-selection.test.ts +57 -0
  404. package/src/lib/level-selection.ts +16 -1
  405. package/src/lib/live-gesture-round-trip.test.ts +360 -0
  406. package/src/lib/lot/__fixtures__/map-dossier-miami-shores.json +1 -0
  407. package/src/lib/lot/__fixtures__/map-dossier-stpete.json +1 -0
  408. package/src/lib/lot/clean-ring.test.ts +187 -0
  409. package/src/lib/lot/clean-ring.ts +217 -0
  410. package/src/lib/lot/dossier.test.ts +202 -0
  411. package/src/lib/lot/dossier.ts +498 -0
  412. package/src/lib/lot/drop-in.test.ts +326 -0
  413. package/src/lib/lot/drop-in.ts +392 -0
  414. package/src/lib/lot/index.ts +43 -0
  415. package/src/lib/lot/lot-patch.test.ts +340 -0
  416. package/src/lib/lot/lot-patch.ts +394 -0
  417. package/src/lib/lot/parcel-provider.ts +43 -0
  418. package/src/lib/lot/terrain.test.ts +161 -0
  419. package/src/lib/lot/terrain.ts +266 -0
  420. package/src/lib/material-catalog-model.ts +3 -1
  421. package/src/lib/material-paint-panel-model.ts +9 -5
  422. package/src/lib/material-paint.ts +4 -0
  423. package/src/lib/measurement-kind.test.ts +6 -8
  424. package/src/lib/measurements.test.ts +23 -0
  425. package/src/lib/mezzanine-actions.test.ts +342 -0
  426. package/src/lib/mezzanine-draft.test.ts +501 -0
  427. package/src/lib/mezzanine-draft.ts +278 -0
  428. package/src/lib/mezzanine-messages.ts +26 -0
  429. package/src/lib/mezzanine-selection.test.ts +207 -0
  430. package/src/lib/mezzanine-stairs.ts +46 -0
  431. package/src/lib/model-export.ts +3 -1
  432. package/src/lib/node-action-movement.ts +17 -6
  433. package/src/lib/node-display-label.ts +37 -0
  434. package/src/lib/orbit-floor-clearance.test.ts +201 -0
  435. package/src/lib/orbit-floor-clearance.ts +154 -0
  436. package/src/lib/paint-commit-route.test.ts +37 -0
  437. package/src/lib/paint-commit-route.ts +17 -0
  438. package/src/lib/paint-eyedropper.test.tsx +165 -0
  439. package/src/lib/paint-eyedropper.ts +99 -0
  440. package/src/lib/paint-pass-through.test.ts +122 -0
  441. package/src/lib/paint-pass-through.ts +78 -0
  442. package/src/lib/paint-region-hover.ts +25 -0
  443. package/src/lib/paint-region-mode.ts +154 -0
  444. package/src/lib/paint-regions.test.ts +166 -0
  445. package/src/lib/paint-regions.ts +192 -0
  446. package/src/lib/paint-scope.test.ts +317 -35
  447. package/src/lib/paint-scope.ts +267 -25
  448. package/src/lib/panel-rows.ts +21 -0
  449. package/src/lib/parametric-node-update.ts +16 -7
  450. package/src/lib/plate-paint-affected.test.ts +300 -0
  451. package/src/lib/plate-paint-affected.ts +177 -0
  452. package/src/lib/plugin-chat.ts +65 -0
  453. package/src/lib/plugin-panels.test.ts +25 -0
  454. package/src/lib/plugin-panels.ts +94 -0
  455. package/src/lib/point-ask/__fixtures__/consumer-browser.tsx +76 -0
  456. package/src/lib/point-ask/__fixtures__/house.ts +81 -0
  457. package/src/lib/point-ask/ask-created.test.ts +195 -0
  458. package/src/lib/point-ask/ask-flow.test.ts +380 -0
  459. package/src/lib/point-ask/build-context.test.ts +173 -0
  460. package/src/lib/point-ask/build-context.ts +94 -0
  461. package/src/lib/point-ask/capture-crop.ts +129 -0
  462. package/src/lib/point-ask/choreography.ts +69 -0
  463. package/src/lib/point-ask/compare.test.ts +145 -0
  464. package/src/lib/point-ask/compare.ts +73 -0
  465. package/src/lib/point-ask/describe-target.test.ts +165 -0
  466. package/src/lib/point-ask/describe-target.ts +234 -0
  467. package/src/lib/point-ask/dictation.ts +79 -0
  468. package/src/lib/point-ask/edge-marker.test.ts +61 -0
  469. package/src/lib/point-ask/edge-marker.ts +27 -0
  470. package/src/lib/point-ask/frame-loop.ts +25 -0
  471. package/src/lib/point-ask/history-marks.test.ts +94 -0
  472. package/src/lib/point-ask/history-marks.ts +39 -0
  473. package/src/lib/point-ask/host-api.ts +32 -0
  474. package/src/lib/point-ask/outline-box.test.ts +30 -0
  475. package/src/lib/point-ask/outline-box.ts +11 -0
  476. package/src/lib/point-ask/overlay-metrics.ts +6 -0
  477. package/src/lib/point-ask/pick.test.ts +220 -0
  478. package/src/lib/point-ask/pick.ts +121 -0
  479. package/src/lib/point-ask/pin-machine.test.ts +494 -0
  480. package/src/lib/point-ask/pin-machine.ts +217 -0
  481. package/src/lib/point-ask/placement.test.ts +203 -0
  482. package/src/lib/point-ask/placement.ts +141 -0
  483. package/src/lib/point-ask/pointer-tracker.ts +34 -0
  484. package/src/lib/point-ask/projector.ts +83 -0
  485. package/src/lib/point-ask/region.test.ts +157 -0
  486. package/src/lib/point-ask/region.ts +136 -0
  487. package/src/lib/point-ask/retry.test.ts +217 -0
  488. package/src/lib/point-ask/retry.ts +37 -0
  489. package/src/lib/point-ask/scene-commit.test.ts +312 -0
  490. package/src/lib/point-ask/scene-commit.ts +32 -0
  491. package/src/lib/point-ask/session.ts +1391 -0
  492. package/src/lib/point-ask/spring-keyframes.test.ts +45 -0
  493. package/src/lib/point-ask/spring-keyframes.ts +38 -0
  494. package/src/lib/point-ask/spring.test.ts +63 -0
  495. package/src/lib/point-ask/spring.ts +107 -0
  496. package/src/lib/point-ask/suggestions.test.ts +80 -0
  497. package/src/lib/point-ask/suggestions.ts +125 -0
  498. package/src/lib/point-ask/target-scope.test.ts +622 -0
  499. package/src/lib/point-ask/target-scope.ts +338 -0
  500. package/src/lib/point-ask/types.ts +48 -0
  501. package/src/lib/portable-export.test.ts +34 -1
  502. package/src/lib/portable-export.ts +119 -53
  503. package/src/lib/print-feature-thickness.ts +10 -1
  504. package/src/lib/print-roof-solids.test.ts +39 -1
  505. package/src/lib/print-roof-solids.ts +43 -10
  506. package/src/lib/print-shell-compiler-manifold-core.ts +1 -1
  507. package/src/lib/print-shell-compiler.ts +1 -0
  508. package/src/lib/registered-draft-snap.ts +12 -0
  509. package/src/lib/roof-duplication.ts +10 -2
  510. package/src/lib/room-assembly-overlay.test.ts +275 -0
  511. package/src/lib/room-assembly-overlay.ts +265 -0
  512. package/src/lib/room-built-on.test.ts +379 -0
  513. package/src/lib/room-built-on.ts +138 -0
  514. package/src/lib/room-construction-commands.ts +712 -0
  515. package/src/lib/room-construction.test.ts +746 -0
  516. package/src/lib/room-construction.ts +385 -0
  517. package/src/lib/room-controls-phase6.test.ts +630 -0
  518. package/src/lib/room-divide-session.ts +523 -0
  519. package/src/lib/room-handle-drag.ts +1068 -0
  520. package/src/lib/room-name-catalog.test.ts +90 -0
  521. package/src/lib/room-name-catalog.ts +168 -0
  522. package/src/lib/room-push-dimensions.test.ts +311 -0
  523. package/src/lib/room-push-dimensions.ts +251 -0
  524. package/src/lib/room-selection-commands.ts +62 -0
  525. package/src/lib/room-selection.test.ts +1017 -0
  526. package/src/lib/room-selection.ts +417 -0
  527. package/src/lib/room-structure-commands.ts +94 -0
  528. package/src/lib/room-structure.test.ts +921 -0
  529. package/src/lib/room-transform-session.ts +570 -0
  530. package/src/lib/room-zone-routing.test.ts +294 -0
  531. package/src/lib/room-zone-routing.ts +197 -0
  532. package/src/lib/scene-clipboard.test.ts +610 -1
  533. package/src/lib/scene-clipboard.ts +269 -58
  534. package/src/lib/scene-material-refs.test.ts +79 -0
  535. package/src/lib/scene-material-refs.ts +79 -0
  536. package/src/lib/scene.test.ts +24 -36
  537. package/src/lib/scene.ts +33 -57
  538. package/src/lib/selection-routing.test.ts +129 -45
  539. package/src/lib/selection-routing.ts +54 -75
  540. package/src/lib/session-writes.test.ts +120 -0
  541. package/src/lib/session-writes.ts +205 -0
  542. package/src/lib/sfx-bus.ts +13 -0
  543. package/src/lib/sfx-player.test.ts +70 -9
  544. package/src/lib/sfx-player.ts +6 -0
  545. package/src/lib/sidebar-panel.ts +62 -0
  546. package/src/lib/site-boundary.test.ts +4 -6
  547. package/src/lib/site-boundary.ts +1 -1
  548. package/src/lib/snapping-mode.test.ts +4 -0
  549. package/src/lib/snapping-mode.ts +8 -1
  550. package/src/lib/stair-levels.test.ts +25 -0
  551. package/src/lib/stair-levels.ts +22 -3
  552. package/src/lib/terrace-draft.test.ts +145 -0
  553. package/src/lib/terrace-draft.ts +214 -0
  554. package/src/lib/tool-cursor-icon.test.ts +16 -0
  555. package/src/lib/tool-cursor-icon.ts +11 -0
  556. package/src/lib/typing-target.ts +9 -0
  557. package/src/lib/units.ts +77 -39
  558. package/src/lib/vector-edges.test.ts +58 -0
  559. package/src/lib/vector-edges.ts +400 -0
  560. package/src/lib/viewer-selection.test.ts +577 -0
  561. package/src/lib/viewer-selection.ts +580 -0
  562. package/src/lib/wall-draw-variant.test.ts +71 -0
  563. package/src/lib/wall-draw-variant.ts +91 -0
  564. package/src/lib/wall-paint-region-session.ts +246 -0
  565. package/src/lib/wall-paint-region.test.ts +373 -0
  566. package/src/lib/wall-placement-pointer.test.ts +42 -0
  567. package/src/lib/wall-placement-pointer.ts +57 -0
  568. package/src/lib/wall-polygon-draft.test.ts +262 -0
  569. package/src/lib/wall-polygon-draft.ts +286 -0
  570. package/src/lib/wall-push.test.ts +453 -0
  571. package/src/lib/wall-region-face.ts +352 -0
  572. package/src/lib/wall-region-handles.test.ts +304 -0
  573. package/src/lib/wall-region-handles.ts +368 -0
  574. package/src/lib/wall-region-list.test.ts +119 -0
  575. package/src/lib/wall-region-list.ts +136 -0
  576. package/src/lib/wall-region-snap.ts +110 -0
  577. package/src/lib/window-changes.ts +35 -0
  578. package/src/lib/window-interaction.ts +14 -0
  579. package/src/lib/zone-content.ts +4 -1
  580. package/src/lib/zone-name.test.ts +17 -0
  581. package/src/lib/zone-name.ts +17 -0
  582. package/src/store/opens-in-select-mode.test.ts +223 -0
  583. package/src/store/paint-mode.test.ts +188 -0
  584. package/src/store/terrain-sculpt-mode.test.ts +19 -21
  585. package/src/store/tool-mode.test.ts +137 -6
  586. package/src/store/use-audio.test.ts +146 -0
  587. package/src/store/use-audio.tsx +127 -3
  588. package/src/store/use-construction-reveal.test.ts +173 -0
  589. package/src/store/use-construction-reveal.ts +145 -0
  590. package/src/store/use-corner-snap-hint.test.ts +24 -0
  591. package/src/store/use-corner-snap-hint.ts +47 -0
  592. package/src/store/use-delete-confirmation.ts +28 -1
  593. package/src/store/use-drawing-view.test.ts +24 -3
  594. package/src/store/use-drawing-view.ts +29 -1
  595. package/src/store/use-editor.tsx +432 -321
  596. package/src/store/use-floorplan-draft-preview.ts +13 -0
  597. package/src/store/use-floorplan-marquee.ts +12 -2
  598. package/src/store/use-follow-pascal.test.ts +62 -0
  599. package/src/store/use-follow-pascal.ts +53 -0
  600. package/src/store/use-point-ask.ts +247 -0
  601. package/src/store/use-quick-measurement-hud.ts +3 -3
  602. package/src/store/use-stair-build-preview.ts +33 -5
  603. package/src/store/use-wall-move-ghosts.ts +4 -2
  604. package/src/store/use-wall-region-handles-selection.test.ts +222 -0
  605. package/src/store/use-wall-region-handles-selection.ts +139 -0
  606. package/src/store/view-layout.test.ts +201 -0
  607. package/src/store/view-layout.ts +199 -0
  608. package/src/test-utils/immediate-animation-frames.ts +16 -0
  609. package/src/test-utils/live-gesture-round-trip.ts +224 -0
  610. package/src/test-utils/render-current-markup.tsx +16 -0
  611. package/src/test-utils/selection-harness.ts +82 -0
  612. package/src/components/editor-2d/renderers/floorplan-stair-layer.tsx +0 -499
  613. package/src/components/systems/zone/zone-label-editor-system.tsx +0 -333
  614. package/src/components/tools/fence/fence-drafting.test.ts +0 -100
  615. package/src/components/tools/fence/fence-drafting.ts +0 -266
  616. package/src/components/tools/stair/stair-click-guard.test.ts +0 -58
  617. package/src/components/tools/stair/stair-click-guard.ts +0 -72
  618. package/src/components/tools/stair/stair-defaults.ts +0 -18
  619. package/src/components/tools/stair/stair-tool.tsx +0 -634
  620. package/src/components/ui/sidebar/panels/plugins-panel.tsx +0 -218
  621. package/src/components/viewer/viewer-units-panel.tsx +0 -116
  622. package/src/lib/floorplan/stairs.ts +0 -489
  623. package/src/lib/level-duplication.test.ts +0 -144
  624. package/src/lib/level-duplication.ts +0 -195
@@ -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
+ })
@@ -0,0 +1,198 @@
1
+ import { type AnyNode, type AnyNodeId, findLevelAncestorId, nodeRegistry } from '@pascal-app/core'
2
+ import { sceneViewBounds, sceneViewFacing } from '@pascal-app/core/agent-operations'
3
+ import type { RevealEvent } from '@pascal-app/viewer'
4
+ import type { Box, FollowWant } from './follow-pascal'
5
+ import { anyExterior } from './follow-pascal-exterior'
6
+
7
+ type Nodes = Readonly<Record<string, AnyNode>>
8
+
9
+ /**
10
+ * The box `view_scene` would frame for `id`, in the site (a building away from the origin is framed
11
+ * where it stands): the same bounds the agent's own views take, so the person sees what the agent
12
+ * would look at. Null for a node there is nothing to frame of.
13
+ */
14
+ function boxOf(nodes: Nodes, id: string): Box | null {
15
+ try {
16
+ const { min, max } = sceneViewBounds(nodes, id)
17
+ return { min, max }
18
+ } catch {
19
+ return null
20
+ }
21
+ }
22
+
23
+ function unionOf(boxes: readonly (Box | null)[]): Box | null {
24
+ const known = boxes.filter((box): box is Box => box !== null)
25
+ const first = known[0]
26
+ if (!first) return null
27
+ const min: Box['min'] = [...first.min]
28
+ const max: Box['max'] = [...first.max]
29
+ for (const box of known) {
30
+ for (let axis = 0; axis < 3; axis += 1) {
31
+ min[axis] = Math.min(min[axis]!, box.min[axis]!)
32
+ max[axis] = Math.max(max[axis]!, box.max[axis]!)
33
+ }
34
+ }
35
+ return { min, max }
36
+ }
37
+
38
+ function buildingOf(nodes: Nodes, id: string): string | null {
39
+ let node = nodes[id]
40
+ for (let guard = 0; node && guard < 32; guard += 1) {
41
+ if (node.type === 'building') return node.id
42
+ node = node.parentId ? nodes[node.parentId] : undefined
43
+ }
44
+ return null
45
+ }
46
+
47
+ /** A roof that lifts for the furniture is looked down through: the eye clears its lifted top by this. */
48
+ const LIFTED_ROOF_MARGIN = 1.5
49
+
50
+ /**
51
+ * How high the eye must stand to look at something in a building: above every roof of it that lifts
52
+ * out of the way of the furnishing, where it is lifted to. Undefined when nothing lifts.
53
+ */
54
+ function clearanceAbove(nodes: Nodes, id: string): number | undefined {
55
+ const building = buildingOf(nodes, id)
56
+ if (!building) return undefined
57
+ let highest: number | undefined
58
+ for (const node of Object.values(nodes)) {
59
+ if (node.type !== 'roof' || buildingOf(nodes, node.id) !== building) continue
60
+ const clears = nodeRegistry.get(node.type)?.capabilities?.reveal?.clears
61
+ const box = boxOf(nodes, node.id)
62
+ if (!(clears && box)) continue
63
+ highest = Math.max(highest ?? Number.NEGATIVE_INFINITY, box.max[1] + clears.height)
64
+ }
65
+ return highest === undefined ? undefined : highest + LIFTED_ROOF_MARGIN
66
+ }
67
+
68
+ /** The building `id` is in, roof and all, seen as a group; the eye clears a roof that is lifted. */
69
+ function wholeHouse(nodes: Nodes, id: string, at: number): FollowWant | null {
70
+ const building = buildingOf(nodes, id)
71
+ if (!building) return null
72
+ const roofs = Object.values(nodes).filter(
73
+ (node) => node.type === 'roof' && buildingOf(nodes, node.id) === building,
74
+ )
75
+ const box = unionOf([boxOf(nodes, building), ...roofs.map((roof) => boxOf(nodes, roof.id))])
76
+ if (!box) return null
77
+ const eyeAbove = clearanceAbove(nodes, id)
78
+ return { kind: 'group', box, ...(eyeAbove === undefined ? {} : { eyeAbove }), at }
79
+ }
80
+
81
+ /**
82
+ * A look at the whole house (the furniture has landed; a build has finished), made a look at the
83
+ * level the person is already on: the level whole, as when its floor starts, with no floors above
84
+ * to unhide and no lifted roof to clear.
85
+ */
86
+ export function stayOnLevel(want: FollowWant, nodes: Nodes, levelId: string): FollowWant {
87
+ const box = boxOf(nodes, levelId)
88
+ return box ? { kind: 'floor', box, levelId, at: want.at } : { ...want, levelId }
89
+ }
90
+
91
+ /**
92
+ * What the camera should look at for something the reveal said, or null when it says nothing about
93
+ * where to look. A floor, whole, when its slabs and walls start (the walls are not targets one by
94
+ * one: the floor already is); each opening close up from outside; each piece of furniture from above;
95
+ * the roof from the group that assembles it; the whole house, roof and all, when it is finished. An
96
+ * undo's reverse play is followed the same way, group by group, on what goes.
97
+ */
98
+ export function wantFromEvent(event: RevealEvent, nodes: Nodes, at: number): FollowWant | null {
99
+ let want = targetFromEvent(event, nodes, at)
100
+ if (want && (want.kind === 'detail' || want.kind === 'furnish' || want.kind === 'roof')) {
101
+ const nodeIds = 'id' in event ? [event.id] : 'nodeIds' in event ? event.nodeIds : []
102
+ want = { ...want, nodeIds }
103
+ }
104
+ if (want && exteriorStep(event, nodes)) want = { ...want, exterior: true }
105
+ if (!want || event.type === 'complete' || event.type === 'finale' || event.type === 'settle')
106
+ return want
107
+ let level = event.levelId ? nodes[event.levelId] : undefined
108
+ if (level?.type !== 'level') {
109
+ const id = 'id' in event ? event.id : event.nodeIds[0]
110
+ const levelId = id ? findLevelAncestorId(id as AnyNodeId, nodes) : null
111
+ level = levelId ? nodes[levelId] : undefined
112
+ }
113
+ return level?.type === 'level' ? { ...want, levelId: level.id } : want
114
+ }
115
+
116
+ /**
117
+ * Whether the step is a change seen from outside, not through a cutaway into a room: a piece the
118
+ * agent edited or moved that stands outdoors (a wall with an exterior face, an opening in one, a
119
+ * roof, the ground). Only edits and moves: a build is followed as it always was, whatever it builds
120
+ * (the owner's call, 15 Oct), and furnishing and interior walls are not outside either.
121
+ */
122
+ function exteriorStep(event: RevealEvent, nodes: Nodes): boolean {
123
+ if (!('changed' in event) || event.changed !== true) return false
124
+ const ids = 'id' in event ? [event.id] : 'nodeIds' in event ? [...event.nodeIds] : []
125
+ return anyExterior(nodes, ids)
126
+ }
127
+
128
+ function targetFromEvent(event: RevealEvent, nodes: Nodes, at: number): FollowWant | null {
129
+ switch (event.type) {
130
+ case 'start': {
131
+ // Hidden floors settle instantly and say only the group, not each node's start.
132
+ if (event.instant && (event.phase === 'openings' || event.phase === 'furnishing')) {
133
+ const box = unionOf(event.nodeIds.map((id) => boxOf(nodes, id)))
134
+ return box ? { kind: event.phase === 'openings' ? 'detail' : 'furnish', box, at } : null
135
+ }
136
+ if (event.phase === 'roof') {
137
+ const box = unionOf(event.nodeIds.map((id) => boxOf(nodes, id)))
138
+ return box ? { kind: 'roof', box, at } : null
139
+ }
140
+ if (
141
+ event.phase !== 'foundation' &&
142
+ event.phase !== 'structure' &&
143
+ event.phase !== 'circulation'
144
+ )
145
+ return null
146
+ const box =
147
+ (event.levelId ? boxOf(nodes, event.levelId) : null) ??
148
+ unionOf(event.nodeIds.map((id) => boxOf(nodes, id)))
149
+ return box ? { kind: 'floor', box, at } : null
150
+ }
151
+ case 'node-start': {
152
+ if (event.phase === 'openings') {
153
+ const box = boxOf(nodes, event.id)
154
+ if (!box) return null
155
+ const normal = sceneViewFacing(nodes, event.id)
156
+ return { kind: 'detail', box, ...(normal ? { normal } : {}), at }
157
+ }
158
+ if (event.phase === 'furnishing') {
159
+ const box = boxOf(nodes, event.id)
160
+ if (!box) return null
161
+ const eyeAbove = clearanceAbove(nodes, event.id)
162
+ return { kind: 'furnish', box, ...(eyeAbove === undefined ? {} : { eyeAbove }), at }
163
+ }
164
+ // A piece the agent edited (a wall bent, a slab resized) plays again: the camera goes to it,
165
+ // from the side that faces outdoors when it has one.
166
+ if (event.changed) {
167
+ const box = boxOf(nodes, event.id)
168
+ if (!box) return null
169
+ const normal = sceneViewFacing(nodes, event.id)
170
+ return { kind: 'detail', box, ...(normal ? { normal } : {}), at }
171
+ }
172
+ return null
173
+ }
174
+ case 'settle': {
175
+ // A piece the agent moved: nothing is coming down on the camera, it stays on the piece.
176
+ if (event.changed) return null
177
+ // The furniture has landed: pull back to the whole house while the roof is still up, so the
178
+ // roof does not come down on a camera that is close above it.
179
+ const id = event.phase === 'furnishing' ? event.nodeIds[0] : undefined
180
+ return id ? wholeHouse(nodes, id, at) : null
181
+ }
182
+ case 'finale':
183
+ return wholeHouse(nodes, event.id, at)
184
+ case 'retract-start': {
185
+ // An undo plays the build backwards: the camera is on what goes, a phase at a time.
186
+ const box = unionOf(event.nodeIds.map((id) => boxOf(nodes, id)))
187
+ if (!box) return null
188
+ if (event.phase === 'roof') return { kind: 'roof', box, at }
189
+ if (event.phase === 'furnishing') {
190
+ const eyeAbove = clearanceAbove(nodes, event.nodeIds[0] ?? '')
191
+ return { kind: 'furnish', box, ...(eyeAbove === undefined ? {} : { eyeAbove }), at }
192
+ }
193
+ return { kind: event.phase === 'openings' ? 'group' : 'floor', box, at }
194
+ }
195
+ default:
196
+ return null
197
+ }
198
+ }