@pascal-app/editor 1.0.3 → 1.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (440) hide show
  1. package/package.json +11 -9
  2. package/src/components/editor/action-menu-button.tsx +83 -0
  3. package/src/components/editor/bake-thumbnail.tsx +315 -4
  4. package/src/components/editor/capture-level-picker.tsx +72 -0
  5. package/src/components/editor/custom-camera-controls.tsx +202 -57
  6. package/src/components/editor/delete-confirmation-dialog.tsx +297 -7
  7. package/src/components/editor/duplicate-node.ts +244 -0
  8. package/src/components/editor/editor-layout-mobile.tsx +36 -25
  9. package/src/components/editor/editor-layout-v2.tsx +131 -58
  10. package/src/components/editor/export-manager.tsx +34 -8
  11. package/src/components/editor/first-person/build-collider-world.test.ts +51 -0
  12. package/src/components/editor/first-person/build-collider-world.ts +6 -2
  13. package/src/components/editor/first-person/mechanism-targets.test.ts +75 -0
  14. package/src/components/editor/first-person/mechanism-targets.ts +36 -0
  15. package/src/components/editor/first-person-controls.tsx +205 -2
  16. package/src/components/editor/floating-action-menu.tsx +63 -207
  17. package/src/components/editor/floor-aware-camera-controls.test.ts +442 -0
  18. package/src/components/editor/floor-aware-camera-controls.ts +324 -0
  19. package/src/components/editor/floor-openings-overlay.tsx +243 -0
  20. package/src/components/editor/floor-region-controls.tsx +846 -0
  21. package/src/components/editor/floor-region-webgpu-materials.test.ts +12 -0
  22. package/src/components/editor/floor-surface.test.ts +165 -0
  23. package/src/components/editor/floor-surface.ts +182 -0
  24. package/src/components/editor/floorplan-background-selection.test.ts +11 -2
  25. package/src/components/editor/floorplan-background-selection.ts +1 -3
  26. package/src/components/editor/floorplan-panel.tsx +344 -342
  27. package/src/components/editor/footprint-height-handle.tsx +204 -0
  28. package/src/components/editor/group-actions.ts +54 -2
  29. package/src/components/editor/handles/handle-hit-priority.test.tsx +63 -0
  30. package/src/components/editor/handles/handle-hit-priority.tsx +37 -0
  31. package/src/components/editor/handles/swallow-next-click.test.ts +28 -0
  32. package/src/components/editor/handles/use-handle-drag.ts +269 -197
  33. package/src/components/editor/index.tsx +294 -169
  34. package/src/components/editor/join-walls-pill.tsx +44 -0
  35. package/src/components/editor/node-action-menu.tsx +68 -70
  36. package/src/components/editor/node-arrow-handles.tsx +25 -6
  37. package/src/components/editor/open-wall-ends-3d-layer.tsx +273 -0
  38. package/src/components/editor/open-wall-ends-hint.tsx +89 -0
  39. package/src/components/editor/quick-measurement-hud.tsx +3 -2
  40. package/src/components/editor/registry-action-contributions.test.tsx +63 -0
  41. package/src/components/editor/registry-action-contributions.tsx +80 -1
  42. package/src/components/editor/room-controls.tsx +847 -0
  43. package/src/components/editor/room-floor-highlight.tsx +128 -0
  44. package/src/components/editor/room-handles-2d.tsx +354 -0
  45. package/src/components/editor/room-handles.tsx +675 -0
  46. package/src/components/editor/room-highlight.tsx +225 -0
  47. package/src/components/editor/room-labels.test.ts +357 -0
  48. package/src/components/editor/room-labels.tsx +990 -0
  49. package/src/components/editor/room-transform-ghost.tsx +256 -0
  50. package/src/components/editor/room-webgpu-materials.test.ts +27 -0
  51. package/src/components/editor/selection-manager.tsx +451 -317
  52. package/src/components/editor/slab-hole-highlights.tsx +3 -0
  53. package/src/components/editor/snapshot-capture-overlay.tsx +4 -0
  54. package/src/components/editor/snapshot-capture.test.ts +52 -4
  55. package/src/components/editor/snapshot-capture.ts +30 -5
  56. package/src/components/editor/thumbnail-generator.test.ts +308 -0
  57. package/src/components/editor/thumbnail-generator.tsx +1035 -229
  58. package/src/components/editor/use-floorplan-background-placement.ts +31 -11
  59. package/src/components/editor/vector-edge-extractor.tsx +121 -0
  60. package/src/components/editor/views/host-view.tsx +61 -0
  61. package/src/components/editor/views/plugins-view.tsx +670 -0
  62. package/src/components/editor/views/scene-region.tsx +59 -0
  63. package/src/components/editor/views/use-editor-views.ts +57 -0
  64. package/src/components/editor/views/view-bar.tsx +220 -0
  65. package/src/components/editor/wall-justification.test.ts +140 -0
  66. package/src/components/editor/wall-move-side-handles.tsx +43 -142
  67. package/src/components/editor/wall-push-arrow.tsx +92 -0
  68. package/src/components/editor/wall-region-handles.tsx +509 -0
  69. package/src/components/editor/wall-snap-beacon-layer.tsx +3 -0
  70. package/src/components/editor-2d/floorplan-cursor-indicator-overlay.tsx +8 -4
  71. package/src/components/editor-2d/floorplan-open-wall-ends-layer.test.tsx +86 -0
  72. package/src/components/editor-2d/floorplan-open-wall-ends-layer.tsx +290 -0
  73. package/src/components/editor-2d/floorplan-registry-action-menu.tsx +11 -1
  74. package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +113 -102
  75. package/src/components/editor-2d/renderers/floorplan-dimension-edit-overlay.test.ts +156 -0
  76. package/src/components/editor-2d/renderers/floorplan-dimension-edit-overlay.tsx +278 -0
  77. package/src/components/editor-2d/renderers/floorplan-dimension-renderer.tsx +77 -0
  78. package/src/components/editor-2d/renderers/{floorplan-draft-wall-measurement.tsx → floorplan-draft-measurement.tsx} +8 -8
  79. package/src/components/editor-2d/renderers/floorplan-geometry-renderer.tsx +4 -1
  80. package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +38 -8
  81. package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +291 -227
  82. package/src/components/editor-2d/svg-paths.ts +12 -15
  83. package/src/components/procedural-items/inspector.tsx +40 -15
  84. package/src/components/systems/ceiling/ceiling-selection-affordance-system.test.ts +97 -14
  85. package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +18 -18
  86. package/src/components/systems/zone/zone-system.test.ts +47 -0
  87. package/src/components/systems/zone/zone-system.tsx +29 -68
  88. package/src/components/tools/item/placement-history.test.tsx +474 -0
  89. package/src/components/tools/item/placement-strategies.ts +8 -4
  90. package/src/components/tools/item/use-draft-node.test.tsx +432 -1
  91. package/src/components/tools/item/use-draft-node.ts +166 -95
  92. package/src/components/tools/item/use-placement-coordinator.tsx +93 -46
  93. package/src/components/tools/paint-region/use-paint-tint.ts +37 -0
  94. package/src/components/tools/paint-region/wall-paint-region-tool.tsx +466 -0
  95. package/src/components/tools/registry/move-registry-node-tool.tsx +3 -1
  96. package/src/components/tools/select/box-select-tool.tsx +2 -12
  97. package/src/components/tools/select/plane-box-select-tool.test.tsx +79 -0
  98. package/src/components/tools/select/plane-box-select-tool.tsx +35 -22
  99. package/src/components/tools/select/select-candidates.test.ts +37 -4
  100. package/src/components/tools/select/select-candidates.ts +15 -11
  101. package/src/components/tools/shared/cursor-sphere.tsx +36 -12
  102. package/src/components/tools/site/site-boundary-editor.tsx +1 -2
  103. package/src/components/tools/stair/stair-defaults.ts +0 -3
  104. package/src/components/tools/stair/stair-tool.tsx +90 -42
  105. package/src/components/tools/tool-manager.tsx +46 -20
  106. package/src/components/tools/wall/wall-drafting.test.ts +19 -4
  107. package/src/components/tools/zone/zone-tool.tsx +4 -12
  108. package/src/components/ui/action-menu/action-button.tsx +3 -0
  109. package/src/components/ui/action-menu/camera-actions.tsx +2 -1
  110. package/src/components/ui/action-menu/control-modes.tsx +2 -35
  111. package/src/components/ui/action-menu/measurement-control.tsx +3 -10
  112. package/src/components/ui/build-panel/build-panel.tsx +235 -0
  113. package/src/components/ui/command-palette/editor-commands.tsx +4 -30
  114. package/src/components/ui/command-palette/index.tsx +5 -24
  115. package/src/components/ui/controls/finish-swatch.tsx +110 -0
  116. package/src/components/ui/controls/material-paint-panel.tsx +97 -26
  117. package/src/components/ui/controls/material-picker.tsx +4 -1
  118. package/src/components/ui/controls/scene-material-list.tsx +4 -32
  119. package/src/components/ui/controls/select-control.tsx +65 -0
  120. package/src/components/ui/floating-level-selector.tsx +9 -14
  121. package/src/components/ui/helpers/building-helper.tsx +4 -1
  122. package/src/components/ui/helpers/contextual-helper-panel.tsx +154 -70
  123. package/src/components/ui/helpers/helper-manager.test.tsx +69 -0
  124. package/src/components/ui/helpers/helper-manager.tsx +325 -14
  125. package/src/components/ui/helpers/item-helper.tsx +8 -0
  126. package/src/components/ui/helpers/registered-tool-helper.tsx +8 -1
  127. package/src/components/ui/icon-ref.tsx +45 -1
  128. package/src/components/ui/level-duplicate-dialog.tsx +1 -1
  129. package/src/components/ui/node-icon-image.tsx +16 -0
  130. package/src/components/ui/panels/collections/collections-popover.tsx +20 -14
  131. package/src/components/ui/panels/floor-foundation-panel.tsx +551 -0
  132. package/src/components/ui/panels/floor-opening-panel.tsx +143 -0
  133. package/src/components/ui/panels/mobile-panel-sheet.tsx +2 -2
  134. package/src/components/ui/panels/mobile-selection-bar.tsx +2 -2
  135. package/src/components/ui/panels/multi-field-value.test.ts +26 -3
  136. package/src/components/ui/panels/multi-field-value.ts +6 -3
  137. package/src/components/ui/panels/multi-parametric-inspector.tsx +52 -2
  138. package/src/components/ui/panels/multi-selection-panel.tsx +43 -4
  139. package/src/components/ui/panels/node-display.ts +8 -3
  140. package/src/components/ui/panels/panel-manager.tsx +103 -68
  141. package/src/components/ui/panels/panel-wrapper.tsx +78 -36
  142. package/src/components/ui/panels/parametric-field-control.tsx +1 -1
  143. package/src/components/ui/panels/parametric-inspector.tsx +4 -4
  144. package/src/components/ui/panels/reference-panel.tsx +2 -3
  145. package/src/components/ui/panels/room-construction-rows.tsx +774 -0
  146. package/src/components/ui/panels/room-name-options.test.ts +55 -0
  147. package/src/components/ui/panels/room-name-options.ts +47 -0
  148. package/src/components/ui/panels/room-openings-field.tsx +110 -0
  149. package/src/components/ui/panels/room-panel.tsx +725 -0
  150. package/src/components/ui/panels/shape-choice.tsx +58 -0
  151. package/src/components/ui/panels/wall-region-list.tsx +109 -0
  152. package/src/components/ui/primitives/color-dot.tsx +36 -24
  153. package/src/components/ui/primitives/sidebar.tsx +2 -0
  154. package/src/components/ui/right-stack.test.ts +29 -0
  155. package/src/components/ui/right-stack.tsx +145 -0
  156. package/src/components/ui/sidebar/icon-rail.tsx +5 -0
  157. package/src/components/ui/sidebar/panels/settings-panel/index.tsx +31 -4
  158. package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +5 -5
  159. package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +4 -4
  160. package/src/components/ui/sidebar/panels/site-panel/chimney-tree-node.tsx +1 -5
  161. package/src/components/ui/sidebar/panels/site-panel/collection-tree-node.tsx +197 -0
  162. package/src/components/ui/sidebar/panels/site-panel/column-tree-node.tsx +1 -5
  163. package/src/components/ui/sidebar/panels/site-panel/door-tree-node.tsx +1 -5
  164. package/src/components/ui/sidebar/panels/site-panel/dormer-tree-node.tsx +1 -5
  165. package/src/components/ui/sidebar/panels/site-panel/elevator-tree-node.tsx +1 -5
  166. package/src/components/ui/sidebar/panels/site-panel/fence-tree-node.tsx +24 -11
  167. package/src/components/ui/sidebar/panels/site-panel/gutter-tree-node.tsx +1 -5
  168. package/src/components/ui/sidebar/panels/site-panel/index.tsx +176 -211
  169. package/src/components/ui/sidebar/panels/site-panel/inline-rename-input.tsx +30 -8
  170. package/src/components/ui/sidebar/panels/site-panel/item-tree-node.tsx +1 -3
  171. package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +4 -1
  172. package/src/components/ui/sidebar/panels/site-panel/registry-tree-node.tsx +1 -3
  173. package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +1 -4
  174. package/src/components/ui/sidebar/panels/site-panel/shelf-tree-node.tsx +1 -3
  175. package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +21 -13
  176. package/src/components/ui/sidebar/panels/site-panel/solar-panel-tree-node.tsx +1 -5
  177. package/src/components/ui/sidebar/panels/site-panel/spawn-tree-node.tsx +1 -5
  178. package/src/components/ui/sidebar/panels/site-panel/stair-tree-node.tsx +1 -5
  179. package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +0 -27
  180. package/src/components/ui/sidebar/panels/site-panel/unit-tree-node.tsx +2 -3
  181. package/src/components/ui/sidebar/panels/site-panel/wall-tree-node.tsx +1 -5
  182. package/src/components/ui/sidebar/panels/site-panel/window-tree-node.tsx +1 -5
  183. package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +9 -4
  184. package/src/components/ui/sidebar/panels/zone-panel/index.tsx +0 -2
  185. package/src/components/ui/sidebar/tab-bar.tsx +10 -0
  186. package/src/components/ui/sidebar/use-plugin-panels.tsx +15 -5
  187. package/src/components/viewer/floorplan-compass-button.tsx +9 -1
  188. package/src/components/viewer/floorplan-preview.tsx +5 -6
  189. package/src/components/viewer/viewer-hover-label.tsx +34 -0
  190. package/src/components/viewer/viewer-rooms.tsx +175 -0
  191. package/src/components/viewer/viewer-scene-header.tsx +181 -145
  192. package/src/components/viewer/viewer-selection-manager.tsx +124 -0
  193. package/src/components/viewer/viewer-stage.tsx +2 -2
  194. package/src/components/viewer-zone-system.tsx +3 -8
  195. package/src/hooks/use-auto-save.integration.test.tsx +123 -9
  196. package/src/hooks/use-auto-save.ts +34 -8
  197. package/src/hooks/use-keyboard.test.ts +178 -3
  198. package/src/hooks/use-keyboard.ts +89 -71
  199. package/src/hooks/use-open-wall-ends.test.ts +99 -0
  200. package/src/hooks/use-open-wall-ends.ts +238 -0
  201. package/src/hooks/use-placement-notice.test.ts +48 -0
  202. package/src/hooks/use-placement-notice.ts +42 -0
  203. package/src/hooks/use-selected-room.ts +143 -0
  204. package/src/index.tsx +246 -39
  205. package/src/lib/ceiling-edit-session.test.ts +321 -0
  206. package/src/lib/ceiling-edit-session.ts +206 -0
  207. package/src/lib/ceiling-region-session.test.ts +179 -0
  208. package/src/lib/continuation.test.ts +19 -1
  209. package/src/lib/continuation.ts +25 -4
  210. package/src/lib/door-interaction.ts +14 -0
  211. package/src/lib/editor-level-display.test.ts +96 -0
  212. package/src/lib/editor-level-display.ts +48 -0
  213. package/src/lib/editor-views.ts +144 -0
  214. package/src/lib/export-geometry-wait.test.ts +90 -0
  215. package/src/lib/floor-edit-session.ts +153 -0
  216. package/src/lib/floor-footprints.test.ts +563 -0
  217. package/src/lib/floor-footprints.ts +395 -0
  218. package/src/lib/floor-foundation-support.test.ts +283 -0
  219. package/src/lib/floor-opening-draft.ts +218 -0
  220. package/src/lib/floor-openings.test.ts +216 -0
  221. package/src/lib/floor-region-geometry.ts +191 -0
  222. package/src/lib/floor-region-session.test.ts +485 -0
  223. package/src/lib/floor-region-session.ts +589 -0
  224. package/src/lib/floor-region-snap.ts +101 -0
  225. package/src/lib/floor-step-hover.test.ts +32 -0
  226. package/src/lib/floorplan/annotation-visibility.ts +17 -0
  227. package/src/lib/floorplan/dimension-drive.test.ts +360 -0
  228. package/src/lib/floorplan/dimension-drive.ts +479 -0
  229. package/src/lib/floorplan/floorplan-export.test.ts +12 -8
  230. package/src/lib/floorplan/floorplan-export.tsx +32 -8
  231. package/src/lib/floorplan/floorplan-extension.ts +32 -0
  232. package/src/lib/floorplan/floorplan-mode.test.ts +4 -0
  233. package/src/lib/floorplan/floorplan-mode.ts +2 -0
  234. package/src/lib/floorplan/floorplan-pdfkit-document.ts +339 -18
  235. package/src/lib/floorplan/floorplan-pdfkit-renderer.test.ts +138 -8
  236. package/src/lib/floorplan/floorplan-pdfkit-renderer.ts +56 -25
  237. package/src/lib/floorplan/floorplan-plugin-install.test.tsx +138 -0
  238. package/src/lib/floorplan/floorplan-readonly.ts +6 -0
  239. package/src/lib/floorplan/geometry.ts +13 -6
  240. package/src/lib/floorplan/index.ts +0 -9
  241. package/src/lib/floorplan/items.ts +5 -2
  242. package/src/lib/floorplan/marks.test.ts +213 -0
  243. package/src/lib/floorplan/marks.ts +238 -0
  244. package/src/lib/floorplan/open-wall-ends.test.ts +268 -0
  245. package/src/lib/floorplan/open-wall-ends.ts +162 -0
  246. package/src/lib/floorplan/pdf-fonts/GeistMono-Bold.ttf +0 -0
  247. package/src/lib/floorplan/pdf-fonts/GeistMono-Regular.ttf +0 -0
  248. package/src/lib/floorplan/pdf-fonts/LICENSE-GeistMono.txt +92 -0
  249. package/src/lib/floorplan/pdf-fonts/LICENSE-LiberationSans.txt +102 -0
  250. package/src/lib/floorplan/pdf-fonts/LiberationSans-Bold.ttf +0 -0
  251. package/src/lib/floorplan/pdf-fonts/LiberationSans-Regular.ttf +0 -0
  252. package/src/lib/floorplan/schedules.test.ts +189 -0
  253. package/src/lib/floorplan/schedules.ts +339 -0
  254. package/src/lib/floorplan/sheet-export.test.ts +100 -0
  255. package/src/lib/floorplan/sheet-export.ts +318 -0
  256. package/src/lib/floorplan/site-plan/build-site-plan-drawing.ts +779 -0
  257. package/src/lib/floorplan/site-plan/contours.test.ts +43 -0
  258. package/src/lib/floorplan/site-plan/contributors.ts +97 -0
  259. package/src/lib/floorplan/site-plan/coverage.ts +232 -0
  260. package/src/lib/floorplan/site-plan/drawing-type-switch.test.ts +15 -0
  261. package/src/lib/floorplan/site-plan/drawing-type-switch.tsx +86 -0
  262. package/src/lib/floorplan/site-plan/front-edge.test.ts +103 -0
  263. package/src/lib/floorplan/site-plan/front-edge.ts +156 -0
  264. package/src/lib/floorplan/site-plan/geometry.test.ts +275 -0
  265. package/src/lib/floorplan/site-plan/geometry.ts +557 -0
  266. package/src/lib/floorplan/site-plan/index.ts +71 -0
  267. package/src/lib/floorplan/site-plan/site-annotations.test.ts +366 -0
  268. package/src/lib/floorplan/site-plan/site-annotations.ts +1083 -0
  269. package/src/lib/floorplan/site-plan/site-parts.ts +379 -0
  270. package/src/lib/floorplan/site-plan/site-plan-layer.tsx +138 -0
  271. package/src/lib/floorplan/site-plan/street-name.test.ts +69 -0
  272. package/src/lib/floorplan/site-plan/street-name.ts +302 -0
  273. package/src/lib/floorplan/site-plan/yards.test.ts +69 -0
  274. package/src/lib/floorplan/types.ts +1 -27
  275. package/src/lib/fresh-planar-placement.test.ts +36 -0
  276. package/src/lib/fresh-planar-placement.ts +59 -36
  277. package/src/lib/geometry-script/author.ts +152 -0
  278. package/src/lib/geometry-script/client.ts +152 -0
  279. package/src/lib/geometry-script/protocol.ts +12 -0
  280. package/src/lib/gesture-lifecycle.test.ts +595 -0
  281. package/src/lib/gesture-lifecycle.ts +290 -0
  282. package/src/lib/glb-export-zone-holes.test.ts +41 -0
  283. package/src/lib/glb-export.test.ts +485 -0
  284. package/src/lib/glb-export.ts +237 -119
  285. package/src/lib/history.test.ts +6 -0
  286. package/src/lib/history.ts +7 -0
  287. package/src/lib/hud-title.ts +109 -0
  288. package/src/lib/hydration-gated-storage.ts +49 -0
  289. package/src/lib/ifc-export.test.ts +174 -0
  290. package/src/lib/ifc-export.ts +201 -0
  291. package/src/lib/ifc-parts.ts +15 -0
  292. package/src/lib/inspector-expanded.test.ts +44 -0
  293. package/src/lib/inspector-expanded.ts +36 -0
  294. package/src/lib/interaction/scope.ts +18 -0
  295. package/src/lib/keyboard-pan.test.ts +129 -7
  296. package/src/lib/keyboard-pan.ts +161 -12
  297. package/src/lib/live-gesture-round-trip.test.ts +360 -0
  298. package/src/lib/lot/__fixtures__/map-dossier-miami-shores.json +1 -0
  299. package/src/lib/lot/__fixtures__/map-dossier-stpete.json +1 -0
  300. package/src/lib/lot/clean-ring.test.ts +186 -0
  301. package/src/lib/lot/clean-ring.ts +217 -0
  302. package/src/lib/lot/dossier.test.ts +202 -0
  303. package/src/lib/lot/dossier.ts +498 -0
  304. package/src/lib/lot/drop-in.test.ts +326 -0
  305. package/src/lib/lot/drop-in.ts +407 -0
  306. package/src/lib/lot/index.ts +43 -0
  307. package/src/lib/lot/lot-patch.test.ts +340 -0
  308. package/src/lib/lot/lot-patch.ts +394 -0
  309. package/src/lib/lot/parcel-provider.ts +43 -0
  310. package/src/lib/lot/terrain.test.ts +161 -0
  311. package/src/lib/lot/terrain.ts +266 -0
  312. package/src/lib/material-catalog-model.ts +3 -1
  313. package/src/lib/material-paint-panel-model.ts +9 -5
  314. package/src/lib/material-paint.ts +4 -0
  315. package/src/lib/mezzanine-actions.test.ts +342 -0
  316. package/src/lib/mezzanine-draft.test.ts +501 -0
  317. package/src/lib/mezzanine-draft.ts +278 -0
  318. package/src/lib/mezzanine-messages.ts +26 -0
  319. package/src/lib/mezzanine-selection.test.ts +207 -0
  320. package/src/lib/mezzanine-stairs.ts +46 -0
  321. package/src/lib/model-export.ts +3 -1
  322. package/src/lib/node-action-movement.ts +17 -6
  323. package/src/lib/node-display-label.ts +37 -0
  324. package/src/lib/orbit-floor-clearance.test.ts +165 -0
  325. package/src/lib/orbit-floor-clearance.ts +106 -0
  326. package/src/lib/paint-commit-route.test.ts +37 -0
  327. package/src/lib/paint-commit-route.ts +17 -0
  328. package/src/lib/paint-eyedropper.test.tsx +165 -0
  329. package/src/lib/paint-eyedropper.ts +99 -0
  330. package/src/lib/paint-pass-through.test.ts +111 -0
  331. package/src/lib/paint-pass-through.ts +76 -0
  332. package/src/lib/paint-region-hover.ts +25 -0
  333. package/src/lib/paint-region-mode.ts +154 -0
  334. package/src/lib/paint-regions.test.ts +166 -0
  335. package/src/lib/paint-regions.ts +192 -0
  336. package/src/lib/paint-scope.test.ts +317 -35
  337. package/src/lib/paint-scope.ts +266 -25
  338. package/src/lib/panel-rows.ts +21 -0
  339. package/src/lib/parametric-node-update.ts +1 -1
  340. package/src/lib/plate-paint-affected.test.ts +300 -0
  341. package/src/lib/plate-paint-affected.ts +177 -0
  342. package/src/lib/plugin-chat.ts +50 -0
  343. package/src/lib/plugin-panels.test.ts +25 -0
  344. package/src/lib/plugin-panels.ts +94 -0
  345. package/src/lib/print-shell-compiler.ts +1 -0
  346. package/src/lib/registered-draft-snap.ts +12 -0
  347. package/src/lib/roof-duplication.ts +10 -2
  348. package/src/lib/room-assembly-overlay.test.ts +275 -0
  349. package/src/lib/room-assembly-overlay.ts +266 -0
  350. package/src/lib/room-built-on.test.ts +379 -0
  351. package/src/lib/room-built-on.ts +138 -0
  352. package/src/lib/room-construction-commands.ts +712 -0
  353. package/src/lib/room-construction.test.ts +746 -0
  354. package/src/lib/room-construction.ts +385 -0
  355. package/src/lib/room-controls-phase6.test.ts +630 -0
  356. package/src/lib/room-divide-session.ts +523 -0
  357. package/src/lib/room-handle-drag.ts +1068 -0
  358. package/src/lib/room-name-catalog.test.ts +90 -0
  359. package/src/lib/room-name-catalog.ts +168 -0
  360. package/src/lib/room-push-dimensions.test.ts +311 -0
  361. package/src/lib/room-push-dimensions.ts +251 -0
  362. package/src/lib/room-selection-commands.ts +62 -0
  363. package/src/lib/room-selection.test.ts +973 -0
  364. package/src/lib/room-selection.ts +417 -0
  365. package/src/lib/room-structure-commands.ts +94 -0
  366. package/src/lib/room-structure.test.ts +921 -0
  367. package/src/lib/room-transform-session.ts +570 -0
  368. package/src/lib/room-zone-routing.test.ts +294 -0
  369. package/src/lib/room-zone-routing.ts +197 -0
  370. package/src/lib/scene-clipboard.test.ts +364 -0
  371. package/src/lib/scene-clipboard.ts +229 -48
  372. package/src/lib/scene-material-refs.test.ts +79 -0
  373. package/src/lib/scene-material-refs.ts +79 -0
  374. package/src/lib/scene.test.ts +24 -36
  375. package/src/lib/scene.ts +33 -57
  376. package/src/lib/selection-routing.test.ts +129 -45
  377. package/src/lib/selection-routing.ts +53 -74
  378. package/src/lib/session-writes.test.ts +120 -0
  379. package/src/lib/session-writes.ts +205 -0
  380. package/src/lib/sidebar-panel.ts +62 -0
  381. package/src/lib/site-boundary.test.ts +4 -6
  382. package/src/lib/site-boundary.ts +1 -1
  383. package/src/lib/snapping-mode.test.ts +4 -0
  384. package/src/lib/snapping-mode.ts +8 -1
  385. package/src/lib/terrace-draft.test.ts +145 -0
  386. package/src/lib/terrace-draft.ts +213 -0
  387. package/src/lib/tool-cursor-icon.test.ts +16 -0
  388. package/src/lib/tool-cursor-icon.ts +11 -0
  389. package/src/lib/typing-target.ts +9 -0
  390. package/src/lib/units.ts +77 -39
  391. package/src/lib/vector-edges.test.ts +58 -0
  392. package/src/lib/vector-edges.ts +400 -0
  393. package/src/lib/viewer-selection.test.ts +577 -0
  394. package/src/lib/viewer-selection.ts +569 -0
  395. package/src/lib/wall-draw-variant.test.ts +71 -0
  396. package/src/lib/wall-draw-variant.ts +91 -0
  397. package/src/lib/wall-paint-region-session.ts +246 -0
  398. package/src/lib/wall-paint-region.test.ts +373 -0
  399. package/src/lib/wall-polygon-draft.test.ts +262 -0
  400. package/src/lib/wall-polygon-draft.ts +286 -0
  401. package/src/lib/wall-push.test.ts +453 -0
  402. package/src/lib/wall-region-face.ts +352 -0
  403. package/src/lib/wall-region-handles.test.ts +304 -0
  404. package/src/lib/wall-region-handles.ts +368 -0
  405. package/src/lib/wall-region-list.test.ts +119 -0
  406. package/src/lib/wall-region-list.ts +136 -0
  407. package/src/lib/wall-region-snap.ts +110 -0
  408. package/src/lib/window-interaction.ts +14 -0
  409. package/src/lib/zone-content.ts +4 -1
  410. package/src/lib/zone-name.test.ts +17 -0
  411. package/src/lib/zone-name.ts +17 -0
  412. package/src/store/opens-in-select-mode.test.ts +223 -0
  413. package/src/store/paint-mode.test.ts +188 -0
  414. package/src/store/terrain-sculpt-mode.test.ts +19 -21
  415. package/src/store/tool-mode.test.ts +137 -6
  416. package/src/store/use-delete-confirmation.ts +28 -1
  417. package/src/store/use-drawing-view.test.ts +24 -3
  418. package/src/store/use-drawing-view.ts +29 -1
  419. package/src/store/use-editor.tsx +432 -321
  420. package/src/store/use-floorplan-draft-preview.ts +13 -0
  421. package/src/store/use-floorplan-marquee.ts +12 -2
  422. package/src/store/use-quick-measurement-hud.ts +3 -3
  423. package/src/store/use-stair-build-preview.ts +13 -5
  424. package/src/store/use-wall-move-ghosts.ts +4 -2
  425. package/src/store/use-wall-region-handles-selection.test.ts +222 -0
  426. package/src/store/use-wall-region-handles-selection.ts +139 -0
  427. package/src/store/view-layout.test.ts +175 -0
  428. package/src/store/view-layout.ts +196 -0
  429. package/src/test-utils/immediate-animation-frames.ts +16 -0
  430. package/src/test-utils/live-gesture-round-trip.ts +224 -0
  431. package/src/test-utils/selection-harness.ts +82 -0
  432. package/src/components/editor-2d/renderers/floorplan-stair-layer.tsx +0 -499
  433. package/src/components/systems/zone/zone-label-editor-system.tsx +0 -333
  434. package/src/components/tools/fence/fence-drafting.test.ts +0 -100
  435. package/src/components/tools/fence/fence-drafting.ts +0 -266
  436. package/src/components/ui/sidebar/panels/plugins-panel.tsx +0 -218
  437. package/src/components/viewer/viewer-units-panel.tsx +0 -116
  438. package/src/lib/floorplan/stairs.ts +0 -489
  439. package/src/lib/level-duplication.test.ts +0 -144
  440. package/src/lib/level-duplication.ts +0 -195
