@pascal-app/editor 1.0.3 → 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 (440) hide show
  1. package/package.json +11 -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 +202 -57
  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 +36 -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 +344 -342
  27. package/src/components/editor/footprint-height-handle.tsx +204 -0
  28. package/src/components/editor/group-actions.ts +54 -2
  29. package/src/components/editor/handles/handle-hit-priority.test.tsx +63 -0
  30. package/src/components/editor/handles/handle-hit-priority.tsx +37 -0
  31. package/src/components/editor/handles/swallow-next-click.test.ts +28 -0
  32. package/src/components/editor/handles/use-handle-drag.ts +269 -197
  33. package/src/components/editor/index.tsx +294 -169
  34. package/src/components/editor/join-walls-pill.tsx +44 -0
  35. package/src/components/editor/node-action-menu.tsx +68 -70
  36. package/src/components/editor/node-arrow-handles.tsx +25 -6
  37. package/src/components/editor/open-wall-ends-3d-layer.tsx +273 -0
  38. package/src/components/editor/open-wall-ends-hint.tsx +89 -0
  39. package/src/components/editor/quick-measurement-hud.tsx +3 -2
  40. package/src/components/editor/registry-action-contributions.test.tsx +63 -0
  41. package/src/components/editor/registry-action-contributions.tsx +80 -1
  42. package/src/components/editor/room-controls.tsx +847 -0
  43. package/src/components/editor/room-floor-highlight.tsx +128 -0
  44. package/src/components/editor/room-handles-2d.tsx +354 -0
  45. package/src/components/editor/room-handles.tsx +675 -0
  46. package/src/components/editor/room-highlight.tsx +225 -0
  47. package/src/components/editor/room-labels.test.ts +357 -0
  48. package/src/components/editor/room-labels.tsx +990 -0
  49. package/src/components/editor/room-transform-ghost.tsx +256 -0
  50. package/src/components/editor/room-webgpu-materials.test.ts +27 -0
  51. package/src/components/editor/selection-manager.tsx +451 -317
  52. package/src/components/editor/slab-hole-highlights.tsx +3 -0
  53. package/src/components/editor/snapshot-capture-overlay.tsx +4 -0
  54. package/src/components/editor/snapshot-capture.test.ts +52 -4
  55. package/src/components/editor/snapshot-capture.ts +30 -5
  56. package/src/components/editor/thumbnail-generator.test.ts +308 -0
  57. package/src/components/editor/thumbnail-generator.tsx +1035 -229
  58. package/src/components/editor/use-floorplan-background-placement.ts +31 -11
  59. package/src/components/editor/vector-edge-extractor.tsx +121 -0
  60. package/src/components/editor/views/host-view.tsx +61 -0
  61. package/src/components/editor/views/plugins-view.tsx +670 -0
  62. package/src/components/editor/views/scene-region.tsx +59 -0
  63. package/src/components/editor/views/use-editor-views.ts +57 -0
  64. package/src/components/editor/views/view-bar.tsx +220 -0
  65. package/src/components/editor/wall-justification.test.ts +140 -0
  66. package/src/components/editor/wall-move-side-handles.tsx +43 -142
  67. package/src/components/editor/wall-push-arrow.tsx +92 -0
  68. package/src/components/editor/wall-region-handles.tsx +509 -0
  69. package/src/components/editor/wall-snap-beacon-layer.tsx +3 -0
  70. package/src/components/editor-2d/floorplan-cursor-indicator-overlay.tsx +8 -4
  71. package/src/components/editor-2d/floorplan-open-wall-ends-layer.test.tsx +86 -0
  72. package/src/components/editor-2d/floorplan-open-wall-ends-layer.tsx +290 -0
  73. package/src/components/editor-2d/floorplan-registry-action-menu.tsx +11 -1
  74. package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +113 -102
  75. package/src/components/editor-2d/renderers/floorplan-dimension-edit-overlay.test.ts +156 -0
  76. package/src/components/editor-2d/renderers/floorplan-dimension-edit-overlay.tsx +278 -0
  77. package/src/components/editor-2d/renderers/floorplan-dimension-renderer.tsx +77 -0
  78. package/src/components/editor-2d/renderers/{floorplan-draft-wall-measurement.tsx → floorplan-draft-measurement.tsx} +8 -8
  79. package/src/components/editor-2d/renderers/floorplan-geometry-renderer.tsx +4 -1
  80. package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +38 -8
  81. package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +291 -227
  82. package/src/components/editor-2d/svg-paths.ts +12 -15
  83. package/src/components/procedural-items/inspector.tsx +40 -15
  84. package/src/components/systems/ceiling/ceiling-selection-affordance-system.test.ts +97 -14
  85. package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +18 -18
  86. package/src/components/systems/zone/zone-system.test.ts +47 -0
  87. package/src/components/systems/zone/zone-system.tsx +29 -68
  88. package/src/components/tools/item/placement-history.test.tsx +474 -0
  89. package/src/components/tools/item/placement-strategies.ts +8 -4
  90. package/src/components/tools/item/use-draft-node.test.tsx +432 -1
  91. package/src/components/tools/item/use-draft-node.ts +166 -95
  92. package/src/components/tools/item/use-placement-coordinator.tsx +93 -46
  93. package/src/components/tools/paint-region/use-paint-tint.ts +37 -0
  94. package/src/components/tools/paint-region/wall-paint-region-tool.tsx +466 -0
  95. package/src/components/tools/registry/move-registry-node-tool.tsx +3 -1
  96. package/src/components/tools/select/box-select-tool.tsx +2 -12
  97. package/src/components/tools/select/plane-box-select-tool.test.tsx +79 -0
  98. package/src/components/tools/select/plane-box-select-tool.tsx +35 -22
  99. package/src/components/tools/select/select-candidates.test.ts +37 -4
  100. package/src/components/tools/select/select-candidates.ts +15 -11
  101. package/src/components/tools/shared/cursor-sphere.tsx +36 -12
  102. package/src/components/tools/site/site-boundary-editor.tsx +1 -2
  103. package/src/components/tools/stair/stair-defaults.ts +0 -3
  104. package/src/components/tools/stair/stair-tool.tsx +90 -42
  105. package/src/components/tools/tool-manager.tsx +46 -20
  106. package/src/components/tools/wall/wall-drafting.test.ts +19 -4
  107. package/src/components/tools/zone/zone-tool.tsx +4 -12
  108. package/src/components/ui/action-menu/action-button.tsx +3 -0
  109. package/src/components/ui/action-menu/camera-actions.tsx +2 -1
  110. package/src/components/ui/action-menu/control-modes.tsx +2 -35
  111. package/src/components/ui/action-menu/measurement-control.tsx +3 -10
  112. package/src/components/ui/build-panel/build-panel.tsx +235 -0
  113. package/src/components/ui/command-palette/editor-commands.tsx +4 -30
  114. package/src/components/ui/command-palette/index.tsx +5 -24
  115. package/src/components/ui/controls/finish-swatch.tsx +110 -0
  116. package/src/components/ui/controls/material-paint-panel.tsx +97 -26
  117. package/src/components/ui/controls/material-picker.tsx +4 -1
  118. package/src/components/ui/controls/scene-material-list.tsx +4 -32
  119. package/src/components/ui/controls/select-control.tsx +65 -0
  120. package/src/components/ui/floating-level-selector.tsx +9 -14
  121. package/src/components/ui/helpers/building-helper.tsx +4 -1
  122. package/src/components/ui/helpers/contextual-helper-panel.tsx +154 -70
  123. package/src/components/ui/helpers/helper-manager.test.tsx +69 -0
  124. package/src/components/ui/helpers/helper-manager.tsx +325 -14
  125. package/src/components/ui/helpers/item-helper.tsx +8 -0
  126. package/src/components/ui/helpers/registered-tool-helper.tsx +8 -1
  127. package/src/components/ui/icon-ref.tsx +45 -1
  128. package/src/components/ui/level-duplicate-dialog.tsx +1 -1
  129. package/src/components/ui/node-icon-image.tsx +16 -0
  130. package/src/components/ui/panels/collections/collections-popover.tsx +20 -14
  131. package/src/components/ui/panels/floor-foundation-panel.tsx +551 -0
  132. package/src/components/ui/panels/floor-opening-panel.tsx +143 -0
  133. package/src/components/ui/panels/mobile-panel-sheet.tsx +2 -2
  134. package/src/components/ui/panels/mobile-selection-bar.tsx +2 -2
  135. package/src/components/ui/panels/multi-field-value.test.ts +26 -3
  136. package/src/components/ui/panels/multi-field-value.ts +6 -3
  137. package/src/components/ui/panels/multi-parametric-inspector.tsx +52 -2
  138. package/src/components/ui/panels/multi-selection-panel.tsx +43 -4
  139. package/src/components/ui/panels/node-display.ts +8 -3
  140. package/src/components/ui/panels/panel-manager.tsx +103 -68
  141. package/src/components/ui/panels/panel-wrapper.tsx +78 -36
  142. package/src/components/ui/panels/parametric-field-control.tsx +1 -1
  143. package/src/components/ui/panels/parametric-inspector.tsx +4 -4
  144. package/src/components/ui/panels/reference-panel.tsx +2 -3
  145. package/src/components/ui/panels/room-construction-rows.tsx +774 -0
  146. package/src/components/ui/panels/room-name-options.test.ts +55 -0
  147. package/src/components/ui/panels/room-name-options.ts +47 -0
  148. package/src/components/ui/panels/room-openings-field.tsx +110 -0
  149. package/src/components/ui/panels/room-panel.tsx +725 -0
  150. package/src/components/ui/panels/shape-choice.tsx +58 -0
  151. package/src/components/ui/panels/wall-region-list.tsx +109 -0
  152. package/src/components/ui/primitives/color-dot.tsx +36 -24
  153. package/src/components/ui/primitives/sidebar.tsx +2 -0
  154. package/src/components/ui/right-stack.test.ts +29 -0
  155. package/src/components/ui/right-stack.tsx +145 -0
  156. package/src/components/ui/sidebar/icon-rail.tsx +5 -0
  157. package/src/components/ui/sidebar/panels/settings-panel/index.tsx +31 -4
  158. package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +5 -5
  159. package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +4 -4
  160. package/src/components/ui/sidebar/panels/site-panel/chimney-tree-node.tsx +1 -5
  161. package/src/components/ui/sidebar/panels/site-panel/collection-tree-node.tsx +197 -0
  162. package/src/components/ui/sidebar/panels/site-panel/column-tree-node.tsx +1 -5
  163. package/src/components/ui/sidebar/panels/site-panel/door-tree-node.tsx +1 -5
  164. package/src/components/ui/sidebar/panels/site-panel/dormer-tree-node.tsx +1 -5
  165. package/src/components/ui/sidebar/panels/site-panel/elevator-tree-node.tsx +1 -5
  166. package/src/components/ui/sidebar/panels/site-panel/fence-tree-node.tsx +24 -11
  167. package/src/components/ui/sidebar/panels/site-panel/gutter-tree-node.tsx +1 -5
  168. package/src/components/ui/sidebar/panels/site-panel/index.tsx +176 -211
  169. package/src/components/ui/sidebar/panels/site-panel/inline-rename-input.tsx +30 -8
  170. package/src/components/ui/sidebar/panels/site-panel/item-tree-node.tsx +1 -3
  171. package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +4 -1
  172. package/src/components/ui/sidebar/panels/site-panel/registry-tree-node.tsx +1 -3
  173. package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +1 -4
  174. package/src/components/ui/sidebar/panels/site-panel/shelf-tree-node.tsx +1 -3
  175. package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +21 -13
  176. package/src/components/ui/sidebar/panels/site-panel/solar-panel-tree-node.tsx +1 -5
  177. package/src/components/ui/sidebar/panels/site-panel/spawn-tree-node.tsx +1 -5
  178. package/src/components/ui/sidebar/panels/site-panel/stair-tree-node.tsx +1 -5
  179. package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +0 -27
  180. package/src/components/ui/sidebar/panels/site-panel/unit-tree-node.tsx +2 -3
  181. package/src/components/ui/sidebar/panels/site-panel/wall-tree-node.tsx +1 -5
  182. package/src/components/ui/sidebar/panels/site-panel/window-tree-node.tsx +1 -5
  183. package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +9 -4
  184. package/src/components/ui/sidebar/panels/zone-panel/index.tsx +0 -2
  185. package/src/components/ui/sidebar/tab-bar.tsx +10 -0
  186. package/src/components/ui/sidebar/use-plugin-panels.tsx +15 -5
  187. package/src/components/viewer/floorplan-compass-button.tsx +9 -1
  188. package/src/components/viewer/floorplan-preview.tsx +5 -6
  189. package/src/components/viewer/viewer-hover-label.tsx +34 -0
  190. package/src/components/viewer/viewer-rooms.tsx +175 -0
  191. package/src/components/viewer/viewer-scene-header.tsx +181 -145
  192. package/src/components/viewer/viewer-selection-manager.tsx +124 -0
  193. package/src/components/viewer/viewer-stage.tsx +2 -2
  194. package/src/components/viewer-zone-system.tsx +3 -8
  195. package/src/hooks/use-auto-save.integration.test.tsx +123 -9
  196. package/src/hooks/use-auto-save.ts +34 -8
  197. package/src/hooks/use-keyboard.test.ts +178 -3
  198. package/src/hooks/use-keyboard.ts +89 -71
  199. package/src/hooks/use-open-wall-ends.test.ts +99 -0
  200. package/src/hooks/use-open-wall-ends.ts +238 -0
  201. package/src/hooks/use-placement-notice.test.ts +48 -0
  202. package/src/hooks/use-placement-notice.ts +42 -0
  203. package/src/hooks/use-selected-room.ts +143 -0
  204. package/src/index.tsx +246 -39
  205. package/src/lib/ceiling-edit-session.test.ts +321 -0
  206. package/src/lib/ceiling-edit-session.ts +206 -0
  207. package/src/lib/ceiling-region-session.test.ts +179 -0
  208. package/src/lib/continuation.test.ts +19 -1
  209. package/src/lib/continuation.ts +25 -4
  210. package/src/lib/door-interaction.ts +14 -0
  211. package/src/lib/editor-level-display.test.ts +96 -0
  212. package/src/lib/editor-level-display.ts +48 -0
  213. package/src/lib/editor-views.ts +144 -0
  214. package/src/lib/export-geometry-wait.test.ts +90 -0
  215. package/src/lib/floor-edit-session.ts +153 -0
  216. package/src/lib/floor-footprints.test.ts +563 -0
  217. package/src/lib/floor-footprints.ts +395 -0
  218. package/src/lib/floor-foundation-support.test.ts +283 -0
  219. package/src/lib/floor-opening-draft.ts +218 -0
  220. package/src/lib/floor-openings.test.ts +216 -0
  221. package/src/lib/floor-region-geometry.ts +191 -0
  222. package/src/lib/floor-region-session.test.ts +485 -0
  223. package/src/lib/floor-region-session.ts +589 -0
  224. package/src/lib/floor-region-snap.ts +101 -0
  225. package/src/lib/floor-step-hover.test.ts +32 -0
  226. package/src/lib/floorplan/annotation-visibility.ts +17 -0
  227. package/src/lib/floorplan/dimension-drive.test.ts +360 -0
  228. package/src/lib/floorplan/dimension-drive.ts +479 -0
  229. package/src/lib/floorplan/floorplan-export.test.ts +12 -8
  230. package/src/lib/floorplan/floorplan-export.tsx +32 -8
  231. package/src/lib/floorplan/floorplan-extension.ts +32 -0
  232. package/src/lib/floorplan/floorplan-mode.test.ts +4 -0
  233. package/src/lib/floorplan/floorplan-mode.ts +2 -0
  234. package/src/lib/floorplan/floorplan-pdfkit-document.ts +339 -18
  235. package/src/lib/floorplan/floorplan-pdfkit-renderer.test.ts +138 -8
  236. package/src/lib/floorplan/floorplan-pdfkit-renderer.ts +56 -25
  237. package/src/lib/floorplan/floorplan-plugin-install.test.tsx +138 -0
  238. package/src/lib/floorplan/floorplan-readonly.ts +6 -0
  239. package/src/lib/floorplan/geometry.ts +13 -6
  240. package/src/lib/floorplan/index.ts +0 -9
  241. package/src/lib/floorplan/items.ts +5 -2
  242. package/src/lib/floorplan/marks.test.ts +213 -0
  243. package/src/lib/floorplan/marks.ts +238 -0
  244. package/src/lib/floorplan/open-wall-ends.test.ts +268 -0
  245. package/src/lib/floorplan/open-wall-ends.ts +162 -0
  246. package/src/lib/floorplan/pdf-fonts/GeistMono-Bold.ttf +0 -0
  247. package/src/lib/floorplan/pdf-fonts/GeistMono-Regular.ttf +0 -0
  248. package/src/lib/floorplan/pdf-fonts/LICENSE-GeistMono.txt +92 -0
  249. package/src/lib/floorplan/pdf-fonts/LICENSE-LiberationSans.txt +102 -0
  250. package/src/lib/floorplan/pdf-fonts/LiberationSans-Bold.ttf +0 -0
  251. package/src/lib/floorplan/pdf-fonts/LiberationSans-Regular.ttf +0 -0
  252. package/src/lib/floorplan/schedules.test.ts +189 -0
  253. package/src/lib/floorplan/schedules.ts +339 -0
  254. package/src/lib/floorplan/sheet-export.test.ts +100 -0
  255. package/src/lib/floorplan/sheet-export.ts +318 -0
  256. package/src/lib/floorplan/site-plan/build-site-plan-drawing.ts +779 -0
  257. package/src/lib/floorplan/site-plan/contours.test.ts +43 -0
  258. package/src/lib/floorplan/site-plan/contributors.ts +97 -0
  259. package/src/lib/floorplan/site-plan/coverage.ts +232 -0
  260. package/src/lib/floorplan/site-plan/drawing-type-switch.test.ts +15 -0
  261. package/src/lib/floorplan/site-plan/drawing-type-switch.tsx +86 -0
  262. package/src/lib/floorplan/site-plan/front-edge.test.ts +103 -0
  263. package/src/lib/floorplan/site-plan/front-edge.ts +156 -0
  264. package/src/lib/floorplan/site-plan/geometry.test.ts +275 -0
  265. package/src/lib/floorplan/site-plan/geometry.ts +557 -0
  266. package/src/lib/floorplan/site-plan/index.ts +71 -0
  267. package/src/lib/floorplan/site-plan/site-annotations.test.ts +366 -0
  268. package/src/lib/floorplan/site-plan/site-annotations.ts +1083 -0
  269. package/src/lib/floorplan/site-plan/site-parts.ts +379 -0
  270. package/src/lib/floorplan/site-plan/site-plan-layer.tsx +138 -0
  271. package/src/lib/floorplan/site-plan/street-name.test.ts +69 -0
  272. package/src/lib/floorplan/site-plan/street-name.ts +302 -0
  273. package/src/lib/floorplan/site-plan/yards.test.ts +69 -0
  274. package/src/lib/floorplan/types.ts +1 -27
  275. package/src/lib/fresh-planar-placement.test.ts +36 -0
  276. package/src/lib/fresh-planar-placement.ts +59 -36
  277. package/src/lib/geometry-script/author.ts +152 -0
  278. package/src/lib/geometry-script/client.ts +152 -0
  279. package/src/lib/geometry-script/protocol.ts +12 -0
  280. package/src/lib/gesture-lifecycle.test.ts +595 -0
  281. package/src/lib/gesture-lifecycle.ts +290 -0
  282. package/src/lib/glb-export-zone-holes.test.ts +41 -0
  283. package/src/lib/glb-export.test.ts +485 -0
  284. package/src/lib/glb-export.ts +237 -119
  285. package/src/lib/history.test.ts +6 -0
  286. package/src/lib/history.ts +7 -0
  287. package/src/lib/hud-title.ts +109 -0
  288. package/src/lib/hydration-gated-storage.ts +49 -0
  289. package/src/lib/ifc-export.test.ts +174 -0
  290. package/src/lib/ifc-export.ts +201 -0
  291. package/src/lib/ifc-parts.ts +15 -0
  292. package/src/lib/inspector-expanded.test.ts +44 -0
  293. package/src/lib/inspector-expanded.ts +36 -0
  294. package/src/lib/interaction/scope.ts +18 -0
  295. package/src/lib/keyboard-pan.test.ts +129 -7
  296. package/src/lib/keyboard-pan.ts +161 -12
  297. package/src/lib/live-gesture-round-trip.test.ts +360 -0
  298. package/src/lib/lot/__fixtures__/map-dossier-miami-shores.json +1 -0
  299. package/src/lib/lot/__fixtures__/map-dossier-stpete.json +1 -0
  300. package/src/lib/lot/clean-ring.test.ts +186 -0
  301. package/src/lib/lot/clean-ring.ts +217 -0
  302. package/src/lib/lot/dossier.test.ts +202 -0
  303. package/src/lib/lot/dossier.ts +498 -0
  304. package/src/lib/lot/drop-in.test.ts +326 -0
  305. package/src/lib/lot/drop-in.ts +407 -0
  306. package/src/lib/lot/index.ts +43 -0
  307. package/src/lib/lot/lot-patch.test.ts +340 -0
  308. package/src/lib/lot/lot-patch.ts +394 -0
  309. package/src/lib/lot/parcel-provider.ts +43 -0
  310. package/src/lib/lot/terrain.test.ts +161 -0
  311. package/src/lib/lot/terrain.ts +266 -0
  312. package/src/lib/material-catalog-model.ts +3 -1
  313. package/src/lib/material-paint-panel-model.ts +9 -5
  314. package/src/lib/material-paint.ts +4 -0
  315. package/src/lib/mezzanine-actions.test.ts +342 -0
  316. package/src/lib/mezzanine-draft.test.ts +501 -0
  317. package/src/lib/mezzanine-draft.ts +278 -0
  318. package/src/lib/mezzanine-messages.ts +26 -0
  319. package/src/lib/mezzanine-selection.test.ts +207 -0
  320. package/src/lib/mezzanine-stairs.ts +46 -0
  321. package/src/lib/model-export.ts +3 -1
  322. package/src/lib/node-action-movement.ts +17 -6
  323. package/src/lib/node-display-label.ts +37 -0
  324. package/src/lib/orbit-floor-clearance.test.ts +165 -0
  325. package/src/lib/orbit-floor-clearance.ts +106 -0
  326. package/src/lib/paint-commit-route.test.ts +37 -0
  327. package/src/lib/paint-commit-route.ts +17 -0
  328. package/src/lib/paint-eyedropper.test.tsx +165 -0
  329. package/src/lib/paint-eyedropper.ts +99 -0
  330. package/src/lib/paint-pass-through.test.ts +111 -0
  331. package/src/lib/paint-pass-through.ts +76 -0
  332. package/src/lib/paint-region-hover.ts +25 -0
  333. package/src/lib/paint-region-mode.ts +154 -0
  334. package/src/lib/paint-regions.test.ts +166 -0
  335. package/src/lib/paint-regions.ts +192 -0
  336. package/src/lib/paint-scope.test.ts +317 -35
  337. package/src/lib/paint-scope.ts +266 -25
  338. package/src/lib/panel-rows.ts +21 -0
  339. package/src/lib/parametric-node-update.ts +1 -1
  340. package/src/lib/plate-paint-affected.test.ts +300 -0
  341. package/src/lib/plate-paint-affected.ts +177 -0
  342. package/src/lib/plugin-chat.ts +50 -0
  343. package/src/lib/plugin-panels.test.ts +25 -0
  344. package/src/lib/plugin-panels.ts +94 -0
  345. package/src/lib/print-shell-compiler.ts +1 -0
  346. package/src/lib/registered-draft-snap.ts +12 -0
  347. package/src/lib/roof-duplication.ts +10 -2
  348. package/src/lib/room-assembly-overlay.test.ts +275 -0
  349. package/src/lib/room-assembly-overlay.ts +266 -0
  350. package/src/lib/room-built-on.test.ts +379 -0
  351. package/src/lib/room-built-on.ts +138 -0
  352. package/src/lib/room-construction-commands.ts +712 -0
  353. package/src/lib/room-construction.test.ts +746 -0
  354. package/src/lib/room-construction.ts +385 -0
  355. package/src/lib/room-controls-phase6.test.ts +630 -0
  356. package/src/lib/room-divide-session.ts +523 -0
  357. package/src/lib/room-handle-drag.ts +1068 -0
  358. package/src/lib/room-name-catalog.test.ts +90 -0
  359. package/src/lib/room-name-catalog.ts +168 -0
  360. package/src/lib/room-push-dimensions.test.ts +311 -0
  361. package/src/lib/room-push-dimensions.ts +251 -0
  362. package/src/lib/room-selection-commands.ts +62 -0
  363. package/src/lib/room-selection.test.ts +973 -0
  364. package/src/lib/room-selection.ts +417 -0
  365. package/src/lib/room-structure-commands.ts +94 -0
  366. package/src/lib/room-structure.test.ts +921 -0
  367. package/src/lib/room-transform-session.ts +570 -0
  368. package/src/lib/room-zone-routing.test.ts +294 -0
  369. package/src/lib/room-zone-routing.ts +197 -0
  370. package/src/lib/scene-clipboard.test.ts +364 -0
  371. package/src/lib/scene-clipboard.ts +229 -48
  372. package/src/lib/scene-material-refs.test.ts +79 -0
  373. package/src/lib/scene-material-refs.ts +79 -0
  374. package/src/lib/scene.test.ts +24 -36
  375. package/src/lib/scene.ts +33 -57
  376. package/src/lib/selection-routing.test.ts +129 -45
  377. package/src/lib/selection-routing.ts +53 -74
  378. package/src/lib/session-writes.test.ts +120 -0
  379. package/src/lib/session-writes.ts +205 -0
  380. package/src/lib/sidebar-panel.ts +62 -0
  381. package/src/lib/site-boundary.test.ts +4 -6
  382. package/src/lib/site-boundary.ts +1 -1
  383. package/src/lib/snapping-mode.test.ts +4 -0
  384. package/src/lib/snapping-mode.ts +8 -1
  385. package/src/lib/terrace-draft.test.ts +145 -0
  386. package/src/lib/terrace-draft.ts +213 -0
  387. package/src/lib/tool-cursor-icon.test.ts +16 -0
  388. package/src/lib/tool-cursor-icon.ts +11 -0
  389. package/src/lib/typing-target.ts +9 -0
  390. package/src/lib/units.ts +77 -39
  391. package/src/lib/vector-edges.test.ts +58 -0
  392. package/src/lib/vector-edges.ts +400 -0
  393. package/src/lib/viewer-selection.test.ts +577 -0
  394. package/src/lib/viewer-selection.ts +569 -0
  395. package/src/lib/wall-draw-variant.test.ts +71 -0
  396. package/src/lib/wall-draw-variant.ts +91 -0
  397. package/src/lib/wall-paint-region-session.ts +246 -0
  398. package/src/lib/wall-paint-region.test.ts +373 -0
  399. package/src/lib/wall-polygon-draft.test.ts +262 -0
  400. package/src/lib/wall-polygon-draft.ts +286 -0
  401. package/src/lib/wall-push.test.ts +453 -0
  402. package/src/lib/wall-region-face.ts +352 -0
  403. package/src/lib/wall-region-handles.test.ts +304 -0
  404. package/src/lib/wall-region-handles.ts +368 -0
  405. package/src/lib/wall-region-list.test.ts +119 -0
  406. package/src/lib/wall-region-list.ts +136 -0
  407. package/src/lib/wall-region-snap.ts +110 -0
  408. package/src/lib/window-interaction.ts +14 -0
  409. package/src/lib/zone-content.ts +4 -1
  410. package/src/lib/zone-name.test.ts +17 -0
  411. package/src/lib/zone-name.ts +17 -0
  412. package/src/store/opens-in-select-mode.test.ts +223 -0
  413. package/src/store/paint-mode.test.ts +188 -0
  414. package/src/store/terrain-sculpt-mode.test.ts +19 -21
  415. package/src/store/tool-mode.test.ts +137 -6
  416. package/src/store/use-delete-confirmation.ts +28 -1
  417. package/src/store/use-drawing-view.test.ts +24 -3
  418. package/src/store/use-drawing-view.ts +29 -1
  419. package/src/store/use-editor.tsx +432 -321
  420. package/src/store/use-floorplan-draft-preview.ts +13 -0
  421. package/src/store/use-floorplan-marquee.ts +12 -2
  422. package/src/store/use-quick-measurement-hud.ts +3 -3
  423. package/src/store/use-stair-build-preview.ts +13 -5
  424. package/src/store/use-wall-move-ghosts.ts +4 -2
  425. package/src/store/use-wall-region-handles-selection.test.ts +222 -0
  426. package/src/store/use-wall-region-handles-selection.ts +139 -0
  427. package/src/store/view-layout.test.ts +175 -0
  428. package/src/store/view-layout.ts +196 -0
  429. package/src/test-utils/immediate-animation-frames.ts +16 -0
  430. package/src/test-utils/live-gesture-round-trip.ts +224 -0
  431. package/src/test-utils/selection-harness.ts +82 -0
  432. package/src/components/editor-2d/renderers/floorplan-stair-layer.tsx +0 -499
  433. package/src/components/systems/zone/zone-label-editor-system.tsx +0 -333
  434. package/src/components/tools/fence/fence-drafting.test.ts +0 -100
  435. package/src/components/tools/fence/fence-drafting.ts +0 -266
  436. package/src/components/ui/sidebar/panels/plugins-panel.tsx +0 -218
  437. package/src/components/viewer/viewer-units-panel.tsx +0 -116
  438. package/src/lib/floorplan/stairs.ts +0 -489
  439. package/src/lib/level-duplication.test.ts +0 -144
  440. package/src/lib/level-duplication.ts +0 -195
