@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,580 @@
1
+ import {
2
+ type AnyNode,
3
+ area,
4
+ type BuildingNode,
5
+ buildUnitReport,
6
+ type CameraControlFrameEvent,
7
+ containsPoint,
8
+ DEFAULT_LEVEL_HEIGHT,
9
+ emitter,
10
+ getLevelDisplayName,
11
+ type LevelNode,
12
+ type NodeEvent,
13
+ resolveSelectionProxyId,
14
+ sceneRegistry,
15
+ type UnitNode,
16
+ type ZoneNode,
17
+ } from '@pascal-app/core'
18
+ import { useViewer } from '@pascal-app/viewer'
19
+ import type { CameraControlsImpl } from '@react-three/drei'
20
+ import { useEffect } from 'react'
21
+ import {
22
+ Box3,
23
+ type Camera,
24
+ type Object3D,
25
+ type OrthographicCamera,
26
+ type PerspectiveCamera,
27
+ Sphere,
28
+ Vector3,
29
+ } from 'three'
30
+ import { useBelowGrade } from './below-grade'
31
+ import { nodeDisplayLabel } from './node-display-label'
32
+ import { roomFloorElevation } from './room-handle-drag'
33
+ import { RoomSelectionIndex, type RoomSelectionRecord, resolveRoomHit } from './room-selection'
34
+ import type { SelectionModifierKeys } from './selection-routing'
35
+
36
+ // One selection model for every viewer surface: the published viewer (baked
37
+ // GLB or parametric), its embed and the editor's Preview. Each renderer only
38
+ // turns a pointer hit into a `ViewerPick`; these rules decide what it selects,
39
+ // on `useViewer.selection`:
40
+ // levelId the current floor
41
+ // zoneId the selected room (or zone)
42
+ // selectedIds the selected element or item
43
+
44
+ export type ViewerNodes = Readonly<Record<string, AnyNode>>
45
+
46
+ /** The scene a viewer surface shows: its graph, and the 3D object standing for an id. */
47
+ export type ViewerSource = {
48
+ nodes: ViewerNodes
49
+ objectFor: (id: string) => Object3D | undefined
50
+ }
51
+
52
+ /** A pointer hit, whichever renderer took it: the node and the hit in its level's local XZ. */
53
+ export type ViewerPick = { nodeId: string; point: readonly [number, number] | null }
54
+
55
+ export type ViewerSelection = {
56
+ buildingId: BuildingNode['id'] | null
57
+ levelId: LevelNode['id'] | null
58
+ zoneId: ZoneNode['id'] | null
59
+ selectedIds: string[]
60
+ }
61
+
62
+ type ViewerTarget =
63
+ | { kind: 'room'; levelId: string; zoneId: string }
64
+ | { kind: 'node'; levelId: string | null; nodeId: string; zoneId: string | null }
65
+
66
+ const NO_MODIFIERS: SelectionModifierKeys = { alt: false, ctrl: false, meta: false, shift: false }
67
+
68
+ /** Surfaces a room is picked through, room first: its walls, its floor, its ceiling. */
69
+ const ROOM_SURFACES = new Set(['wall', 'slab', 'ceiling'])
70
+
71
+ /** Kinds a viewer click never lands on: they pass the pointer to what lies behind. */
72
+ export const VIEWER_PASS_THROUGH_KINDS: ReadonlySet<string> = new Set([
73
+ 'site',
74
+ 'building',
75
+ 'level',
76
+ 'zone',
77
+ 'ceiling',
78
+ 'scan',
79
+ 'guide',
80
+ 'spawn',
81
+ ])
82
+
83
+ const indexes = new WeakMap<ViewerNodes, Map<string, RoomSelectionIndex>>()
84
+
85
+ function roomIndex(levelId: string, nodes: ViewerNodes) {
86
+ let levels = indexes.get(nodes)
87
+ if (!levels) {
88
+ levels = new Map()
89
+ indexes.set(nodes, levels)
90
+ }
91
+ let index = levels.get(levelId)
92
+ if (!index) {
93
+ index = new RoomSelectionIndex(levelId)
94
+ levels.set(levelId, index)
95
+ }
96
+ index.update(nodes as Record<string, AnyNode>)
97
+ return index
98
+ }
99
+
100
+ /** The rooms the walls of a level close, as the editor detects them. */
101
+ export function viewerRooms(levelId: string, nodes: ViewerNodes): RoomSelectionRecord[] {
102
+ return roomIndex(levelId, nodes).update(nodes as Record<string, AnyNode>)
103
+ }
104
+
105
+ export function levelOfNode(nodeId: string, nodes: ViewerNodes): string | null {
106
+ const seen = new Set<string>()
107
+ let current = nodes[nodeId]
108
+ while (current && !seen.has(current.id)) {
109
+ if (current.type === 'level') return current.id
110
+ seen.add(current.id)
111
+ current = current.parentId ? nodes[current.parentId] : undefined
112
+ }
113
+ return null
114
+ }
115
+
116
+ function buildingOfLevel(levelId: string | null, nodes: ViewerNodes): BuildingNode['id'] | null {
117
+ const level = levelId ? nodes[levelId] : undefined
118
+ const parent = level?.parentId ? nodes[level.parentId] : undefined
119
+ if (parent?.type === 'building') return parent.id
120
+ const building = Object.values(nodes).find((node) => node.type === 'building')
121
+ return (building?.id as BuildingNode['id'] | undefined) ?? null
122
+ }
123
+
124
+ const hitPoint = new Vector3()
125
+
126
+ /**
127
+ * The parametric hit source: a node event as a pick, with the hit in its
128
+ * level's local XZ. A hit on a hidden floor, or on a kind a click passes
129
+ * through, is no pick: the pointer goes on to what lies behind it.
130
+ */
131
+ export function viewerPickFromNodeEvent(event: NodeEvent, nodes: ViewerNodes): ViewerPick | null {
132
+ if (VIEWER_PASS_THROUGH_KINDS.has(event.node.type)) return null
133
+ const levelId = levelOfNode(event.node.id, nodes)
134
+ const level = levelId ? sceneRegistry.nodes.get(levelId) : undefined
135
+ if (level?.visible === false) return null
136
+ if (!level) return { nodeId: event.node.id, point: null }
137
+ level.updateWorldMatrix(true, false)
138
+ hitPoint.set(...event.position)
139
+ level.worldToLocal(hitPoint)
140
+ return { nodeId: event.node.id, point: [hitPoint.x, hitPoint.z] }
141
+ }
142
+
143
+ /**
144
+ * What a pick acts on. A wall, floor or ceiling picks the room it bounds;
145
+ * once that room is selected, its own surfaces are picked as themselves.
146
+ * Anything else (an item, a door, a stair) is picked directly. Alt, Shift,
147
+ * Ctrl and Meta skip the room, as in the editor.
148
+ */
149
+ export function resolveViewerTarget(
150
+ selection: Pick<ViewerSelection, 'zoneId'>,
151
+ pick: ViewerPick,
152
+ nodes: ViewerNodes,
153
+ modifiers: SelectionModifierKeys = NO_MODIFIERS,
154
+ ): ViewerTarget | null {
155
+ const node = nodes[pick.nodeId]
156
+ if (!node || VIEWER_PASS_THROUGH_KINDS.has(node.type)) return null
157
+ const levelId = levelOfNode(node.id, nodes)
158
+ const bypass = modifiers.alt || modifiers.shift || modifiers.ctrl || modifiers.meta
159
+ if (!bypass && levelId && pick.point && ROOM_SURFACES.has(node.type)) {
160
+ const room = resolveRoomHit(
161
+ roomIndex(levelId, nodes),
162
+ levelId,
163
+ node,
164
+ [pick.point[0], pick.point[1]],
165
+ '3d',
166
+ )
167
+ if (room) {
168
+ const { zoneId } = room.key
169
+ return selection.zoneId === zoneId
170
+ ? { kind: 'node', levelId, nodeId: node.id, zoneId }
171
+ : { kind: 'room', levelId, zoneId }
172
+ }
173
+ }
174
+ const parent = node.parentId ? nodes[node.parentId] : undefined
175
+ const owner = node.type === 'roof-segment' && parent?.type === 'roof' ? parent : node
176
+ const nodeId = resolveSelectionProxyId(owner, nodes as Record<string, AnyNode | undefined>)
177
+ return { kind: 'node', levelId, nodeId, zoneId: null }
178
+ }
179
+
180
+ /**
181
+ * The selection a click makes. Empty space clears the room and the element
182
+ * and keeps the floor; nothing pops the user up a level.
183
+ */
184
+ export function resolveViewerClick(
185
+ selection: ViewerSelection,
186
+ pick: ViewerPick | null,
187
+ nodes: ViewerNodes,
188
+ modifiers: SelectionModifierKeys = NO_MODIFIERS,
189
+ ): ViewerSelection {
190
+ const target = pick ? resolveViewerTarget(selection, pick, nodes, modifiers) : null
191
+ if (!target) return { ...selection, zoneId: null, selectedIds: [] }
192
+ const levelId = (target.levelId as LevelNode['id'] | null) ?? selection.levelId
193
+ const buildingId = buildingOfLevel(levelId, nodes) ?? selection.buildingId
194
+ if (target.kind === 'room') {
195
+ return { buildingId, levelId, zoneId: target.zoneId as ZoneNode['id'], selectedIds: [] }
196
+ }
197
+ const toggles = modifiers.shift || modifiers.ctrl || modifiers.meta
198
+ const sameFloor = levelId === selection.levelId
199
+ const selectedIds =
200
+ toggles && sameFloor
201
+ ? selection.selectedIds.includes(target.nodeId)
202
+ ? selection.selectedIds.filter((id) => id !== target.nodeId)
203
+ : [...selection.selectedIds, target.nodeId]
204
+ : [target.nodeId]
205
+ return {
206
+ buildingId,
207
+ levelId,
208
+ zoneId: target.zoneId as ZoneNode['id'] | null,
209
+ selectedIds,
210
+ }
211
+ }
212
+
213
+ /** What a hover would select: a room's id (its highlight) or the element's (its outline). */
214
+ export function resolveViewerHover(
215
+ selection: Pick<ViewerSelection, 'zoneId'>,
216
+ pick: ViewerPick | null,
217
+ nodes: ViewerNodes,
218
+ ): string | null {
219
+ const target = pick ? resolveViewerTarget(selection, pick, nodes) : null
220
+ if (!target) return null
221
+ return target.kind === 'room' ? target.zoneId : target.nodeId
222
+ }
223
+
224
+ /**
225
+ * The current floor reads as the editor shows it: the levels above it step
226
+ * aside (in the stacked display) so its rooms are in view and in reach, and the
227
+ * ground is veiled over it when it lies below grade. Mount it in the surface's DOM tree,
228
+ * beside the editor's own level display, so one hands the flag to the other in
229
+ * order.
230
+ */
231
+ export function useViewerFloorDisplay() {
232
+ // A walkthrough tours the whole building.
233
+ const walking = useViewer((s) => s.walkthroughMode)
234
+ useBelowGrade(!walking)
235
+ useEffect(() => {
236
+ if (walking) return
237
+ useViewer.setState({ hideLevelsAboveSelection: true })
238
+ return () => {
239
+ useViewer.setState({ hideLevelsAboveSelection: false })
240
+ }
241
+ }, [walking])
242
+ }
243
+
244
+ // ── Applying it to the shared store ──────────────────────────────────────────
245
+
246
+ function writeSelection(selection: ViewerSelection) {
247
+ const viewer = useViewer.getState()
248
+ viewer.setSelection(selection)
249
+ if (viewer.focusedUnitId) viewer.setFocusedUnit(null)
250
+ viewer.setHoveredId(null)
251
+ }
252
+
253
+ export function applyViewerClick(
254
+ pick: ViewerPick | null,
255
+ nodes: ViewerNodes,
256
+ modifiers: SelectionModifierKeys = NO_MODIFIERS,
257
+ ) {
258
+ writeSelection(resolveViewerClick(useViewer.getState().selection, pick, nodes, modifiers))
259
+ }
260
+
261
+ export function applyViewerHover(pick: ViewerPick | null, nodes: ViewerNodes) {
262
+ const viewer = useViewer.getState()
263
+ const hovered = resolveViewerHover(viewer.selection, pick, nodes)
264
+ viewer.setHoveredId(hovered as never)
265
+ }
266
+
267
+ /** A floor from the floor list: the floor alone, framed. */
268
+ export function selectViewerLevel(levelId: string, source: ViewerSource) {
269
+ writeSelection({
270
+ buildingId: buildingOfLevel(levelId, source.nodes),
271
+ levelId: levelId as LevelNode['id'],
272
+ zoneId: null,
273
+ selectedIds: [],
274
+ })
275
+ frameViewerNode(levelId, source)
276
+ }
277
+
278
+ /** The whole building again, framed. */
279
+ export function selectViewerBuilding(source: ViewerSource) {
280
+ const buildingId = buildingOfLevel(null, source.nodes)
281
+ writeSelection({ buildingId, levelId: null, zoneId: null, selectedIds: [] })
282
+ if (buildingId) frameViewerNode(buildingId, source)
283
+ }
284
+
285
+ /** A room or zone from its pill, the bar or the sidebar: selected on its floor, framed. */
286
+ export function selectViewerRoom(zoneId: string, source: ViewerSource) {
287
+ const zone = source.nodes[zoneId]
288
+ if (zone?.type !== 'zone' || !zone.parentId) return
289
+ const levelId = zone.parentId as LevelNode['id']
290
+ writeSelection({
291
+ buildingId: buildingOfLevel(levelId, source.nodes),
292
+ levelId,
293
+ zoneId: zone.id,
294
+ selectedIds: [],
295
+ })
296
+ frameViewerNode(zone.id, source)
297
+ }
298
+
299
+ /**
300
+ * A unit from the sidebar: its rooms highlighted and framed. The floor stays
301
+ * when it holds one of the unit's rooms, else the unit's lowest floor shows.
302
+ */
303
+ export function selectViewerUnit(unitId: string, source: ViewerSource) {
304
+ const unit = source.nodes[unitId]
305
+ if (unit?.type !== 'unit') return
306
+ const viewer = useViewer.getState()
307
+ const levels = unitLevels(unit, source.nodes)
308
+ const current = viewer.selection.levelId
309
+ const levelId = (current && levels.includes(current) ? current : (levels[0] ?? current)) as
310
+ | LevelNode['id']
311
+ | null
312
+ viewer.setSelection({
313
+ buildingId: buildingOfLevel(levelId, source.nodes),
314
+ levelId,
315
+ zoneId: null,
316
+ selectedIds: [],
317
+ })
318
+ viewer.setHoveredId(null)
319
+ viewer.setFocusedUnit(unit.id)
320
+ frameViewerNode(unit.id, source)
321
+ }
322
+
323
+ function unitLevels(unit: UnitNode, nodes: ViewerNodes): string[] {
324
+ const ordinals = new Map<string, number>()
325
+ for (const member of buildUnitReport(unit, nodes).members) {
326
+ if (member.levelId && member.levelOrdinal !== null)
327
+ ordinals.set(member.levelId, member.levelOrdinal)
328
+ }
329
+ return [...ordinals.entries()].sort((a, b) => a[1] - b[1]).map(([id]) => id)
330
+ }
331
+
332
+ // ── Framing ──────────────────────────────────────────────────────────────────
333
+
334
+ const corner = new Vector3()
335
+ const box = new Box3()
336
+
337
+ function expandByZone(target: Box3, zone: ZoneNode, source: ViewerSource) {
338
+ const level = zone.parentId ? source.objectFor(zone.parentId) : undefined
339
+ level?.updateWorldMatrix(true, false)
340
+ const floorY = roomFloorElevation(source.nodes as Record<string, AnyNode>, zone.id)
341
+ for (const [x, z] of zone.polygon) {
342
+ for (const y of [floorY, floorY + DEFAULT_LEVEL_HEIGHT]) {
343
+ corner.set(x, y, z)
344
+ if (level) corner.applyMatrix4(level.matrixWorld)
345
+ target.expandByPoint(corner)
346
+ }
347
+ }
348
+ }
349
+
350
+ /** The frame request for a node: the box it spans, and its saved view when it has one. */
351
+ function viewerFrameEvent(id: string, source: ViewerSource): CameraControlFrameEvent | null {
352
+ const node = source.nodes[id]
353
+ if (!node) return null
354
+ const saved = node.camera
355
+ const pose =
356
+ saved?.position && saved.target ? { position: saved.position, target: saved.target } : null
357
+ box.makeEmpty()
358
+ if (node.type === 'zone') expandByZone(box, node, source)
359
+ else if (node.type === 'unit') {
360
+ for (const memberId of node.members) {
361
+ const member = source.nodes[memberId]
362
+ if (member?.type === 'zone') expandByZone(box, member, source)
363
+ }
364
+ } else {
365
+ const object = source.objectFor(id)
366
+ if (object) box.setFromObject(object)
367
+ }
368
+ const bounds = box.isEmpty() ? null : { min: box.min.toArray(), max: box.max.toArray() }
369
+ if (pose) return { bounds, pose }
370
+ return bounds ? { bounds } : null
371
+ }
372
+
373
+ /** Framing glides to its pose, or jumps there when the person asks for reduced motion. */
374
+ export function framingAnimates(): boolean {
375
+ return !(
376
+ typeof window !== 'undefined' && window.matchMedia?.('(prefers-reduced-motion: reduce)').matches
377
+ )
378
+ }
379
+
380
+ export function frameViewerNode(id: string, source: ViewerSource) {
381
+ const event = viewerFrameEvent(id, source)
382
+ if (event) emitter.emit('camera-controls:frame', event)
383
+ }
384
+
385
+ type Vec3 = [number, number, number]
386
+
387
+ /**
388
+ * Where a camera frames a box: about 45° above it, far enough for the box to
389
+ * fill the view, along the heading the camera already has so the scene does
390
+ * not swing round.
391
+ */
392
+ export function framedLookAt(
393
+ event: CameraControlFrameEvent,
394
+ from: { position: Vec3; target: Vec3 },
395
+ fovDegrees = 50,
396
+ ): { position: Vec3; target: Vec3 } | null {
397
+ if (event.pose) return event.pose
398
+ if (!event.bounds) return null
399
+ const { min, max } = event.bounds
400
+ const center: Vec3 = [(min[0] + max[0]) / 2, (min[1] + max[1]) / 2, (min[2] + max[2]) / 2]
401
+ const radius = Math.max(Math.hypot(max[0] - min[0], max[1] - min[1], max[2] - min[2]) / 2, 1)
402
+ const halfFov = (Math.min(Math.max(fovDegrees, 10), 120) * Math.PI) / 360
403
+ const distance = Math.max((radius / Math.sin(halfFov)) * 1.6, 6)
404
+ const dx = from.position[0] - from.target[0]
405
+ const dz = from.position[2] - from.target[2]
406
+ const heading = Math.hypot(dx, dz) > 1e-6 ? Math.atan2(dx, dz) : Math.PI / 4
407
+ const elevation = Math.PI / 4
408
+ const flat = Math.cos(elevation) * distance
409
+ return {
410
+ position: [
411
+ center[0] + Math.sin(heading) * flat,
412
+ center[1] + Math.sin(elevation) * distance,
413
+ center[2] + Math.cos(heading) * flat,
414
+ ],
415
+ target: center,
416
+ }
417
+ }
418
+
419
+ const framePosition = new Vector3()
420
+ const frameTarget = new Vector3()
421
+ const frameSphere = new Sphere()
422
+
423
+ /** Apply the shared framing pose, fitting the orthographic viewport as well. */
424
+ export function frameViewerCamera(
425
+ control: CameraControlsImpl,
426
+ camera: Camera,
427
+ event: CameraControlFrameEvent,
428
+ ) {
429
+ control.getPosition(framePosition)
430
+ control.getTarget(frameTarget)
431
+ const pose = framedLookAt(
432
+ event,
433
+ { position: framePosition.toArray(), target: frameTarget.toArray() },
434
+ (camera as PerspectiveCamera).isPerspectiveCamera
435
+ ? (camera as PerspectiveCamera).fov
436
+ : undefined,
437
+ )
438
+ if (!pose) return
439
+ const animate = framingAnimates()
440
+ control.setLookAt(...pose.position, ...pose.target, animate)
441
+ if ((camera as OrthographicCamera).isOrthographicCamera && event.bounds && !event.pose) {
442
+ box.min.set(...event.bounds.min)
443
+ box.max.set(...event.bounds.max)
444
+ box.getBoundingSphere(frameSphere)
445
+ frameSphere.radius = Math.max(frameSphere.radius * 1.3, 1)
446
+ control.fitToSphere(frameSphere, animate)
447
+ }
448
+ }
449
+
450
+ // ── Breadcrumb ───────────────────────────────────────────────────────────────
451
+
452
+ export type ViewerCrumb = {
453
+ kind: 'building' | 'level' | 'unit' | 'room' | 'node'
454
+ id: string
455
+ label: string
456
+ }
457
+
458
+ export function zoneLabel(zone: ZoneNode): string {
459
+ return zone.name?.trim() || (zone.spaceRole === 'room' ? 'Room' : 'Zone')
460
+ }
461
+
462
+ /**
463
+ * The bar: building › floor › room › element. An element inside a room shows
464
+ * that room (`roomOf`) without selecting it; a focused unit stands where the
465
+ * room would.
466
+ */
467
+ export function viewerBreadcrumb(
468
+ selection: ViewerSelection,
469
+ focusedUnitId: string | null,
470
+ nodes: ViewerNodes,
471
+ roomOf: (nodeId: string, levelId: string) => string | null = () => null,
472
+ ): ViewerCrumb[] {
473
+ const crumbs: ViewerCrumb[] = []
474
+ const elementId = selection.selectedIds[0] ?? null
475
+ const element = elementId ? nodes[elementId] : undefined
476
+ const levelId = selection.levelId ?? (element ? levelOfNode(element.id, nodes) : null)
477
+ const buildingId = selection.buildingId ?? buildingOfLevel(levelId, nodes)
478
+ const building = buildingId ? nodes[buildingId] : undefined
479
+ if (building?.type === 'building') {
480
+ crumbs.push({ kind: 'building', id: building.id, label: building.name || 'Building' })
481
+ }
482
+ const level = levelId ? nodes[levelId] : undefined
483
+ if (level?.type !== 'level') return crumbs
484
+ crumbs.push({ kind: 'level', id: level.id, label: getLevelDisplayName(level) })
485
+ const unit = focusedUnitId ? nodes[focusedUnitId] : undefined
486
+ if (unit?.type === 'unit') crumbs.push({ kind: 'unit', id: unit.id, label: unit.name })
487
+ const roomId = selection.zoneId ?? (element ? roomOf(element.id, level.id) : null)
488
+ const room = roomId ? nodes[roomId] : undefined
489
+ if (room?.type === 'zone') crumbs.push({ kind: 'room', id: room.id, label: zoneLabel(room) })
490
+ if (element) crumbs.push({ kind: 'node', id: element.id, label: nodeDisplayLabel(element) })
491
+ return crumbs
492
+ }
493
+
494
+ /** The room or zone of a level around a level-local point: a detected room first. */
495
+ function roomAtLevelPoint(
496
+ levelId: string,
497
+ point: readonly [number, number],
498
+ nodes: ViewerNodes,
499
+ ): string | null {
500
+ const at: [number, number] = [point[0], point[1]]
501
+ const room = roomIndex(levelId, nodes).roomAtPoint(levelId, at)
502
+ if (room) return room.key.zoneId
503
+ for (const node of Object.values(nodes)) {
504
+ if (node.type !== 'zone' || node.parentId !== levelId || node.polygon.length < 3) continue
505
+ if (containsPoint([{ outer: node.polygon, holes: node.holes ?? [] }], at)) return node.id
506
+ }
507
+ return null
508
+ }
509
+
510
+ const center = new Vector3()
511
+
512
+ /** The room an element stands in, from where its object sits on its floor. */
513
+ export function roomOfObject(nodeId: string, levelId: string, source: ViewerSource) {
514
+ const object = source.objectFor(nodeId)
515
+ const level = source.objectFor(levelId)
516
+ if (!(object && level)) return null
517
+ box.setFromObject(object)
518
+ if (box.isEmpty()) object.getWorldPosition(center)
519
+ else box.getCenter(center)
520
+ level.updateWorldMatrix(true, false)
521
+ level.worldToLocal(center)
522
+ return roomAtLevelPoint(levelId, [center.x, center.z], source.nodes)
523
+ }
524
+
525
+ // ── Sidebar ──────────────────────────────────────────────────────────────────
526
+
527
+ export type ViewerSidebarRow = { id: string; label: string; color: string; areaM2: number }
528
+ export type ViewerSidebarSection = { kind: 'units' | 'rooms' | 'zones'; rows: ViewerSidebarRow[] }
529
+
530
+ /**
531
+ * What sits under the current floor: its units, its rooms, its other zones.
532
+ * An empty section is left out.
533
+ */
534
+ export function viewerSidebarSections(levelId: string, nodes: ViewerNodes): ViewerSidebarSection[] {
535
+ const zones = Object.values(nodes).filter(
536
+ (node): node is ZoneNode =>
537
+ node.type === 'zone' && node.parentId === levelId && node.visible !== false,
538
+ )
539
+ const zoneRow = (zone: ZoneNode): ViewerSidebarRow => ({
540
+ id: zone.id,
541
+ label: zoneLabel(zone),
542
+ color: zone.color,
543
+ areaM2: area([{ outer: zone.polygon, holes: zone.holes ?? [] }]),
544
+ })
545
+ const byName = (a: ViewerSidebarRow, b: ViewerSidebarRow) => a.label.localeCompare(b.label)
546
+ const units = Object.values(nodes)
547
+ .filter((node): node is UnitNode => node.type === 'unit')
548
+ .flatMap((unit) => {
549
+ const report = buildUnitReport(unit, nodes)
550
+ const here = report.members.filter((member) => member.levelId === levelId)
551
+ if (here.length === 0) return []
552
+ return [
553
+ {
554
+ id: unit.id,
555
+ label: unit.name,
556
+ color: unit.color,
557
+ areaM2: here.reduce((sum, member) => sum + member.areaM2, 0),
558
+ },
559
+ ]
560
+ })
561
+ .sort(byName)
562
+ const sections: ViewerSidebarSection[] = [
563
+ { kind: 'units', rows: units },
564
+ {
565
+ kind: 'rooms',
566
+ rows: zones
567
+ .filter((zone) => zone.spaceRole === 'room')
568
+ .map(zoneRow)
569
+ .sort(byName),
570
+ },
571
+ {
572
+ kind: 'zones',
573
+ rows: zones
574
+ .filter((zone) => zone.spaceRole !== 'room')
575
+ .map(zoneRow)
576
+ .sort(byName),
577
+ },
578
+ ]
579
+ return sections.filter((section) => section.rows.length > 0)
580
+ }
@@ -0,0 +1,71 @@
1
+ import { afterEach, describe, expect, test } from 'bun:test'
2
+ import { emitter } from '@pascal-app/core'
3
+ import useEditor from '../store/use-editor'
4
+ import { toolHudTitle } from './hud-title'
5
+ import {
6
+ getWallDrawVariant,
7
+ selectWallDrawVariant,
8
+ WALL_DRAW_VARIANTS,
9
+ wallDrawVariantOf,
10
+ } from './wall-draw-variant'
11
+
12
+ afterEach(() => {
13
+ useEditor.getState().setContinuation('wall', 'room')
14
+ useEditor.getState().armToolMode({ mode: 'select' })
15
+ })
16
+
17
+ describe('Rooms variants', () => {
18
+ test('are Rectangle, Polygon and Walls, in the Build panel order', () => {
19
+ expect(WALL_DRAW_VARIANTS.map((variant) => [variant.id, variant.label, variant.mode])).toEqual([
20
+ ['rectangle', 'Rectangle', 'rectangle'],
21
+ ['polygon', 'Polygon', 'room'],
22
+ ['walls', 'Walls', 'single'],
23
+ ])
24
+ })
25
+
26
+ test('default to the chain that closes into a room', () => {
27
+ expect(getWallDrawVariant()).toBe('polygon')
28
+ expect(wallDrawVariantOf('unknown')).toBe('polygon')
29
+ })
30
+
31
+ test('picking one sets the wall tool drawing mode, and it sticks', () => {
32
+ for (const variant of WALL_DRAW_VARIANTS) {
33
+ selectWallDrawVariant(variant.id)
34
+ expect(useEditor.getState().getContinuation('wall')).toBe(variant.mode)
35
+ expect(getWallDrawVariant()).toBe(variant.id)
36
+ }
37
+ // Arming the wall tool (e.g. with B) keeps the picked variant.
38
+ selectWallDrawVariant('rectangle')
39
+ useEditor.getState().armToolMode({ mode: 'build', tool: 'wall' })
40
+ expect(getWallDrawVariant()).toBe('rectangle')
41
+ })
42
+
43
+ test('switching while the wall tool is armed drops the in-flight draft', () => {
44
+ let cancels = 0
45
+ const onCancel = () => {
46
+ cancels += 1
47
+ }
48
+ emitter.on('tool:cancel', onCancel)
49
+ try {
50
+ selectWallDrawVariant('walls')
51
+ expect(cancels).toBe(0)
52
+ useEditor.getState().armToolMode({ mode: 'build', tool: 'wall' })
53
+ selectWallDrawVariant('rectangle')
54
+ expect(cancels).toBe(1)
55
+ selectWallDrawVariant('rectangle')
56
+ expect(cancels).toBe(1)
57
+ } finally {
58
+ emitter.off('tool:cancel', onCancel)
59
+ }
60
+ })
61
+
62
+ test('name the HUD after the variant in hand', () => {
63
+ expect(toolHudTitle('wall', 'rectangle')).toEqual({
64
+ label: 'Rectangle room',
65
+ icon: { kind: 'url', src: '/icons/room.webp' },
66
+ shortcut: 'B',
67
+ })
68
+ expect(toolHudTitle('wall', 'room')?.label).toBe('Polygon room')
69
+ expect(toolHudTitle('wall', 'single')?.label).toBe('Walls')
70
+ })
71
+ })