@pascal-app/editor 1.0.3 → 1.0.5

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 (624) hide show
  1. package/README.md +106 -0
  2. package/package.json +42 -38
  3. package/src/components/editor/__bench__/room-label-placement.bench.ts +108 -0
  4. package/src/components/editor/action-menu-button.tsx +83 -0
  5. package/src/components/editor/bake-thumbnail.tsx +307 -5
  6. package/src/components/editor/capture-level-picker.tsx +72 -0
  7. package/src/components/editor/construction-reveal-audio.test.tsx +150 -0
  8. package/src/components/editor/construction-reveal-audio.tsx +40 -0
  9. package/src/components/editor/custom-camera-controls.tsx +275 -57
  10. package/src/components/editor/delete-confirmation-dialog.tsx +297 -7
  11. package/src/components/editor/duplicate-node.ts +244 -0
  12. package/src/components/editor/editor-layout-mobile.tsx +37 -26
  13. package/src/components/editor/editor-layout-v2.tsx +132 -59
  14. package/src/components/editor/export-manager.tsx +47 -26
  15. package/src/components/editor/first-person/build-collider-world.test.ts +320 -0
  16. package/src/components/editor/first-person/build-collider-world.ts +28 -7
  17. package/src/components/editor/first-person/ground-holes.test.ts +128 -0
  18. package/src/components/editor/first-person/ground-holes.ts +130 -0
  19. package/src/components/editor/first-person/mechanism-targets.test.ts +162 -0
  20. package/src/components/editor/first-person/mechanism-targets.ts +88 -0
  21. package/src/components/editor/first-person-controls.tsx +220 -9
  22. package/src/components/editor/floating-action-menu.tsx +63 -207
  23. package/src/components/editor/floor-aware-camera-controls.test.ts +466 -0
  24. package/src/components/editor/floor-aware-camera-controls.ts +324 -0
  25. package/src/components/editor/floor-openings-overlay.tsx +243 -0
  26. package/src/components/editor/floor-region-controls.tsx +846 -0
  27. package/src/components/editor/floor-region-webgpu-materials.test.ts +12 -0
  28. package/src/components/editor/floor-surface.test.ts +165 -0
  29. package/src/components/editor/floor-surface.ts +182 -0
  30. package/src/components/editor/floorplan-background-selection.test.ts +11 -2
  31. package/src/components/editor/floorplan-background-selection.ts +1 -3
  32. package/src/components/editor/floorplan-mode-coordinator.tsx +2 -2
  33. package/src/components/editor/floorplan-panel.tsx +414 -363
  34. package/src/components/editor/follow-pascal.test.tsx +912 -0
  35. package/src/components/editor/follow-pascal.tsx +354 -0
  36. package/src/components/editor/footprint-height-handle.tsx +204 -0
  37. package/src/components/editor/group-actions.ts +56 -2
  38. package/src/components/editor/group-move-3d.ts +2 -0
  39. package/src/components/editor/group-rotate-handle.tsx +2 -0
  40. package/src/components/editor/group-transform-shared.test.ts +460 -0
  41. package/src/components/editor/handles/handle-hit-priority.test.tsx +63 -0
  42. package/src/components/editor/handles/handle-hit-priority.tsx +37 -0
  43. package/src/components/editor/handles/swallow-next-click.test.ts +28 -0
  44. package/src/components/editor/handles/use-handle-drag.ts +269 -197
  45. package/src/components/editor/index.tsx +409 -197
  46. package/src/components/editor/join-walls-pill.tsx +44 -0
  47. package/src/components/editor/node-action-menu.tsx +68 -70
  48. package/src/components/editor/node-arrow-handles.tsx +25 -6
  49. package/src/components/editor/open-wall-ends-3d-layer.tsx +273 -0
  50. package/src/components/editor/open-wall-ends-hint.tsx +89 -0
  51. package/src/components/editor/quick-measurement-hud.tsx +3 -2
  52. package/src/components/editor/registry-action-contributions.test.tsx +63 -0
  53. package/src/components/editor/registry-action-contributions.tsx +80 -1
  54. package/src/components/editor/riser-diagram-panel.tsx +14 -7
  55. package/src/components/editor/room-controls.tsx +847 -0
  56. package/src/components/editor/room-floor-highlight.tsx +128 -0
  57. package/src/components/editor/room-handles-2d.tsx +354 -0
  58. package/src/components/editor/room-handles.tsx +675 -0
  59. package/src/components/editor/room-highlight.tsx +233 -0
  60. package/src/components/editor/room-labels.test.ts +850 -0
  61. package/src/components/editor/room-labels.tsx +1890 -0
  62. package/src/components/editor/room-transform-ghost.tsx +256 -0
  63. package/src/components/editor/room-webgpu-materials.test.ts +27 -0
  64. package/src/components/editor/selection-manager.tsx +474 -419
  65. package/src/components/editor/slab-hole-highlights.tsx +5 -2
  66. package/src/components/editor/snapshot-capture-overlay.tsx +7 -2
  67. package/src/components/editor/snapshot-capture.test.ts +119 -60
  68. package/src/components/editor/snapshot-capture.ts +36 -5
  69. package/src/components/editor/thumbnail-generator.test.ts +654 -0
  70. package/src/components/editor/thumbnail-generator.tsx +1103 -223
  71. package/src/components/editor/use-floorplan-background-placement.ts +34 -12
  72. package/src/components/editor/vector-edge-extractor.tsx +121 -0
  73. package/src/components/editor/views/host-view.tsx +61 -0
  74. package/src/components/editor/views/plugins-view.tsx +663 -0
  75. package/src/components/editor/views/scene-region.tsx +59 -0
  76. package/src/components/editor/views/use-editor-views.ts +57 -0
  77. package/src/components/editor/views/view-bar.test.tsx +54 -0
  78. package/src/components/editor/views/view-bar.tsx +343 -0
  79. package/src/components/editor/wall-justification.test.ts +140 -0
  80. package/src/components/editor/wall-move-side-handles.tsx +43 -142
  81. package/src/components/editor/wall-push-arrow.tsx +92 -0
  82. package/src/components/editor/wall-region-handles.tsx +509 -0
  83. package/src/components/editor/wall-snap-beacon-layer.tsx +8 -4
  84. package/src/components/editor-2d/floorplan-cursor-indicator-overlay.tsx +8 -4
  85. package/src/components/editor-2d/floorplan-group-move.tsx +3 -0
  86. package/src/components/editor-2d/floorplan-open-wall-ends-layer.test.tsx +86 -0
  87. package/src/components/editor-2d/floorplan-open-wall-ends-layer.tsx +290 -0
  88. package/src/components/editor-2d/floorplan-registered-tool-layer.tsx +2 -2
  89. package/src/components/editor-2d/floorplan-registry-action-menu.tsx +11 -1
  90. package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +116 -106
  91. package/src/components/editor-2d/renderers/floorplan-dimension-edit-overlay.test.ts +156 -0
  92. package/src/components/editor-2d/renderers/floorplan-dimension-edit-overlay.tsx +278 -0
  93. package/src/components/editor-2d/renderers/floorplan-dimension-renderer.tsx +77 -0
  94. package/src/components/editor-2d/renderers/{floorplan-draft-wall-measurement.tsx → floorplan-draft-measurement.tsx} +8 -8
  95. package/src/components/editor-2d/renderers/floorplan-geometry-renderer.tsx +4 -1
  96. package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +38 -8
  97. package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +319 -233
  98. package/src/components/editor-2d/renderers/floorplan-reveal.test.tsx +254 -0
  99. package/src/components/editor-2d/renderers/floorplan-reveal.ts +141 -0
  100. package/src/components/editor-2d/svg-paths.ts +12 -15
  101. package/src/components/point-ask/bubble.tsx +538 -0
  102. package/src/components/point-ask/hover-chip.tsx +91 -0
  103. package/src/components/point-ask/mode-pill.tsx +92 -0
  104. package/src/components/point-ask/optional-motion.ts +22 -0
  105. package/src/components/point-ask/outline-corners.tsx +93 -0
  106. package/src/components/point-ask/pins.tsx +711 -0
  107. package/src/components/point-ask/point-ask-layer.tsx +83 -0
  108. package/src/components/point-ask/point-ask-styles.ts +105 -0
  109. package/src/components/point-ask/region-marquee-styles.ts +15 -0
  110. package/src/components/point-ask/region-marquee.tsx +64 -0
  111. package/src/components/procedural-items/inspector.tsx +40 -15
  112. package/src/components/systems/ceiling/ceiling-bracket-batch.test.ts +12 -2
  113. package/src/components/systems/ceiling/ceiling-selection-affordance-system.test.ts +97 -14
  114. package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +18 -18
  115. package/src/components/systems/roof/roof-edit-system.tsx +1 -0
  116. package/src/components/systems/zone/zone-system.test.ts +47 -0
  117. package/src/components/systems/zone/zone-system.tsx +29 -68
  118. package/src/components/tools/distribution/connection-compatibility.test.ts +34 -0
  119. package/src/components/tools/distribution/connection-compatibility.ts +43 -0
  120. package/src/components/tools/distribution/distribution-public.ts +9 -0
  121. package/src/components/tools/distribution/distribution-run-contract.test.ts +26 -0
  122. package/src/components/tools/distribution/distribution-run-contract.ts +94 -0
  123. package/src/components/tools/distribution/distribution-run-tool.test.ts +241 -0
  124. package/src/components/tools/distribution/distribution-run-tool.test.tsx +125 -0
  125. package/src/components/tools/distribution/distribution-run-tool.tsx +1323 -0
  126. package/src/components/tools/distribution/draw-alignment.ts +35 -0
  127. package/src/components/tools/distribution/ports.test.ts +56 -0
  128. package/src/components/tools/distribution/ports.ts +378 -0
  129. package/src/components/tools/distribution/run-cursor.test.ts +135 -0
  130. package/src/components/tools/distribution/run-cursor.ts +36 -0
  131. package/src/components/tools/distribution/run-direction-feedback.test.ts +60 -0
  132. package/src/components/tools/distribution/run-direction-feedback.tsx +251 -0
  133. package/src/components/tools/distribution/run-port-snap.test.ts +79 -0
  134. package/src/components/tools/distribution/run-port-snap.ts +25 -0
  135. package/src/components/tools/distribution/run-screen-direction.test.ts +23 -0
  136. package/src/components/tools/distribution/run-screen-direction.ts +30 -0
  137. package/src/components/tools/elevator/elevator-tool.tsx +3 -6
  138. package/src/components/tools/elevator/move-elevator-tool.tsx +2 -3
  139. package/src/components/tools/item/item-surface-strategy-resolver.test.ts +67 -68
  140. package/src/components/tools/item/placement-history.test.tsx +512 -0
  141. package/src/components/tools/item/placement-strategies.test.ts +46 -1
  142. package/src/components/tools/item/placement-strategies.ts +11 -10
  143. package/src/components/tools/item/shelf-surface-strategy-resolver.test.ts +56 -56
  144. package/src/components/tools/item/use-draft-node.test.tsx +432 -1
  145. package/src/components/tools/item/use-draft-node.ts +166 -95
  146. package/src/components/tools/item/use-placement-coordinator.tsx +148 -95
  147. package/src/components/tools/paint-region/use-paint-tint.ts +37 -0
  148. package/src/components/tools/paint-region/wall-paint-region-tool.tsx +466 -0
  149. package/src/components/tools/registry/item-surface-move.test.ts +62 -63
  150. package/src/components/tools/registry/item-surface-move.ts +2 -0
  151. package/src/components/tools/registry/item-surface-resolver.test.ts +26 -26
  152. package/src/components/tools/registry/move-registry-node-tool.test.ts +268 -1
  153. package/src/components/tools/registry/move-registry-node-tool.tsx +66 -30
  154. package/src/components/tools/select/box-select-tool.tsx +2 -12
  155. package/src/components/tools/select/plane-box-select-tool.test.tsx +79 -0
  156. package/src/components/tools/select/plane-box-select-tool.tsx +35 -22
  157. package/src/components/tools/select/select-candidates.test.ts +37 -4
  158. package/src/components/tools/select/select-candidates.ts +15 -11
  159. package/src/components/tools/shared/cursor-sphere.tsx +36 -12
  160. package/src/components/tools/site/site-boundary-editor.tsx +1 -2
  161. package/src/components/tools/tool-manager.tsx +59 -24
  162. package/src/components/tools/wall/wall-drafting.test.ts +175 -47
  163. package/src/components/tools/zone/zone-tool.tsx +4 -12
  164. package/src/components/ui/action-menu/action-button.tsx +9 -1
  165. package/src/components/ui/action-menu/camera-actions.tsx +9 -4
  166. package/src/components/ui/action-menu/control-modes.tsx +15 -42
  167. package/src/components/ui/action-menu/follow-pascal-pill.tsx +32 -0
  168. package/src/components/ui/action-menu/follow-pascal-toggle.tsx +36 -0
  169. package/src/components/ui/action-menu/index.tsx +56 -33
  170. package/src/components/ui/action-menu/measurement-control.tsx +14 -19
  171. package/src/components/ui/action-menu/placement.ts +20 -0
  172. package/src/components/ui/action-menu/point-ask-button.tsx +52 -0
  173. package/src/components/ui/action-menu/view-toggles.tsx +47 -42
  174. package/src/components/ui/build-panel/build-panel.tsx +242 -0
  175. package/src/components/ui/command-palette/editor-commands.tsx +7 -42
  176. package/src/components/ui/command-palette/index.tsx +50 -39
  177. package/src/components/ui/command-palette/ranked-groups.test.ts +31 -0
  178. package/src/components/ui/controls/action-button.tsx +5 -2
  179. package/src/components/ui/controls/finish-swatch.tsx +110 -0
  180. package/src/components/ui/controls/material-paint-panel.tsx +97 -26
  181. package/src/components/ui/controls/material-picker.tsx +4 -1
  182. package/src/components/ui/controls/metric-control.tsx +1 -1
  183. package/src/components/ui/controls/scene-material-list.tsx +4 -32
  184. package/src/components/ui/controls/segmented-control.tsx +21 -8
  185. package/src/components/ui/controls/select-control.tsx +65 -0
  186. package/src/components/ui/controls/slider-control.tsx +1 -1
  187. package/src/components/ui/controls/toggle-control.tsx +3 -3
  188. package/src/components/ui/controls/tool-options-panel.tsx +1 -1
  189. package/src/components/ui/floating-level-selector.tsx +159 -59
  190. package/src/components/ui/helpers/building-helper.tsx +4 -1
  191. package/src/components/ui/helpers/contextual-helper-panel.tsx +181 -72
  192. package/src/components/ui/helpers/helper-manager.test.tsx +121 -0
  193. package/src/components/ui/helpers/helper-manager.tsx +331 -14
  194. package/src/components/ui/helpers/item-helper.tsx +17 -1
  195. package/src/components/ui/helpers/registered-tool-helper.tsx +8 -1
  196. package/src/components/ui/icon-ref.tsx +45 -1
  197. package/src/components/ui/level-duplicate-dialog.tsx +1 -1
  198. package/src/components/ui/node-icon-image.tsx +16 -0
  199. package/src/components/ui/panels/collections/collections-popover.tsx +28 -22
  200. package/src/components/ui/panels/floor-foundation-panel.tsx +551 -0
  201. package/src/components/ui/panels/floor-opening-panel.tsx +143 -0
  202. package/src/components/ui/panels/mobile-panel-sheet.tsx +4 -4
  203. package/src/components/ui/panels/mobile-selection-bar.tsx +8 -5
  204. package/src/components/ui/panels/multi-field-value.test.ts +26 -3
  205. package/src/components/ui/panels/multi-field-value.ts +6 -3
  206. package/src/components/ui/panels/multi-parametric-inspector.tsx +52 -2
  207. package/src/components/ui/panels/multi-selection-panel.tsx +161 -5
  208. package/src/components/ui/panels/node-display.ts +8 -3
  209. package/src/components/ui/panels/panel-manager.tsx +103 -68
  210. package/src/components/ui/panels/panel-wrapper.tsx +130 -62
  211. package/src/components/ui/panels/parametric-field-control.tsx +3 -3
  212. package/src/components/ui/panels/parametric-inspector.tsx +7 -7
  213. package/src/components/ui/panels/reference-panel.tsx +2 -3
  214. package/src/components/ui/panels/room-construction-rows.tsx +774 -0
  215. package/src/components/ui/panels/room-name-options.test.ts +55 -0
  216. package/src/components/ui/panels/room-name-options.ts +47 -0
  217. package/src/components/ui/panels/room-openings-field.tsx +110 -0
  218. package/src/components/ui/panels/room-panel.tsx +790 -0
  219. package/src/components/ui/panels/shape-choice.tsx +58 -0
  220. package/src/components/ui/panels/wall-region-list.tsx +109 -0
  221. package/src/components/ui/primitives/badge.tsx +8 -0
  222. package/src/components/ui/primitives/color-dot.tsx +36 -24
  223. package/src/components/ui/primitives/sidebar.tsx +3 -1
  224. package/src/components/ui/right-stack.test.ts +29 -0
  225. package/src/components/ui/right-stack.tsx +164 -0
  226. package/src/components/ui/sidebar/app-sidebar.tsx +1 -1
  227. package/src/components/ui/sidebar/icon-rail.tsx +8 -3
  228. package/src/components/ui/sidebar/panels/items-panel/function-tree-panel.tsx +1 -1
  229. package/src/components/ui/sidebar/panels/items-panel/index.tsx +6 -6
  230. package/src/components/ui/sidebar/panels/settings-panel/animation-speed-setting.tsx +40 -0
  231. package/src/components/ui/sidebar/panels/settings-panel/audio-settings-dialog.tsx +7 -8
  232. package/src/components/ui/sidebar/panels/settings-panel/construction-animation-setting.test.tsx +170 -0
  233. package/src/components/ui/sidebar/panels/settings-panel/construction-animation-setting.tsx +92 -0
  234. package/src/components/ui/sidebar/panels/settings-panel/index.tsx +138 -27
  235. package/src/components/ui/sidebar/panels/settings-panel/interface-theme.test.tsx +20 -0
  236. package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +5 -5
  237. package/src/components/ui/sidebar/panels/settings-panel/print-export-button.tsx +4 -10
  238. package/src/components/ui/sidebar/panels/site-panel/building-tree-node.tsx +3 -11
  239. package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +4 -4
  240. package/src/components/ui/sidebar/panels/site-panel/chimney-tree-node.tsx +1 -5
  241. package/src/components/ui/sidebar/panels/site-panel/collection-tree-node.tsx +197 -0
  242. package/src/components/ui/sidebar/panels/site-panel/column-tree-node.tsx +1 -5
  243. package/src/components/ui/sidebar/panels/site-panel/door-tree-node.tsx +1 -5
  244. package/src/components/ui/sidebar/panels/site-panel/dormer-tree-node.tsx +1 -5
  245. package/src/components/ui/sidebar/panels/site-panel/elevator-tree-node.tsx +1 -5
  246. package/src/components/ui/sidebar/panels/site-panel/fence-tree-node.tsx +24 -11
  247. package/src/components/ui/sidebar/panels/site-panel/gutter-tree-node.tsx +1 -5
  248. package/src/components/ui/sidebar/panels/site-panel/index.tsx +181 -223
  249. package/src/components/ui/sidebar/panels/site-panel/inline-rename-input.tsx +30 -8
  250. package/src/components/ui/sidebar/panels/site-panel/item-tree-node.tsx +1 -3
  251. package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +4 -1
  252. package/src/components/ui/sidebar/panels/site-panel/registry-tree-node.tsx +1 -3
  253. package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +1 -4
  254. package/src/components/ui/sidebar/panels/site-panel/shelf-tree-node.tsx +1 -3
  255. package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +21 -13
  256. package/src/components/ui/sidebar/panels/site-panel/solar-panel-tree-node.tsx +1 -5
  257. package/src/components/ui/sidebar/panels/site-panel/spawn-tree-node.tsx +1 -5
  258. package/src/components/ui/sidebar/panels/site-panel/stair-tree-node.tsx +1 -5
  259. package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +13 -37
  260. package/src/components/ui/sidebar/panels/site-panel/unit-tree-node.tsx +2 -3
  261. package/src/components/ui/sidebar/panels/site-panel/wall-tree-node.tsx +1 -5
  262. package/src/components/ui/sidebar/panels/site-panel/window-tree-node.tsx +1 -5
  263. package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +9 -4
  264. package/src/components/ui/sidebar/panels/zone-panel/index.tsx +2 -4
  265. package/src/components/ui/sidebar/tab-bar.tsx +14 -4
  266. package/src/components/ui/sidebar/use-plugin-panels.tsx +15 -5
  267. package/src/components/viewer/floorplan-compass-button.tsx +9 -1
  268. package/src/components/viewer/floorplan-preview.tsx +5 -6
  269. package/src/components/viewer/viewer-controls-bar.tsx +18 -6
  270. package/src/components/viewer/viewer-hover-label.tsx +34 -0
  271. package/src/components/viewer/viewer-rooms.tsx +191 -0
  272. package/src/components/viewer/viewer-scene-header.tsx +181 -145
  273. package/src/components/viewer/viewer-selection-manager.tsx +124 -0
  274. package/src/components/viewer/viewer-stage.tsx +2 -2
  275. package/src/components/viewer-zone-system.tsx +3 -8
  276. package/src/hooks/use-auto-save.integration.test.tsx +215 -43
  277. package/src/hooks/use-auto-save.ts +60 -9
  278. package/src/hooks/use-ceiling-events.test.ts +123 -118
  279. package/src/hooks/use-grid-events.test.ts +105 -105
  280. package/src/hooks/use-keyboard.test.ts +243 -19
  281. package/src/hooks/use-keyboard.ts +118 -83
  282. package/src/hooks/use-open-wall-ends.test.ts +99 -0
  283. package/src/hooks/use-open-wall-ends.ts +238 -0
  284. package/src/hooks/use-placement-notice.test.ts +71 -0
  285. package/src/hooks/use-placement-notice.ts +73 -0
  286. package/src/hooks/use-selected-room.ts +143 -0
  287. package/src/index.tsx +312 -58
  288. package/src/lib/__fixtures__/owner-victor-seattle-parking.json +1 -0
  289. package/src/lib/__fixtures__/owner-villa-aurelia-basement.json +1 -0
  290. package/src/lib/add-storey.test.ts +177 -0
  291. package/src/lib/add-storey.ts +59 -0
  292. package/src/lib/below-grade.test.tsx +171 -0
  293. package/src/lib/below-grade.ts +133 -0
  294. package/src/lib/camera-facing.test.ts +24 -0
  295. package/src/lib/camera-facing.ts +24 -0
  296. package/src/lib/ceiling-edit-session.test.ts +321 -0
  297. package/src/lib/ceiling-edit-session.ts +206 -0
  298. package/src/lib/ceiling-region-session.test.ts +179 -0
  299. package/src/lib/continuation.test.ts +19 -1
  300. package/src/lib/continuation.ts +25 -4
  301. package/src/lib/direct-manipulation.ts +2 -0
  302. package/src/lib/door-interaction.ts +14 -0
  303. package/src/lib/download-project-file.ts +17 -0
  304. package/src/lib/editor-level-display.test.ts +96 -0
  305. package/src/lib/editor-level-display.ts +55 -0
  306. package/src/lib/editor-views.ts +178 -0
  307. package/src/lib/export-geometry-wait.test.ts +90 -0
  308. package/src/lib/export-texture-utils.ts +2 -4
  309. package/src/lib/floor-edit-session.ts +153 -0
  310. package/src/lib/floor-footprints.test.ts +563 -0
  311. package/src/lib/floor-footprints.ts +395 -0
  312. package/src/lib/floor-foundation-support.test.ts +284 -0
  313. package/src/lib/floor-opening-draft.ts +218 -0
  314. package/src/lib/floor-openings.test.ts +216 -0
  315. package/src/lib/floor-region-geometry.ts +191 -0
  316. package/src/lib/floor-region-session.test.ts +485 -0
  317. package/src/lib/floor-region-session.ts +589 -0
  318. package/src/lib/floor-region-snap.ts +101 -0
  319. package/src/lib/floor-step-hover.test.ts +32 -0
  320. package/src/lib/floorplan/annotation-visibility.ts +17 -0
  321. package/src/lib/floorplan/dimension-drive.test.ts +360 -0
  322. package/src/lib/floorplan/dimension-drive.ts +479 -0
  323. package/src/lib/floorplan/floorplan-export.test.ts +12 -8
  324. package/src/lib/floorplan/floorplan-export.tsx +32 -8
  325. package/src/lib/floorplan/floorplan-extension.ts +32 -0
  326. package/src/lib/floorplan/floorplan-mode.test.ts +4 -0
  327. package/src/lib/floorplan/floorplan-mode.ts +2 -0
  328. package/src/lib/floorplan/floorplan-pdfkit-document.ts +363 -27
  329. package/src/lib/floorplan/floorplan-pdfkit-renderer.test.ts +177 -8
  330. package/src/lib/floorplan/floorplan-pdfkit-renderer.ts +60 -25
  331. package/src/lib/floorplan/floorplan-plugin-install.test.tsx +138 -0
  332. package/src/lib/floorplan/floorplan-readonly.ts +6 -0
  333. package/src/lib/floorplan/geometry.ts +13 -6
  334. package/src/lib/floorplan/index.ts +0 -9
  335. package/src/lib/floorplan/items.ts +5 -2
  336. package/src/lib/floorplan/marks.test.ts +213 -0
  337. package/src/lib/floorplan/marks.ts +238 -0
  338. package/src/lib/floorplan/open-wall-ends.test.ts +268 -0
  339. package/src/lib/floorplan/open-wall-ends.ts +162 -0
  340. package/src/lib/floorplan/pdf-fonts/GeistMono-Bold.ttf +0 -0
  341. package/src/lib/floorplan/pdf-fonts/GeistMono-Regular.ttf +0 -0
  342. package/src/lib/floorplan/pdf-fonts/LICENSE-GeistMono.txt +92 -0
  343. package/src/lib/floorplan/pdf-fonts/LICENSE-LiberationSans.txt +102 -0
  344. package/src/lib/floorplan/pdf-fonts/LiberationSans-Bold.ttf +0 -0
  345. package/src/lib/floorplan/pdf-fonts/LiberationSans-Regular.ttf +0 -0
  346. package/src/lib/floorplan/pdfkit-standard-fonts.d.ts +5 -0
  347. package/src/lib/floorplan/schedules.test.ts +189 -0
  348. package/src/lib/floorplan/schedules.ts +339 -0
  349. package/src/lib/floorplan/sheet-export.test.ts +124 -0
  350. package/src/lib/floorplan/sheet-export.ts +318 -0
  351. package/src/lib/floorplan/site-plan/build-site-plan-drawing.ts +768 -0
  352. package/src/lib/floorplan/site-plan/contours.test.ts +43 -0
  353. package/src/lib/floorplan/site-plan/contributors.ts +97 -0
  354. package/src/lib/floorplan/site-plan/coverage.ts +230 -0
  355. package/src/lib/floorplan/site-plan/drawing-type-switch.test.ts +15 -0
  356. package/src/lib/floorplan/site-plan/drawing-type-switch.tsx +86 -0
  357. package/src/lib/floorplan/site-plan/front-edge.test.ts +103 -0
  358. package/src/lib/floorplan/site-plan/front-edge.ts +157 -0
  359. package/src/lib/floorplan/site-plan/geometry.test.ts +269 -0
  360. package/src/lib/floorplan/site-plan/geometry.ts +142 -0
  361. package/src/lib/floorplan/site-plan/index.ts +55 -0
  362. package/src/lib/floorplan/site-plan/site-annotations.test.ts +483 -0
  363. package/src/lib/floorplan/site-plan/site-annotations.ts +1084 -0
  364. package/src/lib/floorplan/site-plan/site-parts.ts +287 -0
  365. package/src/lib/floorplan/site-plan/site-plan-layer.tsx +138 -0
  366. package/src/lib/floorplan/site-plan/street-name.test.ts +69 -0
  367. package/src/lib/floorplan/site-plan/street-name.ts +302 -0
  368. package/src/lib/floorplan/site-plan/yards.test.ts +70 -0
  369. package/src/lib/floorplan/types.ts +1 -27
  370. package/src/lib/follow-pascal-exterior.test-fixture.ts +17 -0
  371. package/src/lib/follow-pascal-exterior.test.ts +171 -0
  372. package/src/lib/follow-pascal-exterior.ts +39 -0
  373. package/src/lib/follow-pascal-presentation.test.ts +203 -0
  374. package/src/lib/follow-pascal-presentation.ts +129 -0
  375. package/src/lib/follow-pascal-sight.ts +84 -0
  376. package/src/lib/follow-pascal-targets.test.ts +406 -0
  377. package/src/lib/follow-pascal-targets.ts +198 -0
  378. package/src/lib/follow-pascal.test.ts +350 -0
  379. package/src/lib/follow-pascal.ts +284 -0
  380. package/src/lib/fresh-planar-placement.test.ts +148 -0
  381. package/src/lib/fresh-planar-placement.ts +114 -37
  382. package/src/lib/geometry-script/author.ts +157 -0
  383. package/src/lib/geometry-script/client.ts +159 -0
  384. package/src/lib/geometry-script/finish-scripts.ts +127 -0
  385. package/src/lib/geometry-script/protocol.ts +13 -0
  386. package/src/lib/gesture-lifecycle.test.ts +595 -0
  387. package/src/lib/gesture-lifecycle.ts +290 -0
  388. package/src/lib/glb-export-zone-holes.test.ts +41 -0
  389. package/src/lib/glb-export.test.ts +636 -126
  390. package/src/lib/glb-export.ts +241 -120
  391. package/src/lib/history.test.ts +18 -7
  392. package/src/lib/history.ts +7 -0
  393. package/src/lib/hud-title.ts +109 -0
  394. package/src/lib/hydration-gated-storage.ts +49 -0
  395. package/src/lib/ifc-export.test.ts +174 -0
  396. package/src/lib/ifc-export.ts +201 -0
  397. package/src/lib/ifc-parts.ts +15 -0
  398. package/src/lib/inspector-expanded.test.ts +44 -0
  399. package/src/lib/inspector-expanded.ts +36 -0
  400. package/src/lib/interaction/scope.ts +22 -0
  401. package/src/lib/keyboard-pan.test.ts +129 -7
  402. package/src/lib/keyboard-pan.ts +161 -12
  403. package/src/lib/level-selection.test.ts +57 -0
  404. package/src/lib/level-selection.ts +16 -1
  405. package/src/lib/live-gesture-round-trip.test.ts +360 -0
  406. package/src/lib/lot/__fixtures__/map-dossier-miami-shores.json +1 -0
  407. package/src/lib/lot/__fixtures__/map-dossier-stpete.json +1 -0
  408. package/src/lib/lot/clean-ring.test.ts +187 -0
  409. package/src/lib/lot/clean-ring.ts +217 -0
  410. package/src/lib/lot/dossier.test.ts +202 -0
  411. package/src/lib/lot/dossier.ts +498 -0
  412. package/src/lib/lot/drop-in.test.ts +326 -0
  413. package/src/lib/lot/drop-in.ts +392 -0
  414. package/src/lib/lot/index.ts +43 -0
  415. package/src/lib/lot/lot-patch.test.ts +340 -0
  416. package/src/lib/lot/lot-patch.ts +394 -0
  417. package/src/lib/lot/parcel-provider.ts +43 -0
  418. package/src/lib/lot/terrain.test.ts +161 -0
  419. package/src/lib/lot/terrain.ts +266 -0
  420. package/src/lib/material-catalog-model.ts +3 -1
  421. package/src/lib/material-paint-panel-model.ts +9 -5
  422. package/src/lib/material-paint.ts +4 -0
  423. package/src/lib/measurement-kind.test.ts +6 -8
  424. package/src/lib/measurements.test.ts +23 -0
  425. package/src/lib/mezzanine-actions.test.ts +342 -0
  426. package/src/lib/mezzanine-draft.test.ts +501 -0
  427. package/src/lib/mezzanine-draft.ts +278 -0
  428. package/src/lib/mezzanine-messages.ts +26 -0
  429. package/src/lib/mezzanine-selection.test.ts +207 -0
  430. package/src/lib/mezzanine-stairs.ts +46 -0
  431. package/src/lib/model-export.ts +3 -1
  432. package/src/lib/node-action-movement.ts +17 -6
  433. package/src/lib/node-display-label.ts +37 -0
  434. package/src/lib/orbit-floor-clearance.test.ts +201 -0
  435. package/src/lib/orbit-floor-clearance.ts +154 -0
  436. package/src/lib/paint-commit-route.test.ts +37 -0
  437. package/src/lib/paint-commit-route.ts +17 -0
  438. package/src/lib/paint-eyedropper.test.tsx +165 -0
  439. package/src/lib/paint-eyedropper.ts +99 -0
  440. package/src/lib/paint-pass-through.test.ts +122 -0
  441. package/src/lib/paint-pass-through.ts +78 -0
  442. package/src/lib/paint-region-hover.ts +25 -0
  443. package/src/lib/paint-region-mode.ts +154 -0
  444. package/src/lib/paint-regions.test.ts +166 -0
  445. package/src/lib/paint-regions.ts +192 -0
  446. package/src/lib/paint-scope.test.ts +317 -35
  447. package/src/lib/paint-scope.ts +267 -25
  448. package/src/lib/panel-rows.ts +21 -0
  449. package/src/lib/parametric-node-update.ts +16 -7
  450. package/src/lib/plate-paint-affected.test.ts +300 -0
  451. package/src/lib/plate-paint-affected.ts +177 -0
  452. package/src/lib/plugin-chat.ts +65 -0
  453. package/src/lib/plugin-panels.test.ts +25 -0
  454. package/src/lib/plugin-panels.ts +94 -0
  455. package/src/lib/point-ask/__fixtures__/consumer-browser.tsx +76 -0
  456. package/src/lib/point-ask/__fixtures__/house.ts +81 -0
  457. package/src/lib/point-ask/ask-created.test.ts +195 -0
  458. package/src/lib/point-ask/ask-flow.test.ts +380 -0
  459. package/src/lib/point-ask/build-context.test.ts +173 -0
  460. package/src/lib/point-ask/build-context.ts +94 -0
  461. package/src/lib/point-ask/capture-crop.ts +129 -0
  462. package/src/lib/point-ask/choreography.ts +69 -0
  463. package/src/lib/point-ask/compare.test.ts +145 -0
  464. package/src/lib/point-ask/compare.ts +73 -0
  465. package/src/lib/point-ask/describe-target.test.ts +165 -0
  466. package/src/lib/point-ask/describe-target.ts +234 -0
  467. package/src/lib/point-ask/dictation.ts +79 -0
  468. package/src/lib/point-ask/edge-marker.test.ts +61 -0
  469. package/src/lib/point-ask/edge-marker.ts +27 -0
  470. package/src/lib/point-ask/frame-loop.ts +25 -0
  471. package/src/lib/point-ask/history-marks.test.ts +94 -0
  472. package/src/lib/point-ask/history-marks.ts +39 -0
  473. package/src/lib/point-ask/host-api.ts +32 -0
  474. package/src/lib/point-ask/outline-box.test.ts +30 -0
  475. package/src/lib/point-ask/outline-box.ts +11 -0
  476. package/src/lib/point-ask/overlay-metrics.ts +6 -0
  477. package/src/lib/point-ask/pick.test.ts +220 -0
  478. package/src/lib/point-ask/pick.ts +121 -0
  479. package/src/lib/point-ask/pin-machine.test.ts +494 -0
  480. package/src/lib/point-ask/pin-machine.ts +217 -0
  481. package/src/lib/point-ask/placement.test.ts +203 -0
  482. package/src/lib/point-ask/placement.ts +141 -0
  483. package/src/lib/point-ask/pointer-tracker.ts +34 -0
  484. package/src/lib/point-ask/projector.ts +83 -0
  485. package/src/lib/point-ask/region.test.ts +157 -0
  486. package/src/lib/point-ask/region.ts +136 -0
  487. package/src/lib/point-ask/retry.test.ts +217 -0
  488. package/src/lib/point-ask/retry.ts +37 -0
  489. package/src/lib/point-ask/scene-commit.test.ts +312 -0
  490. package/src/lib/point-ask/scene-commit.ts +32 -0
  491. package/src/lib/point-ask/session.ts +1391 -0
  492. package/src/lib/point-ask/spring-keyframes.test.ts +45 -0
  493. package/src/lib/point-ask/spring-keyframes.ts +38 -0
  494. package/src/lib/point-ask/spring.test.ts +63 -0
  495. package/src/lib/point-ask/spring.ts +107 -0
  496. package/src/lib/point-ask/suggestions.test.ts +80 -0
  497. package/src/lib/point-ask/suggestions.ts +125 -0
  498. package/src/lib/point-ask/target-scope.test.ts +622 -0
  499. package/src/lib/point-ask/target-scope.ts +338 -0
  500. package/src/lib/point-ask/types.ts +48 -0
  501. package/src/lib/portable-export.test.ts +34 -1
  502. package/src/lib/portable-export.ts +119 -53
  503. package/src/lib/print-feature-thickness.ts +10 -1
  504. package/src/lib/print-roof-solids.test.ts +39 -1
  505. package/src/lib/print-roof-solids.ts +43 -10
  506. package/src/lib/print-shell-compiler-manifold-core.ts +1 -1
  507. package/src/lib/print-shell-compiler.ts +1 -0
  508. package/src/lib/registered-draft-snap.ts +12 -0
  509. package/src/lib/roof-duplication.ts +10 -2
  510. package/src/lib/room-assembly-overlay.test.ts +275 -0
  511. package/src/lib/room-assembly-overlay.ts +265 -0
  512. package/src/lib/room-built-on.test.ts +379 -0
  513. package/src/lib/room-built-on.ts +138 -0
  514. package/src/lib/room-construction-commands.ts +712 -0
  515. package/src/lib/room-construction.test.ts +746 -0
  516. package/src/lib/room-construction.ts +385 -0
  517. package/src/lib/room-controls-phase6.test.ts +630 -0
  518. package/src/lib/room-divide-session.ts +523 -0
  519. package/src/lib/room-handle-drag.ts +1068 -0
  520. package/src/lib/room-name-catalog.test.ts +90 -0
  521. package/src/lib/room-name-catalog.ts +168 -0
  522. package/src/lib/room-push-dimensions.test.ts +311 -0
  523. package/src/lib/room-push-dimensions.ts +251 -0
  524. package/src/lib/room-selection-commands.ts +62 -0
  525. package/src/lib/room-selection.test.ts +1017 -0
  526. package/src/lib/room-selection.ts +417 -0
  527. package/src/lib/room-structure-commands.ts +94 -0
  528. package/src/lib/room-structure.test.ts +921 -0
  529. package/src/lib/room-transform-session.ts +570 -0
  530. package/src/lib/room-zone-routing.test.ts +294 -0
  531. package/src/lib/room-zone-routing.ts +197 -0
  532. package/src/lib/scene-clipboard.test.ts +610 -1
  533. package/src/lib/scene-clipboard.ts +269 -58
  534. package/src/lib/scene-material-refs.test.ts +79 -0
  535. package/src/lib/scene-material-refs.ts +79 -0
  536. package/src/lib/scene.test.ts +24 -36
  537. package/src/lib/scene.ts +33 -57
  538. package/src/lib/selection-routing.test.ts +129 -45
  539. package/src/lib/selection-routing.ts +54 -75
  540. package/src/lib/session-writes.test.ts +120 -0
  541. package/src/lib/session-writes.ts +205 -0
  542. package/src/lib/sfx-bus.ts +13 -0
  543. package/src/lib/sfx-player.test.ts +70 -9
  544. package/src/lib/sfx-player.ts +6 -0
  545. package/src/lib/sidebar-panel.ts +62 -0
  546. package/src/lib/site-boundary.test.ts +4 -6
  547. package/src/lib/site-boundary.ts +1 -1
  548. package/src/lib/snapping-mode.test.ts +4 -0
  549. package/src/lib/snapping-mode.ts +8 -1
  550. package/src/lib/stair-levels.test.ts +25 -0
  551. package/src/lib/stair-levels.ts +22 -3
  552. package/src/lib/terrace-draft.test.ts +145 -0
  553. package/src/lib/terrace-draft.ts +214 -0
  554. package/src/lib/tool-cursor-icon.test.ts +16 -0
  555. package/src/lib/tool-cursor-icon.ts +11 -0
  556. package/src/lib/typing-target.ts +9 -0
  557. package/src/lib/units.ts +77 -39
  558. package/src/lib/vector-edges.test.ts +58 -0
  559. package/src/lib/vector-edges.ts +400 -0
  560. package/src/lib/viewer-selection.test.ts +577 -0
  561. package/src/lib/viewer-selection.ts +580 -0
  562. package/src/lib/wall-draw-variant.test.ts +71 -0
  563. package/src/lib/wall-draw-variant.ts +91 -0
  564. package/src/lib/wall-paint-region-session.ts +246 -0
  565. package/src/lib/wall-paint-region.test.ts +373 -0
  566. package/src/lib/wall-placement-pointer.test.ts +42 -0
  567. package/src/lib/wall-placement-pointer.ts +57 -0
  568. package/src/lib/wall-polygon-draft.test.ts +262 -0
  569. package/src/lib/wall-polygon-draft.ts +286 -0
  570. package/src/lib/wall-push.test.ts +453 -0
  571. package/src/lib/wall-region-face.ts +352 -0
  572. package/src/lib/wall-region-handles.test.ts +304 -0
  573. package/src/lib/wall-region-handles.ts +368 -0
  574. package/src/lib/wall-region-list.test.ts +119 -0
  575. package/src/lib/wall-region-list.ts +136 -0
  576. package/src/lib/wall-region-snap.ts +110 -0
  577. package/src/lib/window-changes.ts +35 -0
  578. package/src/lib/window-interaction.ts +14 -0
  579. package/src/lib/zone-content.ts +4 -1
  580. package/src/lib/zone-name.test.ts +17 -0
  581. package/src/lib/zone-name.ts +17 -0
  582. package/src/store/opens-in-select-mode.test.ts +223 -0
  583. package/src/store/paint-mode.test.ts +188 -0
  584. package/src/store/terrain-sculpt-mode.test.ts +19 -21
  585. package/src/store/tool-mode.test.ts +137 -6
  586. package/src/store/use-audio.test.ts +146 -0
  587. package/src/store/use-audio.tsx +127 -3
  588. package/src/store/use-construction-reveal.test.ts +173 -0
  589. package/src/store/use-construction-reveal.ts +145 -0
  590. package/src/store/use-corner-snap-hint.test.ts +24 -0
  591. package/src/store/use-corner-snap-hint.ts +47 -0
  592. package/src/store/use-delete-confirmation.ts +28 -1
  593. package/src/store/use-drawing-view.test.ts +24 -3
  594. package/src/store/use-drawing-view.ts +29 -1
  595. package/src/store/use-editor.tsx +432 -321
  596. package/src/store/use-floorplan-draft-preview.ts +13 -0
  597. package/src/store/use-floorplan-marquee.ts +12 -2
  598. package/src/store/use-follow-pascal.test.ts +62 -0
  599. package/src/store/use-follow-pascal.ts +53 -0
  600. package/src/store/use-point-ask.ts +247 -0
  601. package/src/store/use-quick-measurement-hud.ts +3 -3
  602. package/src/store/use-stair-build-preview.ts +33 -5
  603. package/src/store/use-wall-move-ghosts.ts +4 -2
  604. package/src/store/use-wall-region-handles-selection.test.ts +222 -0
  605. package/src/store/use-wall-region-handles-selection.ts +139 -0
  606. package/src/store/view-layout.test.ts +201 -0
  607. package/src/store/view-layout.ts +199 -0
  608. package/src/test-utils/immediate-animation-frames.ts +16 -0
  609. package/src/test-utils/live-gesture-round-trip.ts +224 -0
  610. package/src/test-utils/render-current-markup.tsx +16 -0
  611. package/src/test-utils/selection-harness.ts +82 -0
  612. package/src/components/editor-2d/renderers/floorplan-stair-layer.tsx +0 -499
  613. package/src/components/systems/zone/zone-label-editor-system.tsx +0 -333
  614. package/src/components/tools/fence/fence-drafting.test.ts +0 -100
  615. package/src/components/tools/fence/fence-drafting.ts +0 -266
  616. package/src/components/tools/stair/stair-click-guard.test.ts +0 -58
  617. package/src/components/tools/stair/stair-click-guard.ts +0 -72
  618. package/src/components/tools/stair/stair-defaults.ts +0 -18
  619. package/src/components/tools/stair/stair-tool.tsx +0 -634
  620. package/src/components/ui/sidebar/panels/plugins-panel.tsx +0 -218
  621. package/src/components/viewer/viewer-units-panel.tsx +0 -116
  622. package/src/lib/floorplan/stairs.ts +0 -489
  623. package/src/lib/level-duplication.test.ts +0 -144
  624. package/src/lib/level-duplication.ts +0 -195
