@pascal-app/editor 1.0.6 → 1.0.7

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 (317) hide show
  1. package/package.json +18 -15
  2. package/src/components/editor/action-menu-button.tsx +4 -1
  3. package/src/components/editor/alignment-3d-guide-layer.tsx +102 -11
  4. package/src/components/editor/bake-thumbnail.tsx +3 -2
  5. package/src/components/editor/custom-camera-controls.tsx +20 -8
  6. package/src/components/editor/elevation-3d-guide-layer.tsx +1 -1
  7. package/src/components/editor/first-person-controls.tsx +72 -61
  8. package/src/components/editor/floating-action-menu.tsx +27 -9
  9. package/src/components/editor/floating-building-action-menu.tsx +1 -1
  10. package/src/components/editor/floorplan-panel.test.ts +220 -0
  11. package/src/components/editor/floorplan-panel.tsx +285 -132
  12. package/src/components/editor/footprint-height-handle.tsx +1 -1
  13. package/src/components/editor/grid.tsx +24 -15
  14. package/src/components/editor/group-actions.ts +19 -1
  15. package/src/components/editor/group-floating-action-menu.tsx +1 -1
  16. package/src/components/editor/group-move-3d.ts +27 -2
  17. package/src/components/editor/group-selection-box-3d.tsx +4 -1
  18. package/src/components/editor/group-transform-shared.ts +20 -0
  19. package/src/components/editor/index.tsx +130 -20
  20. package/src/components/editor/inspector-extension-actions.tsx +36 -0
  21. package/src/components/editor/localized-html.tsx +22 -0
  22. package/src/components/editor/node-action-menu.tsx +46 -12
  23. package/src/components/editor/node-arrow-handles.tsx +1 -2
  24. package/src/components/editor/open-wall-ends-3d-layer.tsx +1 -1
  25. package/src/components/editor/opening-guides-3d-layer.tsx +1 -1
  26. package/src/components/editor/registry-action-contributions.test.tsx +67 -6
  27. package/src/components/editor/registry-action-contributions.tsx +115 -15
  28. package/src/components/editor/riser-diagram-panel.tsx +59 -9
  29. package/src/components/editor/room-controls.tsx +1 -1
  30. package/src/components/editor/room-handles-2d.tsx +156 -6
  31. package/src/components/editor/room-handles.tsx +241 -51
  32. package/src/components/editor/room-highlight.tsx +2 -0
  33. package/src/components/editor/room-labels.tsx +29 -15
  34. package/src/components/editor/selection-manager.tsx +15 -102
  35. package/src/components/editor/separator-end-grip.tsx +74 -0
  36. package/src/components/editor/site-edge-labels.tsx +1 -1
  37. package/src/components/editor/thumbnail-generator.test.ts +15 -6
  38. package/src/components/editor/thumbnail-generator.tsx +26 -1
  39. package/src/components/editor/use-floorplan-background-placement.ts +55 -52
  40. package/src/components/editor/views/view-bar.tsx +111 -26
  41. package/src/components/editor/wall-measurement-label.tsx +1 -1
  42. package/src/components/editor/wall-move-side-handles.tsx +5 -1
  43. package/src/components/editor/wall-pull-grip.tsx +193 -0
  44. package/src/components/editor/wall-snap-beacon-layer.tsx +81 -1
  45. package/src/components/editor-2d/floorplan-adjacent-walls-layer.test.ts +76 -0
  46. package/src/components/editor-2d/floorplan-adjacent-walls-layer.tsx +186 -0
  47. package/src/components/editor-2d/floorplan-alignment-guide-layer.tsx +63 -3
  48. package/src/components/editor-2d/floorplan-group-move.tsx +31 -3
  49. package/src/components/editor-2d/floorplan-measurement-tool-layer.test.ts +6 -0
  50. package/src/components/editor-2d/floorplan-measurement-tool-layer.tsx +22 -13
  51. package/src/components/editor-2d/floorplan-open-wall-ends-layer.tsx +11 -3
  52. package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +7 -1
  53. package/src/components/editor-2d/floorplan-snap-beacon-layer.tsx +96 -4
  54. package/src/components/editor-2d/renderers/floorplan-dimension-edit-overlay.tsx +52 -12
  55. package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +12 -1
  56. package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +197 -37
  57. package/src/components/procedural-items/inspector.tsx +59 -12
  58. package/src/components/systems/ceiling/ceiling-bracket-batch.test.ts +2 -0
  59. package/src/components/systems/ceiling/ceiling-system.test.ts +141 -0
  60. package/src/components/systems/ceiling/ceiling-system.tsx +53 -18
  61. package/src/components/tools/distribution/distribution-run-tool.tsx +50 -26
  62. package/src/components/tools/item/placement-history.test.tsx +1 -1
  63. package/src/components/tools/item/placement-strategies.test.ts +64 -1
  64. package/src/components/tools/item/placement-strategies.ts +5 -9
  65. package/src/components/tools/item/use-placement-coordinator.tsx +30 -34
  66. package/src/components/tools/registry/item-surface-move.test.ts +11 -0
  67. package/src/components/tools/select/select-candidates.test.ts +45 -0
  68. package/src/components/tools/select/select-candidates.ts +13 -2
  69. package/src/components/tools/shared/cursor-sphere.tsx +10 -3
  70. package/src/components/tools/shared/draft-measurement-label.tsx +1 -1
  71. package/src/components/tools/shared/placement-box.tsx +1 -1
  72. package/src/components/tools/shared/placement-dimension-guides.tsx +1 -1
  73. package/src/components/tools/site/terrain-brush-cursor.tsx +1 -1
  74. package/src/components/tools/wall/wall-drafting.test.ts +299 -23
  75. package/src/components/tools/wall/wall-drafting.ts +132 -24
  76. package/src/components/tools/wall/wall-pull-snap.test.ts +274 -0
  77. package/src/components/tools/wall/wall-pull-snap.ts +563 -0
  78. package/src/components/tools/wall/wall-snap-geometry.test.ts +113 -1
  79. package/src/components/tools/wall/wall-snap-geometry.ts +137 -0
  80. package/src/components/tools/zone/zone-tool.tsx +3 -3
  81. package/src/components/ui/action-menu/action-button.tsx +2 -0
  82. package/src/components/ui/action-menu/camera-actions.tsx +8 -7
  83. package/src/components/ui/action-menu/control-modes.tsx +9 -5
  84. package/src/components/ui/action-menu/follow-pascal-pill.tsx +3 -1
  85. package/src/components/ui/action-menu/follow-pascal-toggle.tsx +7 -1
  86. package/src/components/ui/action-menu/furnish-tools.tsx +7 -6
  87. package/src/components/ui/action-menu/index.tsx +1 -0
  88. package/src/components/ui/action-menu/measurement-control.tsx +35 -29
  89. package/src/components/ui/action-menu/point-ask-button.tsx +4 -3
  90. package/src/components/ui/action-menu/structure-tools.tsx +25 -23
  91. package/src/components/ui/action-menu/view-toggles.test.ts +18 -0
  92. package/src/components/ui/action-menu/view-toggles.tsx +256 -51
  93. package/src/components/ui/build-panel/build-panel.tsx +163 -25
  94. package/src/components/ui/command-palette/editor-commands.tsx +38 -11
  95. package/src/components/ui/controls/finish-swatch.tsx +45 -19
  96. package/src/components/ui/controls/material-picker.tsx +24 -17
  97. package/src/components/ui/controls/metric-control.test.ts +52 -0
  98. package/src/components/ui/controls/metric-control.tsx +62 -24
  99. package/src/components/ui/controls/segmented-control.tsx +5 -2
  100. package/src/components/ui/controls/slider-control.test.tsx +23 -0
  101. package/src/components/ui/controls/slider-control.tsx +30 -14
  102. package/src/components/ui/floating-level-selector.tsx +100 -48
  103. package/src/components/ui/helpers/building-helper.tsx +5 -3
  104. package/src/components/ui/helpers/contextual-helper-panel.tsx +97 -37
  105. package/src/components/ui/helpers/helper-manager.test.tsx +53 -3
  106. package/src/components/ui/helpers/helper-manager.tsx +82 -57
  107. package/src/components/ui/helpers/item-helper.tsx +6 -4
  108. package/src/components/ui/helpers/registered-tool-helper.tsx +3 -1
  109. package/src/components/ui/level-duplicate-dialog.tsx +32 -23
  110. package/src/components/ui/level-wall-copy-prompt.tsx +124 -0
  111. package/src/components/ui/panels/collections/collections-popover.tsx +11 -11
  112. package/src/components/ui/panels/deck-boards-fields.tsx +136 -0
  113. package/src/components/ui/panels/floor-foundation-panel.tsx +113 -38
  114. package/src/components/ui/panels/floor-opening-panel.tsx +73 -25
  115. package/src/components/ui/panels/inspector-extensions.tsx +66 -0
  116. package/src/components/ui/panels/mobile-panel-sheet.tsx +3 -1
  117. package/src/components/ui/panels/mobile-selection-bar.tsx +8 -6
  118. package/src/components/ui/panels/multi-field-value.test.ts +27 -0
  119. package/src/components/ui/panels/multi-field-value.ts +20 -1
  120. package/src/components/ui/panels/multi-height-mode.tsx +8 -7
  121. package/src/components/ui/panels/multi-parametric-inspector.tsx +122 -34
  122. package/src/components/ui/panels/multi-selection-panel.tsx +75 -35
  123. package/src/components/ui/panels/node-display.ts +35 -24
  124. package/src/components/ui/panels/panel-manager.tsx +5 -3
  125. package/src/components/ui/panels/panel-wrapper.tsx +22 -109
  126. package/src/components/ui/panels/parametric-field-control.tsx +13 -10
  127. package/src/components/ui/panels/parametric-inspector.tsx +18 -12
  128. package/src/components/ui/panels/reference-panel.tsx +51 -35
  129. package/src/components/ui/panels/room-construction-rows.tsx +464 -65
  130. package/src/components/ui/panels/room-name-options.test.ts +36 -3
  131. package/src/components/ui/panels/room-name-options.ts +110 -11
  132. package/src/components/ui/panels/room-openings-field.tsx +21 -12
  133. package/src/components/ui/panels/room-panel.tsx +206 -99
  134. package/src/components/ui/panels/shape-choice.tsx +7 -5
  135. package/src/components/ui/panels/wall-region-list.tsx +7 -7
  136. package/src/components/ui/primitives/shortcut-token.tsx +10 -6
  137. package/src/components/ui/sidebar/icon-rail.tsx +11 -9
  138. package/src/components/ui/sidebar/panels/items-panel/function-tree-panel.tsx +10 -10
  139. package/src/components/ui/sidebar/panels/items-panel/index.tsx +10 -9
  140. package/src/components/ui/sidebar/panels/settings-panel/index.tsx +3 -0
  141. package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +102 -32
  142. package/src/components/ui/sidebar/panels/site-panel/building-tree-node.tsx +4 -3
  143. package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +4 -2
  144. package/src/components/ui/sidebar/panels/site-panel/chimney-tree-node.tsx +4 -2
  145. package/src/components/ui/sidebar/panels/site-panel/collection-tree-node.tsx +7 -5
  146. package/src/components/ui/sidebar/panels/site-panel/column-tree-node.tsx +4 -2
  147. package/src/components/ui/sidebar/panels/site-panel/default-names.ts +75 -0
  148. package/src/components/ui/sidebar/panels/site-panel/door-tree-node.tsx +4 -2
  149. package/src/components/ui/sidebar/panels/site-panel/dormer-tree-node.tsx +4 -2
  150. package/src/components/ui/sidebar/panels/site-panel/elevator-tree-node.tsx +4 -2
  151. package/src/components/ui/sidebar/panels/site-panel/fence-tree-node.tsx +4 -2
  152. package/src/components/ui/sidebar/panels/site-panel/gutter-tree-node.tsx +4 -2
  153. package/src/components/ui/sidebar/panels/site-panel/index.tsx +117 -64
  154. package/src/components/ui/sidebar/panels/site-panel/inline-rename-input.tsx +36 -3
  155. package/src/components/ui/sidebar/panels/site-panel/item-tree-node.tsx +4 -2
  156. package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +7 -4
  157. package/src/components/ui/sidebar/panels/site-panel/registry-tree-node.tsx +8 -2
  158. package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +8 -5
  159. package/src/components/ui/sidebar/panels/site-panel/shelf-tree-node.tsx +4 -2
  160. package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +8 -3
  161. package/src/components/ui/sidebar/panels/site-panel/solar-panel-tree-node.tsx +4 -2
  162. package/src/components/ui/sidebar/panels/site-panel/spawn-tree-node.tsx +4 -2
  163. package/src/components/ui/sidebar/panels/site-panel/stair-tree-node.tsx +8 -7
  164. package/src/components/ui/sidebar/panels/site-panel/tree-node-actions.tsx +7 -6
  165. package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +16 -1
  166. package/src/components/ui/sidebar/panels/site-panel/unit-tree-node.tsx +15 -8
  167. package/src/components/ui/sidebar/panels/site-panel/wall-tree-node.tsx +4 -2
  168. package/src/components/ui/sidebar/panels/site-panel/window-tree-node.tsx +4 -2
  169. package/src/components/ui/sidebar/panels/site-panel/zone-membership-checkbox.tsx +3 -1
  170. package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +18 -8
  171. package/src/components/ui/sidebar/panels/zone-panel/index.tsx +25 -14
  172. package/src/components/ui/sidebar/tab-bar.tsx +4 -1
  173. package/src/components/ui/sidebar/use-plugin-panels.tsx +19 -10
  174. package/src/components/ui/snap-target-badge.tsx +7 -6
  175. package/src/components/ui/wall-type/wall-type-editor-dialog.tsx +721 -0
  176. package/src/components/ui/wall-type/wall-type-menu.tsx +167 -0
  177. package/src/components/viewer/floorplan-compass-button.tsx +12 -3
  178. package/src/components/viewer/floorplan-preview.tsx +27 -19
  179. package/src/components/viewer/render-settings-menu.tsx +294 -0
  180. package/src/components/viewer/sky-control.tsx +710 -0
  181. package/src/components/viewer/use-viewer-ui-sounds.test.ts +15 -0
  182. package/src/components/viewer/use-viewer-ui-sounds.ts +46 -0
  183. package/src/components/viewer/viewer-controls-bar.tsx +278 -260
  184. package/src/components/viewer/viewer-hover-label.tsx +12 -4
  185. package/src/components/viewer/viewer-rooms.tsx +61 -4
  186. package/src/components/viewer/viewer-scene-header.tsx +28 -17
  187. package/src/components/viewer/viewer-stage-switcher.tsx +4 -2
  188. package/src/components/viewer-overlay.tsx +8 -0
  189. package/src/components/walkthrough-hud.tsx +75 -2
  190. package/src/hooks/use-keyboard.test.ts +85 -4
  191. package/src/hooks/use-keyboard.ts +65 -7
  192. package/src/hooks/use-selected-room.ts +12 -1
  193. package/src/i18n/floor-footprint-name.ts +30 -0
  194. package/src/i18n/level-name.ts +39 -0
  195. package/src/i18n/locales/en/messages.po +15117 -0
  196. package/src/i18n/locales/en/messages.ts +1 -0
  197. package/src/i18n/locales/en-XA/messages.ts +1 -0
  198. package/src/i18n/locales/es/messages.po +37501 -0
  199. package/src/i18n/locales/es/messages.ts +1 -0
  200. package/src/i18n/locales/fr/messages.po +37501 -0
  201. package/src/i18n/locales/fr/messages.ts +1 -0
  202. package/src/i18n/locales/ja/messages.po +37501 -0
  203. package/src/i18n/locales/ja/messages.ts +1 -0
  204. package/src/i18n/locales/zh-CN/messages.po +37501 -0
  205. package/src/i18n/locales/zh-CN/messages.ts +1 -0
  206. package/src/i18n/locales.test.ts +63 -0
  207. package/src/i18n/locales.ts +32 -0
  208. package/src/i18n/message-slot.tsx +25 -0
  209. package/src/i18n/node-messages.ts +169 -0
  210. package/src/i18n/pseudo-locale.ts +17 -0
  211. package/src/i18n/published-entry.test.ts +17 -0
  212. package/src/i18n/scene-theme-name.ts +16 -0
  213. package/src/index.tsx +156 -4
  214. package/src/lib/add-storey.test.ts +3 -3
  215. package/src/lib/contextual-help.test.ts +20 -1
  216. package/src/lib/contextual-help.ts +95 -19
  217. package/src/lib/continuation.ts +38 -11
  218. package/src/lib/floor-footprints.test.ts +2 -1
  219. package/src/lib/floor-foundation-support.test.ts +7 -3
  220. package/src/lib/floorplan/dimension-drive.test.ts +36 -2
  221. package/src/lib/floorplan/dimension-drive.ts +68 -3
  222. package/src/lib/floorplan/floorplan-export.test.ts +16 -5
  223. package/src/lib/floorplan/floorplan-export.tsx +13 -16
  224. package/src/lib/floorplan/floorplan-extension.test.ts +11 -12
  225. package/src/lib/floorplan/floorplan-extension.ts +37 -18
  226. package/src/lib/floorplan/floorplan-mode.test.ts +13 -6
  227. package/src/lib/floorplan/floorplan-mode.ts +14 -12
  228. package/src/lib/floorplan/floorplan-plugin-install.test.tsx +1 -1
  229. package/src/lib/floorplan/floorplan-readonly.ts +3 -3
  230. package/src/lib/floorplan/schedules.ts +10 -6
  231. package/src/lib/floorplan/site-plan/site-annotations.test.ts +46 -1
  232. package/src/lib/floorplan/site-plan/site-annotations.ts +3 -0
  233. package/src/lib/floorplan/site-plan/site-parts.ts +6 -3
  234. package/src/lib/geometry-script/author.test.ts +114 -0
  235. package/src/lib/geometry-script/author.ts +55 -23
  236. package/src/lib/geometry-script/client.ts +3 -0
  237. package/src/lib/geometry-script/finish-scripts.ts +7 -7
  238. package/src/lib/glb-export.test.ts +327 -0
  239. package/src/lib/glb-export.ts +70 -2
  240. package/src/lib/grid-frame.test.ts +56 -3
  241. package/src/lib/grid-frame.ts +28 -1
  242. package/src/lib/history.test.ts +2 -1
  243. package/src/lib/hud-title.ts +48 -24
  244. package/src/lib/ifc-export.test.ts +41 -1
  245. package/src/lib/inspector-card-mode.test.ts +3 -31
  246. package/src/lib/inspector-card-mode.ts +5 -10
  247. package/src/lib/inspector-expanded.test.ts +13 -0
  248. package/src/lib/inspector-expanded.ts +13 -1
  249. package/src/lib/keyboard-pan.test.ts +7 -0
  250. package/src/lib/keyboard-pan.ts +9 -2
  251. package/src/lib/layer-thickness.test.ts +22 -0
  252. package/src/lib/layer-thickness.ts +79 -0
  253. package/src/lib/lot/drop-in.test.ts +59 -1
  254. package/src/lib/lot/drop-in.ts +42 -50
  255. package/src/lib/material-swatch-style.ts +24 -0
  256. package/src/lib/measurement-parser.ts +7 -3
  257. package/src/lib/measurements.test.ts +17 -0
  258. package/src/lib/measurements.ts +20 -5
  259. package/src/lib/mezzanine-selection.test.ts +3 -1
  260. package/src/lib/orbit-near-plane.test.ts +12 -0
  261. package/src/lib/orbit-near-plane.ts +13 -0
  262. package/src/lib/paint-regions.test.ts +8 -5
  263. package/src/lib/paint-scope.test.ts +53 -0
  264. package/src/lib/paint-scope.ts +18 -4
  265. package/src/lib/panel-rows.ts +17 -8
  266. package/src/lib/reference-scale.ts +83 -0
  267. package/src/lib/room-built-on.test.ts +26 -12
  268. package/src/lib/room-built-on.ts +20 -2
  269. package/src/lib/room-construction.test.ts +28 -1
  270. package/src/lib/room-construction.ts +14 -0
  271. package/src/lib/room-controls-phase6.test.ts +140 -0
  272. package/src/lib/room-handle-drag.ts +557 -42
  273. package/src/lib/room-name-catalog.test.ts +8 -0
  274. package/src/lib/room-name-catalog.ts +8 -1
  275. package/src/lib/room-name-messages.test.ts +132 -0
  276. package/src/lib/room-name-messages.ts +147 -0
  277. package/src/lib/room-name-storage.test.ts +334 -0
  278. package/src/lib/room-push-dimensions.test.ts +37 -0
  279. package/src/lib/room-push-dimensions.ts +226 -2
  280. package/src/lib/room-selection.test.ts +23 -6
  281. package/src/lib/scene-clipboard.test.ts +70 -49
  282. package/src/lib/scene-clipboard.ts +9 -4
  283. package/src/lib/selection-cycle.ts +63 -0
  284. package/src/lib/separator-edit.test.ts +431 -0
  285. package/src/lib/separator-edit.ts +179 -0
  286. package/src/lib/separator-lines.ts +38 -0
  287. package/src/lib/sfx-bus.ts +27 -1
  288. package/src/lib/sfx-player.ts +16 -0
  289. package/src/lib/tool-cursor-icon.ts +5 -1
  290. package/src/lib/tool-shortcuts.ts +130 -0
  291. package/src/lib/viewer-selection.test.ts +28 -24
  292. package/src/lib/viewer-selection.ts +20 -5
  293. package/src/lib/wall-draw-variant.test.ts +7 -6
  294. package/src/lib/wall-draw-variant.ts +107 -6
  295. package/src/lib/wall-nudge.test.ts +44 -0
  296. package/src/lib/wall-nudge.ts +131 -0
  297. package/src/lib/wall-parallel-snap.test.ts +83 -0
  298. package/src/lib/wall-parallel-snap.ts +176 -0
  299. package/src/lib/wall-placement-pointer.test.ts +23 -0
  300. package/src/lib/wall-placement-pointer.ts +18 -0
  301. package/src/lib/wall-pull.test.ts +245 -0
  302. package/src/lib/wall-pull.ts +209 -0
  303. package/src/lib/wall-push-line-up.test.ts +132 -0
  304. package/src/lib/wall-region-list.ts +14 -5
  305. package/src/lib/wall-run-snap.test.ts +188 -0
  306. package/src/lib/wall-run-snap.ts +83 -0
  307. package/src/lib/wall-type.test.ts +125 -0
  308. package/src/lib/wall-type.ts +119 -0
  309. package/src/lib/window-interaction.test.ts +42 -0
  310. package/src/lib/window-interaction.ts +47 -5
  311. package/src/lib/world-grid-snap.test.ts +139 -2
  312. package/src/lib/world-grid-snap.ts +102 -105
  313. package/src/store/use-alignment-guides.ts +27 -1
  314. package/src/store/use-editor.tsx +48 -0
  315. package/src/store/use-floorplan-annotation-visibility.test.ts +64 -0
  316. package/src/store/use-floorplan-annotation-visibility.ts +47 -22
  317. package/src/store/use-wall-snap-indicator.ts +15 -2
