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