@@ -0,0 +1,774 @@
1
+ 'use client'
2
+
3
+ import {
4
+ type AnyNodeId,
5
+ floorFootprintName,
6
+ floorFootprintSupportClass,
7
+ floorPlateAtGroundContact,
8
+ MIN_GROUND_FLOOR_THICKNESS,
9
+ MIN_SLAB_THICKNESS,
10
+ roomDrawnFloor,
11
+ useScene,
12
+ } from '@pascal-app/core'
13
+ import { useViewer } from '@pascal-app/viewer'
14
+ import { Check, ChevronDown, ChevronRight } from 'lucide-react'
15
+ import Image from 'next/image'
16
+ import { type ReactNode, useMemo, useState } from 'react'
17
+ import { useShallow } from 'zustand/react/shallow'
18
+ import { startCeilingEdit } from '../../../lib/ceiling-edit-session'
19
+ import {
20
+ applyRoomFloorConstruction,
21
+ floorLift,
22
+ foundationHeight,
23
+ openFloorFoundation,
24
+ roomFloorOwner,
25
+ roomFootprint,
26
+ } from '../../../lib/floor-footprints'
27
+ import { formatLinearMeasurement } from '../../../lib/measurements'
28
+ import { startMezzanineDraft } from '../../../lib/mezzanine-draft'
29
+ import { resolveRoomAssemblyHeights } from '../../../lib/room-assembly-overlay'
30
+ import { roomBuiltOn, separateFloorSummary } from '../../../lib/room-built-on'
31
+ import type { RoomConstructionState } from '../../../lib/room-construction'
32
+ import { roomConstructionState } from '../../../lib/room-construction'
33
+ import {
34
+ addRoomConstruction,
35
+ adoptExistingCeiling,
36
+ floorHeightRefusalText,
37
+ lockRoomOutsideFaces,
38
+ type RoomConstructionPart,
39
+ type RoomConstructionResult,
40
+ removeRoomConstruction,
41
+ replaceRoomCeiling,
42
+ roomFloorBase,
43
+ roomOutsideFacesLocked,
44
+ roomRelativeFloorHeight,
45
+ separateFloorOffset,
46
+ setRoomFloor,
47
+ setRoomRelativeFloorHeight,
48
+ unlockRoomOutsideFaces,
49
+ } from '../../../lib/room-construction-commands'
50
+ import { ownFloorElevationBounds } from '../../../lib/room-handle-drag'
51
+ import type { RoomSelectionRecord } from '../../../lib/room-selection'
52
+ import { cn } from '../../../lib/utils'
53
+ import { MetricControl } from '../controls/metric-control'
54
+ import {
55
+ DropdownMenu,
56
+ DropdownMenuContent,
57
+ DropdownMenuItem,
58
+ DropdownMenuSeparator,
59
+ DropdownMenuTrigger,
60
+ } from '../primitives/dropdown-menu'
61
+ import { Switch } from '../primitives/switch'
62
+ import { Tooltip, TooltipContent, TooltipTrigger } from '../primitives/tooltip'
63
+ import { RoomOpeningsField } from './room-openings-field'
64
+ import { ShapeChoice } from './shape-choice'
65
+
66
+ /** The buttons a row offers — derived together with its presence. */
67
+ export function constructionActions(state: RoomConstructionState, part: RoomConstructionPart) {
68
+ return state[part].actions
69
+ }
70
+
71
+ /**
72
+ * The Walls switch reads three ways. A full ring is on and a click takes the
73
+ * room's own walls away (shared ones stay); a partial ring shows half on and
74
+ * a click closes every open side; no walls is off and a click builds them.
75
+ */
76
+ export function wallsSwitch(state: RoomConstructionState) {
77
+ const { state: presence, actions } = state.walls
78
+ const full = presence === 'present'
79
+ return {
80
+ checked: full,
81
+ mixed: presence === 'partial',
82
+ command: full ? ('remove' as const) : ('add' as const),
83
+ disabled: full ? !actions.remove : !actions.add,
84
+ }
85
+ }
86
+
87
+ export const pill =
88
+ 'rounded-full border border-border/50 px-2.5 py-0.5 text-xs transition-colors hover:bg-accent disabled:pointer-events-none disabled:opacity-50'
89
+
90
+ const count = (n: number, one: string, many = `${one}s`) => `${n} ${n === 1 ? one : many}`
91
+
92
+ export const DRAWN_FLOOR_SWITCH_HINT = 'This floor is a drawn slab: select it to edit or delete it.'
93
+
94
+ /**
95
+ * One "Built with" row: its icon, name and one-line summary, a chevron that
96
+ * opens its settings, and the switch (or button) on the right. An excluded
97
+ * part reads "Not included" and has nothing to open.
98
+ */
99
+ export function ConstructionPart({
100
+ part,
101
+ label,
102
+ icon,
103
+ on,
104
+ meta,
105
+ expanded,
106
+ onToggleExpanded,
107
+ control,
108
+ footer,
109
+ children,
110
+ }: {
111
+ part: string
112
+ label: string
113
+ icon: string
114
+ on: boolean
115
+ meta: string
116
+ expanded: boolean
117
+ onToggleExpanded: () => void
118
+ /** The switch, or a button, at the end of the row. */
119
+ control?: ReactNode
120
+ /** Always shown under the row (open or not). */
121
+ footer?: ReactNode
122
+ /** The settings the chevron opens; none means the row does not open. */
123
+ children?: ReactNode
124
+ }) {
125
+ const opens = on && !!children
126
+ const open = opens && expanded
127
+ return (
128
+ <div
129
+ className={cn(
130
+ 'rounded-lg bg-accent/30 transition-colors hover:bg-accent/50',
131
+ open && 'bg-accent/50',
132
+ )}
133
+ data-construction-row={part}
134
+ data-expanded={open || undefined}
135
+ >
136
+ <div className="flex items-center gap-1 pr-2.5">
137
+ <button
138
+ aria-expanded={opens ? open : undefined}
139
+ className="flex min-w-0 flex-1 items-center gap-2.5 rounded-lg py-2 pr-1 pl-2.5 text-left disabled:cursor-default"
140
+ disabled={!opens}
141
+ onClick={onToggleExpanded}
142
+ type="button"
143
+ >
144
+ <Image
145
+ alt=""
146
+ className={cn('shrink-0 object-contain', !on && 'opacity-40')}
147
+ height={20}
148
+ src={icon}
149
+ width={20}
150
+ />
151
+ <span className="flex min-w-0 flex-1 flex-col">
152
+ <span className="font-medium text-[13px] text-foreground">{label}</span>
153
+ <span className="text-pretty text-muted-foreground text-xs">
154
+ {on ? meta : 'Not included'}
155
+ </span>
156
+ </span>
157
+ {opens && (
158
+ <ChevronRight
159
+ className={cn(
160
+ 'size-3.5 shrink-0 text-muted-foreground transition-transform',
161
+ open && 'rotate-90',
162
+ )}
163
+ />
164
+ )}
165
+ </button>
166
+ {control}
167
+ </div>
168
+ {footer}
169
+ {open && (
170
+ <div className="flex flex-col gap-2 pr-2.5 pb-3 pl-[42px] text-muted-foreground text-xs">
171
+ {children}
172
+ </div>
173
+ )}
174
+ </div>
175
+ )
176
+ }
177
+
178
+ /** A label and its value (or control) on one line inside an open row. */
179
+ export function PartField({ label, children }: { label: string; children: ReactNode }) {
180
+ return (
181
+ <div className="flex min-h-7 items-center justify-between gap-3">
182
+ <span>{label}</span>
183
+ {children}
184
+ </div>
185
+ )
186
+ }
187
+
188
+ /**
189
+ * The room floor's height above the floor it stands on (its footprint's floor;
190
+ * for a mezzanine, the room below): typing 0.15 on a raised house lifts the
191
+ * room 15 cm above the house floor. A refused height says why, right here.
192
+ */
193
+ export function FloorHeightField({
194
+ zoneId,
195
+ bounds,
196
+ }: {
197
+ zoneId: string
198
+ /** Allowed heights, in the same relative terms. */
199
+ bounds?: { min: number; max: number } | null
200
+ }) {
201
+ const height = useScene((scene) => roomRelativeFloorHeight(scene.nodes, zoneId))
202
+ const unit = useViewer((s) => s.unit)
203
+ const metricNotation = useViewer((s) => s.metricNotation)
204
+ const length = (meters: number) => formatLinearMeasurement(meters, unit, metricNotation)
205
+ const [draft, setDraft] = useState<number | null>(null)
206
+ const [error, setError] = useState<string | null>(null)
207
+ return (
208
+ <div className="flex flex-col gap-1">
209
+ <MetricControl
210
+ className="h-8 text-xs"
211
+ label="Height"
212
+ max={bounds?.max}
213
+ min={bounds?.min ?? -3}
214
+ onChange={setDraft}
215
+ onCommit={(value) => {
216
+ setDraft(null)
217
+ if (Math.abs(value - height) <= 1e-6) return
218
+ const result = setRoomRelativeFloorHeight(zoneId, value)
219
+ setError(result.status === 'conflict' ? floorHeightRefusalText(result, length) : null)
220
+ }}
221
+ precision={2}
222
+ step={0.05}
223
+ unit="m"
224
+ value={draft ?? height}
225
+ />
226
+ {error && (
227
+ <p className="text-destructive text-xs" data-floor-height-error role="status">
228
+ {error}
229
+ </p>
230
+ )}
231
+ </div>
232
+ )
233
+ }
234
+
235
+ /**
236
+ * A room standing on a drawn slab has no height of its own — the slab's
237
+ * elevation is the floor's — so instead of a height it names that slab (and
238
+ * how many other rooms stand on it) and selects it, as the tree would.
239
+ */
240
+ export function DrawnFloorRow({ zoneId }: { zoneId: string }) {
241
+ const floor = useScene(
242
+ useShallow((scene) => {
243
+ const drawn = roomDrawnFloor(scene.nodes, zoneId)
244
+ if (!drawn) return null
245
+ const slab = scene.nodes[drawn.slabId as AnyNodeId]
246
+ return {
247
+ id: drawn.slabId,
248
+ name: (slab?.type === 'slab' && slab.name?.trim()) || 'Slab',
249
+ shared: drawn.sharedZoneIds.length,
250
+ ambiguous: drawn.ambiguous,
251
+ }
252
+ }),
253
+ )
254
+ if (!floor) return null
255
+ return (
256
+ <div className="flex min-h-7 items-center justify-between gap-3" data-drawn-floor={floor.id}>
257
+ <span className="min-w-0">
258
+ {floor.ambiguous ? 'Floor is several drawn slabs, mostly ' : 'Floor is a drawn slab: '}
259
+ <span className="text-foreground">{floor.name}</span>
260
+ {floor.shared > 0 && ` (shared with ${count(floor.shared, 'room')})`}
261
+ </span>
262
+ <button
263
+ className={cn(pill, 'shrink-0')}
264
+ data-select-drawn-floor
265
+ onClick={() => openFloorFoundation(floor.id)}
266
+ type="button"
267
+ >
268
+ Select
269
+ </button>
270
+ </div>
271
+ )
272
+ }
273
+
274
+ /**
275
+ * "Built on: Shared floor ▾" — the floor the room stands on, picked from the
276
+ * floors it touches on its level (each by its footprint name: "Shared floor",
277
+ * "Lanai floor", a name the user gave it) or a new floor of its own. Hidden
278
+ * where there is no choice (see `roomBuiltOn`). A refused move says why,
279
+ * right here.
280
+ */
281
+ export function BuiltOnField({ zoneId }: { zoneId: string }) {
282
+ const nodes = useScene((scene) => scene.nodes)
283
+ const model = useMemo(() => roomBuiltOn(nodes, zoneId), [nodes, zoneId])
284
+ const [error, setError] = useState<string | null>(null)
285
+ if (!model) return null
286
+ const choose = (key: string | null) => {
287
+ const result = setRoomFloor(zoneId, key)
288
+ setError(result.status === 'conflict' ? result.message : null)
289
+ }
290
+ return (
291
+ <div className="flex flex-col gap-1" data-built-on={model.current.key ?? 'shared'}>
292
+ <PartField label="Built on">
293
+ <DropdownMenu>
294
+ <DropdownMenuTrigger asChild>
295
+ <button
296
+ aria-label="Built on"
297
+ className="flex min-w-0 items-center gap-1.5 rounded-full border border-border/50 px-2.5 py-0.5 text-foreground text-xs transition-colors hover:bg-accent"
298
+ data-built-on-trigger
299
+ type="button"
300
+ >
301
+ <span className="truncate">{model.current.name}</span>
302
+ <ChevronDown className="size-3 shrink-0 text-muted-foreground" />
303
+ </button>
304
+ </DropdownMenuTrigger>
305
+ <DropdownMenuContent align="end" className="min-w-40">
306
+ {model.choices.map((choice) => (
307
+ <DropdownMenuItem
308
+ data-built-on-choice={choice.key ?? 'shared'}
309
+ key={choice.plateId ?? 'shared'}
310
+ onSelect={() => {
311
+ if (choice.plateId !== model.current.plateId) choose(choice.key)
312
+ }}
313
+ >
314
+ <span className="flex-1 truncate">{choice.name}</span>
315
+ {choice.plateId === model.current.plateId && <Check className="size-3.5" />}
316
+ </DropdownMenuItem>
317
+ ))}
318
+ <DropdownMenuSeparator />
319
+ <DropdownMenuItem data-built-on-choice="new" onSelect={() => choose('new')}>
320
+ + New floor
321
+ </DropdownMenuItem>
322
+ </DropdownMenuContent>
323
+ </DropdownMenu>
324
+ </PartField>
325
+ {error && (
326
+ <p className="text-destructive text-xs" data-built-on-error role="status">
327
+ {error}
328
+ </p>
329
+ )}
330
+ </div>
331
+ )
332
+ }
333
+
334
+ /**
335
+ * How tall the room is inside: floor to its ceiling, or to its walls' tops
336
+ * without one. Null when neither exists.
337
+ */
338
+ export function roomInsideHeight(
339
+ room: RoomSelectionRecord,
340
+ nodes: Record<string, import('@pascal-app/core').AnyNode>,
341
+ ): { floor: number; ceiling: number | null; height: number | null } {
342
+ const { floorY, wallTops, ceiling } = resolveRoomAssemblyHeights(room, nodes)
343
+ const top = ceiling?.y ?? (wallTops.size ? Math.max(...wallTops.values()) : null)
344
+ return {
345
+ floor: floorY,
346
+ ceiling: ceiling ? ceiling.y - floorY : null,
347
+ height: top === null ? null : top - floorY,
348
+ }
349
+ }
350
+
351
+ /**
352
+ * "Sits on: Shared floor · Raised 0.30 m ›" — what the room's floor is built
353
+ * as and how it stands, opening its panel: the footprint's Floor & foundation
354
+ * (a room on its own floor, its own plate: "Lanai floor"), or the drawn slab
355
+ * the floor was taken from ("· Drawn slab"). A room without a floor says so
356
+ * rather than naming the footprint next to it.
357
+ */
358
+ function SitsOn({ zoneId, noFloor }: { zoneId: string; noFloor: boolean }) {
359
+ const unit = useViewer((s) => s.unit)
360
+ const metricNotation = useViewer((s) => s.metricNotation)
361
+ const summary = useScene(
362
+ useShallow((scene) => {
363
+ const plate = roomFloorOwner(scene.nodes, zoneId) ?? roomFootprint(scene.nodes, zoneId)
364
+ if (!plate) return { id: '', name: '', drawn: false, lift: 0, supported: false }
365
+ if (plate.plateRole !== 'base')
366
+ return {
367
+ id: plate.id as string,
368
+ name: plate.name?.trim() || 'Slab',
369
+ drawn: true,
370
+ lift: 0,
371
+ supported: false,
372
+ }
373
+ return {
374
+ id: plate.id as string,
375
+ name: floorFootprintName(scene.nodes, plate),
376
+ drawn: false,
377
+ ...footprintStanding(scene.nodes, plate),
378
+ }
379
+ }),
380
+ )
381
+ const preset = summary.drawn
382
+ ? 'Drawn slab'
383
+ : Math.abs(summary.lift) > 0.005
384
+ ? `${summary.lift > 0 ? 'Raised' : 'Lowered'} ${formatLinearMeasurement(Math.abs(summary.lift), unit, metricNotation)}`
385
+ : summary.supported
386
+ ? 'On the walls below'
387
+ : 'On the ground'
388
+ if (noFloor)
389
+ return (
390
+ <span className="mb-2 ml-[42px] text-muted-foreground text-xs" data-sits-on="none">
391
+ No floor
392
+ </span>
393
+ )
394
+ if (!summary.id) return null
395
+ return (
396
+ <button
397
+ className="mb-2 ml-[42px] flex items-center gap-1 text-left text-muted-foreground text-xs transition-colors hover:text-foreground"
398
+ data-sits-on={summary.id}
399
+ onClick={() => openFloorFoundation(summary.id)}
400
+ type="button"
401
+ >
402
+ <span>
403
+ Sits on: <span className="text-foreground">{summary.name}</span> · {preset}
404
+ </span>
405
+ <ChevronRight className="size-3 shrink-0" />
406
+ </button>
407
+ )
408
+ }
409
+
410
+ /**
411
+ * The room's floor construction, edited from the room: its thickness, through
412
+ * core's `setRoomFloorConstruction` (the command MCP uses too), on whatever the
413
+ * floor is built as. A drawn slab takes thickness and finishes only — it has
414
+ * no footprint height or foundation, and says so.
415
+ */
416
+ export function RoomFloorConstructionField({ zoneId }: { zoneId: string }) {
417
+ const owner = useScene(
418
+ useShallow((scene) => {
419
+ // Several drawn slabs show in the room: no one thickness to edit here.
420
+ if (roomDrawnFloor(scene.nodes, zoneId)?.ambiguous) return null
421
+ const plate = roomFloorOwner(scene.nodes, zoneId)
422
+ return plate
423
+ ? {
424
+ id: plate.id as string,
425
+ thickness: plate.thickness,
426
+ drawn: plate.plateRole !== 'base',
427
+ // A slab on the ground has nothing under it to z-fight; it may be thinner.
428
+ min:
429
+ plate.plateRole === 'base' && floorPlateAtGroundContact(scene.nodes, plate)
430
+ ? MIN_GROUND_FLOOR_THICKNESS
431
+ : MIN_SLAB_THICKNESS,
432
+ }
433
+ : null
434
+ }),
435
+ )
436
+ const [draft, setDraft] = useState<number | null>(null)
437
+ const [error, setError] = useState<string | null>(null)
438
+ if (!owner) return null
439
+ return (
440
+ <div className="flex flex-col gap-1" data-room-floor-construction={owner.id}>
441
+ <MetricControl
442
+ className="h-8 text-xs"
443
+ label="Floor thickness"
444
+ max={1}
445
+ min={owner.min}
446
+ onChange={setDraft}
447
+ onCommit={(value) => {
448
+ setDraft(null)
449
+ if (Math.abs(value - owner.thickness) <= 1e-6) return
450
+ setError(applyRoomFloorConstruction(zoneId, { thickness: value }, owner.id))
451
+ }}
452
+ precision={2}
453
+ step={0.01}
454
+ unit="m"
455
+ value={draft ?? owner.thickness}
456
+ />
457
+ {owner.drawn && (
458
+ <p data-drawn-slab-note>
459
+ A drawn slab: set its thickness and finishes here. It has no footprint height or
460
+ foundation.
461
+ </p>
462
+ )}
463
+ {error && (
464
+ <p className="text-destructive text-xs" data-floor-construction-error role="status">
465
+ {error}
466
+ </p>
467
+ )}
468
+ </div>
469
+ )
470
+ }
471
+
472
+ /**
473
+ * How a footprint stands: on the ground, its foundation height ("Raised 0.30 m"
474
+ * or on the ground); upstairs, its lift above resting on the walls below.
475
+ */
476
+ function footprintStanding(
477
+ nodes: Record<string, import('@pascal-app/core').AnyNode>,
478
+ plate: import('@pascal-app/core').SlabNode,
479
+ ) {
480
+ const supported = floorFootprintSupportClass(nodes, plate) === 'supported'
481
+ return {
482
+ supported,
483
+ lift: supported ? floorLift(nodes, plate) : foundationHeight(nodes, plate),
484
+ }
485
+ }
486
+
487
+ /** Rooms sharing any of `wallIds` with `zoneId`, by name. */
488
+ function sharingRoomNames(
489
+ nodes: Record<string, import('@pascal-app/core').AnyNode>,
490
+ zoneId: string,
491
+ wallIds: readonly string[],
492
+ ) {
493
+ return Object.values(nodes).flatMap((node) =>
494
+ node.type === 'zone' &&
495
+ node.id !== zoneId &&
496
+ node.spaceRole === 'room' &&
497
+ node.boundaryWallIds.some((id) => wallIds.includes(id))
498
+ ? [node.name?.trim() || 'the next room']
499
+ : [],
500
+ )
501
+ }
502
+
503
+ /**
504
+ * "Built with": the room's floor, walls and ceiling, each included or not
505
+ * with its switch, and its settings a chevron away. Then Add mezzanine.
506
+ */
507
+ export function RoomConstructionRows({ room }: { room: RoomSelectionRecord }) {
508
+ const zoneId = room.zoneId
509
+ const state = useScene((scene) => roomConstructionState(scene.nodes, zoneId))
510
+ const nodes = useScene((scene) => scene.nodes)
511
+ const unit = useViewer((s) => s.unit)
512
+ const metricNotation = useViewer((s) => s.metricNotation)
513
+ const [expanded, setExpanded] = useState<RoomConstructionPart | null>(null)
514
+ const [message, setMessage] = useState<{ text: string; conflict: boolean } | null>(null)
515
+ if (!state) return null
516
+ const length = (meters: number) => formatLinearMeasurement(meters, unit, metricNotation)
517
+ const report = (result: RoomConstructionResult, unchanged = 'Nothing to change.') =>
518
+ setMessage(
519
+ result.status === 'conflict'
520
+ ? { text: result.message, conflict: true }
521
+ : result.status === 'unchanged'
522
+ ? { text: unchanged, conflict: false }
523
+ : null,
524
+ )
525
+ const toggle = (part: RoomConstructionPart) => () =>
526
+ setExpanded((current) => (current === part ? null : part))
527
+ const drawnFloor = roomDrawnFloor(nodes, zoneId)
528
+ const partSwitch = (part: RoomConstructionPart, label: string) => {
529
+ const { state: presence, actions } = state[part]
530
+ if (part === 'walls') {
531
+ const walls = wallsSwitch(state)
532
+ return (
533
+ <Switch
534
+ aria-checked={walls.mixed ? 'mixed' : walls.checked}
535
+ aria-label="Include walls"
536
+ checked={walls.checked}
537
+ className={cn(
538
+ walls.mixed && 'data-[state=unchecked]:bg-primary/45 [&>span]:translate-x-2!',
539
+ )}
540
+ data-walls-switch={presence}
541
+ disabled={walls.disabled}
542
+ onCheckedChange={() =>
543
+ report(
544
+ walls.command === 'remove'
545
+ ? removeRoomConstruction(zoneId, 'walls')
546
+ : addRoomConstruction(zoneId, 'walls'),
547
+ )
548
+ }
549
+ />
550
+ )
551
+ }
552
+ if (part === 'floor' && drawnFloor) {
553
+ // A drawn-slab floor is the slab: the switch only brings back a floor
554
+ // switched off before; the slab itself is edited or deleted on its own.
555
+ return (
556
+ <Tooltip>
557
+ <TooltipTrigger asChild>
558
+ {/* The span takes the hover: a disabled switch gets no pointer events. */}
559
+ <span
560
+ className={cn('inline-flex', !actions.add && 'cursor-not-allowed')}
561
+ data-drawn-floor-switch
562
+ >
563
+ <Switch
564
+ aria-label="Include floor"
565
+ checked={state.floor.intent}
566
+ className="disabled:pointer-events-none"
567
+ disabled={!actions.add}
568
+ onCheckedChange={() => report(addRoomConstruction(zoneId, 'floor'))}
569
+ />
570
+ </span>
571
+ </TooltipTrigger>
572
+ {!actions.add && (
573
+ <TooltipContent side="top" sideOffset={6}>
574
+ {DRAWN_FLOOR_SWITCH_HINT}
575
+ </TooltipContent>
576
+ )}
577
+ </Tooltip>
578
+ )
579
+ }
580
+ const on = presence !== 'absent'
581
+ return (
582
+ <Switch
583
+ aria-label={`Include ${label.toLowerCase()}`}
584
+ checked={on}
585
+ disabled={on ? !actions.remove : !actions.add}
586
+ onCheckedChange={(next) =>
587
+ report(next ? addRoomConstruction(zoneId, part) : removeRoomConstruction(zoneId, part))
588
+ }
589
+ />
590
+ )
591
+ }
592
+
593
+ const floorElevation = roomRelativeFloorHeight(nodes, zoneId)
594
+ const drawnSlab = drawnFloor ? nodes[drawnFloor.slabId as AnyNodeId] : undefined
595
+ const builtOn = roomBuiltOn(nodes, zoneId)
596
+ const floorMeta =
597
+ state.floor.state === 'partial'
598
+ ? `Partial · ${Math.round(state.floor.coverage * 100)}% covered`
599
+ : drawnSlab?.type === 'slab'
600
+ ? `Drawn slab at ${length(drawnSlab.elevation)}`
601
+ : builtOn && builtOn.current.key !== null
602
+ ? separateFloorSummary(
603
+ { ...builtOn, offset: separateFloorOffset(nodes, zoneId) },
604
+ length,
605
+ )
606
+ : Math.abs(floorElevation) < 0.005
607
+ ? 'At floor level'
608
+ : `${length(Math.abs(floorElevation))} ${floorElevation > 0 ? 'up' : 'down'}`
609
+ // A room on its own floor moves within its plate's range (on the ground, its foundation's).
610
+ const ownBounds = ownFloorElevationBounds(nodes, zoneId)
611
+ const floorBase = ownBounds ? roomFloorBase(nodes, zoneId) : 0
612
+ const heightBounds = ownBounds && {
613
+ min: ownBounds.min - floorBase,
614
+ max: ownBounds.max - floorBase,
615
+ }
616
+
617
+ const wallIds = room.boundaryWallIds
618
+ const openSides = state.walls.separators.length
619
+ const shared = sharingRoomNames(nodes, zoneId, state.walls.sharedWallIds)
620
+ const wallsMeta =
621
+ state.walls.state === 'partial'
622
+ ? `Partial · ${count(wallIds.length, 'wall')}, ${count(openSides, 'open side')}`
623
+ : count(wallIds.length, 'wall')
624
+ const locked = roomOutsideFacesLocked(nodes, wallIds)
625
+
626
+ const ceilingHeight = state.ceiling.ceilingId ? roomInsideHeight(room, nodes).ceiling : null
627
+ const ceilingMeta =
628
+ state.ceiling.state === 'partial'
629
+ ? 'Hand-drawn ceiling'
630
+ : ceilingHeight !== null
631
+ ? `At ${length(ceilingHeight)}`
632
+ : 'Flat, at wall height'
633
+
634
+ return (
635
+ <div className="flex flex-col" data-room-construction={zoneId}>
636
+ <h3 className="mt-4 mb-1.5 font-medium text-muted-foreground text-xs">Built with</h3>
637
+ <div className="flex flex-col gap-1">
638
+ <ConstructionPart
639
+ control={partSwitch('floor', 'Floor')}
640
+ expanded={expanded === 'floor'}
641
+ icon="/icons/floor.webp"
642
+ label="Floor"
643
+ meta={floorMeta}
644
+ on={state.floor.state !== 'absent'}
645
+ footer={<SitsOn noFloor={state.floor.state === 'absent'} zoneId={zoneId} />}
646
+ onToggleExpanded={toggle('floor')}
647
+ part="floor"
648
+ >
649
+ <BuiltOnField zoneId={zoneId} />
650
+ {drawnFloor ? (
651
+ <DrawnFloorRow zoneId={zoneId} />
652
+ ) : (
653
+ <FloorHeightField bounds={heightBounds} zoneId={zoneId} />
654
+ )}
655
+ <RoomFloorConstructionField zoneId={zoneId} />
656
+ <RoomOpeningsField surface="floor" zoneId={zoneId} />
657
+ </ConstructionPart>
658
+ <ConstructionPart
659
+ control={partSwitch('walls', 'Walls')}
660
+ expanded={expanded === 'walls'}
661
+ icon="/icons/wall.webp"
662
+ label="Walls"
663
+ meta={wallsMeta}
664
+ on={state.walls.state !== 'absent'}
665
+ onToggleExpanded={toggle('walls')}
666
+ part="walls"
667
+ >
668
+ <PartField label="Walls">
669
+ <span className="font-mono text-foreground tabular-nums">{wallIds.length}</span>
670
+ </PartField>
671
+ {state.walls.state === 'partial' && state.walls.actions.add && (
672
+ <PartField label={`${count(openSides, 'open side')}`}>
673
+ <button
674
+ className={pill}
675
+ onClick={() => report(addRoomConstruction(zoneId, 'walls'))}
676
+ type="button"
677
+ >
678
+ Close with walls
679
+ </button>
680
+ </PartField>
681
+ )}
682
+ {shared.length > 0 && (
683
+ <p data-shared-walls-note>
684
+ Turning walls off keeps the ones shared with {[...new Set(shared)].join(', ')}.
685
+ </p>
686
+ )}
687
+ {locked !== null && (
688
+ <div className="mt-1 flex items-start gap-3" data-lock-outside-faces>
689
+ <span className="flex flex-1 flex-col gap-0.5">
690
+ <span className="font-medium text-[13px] text-foreground">
691
+ Keep outside dimensions
692
+ </span>
693
+ <span>Walls thicken inward, so the building's outer size stays the same.</span>
694
+ </span>
695
+ <Switch
696
+ aria-label="Keep outside dimensions"
697
+ checked={locked}
698
+ onCheckedChange={(next) =>
699
+ report(next ? lockRoomOutsideFaces(zoneId) : unlockRoomOutsideFaces(wallIds))
700
+ }
701
+ />
702
+ </div>
703
+ )}
704
+ </ConstructionPart>
705
+ <ConstructionPart
706
+ control={partSwitch('ceiling', 'Ceiling')}
707
+ expanded={expanded === 'ceiling'}
708
+ icon="/icons/ceiling.webp"
709
+ label="Ceiling"
710
+ meta={ceilingMeta}
711
+ on={state.ceiling.state !== 'absent'}
712
+ onToggleExpanded={toggle('ceiling')}
713
+ part="ceiling"
714
+ >
715
+ {ceilingHeight !== null && (
716
+ <PartField label="Height">
717
+ <span className="font-mono text-foreground tabular-nums">
718
+ {length(ceilingHeight)}
719
+ </span>
720
+ </PartField>
721
+ )}
722
+ {state.ceiling.ceilingId && (
723
+ <PartField label="Shape">
724
+ <button className={pill} onClick={() => startCeilingEdit(zoneId)} type="button">
725
+ Edit shape
726
+ </button>
727
+ </PartField>
728
+ )}
729
+ {state.ceiling.ceilingId && <RoomOpeningsField surface="ceiling" zoneId={zoneId} />}
730
+ {(state.ceiling.actions.useExisting || state.ceiling.actions.replace) && (
731
+ <div className="flex items-center gap-2" data-construction-manual-ceiling>
732
+ <span className="flex-1">Hand-drawn ceiling</span>
733
+ {state.ceiling.actions.useExisting && (
734
+ <button
735
+ className={pill}
736
+ onClick={() => report(adoptExistingCeiling(zoneId))}
737
+ type="button"
738
+ >
739
+ Use existing
740
+ </button>
741
+ )}
742
+ {state.ceiling.actions.replace && (
743
+ <button
744
+ className={pill}
745
+ onClick={() => report(replaceRoomCeiling(zoneId))}
746
+ type="button"
747
+ >
748
+ Replace
749
+ </button>
750
+ )}
751
+ </div>
752
+ )}
753
+ </ConstructionPart>
754
+ </div>
755
+ {message && (
756
+ <span
757
+ className={cn(
758
+ 'mt-2 text-xs',
759
+ message.conflict ? 'text-destructive' : 'text-muted-foreground',
760
+ )}
761
+ role="status"
762
+ >
763
+ {message.text}
764
+ </span>
765
+ )}
766
+ <ShapeChoice
767
+ className="mt-3 rounded-full border border-border border-dashed py-1.5 pr-1.5 pl-3.5 font-medium text-[13px] text-foreground"
768
+ data="add-mezzanine"
769
+ label="+ Add mezzanine"
770
+ onPick={(shape) => startMezzanineDraft(zoneId, shape)}
771
+ />
772
+ </div>
773
+ )
774
+ }