@@ -0,0 +1,417 @@
1
+ import {
2
+ type AnyNode,
3
+ area,
4
+ type BoundaryNode,
5
+ type BoundarySpan,
6
+ containsPoint,
7
+ createRoomTopologyIndex,
8
+ getWallArcData,
9
+ getWallFaceOffsets,
10
+ type MultiPolygon,
11
+ roomClearPolygon,
12
+ type TopologyRoom,
13
+ type WallNode,
14
+ type ZoneNode,
15
+ } from '@pascal-app/core'
16
+ import type { SelectionModifierKeys } from './selection-routing'
17
+ import { polygonMatchesZoneFootprint } from './zone-content'
18
+
19
+ export type RoomKey = { levelId: string; zoneId: string }
20
+
21
+ export function sameRoom(a: RoomKey | null, b: RoomKey | null): boolean {
22
+ return a === b || (!!a && !!b && a.levelId === b.levelId && a.zoneId === b.zoneId)
23
+ }
24
+
25
+ export function shouldSelectRoom(
26
+ selected: RoomKey | null,
27
+ hit: RoomKey | null,
28
+ modifiers: SelectionModifierKeys,
29
+ ): boolean {
30
+ return (
31
+ !!hit &&
32
+ !sameRoom(selected, hit) &&
33
+ !(modifiers.alt || modifiers.shift || modifiers.ctrl || modifiers.meta)
34
+ )
35
+ }
36
+
37
+ export type RoomSelectionGeometry = TopologyRoom & {
38
+ key: RoomKey
39
+ boundaryWallIds: string[]
40
+ clearPolygon: ReturnType<typeof roomClearPolygon>
41
+ area: number
42
+ hitPolygon: MultiPolygon
43
+ /** A stacked open-below room (`floor.support: 'open'`) inside its host room. */
44
+ mezzanine: { hostZoneId: string } | null
45
+ }
46
+
47
+ export type RoomSelectionRecord = RoomSelectionGeometry & {
48
+ geometry: RoomSelectionGeometry
49
+ name: string
50
+ zoneId: string
51
+ slabId: string | null
52
+ slabName: string | null
53
+ ceilingId: string | null
54
+ ceilingName: string | null
55
+ }
56
+
57
+ type Nodes = Record<string, AnyNode>
58
+
59
+ function describeRoomGeometry(
60
+ levelId: string,
61
+ room: TopologyRoom,
62
+ zone: ZoneNode,
63
+ ): RoomSelectionGeometry {
64
+ const clearPolygon = roomClearPolygon(room)
65
+ return {
66
+ ...room,
67
+ key: { levelId, zoneId: zone.id },
68
+ boundaryWallIds: [
69
+ ...new Set(room.spans.filter((span) => span.kind === 'wall').map((span) => span.boundaryId)),
70
+ ],
71
+ clearPolygon,
72
+ area: area([{ outer: zone.polygon, holes: zone.holes }]),
73
+ hitPolygon: [{ outer: room.polygon, holes: room.holes }],
74
+ mezzanine: null,
75
+ }
76
+ }
77
+
78
+ export function isMezzanineZone(node: AnyNode | undefined): node is ZoneNode {
79
+ return node?.type === 'zone' && node.floor?.support === 'open'
80
+ }
81
+
82
+ /**
83
+ * A mezzanine as a selectable room. It has no walls of its own: its outline is
84
+ * the zone's polygon, its floor the zone's own plate, and it bounds nothing.
85
+ */
86
+ function describeMezzanineGeometry(
87
+ levelId: string,
88
+ zone: ZoneNode,
89
+ context: TopologyRoom['context'],
90
+ ): RoomSelectionGeometry {
91
+ const polygon = zone.polygon.map(([x, z]) => [x, z] as [number, number])
92
+ const holes = (zone.holes ?? []).map((ring) => ring.map(([x, z]) => [x, z] as [number, number]))
93
+ const outline = [{ outer: polygon, holes }]
94
+ return {
95
+ id: `mezzanine:${zone.id}`,
96
+ polygon,
97
+ holes,
98
+ spans: [],
99
+ context,
100
+ key: { levelId, zoneId: zone.id },
101
+ boundaryWallIds: [],
102
+ clearPolygon: outline,
103
+ area: area(outline),
104
+ hitPolygon: outline,
105
+ mezzanine: { hostZoneId: zone.hostZoneId ?? '' },
106
+ }
107
+ }
108
+
109
+ const NO_WALLS: TopologyRoom['context'] = {
110
+ revision: -1,
111
+ walls: new Map(),
112
+ wallFootprints: new Map(),
113
+ }
114
+
115
+ /**
116
+ * A zone that bounds no room (a drawn area), outlined the way a room is: its
117
+ * polygon as the floor, no walls of its own.
118
+ */
119
+ export function describeZoneOutline(levelId: string, zone: ZoneNode): RoomSelectionRecord {
120
+ const geometry: RoomSelectionGeometry = {
121
+ ...describeMezzanineGeometry(levelId, zone, NO_WALLS),
122
+ id: `zone:${zone.id}`,
123
+ mezzanine: null,
124
+ }
125
+ return {
126
+ ...geometry,
127
+ geometry,
128
+ name: zone.name || 'Zone',
129
+ zoneId: zone.id,
130
+ slabId: null,
131
+ slabName: null,
132
+ ceilingId: null,
133
+ ceilingName: null,
134
+ }
135
+ }
136
+
137
+ function describeRoom(
138
+ geometry: RoomSelectionGeometry,
139
+ candidates: readonly AnyNode[],
140
+ ): RoomSelectionRecord {
141
+ const { clearPolygon } = geometry
142
+ const matches = (node: AnyNode) =>
143
+ (node.type === 'zone' || node.type === 'slab' || node.type === 'ceiling') &&
144
+ (polygonMatchesZoneFootprint(node.polygon, geometry.polygon) ||
145
+ clearPolygon.some(({ outer }) => polygonMatchesZoneFootprint(node.polygon, outer)))
146
+ const slab = geometry.mezzanine
147
+ ? candidates.find(
148
+ (node) =>
149
+ node.type === 'slab' &&
150
+ node.support === 'open' &&
151
+ !!node.zoneIds?.includes(geometry.key.zoneId),
152
+ )
153
+ : candidates.find(
154
+ (node) =>
155
+ node.type === 'slab' && node.support !== 'open' && node.autoFromWalls && matches(node),
156
+ )
157
+ const ceiling = candidates.find(
158
+ (node) => node.type === 'ceiling' && node.zoneId === geometry.key.zoneId,
159
+ )
160
+ const zone = candidates.find(
161
+ (node): node is ZoneNode => node.type === 'zone' && node.id === geometry.key.zoneId,
162
+ )!
163
+ return {
164
+ ...geometry,
165
+ geometry,
166
+ name: zone.name || 'Room',
167
+ area: area([{ outer: zone.polygon, holes: zone.holes }]),
168
+ zoneId: zone.id,
169
+ slabId: slab?.id ?? null,
170
+ slabName: slab ? slab.name || 'Slab' : null,
171
+ ceilingId: ceiling?.id ?? null,
172
+ ceilingName: ceiling ? ceiling.name || 'Ceiling' : null,
173
+ }
174
+ }
175
+
176
+ type RoomHitIndex = Pick<
177
+ ReturnType<typeof createRoomTopologyIndex>,
178
+ 'roomAtPoint' | 'roomForWallHit' | 'spansForWall'
179
+ > & {
180
+ /** The selectable room a zone is (a mezzanine's own plate or ceiling points at it). */
181
+ roomForZone?: (levelId: string, zoneId: string) => TopologyRoom | null
182
+ }
183
+ const wallFrames = new WeakMap<WallNode, ReturnType<typeof wallHitFrame>>()
184
+
185
+ function wallHitFrame(wall: WallNode) {
186
+ const dx = wall.end[0] - wall.start[0]
187
+ const dz = wall.end[1] - wall.start[1]
188
+ const length = Math.hypot(dx, dz)
189
+ const offsets = getWallFaceOffsets(wall)
190
+ return {
191
+ dx,
192
+ dz,
193
+ lengthSq: length * length,
194
+ nx: -dz / length,
195
+ nz: dx / length,
196
+ arc: getWallArcData(wall),
197
+ center: (offsets.a + offsets.b) / 2,
198
+ }
199
+ }
200
+
201
+ export function resolveRoomHit<T extends RoomHitIndex>(
202
+ index: T,
203
+ levelId: string,
204
+ node: AnyNode | null,
205
+ point: [number, number],
206
+ view?: '2d' | '3d',
207
+ ): ReturnType<T['roomAtPoint']>
208
+ export function resolveRoomHit(
209
+ index: RoomHitIndex,
210
+ levelId: string,
211
+ node: AnyNode | null,
212
+ point: [number, number],
213
+ view: '2d' | '3d' = '2d',
214
+ ): TopologyRoom | null {
215
+ // A mezzanine's plate (railing included) and ceiling name their own room; the
216
+ // host floor under it and every other surface resolve by the point, which
217
+ // never lands in a mezzanine.
218
+ if (node?.type === 'slab' && node.support === 'open')
219
+ return index.roomForZone?.(levelId, node.zoneIds?.[0] ?? '') ?? null
220
+ if (node?.type === 'ceiling' && node.zoneId) {
221
+ const own = index.roomForZone?.(levelId, node.zoneId)
222
+ if (own) return own
223
+ }
224
+ if (!node || node.type === 'slab' || node.type === 'ceiling')
225
+ return index.roomAtPoint(levelId, point)
226
+ if (node.type !== 'wall') return null
227
+ let frame = wallFrames.get(node)
228
+ if (!frame) {
229
+ frame = wallHitFrame(node)
230
+ wallFrames.set(node, frame)
231
+ }
232
+ const { dx, dz, lengthSq, arc } = frame
233
+ if (!lengthSq) return null
234
+ const station = Math.max(
235
+ 0,
236
+ Math.min(1, ((point[0] - node.start[0]) * dx + (point[1] - node.start[1]) * dz) / lengthSq),
237
+ )
238
+ let x = node.start[0] + station * dx
239
+ let z = node.start[1] + station * dz
240
+ let nx = frame.nx
241
+ let nz = frame.nz
242
+ if (arc) {
243
+ const angle = Math.atan2(point[1] - arc.center.y, point[0] - arc.center.x)
244
+ const delta = Math.atan2(Math.sin(angle - arc.startAngle), Math.cos(angle - arc.startAngle))
245
+ const t = Math.max(0, Math.min(1, delta / arc.delta))
246
+ const theta = arc.startAngle + arc.delta * t
247
+ const cos = Math.cos(theta)
248
+ const sin = Math.sin(theta)
249
+ x = arc.center.x + cos * arc.radius
250
+ z = arc.center.y + sin * arc.radius
251
+ nx = -cos * arc.direction
252
+ nz = -sin * arc.direction
253
+ }
254
+ const localZ = (point[0] - x) * nx + (point[1] - z) * nz
255
+ // Justified bodies can lie entirely on one side of the reference line.
256
+ const center = view === '3d' ? frame.center : 0
257
+ const face = localZ >= center ? 'a' : 'b'
258
+ // Room first: a face with no room behind it (the exterior) still resolves
259
+ // to the room on the other face, or to the wall's only room; only walls
260
+ // bounding no room reach the wall itself.
261
+ return (
262
+ index.roomForWallHit(levelId, node.id, face, station) ??
263
+ index.roomForWallHit(levelId, node.id, face === 'a' ? 'b' : 'a', station) ??
264
+ soleWallRoom(index, levelId, node.id)
265
+ )
266
+ }
267
+
268
+ function soleWallRoom(index: RoomHitIndex, levelId: string, wallId: string) {
269
+ let sole: BoundarySpan | null = null
270
+ for (const span of index.spansForWall(levelId, wallId)) {
271
+ if (sole && sole.roomId !== span.roomId) return null
272
+ sole ??= span
273
+ }
274
+ return sole && index.roomForWallHit(levelId, wallId, sole.face, (sole.t0 + sole.t1) / 2)
275
+ }
276
+
277
+ function isBoundary(node: AnyNode): node is BoundaryNode {
278
+ return node.type === 'wall' || node.type === 'separator'
279
+ }
280
+
281
+ function sameBoundary(a: BoundaryNode, b: AnyNode | undefined) {
282
+ return (
283
+ !!b &&
284
+ isBoundary(b) &&
285
+ a.type === b.type &&
286
+ a.parentId === b.parentId &&
287
+ a.start[0] === b.start[0] &&
288
+ a.start[1] === b.start[1] &&
289
+ a.end[0] === b.end[0] &&
290
+ a.end[1] === b.end[1] &&
291
+ (a.type === 'separator' ||
292
+ (b.type === 'wall' &&
293
+ a.curveOffset === b.curveOffset &&
294
+ a.thickness === b.thickness &&
295
+ a.justification === b.justification))
296
+ )
297
+ }
298
+
299
+ export class RoomSelectionIndex {
300
+ readonly topology = createRoomTopologyIndex()
301
+ private nodes: Nodes | null = null
302
+ private records: RoomSelectionRecord[] = []
303
+ private geometry: RoomSelectionGeometry[] = []
304
+ private candidates: AnyNode[] = []
305
+ private revision = -1
306
+ private readonly roomsById = new Map<string, RoomSelectionGeometry>()
307
+
308
+ roomAtPoint(_levelId: string, point: [number, number]): RoomSelectionGeometry | null {
309
+ for (const room of this.geometry)
310
+ if (!room.mezzanine && containsPoint(room.hitPolygon, point)) return room
311
+ return null
312
+ }
313
+
314
+ roomForZone(_levelId: string, zoneId: string): RoomSelectionGeometry | null {
315
+ return this.geometry.find((room) => room.key.zoneId === zoneId) ?? null
316
+ }
317
+
318
+ roomForWallHit(
319
+ levelId: string,
320
+ wallId: string,
321
+ face: 'a' | 'b',
322
+ t: number,
323
+ ): RoomSelectionGeometry | null {
324
+ const spans = this.topology.getLevelTopology(levelId)?.spansByBoundary.get(wallId)
325
+ if (!spans) return null
326
+ for (const span of spans) {
327
+ if (span.face === face && t >= span.t0 && (t < span.t1 || (t === 1 && span.t1 === 1)))
328
+ return this.roomsById.get(span.roomId) ?? null
329
+ }
330
+ return null
331
+ }
332
+
333
+ spansForWall(levelId: string, wallId: string) {
334
+ return this.topology.spansForWall(levelId, wallId)
335
+ }
336
+
337
+ constructor(readonly levelId: string) {}
338
+
339
+ update(nodes: Nodes): RoomSelectionRecord[] {
340
+ if (nodes === this.nodes) return this.records
341
+ if (!this.nodes) this.topology.rebuildLevel(this.levelId, nodes)
342
+ else {
343
+ const changed = new Set<string>()
344
+ for (const snapshot of [this.nodes, nodes]) {
345
+ for (const node of Object.values(snapshot)) {
346
+ if (
347
+ isBoundary(node) &&
348
+ node.parentId === this.levelId &&
349
+ !sameBoundary(node, snapshot === nodes ? this.nodes[node.id] : nodes[node.id])
350
+ )
351
+ changed.add(node.id)
352
+ }
353
+ }
354
+ if (changed.size) this.topology.applyWallDelta(this.levelId, changed, this.nodes, nodes)
355
+ }
356
+ this.nodes = nodes
357
+ const topology = this.topology.getLevelTopology(this.levelId)
358
+ const geometryChanged = topology?.revision !== this.revision
359
+ const candidates = Object.values(nodes).filter(
360
+ (node) =>
361
+ node.parentId === this.levelId &&
362
+ (node.type === 'slab' || node.type === 'ceiling' || node.type === 'zone'),
363
+ )
364
+ const candidatesChanged =
365
+ candidates.length !== this.candidates.length ||
366
+ candidates.some((node, i) => node !== this.candidates[i])
367
+ if (!geometryChanged && !candidatesChanged) return this.records
368
+ if (geometryChanged || candidatesChanged) {
369
+ this.revision = topology?.revision ?? -1
370
+ const previous = new Map(this.geometry.map((room) => [room.key.zoneId, room]))
371
+ const matched = new Set<string>()
372
+ const zones = candidates
373
+ .filter(
374
+ (node): node is ZoneNode =>
375
+ node.type === 'zone' &&
376
+ node.spaceRole === 'room' &&
377
+ node.enclosureStatus !== 'open' &&
378
+ !isMezzanineZone(node),
379
+ )
380
+ .sort((a, b) => a.id.localeCompare(b.id))
381
+ this.geometry = (topology?.rooms ?? []).flatMap((room) => {
382
+ const boundaryIds = [...new Set(room.spans.map((span) => span.boundaryId))].sort().join('|')
383
+ const available = zones.filter((zone) => !matched.has(zone.id))
384
+ const zone =
385
+ available.find(
386
+ (node) =>
387
+ [...node.boundaryWallIds, ...node.boundarySeparatorIds].sort().join('|') ===
388
+ boundaryIds,
389
+ ) ??
390
+ available.find(
391
+ (node) =>
392
+ !!node.seed && containsPoint([{ outer: room.polygon, holes: room.holes }], node.seed),
393
+ )
394
+ if (!zone) return []
395
+ matched.add(zone.id)
396
+ return [
397
+ (!geometryChanged && previous.get(zone.id)) ||
398
+ describeRoomGeometry(this.levelId, room, zone),
399
+ ]
400
+ })
401
+ const context = topology?.rooms[0]?.context
402
+ if (context)
403
+ for (const zone of candidates) {
404
+ if (!isMezzanineZone(zone) || zone.spaceRole !== 'room') continue
405
+ const hosted =
406
+ zone.hostZoneId && this.geometry.some((room) => room.key.zoneId === zone.hostZoneId)
407
+ if (!hosted) continue
408
+ this.geometry.push(describeMezzanineGeometry(this.levelId, zone, context))
409
+ }
410
+ this.roomsById.clear()
411
+ for (const room of this.geometry) this.roomsById.set(room.id, room)
412
+ }
413
+ this.candidates = candidates
414
+ this.records = this.geometry.map((room) => describeRoom(room, candidates))
415
+ return this.records
416
+ }
417
+ }
@@ -0,0 +1,94 @@
1
+ import {
2
+ type AnyNodeId,
3
+ deleteZone,
4
+ mergeZones,
5
+ type StructureNodes,
6
+ type StructurePlan,
7
+ setZoneIntent,
8
+ structureChangeBatch,
9
+ useScene,
10
+ } from '@pascal-app/core'
11
+ import { useViewer } from '@pascal-app/viewer'
12
+ import useDeleteConfirmation from '../store/use-delete-confirmation'
13
+ import useEditor from '../store/use-editor'
14
+ import { showRoomNotice } from './room-transform-session'
15
+ import { completeElementAction, type ElementActionOrigin } from './room-zone-routing'
16
+
17
+ export function applyRoomPlan(plan: StructurePlan) {
18
+ if (plan.conflicts?.length) return false
19
+ if (plan.changes.length) useScene.getState().applyNodeChanges(structureChangeBatch(plan.changes))
20
+ return true
21
+ }
22
+
23
+ export function renameRoom(zoneId: string, name: string) {
24
+ return applyRoomPlan(setZoneIntent(useScene.getState().nodes, { zoneId, patch: { name } }))
25
+ }
26
+
27
+ /**
28
+ * Asks before deleting a room, per what core's `deleteZone` does with it: a
29
+ * room Divide made merges back into its neighbour, a room with walls of its
30
+ * own goes with them. A room inside walls other rooms share cannot be deleted
31
+ * at all — no dialog, just the reason under its pill.
32
+ */
33
+ export function requestRoomDeletion(zoneId: string) {
34
+ const plan = deleteZone(useScene.getState().nodes, { zoneId, contents: 'delete' })
35
+ if (plan.payload.mode === 'blocked') {
36
+ showRoomNotice({ zoneId, message: plan.conflicts?.[0]?.message ?? '' })
37
+ return
38
+ }
39
+ const commit = (contents: 'delete' | 'keep') => {
40
+ if (!useScene.getState().nodes[zoneId as AnyNodeId]) return
41
+ const current = deleteZone(useScene.getState().nodes, { zoneId, contents })
42
+ if (
43
+ JSON.stringify({ ...current.payload, contents: 'delete' }) !== JSON.stringify(plan.payload)
44
+ ) {
45
+ requestRoomDeletion(zoneId)
46
+ return
47
+ }
48
+ // Read before the write: the selection of the deleted room clears with it.
49
+ const levelId = useScene.getState().nodes[zoneId as AnyNodeId]?.parentId
50
+ if (!applyRoomPlan(current)) return
51
+ const merged = current.payload.mergedIntoZoneId
52
+ useViewer.getState().setSelection({ selectedIds: [] })
53
+ // The area is part of the room it merged into now: that room stays selected.
54
+ if (merged && levelId && useScene.getState().nodes[merged as AnyNodeId])
55
+ useEditor.getState().selectRoom({ levelId, zoneId: merged })
56
+ else useEditor.getState().clearRoom()
57
+ }
58
+ useDeleteConfirmation.getState().requestConfirmation({
59
+ count: plan.changes.filter((c) => c.op === 'delete').length,
60
+ room: plan.payload,
61
+ conflict: plan.conflicts?.map((c) => c.message).join(' '),
62
+ onConfirm: () => commit('delete'),
63
+ onKeepContents: plan.payload.mode === 'merge' ? undefined : () => commit('keep'),
64
+ })
65
+ }
66
+
67
+ export function separatorMergePlan(nodes: StructureNodes, separatorId: string): StructurePlan {
68
+ const zones = Object.values(nodes).filter(
69
+ (node) =>
70
+ node.type === 'zone' &&
71
+ node.spaceRole === 'room' &&
72
+ node.boundarySeparatorIds.includes(separatorId),
73
+ )
74
+ if (zones.length !== 2)
75
+ return {
76
+ changes: [],
77
+ conflicts: [
78
+ {
79
+ code: 'separator-room-count',
80
+ nodeIds: [separatorId],
81
+ message: 'Select a separator between exactly two rooms.',
82
+ },
83
+ ],
84
+ }
85
+ return mergeZones(nodes, { zoneIds: [zones[0]!.id, zones[1]!.id] })
86
+ }
87
+ /** Deleting a separator merges its two rooms; the merged room is selected when the separator was drilled from one. */
88
+ export function deleteSelectedSeparator(id: AnyNodeId, origin: ElementActionOrigin | null = null) {
89
+ const plan = separatorMergePlan(useScene.getState().nodes, id)
90
+ if (!applyRoomPlan(plan)) return false
91
+ useViewer.getState().setSelection({ selectedIds: [] })
92
+ completeElementAction(origin)
93
+ return true
94
+ }