@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,385 @@
1
+ import {
2
+ type AnyNode,
3
+ area,
4
+ type BoundaryNode,
5
+ type BoundarySpan,
6
+ type CeilingNode,
7
+ calculateLevelMiters,
8
+ containsPoint,
9
+ difference,
10
+ type ExtractedRoom,
11
+ extractRooms,
12
+ getWallPlanFootprint,
13
+ intersection,
14
+ isDerivedNode,
15
+ type Polygon,
16
+ roomDrawnFloor,
17
+ type SlabNode,
18
+ type SpanRef,
19
+ type StructureNodes,
20
+ union,
21
+ type WallNode,
22
+ type ZoneNode,
23
+ } from '@pascal-app/core'
24
+
25
+ export type ConstructionPresence = 'present' | 'partial' | 'absent'
26
+
27
+ /** The buttons a row offers, derived with its presence so the two never disagree. */
28
+ export type ConstructionActions = {
29
+ add: boolean
30
+ remove: boolean
31
+ /** Ceiling only: adopt the hand-drawn ceiling over the room as its ceiling. */
32
+ useExisting?: boolean
33
+ /** Ceiling only: swap the hand-drawn ceiling for one generated from the room. */
34
+ replace?: boolean
35
+ }
36
+
37
+ export type RoomConstructionState = {
38
+ zoneId: string
39
+ name: string
40
+ floor: {
41
+ state: ConstructionPresence
42
+ /** `hasFloor !== false`. */
43
+ intent: boolean
44
+ /** Fraction of the room footprint covered by its plate or manual slabs, 0..1. */
45
+ coverage: number
46
+ actions: ConstructionActions
47
+ }
48
+ walls: {
49
+ state: ConstructionPresence
50
+ /** Wall spans no other room faces: what Remove turns into separators. */
51
+ removable: SpanRef[]
52
+ /** Separator spans: what Add turns into walls. */
53
+ separators: SpanRef[]
54
+ /** Distinct walls this room shares with a neighbour; they stay on Remove. */
55
+ sharedWallIds: string[]
56
+ actions: ConstructionActions
57
+ }
58
+ ceiling: {
59
+ state: ConstructionPresence
60
+ /** `hasCeiling !== false`. */
61
+ intent: boolean
62
+ /** The room's own (derived) ceiling. */
63
+ ceilingId: string | null
64
+ /** Items hanging from the room's own ceiling: its `children`. */
65
+ hostedIds: string[]
66
+ /** Hand-drawn ceilings standing in for a missing room ceiling. */
67
+ manualIds: string[]
68
+ /** Items hanging from those hand-drawn ceilings: their `children`. */
69
+ manualHostedIds: string[]
70
+ actions: ConstructionActions
71
+ }
72
+ }
73
+
74
+ // Coverage below/above these reads as Absent/Present; anything between is Partial.
75
+ const COVERED = 0.98
76
+ const UNCOVERED = 0.02
77
+ // Mirrors core's ORPHAN_MERGE_COVERAGE_THRESHOLD: a hand-drawn ceiling covering
78
+ // this share of a room stops the reconciler from generating the room's own.
79
+ const MANUAL_CEILING_BLOCKS = 0.6
80
+ const OPENING_HOLES = new Set(['stair', 'elevator'])
81
+ // Without the type guard: a ceiling that is not derived is still a CeilingNode.
82
+ const derived = (node: AnyNode): boolean => isDerivedNode(node)
83
+
84
+ const footprint = (node: { polygon: [number, number][]; holes?: [number, number][][] }) => ({
85
+ outer: node.polygon,
86
+ holes: node.holes ?? [],
87
+ })
88
+
89
+ function coverageOf(zone: ZoneNode, covers: Polygon[]) {
90
+ const room = footprint(zone)
91
+ const total = area([room])
92
+ if (!(total > 0 && covers.length)) return 0
93
+ return Math.min(1, area(intersection(room, union(covers))) / total)
94
+ }
95
+
96
+ function presence(coverage: number): ConstructionPresence {
97
+ if (coverage >= COVERED) return 'present'
98
+ return coverage > UNCOVERED ? 'partial' : 'absent'
99
+ }
100
+
101
+ // One extraction per level boundary set: intent edits (name, finishes,
102
+ // hasFloor/hasCeiling) keep every boundary node identical and reuse it.
103
+ const facesByLevel = new Map<string, { boundaries: BoundaryNode[]; faces: ExtractedRoom[] }>()
104
+
105
+ function levelFaces(nodes: StructureNodes, levelId: string) {
106
+ const boundaries = Object.values(nodes).filter(
107
+ (node): node is BoundaryNode =>
108
+ node.parentId === levelId && (node.type === 'wall' || node.type === 'separator'),
109
+ )
110
+ const cached = facesByLevel.get(levelId)
111
+ if (
112
+ cached &&
113
+ cached.boundaries.length === boundaries.length &&
114
+ cached.boundaries.every((node, i) => node === boundaries[i])
115
+ )
116
+ return cached.faces
117
+ const faces = extractRooms(boundaries)
118
+ facesByLevel.set(levelId, { boundaries, faces })
119
+ return faces
120
+ }
121
+
122
+ // Mirrors core's `roomFace`, which setZoneEdges and lockOutsideFaces use to
123
+ // validate spans: the face with the largest overlap with the zone footprint.
124
+ function zoneFace(zone: ZoneNode, faces: ExtractedRoom[]) {
125
+ let best: ExtractedRoom | undefined
126
+ let bestOverlap = 0
127
+ for (const face of faces) {
128
+ const overlap = area(
129
+ intersection(footprint(zone), { outer: face.referencePolygon, holes: face.holes }),
130
+ )
131
+ if (overlap > bestOverlap) {
132
+ best = face
133
+ bestOverlap = overlap
134
+ }
135
+ }
136
+ return best
137
+ }
138
+
139
+ /**
140
+ * The room's walkable area: its boundary face minus the footprints of the
141
+ * walls on it — the same `clear` polygon the reconciler fits plates and
142
+ * ceilings to. Null when the room has no face.
143
+ */
144
+ export function roomClearPolygon(nodes: StructureNodes, zoneId: string): Polygon | null {
145
+ const zone = nodes[zoneId]
146
+ if (zone?.type !== 'zone' || !zone.parentId) return null
147
+ const face = zoneFace(zone, levelFaces(nodes, zone.parentId))
148
+ if (!face) return null
149
+ const polygon = { outer: face.referencePolygon, holes: face.holes }
150
+ const wallIds = new Set(face.spans.filter((s) => s.kind === 'wall').map((s) => s.boundaryId))
151
+ if (!wallIds.size) return polygon
152
+ const walls = Object.values(nodes).filter(
153
+ (node): node is WallNode => node.type === 'wall' && node.parentId === zone.parentId,
154
+ )
155
+ const miters = calculateLevelMiters(walls)
156
+ const footprints = walls
157
+ .filter((wall) => wallIds.has(wall.id))
158
+ .map((wall) => getWallPlanFootprint(wall, miters).map(({ x, y }): [number, number] => [x, y]))
159
+ return difference(polygon, union(footprints)).sort((a, b) => area([b]) - area([a]))[0] ?? null
160
+ }
161
+
162
+ const overlaps = (a: SpanRef, b: SpanRef) =>
163
+ a.boundaryId === b.boundaryId &&
164
+ a.face !== b.face &&
165
+ Math.min(a.t1, b.t1) - Math.max(a.t0, b.t0) > 1e-6
166
+
167
+ const spanRef = ({ boundaryId, face, t0, t1 }: BoundarySpan): SpanRef => ({
168
+ boundaryId,
169
+ face,
170
+ t0,
171
+ t1,
172
+ })
173
+
174
+ function wallsState(nodes: StructureNodes, zone: ZoneNode): RoomConstructionState['walls'] {
175
+ const faces = levelFaces(nodes, zone.parentId!)
176
+ const spans = zoneFace(zone, faces)?.spans ?? []
177
+ const wallSpans = spans.filter((span) => span.kind === 'wall')
178
+ // A span is shared when a neighbouring room's face covers the other side of
179
+ // it — the same test setZoneEdges applies before leaving a wall in place.
180
+ const neighbourZones = Object.values(nodes).filter(
181
+ (node): node is ZoneNode =>
182
+ node.type === 'zone' &&
183
+ node.id !== zone.id &&
184
+ node.parentId === zone.parentId &&
185
+ node.spaceRole === 'room',
186
+ )
187
+ const neighbourFaces = new Map<ZoneNode, ExtractedRoom | undefined>()
188
+ const faceOf = (other: ZoneNode) => {
189
+ if (!neighbourFaces.has(other)) neighbourFaces.set(other, zoneFace(other, faces))
190
+ return neighbourFaces.get(other)
191
+ }
192
+ const shared = (span: BoundarySpan) =>
193
+ neighbourZones.some(
194
+ (other) =>
195
+ other.boundaryWallIds.includes(span.boundaryId as ZoneNode['boundaryWallIds'][number]) &&
196
+ faceOf(other)?.spans.some((candidate) => overlaps(candidate, span)),
197
+ )
198
+ const sharedSpans = wallSpans.filter(shared)
199
+ const walls = wallSpans.length
200
+ const removable = wallSpans.filter((span) => !sharedSpans.includes(span)).map(spanRef)
201
+ const separators = spans.filter((span) => span.kind === 'separator').map(spanRef)
202
+ return {
203
+ state: walls === 0 ? 'absent' : walls === spans.length ? 'present' : 'partial',
204
+ removable,
205
+ separators,
206
+ sharedWallIds: [...new Set(sharedSpans.map((span) => span.boundaryId))],
207
+ actions: { add: separators.length > 0, remove: removable.length > 0 },
208
+ }
209
+ }
210
+
211
+ /** The room's floor plates: derived slabs that list the zone. */
212
+ function roomPlates(nodes: StructureNodes, zone: ZoneNode) {
213
+ return Object.values(nodes).filter(
214
+ (node): node is SlabNode =>
215
+ node.type === 'slab' &&
216
+ node.parentId === zone.parentId &&
217
+ isDerivedNode(node) &&
218
+ !!node.zoneIds?.includes(zone.id),
219
+ )
220
+ }
221
+
222
+ function floorState(nodes: StructureNodes, zone: ZoneNode): RoomConstructionState['floor'] {
223
+ const intent = zone.hasFloor !== false
224
+ // A drawn-slab floor is the slab: switching the room's floor off would hide
225
+ // nothing, so only the slab itself can go. A floor switched off before still
226
+ // shows the slab it stands on, and can be switched back on.
227
+ const drawn = roomDrawnFloor(nodes, zone.id) !== null
228
+ const actions = { add: !intent, remove: intent && !drawn }
229
+ if (!intent && !drawn) return { state: 'absent', intent, coverage: 0, actions }
230
+ // The walking surface is this room's plate(s) plus any manual slab on the
231
+ // level; stair and elevator holes are openings, not missing floor. Once
232
+ // the room has a plate, another floor's base plate counts too: a room on a
233
+ // separate floor meets its neighbour's floor under the wall they share.
234
+ const listed = roomPlates(nodes, zone).length > 0
235
+ const covers = Object.values(nodes).flatMap((node) => {
236
+ if (node.type !== 'slab' || node.parentId !== zone.parentId) return []
237
+ if (
238
+ isDerivedNode(node) &&
239
+ !node.zoneIds?.includes(zone.id) &&
240
+ !(listed && node.plateRole === 'base')
241
+ )
242
+ return []
243
+ return [
244
+ {
245
+ outer: node.polygon,
246
+ holes: node.holes.filter(
247
+ (_, i) => !OPENING_HOLES.has(node.holeMetadata[i]?.source ?? 'manual'),
248
+ ),
249
+ },
250
+ ]
251
+ })
252
+ const coverage = coverageOf(zone, covers)
253
+ return { state: presence(coverage), intent, coverage, actions }
254
+ }
255
+
256
+ const childrenOf = (nodes: StructureNodes, ceilings: CeilingNode[]) =>
257
+ ceilings.flatMap((ceiling) => ceiling.children.filter((id) => nodes[id]))
258
+
259
+ function ceilingState(nodes: StructureNodes, zone: ZoneNode): RoomConstructionState['ceiling'] {
260
+ const intent = zone.hasCeiling !== false
261
+ const ceilings = Object.values(nodes).filter(
262
+ (node): node is CeilingNode => node.type === 'ceiling' && node.parentId === zone.parentId,
263
+ )
264
+ const linked = intent
265
+ ? ceilings.find((ceiling) => isDerivedNode(ceiling) && ceiling.zoneId === zone.id)
266
+ : undefined
267
+ if (linked)
268
+ return {
269
+ state: 'present',
270
+ intent,
271
+ ceilingId: linked.id,
272
+ hostedIds: childrenOf(nodes, [linked]),
273
+ manualIds: [],
274
+ manualHostedIds: [],
275
+ actions: { add: false, remove: true },
276
+ }
277
+ // Without its own ceiling, a hand-drawn one over the room stands in for it.
278
+ const manual = ceilings.filter(
279
+ (ceiling) => !derived(ceiling) && coverageOf(zone, [footprint(ceiling)]) > UNCOVERED,
280
+ )
281
+ const base = { intent, ceilingId: null, hostedIds: [] }
282
+ if (!manual.length)
283
+ return {
284
+ ...base,
285
+ state: 'absent',
286
+ manualIds: [],
287
+ manualHostedIds: [],
288
+ actions: { add: !intent, remove: false },
289
+ }
290
+ // Clearing the opt-out under a ceiling that covers this much keeps it as the
291
+ // room's ceiling; under a smaller one the reconciler generates a room ceiling.
292
+ const blocks = coverageOf(zone, manual.map(footprint)) >= MANUAL_CEILING_BLOCKS
293
+ return {
294
+ ...base,
295
+ // Someone else's surface: the room is only partly its own.
296
+ state: 'partial',
297
+ manualIds: manual.map((ceiling) => ceiling.id),
298
+ manualHostedIds: childrenOf(nodes, manual),
299
+ actions: {
300
+ add: !(intent || blocks),
301
+ remove: true,
302
+ useExisting: !intent && blocks,
303
+ replace: true,
304
+ },
305
+ }
306
+ }
307
+
308
+ // Floor-standing furnishings; structure (stairs, columns) stays out of it.
309
+ const FLOOR_CONTENT = new Set(['item', 'procedural-item', 'cabinet', 'shelf'])
310
+ const RESTING = 1e-3
311
+
312
+ type Positioned = AnyNode & {
313
+ position: [number, number, number]
314
+ supportSlabId?: string
315
+ }
316
+
317
+ /**
318
+ * What stands on the room's floor: furnishings pinned to one of its plates
319
+ * (`supportSlabId`, or parented to a plate), plus unpinned ones on the level
320
+ * resting at floor height inside the room's clear polygon. Each comes with
321
+ * the elevation of the plate it stands on, so it can stay put when the plate
322
+ * goes. Children of these (a lamp on a table) move and go with them.
323
+ */
324
+ export function roomFloorContents(
325
+ nodes: StructureNodes,
326
+ zoneId: string,
327
+ ): { id: string; elevation: number }[] {
328
+ const zone = nodes[zoneId]
329
+ if (zone?.type !== 'zone' || !zone.parentId) return []
330
+ const plates = roomPlates(nodes, zone)
331
+ if (!plates.length) return []
332
+ const byId = new Map(plates.map((plate) => [plate.id as string, plate]))
333
+ const clear = roomClearPolygon(nodes, zoneId)
334
+ const plateAt = (point: [number, number]) =>
335
+ plates.find((plate) => containsPoint([footprint(plate)], point)) ?? plates[0]!
336
+ return Object.values(nodes).flatMap((node) => {
337
+ if (!(FLOOR_CONTENT.has(node.type) && 'position' in node)) return []
338
+ const { parentId, position, supportSlabId } = node as Positioned
339
+ const point: [number, number] = [position[0], position[2]]
340
+ let plate = parentId ? byId.get(parentId) : undefined
341
+ if (!plate && parentId === zone.parentId) {
342
+ if (supportSlabId) plate = byId.get(supportSlabId)
343
+ else if (Math.abs(position[1]) <= RESTING && clear && containsPoint([clear], point))
344
+ plate = plateAt(point)
345
+ }
346
+ return plate ? [{ id: node.id, elevation: plate.elevation }] : []
347
+ })
348
+ }
349
+
350
+ let memo: { nodes: StructureNodes; states: Map<string, RoomConstructionState | null> } | null = null
351
+
352
+ /**
353
+ * Floor / Walls / Ceiling presence for a room, and the actions each row
354
+ * offers, derived together from intent and the reconciled graph. Pure;
355
+ * memoized per `nodes` identity and zone id.
356
+ *
357
+ * - Floor: Absent when `hasFloor === false` (unless a drawn slab still floors
358
+ * the room: the switch cannot take that away); otherwise the share of the zone
359
+ * footprint covered by plates listing the zone plus manual slabs (stair and
360
+ * elevator holes count as floor) — ≥ 98 % Present, ≤ 2 % Absent, else Partial.
361
+ * - Walls: every boundary span a wall → Present, none → Absent, a mix → Partial.
362
+ * - Ceiling: Present with the room's own (derived) ceiling; Partial when only a
363
+ * hand-drawn ceiling covers the room (Use existing / Replace / Remove);
364
+ * otherwise Absent.
365
+ */
366
+ export function roomConstructionState(
367
+ nodes: StructureNodes,
368
+ zoneId: string,
369
+ ): RoomConstructionState | null {
370
+ if (memo?.nodes !== nodes) memo = { nodes, states: new Map() }
371
+ if (memo.states.has(zoneId)) return memo.states.get(zoneId)!
372
+ const zone = nodes[zoneId]
373
+ const state =
374
+ zone?.type === 'zone' && zone.spaceRole === 'room' && zone.parentId
375
+ ? {
376
+ zoneId,
377
+ name: zone.name,
378
+ floor: floorState(nodes, zone),
379
+ walls: wallsState(nodes, zone),
380
+ ceiling: ceilingState(nodes, zone),
381
+ }
382
+ : null
383
+ memo.states.set(zoneId, state)
384
+ return state
385
+ }