@pascal-app/editor 1.0.1 → 1.0.4

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 (498) hide show
  1. package/package.json +16 -9
  2. package/src/components/editor/action-menu-button.tsx +83 -0
  3. package/src/components/editor/bake-thumbnail.tsx +315 -4
  4. package/src/components/editor/capture-level-picker.tsx +72 -0
  5. package/src/components/editor/custom-camera-controls.tsx +221 -128
  6. package/src/components/editor/delete-confirmation-dialog.tsx +297 -7
  7. package/src/components/editor/duplicate-node.ts +244 -0
  8. package/src/components/editor/editor-layout-mobile.tsx +37 -25
  9. package/src/components/editor/editor-layout-v2.tsx +131 -58
  10. package/src/components/editor/export-manager.tsx +34 -8
  11. package/src/components/editor/first-person/build-collider-world.test.ts +51 -0
  12. package/src/components/editor/first-person/build-collider-world.ts +6 -2
  13. package/src/components/editor/first-person/mechanism-targets.test.ts +75 -0
  14. package/src/components/editor/first-person/mechanism-targets.ts +36 -0
  15. package/src/components/editor/first-person-controls.tsx +205 -2
  16. package/src/components/editor/floating-action-menu.tsx +63 -207
  17. package/src/components/editor/floor-aware-camera-controls.test.ts +442 -0
  18. package/src/components/editor/floor-aware-camera-controls.ts +324 -0
  19. package/src/components/editor/floor-openings-overlay.tsx +243 -0
  20. package/src/components/editor/floor-region-controls.tsx +846 -0
  21. package/src/components/editor/floor-region-webgpu-materials.test.ts +12 -0
  22. package/src/components/editor/floor-surface.test.ts +165 -0
  23. package/src/components/editor/floor-surface.ts +182 -0
  24. package/src/components/editor/floorplan-background-selection.test.ts +11 -2
  25. package/src/components/editor/floorplan-background-selection.ts +1 -3
  26. package/src/components/editor/floorplan-panel.tsx +536 -487
  27. package/src/components/editor/footprint-height-handle.tsx +204 -0
  28. package/src/components/editor/grid.tsx +31 -7
  29. package/src/components/editor/group-actions.ts +59 -11
  30. package/src/components/editor/group-floating-action-menu.tsx +19 -9
  31. package/src/components/editor/group-move-3d.ts +4 -6
  32. package/src/components/editor/group-rotate-handle.tsx +81 -35
  33. package/src/components/editor/group-selection-box-3d.tsx +25 -17
  34. package/src/components/editor/group-transform-shared.test.ts +126 -1
  35. package/src/components/editor/group-transform-shared.ts +79 -73
  36. package/src/components/editor/handles/handle-arrow.tsx +72 -2
  37. package/src/components/editor/handles/handle-hit-priority.test.tsx +63 -0
  38. package/src/components/editor/handles/handle-hit-priority.tsx +37 -0
  39. package/src/components/editor/handles/swallow-next-click.test.ts +28 -0
  40. package/src/components/editor/handles/use-handle-drag.ts +272 -131
  41. package/src/components/editor/index.tsx +316 -176
  42. package/src/components/editor/join-walls-pill.tsx +44 -0
  43. package/src/components/editor/node-action-menu.tsx +70 -70
  44. package/src/components/editor/node-arrow-handles.tsx +167 -14
  45. package/src/components/editor/open-wall-ends-3d-layer.tsx +273 -0
  46. package/src/components/editor/open-wall-ends-hint.tsx +89 -0
  47. package/src/components/editor/quick-measurement-hud.tsx +3 -2
  48. package/src/components/editor/registry-action-contributions.test.tsx +63 -0
  49. package/src/components/editor/registry-action-contributions.tsx +132 -0
  50. package/src/components/editor/room-controls.tsx +847 -0
  51. package/src/components/editor/room-floor-highlight.tsx +128 -0
  52. package/src/components/editor/room-handles-2d.tsx +354 -0
  53. package/src/components/editor/room-handles.tsx +675 -0
  54. package/src/components/editor/room-highlight.tsx +225 -0
  55. package/src/components/editor/room-labels.test.ts +357 -0
  56. package/src/components/editor/room-labels.tsx +990 -0
  57. package/src/components/editor/room-transform-ghost.tsx +256 -0
  58. package/src/components/editor/room-webgpu-materials.test.ts +27 -0
  59. package/src/components/editor/selection-manager.tsx +451 -317
  60. package/src/components/editor/slab-hole-highlights.tsx +3 -0
  61. package/src/components/editor/snapshot-capture-overlay.tsx +4 -0
  62. package/src/components/editor/snapshot-capture.test.ts +52 -4
  63. package/src/components/editor/snapshot-capture.ts +30 -5
  64. package/src/components/editor/thumbnail-generator.test.ts +308 -0
  65. package/src/components/editor/thumbnail-generator.tsx +1035 -229
  66. package/src/components/editor/use-floorplan-background-placement.ts +31 -11
  67. package/src/components/editor/vector-edge-extractor.tsx +121 -0
  68. package/src/components/editor/views/host-view.tsx +61 -0
  69. package/src/components/editor/views/plugins-view.tsx +670 -0
  70. package/src/components/editor/views/scene-region.tsx +59 -0
  71. package/src/components/editor/views/use-editor-views.ts +57 -0
  72. package/src/components/editor/views/view-bar.tsx +220 -0
  73. package/src/components/editor/wall-justification.test.ts +140 -0
  74. package/src/components/editor/wall-move-side-handles.tsx +114 -163
  75. package/src/components/editor/wall-opening-highlights.tsx +2 -3
  76. package/src/components/editor/wall-push-arrow.tsx +92 -0
  77. package/src/components/editor/wall-region-handles.tsx +509 -0
  78. package/src/components/editor/wall-snap-beacon-layer.tsx +3 -0
  79. package/src/components/editor-2d/floorplan-cursor-indicator-overlay.tsx +8 -4
  80. package/src/components/editor-2d/floorplan-group-move.tsx +17 -24
  81. package/src/components/editor-2d/floorplan-open-wall-ends-layer.test.tsx +86 -0
  82. package/src/components/editor-2d/floorplan-open-wall-ends-layer.tsx +290 -0
  83. package/src/components/editor-2d/floorplan-registered-tool-layer.tsx +33 -12
  84. package/src/components/editor-2d/floorplan-registry-action-menu.tsx +11 -1
  85. package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +242 -126
  86. package/src/components/editor-2d/renderers/floorplan-dimension-edit-overlay.test.ts +156 -0
  87. package/src/components/editor-2d/renderers/floorplan-dimension-edit-overlay.tsx +278 -0
  88. package/src/components/editor-2d/renderers/floorplan-dimension-renderer.tsx +77 -0
  89. package/src/components/editor-2d/renderers/floorplan-draft-measurement.tsx +166 -0
  90. package/src/components/editor-2d/renderers/floorplan-geometry-renderer.tsx +4 -1
  91. package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +87 -8
  92. package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +355 -245
  93. package/src/components/editor-2d/svg-paths.ts +12 -15
  94. package/src/components/procedural-items/inspector.tsx +40 -15
  95. package/src/components/systems/ceiling/ceiling-selection-affordance-system.test.ts +97 -14
  96. package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +18 -18
  97. package/src/components/systems/zone/zone-system.test.ts +47 -0
  98. package/src/components/systems/zone/zone-system.tsx +31 -66
  99. package/src/components/tools/__fixtures__/hosting.ts +10 -0
  100. package/src/components/tools/item/item-surface-placement.test.ts +3 -9
  101. package/src/components/tools/item/item-surface-strategy-resolver.test.ts +3 -9
  102. package/src/components/tools/item/placement-history.test.tsx +474 -0
  103. package/src/components/tools/item/placement-strategies.ts +63 -47
  104. package/src/components/tools/item/shelf-surface-strategy-resolver.test.ts +3 -3
  105. package/src/components/tools/item/use-draft-node.test.tsx +432 -1
  106. package/src/components/tools/item/use-draft-node.ts +207 -97
  107. package/src/components/tools/item/use-placement-coordinator.tsx +251 -111
  108. package/src/components/tools/paint-region/use-paint-tint.ts +37 -0
  109. package/src/components/tools/paint-region/wall-paint-region-tool.tsx +466 -0
  110. package/src/components/tools/registry/item-surface-move.test.ts +10 -2
  111. package/src/components/tools/registry/item-surface-move.ts +46 -24
  112. package/src/components/tools/registry/item-surface-resolver.test.ts +2 -2
  113. package/src/components/tools/registry/move-registry-node-tool.tsx +26 -10
  114. package/src/components/tools/select/box-select-tool.tsx +2 -12
  115. package/src/components/tools/select/plane-box-select-tool.test.tsx +79 -0
  116. package/src/components/tools/select/plane-box-select-tool.tsx +35 -22
  117. package/src/components/tools/select/select-candidates.test.ts +37 -4
  118. package/src/components/tools/select/select-candidates.ts +15 -11
  119. package/src/components/tools/shared/affordance-dispatch.test.ts +24 -0
  120. package/src/components/tools/shared/affordance-dispatch.ts +13 -0
  121. package/src/components/tools/shared/cursor-sphere.tsx +36 -12
  122. package/src/components/tools/shared/draft-measurement-label.tsx +38 -0
  123. package/src/components/tools/shared/pointer-support-cap.test.ts +27 -0
  124. package/src/components/tools/shared/pointer-support-cap.ts +17 -5
  125. package/src/components/tools/shared/surface-rejection.test.tsx +5 -4
  126. package/src/components/tools/shared/surface-rejection.tsx +14 -0
  127. package/src/components/tools/site/site-boundary-editor.tsx +1 -2
  128. package/src/components/tools/stair/stair-defaults.ts +0 -3
  129. package/src/components/tools/stair/stair-tool.tsx +90 -42
  130. package/src/components/tools/tool-manager.tsx +82 -23
  131. package/src/components/tools/wall/wall-drafting.test.ts +19 -4
  132. package/src/components/tools/zone/zone-tool.tsx +4 -12
  133. package/src/components/ui/action-menu/action-button.tsx +3 -0
  134. package/src/components/ui/action-menu/camera-actions.tsx +2 -1
  135. package/src/components/ui/action-menu/control-modes.tsx +2 -35
  136. package/src/components/ui/action-menu/measurement-control.tsx +3 -10
  137. package/src/components/ui/build-panel/build-panel.tsx +235 -0
  138. package/src/components/ui/command-palette/editor-commands.tsx +4 -30
  139. package/src/components/ui/command-palette/index.tsx +5 -24
  140. package/src/components/ui/controls/finish-swatch.tsx +110 -0
  141. package/src/components/ui/controls/material-paint-panel.tsx +101 -91
  142. package/src/components/ui/controls/material-picker.tsx +8 -67
  143. package/src/components/ui/controls/scene-material-list.tsx +4 -32
  144. package/src/components/ui/controls/select-control.tsx +65 -0
  145. package/src/components/ui/controls/slider-control.tsx +76 -31
  146. package/src/components/ui/floating-level-selector.tsx +9 -14
  147. package/src/components/ui/helpers/building-helper.tsx +4 -1
  148. package/src/components/ui/helpers/contextual-helper-panel.tsx +154 -70
  149. package/src/components/ui/helpers/helper-manager.test.tsx +69 -0
  150. package/src/components/ui/helpers/helper-manager.tsx +348 -19
  151. package/src/components/ui/helpers/item-helper.tsx +8 -0
  152. package/src/components/ui/helpers/registered-tool-helper.tsx +10 -33
  153. package/src/components/ui/icon-ref.tsx +45 -1
  154. package/src/components/ui/item-catalog/item-catalog.tsx +5 -30
  155. package/src/components/ui/level-duplicate-dialog.tsx +1 -1
  156. package/src/components/ui/node-icon-image.tsx +16 -0
  157. package/src/components/ui/panels/collections/collections-popover.tsx +20 -14
  158. package/src/components/ui/panels/floor-foundation-panel.tsx +551 -0
  159. package/src/components/ui/panels/floor-opening-panel.tsx +143 -0
  160. package/src/components/ui/panels/mobile-panel-sheet.tsx +2 -2
  161. package/src/components/ui/panels/mobile-selection-bar.tsx +2 -2
  162. package/src/components/ui/panels/multi-field-value.test.ts +26 -3
  163. package/src/components/ui/panels/multi-field-value.ts +6 -3
  164. package/src/components/ui/panels/multi-height-mode.tsx +27 -21
  165. package/src/components/ui/panels/multi-parametric-inspector.tsx +52 -2
  166. package/src/components/ui/panels/multi-selection-panel.tsx +43 -4
  167. package/src/components/ui/panels/node-display.ts +8 -3
  168. package/src/components/ui/panels/panel-manager.tsx +103 -68
  169. package/src/components/ui/panels/panel-wrapper.tsx +78 -36
  170. package/src/components/ui/panels/parametric-field-control.tsx +1 -1
  171. package/src/components/ui/panels/parametric-inspector.tsx +7 -19
  172. package/src/components/ui/panels/reference-panel.tsx +2 -3
  173. package/src/components/ui/panels/room-construction-rows.tsx +774 -0
  174. package/src/components/ui/panels/room-name-options.test.ts +55 -0
  175. package/src/components/ui/panels/room-name-options.ts +47 -0
  176. package/src/components/ui/panels/room-openings-field.tsx +110 -0
  177. package/src/components/ui/panels/room-panel.tsx +725 -0
  178. package/src/components/ui/panels/shape-choice.tsx +58 -0
  179. package/src/components/ui/panels/wall-region-list.tsx +109 -0
  180. package/src/components/ui/primitives/color-dot.tsx +36 -24
  181. package/src/components/ui/primitives/shortcut-token.tsx +4 -0
  182. package/src/components/ui/primitives/sidebar.tsx +2 -0
  183. package/src/components/ui/right-stack.test.ts +29 -0
  184. package/src/components/ui/right-stack.tsx +145 -0
  185. package/src/components/ui/sidebar/icon-rail.tsx +5 -0
  186. package/src/components/ui/sidebar/panels/settings-panel/audio-settings-dialog.tsx +2 -2
  187. package/src/components/ui/sidebar/panels/settings-panel/index.tsx +615 -372
  188. package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +7 -7
  189. package/src/components/ui/sidebar/panels/settings-panel/print-export-button.tsx +1 -1
  190. package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +4 -4
  191. package/src/components/ui/sidebar/panels/site-panel/chimney-tree-node.tsx +1 -5
  192. package/src/components/ui/sidebar/panels/site-panel/collection-tree-node.tsx +197 -0
  193. package/src/components/ui/sidebar/panels/site-panel/column-tree-node.tsx +1 -5
  194. package/src/components/ui/sidebar/panels/site-panel/door-tree-node.tsx +1 -5
  195. package/src/components/ui/sidebar/panels/site-panel/dormer-tree-node.tsx +1 -5
  196. package/src/components/ui/sidebar/panels/site-panel/elevator-tree-node.tsx +1 -5
  197. package/src/components/ui/sidebar/panels/site-panel/fence-tree-node.tsx +24 -11
  198. package/src/components/ui/sidebar/panels/site-panel/gutter-tree-node.tsx +1 -5
  199. package/src/components/ui/sidebar/panels/site-panel/index.tsx +176 -211
  200. package/src/components/ui/sidebar/panels/site-panel/inline-rename-input.tsx +30 -8
  201. package/src/components/ui/sidebar/panels/site-panel/item-tree-node.tsx +1 -3
  202. package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +4 -1
  203. package/src/components/ui/sidebar/panels/site-panel/registry-tree-node.tsx +1 -3
  204. package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +1 -4
  205. package/src/components/ui/sidebar/panels/site-panel/shelf-tree-node.tsx +1 -3
  206. package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +21 -13
  207. package/src/components/ui/sidebar/panels/site-panel/solar-panel-tree-node.tsx +1 -5
  208. package/src/components/ui/sidebar/panels/site-panel/spawn-tree-node.tsx +1 -5
  209. package/src/components/ui/sidebar/panels/site-panel/stair-tree-node.tsx +1 -5
  210. package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +0 -27
  211. package/src/components/ui/sidebar/panels/site-panel/unit-tree-node.tsx +2 -3
  212. package/src/components/ui/sidebar/panels/site-panel/wall-tree-node.tsx +1 -5
  213. package/src/components/ui/sidebar/panels/site-panel/window-tree-node.tsx +1 -5
  214. package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +9 -4
  215. package/src/components/ui/sidebar/panels/zone-panel/index.tsx +0 -2
  216. package/src/components/ui/sidebar/tab-bar.tsx +10 -0
  217. package/src/components/ui/sidebar/use-plugin-panels.tsx +15 -5
  218. package/src/components/viewer/floorplan-compass-button.tsx +9 -1
  219. package/src/components/viewer/floorplan-preview-visibility.test.ts +35 -0
  220. package/src/components/viewer/floorplan-preview-visibility.ts +19 -0
  221. package/src/components/viewer/floorplan-preview.tsx +6 -18
  222. package/src/components/viewer/viewer-hover-label.tsx +34 -0
  223. package/src/components/viewer/viewer-rooms.tsx +175 -0
  224. package/src/components/viewer/viewer-scene-header.tsx +181 -145
  225. package/src/components/viewer/viewer-selection-manager.tsx +124 -0
  226. package/src/components/viewer/viewer-stage.tsx +2 -2
  227. package/src/components/viewer-zone-system.tsx +9 -9
  228. package/src/hooks/use-auto-save.integration.test.tsx +311 -0
  229. package/src/hooks/use-auto-save.ts +55 -14
  230. package/src/hooks/use-ceiling-events.test.ts +1 -0
  231. package/src/hooks/use-ceiling-events.ts +2 -0
  232. package/src/hooks/use-grid-events.test.ts +2 -0
  233. package/src/hooks/use-grid-events.ts +16 -11
  234. package/src/hooks/use-keyboard.test.ts +182 -0
  235. package/src/hooks/use-keyboard.ts +122 -91
  236. package/src/hooks/use-open-wall-ends.test.ts +99 -0
  237. package/src/hooks/use-open-wall-ends.ts +238 -0
  238. package/src/hooks/use-placement-notice.test.ts +48 -0
  239. package/src/hooks/use-placement-notice.ts +42 -0
  240. package/src/hooks/use-selected-room.ts +143 -0
  241. package/src/index.tsx +306 -39
  242. package/src/lib/catalog-panel-model.ts +39 -0
  243. package/src/lib/catalog-selection.test.ts +27 -0
  244. package/src/lib/catalog-selection.ts +8 -0
  245. package/src/lib/ceiling-edit-session.test.ts +321 -0
  246. package/src/lib/ceiling-edit-session.ts +206 -0
  247. package/src/lib/ceiling-region-session.test.ts +179 -0
  248. package/src/lib/contextual-help.test.ts +27 -1
  249. package/src/lib/contextual-help.ts +16 -9
  250. package/src/lib/continuation.test.ts +19 -1
  251. package/src/lib/continuation.ts +25 -4
  252. package/src/lib/door-interaction.ts +14 -0
  253. package/src/lib/editor-level-display.test.ts +96 -0
  254. package/src/lib/editor-level-display.ts +48 -0
  255. package/src/lib/editor-views.ts +144 -0
  256. package/src/lib/export-geometry-wait.test.ts +90 -0
  257. package/src/lib/floor-edit-session.ts +153 -0
  258. package/src/lib/floor-footprints.test.ts +563 -0
  259. package/src/lib/floor-footprints.ts +395 -0
  260. package/src/lib/floor-foundation-support.test.ts +283 -0
  261. package/src/lib/floor-opening-draft.ts +218 -0
  262. package/src/lib/floor-openings.test.ts +216 -0
  263. package/src/lib/floor-region-geometry.ts +191 -0
  264. package/src/lib/floor-region-session.test.ts +485 -0
  265. package/src/lib/floor-region-session.ts +589 -0
  266. package/src/lib/floor-region-snap.ts +101 -0
  267. package/src/lib/floor-step-hover.test.ts +32 -0
  268. package/src/lib/floorplan/annotation-visibility.ts +17 -0
  269. package/src/lib/floorplan/dimension-drive.test.ts +360 -0
  270. package/src/lib/floorplan/dimension-drive.ts +479 -0
  271. package/src/lib/floorplan/floorplan-export.test.ts +15 -9
  272. package/src/lib/floorplan/floorplan-export.tsx +32 -8
  273. package/src/lib/floorplan/floorplan-extension.ts +48 -0
  274. package/src/lib/floorplan/floorplan-mode.test.ts +4 -0
  275. package/src/lib/floorplan/floorplan-mode.ts +2 -0
  276. package/src/lib/floorplan/floorplan-pdfkit-document.ts +339 -18
  277. package/src/lib/floorplan/floorplan-pdfkit-renderer.test.ts +138 -8
  278. package/src/lib/floorplan/floorplan-pdfkit-renderer.ts +56 -25
  279. package/src/lib/floorplan/floorplan-plugin-install.test.tsx +138 -0
  280. package/src/lib/floorplan/floorplan-readonly.ts +6 -0
  281. package/src/lib/floorplan/geometry.ts +13 -6
  282. package/src/lib/floorplan/index.ts +0 -9
  283. package/src/lib/floorplan/items.ts +5 -2
  284. package/src/lib/floorplan/marks.test.ts +213 -0
  285. package/src/lib/floorplan/marks.ts +238 -0
  286. package/src/lib/floorplan/open-wall-ends.test.ts +268 -0
  287. package/src/lib/floorplan/open-wall-ends.ts +162 -0
  288. package/src/lib/floorplan/pdf-fonts/GeistMono-Bold.ttf +0 -0
  289. package/src/lib/floorplan/pdf-fonts/GeistMono-Regular.ttf +0 -0
  290. package/src/lib/floorplan/pdf-fonts/LICENSE-GeistMono.txt +92 -0
  291. package/src/lib/floorplan/pdf-fonts/LICENSE-LiberationSans.txt +102 -0
  292. package/src/lib/floorplan/pdf-fonts/LiberationSans-Bold.ttf +0 -0
  293. package/src/lib/floorplan/pdf-fonts/LiberationSans-Regular.ttf +0 -0
  294. package/src/lib/floorplan/schedules.test.ts +189 -0
  295. package/src/lib/floorplan/schedules.ts +339 -0
  296. package/src/lib/floorplan/sheet-export.test.ts +100 -0
  297. package/src/lib/floorplan/sheet-export.ts +318 -0
  298. package/src/lib/floorplan/site-plan/build-site-plan-drawing.ts +779 -0
  299. package/src/lib/floorplan/site-plan/contours.test.ts +43 -0
  300. package/src/lib/floorplan/site-plan/contributors.ts +97 -0
  301. package/src/lib/floorplan/site-plan/coverage.ts +232 -0
  302. package/src/lib/floorplan/site-plan/drawing-type-switch.test.ts +15 -0
  303. package/src/lib/floorplan/site-plan/drawing-type-switch.tsx +86 -0
  304. package/src/lib/floorplan/site-plan/front-edge.test.ts +103 -0
  305. package/src/lib/floorplan/site-plan/front-edge.ts +156 -0
  306. package/src/lib/floorplan/site-plan/geometry.test.ts +275 -0
  307. package/src/lib/floorplan/site-plan/geometry.ts +557 -0
  308. package/src/lib/floorplan/site-plan/index.ts +71 -0
  309. package/src/lib/floorplan/site-plan/site-annotations.test.ts +366 -0
  310. package/src/lib/floorplan/site-plan/site-annotations.ts +1083 -0
  311. package/src/lib/floorplan/site-plan/site-parts.ts +379 -0
  312. package/src/lib/floorplan/site-plan/site-plan-layer.tsx +138 -0
  313. package/src/lib/floorplan/site-plan/street-name.test.ts +69 -0
  314. package/src/lib/floorplan/site-plan/street-name.ts +302 -0
  315. package/src/lib/floorplan/site-plan/yards.test.ts +69 -0
  316. package/src/lib/floorplan/types.ts +1 -27
  317. package/src/lib/fresh-planar-placement.test.ts +36 -0
  318. package/src/lib/fresh-planar-placement.ts +191 -36
  319. package/src/lib/geometry-script/author.ts +152 -0
  320. package/src/lib/geometry-script/client.ts +152 -0
  321. package/src/lib/geometry-script/protocol.ts +12 -0
  322. package/src/lib/gesture-lifecycle.test.ts +595 -0
  323. package/src/lib/gesture-lifecycle.ts +290 -0
  324. package/src/lib/glb-export-zone-holes.test.ts +41 -0
  325. package/src/lib/glb-export.test.ts +544 -16
  326. package/src/lib/glb-export.ts +256 -125
  327. package/src/lib/grid-frame.test.ts +19 -0
  328. package/src/lib/grid-frame.ts +18 -0
  329. package/src/lib/history.test.ts +6 -0
  330. package/src/lib/history.ts +7 -0
  331. package/src/lib/hud-title.ts +109 -0
  332. package/src/lib/hydration-gated-storage.ts +49 -0
  333. package/src/lib/ifc-export.test.ts +174 -0
  334. package/src/lib/ifc-export.ts +201 -0
  335. package/src/lib/ifc-parts.ts +15 -0
  336. package/src/lib/inspector-expanded.test.ts +44 -0
  337. package/src/lib/inspector-expanded.ts +36 -0
  338. package/src/lib/interaction/scope.ts +27 -1
  339. package/src/lib/keyboard-pan.test.ts +184 -0
  340. package/src/lib/keyboard-pan.ts +238 -0
  341. package/src/lib/live-gesture-round-trip.test.ts +360 -0
  342. package/src/lib/lot/__fixtures__/map-dossier-miami-shores.json +1 -0
  343. package/src/lib/lot/__fixtures__/map-dossier-stpete.json +1 -0
  344. package/src/lib/lot/clean-ring.test.ts +186 -0
  345. package/src/lib/lot/clean-ring.ts +217 -0
  346. package/src/lib/lot/dossier.test.ts +202 -0
  347. package/src/lib/lot/dossier.ts +498 -0
  348. package/src/lib/lot/drop-in.test.ts +326 -0
  349. package/src/lib/lot/drop-in.ts +407 -0
  350. package/src/lib/lot/index.ts +43 -0
  351. package/src/lib/lot/lot-patch.test.ts +340 -0
  352. package/src/lib/lot/lot-patch.ts +394 -0
  353. package/src/lib/lot/parcel-provider.ts +43 -0
  354. package/src/lib/lot/terrain.test.ts +161 -0
  355. package/src/lib/lot/terrain.ts +266 -0
  356. package/src/lib/material-catalog-model.ts +74 -0
  357. package/src/lib/material-paint-panel-model.ts +80 -0
  358. package/src/lib/material-paint.ts +4 -0
  359. package/src/lib/mezzanine-actions.test.ts +342 -0
  360. package/src/lib/mezzanine-draft.test.ts +501 -0
  361. package/src/lib/mezzanine-draft.ts +278 -0
  362. package/src/lib/mezzanine-messages.ts +26 -0
  363. package/src/lib/mezzanine-selection.test.ts +207 -0
  364. package/src/lib/mezzanine-stairs.ts +46 -0
  365. package/src/lib/model-export.ts +3 -1
  366. package/src/lib/node-action-movement.ts +17 -6
  367. package/src/lib/node-display-label.ts +37 -0
  368. package/src/lib/orbit-floor-clearance.test.ts +165 -0
  369. package/src/lib/orbit-floor-clearance.ts +106 -0
  370. package/src/lib/overlay-transform.test.ts +28 -0
  371. package/src/lib/overlay-transform.ts +12 -0
  372. package/src/lib/paint-commit-route.test.ts +37 -0
  373. package/src/lib/paint-commit-route.ts +17 -0
  374. package/src/lib/paint-eyedropper.test.tsx +165 -0
  375. package/src/lib/paint-eyedropper.ts +99 -0
  376. package/src/lib/paint-pass-through.test.ts +111 -0
  377. package/src/lib/paint-pass-through.ts +76 -0
  378. package/src/lib/paint-region-hover.ts +25 -0
  379. package/src/lib/paint-region-mode.ts +154 -0
  380. package/src/lib/paint-regions.test.ts +166 -0
  381. package/src/lib/paint-regions.ts +192 -0
  382. package/src/lib/paint-scope.test.ts +317 -35
  383. package/src/lib/paint-scope.ts +266 -25
  384. package/src/lib/panel-rows.ts +96 -0
  385. package/src/lib/panel-tool-options.test.ts +34 -0
  386. package/src/lib/panel-tool-options.ts +46 -0
  387. package/src/lib/parametric-node-update.ts +37 -0
  388. package/src/lib/plate-paint-affected.test.ts +300 -0
  389. package/src/lib/plate-paint-affected.ts +177 -0
  390. package/src/lib/plugin-chat.ts +50 -0
  391. package/src/lib/plugin-panels.test.ts +25 -0
  392. package/src/lib/plugin-panels.ts +94 -0
  393. package/src/lib/print-shell-compiler.ts +1 -0
  394. package/src/lib/registered-draft-snap.ts +12 -0
  395. package/src/lib/roof-duplication.ts +10 -2
  396. package/src/lib/room-assembly-overlay.test.ts +275 -0
  397. package/src/lib/room-assembly-overlay.ts +266 -0
  398. package/src/lib/room-built-on.test.ts +379 -0
  399. package/src/lib/room-built-on.ts +138 -0
  400. package/src/lib/room-construction-commands.ts +712 -0
  401. package/src/lib/room-construction.test.ts +746 -0
  402. package/src/lib/room-construction.ts +385 -0
  403. package/src/lib/room-controls-phase6.test.ts +630 -0
  404. package/src/lib/room-divide-session.ts +523 -0
  405. package/src/lib/room-handle-drag.ts +1068 -0
  406. package/src/lib/room-name-catalog.test.ts +90 -0
  407. package/src/lib/room-name-catalog.ts +168 -0
  408. package/src/lib/room-push-dimensions.test.ts +311 -0
  409. package/src/lib/room-push-dimensions.ts +251 -0
  410. package/src/lib/room-selection-commands.ts +62 -0
  411. package/src/lib/room-selection.test.ts +973 -0
  412. package/src/lib/room-selection.ts +417 -0
  413. package/src/lib/room-structure-commands.ts +94 -0
  414. package/src/lib/room-structure.test.ts +921 -0
  415. package/src/lib/room-transform-session.ts +570 -0
  416. package/src/lib/room-zone-routing.test.ts +294 -0
  417. package/src/lib/room-zone-routing.ts +197 -0
  418. package/src/lib/scene-clipboard.test.ts +364 -0
  419. package/src/lib/scene-clipboard.ts +229 -48
  420. package/src/lib/scene-material-refs.test.ts +79 -0
  421. package/src/lib/scene-material-refs.ts +79 -0
  422. package/src/lib/scene.test.ts +87 -0
  423. package/src/lib/scene.ts +39 -45
  424. package/src/lib/selection-routing.test.ts +129 -45
  425. package/src/lib/selection-routing.ts +53 -74
  426. package/src/lib/session-writes.test.ts +120 -0
  427. package/src/lib/session-writes.ts +205 -0
  428. package/src/lib/sidebar-panel.ts +62 -0
  429. package/src/lib/site-boundary.test.ts +4 -6
  430. package/src/lib/site-boundary.ts +1 -1
  431. package/src/lib/snapping-mode.test.ts +17 -2
  432. package/src/lib/snapping-mode.ts +19 -7
  433. package/src/lib/spatial-drag-plane.test.ts +53 -0
  434. package/src/lib/spatial-drag-plane.ts +19 -0
  435. package/src/lib/spatial-pointer-input.test.ts +72 -0
  436. package/src/lib/spatial-pointer-input.ts +62 -0
  437. package/src/lib/spatial-snap.test.ts +47 -0
  438. package/src/lib/terrace-draft.test.ts +145 -0
  439. package/src/lib/terrace-draft.ts +213 -0
  440. package/src/lib/terrain-panel-model.ts +117 -0
  441. package/src/lib/tool-cursor-icon.test.ts +16 -0
  442. package/src/lib/tool-cursor-icon.ts +11 -0
  443. package/src/lib/typing-target.ts +9 -0
  444. package/src/lib/units.ts +77 -39
  445. package/src/lib/vector-edges.test.ts +58 -0
  446. package/src/lib/vector-edges.ts +400 -0
  447. package/src/lib/viewer-selection.test.ts +577 -0
  448. package/src/lib/viewer-selection.ts +569 -0
  449. package/src/lib/wall-draw-variant.test.ts +71 -0
  450. package/src/lib/wall-draw-variant.ts +91 -0
  451. package/src/lib/wall-paint-region-session.ts +246 -0
  452. package/src/lib/wall-paint-region.test.ts +373 -0
  453. package/src/lib/wall-polygon-draft.test.ts +262 -0
  454. package/src/lib/wall-polygon-draft.ts +286 -0
  455. package/src/lib/wall-push.test.ts +453 -0
  456. package/src/lib/wall-region-face.ts +352 -0
  457. package/src/lib/wall-region-handles.test.ts +304 -0
  458. package/src/lib/wall-region-handles.ts +368 -0
  459. package/src/lib/wall-region-list.test.ts +119 -0
  460. package/src/lib/wall-region-list.ts +136 -0
  461. package/src/lib/wall-region-snap.ts +110 -0
  462. package/src/lib/window-interaction.ts +14 -0
  463. package/src/lib/world-grid-snap.test.ts +48 -0
  464. package/src/lib/world-grid-snap.ts +9 -5
  465. package/src/lib/zone-content.ts +4 -1
  466. package/src/lib/zone-name.test.ts +17 -0
  467. package/src/lib/zone-name.ts +17 -0
  468. package/src/store/opens-in-select-mode.test.ts +223 -0
  469. package/src/store/paint-mode.test.ts +188 -0
  470. package/src/store/rotation-snapping.test.ts +53 -0
  471. package/src/store/terrain-sculpt-mode.test.ts +19 -21
  472. package/src/store/tool-mode.test.ts +137 -6
  473. package/src/store/use-delete-confirmation.ts +28 -1
  474. package/src/store/use-drawing-view.test.ts +24 -3
  475. package/src/store/use-drawing-view.ts +29 -1
  476. package/src/store/use-editor.tsx +442 -323
  477. package/src/store/use-floorplan-draft-preview.ts +23 -1
  478. package/src/store/use-floorplan-marquee.ts +12 -2
  479. package/src/store/use-interaction-scope.ts +155 -6
  480. package/src/store/use-quick-measurement-hud.ts +3 -3
  481. package/src/store/use-stair-build-preview.ts +13 -5
  482. package/src/store/use-wall-move-ghosts.ts +4 -2
  483. package/src/store/use-wall-region-handles-selection.test.ts +222 -0
  484. package/src/store/use-wall-region-handles-selection.ts +139 -0
  485. package/src/store/view-layout.test.ts +175 -0
  486. package/src/store/view-layout.ts +196 -0
  487. package/src/test-utils/immediate-animation-frames.ts +16 -0
  488. package/src/test-utils/live-gesture-round-trip.ts +224 -0
  489. package/src/test-utils/selection-harness.ts +82 -0
  490. package/src/components/editor-2d/renderers/floorplan-stair-layer.tsx +0 -499
  491. package/src/components/systems/zone/zone-label-editor-system.tsx +0 -333
  492. package/src/components/tools/fence/fence-drafting.test.ts +0 -100
  493. package/src/components/tools/fence/fence-drafting.ts +0 -266
  494. package/src/components/ui/sidebar/panels/plugins-panel.tsx +0 -218
  495. package/src/components/viewer/viewer-units-panel.tsx +0 -116
  496. package/src/lib/floorplan/stairs.ts +0 -489
  497. package/src/lib/level-duplication.test.ts +0 -144
  498. package/src/lib/level-duplication.ts +0 -195