@@ -1,13 +1,23 @@
1
1
  'use client'
2
2
 
3
3
  import {
4
+ type AnyNode,
4
5
  type AnyNodeId,
5
- floorFootprintName,
6
+ BEAM_SIZE_PRESETS,
6
7
  floorFootprintSupportClass,
8
+ formatList,
7
9
  floorPlateAtGroundContact,
8
10
  MIN_GROUND_FLOOR_THICKNESS,
9
11
  MIN_SLAB_THICKNESS,
12
+ type MessageDescriptor,
13
+ type OutdoorRoomType,
14
+ type PorchFramingSettings,
15
+ porchFramingSettings,
10
16
  roomDrawnFloor,
17
+ setZoneIntent,
18
+ type PascalI18n,
19
+ useLocale,
20
+ usePascalI18n,
11
21
  useScene,
12
22
  } from '@pascal-app/core'
13
23
  import { useViewer } from '@pascal-app/viewer'
@@ -27,7 +37,7 @@ import {
27
37
  import { formatLinearMeasurement } from '../../../lib/measurements'
28
38
  import { startMezzanineDraft } from '../../../lib/mezzanine-draft'
29
39
  import { resolveRoomAssemblyHeights } from '../../../lib/room-assembly-overlay'
30
- import { roomBuiltOn, separateFloorSummary } from '../../../lib/room-built-on'
40
+ import { type FloorChoice, roomBuiltOn, separateFloorSummary } from '../../../lib/room-built-on'
31
41
  import type { RoomConstructionState } from '../../../lib/room-construction'
32
42
  import { roomConstructionState } from '../../../lib/room-construction'
33
43
  import {
@@ -50,7 +60,10 @@ import {
50
60
  import { ownFloorElevationBounds } from '../../../lib/room-handle-drag'
51
61
  import type { RoomSelectionRecord } from '../../../lib/room-selection'
52
62
  import { cn } from '../../../lib/utils'
63
+ import { roomPorchFramingCounts } from '../../../lib/room-construction'
64
+ import { applyRoomPlan } from '../../../lib/room-structure-commands'
53
65
  import { MetricControl } from '../controls/metric-control'
66
+ import { SegmentedControl } from '../controls/segmented-control'
54
67
  import {
55
68
  DropdownMenu,
56
69
  DropdownMenuContent,
@@ -60,8 +73,13 @@ import {
60
73
  } from '../primitives/dropdown-menu'
61
74
  import { Switch } from '../primitives/switch'
62
75
  import { Tooltip, TooltipContent, TooltipTrigger } from '../primitives/tooltip'
76
+ import { localizedFloorFootprintName } from '../../../i18n/floor-footprint-name'
77
+ import { messageWithSlot } from '../../../i18n/message-slot'
78
+ import { nodeLabel } from '../../../i18n/node-messages'
79
+ import { DeckBoardsFields } from './deck-boards-fields'
63
80
  import { RoomOpeningsField } from './room-openings-field'
64
81
  import { ShapeChoice } from './shape-choice'
82
+ import { displayRoomName } from '../../../lib/room-name-messages'
65
83
 
66
84
  /** The buttons a row offers — derived together with its presence. */
67
85
  export function constructionActions(state: RoomConstructionState, part: RoomConstructionPart) {
@@ -84,12 +102,22 @@ export function wallsSwitch(state: RoomConstructionState) {
84
102
  }
85
103
  }
86
104
 
105
+ /** A "Built on" choice in the UI language: its plate's footprint name, or the shared floor. */
106
+ function floorChoiceName(
107
+ nodes: Readonly<Record<string, AnyNode>>,
108
+ choice: Pick<FloorChoice, 'plateId' | 'name'>,
109
+ i18n: PascalI18n,
110
+ ): string {
111
+ const plate = choice.plateId ? nodes[choice.plateId] : undefined
112
+ if (plate?.type === 'slab') return localizedFloorFootprintName(nodes, plate, i18n)
113
+ return choice.plateId
114
+ ? choice.name
115
+ : i18n._({ id: 'editor.floorFootprint.shared', message: 'Shared floor' })
116
+ }
117
+
87
118
  export const pill =
88
119
  '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
120
 
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
121
 
94
122
  /**
95
123
  * One "Built with" row: its icon, name and one-line summary, a chevron that
@@ -122,6 +150,7 @@ export function ConstructionPart({
122
150
  /** The settings the chevron opens; none means the row does not open. */
123
151
  children?: ReactNode
124
152
  }) {
153
+ const i18n = usePascalI18n()
125
154
  const opens = on && !!children
126
155
  const open = opens && expanded
127
156
  return (
@@ -151,7 +180,7 @@ export function ConstructionPart({
151
180
  <span className="flex min-w-0 flex-1 flex-col">
152
181
  <span className="font-medium text-[13px] text-foreground">{label}</span>
153
182
  <span className="text-pretty text-muted-foreground text-xs">
154
- {on ? meta : 'Not included'}
183
+ {on ? meta : i18n._({ id: 'editor.panels.roomConstructionRows.notIncluded', message: 'Not included' })}
155
184
  </span>
156
185
  </span>
157
186
  {opens && (
@@ -198,6 +227,7 @@ export function FloorHeightField({
198
227
  /** Allowed heights, in the same relative terms. */
199
228
  bounds?: { min: number; max: number } | null
200
229
  }) {
230
+ const i18n = usePascalI18n()
201
231
  const height = useScene((scene) => roomRelativeFloorHeight(scene.nodes, zoneId))
202
232
  const unit = useViewer((s) => s.unit)
203
233
  const metricNotation = useViewer((s) => s.metricNotation)
@@ -208,7 +238,7 @@ export function FloorHeightField({
208
238
  <div className="flex flex-col gap-1">
209
239
  <MetricControl
210
240
  className="h-8 text-xs"
211
- label="Height"
241
+ label={i18n._({ id: 'editor.panels.roomConstructionRows.height', message: 'Height' })}
212
242
  max={bounds?.max}
213
243
  min={bounds?.min ?? -3}
214
244
  onChange={setDraft}
@@ -238,6 +268,7 @@ export function FloorHeightField({
238
268
  * how many other rooms stand on it) and selects it, as the tree would.
239
269
  */
240
270
  export function DrawnFloorRow({ zoneId }: { zoneId: string }) {
271
+ const i18n = usePascalI18n()
241
272
  const floor = useScene(
242
273
  useShallow((scene) => {
243
274
  const drawn = roomDrawnFloor(scene.nodes, zoneId)
@@ -255,9 +286,21 @@ export function DrawnFloorRow({ zoneId }: { zoneId: string }) {
255
286
  return (
256
287
  <div className="flex min-h-7 items-center justify-between gap-3" data-drawn-floor={floor.id}>
257
288
  <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')})`}
289
+ {messageWithSlot(
290
+ i18n,
291
+ /* i18n */ {
292
+ id: 'editor.panels.roomConstructionRows.drawnFloor',
293
+ message:
294
+ '{ambiguous, select, yes {Floor is several drawn slabs, mostly {name}} other {Floor is a drawn slab: {name}}}{shared, plural, =0 {} one { (shared with {sharedCount} room)} other { (shared with {sharedCount} rooms)}}',
295
+ values: {
296
+ ambiguous: floor.ambiguous ? 'yes' : 'no',
297
+ shared: floor.shared,
298
+ sharedCount: String(floor.shared),
299
+ },
300
+ },
301
+ 'name',
302
+ <span className="text-foreground">{floor.name}</span>,
303
+ )}
261
304
  </span>
262
305
  <button
263
306
  className={cn(pill, 'shrink-0')}
@@ -265,7 +308,7 @@ export function DrawnFloorRow({ zoneId }: { zoneId: string }) {
265
308
  onClick={() => openFloorFoundation(floor.id)}
266
309
  type="button"
267
310
  >
268
- Select
311
+ {i18n._({ id: 'editor.panels.roomConstructionRows.select', message: 'Select' })}
269
312
  </button>
270
313
  </div>
271
314
  )
@@ -279,6 +322,7 @@ export function DrawnFloorRow({ zoneId }: { zoneId: string }) {
279
322
  * right here.
280
323
  */
281
324
  export function BuiltOnField({ zoneId }: { zoneId: string }) {
325
+ const i18n = usePascalI18n()
282
326
  const nodes = useScene((scene) => scene.nodes)
283
327
  const model = useMemo(() => roomBuiltOn(nodes, zoneId), [nodes, zoneId])
284
328
  const [error, setError] = useState<string | null>(null)
@@ -289,16 +333,16 @@ export function BuiltOnField({ zoneId }: { zoneId: string }) {
289
333
  }
290
334
  return (
291
335
  <div className="flex flex-col gap-1" data-built-on={model.current.key ?? 'shared'}>
292
- <PartField label="Built on">
336
+ <PartField label={i18n._({ id: 'editor.panels.roomConstructionRows.builtOn', message: 'Built on' })}>
293
337
  <DropdownMenu>
294
338
  <DropdownMenuTrigger asChild>
295
339
  <button
296
- aria-label="Built on"
340
+ aria-label={i18n._({ id: 'editor.panels.roomConstructionRows.builtOn', message: 'Built on' })}
297
341
  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
342
  data-built-on-trigger
299
343
  type="button"
300
344
  >
301
- <span className="truncate">{model.current.name}</span>
345
+ <span className="truncate">{floorChoiceName(nodes, model.current, i18n)}</span>
302
346
  <ChevronDown className="size-3 shrink-0 text-muted-foreground" />
303
347
  </button>
304
348
  </DropdownMenuTrigger>
@@ -311,13 +355,13 @@ export function BuiltOnField({ zoneId }: { zoneId: string }) {
311
355
  if (choice.plateId !== model.current.plateId) choose(choice.key)
312
356
  }}
313
357
  >
314
- <span className="flex-1 truncate">{choice.name}</span>
358
+ <span className="flex-1 truncate">{floorChoiceName(nodes, choice, i18n)}</span>
315
359
  {choice.plateId === model.current.plateId && <Check className="size-3.5" />}
316
360
  </DropdownMenuItem>
317
361
  ))}
318
362
  <DropdownMenuSeparator />
319
363
  <DropdownMenuItem data-built-on-choice="new" onSelect={() => choose('new')}>
320
- + New floor
364
+ {i18n._({ id: 'editor.panels.roomConstructionRows.newFloor', message: '+ New floor' })}
321
365
  </DropdownMenuItem>
322
366
  </DropdownMenuContent>
323
367
  </DropdownMenu>
@@ -356,6 +400,7 @@ export function roomInsideHeight(
356
400
  * rather than naming the footprint next to it.
357
401
  */
358
402
  function SitsOn({ zoneId, noFloor }: { zoneId: string; noFloor: boolean }) {
403
+ const i18n = usePascalI18n()
359
404
  const unit = useViewer((s) => s.unit)
360
405
  const metricNotation = useViewer((s) => s.metricNotation)
361
406
  const summary = useScene(
@@ -365,30 +410,33 @@ function SitsOn({ zoneId, noFloor }: { zoneId: string; noFloor: boolean }) {
365
410
  if (plate.plateRole !== 'base')
366
411
  return {
367
412
  id: plate.id as string,
368
- name: plate.name?.trim() || 'Slab',
413
+ name: plate.name?.trim() || nodeLabel('slab', 'Slab', i18n),
369
414
  drawn: true,
370
415
  lift: 0,
371
416
  supported: false,
372
417
  }
373
418
  return {
374
419
  id: plate.id as string,
375
- name: floorFootprintName(scene.nodes, plate),
420
+ name: localizedFloorFootprintName(scene.nodes, plate, i18n),
376
421
  drawn: false,
377
422
  ...footprintStanding(scene.nodes, plate),
378
423
  }
379
424
  }),
380
425
  )
426
+ const lift = formatLinearMeasurement(Math.abs(summary.lift), unit, metricNotation)
381
427
  const preset = summary.drawn
382
- ? 'Drawn slab'
428
+ ? i18n._({ id: 'editor.panels.roomConstructionRows.drawnSlab', message: 'Drawn slab' })
383
429
  : Math.abs(summary.lift) > 0.005
384
- ? `${summary.lift > 0 ? 'Raised' : 'Lowered'} ${formatLinearMeasurement(Math.abs(summary.lift), unit, metricNotation)}`
430
+ ? summary.lift > 0
431
+ ? i18n._({ id: 'editor.panels.roomConstructionRows.raised', message: 'Raised {lift}', values: { lift } })
432
+ : i18n._({ id: 'editor.panels.roomConstructionRows.lowered', message: 'Lowered {lift}', values: { lift } })
385
433
  : summary.supported
386
- ? 'On the walls below'
387
- : 'On the ground'
434
+ ? i18n._({ id: 'editor.panels.roomConstructionRows.onTheWallsBelow', message: 'On the walls below' })
435
+ : i18n._({ id: 'editor.panels.roomConstructionRows.onTheGround', message: 'On the ground' })
388
436
  if (noFloor)
389
437
  return (
390
438
  <span className="mb-2 ml-[42px] text-muted-foreground text-xs" data-sits-on="none">
391
- No floor
439
+ {i18n._({ id: 'editor.panels.roomConstructionRows.noFloor', message: 'No floor' })}
392
440
  </span>
393
441
  )
394
442
  if (!summary.id) return null
@@ -400,13 +448,281 @@ function SitsOn({ zoneId, noFloor }: { zoneId: string; noFloor: boolean }) {
400
448
  type="button"
401
449
  >
402
450
  <span>
403
- Sits on: <span className="text-foreground">{summary.name}</span> · {preset}
451
+ {messageWithSlot(
452
+ i18n,
453
+ /* i18n */ {
454
+ id: 'editor.panels.roomConstructionRows.sitsOnPreset',
455
+ message: 'Sits on: {name} · {preset}',
456
+ values: { preset },
457
+ },
458
+ 'name',
459
+ <span className="text-foreground">{summary.name}</span>,
460
+ )}
404
461
  </span>
405
462
  <ChevronRight className="size-3 shrink-0" />
406
463
  </button>
407
464
  )
408
465
  }
409
466
 
467
+ const ROOM_TYPE_OPTIONS: ReadonlyArray<{
468
+ label: MessageDescriptor
469
+ value: 'room' | OutdoorRoomType
470
+ }> = [
471
+ {
472
+ label: /* i18n */ { id: 'editor.panels.roomConstructionRows.roomTypeRoom', message: 'Room' },
473
+ value: 'room',
474
+ },
475
+ {
476
+ label: /* i18n */ { id: 'editor.panels.roomConstructionRows.roomTypePorch', message: 'Porch' },
477
+ value: 'porch',
478
+ },
479
+ {
480
+ label: /* i18n */ { id: 'editor.panels.roomConstructionRows.roomTypeDeck', message: 'Deck' },
481
+ value: 'deck',
482
+ },
483
+ {
484
+ label: /* i18n */ { id: 'editor.panels.roomConstructionRows.roomTypePatio', message: 'Patio' },
485
+ value: 'patio',
486
+ },
487
+ ]
488
+
489
+ const ROOM_TYPE_HINTS: Record<'room' | OutdoorRoomType, MessageDescriptor> = {
490
+ room: /* i18n */ {
491
+ id: 'editor.panels.roomConstructionRows.roomTypeRoomHint',
492
+ message: 'An ordinary room inside the house.',
493
+ },
494
+ porch: /* i18n */ {
495
+ id: 'editor.panels.roomConstructionRows.roomTypePorchHint',
496
+ message:
497
+ 'Outdoors under a ceiling: a concrete slab 1½ in down from the house, ceiling at the house’s.',
498
+ },
499
+ deck: /* i18n */ {
500
+ id: 'editor.panels.roomConstructionRows.roomTypeDeckHint',
501
+ message:
502
+ 'Outdoors: deck boards on a wood frame and posts, 1 in down from the house; no ceiling.',
503
+ },
504
+ patio: /* i18n */ {
505
+ id: 'editor.panels.roomConstructionRows.roomTypePatioHint',
506
+ message: 'Outdoors and open: a concrete slab 1½ in down from the house, no ceiling.',
507
+ },
508
+ }
509
+
510
+ /**
511
+ * "Room | Porch | Deck | Patio": the room type (`roomType`, through core's
512
+ * `setZoneIntent`). A porch, deck or patio is outdoors whatever covers it, steps its floor down
513
+ * from the house's and takes its floor and ceiling defaults in the same step; each stays
514
+ * adjustable below.
515
+ */
516
+ export function RoomTypeChoice({ zoneId }: { zoneId: string }) {
517
+ const i18n = usePascalI18n()
518
+ const value = useScene((scene) => {
519
+ const zone = scene.nodes[zoneId as AnyNodeId]
520
+ return zone?.type === 'zone' ? (zone.roomType ?? 'room') : null
521
+ })
522
+ if (!value) return null
523
+ return (
524
+ <div className="mt-3 flex flex-col gap-1" data-room-type={value}>
525
+ <h3 className="font-medium text-muted-foreground text-xs">
526
+ {i18n._({ id: 'editor.panels.roomConstructionRows.roomType', message: 'Room type' })}
527
+ </h3>
528
+ <SegmentedControl
529
+ onChange={(next: 'room' | OutdoorRoomType) =>
530
+ applyRoomPlan(
531
+ setZoneIntent(useScene.getState().nodes, {
532
+ zoneId,
533
+ patch: { roomType: next === 'room' ? null : next },
534
+ }),
535
+ )
536
+ }
537
+ options={ROOM_TYPE_OPTIONS.map((option) => ({
538
+ label: i18n._(option.label),
539
+ value: option.value,
540
+ }))}
541
+ value={value}
542
+ />
543
+ <p className="text-muted-foreground text-xs">{i18n._(ROOM_TYPE_HINTS[value])}</p>
544
+ </div>
545
+ )
546
+ }
547
+
548
+ const POST_SIZE_OPTIONS: ReadonlyArray<{
549
+ label: string
550
+ value: PorchFramingSettings['postSize']
551
+ }> = [
552
+ { label: '4x4', value: '4x4' },
553
+ { label: '6x6', value: '6x6' },
554
+ { label: '8x8', value: '8x8' },
555
+ ]
556
+
557
+ type PorchFramingMode = 'off' | 'auto' | 'manual'
558
+
559
+ /**
560
+ * A covered porch's posts and beam (`porchFraming`, through core's `setZoneIntent`), and a deck's
561
+ * or patio's once switched on, to carry a roof over it (Off by default): while Automatic the
562
+ * structure kernel keeps a post on each outside corner, more at most the spacing apart, and a beam
563
+ * along each open side under the ceiling. A size or the spacing rebuilds them; a post or beam
564
+ * edited by hand turns the room to Manual, and Automatic rebuilds them again.
565
+ */
566
+ export function PorchFramingFields({ zoneId }: { zoneId: string }) {
567
+ const i18n = usePascalI18n()
568
+ const porch = useScene(
569
+ useShallow((scene) => {
570
+ const zone = scene.nodes[zoneId as AnyNodeId]
571
+ if (zone?.type !== 'zone') return null
572
+ const optional = zone.roomType === 'deck' || zone.roomType === 'patio'
573
+ if (!(optional || (zone.roomType === 'porch' && zone.hasCeiling !== false))) return null
574
+ const { posts, beams } = roomPorchFramingCounts(scene.nodes, zone)
575
+ const auto = zone.porchFraming?.auto
576
+ const mode: PorchFramingMode =
577
+ auto === false ? 'manual' : optional && auto !== true ? 'off' : 'auto'
578
+ return {
579
+ mode,
580
+ optional,
581
+ room: zone.roomType ?? 'porch',
582
+ ...porchFramingSettings(zone),
583
+ posts,
584
+ beams,
585
+ }
586
+ }),
587
+ )
588
+ const [spacing, setSpacing] = useState<number | null>(null)
589
+ if (!porch) return null
590
+ const set = (porchFraming: NonNullable<Parameters<typeof setZoneIntent>[1]['patch']['porchFraming']>) =>
591
+ applyRoomPlan(setZoneIntent(useScene.getState().nodes, { zoneId, patch: { porchFraming } }))
592
+ const beam = BEAM_SIZE_PRESETS.find((preset) => preset.id === porch.beamSize)
593
+ return (
594
+ <div className="mt-3 flex flex-col gap-1.5" data-porch-framing={porch.mode}>
595
+ <h3 className="font-medium text-muted-foreground text-xs">
596
+ {i18n._({ id: 'editor.panels.roomConstructionRows.postsAndBeam', message: 'Posts and beam' })}
597
+ </h3>
598
+ <SegmentedControl
599
+ onChange={(next: PorchFramingMode) =>
600
+ set({ auto: next === 'off' ? null : next === 'auto' })
601
+ }
602
+ options={[
603
+ ...(porch.optional
604
+ ? [
605
+ {
606
+ label: i18n._({
607
+ id: 'editor.panels.roomConstructionRows.framingOff',
608
+ message: 'Off',
609
+ }),
610
+ value: 'off' as const,
611
+ },
612
+ ]
613
+ : []),
614
+ {
615
+ label: i18n._({
616
+ id: 'editor.panels.roomConstructionRows.framingAutomatic',
617
+ message: 'Automatic',
618
+ }),
619
+ value: 'auto' as const,
620
+ },
621
+ {
622
+ label: i18n._({
623
+ id: 'editor.panels.roomConstructionRows.framingManual',
624
+ message: 'Manual',
625
+ }),
626
+ value: 'manual' as const,
627
+ },
628
+ ]}
629
+ value={porch.mode}
630
+ />
631
+ <p className="text-muted-foreground text-xs">
632
+ {porch.mode === 'off'
633
+ ? i18n._({
634
+ id: 'editor.panels.roomConstructionRows.framingOffHint',
635
+ message:
636
+ 'None. Automatic puts a post on each outside corner and a beam on them, to carry a roof over {room, select, deck {the deck} patio {the patio} other {the porch}}.',
637
+ values: { room: porch.room },
638
+ })
639
+ : porch.mode === 'auto'
640
+ ? i18n._({
641
+ id: 'editor.panels.roomConstructionRows.framingAutoHint',
642
+ message:
643
+ '{posts, plural, one {{postCount} post} other {{postCount} posts}} and {beams, plural, one {{beamCount} beam} other {{beamCount} beams}}, a post on each outside corner, kept with {room, select, deck {the deck} patio {the patio} other {the porch}}.',
644
+ values: {
645
+ posts: porch.posts,
646
+ postCount: String(porch.posts),
647
+ beams: porch.beams,
648
+ beamCount: String(porch.beams),
649
+ room: porch.room,
650
+ },
651
+ })
652
+ : i18n._({
653
+ id: 'editor.panels.roomConstructionRows.framingManualHint',
654
+ message: 'Kept as you edited them. Automatic rebuilds them from these settings.',
655
+ })}
656
+ </p>
657
+ {porch.mode !== 'off' && (
658
+ <>
659
+ <PartField
660
+ label={i18n._({ id: 'editor.panels.roomConstructionRows.postSize', message: 'Post size' })}
661
+ >
662
+ <SegmentedControl
663
+ onChange={(postSize: PorchFramingSettings['postSize']) => set({ postSize })}
664
+ options={[...POST_SIZE_OPTIONS]}
665
+ value={porch.postSize}
666
+ />
667
+ </PartField>
668
+ <MetricControl
669
+ className="h-8 text-xs"
670
+ label={i18n._({
671
+ id: 'editor.panels.roomConstructionRows.postSpacingMax',
672
+ message: 'Post spacing (max)',
673
+ })}
674
+ max={6}
675
+ min={0.6}
676
+ onChange={setSpacing}
677
+ onCommit={(value) => {
678
+ setSpacing(null)
679
+ if (Math.abs(value - porch.postSpacing) > 1e-6) set({ postSpacing: value })
680
+ }}
681
+ precision={2}
682
+ step={0.05}
683
+ unit="m"
684
+ value={spacing ?? porch.postSpacing}
685
+ />
686
+ <PartField
687
+ label={i18n._({ id: 'editor.panels.roomConstructionRows.beam', message: 'Beam' })}
688
+ >
689
+ <DropdownMenu>
690
+ <DropdownMenuTrigger asChild>
691
+ <button
692
+ aria-label={i18n._({
693
+ id: 'editor.panels.roomConstructionRows.beamSize',
694
+ message: 'Beam size',
695
+ })}
696
+ 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"
697
+ data-porch-beam-size
698
+ type="button"
699
+ >
700
+ <span className="truncate">{beam?.label ?? porch.beamSize}</span>
701
+ <ChevronDown className="size-3 shrink-0 text-muted-foreground" />
702
+ </button>
703
+ </DropdownMenuTrigger>
704
+ <DropdownMenuContent align="end" className="max-h-72 min-w-40 overflow-y-auto">
705
+ {BEAM_SIZE_PRESETS.map((preset) => (
706
+ <DropdownMenuItem
707
+ data-porch-beam-choice={preset.id}
708
+ key={preset.id}
709
+ onSelect={() => {
710
+ if (preset.id !== porch.beamSize) set({ beamSize: preset.id })
711
+ }}
712
+ >
713
+ <span className="flex-1 truncate">{preset.label}</span>
714
+ {preset.id === porch.beamSize && <Check className="size-3.5" />}
715
+ </DropdownMenuItem>
716
+ ))}
717
+ </DropdownMenuContent>
718
+ </DropdownMenu>
719
+ </PartField>
720
+ </>
721
+ )}
722
+ </div>
723
+ )
724
+ }
725
+
410
726
  /**
411
727
  * The room's floor construction, edited from the room: its thickness, through
412
728
  * core's `setRoomFloorConstruction` (the command MCP uses too), on whatever the
@@ -414,6 +730,7 @@ function SitsOn({ zoneId, noFloor }: { zoneId: string; noFloor: boolean }) {
414
730
  * no footprint height or foundation, and says so.
415
731
  */
416
732
  export function RoomFloorConstructionField({ zoneId }: { zoneId: string }) {
733
+ const i18n = usePascalI18n()
417
734
  const owner = useScene(
418
735
  useShallow((scene) => {
419
736
  // Several drawn slabs show in the room: no one thickness to edit here.
@@ -440,7 +757,7 @@ export function RoomFloorConstructionField({ zoneId }: { zoneId: string }) {
440
757
  <div className="flex flex-col gap-1" data-room-floor-construction={owner.id}>
441
758
  <MetricControl
442
759
  className="h-8 text-xs"
443
- label="Floor thickness"
760
+ label={i18n._({ id: 'editor.panels.roomConstructionRows.floorThickness', message: 'Floor thickness' })}
444
761
  max={1}
445
762
  min={owner.min}
446
763
  onChange={setDraft}
@@ -456,8 +773,7 @@ export function RoomFloorConstructionField({ zoneId }: { zoneId: string }) {
456
773
  />
457
774
  {owner.drawn && (
458
775
  <p data-drawn-slab-note>
459
- A drawn slab: set its thickness and finishes here. It has no footprint height or
460
- foundation.
776
+ {i18n._({ id: 'editor.panels.roomConstructionRows.aDrawnSlabSetItsThickness', message: 'A drawn slab: set its thickness and finishes here. It has no footprint height or foundation.' })}
461
777
  </p>
462
778
  )}
463
779
  {error && (
@@ -495,7 +811,7 @@ function sharingRoomNames(
495
811
  node.id !== zoneId &&
496
812
  node.spaceRole === 'room' &&
497
813
  node.boundaryWallIds.some((id) => wallIds.includes(id))
498
- ? [node.name?.trim() || 'the next room']
814
+ ? [node.name?.trim() ?? '']
499
815
  : [],
500
816
  )
501
817
  }
@@ -505,6 +821,8 @@ function sharingRoomNames(
505
821
  * with its switch, and its settings a chevron away. Then Add mezzanine.
506
822
  */
507
823
  export function RoomConstructionRows({ room }: { room: RoomSelectionRecord }) {
824
+ const i18n = usePascalI18n()
825
+ const locale = useLocale()
508
826
  const zoneId = room.zoneId
509
827
  const state = useScene((scene) => roomConstructionState(scene.nodes, zoneId))
510
828
  const nodes = useScene((scene) => scene.nodes)
@@ -514,7 +832,10 @@ export function RoomConstructionRows({ room }: { room: RoomSelectionRecord }) {
514
832
  const [message, setMessage] = useState<{ text: string; conflict: boolean } | null>(null)
515
833
  if (!state) return null
516
834
  const length = (meters: number) => formatLinearMeasurement(meters, unit, metricNotation)
517
- const report = (result: RoomConstructionResult, unchanged = 'Nothing to change.') =>
835
+ const report = (
836
+ result: RoomConstructionResult,
837
+ unchanged = i18n._({ id: 'editor.panels.roomConstructionRows.nothingToChange', message: 'Nothing to change.' }),
838
+ ) =>
518
839
  setMessage(
519
840
  result.status === 'conflict'
520
841
  ? { text: result.message, conflict: true }
@@ -525,14 +846,14 @@ export function RoomConstructionRows({ room }: { room: RoomSelectionRecord }) {
525
846
  const toggle = (part: RoomConstructionPart) => () =>
526
847
  setExpanded((current) => (current === part ? null : part))
527
848
  const drawnFloor = roomDrawnFloor(nodes, zoneId)
528
- const partSwitch = (part: RoomConstructionPart, label: string) => {
849
+ const partSwitch = (part: RoomConstructionPart) => {
529
850
  const { state: presence, actions } = state[part]
530
851
  if (part === 'walls') {
531
852
  const walls = wallsSwitch(state)
532
853
  return (
533
854
  <Switch
534
855
  aria-checked={walls.mixed ? 'mixed' : walls.checked}
535
- aria-label="Include walls"
856
+ aria-label={i18n._({ id: 'editor.panels.roomConstructionRows.includeWalls', message: 'Include walls' })}
536
857
  checked={walls.checked}
537
858
  className={cn(
538
859
  walls.mixed && 'data-[state=unchecked]:bg-primary/45 [&>span]:translate-x-2!',
@@ -561,7 +882,7 @@ export function RoomConstructionRows({ room }: { room: RoomSelectionRecord }) {
561
882
  data-drawn-floor-switch
562
883
  >
563
884
  <Switch
564
- aria-label="Include floor"
885
+ aria-label={i18n._({ id: 'editor.panels.roomConstructionRows.includeFloor', message: 'Include floor' })}
565
886
  checked={state.floor.intent}
566
887
  className="disabled:pointer-events-none"
567
888
  disabled={!actions.add}
@@ -571,7 +892,10 @@ export function RoomConstructionRows({ room }: { room: RoomSelectionRecord }) {
571
892
  </TooltipTrigger>
572
893
  {!actions.add && (
573
894
  <TooltipContent side="top" sideOffset={6}>
574
- {DRAWN_FLOOR_SWITCH_HINT}
895
+ {i18n._({
896
+ id: 'editor.panels.roomConstructionRows.drawnFloorSwitchHint',
897
+ message: 'This floor is a drawn slab: select it to edit or delete it.',
898
+ })}
575
899
  </TooltipContent>
576
900
  )}
577
901
  </Tooltip>
@@ -580,7 +904,11 @@ export function RoomConstructionRows({ room }: { room: RoomSelectionRecord }) {
580
904
  const on = presence !== 'absent'
581
905
  return (
582
906
  <Switch
583
- aria-label={`Include ${label.toLowerCase()}`}
907
+ aria-label={i18n._({
908
+ id: 'editor.panels.roomConstructionRows.includePart',
909
+ message: '{part, select, floor {Include floor} walls {Include walls} other {Include ceiling}}',
910
+ values: { part },
911
+ })}
584
912
  checked={on}
585
913
  disabled={on ? !actions.remove : !actions.add}
586
914
  onCheckedChange={(next) =>
@@ -595,17 +923,40 @@ export function RoomConstructionRows({ room }: { room: RoomSelectionRecord }) {
595
923
  const builtOn = roomBuiltOn(nodes, zoneId)
596
924
  const floorMeta =
597
925
  state.floor.state === 'partial'
598
- ? `Partial · ${Math.round(state.floor.coverage * 100)}% covered`
926
+ ? i18n._({
927
+ id: 'editor.panels.roomConstructionRows.partialCovered',
928
+ message: 'Partial · {coverage, number, percent} covered',
929
+ values: { coverage: state.floor.coverage },
930
+ })
599
931
  : drawnSlab?.type === 'slab'
600
- ? `Drawn slab at ${length(drawnSlab.elevation)}`
932
+ ? i18n._({
933
+ id: 'editor.panels.roomConstructionRows.drawnSlabAt',
934
+ message: 'Drawn slab at {height}',
935
+ values: { height: length(drawnSlab.elevation) },
936
+ })
601
937
  : builtOn && builtOn.current.key !== null
602
938
  ? separateFloorSummary(
603
- { ...builtOn, offset: separateFloorOffset(nodes, zoneId) },
939
+ {
940
+ current: { name: floorChoiceName(nodes, builtOn.current, i18n) },
941
+ shared: builtOn.shared && { name: floorChoiceName(nodes, builtOn.shared, i18n) },
942
+ offset: separateFloorOffset(nodes, zoneId),
943
+ },
604
944
  length,
945
+ i18n,
605
946
  )
606
947
  : Math.abs(floorElevation) < 0.005
607
- ? 'At floor level'
608
- : `${length(Math.abs(floorElevation))} ${floorElevation > 0 ? 'up' : 'down'}`
948
+ ? i18n._({ id: 'editor.panels.roomConstructionRows.atFloorLevel', message: 'At floor level' })
949
+ : floorElevation > 0
950
+ ? i18n._({
951
+ id: 'editor.panels.roomConstructionRows.floorUp',
952
+ message: '{distance} up',
953
+ values: { distance: length(Math.abs(floorElevation)) },
954
+ })
955
+ : i18n._({
956
+ id: 'editor.panels.roomConstructionRows.floorDown',
957
+ message: '{distance} down',
958
+ values: { distance: length(Math.abs(floorElevation)) },
959
+ })
609
960
  // A room on its own floor moves within its plate's range (on the ground, its foundation's).
610
961
  const ownBounds = ownFloorElevationBounds(nodes, zoneId)
611
962
  const floorBase = ownBounds ? roomFloorBase(nodes, zoneId) : 0
@@ -619,27 +970,48 @@ export function RoomConstructionRows({ room }: { room: RoomSelectionRecord }) {
619
970
  const shared = sharingRoomNames(nodes, zoneId, state.walls.sharedWallIds)
620
971
  const wallsMeta =
621
972
  state.walls.state === 'partial'
622
- ? `Partial · ${count(wallIds.length, 'wall')}, ${count(openSides, 'open side')}`
623
- : count(wallIds.length, 'wall')
973
+ ? i18n._({
974
+ id: 'editor.panels.roomConstructionRows.partialWalls',
975
+ message:
976
+ 'Partial · {walls, plural, one {{wallCount} wall} other {{wallCount} walls}}, {openSides, plural, one {{openSideCount} open side} other {{openSideCount} open sides}}',
977
+ values: {
978
+ walls: wallIds.length,
979
+ openSides,
980
+ wallCount: String(wallIds.length),
981
+ openSideCount: String(openSides),
982
+ },
983
+ })
984
+ : i18n._({
985
+ id: 'editor.panels.roomConstructionRows.wallCount',
986
+ message: '{walls, plural, one {{wallCount} wall} other {{wallCount} walls}}',
987
+ values: { walls: wallIds.length, wallCount: String(wallIds.length) },
988
+ })
624
989
  const locked = roomOutsideFacesLocked(nodes, wallIds)
625
990
 
626
991
  const ceilingHeight = state.ceiling.ceilingId ? roomInsideHeight(room, nodes).ceiling : null
627
992
  const ceilingMeta =
628
993
  state.ceiling.state === 'partial'
629
- ? 'Hand-drawn ceiling'
994
+ ? i18n._({ id: 'editor.panels.roomConstructionRows.handDrawnCeiling', message: 'Hand-drawn ceiling' })
630
995
  : ceilingHeight !== null
631
- ? `At ${length(ceilingHeight)}`
632
- : 'Flat, at wall height'
996
+ ? i18n._({
997
+ id: 'editor.panels.roomConstructionRows.ceilingAt',
998
+ message: 'At {height}',
999
+ values: { height: length(ceilingHeight) },
1000
+ })
1001
+ : i18n._({ id: 'editor.panels.roomConstructionRows.flatAtWallHeight', message: 'Flat, at wall height' })
633
1002
 
634
1003
  return (
635
1004
  <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>
1005
+ <RoomTypeChoice zoneId={zoneId} />
1006
+ <DeckBoardsFields zoneId={zoneId} />
1007
+ <PorchFramingFields zoneId={zoneId} />
1008
+ <h3 className="mt-4 mb-1.5 font-medium text-muted-foreground text-xs">{i18n._({ id: 'editor.panels.roomConstructionRows.builtWith', message: 'Built with' })}</h3>
637
1009
  <div className="flex flex-col gap-1">
638
1010
  <ConstructionPart
639
- control={partSwitch('floor', 'Floor')}
1011
+ control={partSwitch('floor')}
640
1012
  expanded={expanded === 'floor'}
641
1013
  icon="/icons/floor.webp"
642
- label="Floor"
1014
+ label={i18n._({ id: 'editor.panels.roomConstructionRows.floor', message: 'Floor' })}
643
1015
  meta={floorMeta}
644
1016
  on={state.floor.state !== 'absent'}
645
1017
  footer={<SitsOn noFloor={state.floor.state === 'absent'} zoneId={zoneId} />}
@@ -656,44 +1028,71 @@ export function RoomConstructionRows({ room }: { room: RoomSelectionRecord }) {
656
1028
  <RoomOpeningsField surface="floor" zoneId={zoneId} />
657
1029
  </ConstructionPart>
658
1030
  <ConstructionPart
659
- control={partSwitch('walls', 'Walls')}
1031
+ control={partSwitch('walls')}
660
1032
  expanded={expanded === 'walls'}
661
1033
  icon="/icons/wall.webp"
662
- label="Walls"
1034
+ label={i18n._({ id: 'editor.panels.roomConstructionRows.walls', message: 'Walls' })}
663
1035
  meta={wallsMeta}
664
1036
  on={state.walls.state !== 'absent'}
665
1037
  onToggleExpanded={toggle('walls')}
666
1038
  part="walls"
667
1039
  >
668
- <PartField label="Walls">
1040
+ <PartField label={i18n._({ id: 'editor.panels.roomConstructionRows.walls', message: 'Walls' })}>
669
1041
  <span className="font-mono text-foreground tabular-nums">{wallIds.length}</span>
670
1042
  </PartField>
671
1043
  {state.walls.state === 'partial' && state.walls.actions.add && (
672
- <PartField label={`${count(openSides, 'open side')}`}>
1044
+ <PartField
1045
+ label={i18n._({
1046
+ id: 'editor.panels.roomConstructionRows.openSides',
1047
+ message:
1048
+ '{openSides, plural, one {{openSideCount} open side} other {{openSideCount} open sides}}',
1049
+ values: { openSides, openSideCount: String(openSides) },
1050
+ })}
1051
+ >
673
1052
  <button
674
1053
  className={pill}
675
1054
  onClick={() => report(addRoomConstruction(zoneId, 'walls'))}
676
1055
  type="button"
677
1056
  >
678
- Close with walls
1057
+ {i18n._({ id: 'editor.panels.roomConstructionRows.closeWithWalls', message: 'Close with walls' })}
679
1058
  </button>
680
1059
  </PartField>
681
1060
  )}
682
1061
  {shared.length > 0 && (
683
1062
  <p data-shared-walls-note>
684
- Turning walls off keeps the ones shared with {[...new Set(shared)].join(', ')}.
1063
+ {i18n._({
1064
+ id: 'editor.panels.roomConstructionRows.sharedWallsNote',
1065
+ message: 'Turning walls off keeps the ones shared with {rooms}.',
1066
+ values: {
1067
+ rooms: formatList(
1068
+ [
1069
+ ...new Set(
1070
+ shared.map((name) =>
1071
+ name
1072
+ ? displayRoomName(name, i18n)
1073
+ : i18n._({
1074
+ id: 'editor.panels.roomConstructionRows.theNextRoom',
1075
+ message: 'the next room',
1076
+ }),
1077
+ ),
1078
+ ),
1079
+ ],
1080
+ locale,
1081
+ ),
1082
+ },
1083
+ })}
685
1084
  </p>
686
1085
  )}
687
1086
  {locked !== null && (
688
1087
  <div className="mt-1 flex items-start gap-3" data-lock-outside-faces>
689
1088
  <span className="flex flex-1 flex-col gap-0.5">
690
1089
  <span className="font-medium text-[13px] text-foreground">
691
- Keep outside dimensions
1090
+ {i18n._({ id: 'editor.panels.roomConstructionRows.keepOutsideDimensions', message: 'Keep outside dimensions' })}
692
1091
  </span>
693
- <span>Walls thicken inward, so the building's outer size stays the same.</span>
1092
+ <span>{i18n._({ id: 'editor.panels.roomConstructionRows.wallsThickenInwardSoTheBuilding', message: 'Walls thicken inward, so the building\'s outer size stays the same.' })}</span>
694
1093
  </span>
695
1094
  <Switch
696
- aria-label="Keep outside dimensions"
1095
+ aria-label={i18n._({ id: 'editor.panels.roomConstructionRows.keepOutsideDimensions', message: 'Keep outside dimensions' })}
697
1096
  checked={locked}
698
1097
  onCheckedChange={(next) =>
699
1098
  report(next ? lockRoomOutsideFaces(zoneId) : unlockRoomOutsideFaces(wallIds))
@@ -703,40 +1102,40 @@ export function RoomConstructionRows({ room }: { room: RoomSelectionRecord }) {
703
1102
  )}
704
1103
  </ConstructionPart>
705
1104
  <ConstructionPart
706
- control={partSwitch('ceiling', 'Ceiling')}
1105
+ control={partSwitch('ceiling')}
707
1106
  expanded={expanded === 'ceiling'}
708
1107
  icon="/icons/ceiling.webp"
709
- label="Ceiling"
1108
+ label={i18n._({ id: 'editor.panels.roomConstructionRows.ceiling', message: 'Ceiling' })}
710
1109
  meta={ceilingMeta}
711
1110
  on={state.ceiling.state !== 'absent'}
712
1111
  onToggleExpanded={toggle('ceiling')}
713
1112
  part="ceiling"
714
1113
  >
715
1114
  {ceilingHeight !== null && (
716
- <PartField label="Height">
1115
+ <PartField label={i18n._({ id: 'editor.panels.roomConstructionRows.height', message: 'Height' })}>
717
1116
  <span className="font-mono text-foreground tabular-nums">
718
1117
  {length(ceilingHeight)}
719
1118
  </span>
720
1119
  </PartField>
721
1120
  )}
722
1121
  {state.ceiling.ceilingId && (
723
- <PartField label="Shape">
1122
+ <PartField label={i18n._({ id: 'editor.panels.roomConstructionRows.shape', message: 'Shape' })}>
724
1123
  <button className={pill} onClick={() => startCeilingEdit(zoneId)} type="button">
725
- Edit shape
1124
+ {i18n._({ id: 'editor.panels.roomConstructionRows.editShape', message: 'Edit shape' })}
726
1125
  </button>
727
1126
  </PartField>
728
1127
  )}
729
1128
  {state.ceiling.ceilingId && <RoomOpeningsField surface="ceiling" zoneId={zoneId} />}
730
1129
  {(state.ceiling.actions.useExisting || state.ceiling.actions.replace) && (
731
1130
  <div className="flex items-center gap-2" data-construction-manual-ceiling>
732
- <span className="flex-1">Hand-drawn ceiling</span>
1131
+ <span className="flex-1">{i18n._({ id: 'editor.panels.roomConstructionRows.handDrawnCeiling', message: 'Hand-drawn ceiling' })}</span>
733
1132
  {state.ceiling.actions.useExisting && (
734
1133
  <button
735
1134
  className={pill}
736
1135
  onClick={() => report(adoptExistingCeiling(zoneId))}
737
1136
  type="button"
738
1137
  >
739
- Use existing
1138
+ {i18n._({ id: 'editor.panels.roomConstructionRows.useExisting', message: 'Use existing' })}
740
1139
  </button>
741
1140
  )}
742
1141
  {state.ceiling.actions.replace && (
@@ -745,7 +1144,7 @@ export function RoomConstructionRows({ room }: { room: RoomSelectionRecord }) {
745
1144
  onClick={() => report(replaceRoomCeiling(zoneId))}
746
1145
  type="button"
747
1146
  >
748
- Replace
1147
+ {i18n._({ id: 'editor.panels.roomConstructionRows.replace', message: 'Replace' })}
749
1148
  </button>
750
1149
  )}
751
1150
  </div>
@@ -766,7 +1165,7 @@ export function RoomConstructionRows({ room }: { room: RoomSelectionRecord }) {
766
1165
  <ShapeChoice
767
1166
  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
1167
  data="add-mezzanine"
769
- label="+ Add mezzanine"
1168
+ label={i18n._({ id: 'editor.panels.roomConstructionRows.addMezzanine', message: '+ Add mezzanine' })}
770
1169
  onPick={(shape) => startMezzanineDraft(zoneId, shape)}
771
1170
  />
772
1171
  </div>