@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,654 @@
1
+ import { afterEach, beforeEach, describe, expect, test } from 'bun:test'
2
+ import {
3
+ type AnyNode,
4
+ type AnyNodeId,
5
+ createTerrainField,
6
+ emitter,
7
+ encodeTerrainField,
8
+ getLevelElevations,
9
+ sceneRegistry,
10
+ useScene,
11
+ } from '@pascal-app/core'
12
+ import { type SceneViewPose, sceneViewPlan } from '@pascal-app/core/agent-operations'
13
+ import {
14
+ BuildingNode,
15
+ CeilingNode,
16
+ LevelNode,
17
+ SiteNode,
18
+ SlabNode,
19
+ WallNode,
20
+ } from '@pascal-app/core/schema'
21
+ import {
22
+ groundWindowBoxNow,
23
+ hideFromScene,
24
+ SCENE_LAYER,
25
+ SITE_GROUND,
26
+ setGroundWindow,
27
+ useViewer,
28
+ } from '@pascal-app/viewer'
29
+ import {
30
+ Group,
31
+ Layers,
32
+ Mesh,
33
+ type Object3D,
34
+ OrthographicCamera,
35
+ PerspectiveCamera,
36
+ PlaneGeometry,
37
+ Raycaster,
38
+ Scene,
39
+ Vector2,
40
+ Vector3,
41
+ } from 'three'
42
+ import { CanvasTarget, ClippingGroup, type WebGPURenderer } from 'three/webgpu'
43
+ import hillside from '../../../../core/src/lib/__fixtures__/owner-hillside-grade.json'
44
+ import victorSeattle from '../../lib/__fixtures__/owner-victor-seattle-parking.json'
45
+ import villaAurelia from '../../lib/__fixtures__/owner-villa-aurelia-basement.json'
46
+ import {
47
+ atCaptureSize,
48
+ captureDimensions,
49
+ capturesOrthographically,
50
+ exteriorGroundForCapture,
51
+ interiorCutaway,
52
+ presentFinished,
53
+ presentWhileCapturing,
54
+ pumpFrames,
55
+ tileGrid,
56
+ } from './thumbnail-generator'
57
+
58
+ type FakeCanvas = { width: number; height: number; style: Record<string, string> }
59
+
60
+ const realDocument = (globalThis as { document?: unknown }).document
61
+
62
+ beforeEach(() => {
63
+ ;(globalThis as { document?: unknown }).document = {
64
+ createElement: (): FakeCanvas => ({ width: 0, height: 0, style: {} }),
65
+ }
66
+ })
67
+
68
+ afterEach(() => {
69
+ ;(globalThis as { document?: unknown }).document = realDocument
70
+ })
71
+
72
+ /** The part of three's renderer a capture's passes size themselves from. */
73
+ function fakeRenderer(canvas: FakeCanvas, pixelRatio: number) {
74
+ const [cssWidth, cssHeight] = [canvas.width / pixelRatio, canvas.height / pixelRatio]
75
+ let target = new CanvasTarget(canvas as unknown as HTMLCanvasElement)
76
+ target.setPixelRatio(pixelRatio)
77
+ target.setSize(cssWidth, cssHeight, false)
78
+ const renderer = {
79
+ getCanvasTarget: () => target,
80
+ setCanvasTarget: (next: CanvasTarget) => {
81
+ target = next
82
+ },
83
+ getDrawingBufferSize: (v: Vector2) => target.getDrawingBufferSize(v),
84
+ get domElement() {
85
+ return target.domElement
86
+ },
87
+ }
88
+ return renderer as unknown as WebGPURenderer & typeof renderer
89
+ }
90
+
91
+ describe('supersampled capture', () => {
92
+ test('an explicit camera renders at the requested resolution and aspect without resizing the pane', () => {
93
+ const live: FakeCanvas = { width: 800, height: 1000, style: {} }
94
+ const renderer = fakeRenderer(live, 1)
95
+ const position: [number, number, number] = [2, 1, 3]
96
+ const target: [number, number, number] = [0, 0, 0]
97
+ for (const pose of [
98
+ { perspective: { position, target, fov: 35 } },
99
+ { ortho: { position, target, viewWidth: 4 } },
100
+ ]) {
101
+ const size = captureDimensions(
102
+ [800, 1000],
103
+ { ...pose, standardSize: { w: 1920, h: 1280 } },
104
+ 24_000_000,
105
+ )
106
+ expect(size).toEqual([1920, 1280])
107
+ const buffer = atCaptureSize(renderer, ...size, () =>
108
+ renderer.getDrawingBufferSize(new Vector2()).toArray(),
109
+ )
110
+ expect(buffer).toEqual([1920, 1280])
111
+ expect([live.width, live.height]).toEqual([800, 1000])
112
+ }
113
+ expect(
114
+ captureDimensions([800, 1000], { standardSize: { w: 1920, h: 1280 } }, 24_000_000),
115
+ ).toEqual([800, 1000])
116
+ expect(captureDimensions([800, 1000], { supersample: 2 }, 24_000_000)).toEqual([1600, 2000])
117
+ expect(captureDimensions([6000, 4000], { supersample: 2 }, 6_000_000)).toEqual([3000, 2000])
118
+ })
119
+
120
+ test('renders at its own size while the live canvas keeps its size', () => {
121
+ // a 1.5 dpr screen: the old capture resized the live canvas with drawing
122
+ // buffer pixels where CSS pixels were due — 3× during the capture, 1.5×
123
+ // after it, compounding capture after capture
124
+ const live: FakeCanvas = { width: 1156, height: 791, style: {} }
125
+ const renderer = fakeRenderer(live, 1.5)
126
+ const liveTarget = renderer.getCanvasTarget()
127
+
128
+ const seen = atCaptureSize(renderer, 2312, 1582, () => {
129
+ expect(live.width).toBe(1156)
130
+ expect(live.height).toBe(791)
131
+ return {
132
+ buffer: renderer.getDrawingBufferSize(new Vector2()).toArray(),
133
+ canvas: [renderer.domElement.width, renderer.domElement.height],
134
+ }
135
+ })
136
+
137
+ expect(seen.buffer).toEqual([2312, 1582])
138
+ expect(seen.canvas).toEqual([2312, 1582])
139
+ expect(renderer.getCanvasTarget()).toBe(liveTarget)
140
+ expect([live.width, live.height]).toEqual([1156, 791])
141
+ expect(renderer.getDrawingBufferSize(new Vector2()).toArray()).toEqual([1156, 791])
142
+ })
143
+
144
+ test('puts the live canvas target back when the render throws', () => {
145
+ const live: FakeCanvas = { width: 800, height: 600, style: {} }
146
+ const renderer = fakeRenderer(live, 1)
147
+ const liveTarget = renderer.getCanvasTarget()
148
+ expect(() =>
149
+ atCaptureSize(renderer, 1600, 1200, () => {
150
+ throw new Error('pipeline failed')
151
+ }),
152
+ ).toThrow('pipeline failed')
153
+ expect(renderer.getCanvasTarget()).toBe(liveTarget)
154
+ expect([live.width, live.height]).toEqual([800, 600])
155
+ })
156
+
157
+ test('a capture at the canvas size renders through the live target', () => {
158
+ const live: FakeCanvas = { width: 800, height: 600, style: {} }
159
+ const renderer = fakeRenderer(live, 1)
160
+ const liveTarget = renderer.getCanvasTarget()
161
+ const during = atCaptureSize(renderer, 800, 600, () => renderer.getCanvasTarget())
162
+ expect(during).toBe(liveTarget)
163
+ })
164
+ })
165
+
166
+ /** R3F's `advance` with `frameloop="never"`: the delta is the timestamp less the clock's elapsed time. */
167
+ function fakeLoop(startSeconds: number) {
168
+ const clock = { elapsedTime: startSeconds, oldTime: startSeconds }
169
+ const deltas: number[] = []
170
+ const advance = (timestamp: number) => {
171
+ deltas.push(timestamp - clock.elapsedTime)
172
+ clock.oldTime = clock.elapsedTime
173
+ clock.elapsedTime = timestamp
174
+ }
175
+ return { clock, deltas, advance }
176
+ }
177
+
178
+ describe('frames a capture runs by hand', () => {
179
+ test('never hand the next real frame a delta it did not earn', async () => {
180
+ // the viewer's frame limiter drives R3F's clock in seconds since it
181
+ // mounted; performance.now() is milliseconds since the page loaded
182
+ const loop = fakeLoop(330.62)
183
+ await pumpFrames(loop.advance, loop.clock, 2)
184
+ // the frame limiter's next frame, one 50 fps interval on
185
+ loop.advance(330.64)
186
+ expect(loop.deltas).toHaveLength(3)
187
+ expect(loop.deltas[0]).toBeCloseTo(0.001, 9)
188
+ expect(loop.deltas[1]).toBeCloseTo(0.001, 9)
189
+ expect(loop.deltas[2]).toBeCloseTo(0.02, 9)
190
+ expect(loop.clock.elapsedTime).toBe(330.64)
191
+ })
192
+
193
+ test('put the clock back when a frame throws', async () => {
194
+ const loop = fakeLoop(12)
195
+ const failing = () => {
196
+ throw new Error('a system failed')
197
+ }
198
+ await expect(pumpFrames(failing, loop.clock, 1)).rejects.toThrow('a system failed')
199
+ expect(loop.clock.elapsedTime).toBe(12)
200
+ })
201
+ })
202
+
203
+ describe('a sheet capture session', () => {
204
+ test('shows the finished house whatever view the user was in, then puts the view back', () => {
205
+ const viewer = useViewer.getState()
206
+ viewer.setWallMode('down')
207
+ viewer.setLevelMode('exploded')
208
+ viewer.setShading('solid')
209
+ viewer.setTextures(false)
210
+ viewer.setColorPreset('white')
211
+ const view = () => {
212
+ const { wallMode, levelMode, shading, textures, colorPreset } = useViewer.getState()
213
+ return { wallMode, levelMode, shading, textures, colorPreset }
214
+ }
215
+ // the shading stays the user's: the capture brings its own AO
216
+
217
+ const restore = presentFinished()
218
+ expect(view()).toEqual({
219
+ wallMode: 'up',
220
+ levelMode: 'stacked',
221
+ shading: 'solid',
222
+ textures: true,
223
+ colorPreset: 'clay',
224
+ })
225
+
226
+ restore()
227
+ expect(view()).toEqual({
228
+ wallMode: 'down',
229
+ levelMode: 'exploded',
230
+ shading: 'solid',
231
+ textures: false,
232
+ colorPreset: 'white',
233
+ })
234
+ })
235
+
236
+ test('leaves a view already finished untouched', () => {
237
+ const viewer = useViewer.getState()
238
+ viewer.setWallMode('up')
239
+ viewer.setLevelMode('stacked')
240
+ viewer.setTextures(true)
241
+ viewer.setColorPreset('clay')
242
+ let writes = 0
243
+ const unsubscribe = useViewer.subscribe(() => {
244
+ writes++
245
+ })
246
+ presentFinished()()
247
+ unsubscribe()
248
+ expect(writes).toBe(0)
249
+ })
250
+
251
+ // A sheet's pictures and an agent's view can overlap: the user's view is the one before the
252
+ // first, never a run's finished presentation.
253
+ test('runs that overlap present once and put the view back after the last', () => {
254
+ const session = (owner: string, active: boolean) =>
255
+ emitter.emit('camera-controls:capture-session', { owner, active })
256
+ const steps: string[] = []
257
+ const stop = presentWhileCapturing(() => {
258
+ steps.push('present')
259
+ return () => steps.push('restore')
260
+ })
261
+ session('sheets-a', true)
262
+ session('view-b', true)
263
+ session('sheets-a', false)
264
+ expect(steps).toEqual(['present'])
265
+ session('nobody', false)
266
+ session('view-b', false)
267
+ expect(steps).toEqual(['present', 'restore'])
268
+ session('view-c', true)
269
+ stop()
270
+ expect(steps).toEqual(['present', 'restore', 'present', 'restore'])
271
+ session('view-c', false)
272
+ expect(steps).toHaveLength(4)
273
+ })
274
+
275
+ // The generator mounted while a sheet captures (a remount, the editor opened mid-run): the run's
276
+ // finished presentation from the first frame, the user's view back at its end.
277
+ test('a run already on when the generator mounts is presented at once', () => {
278
+ emitter.emit('camera-controls:capture-session', { owner: 'sheets-early', active: true })
279
+ const steps: string[] = []
280
+ const stop = presentWhileCapturing(() => {
281
+ steps.push('present')
282
+ return () => steps.push('restore')
283
+ })
284
+ expect(steps).toEqual(['present'])
285
+ emitter.emit('camera-controls:capture-session', { owner: 'sheets-early', active: false })
286
+ expect(steps).toEqual(['present', 'restore'])
287
+ stop()
288
+ expect(steps).toEqual(['present', 'restore'])
289
+ })
290
+ })
291
+
292
+ describe('a print-size picture past one render pass', () => {
293
+ test('fits one pass: one tile, the whole picture', () => {
294
+ expect(tileGrid(3209, 2588, 4096)).toEqual([
295
+ { x: 0, y: 0, w: 3209, h: 2588, inner: { x: 0, y: 0, w: 3209, h: 2588 } },
296
+ ])
297
+ })
298
+
299
+ test('splits into equal tiles that cover every pixel once, each rendering past its seams', () => {
300
+ // a 200 ppi elevation on Arch D: 22.1 in across
301
+ const width = 4423
302
+ const height = 3567
303
+ const tiles = tileGrid(width, height, 4096)
304
+ expect(tiles).toHaveLength(2)
305
+ let covered = 0
306
+ for (const tile of tiles) {
307
+ expect(tile.w).toBeLessThanOrEqual(4096)
308
+ expect(tile.h).toBeLessThanOrEqual(4096)
309
+ // the rendered rect holds the contributed one, on the picture
310
+ expect(tile.x).toBeLessThanOrEqual(tile.inner.x)
311
+ expect(tile.x + tile.w).toBeGreaterThanOrEqual(tile.inner.x + tile.inner.w)
312
+ expect(tile.x).toBeGreaterThanOrEqual(0)
313
+ expect(tile.x + tile.w).toBeLessThanOrEqual(width)
314
+ covered += tile.inner.w * tile.inner.h
315
+ }
316
+ expect(covered).toBe(width * height)
317
+ const [left, right] = tiles as [(typeof tiles)[number], (typeof tiles)[number]]
318
+ expect(left.inner.x + left.inner.w).toBe(right.inner.x)
319
+ // the seam: each side renders the other's first pixels too
320
+ expect(left.x + left.w - (left.inner.x + left.inner.w)).toBeGreaterThan(0)
321
+ expect(right.inner.x - right.x).toBeGreaterThan(0)
322
+ })
323
+ })
324
+
325
+ describe('an interior view of a floor', () => {
326
+ /** A two-storey house as the editor holds it: the scene graph, and each node's object. */
327
+ function house() {
328
+ const ground = LevelNode.parse({ id: 'level_ground', parentId: 'building_h', level: 0 })
329
+ const upper = LevelNode.parse({ id: 'level_upper', parentId: 'building_h', level: 1 })
330
+ const building = BuildingNode.parse({ id: 'building_h', children: [ground.id, upper.id] })
331
+ const wall = WallNode.parse({
332
+ id: 'wall_ground',
333
+ parentId: ground.id,
334
+ start: [0, 0],
335
+ end: [6, 0],
336
+ })
337
+ const ceiling = CeilingNode.parse({
338
+ id: 'ceiling_ground',
339
+ parentId: ground.id,
340
+ polygon: [
341
+ [0, 0],
342
+ [6, 0],
343
+ [6, 4],
344
+ [0, 4],
345
+ ],
346
+ })
347
+ ground.children = [wall.id, ceiling.id]
348
+ const nodes = Object.fromEntries(
349
+ [building, ground, upper, wall, ceiling].map((node) => [node.id, node]),
350
+ ) as Record<string, AnyNode>
351
+ useScene.setState({ nodes } as never)
352
+ const objects = Object.fromEntries(
353
+ [ground, upper, ceiling].map((node) => [node.id, new Group()]),
354
+ ) as Record<string, Group>
355
+ const wallMesh = new Mesh()
356
+ wallMesh.layers.set(SCENE_LAYER)
357
+ objects.level_ground!.add(wallMesh, objects.ceiling_ground!)
358
+ for (const [id, object] of Object.entries({ ...objects, wall_ground: wallMesh }))
359
+ sceneRegistry.nodes.set(id, object)
360
+ return { objects, wallMesh }
361
+ }
362
+
363
+ afterEach(() => {
364
+ for (const id of ['level_ground', 'level_upper', 'ceiling_ground', 'wall_ground'])
365
+ sceneRegistry.nodes.delete(id)
366
+ })
367
+
368
+ test('leaves the rest of the house out and cuts the floor, for the one render', () => {
369
+ const { objects, wallMesh } = house()
370
+ hideFromScene(wallMesh, 'isolated')
371
+ const restorers: (() => void)[] = []
372
+ interiorCutaway('level_ground', (restore) => restorers.push(restore))
373
+
374
+ expect(objects.level_upper!.visible).toBe(false)
375
+ expect(objects.ceiling_ground!.visible).toBe(false)
376
+ const [cut] = objects.level_ground!.children as ClippingGroup[]
377
+ expect(objects.level_ground!.children).toHaveLength(1)
378
+ expect(cut).toBeInstanceOf(ClippingGroup)
379
+ expect(cut!.clipShadows).toBe(true)
380
+ // Walls stand below the plane: y = 0.65 × a 2.5 m storey.
381
+ const [plane] = cut!.clippingPlanes
382
+ expect(plane!.normal.toArray()).toEqual([0, -1, 0])
383
+ expect(plane!.constant).toBeCloseTo(1.625, 6)
384
+ expect(cut!.children).toContain(wallMesh)
385
+ // The user isolated something else: the floor still shows whole.
386
+ expect(wallMesh.layers.test(new Layers())).toBe(true)
387
+
388
+ for (const restore of restorers.reverse()) restore()
389
+ expect(objects.level_upper!.visible).toBe(true)
390
+ expect(objects.ceiling_ground!.visible).toBe(true)
391
+ expect(objects.level_ground!.children).toEqual([wallMesh, objects.ceiling_ground!])
392
+ expect(wallMesh.layers.test(new Layers())).toBe(false)
393
+ })
394
+
395
+ test('refuses a floor the editor has not built', () => {
396
+ house()
397
+ sceneRegistry.nodes.delete('level_ground')
398
+ expect(() => interiorCutaway('level_ground', () => {})).toThrow('not built yet')
399
+ })
400
+ })
401
+
402
+ // What a capture's own camera sees of the ground, on the owner's scenes (13 and 14 Oct, trimmed to
403
+ // their levels, slabs and walls) and a review's plate: the basement's floor and not the lawn, the
404
+ // window opened for the frame (and the person's own given back), and nothing opened from the
405
+ // garden side. Each test runs the cutaway as the capture does, looks, and restores.
406
+ describe('what a capture sees of the ground', () => {
407
+ let previousScene: ReturnType<typeof useScene.getState>
408
+ beforeEach(() => {
409
+ previousScene = useScene.getState()
410
+ })
411
+ afterEach(() => {
412
+ sceneRegistry.clear()
413
+ useScene.setState(previousScene, true)
414
+ })
415
+
416
+ /**
417
+ * The scene as the editor holds it: every level's object at its height, a floor in `basement`, and
418
+ * the site's lawn (wider than any frame) with its veil, hidden. The veil is never picked, as
419
+ * GroundVeil mounts it: the ray below sees through it.
420
+ */
421
+ function editorScene(
422
+ nodes: Record<AnyNodeId, AnyNode>,
423
+ basement?: string,
424
+ siteId = 'site_owner',
425
+ ) {
426
+ useScene.setState({ nodes } as never)
427
+ const scene = new Scene()
428
+ for (const [levelId, { baseY }] of getLevelElevations(nodes)) {
429
+ const level = new Group()
430
+ level.position.y = baseY
431
+ scene.add(level)
432
+ sceneRegistry.nodes.set(levelId, level)
433
+ }
434
+ const floor = new Mesh(new PlaneGeometry(400, 400).rotateX(-Math.PI / 2))
435
+ if (basement) sceneRegistry.nodes.get(basement)?.add(floor)
436
+ const ground = new Mesh(new PlaneGeometry(400, 400).rotateX(-Math.PI / 2))
437
+ ground.userData[SITE_GROUND] = 'solid'
438
+ const veil = new Mesh(ground.geometry)
439
+ veil.userData[SITE_GROUND] = 'veil'
440
+ veil.visible = false
441
+ veil.raycast = () => {}
442
+ const lotLine = new Mesh()
443
+ const site = new Group().add(ground, veil, lotLine)
444
+ scene.add(site)
445
+ sceneRegistry.nodes.set(siteId, site)
446
+ sceneRegistry.byType.site?.add(siteId)
447
+ return { scene, floor, ground, veil, lotLine }
448
+ }
449
+
450
+ /** Opens the capture's ground cutaway, runs `look`, restores in reverse; returns what `look` returns. */
451
+ function whileCut<T>(open: (add: (restore: () => void) => void) => void, look: () => T): T {
452
+ const restorers: (() => void)[] = []
453
+ open((restore) => restorers.push(restore))
454
+ try {
455
+ return look()
456
+ } finally {
457
+ for (const restore of restorers.reverse()) restore()
458
+ }
459
+ }
460
+
461
+ /** The first thing the capture camera draws on the line from its eye to its aim. */
462
+ function firstSeen(scene: Scene, pose: SceneViewPose): Object3D | undefined {
463
+ const eye = new Vector3(...pose.position)
464
+ const ray = new Raycaster(eye, new Vector3(...pose.target).sub(eye).normalize())
465
+ ray.layers.set(SCENE_LAYER)
466
+ scene.updateMatrixWorld(true)
467
+ const drawn = (object: Object3D | null): boolean =>
468
+ !object || (object.visible && drawn(object.parent))
469
+ return ray.intersectObject(scene, true).find((hit) => drawn(hit.object))?.object
470
+ }
471
+
472
+ const cameraAt = (position: [number, number, number]) => {
473
+ const camera = new PerspectiveCamera()
474
+ camera.position.set(...position)
475
+ return camera
476
+ }
477
+ const persons = {
478
+ min: [-6, 0, -7] as [number, number, number],
479
+ max: [6, 3, 7] as [number, number, number],
480
+ }
481
+
482
+ // The villa's Lower Ground 3.35 m under the lawn, the Victor's parking 2.61 m under it and past
483
+ // its west lot line.
484
+ test.each([
485
+ ['the villa Aurelia', villaAurelia, 'level_68jf2zqaukfjof3m', 'level_ztimtvrgdzzd6c9f'],
486
+ ['the Victor Seattle', victorSeattle, 'level_q209tmgwuzcahwzj', 'level_wj5tz664u03eolux'],
487
+ ] as const)(
488
+ "the agent's picture of %s's basement sees its floor, not the lawn; a floor at grade is pictured on solid ground, even veiled",
489
+ (_, owner, basement, atGrade) => {
490
+ const nodes = owner.nodes as unknown as Record<AnyNodeId, AnyNode>
491
+ const plan = sceneViewPlan(nodes, { target: basement })
492
+ const { scene, floor, ground, veil, lotLine } = editorScene(nodes, basement)
493
+ expect(firstSeen(scene, plan.pose)).toBe(ground)
494
+ whileCut(
495
+ (add) => interiorCutaway(basement, add),
496
+ () => {
497
+ expect(firstSeen(scene, plan.pose)).toBe(floor)
498
+ expect(veil.visible).toBe(true)
499
+ expect(lotLine.layers.isEnabled(SCENE_LAYER)).toBe(true)
500
+ },
501
+ )
502
+ expect(firstSeen(scene, plan.pose)).toBe(ground)
503
+ expect(veil.visible).toBe(false)
504
+
505
+ // A floor at grade while the person has the ground veiled and aside.
506
+ hideFromScene(ground, 'ground-aside')
507
+ veil.visible = true
508
+ whileCut(
509
+ (add) => interiorCutaway(atGrade, add),
510
+ () => {
511
+ expect(ground.layers.isEnabled(SCENE_LAYER)).toBe(true)
512
+ expect(veil.visible).toBe(false)
513
+ },
514
+ )
515
+ expect(ground.layers.isEnabled(SCENE_LAYER)).toBe(false)
516
+ expect(veil.visible).toBe(true)
517
+ },
518
+ )
519
+
520
+ // The owner's hillside house (14 Oct): no floor buried, a 6 m plateau on the street side. A
521
+ // capture from the street sees the house through the ground window, from its own camera,
522
+ // whatever the person's view holds; from the garden side the ground stands solid.
523
+ describe('on the hillside house', () => {
524
+ const nodes = hillside.nodes as unknown as Record<AnyNodeId, AnyNode>
525
+ const garden = Object.values(nodes).find(
526
+ (node) => node.type === 'level' && node.name === 'Garden level · walk-out',
527
+ )
528
+ if (!garden) throw new Error('the hillside fixture is missing its garden level')
529
+
530
+ test('from the street side the window opens for the frame and closes after it, the solid ground staying drawn; from the garden side none opens', () => {
531
+ const { ground, veil } = editorScene(nodes, undefined, 'site_hillside')
532
+ whileCut(
533
+ (add) => interiorCutaway(garden.id, add, cameraAt([0, 7, -20])),
534
+ () => {
535
+ expect(groundWindowBoxNow()).not.toBeNull()
536
+ expect(veil.visible).toBe(true)
537
+ // Its window is cut per fragment, not by hiding it.
538
+ expect(ground.layers.isEnabled(SCENE_LAYER)).toBe(true)
539
+ },
540
+ )
541
+ expect(groundWindowBoxNow()).toBeNull()
542
+ expect(veil.visible).toBe(false)
543
+ whileCut(
544
+ (add) => interiorCutaway(garden.id, add, cameraAt([0, 3, 20])),
545
+ () => {
546
+ expect(groundWindowBoxNow()).toBeNull()
547
+ expect(veil.visible).toBe(false)
548
+ },
549
+ )
550
+ })
551
+
552
+ // Both agent lanes ask with a perspective/orthographic pose of their own, not a cameraPose:
553
+ // the posed exterior frame decides its window from its own eye, not the person's closed one.
554
+ test('a posed exterior capture decides its own window and gives the person’s back; an unposed one decides none', () => {
555
+ const { veil } = editorScene(nodes, undefined, 'site_hillside')
556
+ whileCut(
557
+ (add) => exteriorGroundForCapture(add, true, cameraAt([0, 7, -20])),
558
+ () => {
559
+ expect(groundWindowBoxNow()).not.toBeNull()
560
+ expect(veil.visible).toBe(true)
561
+ },
562
+ )
563
+ expect(groundWindowBoxNow()).toBeNull()
564
+ expect(veil.visible).toBe(false)
565
+ // The person's window, open for their eye, closes for a garden-side frame and returns; an
566
+ // unposed capture leaves it alone.
567
+ setGroundWindow(persons)
568
+ whileCut(
569
+ (add) => exteriorGroundForCapture(add, true, cameraAt([0, 3, 20])),
570
+ () => expect(groundWindowBoxNow()).toBeNull(),
571
+ )
572
+ expect(groundWindowBoxNow()).toBe(persons)
573
+ whileCut(
574
+ (add) => exteriorGroundForCapture(add, false, cameraAt([0, 3, 20])),
575
+ () => expect(groundWindowBoxNow()).toBe(persons),
576
+ )
577
+ expect(groundWindowBoxNow()).toBe(persons)
578
+ setGroundWindow(null)
579
+ })
580
+ })
581
+
582
+ // The review's plate (14 Oct): [-5,5] × [-4,4] at Y 0.2, terrain 2 m only inside x 0…4, z -3…3.
583
+ // An interior capture from overhead opens the window over the buried eastern rooms: partly below
584
+ // grade has no wholesale veil to fall back on.
585
+ test('from overhead the window opens over the buried rooms of a floor partly below grade, and closes after', () => {
586
+ const field = createTerrainField({ cols: 25, rows: 25, spacing: 1, origin: [-12, -12] })
587
+ const heights = new Int16Array(field.heights)
588
+ for (let row = 0; row < field.rows; row += 1)
589
+ for (let col = 0; col < field.cols; col += 1) {
590
+ const x = field.origin[0] + col * field.spacing
591
+ const z = field.origin[1] + row * field.spacing
592
+ if (x >= 0 && x <= 4 && z >= -3 && z <= 3) heights[row * field.cols + col] = 200
593
+ }
594
+ const list: AnyNode[] = [
595
+ SiteNode.parse({
596
+ id: 'site_a',
597
+ children: ['building_a'],
598
+ terrain: encodeTerrainField({ ...field, heights }),
599
+ }),
600
+ BuildingNode.parse({ id: 'building_a', parentId: 'site_a', children: ['level_0'] }),
601
+ LevelNode.parse({
602
+ id: 'level_0',
603
+ level: 0,
604
+ height: 3,
605
+ parentId: 'building_a',
606
+ children: ['slab_0'],
607
+ }),
608
+ SlabNode.parse({
609
+ id: 'slab_0',
610
+ parentId: 'level_0',
611
+ elevation: 0.2,
612
+ polygon: [
613
+ [-5, -4],
614
+ [5, -4],
615
+ [5, 4],
616
+ [-5, 4],
617
+ ],
618
+ }),
619
+ ]
620
+ const { ground, veil } = editorScene(
621
+ Object.fromEntries(list.map((node) => [node.id, node])) as Record<AnyNodeId, AnyNode>,
622
+ undefined,
623
+ 'site_a',
624
+ )
625
+ whileCut(
626
+ (add) => interiorCutaway('level_0', add, cameraAt([0, 15, 0])),
627
+ () => {
628
+ expect(groundWindowBoxNow()).not.toBeNull()
629
+ expect(veil.visible).toBe(true)
630
+ expect(ground.layers.isEnabled(SCENE_LAYER)).toBe(true)
631
+ },
632
+ )
633
+ expect(groundWindowBoxNow()).toBeNull()
634
+ expect(veil.visible).toBe(false)
635
+ })
636
+ })
637
+
638
+ describe("a capture's projection", () => {
639
+ const user = { ortho: new OrthographicCamera(), perspective: new PerspectiveCamera() }
640
+ const view = { position: [8, 6, 8], target: [0, 1, 0] } as never
641
+
642
+ test("a perspective view asked for stays perspective under the user's orthographic camera", () => {
643
+ // Else view_scene's planned camera, a look into a floor from 55° or a photo's, would give
644
+ // way to the user's own orthographic view while the result reports the planned one.
645
+ expect(capturesOrthographically({ perspective: view }, user.ortho)).toBe(false)
646
+ expect(capturesOrthographically({ ortho: view }, user.perspective)).toBe(true)
647
+ })
648
+
649
+ test("with no view of its own, a capture takes the user's projection", () => {
650
+ expect(capturesOrthographically({}, user.ortho)).toBe(true)
651
+ expect(capturesOrthographically({}, user.perspective)).toBe(false)
652
+ expect(capturesOrthographically({ snapLevels: true }, user.ortho)).toBe(false)
653
+ })
654
+ })