@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,395 @@
1
+ import {
2
+ type AnyNode,
3
+ type AnyNodeId,
4
+ acquireSceneHistoryPause,
5
+ applyStructureReconciliation,
6
+ area,
7
+ DEFAULT_FOUNDATION_HEIGHT,
8
+ type FloorFoundationPatch,
9
+ generateId,
10
+ getRoomBaseElevation,
11
+ groundFloorConstruction,
12
+ intersection,
13
+ roomDrawnFloor,
14
+ type SlabNode,
15
+ setFloorFoundation,
16
+ setRoomFloorConstruction,
17
+ upperFloorHeightControl,
18
+ useScene,
19
+ } from '@pascal-app/core'
20
+ import { useViewer } from '@pascal-app/viewer'
21
+ import useEditor from '../store/use-editor'
22
+ import { applyRoomPlan } from './room-structure-commands'
23
+ import { createSessionWrites } from './session-writes'
24
+
25
+ // "Floor & foundation": each connected footprint of a level (the house, a
26
+ // garden shed…) stands on one base plate. On the ground it is built bottom up:
27
+ // the foundation (none, or a height) on the ground, the slab (its thickness)
28
+ // on the foundation; the floor top follows from the two. These helpers name a
29
+ // footprint and read or write those settings in the words the panel uses.
30
+
31
+ export type FootprintPreset = 'ground' | 'raised'
32
+
33
+ /** A footprint raised on a foundation starts with one this high. */
34
+ export { DEFAULT_FOUNDATION_HEIGHT }
35
+
36
+ /** Foundations this low count as on the ground. */
37
+ const ON_GROUND = 0.0005
38
+
39
+ const isBase = (node: AnyNode | undefined): node is SlabNode =>
40
+ node?.type === 'slab' && node.plateRole === 'base'
41
+
42
+ const footprintArea = (plate: SlabNode) => area([{ outer: plate.polygon, holes: plate.holes }])
43
+
44
+ /** The level's footprints, largest first (ties by id, so the order is stable). */
45
+ export function levelFootprints(nodes: Record<string, AnyNode>, levelId: string): SlabNode[] {
46
+ return Object.values(nodes)
47
+ .filter((node): node is SlabNode => isBase(node) && node.parentId === levelId)
48
+ .sort((a, b) => footprintArea(b) - footprintArea(a) || a.id.localeCompare(b.id))
49
+ }
50
+
51
+ /**
52
+ * The drawn slab a room's floor is: the one it visibly stands on (a legacy
53
+ * floor can show a hand-drawn slab over the one it was taken from), else the
54
+ * one it was taken from, while it still stands on the room's level.
55
+ */
56
+ function authoredFloorOwner(nodes: Record<string, AnyNode>, zoneId: string): SlabNode | null {
57
+ const zone = nodes[zoneId]
58
+ if (zone?.type !== 'zone' || !zone.parentId) return null
59
+ const drawn = roomDrawnFloor(nodes, zoneId)
60
+ if (drawn) return nodes[drawn.slabId] as SlabNode
61
+ const source = zone.floor?.sourceSlabId ? nodes[zone.floor.sourceSlabId] : undefined
62
+ return source?.type === 'slab' && source.parentId === zone.parentId ? source : null
63
+ }
64
+
65
+ /**
66
+ * What a room's floor is built as — the same owner core's
67
+ * `setRoomFloorConstruction` edits: the drawn slab the floor came from, else
68
+ * the footprint plate listing the room. Null when the room has none.
69
+ */
70
+ export function roomFloorOwner(nodes: Record<string, AnyNode>, zoneId: string): SlabNode | null {
71
+ const zone = nodes[zoneId]
72
+ if (zone?.type !== 'zone' || !zone.parentId) return null
73
+ return (
74
+ authoredFloorOwner(nodes, zoneId) ??
75
+ levelFootprints(nodes, zone.parentId).find((plate) => plate.zoneIds?.includes(zoneId)) ??
76
+ null
77
+ )
78
+ }
79
+
80
+ /**
81
+ * The footprint a room stands on: the one listing it, else the one it
82
+ * overlaps most — unless its floor is a drawn slab, which is its own and not
83
+ * a footprint's (a plate that only brushes it is not where its floor is).
84
+ */
85
+ export function roomFootprint(nodes: Record<string, AnyNode>, zoneId: string): SlabNode | null {
86
+ const zone = nodes[zoneId]
87
+ if (zone?.type !== 'zone' || !zone.parentId) return null
88
+ const plates = levelFootprints(nodes, zone.parentId)
89
+ const listed = plates.find((plate) => plate.zoneIds?.includes(zoneId))
90
+ if (listed) return listed
91
+ if (authoredFloorOwner(nodes, zoneId)) return null
92
+ let best: SlabNode | null = null
93
+ let bestOverlap = 0
94
+ for (const plate of plates) {
95
+ const overlap = area(
96
+ intersection(
97
+ { outer: plate.polygon, holes: plate.holes },
98
+ { outer: zone.polygon, holes: zone.holes },
99
+ ),
100
+ )
101
+ if (overlap > bestOverlap) {
102
+ best = plate
103
+ bestOverlap = overlap
104
+ }
105
+ }
106
+ return best
107
+ }
108
+
109
+ /**
110
+ * The floor top an upper footprint rests at by default: mirrors core's
111
+ * `automaticFloorHeight` — its rooms' base heights read with the base plates
112
+ * set aside.
113
+ */
114
+ export function groundFloorHeight(nodes: Record<string, AnyNode>, plate: SlabNode): number {
115
+ const withoutBase = Object.fromEntries(Object.entries(nodes).filter(([, node]) => !isBase(node)))
116
+ const heights = (plate.zoneIds ?? []).map((id) => getRoomBaseElevation(withoutBase, id))
117
+ return heights.length ? Math.max(...heights) : 0.05
118
+ }
119
+
120
+ /**
121
+ * How far an upper floor's top is raised above its default position (resting
122
+ * on the walls below), meters; negative when set lower than its default.
123
+ */
124
+ export function floorLift(nodes: Record<string, AnyNode>, plate: SlabNode): number {
125
+ return plate.elevation - groundFloorHeight(nodes, plate)
126
+ }
127
+
128
+ /**
129
+ * A ground footprint's foundation height, meters: 0 on the ground. Derived
130
+ * from the floor top, so a legacy slab reaching below the ground reads 0.
131
+ */
132
+ export function foundationHeight(nodes: Record<string, AnyNode>, plate: SlabNode): number {
133
+ return groundFloorConstruction(nodes, plate).foundationHeight
134
+ }
135
+
136
+ /** A ground footprint's floor top above the ground: foundation + slab. Read-only. */
137
+ export function floorTopAboveGround(nodes: Record<string, AnyNode>, plate: SlabNode): number {
138
+ const { top, grade } = groundFloorConstruction(nodes, plate)
139
+ return Math.round((top - grade) * 1e6) / 1e6
140
+ }
141
+
142
+ /**
143
+ * The one height a footprint's handle and panel field edit. On the ground it
144
+ * is the foundation height (the slab stays on it; the top follows); an upper
145
+ * floor rests on the walls below, so its height is its floor top, and raising
146
+ * it thickens the plate — the underside stays, the storeys above ride up.
147
+ */
148
+ export function footprintHeightValue(nodes: Record<string, AnyNode>, plate: SlabNode): number {
149
+ return upperFloorHeightControl(nodes, plate)?.currentTop ?? foundationHeight(nodes, plate)
150
+ }
151
+
152
+ /**
153
+ * The lowest height the handle and field go to: on the ground (no
154
+ * foundation); upstairs, resting on the walls below. An upper floor already
155
+ * lower starts from where it is, so the first drag never jumps.
156
+ */
157
+ export function footprintHeightMinimum(nodes: Record<string, AnyNode>, plate: SlabNode): number {
158
+ const upper = upperFloorHeightControl(nodes, plate)
159
+ return upper ? Math.min(upper.currentTop, upper.minimumTop) : 0
160
+ }
161
+
162
+ /** The patch a footprint height writes: upstairs the plate's thickness, on the ground its foundation. */
163
+ export function footprintHeightPatch(
164
+ nodes: Record<string, AnyNode>,
165
+ plate: SlabNode,
166
+ value: number,
167
+ ): FloorFoundationPatch {
168
+ const upper = upperFloorHeightControl(nodes, plate, value)
169
+ return upper
170
+ ? { thickness: upper.write.thickness }
171
+ : { foundationHeight: Math.max(0, Math.round(value * 1e6) / 1e6) }
172
+ }
173
+
174
+ export const THICK_FLOOR_HINT = 'Very thick floor — a mezzanine may fit better.'
175
+
176
+ /** A plate thick enough that a mezzanine would likely serve better (core's advice; never a refusal). */
177
+ export function thickFloorAdvice(nodes: Record<string, AnyNode>, plate: SlabNode, top?: number) {
178
+ return upperFloorHeightControl(nodes, plate, top)?.advice === 'thick-floor'
179
+ }
180
+
181
+ /** On the ground or raised on a foundation: raised exactly when the foundation has a height. */
182
+ export function footprintPreset(nodes: Record<string, AnyNode>, plate: SlabNode): FootprintPreset {
183
+ return foundationHeight(nodes, plate) > ON_GROUND ? 'raised' : 'ground'
184
+ }
185
+
186
+ /**
187
+ * The patch a preset choice writes: only the foundation height. Core's
188
+ * `setFloorFoundation` makes the foundation solid while it has a height. Raising
189
+ * keeps a height already set.
190
+ */
191
+ export function presetPatch(
192
+ nodes: Record<string, AnyNode>,
193
+ plate: SlabNode,
194
+ preset: FootprintPreset,
195
+ ): FloorFoundationPatch {
196
+ if (preset === 'ground') return { foundationHeight: 0 }
197
+ const current = foundationHeight(nodes, plate)
198
+ return { foundationHeight: current > ON_GROUND ? current : DEFAULT_FOUNDATION_HEIGHT }
199
+ }
200
+
201
+ /**
202
+ * Writes Floor & foundation settings through core's `setFloorFoundation`: the
203
+ * footprint's floor, walls, openings and room floors move together, in one
204
+ * undo step. Returns why it was refused, or null.
205
+ */
206
+ export function applyFloorFoundation(
207
+ slabId: string | readonly string[],
208
+ patch: FloorFoundationPatch,
209
+ ): string | null {
210
+ const nodes = useScene.getState().nodes
211
+ const plan =
212
+ typeof slabId === 'string'
213
+ ? setFloorFoundation(nodes, { slabId, patch })
214
+ : setFloorFoundation(nodes, { slabIds: [...slabId], patch })
215
+ if (plan.conflicts?.length) return plan.conflicts.map((conflict) => conflict.message).join(' ')
216
+ applyRoomPlan(plan)
217
+ return null
218
+ }
219
+
220
+ /**
221
+ * Edits a room's floor construction through core's `setRoomFloorConstruction`
222
+ * — the one command the room panel and MCP share. It picks the room's floor
223
+ * owner (a drawn slab, or the footprint plate listing the room). A numeric
224
+ * floorHeight moves a drawn slab's top; only a ground footprint has a
225
+ * foundation. Returns why a change was refused, or null.
226
+ */
227
+ export function applyRoomFloorConstruction(
228
+ zoneId: string,
229
+ patch: FloorFoundationPatch,
230
+ slabId?: string,
231
+ ): string | null {
232
+ const plan = setRoomFloorConstruction(useScene.getState().nodes, { zoneId, slabId, patch })
233
+ if (plan.conflicts?.length) return plan.conflicts.map((conflict) => conflict.message).join(' ')
234
+ applyRoomPlan(plan)
235
+ return null
236
+ }
237
+
238
+ const SHARED_STOREY = 'floor-foundation-shared-storey'
239
+
240
+ /**
241
+ * When a change to one footprint is refused only because an upper floor (or a
242
+ * roof, a ceiling) also stands on other footprints, the footprints that would
243
+ * have to move with it — this one first, so the patch stays its own — if
244
+ * moving them together is accepted. Otherwise null.
245
+ */
246
+ export function footprintsToMoveTogether(
247
+ nodes: Record<string, AnyNode>,
248
+ slabId: string,
249
+ patch: FloorFoundationPatch,
250
+ ): string[] | null {
251
+ const conflicts = setFloorFoundation(nodes, { slabId, patch }).conflicts ?? []
252
+ if (conflicts.length === 0 || conflicts.some((conflict) => conflict.code !== SHARED_STOREY))
253
+ return null
254
+ const ids = new Set([slabId])
255
+ for (const conflict of conflicts)
256
+ for (const id of conflict.nodeIds) if (isBase(nodes[id])) ids.add(id)
257
+ if (ids.size < 2) return null
258
+ const together = setFloorFoundation(nodes, { slabIds: [...ids], patch })
259
+ return together.conflicts?.length ? null : [...ids]
260
+ }
261
+
262
+ /** Sets a footprint's height (`footprintHeightValue`'s terms). */
263
+ export function setFootprintHeight(slabId: string, value: number): string | null {
264
+ const plate = useScene.getState().nodes[slabId as AnyNodeId]
265
+ if (!isBase(plate)) return 'The floor is gone.'
266
+ return applyFloorFoundation(slabId, footprintHeightPatch(useScene.getState().nodes, plate, value))
267
+ }
268
+
269
+ type NodeMap = Record<string, AnyNode>
270
+
271
+ /** Every level of the building a level belongs to (just that level when it has none). */
272
+ function buildingLevelIds(nodes: NodeMap, levelId: string | null | undefined): string[] {
273
+ const level = levelId ? nodes[levelId] : undefined
274
+ if (level?.type !== 'level') return []
275
+ const building = level.parentId ? nodes[level.parentId] : undefined
276
+ if (building?.type !== 'building') return [level.id]
277
+ return building.children.filter((id) => nodes[id]?.type === 'level')
278
+ }
279
+
280
+ /**
281
+ * A live height preview (a handle drag, a scrub): each value moves the
282
+ * building outside history; `commit` takes the preview's writes back and
283
+ * writes the final height once — one undo step and one shared change whose
284
+ * "before" is what everyone else has. (Writing the start height back through
285
+ * the command instead is not exact: a foundation finish picked up on the way
286
+ * would stay, and collaborators would refuse the change.)
287
+ */
288
+ export function beginFootprintHeightPreview(slabId: string) {
289
+ return beginFootprintPreview(slabId, setFootprintHeight, footprintHeightValue)
290
+ }
291
+
292
+ /** Sets a ground footprint's slab thickness: the top and everything on it follow. */
293
+ export function setFootprintThickness(slabId: string, value: number): string | null {
294
+ return applyFloorFoundation(slabId, { thickness: value })
295
+ }
296
+
297
+ /** A live slab-thickness preview (a scrub): the same session as the height's, one undo step. */
298
+ export function beginFootprintThicknessPreview(slabId: string) {
299
+ return beginFootprintPreview(slabId, setFootprintThickness, (_, plate) => plate.thickness)
300
+ }
301
+
302
+ function beginFootprintPreview(
303
+ slabId: string,
304
+ write: (slabId: string, value: number) => string | null,
305
+ read: (nodes: Record<string, AnyNode>, plate: SlabNode) => number,
306
+ ) {
307
+ let writes = createSessionWrites()
308
+ let release: (() => void) | null = null
309
+ const end = () => {
310
+ if (!release) return false
311
+ release()
312
+ release = null
313
+ writes.revert()
314
+ writes = createSessionWrites()
315
+ return true
316
+ }
317
+ return {
318
+ preview(value: number): string | null {
319
+ release ??= acquireSceneHistoryPause(useScene)
320
+ return writes.record(() => {
321
+ const before = useScene.getState().nodes
322
+ const refused = write(slabId, value)
323
+ // Paused writes skip the structure reconciliation a commit runs (the
324
+ // plates' derived heights, the rooms on them); run it here so the
325
+ // building follows the preview.
326
+ // An upper floor's storeys above ride up with it: reconcile the building.
327
+ const levelIds = buildingLevelIds(before, before[slabId as AnyNodeId]?.parentId)
328
+ if (!refused && levelIds.length)
329
+ applyStructureReconciliation(useScene, {
330
+ levelIds,
331
+ previousNodes: before,
332
+ mintId: generateId,
333
+ })
334
+ return refused
335
+ })
336
+ },
337
+ commit(value: number): string | null {
338
+ end()
339
+ const plate = useScene.getState().nodes[slabId as AnyNodeId]
340
+ if (isBase(plate) && Math.abs(read(useScene.getState().nodes, plate) - value) < 1e-6)
341
+ return null
342
+ return write(slabId, value)
343
+ },
344
+ cancel() {
345
+ end()
346
+ },
347
+ }
348
+ }
349
+
350
+ /** Opens a footprint's Floor & foundation panel: its base plate becomes the selection. */
351
+ export function openFloorFoundation(slabId: string) {
352
+ useEditor.getState().clearRoom()
353
+ useViewer.getState().setSelection({ selectedIds: [slabId as SlabNode['id']] })
354
+ }
355
+
356
+ /** A base plate's own outside faces: what a click opens Floor & foundation for. */
357
+ const FOOTPRINT_ROLES = new Set(['edge', 'foundation', 'side', 'riser', 'underside', 'surface'])
358
+
359
+ /**
360
+ * Whether a pointer hit is on a footprint's floor edge band or foundation (not
361
+ * a room's floor, step or edge): a click there opens that footprint's Floor &
362
+ * foundation panel rather than the room above it.
363
+ */
364
+ export function isFootprintConstructionHit(
365
+ node: AnyNode | undefined,
366
+ object: { userData?: Record<string, unknown> } | null | undefined,
367
+ ): boolean {
368
+ if (!isBase(node)) return false
369
+ const role = object?.userData?.paintRole
370
+ return typeof role === 'string' && FOOTPRINT_ROLES.has(role)
371
+ }
372
+
373
+ /**
374
+ * Where a click on a room-owned plate lands once it is past the room (the
375
+ * drill click, or Alt): a raised or sunken room's plate is that room's floor,
376
+ * so the drill continues to the footprint floor the room stands on — where a
377
+ * ground-level room's drill lands too. A mezzanine's deck has none (null): it
378
+ * stays on its mezzanine.
379
+ */
380
+ export function roomOwnedPlateDrillTarget(
381
+ nodes: Record<string, AnyNode>,
382
+ plate: SlabNode,
383
+ ): SlabNode | null {
384
+ if (plate.support === 'open') return null
385
+ const zoneId = plate.zoneIds?.[0]
386
+ return zoneId ? roomFootprint(nodes, zoneId) : null
387
+ }
388
+
389
+ /** A raised or sunken room's plate, or a mezzanine's: its room is what gets picked. */
390
+ export function isRoomOwnedPlate(node: AnyNode | undefined): boolean {
391
+ return (
392
+ node?.type === 'slab' &&
393
+ (node.plateRole === 'platform' || node.plateRole === 'sunken' || node.support === 'open')
394
+ )
395
+ }
@@ -0,0 +1,283 @@
1
+ import { afterEach, beforeEach, describe, expect, test } from 'bun:test'
2
+ import {
3
+ type AnyNode,
4
+ type AnyNodeId,
5
+ BuildingNode,
6
+ createZone,
7
+ floorFootprintSupportClass,
8
+ generateId,
9
+ getCeilingClampBound,
10
+ getLevelElevations,
11
+ initSpaceDetectionSync,
12
+ LevelNode,
13
+ type SlabNode,
14
+ upperFloorHeightControl,
15
+ useScene,
16
+ } from '@pascal-app/core'
17
+ import { createElement } from 'react'
18
+ import { renderToStaticMarkup } from 'react-dom/server'
19
+ import {
20
+ FloorSection,
21
+ floorFoundationModel,
22
+ floorPlateKicker,
23
+ } from '../components/ui/panels/floor-foundation-panel'
24
+ import { installImmediateAnimationFrames } from '../test-utils/immediate-animation-frames'
25
+ import {
26
+ applyFloorFoundation,
27
+ beginFootprintHeightPreview,
28
+ beginFootprintThicknessPreview,
29
+ footprintHeightMinimum,
30
+ footprintHeightPatch,
31
+ footprintHeightValue,
32
+ levelFootprints,
33
+ presetPatch,
34
+ setFootprintHeight,
35
+ thickFloorAdvice,
36
+ } from './floor-footprints'
37
+ import { applyRoomPlan } from './room-structure-commands'
38
+
39
+ const GROUND = 'level_support_ground'
40
+ const UPPER = 'level_support_upper'
41
+ let stop = () => {}
42
+ let restoreFrames = () => {}
43
+ const nodes = () => useScene.getState().nodes as Record<string, AnyNode>
44
+
45
+ const rect = (x0: number, z0: number, x1: number, z1: number): [number, number][] => [
46
+ [x0, z0],
47
+ [x1, z0],
48
+ [x1, z1],
49
+ [x0, z1],
50
+ ]
51
+ const room = (levelId: string, polygon: [number, number][], name: string) =>
52
+ applyRoomPlan(createZone(nodes(), { levelId, polygon, enclose: true, mintId: generateId, name }))
53
+
54
+ beforeEach(() => {
55
+ restoreFrames = installImmediateAnimationFrames()
56
+ const building = BuildingNode.parse({ id: 'building_support', children: [GROUND, UPPER] })
57
+ const ground = LevelNode.parse({
58
+ id: GROUND,
59
+ parentId: building.id,
60
+ level: 0,
61
+ name: 'Ground floor',
62
+ })
63
+ const upper = LevelNode.parse({ id: UPPER, parentId: building.id, level: 1, name: 'Floor 1' })
64
+ useScene.setState({
65
+ nodes: { [building.id]: building, [ground.id]: ground, [upper.id]: upper },
66
+ rootNodeIds: [building.id],
67
+ dirtyNodes: new Set(),
68
+ materials: {},
69
+ collections: {},
70
+ readOnly: false,
71
+ })
72
+ useScene.temporal.getState().resume()
73
+ stop = initSpaceDetectionSync(useScene, { getState: () => ({ spaces: {}, setSpaces: () => {} }) })
74
+ room(GROUND, rect(0, 0, 8, 5), 'Living')
75
+ room(UPPER, rect(0, 0, 8, 5), 'Bedroom')
76
+ // A wing upstairs with nothing under it.
77
+ room(UPPER, rect(20, 0, 24, 4), 'Studio')
78
+ })
79
+ afterEach(() => {
80
+ stop()
81
+ restoreFrames()
82
+ })
83
+
84
+ const plateOf = (levelId: string, name: string) =>
85
+ levelFootprints(nodes(), levelId).find((plate) =>
86
+ plate.zoneIds?.some((id) => nodes()[id]?.name === name),
87
+ ) as SlabNode
88
+
89
+ describe('Floor & foundation support class (point 5)', () => {
90
+ test('a floor over the storey below is supported; the ground and a free wing bear on the ground', () => {
91
+ const house = plateOf(GROUND, 'Living')
92
+ const bedroom = plateOf(UPPER, 'Bedroom')
93
+ const studio = plateOf(UPPER, 'Studio')
94
+ expect(floorFootprintSupportClass(nodes(), house)).toBe('ground-bearing')
95
+ expect(floorFootprintSupportClass(nodes(), bedroom)).toBe('supported')
96
+ expect(floorFootprintSupportClass(nodes(), studio)).toBe('ground-bearing')
97
+ })
98
+
99
+ test('the kicker says where the plate is and what it rests on', () => {
100
+ expect(floorPlateKicker(nodes(), plateOf(GROUND, 'Living'))).toBe('Floor plate · Ground floor')
101
+ expect(floorPlateKicker(nodes(), plateOf(UPPER, 'Bedroom'))).toBe(
102
+ 'Floor plate · Floor 1 · rests on Ground floor',
103
+ )
104
+ })
105
+
106
+ test('the height handle drags down to the ground, or upstairs to resting on the walls below', () => {
107
+ const house = plateOf(GROUND, 'Living')
108
+ const bedroom = plateOf(UPPER, 'Bedroom')
109
+ expect(footprintHeightValue(nodes(), house)).toBe(0)
110
+ expect(footprintHeightMinimum(nodes(), house)).toBe(0)
111
+ const control = upperFloorHeightControl(nodes(), bedroom)!
112
+ expect(footprintHeightValue(nodes(), bedroom)).toBeCloseTo(control.currentTop)
113
+ expect(footprintHeightMinimum(nodes(), bedroom)).toBeCloseTo(control.minimumTop)
114
+ // A legacy plate hanging below its resting position starts where it is: no jump.
115
+ const legacy = { ...bedroom, elevation: bedroom.elevation - 0.1, thickness: 0.3 }
116
+ const legacyNodes = { ...nodes(), [legacy.id]: legacy }
117
+ expect(footprintHeightMinimum(legacyNodes, legacy)).toBeCloseTo(
118
+ footprintHeightValue(legacyNodes, legacy),
119
+ )
120
+ })
121
+
122
+ test('raising an upper floor thickens it: the underside stays, the storey above rides up', () => {
123
+ const floor2 = LevelNode.parse({
124
+ id: 'level_support_2',
125
+ parentId: 'building_support',
126
+ level: 2,
127
+ })
128
+ const building = nodes().building_support as BuildingNode
129
+ useScene.getState().createNode(floor2, building.id)
130
+ room(floor2.id, rect(0, 0, 8, 5), 'Attic')
131
+ const bedroom = plateOf(UPPER, 'Bedroom')
132
+ const underside = bedroom.elevation - bedroom.thickness
133
+ const groundCeiling = getCeilingClampBound(GROUND as AnyNodeId, nodes(), rect(0, 0, 8, 5))
134
+ const atticBefore = getLevelElevations(nodes()).get(floor2.id)!.baseY
135
+ const top = footprintHeightValue(nodes(), bedroom) + 0.25
136
+ expect(footprintHeightPatch(nodes(), bedroom, top)).toEqual({
137
+ thickness: expect.closeTo(bedroom.thickness + 0.25, 6),
138
+ })
139
+ expect(setFootprintHeight(bedroom.id, top)).toBeNull()
140
+ const raised = nodes()[bedroom.id] as SlabNode
141
+ expect(raised.elevation - raised.thickness).toBeCloseTo(underside)
142
+ expect(raised.elevation).toBeCloseTo(bedroom.elevation + 0.25)
143
+ expect(getCeilingClampBound(GROUND as AnyNodeId, nodes(), rect(0, 0, 8, 5))).toBeCloseTo(
144
+ groundCeiling,
145
+ )
146
+ expect(getLevelElevations(nodes()).get(floor2.id)!.baseY).toBeCloseTo(atticBefore + 0.25)
147
+ })
148
+
149
+ test('a very thick upper floor advises a mezzanine, never refuses', () => {
150
+ const bedroom = plateOf(UPPER, 'Bedroom')
151
+ const underside = bedroom.elevation - bedroom.thickness
152
+ expect(thickFloorAdvice(nodes(), bedroom, underside + 0.35)).toBe(false)
153
+ expect(thickFloorAdvice(nodes(), bedroom, underside + 0.45)).toBe(true)
154
+ expect(setFootprintHeight(bedroom.id, underside + 0.45)).toBeNull()
155
+ expect(floorFoundationModel(nodes(), nodes()[bedroom.id] as SlabNode).thickFloor).toBe(true)
156
+ })
157
+ })
158
+
159
+ describe('Upper-floor height drag release (round-final item 6)', () => {
160
+ // The handle and the panel scrub both preview live and land once on release:
161
+ // every release, small or thick, must write the value as one undo step.
162
+ for (const delta of [0.01, 0.05, 0.2, 0.5, -0.02]) {
163
+ test(`a drag of ${delta} m lands on release`, () => {
164
+ const bedroom = plateOf(UPPER, 'Bedroom')
165
+ const start = footprintHeightValue(nodes(), bedroom)
166
+ const past = useScene.temporal.getState().pastStates.length
167
+ const preview = beginFootprintHeightPreview(bedroom.id)
168
+ expect(preview.preview(start + delta / 2)).toBeNull()
169
+ expect(preview.preview(start + delta)).toBeNull()
170
+ expect(preview.commit(start + delta)).toBeNull()
171
+ const after = nodes()[bedroom.id] as SlabNode
172
+ expect(after.thickness).toBeCloseTo(bedroom.thickness + delta, 6)
173
+ expect(footprintHeightValue(nodes(), after)).toBeCloseTo(start + delta, 6)
174
+ expect(useScene.temporal.getState().pastStates.length).toBe(past + 1)
175
+ })
176
+ }
177
+ })
178
+
179
+ describe('Floor & foundation panel (point 5)', () => {
180
+ test('a ground floor gets the foundation group; an upper floor never offers the ground', () => {
181
+ const ground = floorFoundationModel(nodes(), plateOf(GROUND, 'Living'))
182
+ expect(ground).toMatchObject({ upper: false, preset: 'ground', underFloor: true, minHeight: 0 })
183
+ const upper = floorFoundationModel(nodes(), plateOf(UPPER, 'Bedroom'))
184
+ expect(upper).toMatchObject({ upper: true, underFloor: false, thickFloor: false })
185
+ expect(upper.kicker).toBe('Floor plate · Floor 1 · rests on Ground floor')
186
+ })
187
+
188
+ test('on the ground the two inputs are slab thickness and foundation height; the top follows', () => {
189
+ const living = plateOf(GROUND, 'Living')
190
+ const start = floorFoundationModel(nodes(), living)
191
+ expect(start).toMatchObject({ preset: 'ground', height: 0, minThickness: 0.01 })
192
+ expect(start.floorTop).toBeCloseTo(living.thickness)
193
+ expect(applyFloorFoundation(living.id, { thickness: 0.1 })).toBeNull()
194
+ let plate = nodes()[living.id] as SlabNode
195
+ expect(floorFoundationModel(nodes(), plate)).toMatchObject({
196
+ preset: 'ground',
197
+ height: 0,
198
+ floorTop: expect.closeTo(0.1),
199
+ })
200
+ expect(applyFloorFoundation(living.id, presetPatch(nodes(), plate, 'raised'))).toBeNull()
201
+ plate = nodes()[living.id] as SlabNode
202
+ expect(floorFoundationModel(nodes(), plate)).toMatchObject({
203
+ preset: 'raised',
204
+ height: expect.closeTo(0.3),
205
+ floorTop: expect.closeTo(0.4),
206
+ })
207
+ expect(plate.foundation?.type).toBe('solid')
208
+ expect(applyFloorFoundation(living.id, presetPatch(nodes(), plate, 'ground'))).toBeNull()
209
+ plate = nodes()[living.id] as SlabNode
210
+ expect(floorFoundationModel(nodes(), plate)).toMatchObject({
211
+ preset: 'ground',
212
+ height: 0,
213
+ floorTop: expect.closeTo(0.1),
214
+ })
215
+ expect(plate.foundation?.type).toBe('none')
216
+ })
217
+
218
+ test('a slab thickness scrub moves the building live and lands as one undo step', () => {
219
+ const living = plateOf(GROUND, 'Living')
220
+ useScene.temporal.getState().clear()
221
+ const past = 0
222
+ const scrub = beginFootprintThicknessPreview(living.id)
223
+ expect(scrub.preview(0.2)).toBeNull()
224
+ const live = nodes()[living.id] as SlabNode
225
+ expect(live.thickness).toBeCloseTo(0.2)
226
+ expect(live.elevation).toBeCloseTo(0.2)
227
+ expect(useScene.temporal.getState().pastStates.length).toBe(past)
228
+ expect(scrub.preview(0.12)).toBeNull()
229
+ expect(scrub.commit(0.12)).toBeNull()
230
+ const after = nodes()[living.id] as SlabNode
231
+ expect(after.thickness).toBeCloseTo(0.12)
232
+ expect(after.elevation).toBeCloseTo(0.12)
233
+ expect(useScene.temporal.getState().pastStates.length).toBe(past + 1)
234
+ useScene.temporal.getState().undo()
235
+ expect((nodes()[living.id] as SlabNode).thickness).toBeCloseTo(living.thickness)
236
+ })
237
+
238
+ test('upstairs the one control is the thickness; the floor top above the ground is its caption', () => {
239
+ const bedroom = plateOf(UPPER, 'Bedroom')
240
+ const before = floorFoundationModel(nodes(), bedroom)
241
+ const base = getLevelElevations(nodes()).get(UPPER as AnyNodeId)!.baseY
242
+ expect(before.floorTop).toBeCloseTo(base + before.height)
243
+ expect(setFootprintHeight(bedroom.id, before.height + 0.1)).toBeNull()
244
+ const after = floorFoundationModel(nodes(), nodes()[bedroom.id] as SlabNode)
245
+ expect((nodes()[bedroom.id] as SlabNode).thickness).toBeCloseTo(bedroom.thickness + 0.1)
246
+ expect(after.floorTop).toBeCloseTo(before.floorTop + 0.1)
247
+ })
248
+
249
+ test('the section draws top to bottom in build order, labelled not to scale', () => {
250
+ const section = (props: Partial<Parameters<typeof FloorSection>[0]>) =>
251
+ renderToStaticMarkup(
252
+ createElement(FloorSection, {
253
+ supported: false,
254
+ raised: false,
255
+ thickness: 0.2,
256
+ thicknessLabel: '0.20 m',
257
+ ...props,
258
+ }),
259
+ )
260
+ const raised = section({
261
+ raised: true,
262
+ foundationHeight: 0.5,
263
+ foundationLabel: '0.50 m',
264
+ topLabel: '0.70 m',
265
+ })
266
+ const order = ['Floor top', 'Slab · 0.20 m', 'Foundation · 0.50 m', 'Ground'].map((label) =>
267
+ raised.indexOf(`>${label}<`),
268
+ )
269
+ expect(order.every((at) => at >= 0)).toBe(true)
270
+ expect([...order].sort((a, b) => a - b)).toEqual(order)
271
+ // The floor top is measured from the ground: foundation + slab.
272
+ expect(raised).toContain('>0.70 m<')
273
+ const onGround = section({ topLabel: '0.20 m' })
274
+ expect(onGround).not.toContain('data-section-foundation')
275
+ expect(onGround).toContain('>0.20 m<')
276
+ expect(raised).toContain('Not to scale')
277
+ const upstairs = section({ supported: true, lift: 0.25, liftLabel: '+0.25 m' })
278
+ expect(upstairs).toContain('>Walls below<')
279
+ expect(upstairs).not.toContain('>Foundation<')
280
+ expect(upstairs).not.toContain('>Ground<')
281
+ expect(section({})).not.toContain('Floor edge band')
282
+ })
283
+ })