@@ -1,12 +1,13 @@
1
1
  'use client'
2
2
 
3
- import type { AnyNodeId, Collection, CollectionId } from '@pascal-app/core'
3
+ import type { AnyNodeId, CollectionId } from '@pascal-app/core'
4
4
  import { useScene } from '@pascal-app/core'
5
5
  import {
6
6
  Check,
7
7
  ChevronDown,
8
8
  ChevronRight,
9
9
  Layers,
10
+ Minus,
10
11
  MoreHorizontal,
11
12
  Pencil,
12
13
  Plus,
@@ -29,12 +30,12 @@ import {
29
30
  import { cn } from '../../../../lib/utils'
30
31
 
31
32
  interface CollectionsPopoverProps {
32
- nodeId: AnyNodeId
33
- collectionIds?: CollectionId[]
33
+ /** The elements the popover adds to and removes from collections, together. */
34
+ nodeIds: AnyNodeId[]
34
35
  children: React.ReactNode
35
36
  }
36
37
 
37
- export function CollectionsPopover({ nodeId, collectionIds, children }: CollectionsPopoverProps) {
38
+ export function CollectionsPopover({ nodeIds, children }: CollectionsPopoverProps) {
38
39
  const collections = useScene((s) => s.collections)
39
40
  const nodes = useScene((s) => s.nodes)
40
41
  const createCollection = useScene((s) => s.createCollection)
@@ -54,12 +55,11 @@ export function CollectionsPopover({ nodeId, collectionIds, children }: Collecti
54
55
  const [deletingId, setDeletingId] = useState<CollectionId | null>(null)
55
56
  const [expandedIds, setExpandedIds] = useState<Set<CollectionId>>(new Set())
56
57
 
57
- const memberIds = collectionIds ?? []
58
58
  const allCollections = Object.values(collections)
59
59
 
60
60
  const handleCreate = () => {
61
61
  if (!createName.trim()) return
62
- createCollection(createName.trim(), [nodeId])
62
+ createCollection(createName.trim(), nodeIds)
63
63
  setCreateName('')
64
64
  setShowCreateInput(false)
65
65
  }
@@ -71,10 +71,11 @@ export function CollectionsPopover({ nodeId, collectionIds, children }: Collecti
71
71
  }
72
72
 
73
73
  const toggleMembership = (collectionId: CollectionId) => {
74
- if (memberIds.includes(collectionId)) {
75
- removeFromCollection(collectionId, nodeId)
74
+ const memberIds = collections[collectionId]?.nodeIds ?? []
75
+ if (nodeIds.every((id) => memberIds.includes(id))) {
76
+ removeFromCollection(collectionId, nodeIds)
76
77
  } else {
77
- addToCollection(collectionId, nodeId)
78
+ addToCollection(collectionId, nodeIds)
78
79
  }
79
80
  }
80
81
 
@@ -161,13 +162,15 @@ export function CollectionsPopover({ nodeId, collectionIds, children }: Collecti
161
162
  <div className="flex flex-col items-center justify-center gap-2 px-4 py-8 text-center">
162
163
  <Layers className="h-6 w-6 text-muted-foreground/40" />
163
164
  <p className="text-muted-foreground text-xs">
164
- No collections yet. Create one to group items together.
165
+ No collections yet. Create one to group elements together.
165
166
  </p>
166
167
  </div>
167
168
  ) : (
168
169
  <ul className="divide-y divide-border/30">
169
170
  {allCollections.map((collection) => {
170
- const isIn = memberIds.includes(collection.id)
171
+ const memberCount = nodeIds.filter((id) => collection.nodeIds.includes(id)).length
172
+ const isIn = memberCount === nodeIds.length
173
+ const isPartly = memberCount > 0 && !isIn
171
174
  const isExpanded = expandedIds.has(collection.id)
172
175
  const isRenaming = renamingId === collection.id
173
176
  const isDeleting = deletingId === collection.id
@@ -268,10 +271,13 @@ export function CollectionsPopover({ nodeId, collectionIds, children }: Collecti
268
271
  <div
269
272
  className={cn(
270
273
  'pointer-events-none flex h-4 w-4 shrink-0 items-center justify-center rounded border transition-colors',
271
- isIn ? 'border-primary bg-primary/20 text-primary' : 'border-border/50',
274
+ isIn || isPartly
275
+ ? 'border-primary bg-primary/20 text-primary'
276
+ : 'border-border/50',
272
277
  )}
273
278
  >
274
279
  {isIn && <Check className="h-2.5 w-2.5" />}
280
+ {isPartly && <Minus className="h-2.5 w-2.5" />}
275
281
  </div>
276
282
 
277
283
  {/* Expand toggle (only if has members) */}
@@ -290,7 +296,7 @@ export function CollectionsPopover({ nodeId, collectionIds, children }: Collecti
290
296
  )}
291
297
 
292
298
  {/* More dropdown */}
293
- <DropdownMenu>
299
+ <DropdownMenu modal={false}>
294
300
  <DropdownMenuTrigger asChild>
295
301
  <button
296
302
  className="flex h-5 w-5 shrink-0 items-center justify-center rounded text-muted-foreground opacity-0 transition-colors hover:bg-white/10 hover:text-foreground group-hover:opacity-100"
@@ -332,7 +338,7 @@ export function CollectionsPopover({ nodeId, collectionIds, children }: Collecti
332
338
  <span
333
339
  className={cn(
334
340
  'truncate text-[11px]',
335
- nid === nodeId
341
+ nodeIds.includes(nid)
336
342
  ? 'font-medium text-foreground'
337
343
  : 'text-muted-foreground',
338
344
  )}
@@ -0,0 +1,551 @@
1
+ 'use client'
2
+
3
+ import {
4
+ type AnyNode,
5
+ type AnyNodeId,
6
+ type FloorFoundationPatch,
7
+ floorFootprintName,
8
+ floorFootprintSupportClass,
9
+ upperFloorHeightControl,
10
+ getLevelDisplayName,
11
+ getLevelElevations,
12
+ getStoredLevelHeight,
13
+ type LevelNode,
14
+ MIN_GROUND_FLOOR_THICKNESS,
15
+ MIN_SLAB_THICKNESS,
16
+ type SlabNode,
17
+ useScene,
18
+ } from '@pascal-app/core'
19
+ import { useViewer } from '@pascal-app/viewer'
20
+ import { useRef, useState } from 'react'
21
+ import {
22
+ applyFloorFoundation,
23
+ beginFootprintHeightPreview,
24
+ beginFootprintThicknessPreview,
25
+ type FootprintPreset,
26
+ footprintHeightMinimum,
27
+ footprintHeightPatch,
28
+ footprintHeightValue,
29
+ footprintPreset,
30
+ footprintsToMoveTogether,
31
+ floorTopAboveGround,
32
+ presetPatch,
33
+ THICK_FLOOR_HINT,
34
+ thickFloorAdvice,
35
+ } from '../../../lib/floor-footprints'
36
+ import { formatLinearMeasurement } from '../../../lib/measurements'
37
+ import { FinishSwatch } from '../controls/finish-swatch'
38
+ import { MetricControl } from '../controls/metric-control'
39
+ import { SegmentedControl } from '../controls/segmented-control'
40
+ import { Button } from '../primitives/button'
41
+ import { PanelWrapper } from './panel-wrapper'
42
+
43
+ /** Unpainted finishes, as the plate renders them (nodes' slab slot defaults). */
44
+ const EDGE_DEFAULT = '#cccccc'
45
+ const FOUNDATION_DEFAULT = 'library:concrete-raw'
46
+
47
+ /**
48
+ * A side section through the footprint's edge, top to bottom in the order it
49
+ * is built. On the ground: the floor top, the slab, the foundation when the
50
+ * footprint is raised on one, the ground — the slab always sits on the
51
+ * foundation (or the ground), so the top is their sum. Upstairs: the floor
52
+ * plate resting on the walls of the storey below. Drawn to a readable scale,
53
+ * not to size.
54
+ */
55
+ export function FloorSection({
56
+ supported,
57
+ raised,
58
+ lift = 0,
59
+ liftLabel = '',
60
+ foundationHeight = 0,
61
+ foundationLabel = '',
62
+ topLabel,
63
+ thickness,
64
+ thicknessLabel,
65
+ }: {
66
+ /** Upstairs (or over a basement): it rests on the walls below, never on a foundation. */
67
+ supported: boolean
68
+ /** On a foundation (ground-bearing footprints only). */
69
+ raised: boolean
70
+ /** Upstairs: meters the top sits above resting on the walls below. */
71
+ lift?: number
72
+ liftLabel?: string
73
+ /** On the ground: the foundation's height, meters. */
74
+ foundationHeight?: number
75
+ foundationLabel?: string
76
+ /** On the ground: the floor top above the ground (foundation + slab). */
77
+ topLabel?: string
78
+ thickness: number
79
+ thicknessLabel: string
80
+ }) {
81
+ const base = 100
82
+ const foundation = !supported && raised ? Math.min(40, 14 + foundationHeight * 30) : 0
83
+ const band = Math.max(7, Math.min(16, thickness * 60))
84
+ // Upstairs a lift moves the plate off its default position (on the walls below).
85
+ const upstairsLift =
86
+ supported && Math.abs(lift) > 0.005
87
+ ? lift > 0
88
+ ? Math.min(18, 6 + lift * 30)
89
+ : -Math.min(10, 4 + -lift * 30)
90
+ : 0
91
+ const bandBottom = base - foundation - upstairsLift
92
+ const bandTop = bandBottom - band
93
+ // Upstairs the lift is measured from resting on the walls below; on the
94
+ // ground the top is measured from the ground.
95
+ const measureFrom = supported ? base - band : base
96
+ const measured = supported ? Math.abs(lift) > 0.005 : true
97
+ const measureLabel = supported ? liftLabel : (topLabel ?? '')
98
+ const left = 64
99
+ const right = 164
100
+ const labels: { y: number; text: string; key: string }[] = [
101
+ { key: 'top', y: bandTop - 9, text: 'Floor top' },
102
+ {
103
+ key: 'plate',
104
+ y: bandTop + band / 2,
105
+ text: supported ? `Floor plate · ${thicknessLabel}` : `Slab · ${thicknessLabel}`,
106
+ },
107
+ ...(foundation
108
+ ? [
109
+ {
110
+ key: 'foundation',
111
+ y: bandBottom + foundation / 2,
112
+ text: foundationLabel ? `Foundation · ${foundationLabel}` : 'Foundation',
113
+ },
114
+ ]
115
+ : []),
116
+ { key: 'base', y: base + 9, text: supported ? 'Walls below' : 'Ground' },
117
+ ]
118
+ return (
119
+ <figure className="flex flex-col gap-1" data-floor-section>
120
+ <svg
121
+ aria-label={
122
+ supported
123
+ ? `Floor plate ${thicknessLabel} thick, resting on the walls below`
124
+ : raised
125
+ ? `Slab ${thicknessLabel} thick on a ${foundationLabel} foundation, floor top ${topLabel ?? ''} above the ground`
126
+ : `Slab ${thicknessLabel} thick on the ground, floor top ${topLabel ?? ''} above the ground`
127
+ }
128
+ className="h-auto w-full text-muted-foreground"
129
+ role="img"
130
+ viewBox="0 0 280 118"
131
+ >
132
+ {supported ? (
133
+ // The walls of the storey below, under the plate's edge.
134
+ <g data-section-walls-below>
135
+ <rect
136
+ className="fill-muted"
137
+ height={118 - bandBottom}
138
+ stroke="currentColor"
139
+ strokeOpacity="0.4"
140
+ width="14"
141
+ x={left + 6}
142
+ y={bandBottom}
143
+ />
144
+ <line
145
+ opacity="0.6"
146
+ stroke="currentColor"
147
+ strokeDasharray="3 3"
148
+ x1="8"
149
+ x2={right}
150
+ y1={base}
151
+ y2={base}
152
+ />
153
+ </g>
154
+ ) : (
155
+ <g data-section-ground>
156
+ <line stroke="currentColor" strokeWidth="1" x1="8" x2="272" y1={base} y2={base} />
157
+ {Array.from({ length: 22 }, (_, i) => (
158
+ <line
159
+ key={i}
160
+ opacity="0.35"
161
+ stroke="currentColor"
162
+ strokeWidth="1"
163
+ x1={12 + i * 12}
164
+ x2={6 + i * 12}
165
+ y1={base + 1}
166
+ y2={base + 7}
167
+ />
168
+ ))}
169
+ </g>
170
+ )}
171
+ {/* A wall standing on the floor. */}
172
+ <rect
173
+ className="fill-muted"
174
+ height={Math.min(26, bandTop - 12)}
175
+ stroke="currentColor"
176
+ strokeOpacity="0.4"
177
+ width="14"
178
+ x={left + 6}
179
+ y={bandTop - Math.min(26, bandTop - 12)}
180
+ />
181
+ {foundation > 0 && (
182
+ <rect
183
+ data-section-foundation
184
+ fill="#8a8a8a"
185
+ height={foundation}
186
+ stroke="currentColor"
187
+ strokeOpacity="0.5"
188
+ width={right - left - 8}
189
+ x={left + 4}
190
+ y={bandBottom}
191
+ />
192
+ )}
193
+ <rect
194
+ className="fill-foreground"
195
+ data-section-band
196
+ height={band}
197
+ width={right - left}
198
+ x={left}
199
+ y={bandTop}
200
+ />
201
+ {/* The floor top. */}
202
+ <line
203
+ className="text-foreground"
204
+ stroke="currentColor"
205
+ strokeWidth="1.5"
206
+ x1={left - 4}
207
+ x2={right + 4}
208
+ y1={bandTop}
209
+ y2={bandTop}
210
+ />
211
+ {measured && measureLabel && (
212
+ <g className="text-foreground" data-section-height>
213
+ <line stroke="currentColor" strokeWidth="1" x1="50" x2="50" y1={bandTop} y2={measureFrom} />
214
+ <line stroke="currentColor" strokeWidth="1" x1="45" x2="55" y1={bandTop} y2={bandTop} />
215
+ <line stroke="currentColor" strokeWidth="1" x1="45" x2="55" y1={measureFrom} y2={measureFrom} />
216
+ {supported && (
217
+ <line
218
+ opacity="0.6"
219
+ stroke="currentColor"
220
+ strokeDasharray="2 3"
221
+ x1="55"
222
+ x2={left}
223
+ y1={measureFrom}
224
+ y2={measureFrom}
225
+ />
226
+ )}
227
+ <text
228
+ className="fill-foreground font-mono"
229
+ dominantBaseline="middle"
230
+ fontSize="10"
231
+ textAnchor="end"
232
+ x="42"
233
+ y={(bandTop + measureFrom) / 2}
234
+ >
235
+ {measureLabel}
236
+ </text>
237
+ </g>
238
+ )}
239
+ {labels.map((label) => (
240
+ <g key={label.key}>
241
+ <line
242
+ opacity="0.6"
243
+ stroke="currentColor"
244
+ x1={right + 4}
245
+ x2={right + 12}
246
+ y1={label.y}
247
+ y2={label.y}
248
+ />
249
+ <text
250
+ className="fill-foreground"
251
+ dominantBaseline="middle"
252
+ fontSize="10"
253
+ x={right + 16}
254
+ y={label.y}
255
+ >
256
+ {label.text}
257
+ </text>
258
+ </g>
259
+ ))}
260
+ </svg>
261
+ <figcaption className="text-right text-[10px] text-muted-foreground/70">
262
+ Not to scale
263
+ </figcaption>
264
+ </figure>
265
+ )
266
+ }
267
+
268
+ type Notice = {
269
+ message: string
270
+ /** The footprints to move together, and the change, when that would be accepted. */
271
+ together: { ids: string[]; patch: FloorFoundationPatch; lower: boolean } | null
272
+ }
273
+
274
+ export function moveTogetherLabel(count: number, lower: boolean) {
275
+ const verb = lower ? 'Lower' : 'Raise'
276
+ return count === 2 ? `${verb} both` : `${verb} all ${count}`
277
+ }
278
+
279
+ /** "Floor plate · Ground floor", upstairs "Floor plate · Floor 1 · rests on Ground floor". */
280
+ export function floorPlateKicker(nodes: Record<string, AnyNode>, plate: SlabNode): string {
281
+ const level = plate.parentId ? nodes[plate.parentId] : undefined
282
+ const levelName = level?.type === 'level' ? ` · ${getLevelDisplayName(level)}` : ''
283
+ // A supported floor rests on the storey right below it.
284
+ let below: LevelNode | null = null
285
+ if (level?.type === 'level' && floorFootprintSupportClass(nodes, plate) === 'supported')
286
+ for (const node of Object.values(nodes))
287
+ if (
288
+ node.type === 'level' &&
289
+ node.parentId === level.parentId &&
290
+ node.level < level.level &&
291
+ (!below || node.level > below.level)
292
+ )
293
+ below = node
294
+ const restsOn = below ? ` · rests on ${getLevelDisplayName(below)}` : ''
295
+ return `Floor plate${levelName}${restsOn}`
296
+ }
297
+
298
+ /**
299
+ * What the panel shows for a footprint. An upper floor rests on the walls
300
+ * below: its height is its floor top, and raising it thickens the plate (the
301
+ * two stay in sync); it has no ground preset and no foundation. A footprint on
302
+ * the ground has two inputs, the slab thickness and the foundation height (0 on
303
+ * the ground); its floor top above the ground is their sum, read-only.
304
+ */
305
+ export function floorFoundationModel(nodes: Record<string, AnyNode>, plate: SlabNode) {
306
+ const upper = upperFloorHeightControl(nodes, plate) !== null
307
+ const height = footprintHeightValue(nodes, plate)
308
+ const levelBase = plate.parentId
309
+ ? (getLevelElevations(nodes as Record<AnyNodeId, AnyNode>).get(plate.parentId as AnyNodeId)
310
+ ?.baseY ?? 0)
311
+ : 0
312
+ return {
313
+ upper,
314
+ kicker: floorPlateKicker(nodes, plate),
315
+ preset: upper ? ('ground' as const) : footprintPreset(nodes, plate),
316
+ /** Upstairs: the floor top (level-local). On the ground: the foundation height. */
317
+ height,
318
+ /** Upstairs: the floor top above the ground. On the ground: above the ground (foundation + slab). */
319
+ floorTop: upper ? levelBase + height : floorTopAboveGround(nodes, plate),
320
+ minHeight: footprintHeightMinimum(nodes, plate),
321
+ minThickness: upper ? MIN_SLAB_THICKNESS : MIN_GROUND_FLOOR_THICKNESS,
322
+ underFloor: !upper,
323
+ thickFloor: thickFloorAdvice(nodes, plate),
324
+ }
325
+ }
326
+
327
+ /**
328
+ * "Floor & foundation" for one footprint (the house, a shed, an upper storey),
329
+ * in one column in the order it is built: the section, the slab (thickness,
330
+ * edge finish), then what is under the floor — on a ground-bearing footprint
331
+ * only — and the floor top that follows. Scrubbing the foundation height moves
332
+ * the building live and lands as one undo step.
333
+ */
334
+ export function FloorFoundationPanel({ node, onClose }: { node: SlabNode; onClose: () => void }) {
335
+ const nodes = useScene((state) => state.nodes)
336
+ const unit = useViewer((s) => s.unit)
337
+ const metricNotation = useViewer((s) => s.metricNotation)
338
+ const [notice, setNotice] = useState<Notice | null>(null)
339
+ const scrub = useRef<ReturnType<typeof beginFootprintHeightPreview> | null>(null)
340
+ const thicknessScrub = useRef<ReturnType<typeof beginFootprintThicknessPreview> | null>(null)
341
+ const plate = nodes[node.id as AnyNodeId]
342
+ if (plate?.type !== 'slab' || plate.plateRole !== 'base') return null
343
+ const length = (meters: number) => formatLinearMeasurement(meters, unit, metricNotation)
344
+ const { upper, kicker, preset, height, floorTop, minHeight, minThickness, underFloor, thickFloor } =
345
+ floorFoundationModel(nodes, plate)
346
+ const level = plate.parentId ? nodes[plate.parentId as AnyNodeId] : undefined
347
+ const maxHeight = level?.type === 'level' ? getStoredLevelHeight(level) - 0.5 : 3
348
+ // A refused change says why, in the panel's words, never as an error; when
349
+ // only the shared-storey rule refused it, it offers to move every footprint
350
+ // under that storey together (checked once the change settles, not per frame).
351
+ const report = (message: string | null, patch?: FloorFoundationPatch) => {
352
+ if (!message) return setNotice(null)
353
+ const current = useScene.getState().nodes
354
+ const together = patch ? footprintsToMoveTogether(current, plate.id, patch) : null
355
+ const latest = current[plate.id as AnyNodeId]
356
+ const lower =
357
+ latest?.type !== 'slab'
358
+ ? false
359
+ : patch?.thickness !== undefined
360
+ ? patch.thickness < latest.thickness
361
+ : patch?.foundationHeight !== undefined
362
+ ? patch.foundationHeight < footprintHeightValue(current, latest)
363
+ : patch?.floorHeight == null ||
364
+ patch.floorHeight < (latest.floorHeight ?? latest.elevation)
365
+ setNotice({ message, together: together && patch ? { ids: together, patch, lower } : null })
366
+ }
367
+ const heightPatchFor = (value: number) => {
368
+ const current = useScene.getState().nodes
369
+ const latest = current[plate.id as AnyNodeId]
370
+ return latest?.type === 'slab' ? footprintHeightPatch(current, latest, value) : undefined
371
+ }
372
+ // A scrub previews live and lands once on release; a typed value lands directly.
373
+ const previewHeight = (value: number) => {
374
+ scrub.current ??= beginFootprintHeightPreview(plate.id)
375
+ report(scrub.current.preview(value))
376
+ }
377
+ const commitHeight = (value: number) => {
378
+ const session = scrub.current ?? beginFootprintHeightPreview(plate.id)
379
+ scrub.current = null
380
+ const message = session.commit(value)
381
+ report(message, message ? heightPatchFor(value) : undefined)
382
+ }
383
+ // Upstairs the underside stays on the walls below: the thickness is the one
384
+ // control, and it moves the floor top (and the storeys above) with it.
385
+ const underside = height - plate.thickness
386
+
387
+ return (
388
+ <PanelWrapper
389
+ icon="/icons/floor.webp"
390
+ kicker={kicker}
391
+ onClose={onClose}
392
+ title={floorFootprintName(nodes, plate)}
393
+ >
394
+ <div
395
+ className="flex flex-col gap-3 px-4 pt-3 pb-4 text-sm"
396
+ data-floor-foundation={plate.id}
397
+ data-support={upper ? 'supported' : 'ground-bearing'}
398
+ >
399
+ <FloorSection
400
+ foundationHeight={upper ? 0 : height}
401
+ foundationLabel={length(height)}
402
+ raised={preset === 'raised'}
403
+ supported={upper}
404
+ thickness={plate.thickness}
405
+ thicknessLabel={length(plate.thickness)}
406
+ topLabel={upper ? undefined : length(floorTop)}
407
+ />
408
+ <section className="flex flex-col gap-1.5" data-floor-plate-group>
409
+ <h3 className="font-medium text-muted-foreground text-xs">
410
+ {upper ? 'Floor plate' : 'Slab'}
411
+ </h3>
412
+ {upper ? (
413
+ <div title="A thicker floor grows upward: the rooms below keep their height.">
414
+ <MetricControl
415
+ className="h-9 text-xs"
416
+ label="Floor thickness"
417
+ max={Math.max(MIN_SLAB_THICKNESS, maxHeight - underside)}
418
+ min={Math.max(MIN_SLAB_THICKNESS, minHeight - underside)}
419
+ onChange={(next) => previewHeight(underside + next)}
420
+ onCommit={(next) => commitHeight(underside + next)}
421
+ precision={2}
422
+ restoreOnCommit={false}
423
+ step={0.01}
424
+ unit="m"
425
+ value={plate.thickness}
426
+ />
427
+ </div>
428
+ ) : (
429
+ <div title="The slab sits on the foundation (or the ground): a thicker slab raises the floor top and everything on it.">
430
+ <MetricControl
431
+ className="h-9 text-xs"
432
+ label="Slab thickness"
433
+ max={1}
434
+ min={minThickness}
435
+ onChange={(next) => {
436
+ thicknessScrub.current ??= beginFootprintThicknessPreview(plate.id)
437
+ report(thicknessScrub.current.preview(Math.max(minThickness, next)))
438
+ }}
439
+ onCommit={(value) => {
440
+ const session =
441
+ thicknessScrub.current ?? beginFootprintThicknessPreview(plate.id)
442
+ thicknessScrub.current = null
443
+ const patch = { thickness: Math.max(minThickness, value) }
444
+ const message = session.commit(patch.thickness)
445
+ report(message, message ? patch : undefined)
446
+ }}
447
+ precision={2}
448
+ restoreOnCommit={false}
449
+ step={0.01}
450
+ unit="m"
451
+ value={plate.thickness}
452
+ />
453
+ </div>
454
+ )}
455
+ {upper && (
456
+ <p className="px-0.5 text-muted-foreground text-xs" data-floor-top>
457
+ Floor top +{length(floorTop)}
458
+ </p>
459
+ )}
460
+ <FinishSwatch
461
+ fallback={EDGE_DEFAULT}
462
+ hint="The floor's outside edge, all around the footprint."
463
+ label="Edge finish"
464
+ node={plate}
465
+ role="edge"
466
+ value={plate.slots?.edge}
467
+ />
468
+ {thickFloor && (
469
+ <p className="text-muted-foreground text-xs" data-thick-floor-hint>
470
+ {THICK_FLOOR_HINT}
471
+ </p>
472
+ )}
473
+ </section>
474
+ {underFloor && (
475
+ <section className="flex flex-col gap-1.5" data-under-floor-group>
476
+ <h3 className="font-medium text-muted-foreground text-xs">Under the floor</h3>
477
+ <SegmentedControl
478
+ onChange={(next: FootprintPreset) => {
479
+ if (next === preset) return
480
+ const patch = presetPatch(nodes, plate, next)
481
+ report(applyFloorFoundation(plate.id, patch), patch)
482
+ }}
483
+ options={[
484
+ { label: 'On the ground', value: 'ground' },
485
+ { label: 'Raised on a foundation', value: 'raised' },
486
+ ]}
487
+ value={preset}
488
+ />
489
+ {preset === 'raised' && (
490
+ <>
491
+ <div title="The foundation stands on the ground; the slab sits on it.">
492
+ <MetricControl
493
+ className="h-9 text-xs"
494
+ label="Foundation height"
495
+ max={Math.max(0.05, maxHeight)}
496
+ min={minHeight}
497
+ onChange={previewHeight}
498
+ onCommit={commitHeight}
499
+ precision={2}
500
+ restoreOnCommit={false}
501
+ step={0.05}
502
+ unit="m"
503
+ value={height}
504
+ />
505
+ </div>
506
+ <FinishSwatch
507
+ fallback={FOUNDATION_DEFAULT}
508
+ label="Foundation finish"
509
+ node={plate}
510
+ role="foundation"
511
+ value={
512
+ typeof plate.foundation?.material === 'string'
513
+ ? plate.foundation.material
514
+ : undefined
515
+ }
516
+ />
517
+ </>
518
+ )}
519
+ <p className="px-0.5 text-muted-foreground text-xs" data-floor-top>
520
+ Floor top {length(floorTop)}
521
+ </p>
522
+ </section>
523
+ )}
524
+ {notice && (
525
+ <div
526
+ className="flex flex-col items-start gap-2 rounded-lg bg-accent/30 px-2.5 py-2"
527
+ data-floor-foundation-notice
528
+ >
529
+ <p className="text-muted-foreground text-xs" role="status">
530
+ {notice.message}
531
+ </p>
532
+ {notice.together && (
533
+ <Button
534
+ className="rounded-full"
535
+ onClick={() => {
536
+ const { ids, patch } = notice.together!
537
+ report(applyFloorFoundation(ids, patch))
538
+ }}
539
+ size="sm"
540
+ type="button"
541
+ variant="outline"
542
+ >
543
+ {moveTogetherLabel(notice.together.ids.length, notice.together.lower)}
544
+ </Button>
545
+ )}
546
+ </div>
547
+ )}
548
+ </div>
549
+ </PanelWrapper>
550
+ )
551
+ }