@@ -0,0 +1,294 @@
1
+ import { afterEach, beforeEach, describe, expect, test } from 'bun:test'
2
+ import {
3
+ type AnyNodeId,
4
+ BuildingNode,
5
+ clearSceneHistory,
6
+ createZone,
7
+ divideZone,
8
+ generateId,
9
+ initSpaceDetectionSync,
10
+ LevelNode,
11
+ UnitNode,
12
+ useScene,
13
+ type ZoneNode,
14
+ ZoneNode as ZoneNodeSchema,
15
+ } from '@pascal-app/core'
16
+ import { useViewer } from '@pascal-app/viewer'
17
+ import { deleteSelection } from '../components/editor/group-actions'
18
+ import { getRoomSelectionIndex } from '../hooks/use-selected-room'
19
+ import useEditor from '../store/use-editor'
20
+ import { installImmediateAnimationFrames } from '../test-utils/immediate-animation-frames'
21
+ import { exitCeilingEditToRoom, startCeilingEdit } from './ceiling-edit-session'
22
+ import { applyRoomPlan } from './room-structure-commands'
23
+ import {
24
+ captureElementActionOrigin,
25
+ clickZoneArea,
26
+ completeElementAction,
27
+ hoverZoneArea,
28
+ leaveZoneArea,
29
+ roomKeyForZone,
30
+ selectZoneOrRoom,
31
+ zoneKindLabel,
32
+ } from './room-zone-routing'
33
+ import { addZoneToNewUnit, assignZoneToUnit, zoneUnits } from './units'
34
+
35
+ const LEVEL = 'level_zone_routing'
36
+ const BUILDING = 'building_zone_routing'
37
+ let zoneId: string
38
+ let stop = () => {}
39
+ let restoreFrames = () => {}
40
+
41
+ const nodes = () => useScene.getState().nodes
42
+ const zones = () => Object.values(nodes()).filter((n): n is ZoneNode => n.type === 'zone')
43
+ const roomWalls = (id: string) =>
44
+ getRoomSelectionIndex(LEVEL)
45
+ .update(nodes())
46
+ .find((room) => room.key.zoneId === id)!.boundaryWallIds
47
+
48
+ beforeEach(() => {
49
+ restoreFrames = installImmediateAnimationFrames()
50
+ const building = BuildingNode.parse({ id: BUILDING, children: [LEVEL] })
51
+ const level = LevelNode.parse({ id: LEVEL, parentId: building.id })
52
+ useScene.setState({
53
+ nodes: { [building.id]: building, [level.id]: level },
54
+ rootNodeIds: [building.id],
55
+ dirtyNodes: new Set(),
56
+ materials: {},
57
+ collections: {},
58
+ readOnly: false,
59
+ })
60
+ useScene.temporal.getState().resume()
61
+ stop = initSpaceDetectionSync(useScene, { getState: () => ({ spaces: {}, setSpaces: () => {} }) })
62
+ const plan = createZone(nodes(), {
63
+ levelId: LEVEL,
64
+ polygon: [
65
+ [0, 0],
66
+ [8, 0],
67
+ [8, 4],
68
+ [0, 4],
69
+ ],
70
+ enclose: true,
71
+ mintId: generateId,
72
+ })
73
+ applyRoomPlan(plan)
74
+ zoneId = plan.zoneId
75
+ useViewer.getState().setFocusedUnit(null)
76
+ useViewer
77
+ .getState()
78
+ .setSelection({ buildingId: building.id, levelId: level.id, selectedIds: [], zoneId: null })
79
+ useEditor.setState({ phase: 'building', mode: 'select', room: null, hoveredRoom: null })
80
+ clearSceneHistory()
81
+ })
82
+ afterEach(() => {
83
+ stop()
84
+ restoreFrames()
85
+ })
86
+
87
+ describe('a room is the room from every route (point 1)', () => {
88
+ test('an enclosed room selected from a list or label becomes the room selection', () => {
89
+ expect(selectZoneOrRoom(zoneId)).toBe('room')
90
+ expect(useEditor.getState().room).toEqual({ levelId: LEVEL, zoneId })
91
+ expect(useViewer.getState().selection.zoneId).toBeNull()
92
+ })
93
+
94
+ test('a generic zone and an open room stay zones', () => {
95
+ const lawn = ZoneNodeSchema.parse({
96
+ parentId: LEVEL,
97
+ name: 'Lawn',
98
+ polygon: [
99
+ [20, 0],
100
+ [24, 0],
101
+ [24, 4],
102
+ ],
103
+ })
104
+ const open = ZoneNodeSchema.parse({
105
+ parentId: LEVEL,
106
+ name: 'Porch',
107
+ spaceRole: 'room',
108
+ enclosureStatus: 'open',
109
+ polygon: [
110
+ [30, 0],
111
+ [34, 0],
112
+ [34, 4],
113
+ ],
114
+ })
115
+ useScene.getState().createNodes([
116
+ { node: lawn, parentId: LEVEL as AnyNodeId },
117
+ { node: open, parentId: LEVEL as AnyNodeId },
118
+ ])
119
+ for (const zone of [lawn, open]) {
120
+ useEditor.getState().selectRoom({ levelId: LEVEL, zoneId })
121
+ expect(roomKeyForZone(zone.id)).toBeNull()
122
+ expect(selectZoneOrRoom(zone.id)).toBe('zone')
123
+ expect(useViewer.getState().selection.zoneId).toBe(zone.id)
124
+ expect(useEditor.getState().room).toBeNull()
125
+ }
126
+ expect(zoneKindLabel(lawn)).toBe('Zone')
127
+ expect(zoneKindLabel(open)).toBe('Room')
128
+ })
129
+
130
+ test('while a unit is focused, a room stays a zone so arranging the unit keeps working', () => {
131
+ const unit = UnitNode.parse({ name: 'Flat A' })
132
+ useScene.getState().createNode(unit, BUILDING as AnyNodeId)
133
+ useViewer.getState().setFocusedUnit(unit.id)
134
+ expect(selectZoneOrRoom(zoneId)).toBe('zone')
135
+ expect(useEditor.getState().room).toBeNull()
136
+ })
137
+
138
+ test('the room panel unit row reads, changes and clears membership (§13 Q7)', () => {
139
+ expect(zoneUnits(nodes(), zoneId)).toMatchObject({ buildingId: BUILDING, current: null })
140
+ const created = addZoneToNewUnit(zoneId as ZoneNode['id'])!
141
+ expect(useScene.temporal.getState().pastStates).toHaveLength(1)
142
+ expect(zoneUnits(nodes(), zoneId).current?.id).toBe(created)
143
+ expect(zoneUnits(nodes(), zoneId).current?.name).toBe('Unit 1')
144
+ const other = UnitNode.parse({ name: 'Flat B' })
145
+ useScene.getState().createNode(other, BUILDING as AnyNodeId)
146
+ assignZoneToUnit(zoneId as ZoneNode['id'], other.id)
147
+ expect(zoneUnits(nodes(), zoneId).current?.id).toBe(other.id)
148
+ expect(zoneUnits(nodes(), zoneId).units.map((u) => u.id)).toEqual([created, other.id])
149
+ assignZoneToUnit(zoneId as ZoneNode['id'], null)
150
+ expect(zoneUnits(nodes(), zoneId).current).toBeNull()
151
+ })
152
+ })
153
+
154
+ describe('finishing an action on a piece of a room returns to the room (point 3)', () => {
155
+ test('only pieces of the selected room carry it as the origin', () => {
156
+ const wallId = roomWalls(zoneId)[0]!
157
+ expect(captureElementActionOrigin([wallId])).toBeNull()
158
+ useEditor.getState().selectRoom({ levelId: LEVEL, zoneId })
159
+ expect(captureElementActionOrigin([wallId])?.room).toEqual({ levelId: LEVEL, zoneId })
160
+ expect(captureElementActionOrigin([wallId, 'wall_elsewhere'])).toBeNull()
161
+ })
162
+
163
+ test('completing lands on the origin room with nothing drilled', () => {
164
+ useEditor.getState().selectRoom({ levelId: LEVEL, zoneId })
165
+ const wallId = roomWalls(zoneId)[0]!
166
+ useViewer.getState().setSelection({ selectedIds: [wallId as AnyNodeId] })
167
+ const origin = captureElementActionOrigin([wallId])
168
+ expect(completeElementAction(origin)).toBe(true)
169
+ expect(useViewer.getState().selection.selectedIds).toEqual([])
170
+ expect(useEditor.getState().room).toEqual({ levelId: LEVEL, zoneId })
171
+ })
172
+
173
+ test('without a room context the action keeps its own selection', () => {
174
+ const wallId = roomWalls(zoneId)[0]!
175
+ useViewer.getState().setSelection({ selectedIds: [wallId as AnyNodeId] })
176
+ expect(completeElementAction(captureElementActionOrigin([wallId]))).toBe(false)
177
+ expect(useViewer.getState().selection.selectedIds).toEqual([wallId])
178
+ })
179
+
180
+ test('a room that is gone hands over to the room now standing there, else to nothing', () => {
181
+ const gone = { levelId: LEVEL, zoneId: 'zone_gone' }
182
+ useEditor.getState().selectRoom(gone)
183
+ expect(completeElementAction({ room: gone, point: [4, 2] })).toBe(true)
184
+ expect(useEditor.getState().room).toEqual({ levelId: LEVEL, zoneId })
185
+ useEditor.getState().selectRoom(gone)
186
+ expect(completeElementAction({ room: gone, point: [40, 2] })).toBe(false)
187
+ expect(useEditor.getState().room).toBeNull()
188
+ expect(useViewer.getState().selection.selectedIds).toEqual([])
189
+ })
190
+
191
+ test('deleting a separator drilled from one side lands on the merged room', () => {
192
+ const plan = divideZone(nodes(), {
193
+ zoneId,
194
+ cut: [
195
+ [2, 0],
196
+ [2, 4],
197
+ ],
198
+ mintId: generateId,
199
+ })
200
+ applyRoomPlan(plan)
201
+ const sides = zones().map((z) => z.id as string)
202
+ expect(sides).toHaveLength(2)
203
+ // Drilled from the side Divide made, which merges back into the first room.
204
+ const origin = sides.find((id) => id !== zoneId) ?? zoneId
205
+ useEditor.getState().selectRoom({ levelId: LEVEL, zoneId: origin })
206
+ useViewer.getState().setSelection({ selectedIds: [plan.separatorId as AnyNodeId] })
207
+ expect(deleteSelection()).toBe(true)
208
+ const [merged] = zones()
209
+ expect(zones()).toHaveLength(1)
210
+ expect(useEditor.getState().room).toEqual({ levelId: LEVEL, zoneId: merged!.id })
211
+ expect(useViewer.getState().selection.selectedIds).toEqual([])
212
+ })
213
+
214
+ test('deleting a drilled wall lands back on its room (its edge stays open)', () => {
215
+ useEditor.getState().selectRoom({ levelId: LEVEL, zoneId })
216
+ const wallId = roomWalls(zoneId)[0]!
217
+ useViewer.getState().setSelection({ selectedIds: [wallId as AnyNodeId] })
218
+ expect(deleteSelection()).toBe(true)
219
+ expect(nodes()[wallId as AnyNodeId]).toBeUndefined()
220
+ expect(useEditor.getState().room).toEqual({ levelId: LEVEL, zoneId })
221
+ expect(useViewer.getState().selection.selectedIds).toEqual([])
222
+ })
223
+
224
+ test('leaving Edit ceiling lands on the ceiling’s room', () => {
225
+ useEditor.getState().selectRoom({ levelId: LEVEL, zoneId })
226
+ expect(startCeilingEdit(zoneId)).not.toBe(false)
227
+ expect(exitCeilingEditToRoom()).toBe(true)
228
+ expect(useEditor.getState().room).toEqual({ levelId: LEVEL, zoneId })
229
+ expect(useViewer.getState().selection.selectedIds).toEqual([])
230
+ })
231
+ })
232
+
233
+ describe('a zone area (plan fill, label, pill): hover shows what the click selects', () => {
234
+ const plain = { meta: false, ctrl: false, shift: false, alt: false }
235
+ const shown = () => ({
236
+ room: useEditor.getState().hoveredRoom?.zoneId ?? null,
237
+ zone: useViewer.getState().hoveredId,
238
+ })
239
+ const picked = () => ({
240
+ room: useEditor.getState().room?.zoneId ?? null,
241
+ zone: useViewer.getState().selection.selectedIds[0] ?? null,
242
+ })
243
+ const drawn = () => {
244
+ const zone = ZoneNodeSchema.parse({
245
+ name: 'Reading nook',
246
+ polygon: [
247
+ [20, 20],
248
+ [22, 20],
249
+ [22, 22],
250
+ [20, 22],
251
+ ],
252
+ })
253
+ useScene.getState().createNode(zone, LEVEL as AnyNodeId)
254
+ return zone.id
255
+ }
256
+
257
+ test('a room hovers and selects as the room; a modifier reaches the zone itself', () => {
258
+ for (const modifiers of [
259
+ plain,
260
+ { ...plain, alt: true },
261
+ { ...plain, shift: true },
262
+ { ...plain, ctrl: true },
263
+ { ...plain, meta: true },
264
+ ]) {
265
+ useEditor.getState().clearRoom()
266
+ useViewer.getState().setSelection({ selectedIds: [], zoneId: null })
267
+ hoverZoneArea(zoneId, modifiers)
268
+ const hover = shown()
269
+ clickZoneArea(zoneId, modifiers)
270
+ leaveZoneArea(zoneId)
271
+ expect(picked()).toEqual(hover)
272
+ expect(shown()).toEqual({ room: null, zone: null })
273
+ }
274
+ expect(roomKeyForZone(zoneId)).not.toBeNull()
275
+ })
276
+
277
+ test('a drawn zone that bounds no room hovers and selects as itself, and Shift adds to it', () => {
278
+ const nook = drawn()
279
+ hoverZoneArea(nook, plain)
280
+ expect(shown()).toEqual({ room: null, zone: nook })
281
+ clickZoneArea(nook, plain)
282
+ expect(picked()).toEqual({ room: null, zone: nook })
283
+ clickZoneArea(zoneId, { ...plain, shift: true })
284
+ expect(useViewer.getState().selection.selectedIds).toEqual([nook, zoneId])
285
+ })
286
+
287
+ test('hovering an already selected room area still shows the room its click keeps', () => {
288
+ clickZoneArea(zoneId, plain)
289
+ hoverZoneArea(zoneId, plain)
290
+ expect(shown()).toEqual({ room: zoneId, zone: null })
291
+ clickZoneArea(zoneId, plain)
292
+ expect(picked()).toEqual({ room: zoneId, zone: null })
293
+ })
294
+ })
@@ -0,0 +1,197 @@
1
+ import { type AnyNode, type AnyNodeId, useScene, type ZoneNode } from '@pascal-app/core'
2
+ import { useViewer } from '@pascal-app/viewer'
3
+ import { getRoomSelectionIndex, hoverRoomFromHit } from '../hooks/use-selected-room'
4
+ import useEditor from '../store/use-editor'
5
+ import { expandSessionSelectionForNode } from '../store/use-session-groups'
6
+ import { type RoomKey, sameRoom } from './room-selection'
7
+ import { selectRoom } from './room-selection-commands'
8
+ import { resolveSelectedIdsForNodeClick, type SelectionModifierKeys } from './selection-routing'
9
+
10
+ // A room is one thing to the user whichever way they reach it: the canvas, the
11
+ // Rooms list, the plan's fill or its label. Rooms the room index knows (closed,
12
+ // or a mezzanine) are selected as rooms; everything else stays a zone.
13
+
14
+ export function isRoomZone(node: AnyNode | undefined): node is ZoneNode {
15
+ return node?.type === 'zone' && node.spaceRole === 'room'
16
+ }
17
+
18
+ /** The room key of a zone that is a selectable room, else null. */
19
+ export function roomKeyForZone(
20
+ zoneId: string,
21
+ nodes: Record<string, AnyNode> = useScene.getState().nodes,
22
+ ): RoomKey | null {
23
+ const zone = nodes[zoneId]
24
+ if (!isRoomZone(zone) || !zone.parentId) return null
25
+ const records = getRoomSelectionIndex(zone.parentId).update(
26
+ nodes as ReturnType<typeof useScene.getState>['nodes'],
27
+ )
28
+ return records.some((room) => room.key.zoneId === zoneId)
29
+ ? { levelId: zone.parentId, zoneId }
30
+ : null
31
+ }
32
+
33
+ /**
34
+ * Selects a zone the way the user means it: an enclosed room as the room (its
35
+ * panel, its pill, Escape back out), anything else as the zone. While a unit
36
+ * is focused a zone stays a zone — arranging a unit works on zones.
37
+ */
38
+ export function selectZoneOrRoom(zoneId: string): 'room' | 'zone' {
39
+ const room = useViewer.getState().focusedUnitId ? null : roomKeyForZone(zoneId)
40
+ if (room) {
41
+ selectRoom(room)
42
+ return 'room'
43
+ }
44
+ useEditor.getState().clearRoom()
45
+ useViewer.getState().setSelection({ zoneId: zoneId as ZoneNode['id'] })
46
+ return 'zone'
47
+ }
48
+
49
+ /**
50
+ * The hover a pointer on a zone's area shows — its fill or label in the plan,
51
+ * or its pill in either view. A room hovers room-first through the same rule
52
+ * as its floor on the canvas (`hoverRoomFromHit`); held modifiers, or a zone
53
+ * that bounds no room, hover the zone itself (its outline).
54
+ */
55
+ export function hoverZoneArea(zoneId: string, modifiers: SelectionModifierKeys): void {
56
+ const room = roomKeyForZone(zoneId)
57
+ if (
58
+ room &&
59
+ sameRoom(useEditor.getState().room, room) &&
60
+ !(modifiers.alt || modifiers.shift || modifiers.ctrl || modifiers.meta)
61
+ ) {
62
+ useEditor.getState().setHoveredRoom(room)
63
+ useViewer.getState().setHoveredId(null)
64
+ return
65
+ }
66
+ if (room && hoverRoomFromHit(room, modifiers, null)) return
67
+ useEditor.getState().setHoveredRoom(null)
68
+ if (useViewer.getState().hoveredId !== zoneId) useViewer.getState().setHoveredId(zoneId as never)
69
+ }
70
+
71
+ /** Ends a `hoverZoneArea` hover, leaving any other hover alone. */
72
+ export function leaveZoneArea(zoneId: string): void {
73
+ const room = roomKeyForZone(zoneId)
74
+ if (room && sameRoom(useEditor.getState().hoveredRoom, room))
75
+ useEditor.getState().setHoveredRoom(null)
76
+ if (useViewer.getState().hoveredId === zoneId) useViewer.getState().setHoveredId(null)
77
+ }
78
+
79
+ /**
80
+ * The selection a click on a zone's area makes, whichever surface took it.
81
+ * A plain click on a room selects the room (keeping it when it already is);
82
+ * Shift/Ctrl/Meta toggle and Alt isolate the zone itself, as on any canvas
83
+ * element. A zone that bounds no room selects as itself, which opens its panel
84
+ * and its outline editor.
85
+ */
86
+ export function clickZoneArea(zoneId: string, modifiers: SelectionModifierKeys): void {
87
+ const room = roomKeyForZone(zoneId)
88
+ const bypass = modifiers.alt || modifiers.shift || modifiers.ctrl || modifiers.meta
89
+ if (room && !bypass) {
90
+ if (!sameRoom(useEditor.getState().room, room)) selectRoom(room)
91
+ return
92
+ }
93
+ useEditor.getState().clearRoom()
94
+ const { selection, setSelection } = useViewer.getState()
95
+ setSelection({
96
+ zoneId: null,
97
+ selectedIds: resolveSelectedIdsForNodeClick({
98
+ currentSelectedIds: selection.selectedIds,
99
+ modifierKeys: modifiers,
100
+ nodeId: zoneId,
101
+ expandIdsForNode: expandSessionSelectionForNode,
102
+ }),
103
+ })
104
+ }
105
+
106
+ /** Whether a zone's row is the selection, whichever store holds it. */
107
+ export function useZoneSelected(zoneId: string): boolean {
108
+ const asRoom = useEditor((state) => state.room?.zoneId === zoneId)
109
+ const asZone = useViewer(
110
+ (state) =>
111
+ state.selection.zoneId === zoneId ||
112
+ (state.selection.selectedIds.length === 1 && state.selection.selectedIds[0] === zoneId),
113
+ )
114
+ return asRoom || asZone
115
+ }
116
+
117
+ /** The user word for a zone: rooms are "Room", every other area stays "Zone". */
118
+ export function zoneKindLabel(zone: Pick<ZoneNode, 'spaceRole'> | undefined): 'Room' | 'Zone' {
119
+ return zone?.spaceRole === 'room' ? 'Room' : 'Zone'
120
+ }
121
+
122
+ /** Where a one-shot action on a drilled element started: its room, and a point inside it. */
123
+ export type ElementActionOrigin = { room: RoomKey; point: [number, number] | null }
124
+
125
+ /**
126
+ * The room to come back to once an action on `elementIds` completes: the
127
+ * selected room, when every element is part of it (its walls, separators,
128
+ * floor or ceiling). Null when there is no room context (an Alt-selected
129
+ * element, walls of another room), so the action keeps its own selection.
130
+ */
131
+ export function captureElementActionOrigin(
132
+ elementIds: readonly string[],
133
+ ): ElementActionOrigin | null {
134
+ const room = useEditor.getState().room
135
+ if (!room) return null
136
+ const nodes = useScene.getState().nodes
137
+ const record = getRoomSelectionIndex(room.levelId)
138
+ .update(nodes)
139
+ .find((candidate) => candidate.key.zoneId === room.zoneId)
140
+ if (!record) return null
141
+ const parts = new Set<string>([
142
+ ...record.spans.map((span) => span.boundaryId),
143
+ ...(record.slabId ? [record.slabId] : []),
144
+ ...(record.ceilingId ? [record.ceilingId] : []),
145
+ ])
146
+ if (!elementIds.every((id) => parts.has(id))) return null
147
+ const zone = nodes[room.zoneId as AnyNodeId]
148
+ const seed = zone?.type === 'zone' ? zone.seed : undefined
149
+ return { room, point: seed ? [seed[0], seed[1]] : interiorPoint(record.clearPolygon) }
150
+ }
151
+
152
+ function interiorPoint(
153
+ polygon: readonly { outer: readonly (readonly [number, number])[] }[],
154
+ ): [number, number] | null {
155
+ const ring = polygon[0]?.outer
156
+ if (!ring?.length) return null
157
+ let x = 0
158
+ let z = 0
159
+ for (const [px, pz] of ring) {
160
+ x += px
161
+ z += pz
162
+ }
163
+ return [x / ring.length, z / ring.length]
164
+ }
165
+
166
+ /**
167
+ * Finishing a one-shot action on a piece of a room (Split, Merge, Delete, a
168
+ * hole's Done) puts the user back on the room: the origin room when it is
169
+ * still a room, else the room it became part of (`survivorZoneId`, or the room
170
+ * now standing where it was), else nothing. Called at successful commits only —
171
+ * a cancelled action keeps its element. Returns whether a room was selected.
172
+ */
173
+ export function completeElementAction(
174
+ origin: ElementActionOrigin | null,
175
+ result: { survivorZoneId?: string } = {},
176
+ ): boolean {
177
+ if (!origin) return false
178
+ const { levelId } = origin.room
179
+ const index = getRoomSelectionIndex(levelId)
180
+ const records = index.update(useScene.getState().nodes)
181
+ const known = (zoneId: string | undefined) =>
182
+ !!zoneId && records.some((room) => room.key.zoneId === zoneId)
183
+ const zoneId = known(origin.room.zoneId)
184
+ ? origin.room.zoneId
185
+ : known(result.survivorZoneId)
186
+ ? result.survivorZoneId!
187
+ : origin.point
188
+ ? (index.roomAtPoint(levelId, origin.point)?.key.zoneId ?? null)
189
+ : null
190
+ if (!zoneId) {
191
+ useViewer.getState().setSelection({ selectedIds: [] })
192
+ useEditor.getState().clearRoom()
193
+ return false
194
+ }
195
+ selectRoom({ levelId, zoneId })
196
+ return true
197
+ }