@pascal-app/editor 1.0.6 → 1.1.0

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 (382) 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 +110 -42
  6. package/src/components/editor/duplicate-node.ts +2 -1
  7. package/src/components/editor/elevation-3d-guide-layer.tsx +1 -1
  8. package/src/components/editor/first-person-controls.tsx +77 -68
  9. package/src/components/editor/fit-scene-framing.test.ts +318 -0
  10. package/src/components/editor/fit-scene-framing.ts +142 -0
  11. package/src/components/editor/floating-action-menu.tsx +31 -11
  12. package/src/components/editor/floating-building-action-menu.tsx +1 -1
  13. package/src/components/editor/floor-openings-overlay.tsx +1 -1
  14. package/src/components/editor/floor-region-controls.tsx +2 -2
  15. package/src/components/editor/floorplan-panel.test.ts +220 -0
  16. package/src/components/editor/floorplan-panel.tsx +458 -140
  17. package/src/components/editor/floorplan-wall-typing-keys.test.tsx +152 -0
  18. package/src/components/editor/floorplan-wall-typing-keys.ts +79 -0
  19. package/src/components/editor/footprint-height-handle.tsx +2 -2
  20. package/src/components/editor/grid.tsx +24 -15
  21. package/src/components/editor/group-actions.ts +19 -1
  22. package/src/components/editor/group-floating-action-menu.tsx +1 -1
  23. package/src/components/editor/group-move-3d.ts +27 -2
  24. package/src/components/editor/group-selection-box-3d.tsx +4 -1
  25. package/src/components/editor/group-transform-shared.ts +23 -1
  26. package/src/components/editor/index.tsx +150 -20
  27. package/src/components/editor/inspector-extension-actions.tsx +36 -0
  28. package/src/components/editor/localized-html.tsx +22 -0
  29. package/src/components/editor/measurement-pill.tsx +1 -1
  30. package/src/components/editor/node-action-menu.tsx +46 -12
  31. package/src/components/editor/node-arrow-handles.tsx +2 -3
  32. package/src/components/editor/open-wall-ends-3d-layer.tsx +2 -3
  33. package/src/components/editor/open-wall-ends-hint.tsx +1 -1
  34. package/src/components/editor/opening-guides-3d-layer.tsx +1 -1
  35. package/src/components/editor/quick-measurement-card.tsx +1 -1
  36. package/src/components/editor/registry-action-contributions.test.tsx +67 -6
  37. package/src/components/editor/registry-action-contributions.tsx +115 -15
  38. package/src/components/editor/riser-diagram-panel.tsx +59 -9
  39. package/src/components/editor/room-controls.tsx +4 -3
  40. package/src/components/editor/room-handles-2d.tsx +158 -7
  41. package/src/components/editor/room-handles.tsx +244 -54
  42. package/src/components/editor/room-highlight.tsx +2 -0
  43. package/src/components/editor/room-labels.tsx +33 -17
  44. package/src/components/editor/selection-manager.tsx +15 -102
  45. package/src/components/editor/separator-end-grip.tsx +74 -0
  46. package/src/components/editor/site-edge-labels.tsx +7 -3
  47. package/src/components/editor/thumbnail-generator.test.ts +15 -6
  48. package/src/components/editor/thumbnail-generator.tsx +26 -1
  49. package/src/components/editor/use-floorplan-background-placement.ts +55 -52
  50. package/src/components/editor/views/view-bar.tsx +111 -26
  51. package/src/components/editor/wall-measurement-label.tsx +31 -15
  52. package/src/components/editor/wall-move-side-handles.tsx +15 -7
  53. package/src/components/editor/wall-pull-grip.tsx +193 -0
  54. package/src/components/editor/wall-region-handles.tsx +2 -2
  55. package/src/components/editor/wall-snap-beacon-layer.tsx +134 -33
  56. package/src/components/editor-2d/floorplan-adjacent-walls-layer.test.ts +76 -0
  57. package/src/components/editor-2d/floorplan-adjacent-walls-layer.tsx +186 -0
  58. package/src/components/editor-2d/floorplan-alignment-guide-layer.tsx +63 -3
  59. package/src/components/editor-2d/floorplan-group-move.tsx +32 -4
  60. package/src/components/editor-2d/floorplan-measurement-tool-layer.test.ts +6 -0
  61. package/src/components/editor-2d/floorplan-measurement-tool-layer.tsx +33 -24
  62. package/src/components/editor-2d/floorplan-open-wall-ends-layer.tsx +12 -5
  63. package/src/components/editor-2d/floorplan-registry-action-menu.tsx +2 -1
  64. package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +29 -5
  65. package/src/components/editor-2d/floorplan-snap-beacon-layer.tsx +96 -4
  66. package/src/components/editor-2d/renderers/floorplan-dimension-edit-overlay.tsx +52 -12
  67. package/src/components/editor-2d/renderers/floorplan-draft-measurement.tsx +7 -1
  68. package/src/components/editor-2d/renderers/floorplan-placement-preview-layer.tsx +13 -1
  69. package/src/components/editor-2d/renderers/floorplan-placement-typing-hud.test.tsx +35 -0
  70. package/src/components/editor-2d/renderers/floorplan-placement-typing-hud.tsx +65 -0
  71. package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +34 -1
  72. package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +211 -39
  73. package/src/components/procedural-items/inspector.tsx +59 -12
  74. package/src/components/systems/ceiling/ceiling-bracket-batch.test.ts +2 -0
  75. package/src/components/systems/ceiling/ceiling-system.test.ts +141 -0
  76. package/src/components/systems/ceiling/ceiling-system.tsx +53 -18
  77. package/src/components/systems/roof/roof-edit-system.test.tsx +15 -1
  78. package/src/components/systems/roof/roof-edit-system.tsx +30 -19
  79. package/src/components/tools/distribution/distribution-run-tool.test.tsx +371 -3
  80. package/src/components/tools/distribution/distribution-run-tool.tsx +200 -58
  81. package/src/components/tools/item/move-tool.tsx +1 -6
  82. package/src/components/tools/item/placement-history.test.tsx +1 -1
  83. package/src/components/tools/item/placement-strategies.test.ts +64 -1
  84. package/src/components/tools/item/placement-strategies.ts +5 -9
  85. package/src/components/tools/item/use-placement-coordinator.tsx +31 -35
  86. package/src/components/tools/paint-region/wall-paint-region-tool.tsx +1 -1
  87. package/src/components/tools/registry/item-surface-move.test.ts +11 -0
  88. package/src/components/tools/registry/move-registry-node-tool.test.ts +42 -1
  89. package/src/components/tools/registry/move-registry-node-tool.tsx +38 -14
  90. package/src/components/tools/select/select-candidates.test.ts +45 -0
  91. package/src/components/tools/select/select-candidates.ts +13 -2
  92. package/src/components/tools/shared/cursor-sphere.tsx +10 -3
  93. package/src/components/tools/shared/draft-measurement-label.tsx +1 -1
  94. package/src/components/tools/shared/placement-box.tsx +2 -6
  95. package/src/components/tools/shared/placement-coordinate-input.tsx +94 -0
  96. package/src/components/tools/shared/placement-dimension-guides.tsx +1 -1
  97. package/src/components/tools/shared/segment-angle.ts +0 -4
  98. package/src/components/tools/site/terrain-brush-cursor.tsx +1 -1
  99. package/src/components/tools/tool-manager.tsx +1 -24
  100. package/src/components/tools/wall/wall-drafting.test.ts +299 -23
  101. package/src/components/tools/wall/wall-drafting.ts +132 -24
  102. package/src/components/tools/wall/wall-pull-snap.test.ts +274 -0
  103. package/src/components/tools/wall/wall-pull-snap.ts +563 -0
  104. package/src/components/tools/wall/wall-snap-geometry.test.ts +113 -1
  105. package/src/components/tools/wall/wall-snap-geometry.ts +137 -0
  106. package/src/components/tools/wall/wall-typed-entry.test.ts +99 -0
  107. package/src/components/tools/wall/wall-typed-entry.ts +70 -0
  108. package/src/components/tools/zone/zone-tool.tsx +3 -3
  109. package/src/components/ui/action-menu/action-button.tsx +2 -0
  110. package/src/components/ui/action-menu/camera-actions.tsx +8 -7
  111. package/src/components/ui/action-menu/control-modes.tsx +9 -5
  112. package/src/components/ui/action-menu/follow-pascal-pill.tsx +3 -1
  113. package/src/components/ui/action-menu/follow-pascal-toggle.tsx +7 -1
  114. package/src/components/ui/action-menu/furnish-tools.tsx +7 -6
  115. package/src/components/ui/action-menu/index.tsx +1 -0
  116. package/src/components/ui/action-menu/measurement-control.tsx +35 -29
  117. package/src/components/ui/action-menu/point-ask-button.tsx +4 -3
  118. package/src/components/ui/action-menu/structure-tools.tsx +25 -23
  119. package/src/components/ui/action-menu/view-toggles.test.ts +18 -0
  120. package/src/components/ui/action-menu/view-toggles.tsx +256 -51
  121. package/src/components/ui/build-panel/build-panel.tsx +163 -25
  122. package/src/components/ui/command-palette/editor-commands.tsx +38 -11
  123. package/src/components/ui/controls/finish-swatch.tsx +45 -19
  124. package/src/components/ui/controls/material-picker.tsx +24 -17
  125. package/src/components/ui/controls/metric-control.test.ts +52 -0
  126. package/src/components/ui/controls/metric-control.tsx +62 -24
  127. package/src/components/ui/controls/segmented-control.tsx +5 -2
  128. package/src/components/ui/controls/slider-control.test.tsx +23 -0
  129. package/src/components/ui/controls/slider-control.tsx +47 -19
  130. package/src/components/ui/controls/tool-options-panel.tsx +74 -44
  131. package/src/components/ui/floating-level-selector.tsx +100 -48
  132. package/src/components/ui/helpers/building-helper.tsx +5 -3
  133. package/src/components/ui/helpers/contextual-helper-panel.tsx +97 -37
  134. package/src/components/ui/helpers/helper-manager.test.tsx +106 -3
  135. package/src/components/ui/helpers/helper-manager.tsx +136 -57
  136. package/src/components/ui/helpers/item-helper.tsx +6 -4
  137. package/src/components/ui/helpers/registered-tool-helper.tsx +3 -1
  138. package/src/components/ui/level-duplicate-dialog.tsx +32 -23
  139. package/src/components/ui/level-wall-copy-prompt.tsx +124 -0
  140. package/src/components/ui/panels/collections/collections-popover.tsx +11 -11
  141. package/src/components/ui/panels/deck-boards-fields.tsx +136 -0
  142. package/src/components/ui/panels/floor-foundation-panel.tsx +114 -39
  143. package/src/components/ui/panels/floor-opening-panel.tsx +74 -26
  144. package/src/components/ui/panels/inspector-extensions.tsx +66 -0
  145. package/src/components/ui/panels/mobile-panel-sheet.tsx +3 -1
  146. package/src/components/ui/panels/mobile-selection-bar.tsx +34 -28
  147. package/src/components/ui/panels/multi-field-value.test.ts +27 -0
  148. package/src/components/ui/panels/multi-field-value.ts +20 -1
  149. package/src/components/ui/panels/multi-height-mode.tsx +11 -10
  150. package/src/components/ui/panels/multi-parametric-inspector.tsx +122 -34
  151. package/src/components/ui/panels/multi-selection-panel.tsx +76 -36
  152. package/src/components/ui/panels/node-display.ts +35 -24
  153. package/src/components/ui/panels/panel-manager.tsx +9 -5
  154. package/src/components/ui/panels/panel-wrapper.tsx +63 -127
  155. package/src/components/ui/panels/parametric-field-control.tsx +13 -10
  156. package/src/components/ui/panels/parametric-inspector.tsx +18 -12
  157. package/src/components/ui/panels/reference-panel.tsx +51 -35
  158. package/src/components/ui/panels/room-construction-rows.tsx +465 -66
  159. package/src/components/ui/panels/room-name-options.test.ts +36 -3
  160. package/src/components/ui/panels/room-name-options.ts +110 -11
  161. package/src/components/ui/panels/room-openings-field.tsx +22 -13
  162. package/src/components/ui/panels/room-panel.tsx +207 -100
  163. package/src/components/ui/panels/shape-choice.tsx +7 -5
  164. package/src/components/ui/panels/wall-region-list.tsx +7 -7
  165. package/src/components/ui/primitives/shortcut-token.tsx +10 -6
  166. package/src/components/ui/right-stack.test.ts +13 -6
  167. package/src/components/ui/right-stack.tsx +21 -10
  168. package/src/components/ui/sidebar/icon-rail.tsx +11 -9
  169. package/src/components/ui/sidebar/panels/items-panel/function-tree-panel.tsx +10 -10
  170. package/src/components/ui/sidebar/panels/items-panel/index.tsx +10 -9
  171. package/src/components/ui/sidebar/panels/settings-panel/index.tsx +3 -0
  172. package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +102 -32
  173. package/src/components/ui/sidebar/panels/settings-panel/load-build-dialog.tsx +1 -5
  174. package/src/components/ui/sidebar/panels/site-panel/building-tree-node.tsx +4 -3
  175. package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +5 -3
  176. package/src/components/ui/sidebar/panels/site-panel/chimney-tree-node.tsx +4 -2
  177. package/src/components/ui/sidebar/panels/site-panel/collection-tree-node.tsx +7 -5
  178. package/src/components/ui/sidebar/panels/site-panel/column-tree-node.tsx +4 -2
  179. package/src/components/ui/sidebar/panels/site-panel/default-names.ts +75 -0
  180. package/src/components/ui/sidebar/panels/site-panel/door-tree-node.tsx +4 -2
  181. package/src/components/ui/sidebar/panels/site-panel/dormer-tree-node.tsx +4 -2
  182. package/src/components/ui/sidebar/panels/site-panel/elevator-tree-node.tsx +4 -2
  183. package/src/components/ui/sidebar/panels/site-panel/fence-tree-node.tsx +4 -2
  184. package/src/components/ui/sidebar/panels/site-panel/gutter-tree-node.tsx +4 -2
  185. package/src/components/ui/sidebar/panels/site-panel/index.tsx +118 -72
  186. package/src/components/ui/sidebar/panels/site-panel/inline-rename-input.tsx +36 -3
  187. package/src/components/ui/sidebar/panels/site-panel/item-tree-node.tsx +4 -2
  188. package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +7 -4
  189. package/src/components/ui/sidebar/panels/site-panel/registry-tree-node.tsx +8 -2
  190. package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +8 -5
  191. package/src/components/ui/sidebar/panels/site-panel/shelf-tree-node.tsx +4 -2
  192. package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +9 -4
  193. package/src/components/ui/sidebar/panels/site-panel/solar-panel-tree-node.tsx +4 -2
  194. package/src/components/ui/sidebar/panels/site-panel/spawn-tree-node.tsx +4 -2
  195. package/src/components/ui/sidebar/panels/site-panel/stair-tree-node.tsx +8 -7
  196. package/src/components/ui/sidebar/panels/site-panel/tree-node-actions.tsx +7 -6
  197. package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +16 -1
  198. package/src/components/ui/sidebar/panels/site-panel/unit-tree-node.tsx +15 -8
  199. package/src/components/ui/sidebar/panels/site-panel/wall-tree-node.tsx +4 -2
  200. package/src/components/ui/sidebar/panels/site-panel/window-tree-node.tsx +4 -2
  201. package/src/components/ui/sidebar/panels/site-panel/zone-membership-checkbox.tsx +3 -1
  202. package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +19 -9
  203. package/src/components/ui/sidebar/panels/zone-panel/index.tsx +25 -14
  204. package/src/components/ui/sidebar/tab-bar.tsx +4 -1
  205. package/src/components/ui/sidebar/use-plugin-panels.tsx +19 -10
  206. package/src/components/ui/snap-target-badge.tsx +7 -6
  207. package/src/components/ui/wall-type/wall-type-editor-dialog.tsx +721 -0
  208. package/src/components/ui/wall-type/wall-type-menu.tsx +167 -0
  209. package/src/components/viewer/floorplan-compass-button.tsx +12 -3
  210. package/src/components/viewer/floorplan-preview.tsx +27 -19
  211. package/src/components/viewer/render-settings-menu.tsx +294 -0
  212. package/src/components/viewer/sky-control.tsx +710 -0
  213. package/src/components/viewer/use-viewer-ui-sounds.test.ts +15 -0
  214. package/src/components/viewer/use-viewer-ui-sounds.ts +46 -0
  215. package/src/components/viewer/viewer-controls-bar.tsx +278 -260
  216. package/src/components/viewer/viewer-hover-label.tsx +12 -4
  217. package/src/components/viewer/viewer-rooms.tsx +62 -5
  218. package/src/components/viewer/viewer-scene-header.tsx +28 -17
  219. package/src/components/viewer/viewer-stage-switcher.tsx +4 -2
  220. package/src/components/viewer-overlay.tsx +8 -0
  221. package/src/components/walkthrough-hud.tsx +75 -2
  222. package/src/hooks/use-keyboard.test.ts +85 -4
  223. package/src/hooks/use-keyboard.ts +65 -7
  224. package/src/hooks/use-selected-room.ts +12 -1
  225. package/src/i18n/floor-footprint-name.ts +30 -0
  226. package/src/i18n/level-name.ts +39 -0
  227. package/src/i18n/locales/en/messages.po +15849 -0
  228. package/src/i18n/locales/en/messages.ts +1 -0
  229. package/src/i18n/locales/en-XA/messages.ts +1 -0
  230. package/src/i18n/locales/es/messages.po +39331 -0
  231. package/src/i18n/locales/es/messages.ts +1 -0
  232. package/src/i18n/locales/fr/messages.po +39331 -0
  233. package/src/i18n/locales/fr/messages.ts +1 -0
  234. package/src/i18n/locales/ja/messages.po +39331 -0
  235. package/src/i18n/locales/ja/messages.ts +1 -0
  236. package/src/i18n/locales/zh-CN/messages.po +39331 -0
  237. package/src/i18n/locales/zh-CN/messages.ts +1 -0
  238. package/src/i18n/locales.test.ts +63 -0
  239. package/src/i18n/locales.ts +32 -0
  240. package/src/i18n/message-slot.tsx +25 -0
  241. package/src/i18n/node-messages.ts +169 -0
  242. package/src/i18n/pseudo-locale.ts +17 -0
  243. package/src/i18n/published-entry.test.ts +17 -0
  244. package/src/i18n/scene-theme-name.ts +16 -0
  245. package/src/index.tsx +224 -45
  246. package/src/lib/add-storey.test.ts +3 -3
  247. package/src/lib/contextual-help.test.ts +20 -1
  248. package/src/lib/contextual-help.ts +101 -19
  249. package/src/lib/continuation.ts +38 -11
  250. package/src/lib/elevation-guides.ts +21 -6
  251. package/src/lib/elevator-support.ts +1 -72
  252. package/src/lib/floor-footprints.test.ts +3 -2
  253. package/src/lib/floor-footprints.ts +1 -9
  254. package/src/lib/floor-foundation-support.test.ts +15 -6
  255. package/src/lib/floor-opening-draft.ts +1 -1
  256. package/src/lib/floor-region-session.ts +1 -1
  257. package/src/lib/floorplan/dimension-drive.test.ts +36 -2
  258. package/src/lib/floorplan/dimension-drive.ts +68 -3
  259. package/src/lib/floorplan/floorplan-export.test.ts +16 -5
  260. package/src/lib/floorplan/floorplan-export.tsx +13 -16
  261. package/src/lib/floorplan/floorplan-extension.test.ts +11 -12
  262. package/src/lib/floorplan/floorplan-extension.ts +37 -18
  263. package/src/lib/floorplan/floorplan-mode.test.ts +13 -6
  264. package/src/lib/floorplan/floorplan-mode.ts +14 -12
  265. package/src/lib/floorplan/floorplan-plugin-install.test.tsx +1 -1
  266. package/src/lib/floorplan/floorplan-readonly.ts +3 -3
  267. package/src/lib/floorplan/open-wall-ends.ts +2 -1
  268. package/src/lib/floorplan/schedules.ts +10 -6
  269. package/src/lib/floorplan/site-plan/site-annotations.test.ts +46 -1
  270. package/src/lib/floorplan/site-plan/site-annotations.ts +3 -0
  271. package/src/lib/floorplan/site-plan/site-parts.ts +6 -3
  272. package/src/lib/geometry-script/author.test.ts +114 -0
  273. package/src/lib/geometry-script/author.ts +55 -23
  274. package/src/lib/geometry-script/client.ts +3 -0
  275. package/src/lib/geometry-script/finish-scripts.ts +7 -7
  276. package/src/lib/gesture-lifecycle.ts +3 -1
  277. package/src/lib/glb-export.test.ts +327 -0
  278. package/src/lib/glb-export.ts +70 -2
  279. package/src/lib/grid-frame.test.ts +56 -3
  280. package/src/lib/grid-frame.ts +28 -1
  281. package/src/lib/history.test.ts +2 -1
  282. package/src/lib/hud-title.ts +48 -24
  283. package/src/lib/ifc-export.test.ts +41 -1
  284. package/src/lib/inspector-card-mode.test.ts +3 -31
  285. package/src/lib/inspector-card-mode.ts +5 -10
  286. package/src/lib/inspector-expanded.test.ts +13 -0
  287. package/src/lib/inspector-expanded.ts +13 -1
  288. package/src/lib/interaction/scope.ts +1 -0
  289. package/src/lib/keyboard-pan.test.ts +7 -0
  290. package/src/lib/keyboard-pan.ts +9 -2
  291. package/src/lib/layer-thickness.test.ts +22 -0
  292. package/src/lib/layer-thickness.ts +79 -0
  293. package/src/lib/level-selection.test.ts +18 -2
  294. package/src/lib/level-selection.ts +31 -8
  295. package/src/lib/linear-display.ts +1 -1
  296. package/src/lib/live-gesture-round-trip.test.ts +1 -1
  297. package/src/lib/lot/drop-in.test.ts +59 -1
  298. package/src/lib/lot/drop-in.ts +42 -50
  299. package/src/lib/material-swatch-style.ts +24 -0
  300. package/src/lib/measurement-label.test.ts +1 -1
  301. package/src/lib/measurement-parser.ts +7 -3
  302. package/src/lib/measurements.test.ts +24 -6
  303. package/src/lib/measurements.ts +1 -203
  304. package/src/lib/mezzanine-selection.test.ts +3 -1
  305. package/src/lib/node-action-movement.test.tsx +115 -0
  306. package/src/lib/node-action-movement.ts +2 -0
  307. package/src/lib/orbit-near-plane.test.ts +12 -0
  308. package/src/lib/orbit-near-plane.ts +13 -0
  309. package/src/lib/paint-regions.test.ts +8 -5
  310. package/src/lib/paint-scope.test.ts +53 -0
  311. package/src/lib/paint-scope.ts +18 -4
  312. package/src/lib/panel-rows.ts +17 -8
  313. package/src/lib/placement-metadata.ts +4 -0
  314. package/src/lib/point-ask/describe-target.ts +6 -8
  315. package/src/lib/point-ask/region.ts +1 -1
  316. package/src/lib/print-shell-compiler.ts +12 -1
  317. package/src/lib/print-wall-solids.test.ts +11 -0
  318. package/src/lib/print-wall-solids.ts +9 -0
  319. package/src/lib/reference-scale.ts +83 -0
  320. package/src/lib/room-assembly-overlay.ts +1 -1
  321. package/src/lib/room-built-on.test.ts +28 -17
  322. package/src/lib/room-built-on.ts +21 -17
  323. package/src/lib/room-construction-commands.ts +3 -2
  324. package/src/lib/room-construction.test.ts +28 -1
  325. package/src/lib/room-construction.ts +14 -0
  326. package/src/lib/room-controls-phase6.test.ts +141 -1
  327. package/src/lib/room-handle-drag.ts +554 -58
  328. package/src/lib/room-name-catalog.test.ts +8 -0
  329. package/src/lib/room-name-catalog.ts +8 -1
  330. package/src/lib/room-name-messages.test.ts +132 -0
  331. package/src/lib/room-name-messages.ts +147 -0
  332. package/src/lib/room-name-storage.test.ts +334 -0
  333. package/src/lib/room-push-dimensions.test.ts +38 -1
  334. package/src/lib/room-push-dimensions.ts +226 -2
  335. package/src/lib/room-selection.test.ts +23 -6
  336. package/src/lib/scene-bounds.test.ts +144 -0
  337. package/src/lib/scene-bounds.ts +79 -15
  338. package/src/lib/scene-clipboard.test.ts +70 -49
  339. package/src/lib/scene-clipboard.ts +11 -5
  340. package/src/lib/selection-cycle.ts +63 -0
  341. package/src/lib/separator-edit.test.ts +431 -0
  342. package/src/lib/separator-edit.ts +179 -0
  343. package/src/lib/separator-lines.ts +38 -0
  344. package/src/lib/sfx-bus.ts +27 -1
  345. package/src/lib/sfx-player.ts +16 -0
  346. package/src/lib/snapping-mode.test.ts +23 -1
  347. package/src/lib/tool-cursor-icon.ts +5 -1
  348. package/src/lib/tool-shortcuts.ts +130 -0
  349. package/src/lib/viewer-selection.test.ts +28 -24
  350. package/src/lib/viewer-selection.ts +21 -6
  351. package/src/lib/wall-draw-variant.test.ts +7 -6
  352. package/src/lib/wall-draw-variant.ts +107 -6
  353. package/src/lib/wall-nudge.test.ts +44 -0
  354. package/src/lib/wall-nudge.ts +131 -0
  355. package/src/lib/wall-parallel-snap.test.ts +83 -0
  356. package/src/lib/wall-parallel-snap.ts +176 -0
  357. package/src/lib/wall-placement-pointer.test.ts +23 -0
  358. package/src/lib/wall-placement-pointer.ts +18 -0
  359. package/src/lib/wall-pull.test.ts +245 -0
  360. package/src/lib/wall-pull.ts +209 -0
  361. package/src/lib/wall-push-line-up.test.ts +132 -0
  362. package/src/lib/wall-region-list.ts +17 -6
  363. package/src/lib/wall-run-snap.test.ts +188 -0
  364. package/src/lib/wall-run-snap.ts +83 -0
  365. package/src/lib/wall-type.test.ts +125 -0
  366. package/src/lib/wall-type.ts +119 -0
  367. package/src/lib/window-interaction.test.ts +44 -0
  368. package/src/lib/world-grid-snap.test.ts +139 -2
  369. package/src/lib/world-grid-snap.ts +102 -105
  370. package/src/store/use-alignment-guides.ts +27 -1
  371. package/src/store/use-editor.tsx +53 -0
  372. package/src/store/use-floorplan-annotation-visibility.test.ts +64 -0
  373. package/src/store/use-floorplan-annotation-visibility.ts +47 -22
  374. package/src/store/use-placement-typing.test.ts +165 -0
  375. package/src/store/use-placement-typing.ts +186 -0
  376. package/src/store/use-wall-snap-indicator.ts +15 -2
  377. package/src/components/tools/elevator/elevator-defaults.ts +0 -8
  378. package/src/components/tools/elevator/elevator-tool.tsx +0 -372
  379. package/src/components/tools/elevator/move-elevator-tool.tsx +0 -262
  380. package/src/lib/door-interaction.ts +0 -102
  381. package/src/lib/measurement-label.ts +0 -68
  382. package/src/lib/window-interaction.ts +0 -100
@@ -12,12 +12,16 @@ import {
12
12
  calculateLevelMiters,
13
13
  createSizedStairFlight,
14
14
  DEFAULT_ANGLE_STEP,
15
+ DEFAULT_WALL_THICKNESS,
15
16
  type DoorNode,
16
17
  DoorNode as DoorNodeSchema,
17
18
  type ElevatorNode,
18
19
  emitter,
19
20
  type FenceNode,
20
21
  type FloorplanGeometry,
22
+ formatAngleRadians,
23
+ formatLinearMeasurement,
24
+ formatNumber as formatLocalizedNumber,
21
25
  type GeometryContext,
22
26
  type GuideNode,
23
27
  getRenderableSlabPolygon,
@@ -27,6 +31,7 @@ import {
27
31
  type ItemNode,
28
32
  isCurvedWall,
29
33
  type LevelNode,
34
+ linearUnitToMeters,
30
35
  loadAssetUrl,
31
36
  newZone,
32
37
  nodeRegistry,
@@ -37,6 +42,7 @@ import {
37
42
  resolveSlabPlacementElevation,
38
43
  resolveTerrainWallConstructionOptions,
39
44
  runAsSingleSceneHistoryStep,
45
+ type SeparatorNode,
40
46
  type SiteNode,
41
47
  type SlabNode,
42
48
  SlabNode as SlabNodeSchema,
@@ -50,6 +56,7 @@ import {
50
56
  useInteractive,
51
57
  useLiveNodeOverrides,
52
58
  useLiveTransforms,
59
+ useLocale,
53
60
  useScene,
54
61
  type WallNode,
55
62
  WallNode as WallNodeSchema,
@@ -120,7 +127,13 @@ import {
120
127
  syncKeyboardPanMotion,
121
128
  } from '../../lib/keyboard-pan'
122
129
  import { measurementHint, parseMeasurement } from '../../lib/measurement-parser'
123
- import { formatLinearMeasurement, linearUnitToMeters } from '../../lib/measurements'
130
+ import {
131
+ createGuideScaleReference,
132
+ formatReferenceScaleLabel,
133
+ isValidReferenceScaleDisplayLength,
134
+ MAX_REFERENCE_SCALE_DISPLAY_LENGTH,
135
+ type ReferenceScaleUnit,
136
+ } from '../../lib/reference-scale'
124
137
  import { snapRegisteredDraftPoint } from '../../lib/registered-draft-snap'
125
138
  import { selectRoomFromHit } from '../../lib/room-selection-commands'
126
139
  import { enterBuildingFromPlanHit } from '../../lib/selection-routing'
@@ -135,7 +148,13 @@ import {
135
148
  } from '../../lib/units'
136
149
  import { cn } from '../../lib/utils'
137
150
  import { framingAnimates } from '../../lib/viewer-selection'
138
- import { getWallDrawVariant } from '../../lib/wall-draw-variant'
151
+ import {
152
+ getWallDrawVariant,
153
+ getWallLineKind,
154
+ invisibleWallDraftSnapLines,
155
+ useWallDraftThickness,
156
+ } from '../../lib/wall-draw-variant'
157
+ import { wallSideOfPlanPoint } from '../../lib/wall-placement-pointer'
139
158
  import {
140
159
  addWallPolygonDraftCorner,
141
160
  commitWallPolygonDraft,
@@ -143,6 +162,18 @@ import {
143
162
  startWallPolygonDraft,
144
163
  wallPolygonDraftWalls,
145
164
  } from '../../lib/wall-polygon-draft'
165
+ import {
166
+ endWallPullRun,
167
+ releaseWallPullDrag,
168
+ useWallPullRun,
169
+ wallDraftIsRun,
170
+ } from '../../lib/wall-pull'
171
+ import {
172
+ isWallRunFree,
173
+ publishWallRunFeedback,
174
+ snapWallRunDraftPoint,
175
+ wallRunDirection,
176
+ } from '../../lib/wall-run-snap'
146
177
  import { snapBuildingLocalToWorldGrid } from '../../lib/world-grid-snap'
147
178
  import { nextZoneName } from '../../lib/zone-name'
148
179
  import { subscribeNavigationSyncPose } from '../../store/navigation-sync-pose-store'
@@ -164,6 +195,7 @@ import useInteractionScope, {
164
195
  useReshapingNode,
165
196
  } from '../../store/use-interaction-scope'
166
197
  import usePlacementPreview from '../../store/use-placement-preview'
198
+ import { placementTypingLengthOptions, usePlacementTyping } from '../../store/use-placement-typing'
167
199
  import { expandSessionSelectionForNode } from '../../store/use-session-groups'
168
200
  import { useStairBuildPreview } from '../../store/use-stair-build-preview'
169
201
  import {
@@ -173,6 +205,7 @@ import {
173
205
  VIEW_3D,
174
206
  visibleScene,
175
207
  } from '../../store/view-layout'
208
+ import { FloorplanAdjacentWallsLayer } from '../editor-2d/floorplan-adjacent-walls-layer'
176
209
  import { FloorplanAlignmentGuideLayer } from '../editor-2d/floorplan-alignment-guide-layer'
177
210
  import { FloorplanCursorIndicatorOverlay as Editor2dFloorplanCursorIndicatorOverlay } from '../editor-2d/floorplan-cursor-indicator-overlay'
178
211
  import { FloorplanGroupActionMenu } from '../editor-2d/floorplan-group-action-menu'
@@ -193,6 +226,7 @@ import { FloorplanDraftMeasurement } from '../editor-2d/renderers/floorplan-draf
193
226
  import { FloorplanGeometryRenderer } from '../editor-2d/renderers/floorplan-geometry-renderer'
194
227
  import { FloorplanMarqueeLayer } from '../editor-2d/renderers/floorplan-marquee-layer'
195
228
  import { FloorplanPlacementPreviewLayer } from '../editor-2d/renderers/floorplan-placement-preview-layer'
229
+ import { FloorplanPlacementTypingHud } from '../editor-2d/renderers/floorplan-placement-typing-hud'
196
230
  import {
197
231
  FloorplanRegistryLayer,
198
232
  RotationAngleOverlay,
@@ -223,14 +257,15 @@ import {
223
257
  } from '../tools/select/screen-rectangle-selection'
224
258
  import { collectSelectableCandidateIds } from '../tools/select/select-candidates'
225
259
  import {
226
- formatAngleRadians,
227
260
  getAngleArcToSegmentReference,
228
261
  getAngleToSegmentReference,
229
262
  getSegmentAngleReferenceAtPoint,
230
263
  } from '../tools/shared/segment-angle'
231
264
  import {
232
265
  chainEndJoinsExistingWall,
266
+ createInvisibleWallOnCurrentLevel,
233
267
  createWallOnCurrentLevel,
268
+ getSegmentGridStep,
234
269
  isSegmentLongEnough,
235
270
  snapWallDraftPoint,
236
271
  snapWallDraftPointDetailed,
@@ -239,6 +274,7 @@ import {
239
274
  WALL_JOIN_SNAP_RADIUS,
240
275
  type WallPlanPoint,
241
276
  } from '../tools/wall/wall-drafting'
277
+ import { formatWallDraftHeading, resolveTypedWallEnd } from '../tools/wall/wall-typed-entry'
242
278
  import { FloorplanCompassButton } from '../viewer/floorplan-compass-button'
243
279
  import { resolveFloorplanBackgroundSelection } from './floorplan-background-selection'
244
280
  import {
@@ -258,6 +294,11 @@ import {
258
294
  resolveFloorplanPresentationViewBox,
259
295
  setFloorplanCompassRotation,
260
296
  } from './floorplan-navigation-presentation'
297
+ import {
298
+ endFloorplanWallTypingOnCancel,
299
+ installFloorplanWallTypingKeys,
300
+ } from './floorplan-wall-typing-keys'
301
+ import { swallowNextClick } from './handles/use-handle-drag'
261
302
  import { useFloorplanBackgroundPlacement } from './use-floorplan-background-placement'
262
303
  import { useFloorplanHitTesting } from './use-floorplan-hit-testing'
263
304
  import { useFloorplanSceneData } from './use-floorplan-scene-data'
@@ -266,6 +307,9 @@ import { sceneSpan } from './views/scene-region'
266
307
  const FALLBACK_VIEW_SIZE = 12
267
308
  const FLOORPLAN_PADDING = 2
268
309
  const MIN_VIEWPORT_WIDTH_RATIO = 0.08
310
+ // However large the fitted plan (a whole lot), zooming in goes down to a view this wide (m): a
311
+ // casing leg or a stud's edge fills the screen.
312
+ const MIN_VIEWPORT_WIDTH = 0.6
269
313
  const MAX_VIEWPORT_WIDTH_RATIO = 40
270
314
  const PANEL_MIN_WIDTH = 420
271
315
  const PANEL_MIN_HEIGHT = 320
@@ -526,8 +570,6 @@ type GuideTransformDraft = {
526
570
  rotation: number
527
571
  }
528
572
 
529
- type ReferenceScaleUnit = 'meters' | 'centimeters' | 'feet' | 'inches'
530
-
531
573
  // The in-flight reference-scale measurement. Only the per-CLICK fields live
532
574
  // here (guide + start anchor); the rubber-band's moving END is the shared
533
575
  // `useFloorplanDraftPreview.cursorPoint` (set on every move anyway), so it
@@ -835,7 +877,7 @@ function resolveFloorplanViewWidth(
835
877
  return Math.max(0.001, requestedWidth)
836
878
  }
837
879
 
838
- const minWidth = fittedViewport.width * MIN_VIEWPORT_WIDTH_RATIO
880
+ const minWidth = Math.min(fittedViewport.width * MIN_VIEWPORT_WIDTH_RATIO, MIN_VIEWPORT_WIDTH)
839
881
  const maxWidth = fittedViewport.width * MAX_VIEWPORT_WIDTH_RATIO
840
882
 
841
883
  return clamp(requestedWidth, Math.min(minWidth, currentWidth), Math.max(maxWidth, currentWidth))
@@ -2281,7 +2323,12 @@ function getWallHoverSidePaths(polygon: Point2D[], wall: WallNode): [string, str
2281
2323
  return [rightSidePath, leftSidePath]
2282
2324
  }
2283
2325
 
2284
- function buildDraftWall(levelId: string, start: WallPlanPoint, end: WallPlanPoint): WallNode {
2326
+ function buildDraftWall(
2327
+ levelId: string,
2328
+ start: WallPlanPoint,
2329
+ end: WallPlanPoint,
2330
+ thickness?: number,
2331
+ ): WallNode {
2285
2332
  return {
2286
2333
  object: 'node',
2287
2334
  id: 'wall_draft' as WallNode['id'],
@@ -2293,6 +2340,7 @@ function buildDraftWall(levelId: string, start: WallPlanPoint, end: WallPlanPoin
2293
2340
  children: [],
2294
2341
  start,
2295
2342
  end,
2343
+ thickness,
2296
2344
  frontSide: 'unknown',
2297
2345
  backSide: 'unknown',
2298
2346
  }
@@ -2438,8 +2486,8 @@ function formatMeasurement(
2438
2486
  return formatLinearMeasurement(measuredValue, unit, metricNotation)
2439
2487
  }
2440
2488
 
2441
- function formatNumber(value: number, fractionDigits = 2) {
2442
- return Number.parseFloat(value.toFixed(fractionDigits)).toString()
2489
+ export function formatFloorplanNumber(value: number, locale: string, fractionDigits = 2) {
2490
+ return formatLocalizedNumber(value, { locale, max: fractionDigits })
2443
2491
  }
2444
2492
 
2445
2493
  function convertReferenceLengthToMeters(value: number, unit: ReferenceScaleUnit) {
@@ -2465,22 +2513,52 @@ const REFERENCE_SCALE_LINGO_UNIT: Record<ReferenceScaleUnit, 'm' | 'cm' | 'ft' |
2465
2513
  /** Lingo-parse the free-text real-length input in the dropdown's unit — a
2466
2514
  * bare number means the dropdown unit, while `180cm`, `1m80` or `5'11"`
2467
2515
  * override it. Returns `null` when the text isn't a readable length. */
2468
- function parseReferenceScaleLength(raw: string, unit: ReferenceScaleUnit): number | null {
2516
+ export function parseReferenceScaleLength(
2517
+ raw: string,
2518
+ unit: ReferenceScaleUnit,
2519
+ locale: string,
2520
+ ): number | null {
2469
2521
  const unitId = REFERENCE_SCALE_LINGO_UNIT[unit]
2470
2522
  return parseMeasurement(
2471
2523
  raw,
2472
2524
  { kind: 'length', unitId },
2473
- { bareUnit: unitId, system: unit === 'feet' || unit === 'inches' ? 'us' : 'metric' },
2525
+ {
2526
+ bareUnit: unitId,
2527
+ locale,
2528
+ system: unit === 'feet' || unit === 'inches' ? 'us' : 'metric',
2529
+ },
2474
2530
  )
2475
2531
  }
2476
2532
 
2477
- function referenceScaleLengthHint(raw: string, unit: ReferenceScaleUnit): string | null {
2533
+ export function getReferenceScaleInputError(
2534
+ raw: string,
2535
+ unit: ReferenceScaleUnit,
2536
+ locale: string,
2537
+ ): string | null {
2538
+ if (raw.trim() === '') return 'Enter the real length of the line.'
2539
+ const displayLength = parseReferenceScaleLength(raw, unit, locale)
2540
+ if (displayLength === null || Number.isNaN(displayLength)) {
2541
+ return `Enter a length like 3.5, 180cm or 5'11".`
2542
+ }
2543
+ if (!(displayLength > 0)) return 'Length must be greater than 0.'
2544
+ if (!isValidReferenceScaleDisplayLength(displayLength)) {
2545
+ return `Length must be at most ${formatFloorplanNumber(MAX_REFERENCE_SCALE_DISPLAY_LENGTH, locale)} ${REFERENCE_SCALE_LINGO_UNIT[unit]}.`
2546
+ }
2547
+ return null
2548
+ }
2549
+
2550
+ function referenceScaleLengthHint(
2551
+ raw: string,
2552
+ unit: ReferenceScaleUnit,
2553
+ locale: string,
2554
+ ): string | null {
2478
2555
  const unitId = REFERENCE_SCALE_LINGO_UNIT[unit]
2479
2556
  return measurementHint(
2480
2557
  raw,
2481
2558
  { kind: 'length', unitId },
2482
2559
  {
2483
2560
  bareUnit: unitId,
2561
+ locale,
2484
2562
  system: unit === 'feet' || unit === 'inches' ? 'us' : 'metric',
2485
2563
  displayUnit: unitId,
2486
2564
  precision: 2,
@@ -2488,23 +2566,6 @@ function referenceScaleLengthHint(raw: string, unit: ReferenceScaleUnit): string
2488
2566
  )
2489
2567
  }
2490
2568
 
2491
- function getReferenceScaleUnitLabel(unit: ReferenceScaleUnit) {
2492
- switch (unit) {
2493
- case 'centimeters':
2494
- return 'cm'
2495
- case 'feet':
2496
- return 'ft'
2497
- case 'inches':
2498
- return 'in'
2499
- default:
2500
- return 'm'
2501
- }
2502
- }
2503
-
2504
- function formatReferenceScaleLabel(value: number, unit: ReferenceScaleUnit) {
2505
- return `${formatNumber(value)} ${getReferenceScaleUnitLabel(unit)}`
2506
- }
2507
-
2508
2569
  function getPolygonAreaAndCentroid(polygon: Point2D[]) {
2509
2570
  let cx = 0
2510
2571
  let cy = 0
@@ -2810,8 +2871,10 @@ function findClosestWallPoint(
2810
2871
  const distSq = (point[0] - px) ** 2 + (point[1] - pz) ** 2
2811
2872
  if (distSq < bestDistSq) {
2812
2873
  bestDistSq = distSq
2813
- // Provide an arbitrary front-facing normal so the tool knows it's a valid wall side
2814
- best = { wall, point: [px, pz], t, normal: [0, 0, 1] }
2874
+ // The face the cursor is on, as a 3D hover's normal reports it: a door
2875
+ // swings and a window faces toward the cursor's side, in 2D as in 3D.
2876
+ const side = wallSideOfPlanPoint(wall, [point[0], point[1]])
2877
+ best = { wall, point: [px, pz], t, normal: [0, 0, side === 'front' ? 1 : -1] }
2815
2878
  }
2816
2879
  }
2817
2880
 
@@ -3281,6 +3344,7 @@ function FloorplanReferenceScaleLayer({
3281
3344
  unit: 'metric' | 'imperial'
3282
3345
  unitsPerPixel: number
3283
3346
  }) {
3347
+ const locale = useLocale()
3284
3348
  const visibleReferences = guides
3285
3349
  .filter((guide) => guideUi[guide.id]?.scaleReferenceVisible !== false)
3286
3350
  .map((guide) => guide.scaleReference)
@@ -3290,16 +3354,19 @@ function FloorplanReferenceScaleLayer({
3290
3354
 
3291
3355
  return (
3292
3356
  <>
3293
- {visibleReferences.map((reference, index) => (
3294
- <FloorplanReferenceScaleLine
3295
- end={reference.end}
3296
- key={`${reference.label}-${index}-${reference.start.join(',')}-${reference.end.join(',')}`}
3297
- label={reference.label}
3298
- palette={palette}
3299
- start={reference.start}
3300
- unitsPerPixel={unitsPerPixel}
3301
- />
3302
- ))}
3357
+ {visibleReferences.map((reference, index) => {
3358
+ const label = formatReferenceScaleLabel(reference, locale)
3359
+ return (
3360
+ <FloorplanReferenceScaleLine
3361
+ end={reference.end}
3362
+ key={`${index}-${reference.start.join(',')}-${reference.end.join(',')}`}
3363
+ label={label}
3364
+ palette={palette}
3365
+ start={reference.start}
3366
+ unitsPerPixel={unitsPerPixel}
3367
+ />
3368
+ )
3369
+ })}
3303
3370
  {draft?.start && (
3304
3371
  <FloorplanReferenceScaleDraftLine
3305
3372
  palette={palette}
@@ -4442,7 +4509,10 @@ function FloorplanLinearDraftLayer({
4442
4509
  sceneRotationDeg: number
4443
4510
  }) {
4444
4511
  const metricNotation = useViewer((state) => state.metricNotation)
4512
+ // The thickness the wall commits at, as the 3D draft shows it.
4513
+ const wallDraftThickness = useWallDraftThickness(DEFAULT_WALL_THICKNESS)
4445
4514
  const wallDraftEnd = useFloorplanDraftPreview((s) => s.wallDraftEnd)
4515
+ const wallTypingActive = usePlacementTyping((s) => s.isActive)
4446
4516
  const fenceDraftEnd = useFloorplanDraftPreview((s) => s.fenceDraftEnd)
4447
4517
  const roofDraftEnd = useFloorplanDraftPreview((s) => s.roofDraftEnd)
4448
4518
  const roofDraftQuarterTurn = useFloorplanDraftPreview((s) => s.roofDraftQuarterTurn)
@@ -4465,10 +4535,12 @@ function FloorplanLinearDraftLayer({
4465
4535
  ) {
4466
4536
  return null
4467
4537
  }
4468
- const draftWall = getSharedFloorplanWall(buildDraftWall(levelId, wallDraftStart, wallDraftEnd))
4538
+ const draftWall = getSharedFloorplanWall(
4539
+ buildDraftWall(levelId, wallDraftStart, wallDraftEnd, wallDraftThickness),
4540
+ )
4469
4541
  // Keep the live draft preview cheap; full level-wide mitering here runs on every mouse move.
4470
4542
  return getWallPlanFootprint(draftWall, EMPTY_WALL_MITER_DATA)
4471
- }, [levelId, wallDraftStart, wallDraftEnd])
4543
+ }, [levelId, wallDraftStart, wallDraftEnd, wallDraftThickness])
4472
4544
 
4473
4545
  // Rectangle mode drafts four walls at once; they read exactly like a line
4474
4546
  // draft — same mitered footprints, measurement plates and axis guides.
@@ -4766,6 +4838,7 @@ function FloorplanLinearDraftLayer({
4766
4838
  <FloorplanDraftMeasurement
4767
4839
  labelBackground={isDark ? '#0f172a' : '#ffffff'}
4768
4840
  labelText={isDark ? '#e2e8f0' : '#171717'}
4841
+ lengthHidden={wallTypingActive}
4769
4842
  measurement={draftWallMeasurement}
4770
4843
  measurementStroke={measurementStroke}
4771
4844
  sceneRotationDeg={sceneRotationDeg}
@@ -4773,6 +4846,19 @@ function FloorplanLinearDraftLayer({
4773
4846
  />
4774
4847
  )}
4775
4848
 
4849
+ {wallTypingActive && isWallBuildActive && wallDraftStart ? (
4850
+ <FloorplanPlacementTypingHud
4851
+ placeholders={[
4852
+ draftWallMeasurement?.lengthLabel ?? '',
4853
+ wallDraftEnd ? formatWallDraftHeading(wallDraftStart, wallDraftEnd) : '',
4854
+ ]}
4855
+ sceneRotationDeg={sceneRotationDeg}
4856
+ unitsPerPixel={unitsPerPixel}
4857
+ x={(draftWallMeasurement?.midpoint ?? wallDraftStart)[0]}
4858
+ y={(draftWallMeasurement?.midpoint ?? wallDraftStart)[1]}
4859
+ />
4860
+ ) : null}
4861
+
4776
4862
  {polygonRoomDraft?.map((points, index) => (
4777
4863
  <FloorplanDraftLayer
4778
4864
  anchorFill={draftStroke}
@@ -4838,6 +4924,7 @@ export function FloorplanPanel({
4838
4924
  floorplanSceneSlot?: ReactNode
4839
4925
  }) {
4840
4926
  useFloorplanCameraSyncBridge()
4927
+ const locale = useLocale()
4841
4928
  const viewportHostRef = useRef<HTMLDivElement>(null)
4842
4929
  const svgRef = useRef<SVGSVGElement>(null)
4843
4930
  const floorplanBackgroundRef = useRef<SVGRectElement>(null)
@@ -4849,6 +4936,9 @@ export function FloorplanPanel({
4849
4936
  const floorplanSpacePanPressedRef = useRef(false)
4850
4937
  const floorplanNavigationClickSuppressedRef = useRef(false)
4851
4938
  const guideInteractionRef = useRef<GuideInteractionState | null>(null)
4939
+ // Late-bound so the window keydown effect can commit the wall draft at the
4940
+ // typed length without an ordering dependency on `handleWallPlacementPoint`.
4941
+ const wallPlacementPointRef = useRef<((point: WallPlanPoint) => void) | null>(null)
4852
4942
  const guideTransformDraftRef = useRef<GuideTransformDraft | null>(null)
4853
4943
  const pendingFenceDragRef = useRef<PendingFenceDragState | null>(null)
4854
4944
  const wallEndpointDragRef = useRef<WallEndpointDragState | null>(null)
@@ -5042,6 +5132,28 @@ export function FloorplanPanel({
5042
5132
  // Walls committed by the current 2D-only chain — exclusion set for the
5043
5133
  // T-junction chain-termination test (mirrors the 3D tool's `chainWallIds`).
5044
5134
  const wallChainWallIdsRef = useRef<string[]>([])
5135
+ // Split view: the 3D wall tool owns the commit and publishes each segment's
5136
+ // resolved end as the chain start. Re-base this panel's draft onto it so a
5137
+ // typed Enter in the plan projects from the segment the 3D chain is
5138
+ // actually drafting — without this, `draftStart` stays on the last point
5139
+ // the panel itself placed and the typed commit uses a stale origin. In
5140
+ // 2D-only the panel is the committer and never publishes, so this
5141
+ // subscription stays dormant there.
5142
+ useEffect(() => {
5143
+ if (!(mode === 'build' && tool === 'wall')) return
5144
+ return useSegmentDraftChain.subscribe((state, previousState) => {
5145
+ const next = state.wall
5146
+ if (next === previousState.wall) return
5147
+ if (!next) return
5148
+ setDraftStart((current) =>
5149
+ current && current[0] === next[0] && current[1] === next[1] ? current : next,
5150
+ )
5151
+ setWallChainFirstVertex((current) => current ?? next)
5152
+ })
5153
+ }, [mode, tool])
5154
+ // Direction of the run's last wall (a pulled run's source first): the next
5155
+ // holds square to it. Null before the run has a wall.
5156
+ const wallRunReferenceRef = useRef<WallPlanPoint | null>(null)
5045
5157
  const setDraftEnd = useCallback(
5046
5158
  (next: WallPlanPoint | null | ((prev: WallPlanPoint | null) => WallPlanPoint | null)) => {
5047
5159
  const store = useFloorplanDraftPreview.getState()
@@ -5049,6 +5161,8 @@ export function FloorplanPanel({
5049
5161
  },
5050
5162
  [],
5051
5163
  )
5164
+ // The pointer's own (snapped) wall draft end; a typed session re-projects it.
5165
+ const wallDraftPointerRef = useRef<WallPlanPoint | null>(null)
5052
5166
  const [fenceDraftStart, setFenceDraftStart] = useState<WallPlanPoint | null>(null)
5053
5167
  const setFenceDraftEnd = useCallback(
5054
5168
  (next: WallPlanPoint | null | ((prev: WallPlanPoint | null) => WallPlanPoint | null)) => {
@@ -5057,14 +5171,10 @@ export function FloorplanPanel({
5057
5171
  },
5058
5172
  [],
5059
5173
  )
5060
- const [roofDraftStart, setRoofDraftStart] = useState<WallPlanPoint | null>(null)
5061
- const setRoofDraftEnd = useCallback(
5062
- (next: WallPlanPoint | null | ((prev: WallPlanPoint | null) => WallPlanPoint | null)) => {
5063
- const store = useFloorplanDraftPreview.getState()
5064
- store.setRoofDraftEnd(typeof next === 'function' ? next(store.roofDraftEnd) : next)
5065
- },
5066
- [],
5067
- )
5174
+ // The roof tool that owns the input (the 3D tool, or the plan's own in 2D-only view) writes the
5175
+ // roof draft from the corners it snaps and commits; the plan only draws it, so its rectangle is
5176
+ // the roof the next click makes.
5177
+ const roofDraftStart = useFloorplanDraftPreview((s) => s.roofDraftStart)
5068
5178
  const [ceilingDraftPoints, setCeilingDraftPoints] = useState<WallPlanPoint[]>([])
5069
5179
  const [slabDraftPoints, setSlabDraftPoints] = useState<WallPlanPoint[]>([])
5070
5180
  const slabPlacementBaseRef = useRef<number | null>(null)
@@ -5077,9 +5187,6 @@ export function FloorplanPanel({
5077
5187
  useEffect(() => {
5078
5188
  useFloorplanDraftPreview.getState().setFenceDraftStart(fenceDraftStart)
5079
5189
  }, [fenceDraftStart])
5080
- useEffect(() => {
5081
- useFloorplanDraftPreview.getState().setRoofDraftStart(roofDraftStart)
5082
- }, [roofDraftStart])
5083
5190
  const [zoneDraftPoints, setZoneDraftPoints] = useState<WallPlanPoint[]>([])
5084
5191
  const [siteBoundaryDraft, setSiteBoundaryDraft] = useState<SiteBoundaryDraft | null>(null)
5085
5192
  const [siteVertexDragState, setSiteVertexDragState] = useState<SiteVertexDragState | null>(null)
@@ -7161,8 +7268,8 @@ export function FloorplanPanel({
7161
7268
  return
7162
7269
  }
7163
7270
 
7164
- const displayLength = parseReferenceScaleLength(referenceScaleValue, referenceScaleUnit)
7165
- if (!(displayLength && displayLength > 0)) {
7271
+ const displayLength = parseReferenceScaleLength(referenceScaleValue, referenceScaleUnit, locale)
7272
+ if (displayLength === null || !isValidReferenceScaleDisplayLength(displayLength)) {
7166
7273
  return
7167
7274
  }
7168
7275
 
@@ -7199,14 +7306,17 @@ export function FloorplanPanel({
7199
7306
  {
7200
7307
  position: nextGuidePosition,
7201
7308
  scale: nextGuideScale,
7202
- scaleReference: {
7203
- start: pendingReferenceScale.start,
7204
- end: scaledEnd,
7205
- realLengthMeters,
7206
- measuredLengthUnits: scaledMeasuredLengthUnits,
7207
- metersPerUnit,
7208
- label: formatReferenceScaleLabel(displayLength, referenceScaleUnit),
7209
- },
7309
+ scaleReference: createGuideScaleReference(
7310
+ {
7311
+ start: pendingReferenceScale.start,
7312
+ end: scaledEnd,
7313
+ realLengthMeters,
7314
+ measuredLengthUnits: scaledMeasuredLengthUnits,
7315
+ metersPerUnit,
7316
+ },
7317
+ displayLength,
7318
+ referenceScaleUnit,
7319
+ ),
7210
7320
  } as Partial<GuideNode>,
7211
7321
  )
7212
7322
  setGuideLocked(pendingReferenceScale.guideId, true)
@@ -7215,6 +7325,7 @@ export function FloorplanPanel({
7215
7325
  setPendingReferenceScale(null)
7216
7326
  }, [
7217
7327
  guideById,
7328
+ locale,
7218
7329
  pendingReferenceScale,
7219
7330
  referenceScaleUnit,
7220
7331
  referenceScaleValue,
@@ -7853,6 +7964,26 @@ export function FloorplanPanel({
7853
7964
  }
7854
7965
  }, [isFloorplanOpen, stopFloorplanViewAnimation])
7855
7966
 
7967
+ // Typing reshapes the plan's own draft without a pointer move.
7968
+ useEffect(() => {
7969
+ if (!draftStart) return
7970
+ return usePlacementTyping.subscribe((state, previous) => {
7971
+ if (state.fields === previous.fields && state.isActive === previous.isActive) return
7972
+ const pointer = wallDraftPointerRef.current
7973
+ if (!pointer) return
7974
+ setDraftEnd(
7975
+ (state.isActive &&
7976
+ resolveTypedWallEnd(
7977
+ draftStart,
7978
+ pointer,
7979
+ state.fields,
7980
+ placementTypingLengthOptions(unit, metricNotation),
7981
+ getSegmentGridStep(),
7982
+ )) ||
7983
+ pointer,
7984
+ )
7985
+ })
7986
+ }, [draftStart, metricNotation, setDraftEnd, unit])
7856
7987
  // Clearing an open wall draft drops a Polygon room in progress (nothing of
7857
7988
  // it was written); paths that keep it commit first.
7858
7989
  const clearWallPlacementDraft = useCallback(() => {
@@ -7861,20 +7992,31 @@ export function FloorplanPanel({
7861
7992
  setWallChainFirstVertex(null)
7862
7993
  wallConstructionOptionsRef.current = undefined
7863
7994
  wallChainWallIdsRef.current = []
7995
+ wallRunReferenceRef.current = null
7864
7996
  setDraftEnd(null)
7997
+ wallDraftPointerRef.current = null
7998
+ usePlacementTyping.getState().clear()
7865
7999
  useSegmentDraftChain.getState().clear('wall')
8000
+ // A run pulled from a wall end ends with the plan's draft: back to Select.
8001
+ endWallPullRun()
7866
8002
  }, [setDraftEnd])
7867
8003
  const clearFencePlacementDraft = useCallback(() => {
7868
8004
  setFenceDraftStart(null)
7869
8005
  setFenceDraftEnd(null)
7870
8006
  }, [setFenceDraftEnd])
7871
8007
  const clearRoofPlacementDraft = useCallback(() => {
7872
- setRoofDraftStart(null)
7873
- setRoofDraftEnd(null)
7874
- }, [setRoofDraftEnd])
8008
+ const draft = useFloorplanDraftPreview.getState()
8009
+ draft.setRoofDraftStart(null)
8010
+ draft.setRoofDraftEnd(null)
8011
+ }, [])
7875
8012
  const clearCeilingPlacementDraft = useCallback(() => {
7876
8013
  setCeilingDraftPoints([])
7877
8014
  }, [])
8015
+ // Beside the 3D view its ceiling tool commits; in 2D-only view the plan does. Each keeps its own
8016
+ // corners, so a polygon begun under one owner must not be closed against the other's.
8017
+ useEffect(() => {
8018
+ if (shownScene) clearCeilingPlacementDraft()
8019
+ }, [clearCeilingPlacementDraft, shownScene])
7878
8020
  const clearSlabPlacementDraft = useCallback(() => {
7879
8021
  setSlabDraftPoints([])
7880
8022
  slabPlacementBaseRef.current = null
@@ -7961,6 +8103,7 @@ export function FloorplanPanel({
7961
8103
 
7962
8104
  useEffect(() => {
7963
8105
  const handleCancel = () => {
8106
+ if (endFloorplanWallTypingOnCancel()) return
7964
8107
  clearDraft()
7965
8108
  }
7966
8109
 
@@ -7970,6 +8113,44 @@ export function FloorplanPanel({
7970
8113
  }
7971
8114
  }, [clearDraft])
7972
8115
 
8116
+ // Split view: when the 3D wall tool stops drafting (auto-close,
8117
+ // T-junction, single-segment, cancel) it clears the published chain start.
8118
+ // That null clear must tear this panel's draft down too, or the 2D side
8119
+ // keeps a live draft that swallows typing keys against a chain the 3D
8120
+ // view already ended. Lives here (below `clearDraft`)
8121
+ // rather than beside the re-base subscription so the teardown stays
8122
+ // reachable.
8123
+ useEffect(() => {
8124
+ if (!(mode === 'build' && tool === 'wall')) return
8125
+ return useSegmentDraftChain.subscribe((state, previousState) => {
8126
+ if (state.wall === null && previousState.wall !== null) clearDraft()
8127
+ })
8128
+ }, [mode, tool, clearDraft])
8129
+
8130
+ // A wall end's pull grip pressed in the plan opens the Walls run at that
8131
+ // end: start the plan's draft there, as if the end had been clicked. A pull
8132
+ // from the 3D view drafts there (the plan beside it follows its moves).
8133
+ const wallPullRun = useWallPullRun((state) => state.run)
8134
+ const seededWallPullSeqRef = useRef<number | null>(null)
8135
+ useEffect(() => {
8136
+ if (!(wallPullRun && wallPullRun.origin === '2d' && isWallBuildActive && levelId)) return
8137
+ if (seededWallPullSeqRef.current === wallPullRun.seq) return
8138
+ seededWallPullSeqRef.current = wallPullRun.seq
8139
+ const start = wallPullRun.start
8140
+ wallConstructionOptionsRef.current = resolveTerrainWallConstructionOptions(
8141
+ useScene.getState().nodes,
8142
+ levelId,
8143
+ start,
8144
+ useEditor.getState().toolDefaults.wall,
8145
+ )
8146
+ wallChainWallIdsRef.current = []
8147
+ wallRunReferenceRef.current = wallPullRun.reference
8148
+ setDraftStart(start)
8149
+ setWallChainFirstVertex(start)
8150
+ setDraftEnd(start)
8151
+ setCursorPoint(start)
8152
+ }, [isWallBuildActive, levelId, setCursorPoint, setDraftEnd, wallPullRun])
8153
+
7973
8154
  const createZoneOnCurrentLevel = useCallback(
7974
8155
  (points: WallPlanPoint[]) => {
7975
8156
  if (!levelId) {
@@ -8219,6 +8400,31 @@ export function FloorplanPanel({
8219
8400
  return unsubscribe
8220
8401
  }, [movingNode, scheduleMovingFloorplanNodeRefresh])
8221
8402
 
8403
+ // Read by the typed-entry listener, registered once so it stays ahead of
8404
+ // the global shortcuts.
8405
+ const wallTypingSourceRef = useRef({
8406
+ draftStart: null as WallPlanPoint | null,
8407
+ lengthOptions: placementTypingLengthOptions(unit, metricNotation),
8408
+ })
8409
+ wallTypingSourceRef.current = {
8410
+ draftStart: isWallBuildActive ? draftStart : null,
8411
+ lengthOptions: placementTypingLengthOptions(unit, metricNotation),
8412
+ }
8413
+ useEffect(
8414
+ () =>
8415
+ installFloorplanWallTypingKeys({
8416
+ getDraft: () => {
8417
+ const { draftStart: start, lengthOptions } = wallTypingSourceRef.current
8418
+ if (!start) return null
8419
+ const pointer =
8420
+ wallDraftPointerRef.current ?? useFloorplanDraftPreview.getState().wallDraftEnd ?? start
8421
+ return { start, pointer, lengthOptions }
8422
+ },
8423
+ commit: (pointer) => wallPlacementPointRef.current?.(pointer),
8424
+ }),
8425
+ [],
8426
+ )
8427
+
8222
8428
  useEffect(() => {
8223
8429
  const handleKeyDown = (event: KeyboardEvent) => {
8224
8430
  const target = event.target as HTMLElement | null
@@ -8297,7 +8503,16 @@ export function FloorplanPanel({
8297
8503
  window.removeEventListener('keyup', handleKeyUp)
8298
8504
  window.removeEventListener('blur', handleBlur)
8299
8505
  }
8300
- }, [isFloorplanOpen, isStairBuildActive, movingNode])
8506
+ }, [
8507
+ isFloorplanOpen,
8508
+ isStairBuildActive,
8509
+ movingNode,
8510
+ isWallBuildActive,
8511
+ draftStart,
8512
+ unit,
8513
+ metricNotation,
8514
+ setDraftEnd,
8515
+ ])
8301
8516
 
8302
8517
  useEffect(() => {
8303
8518
  const handleWindowPointerMove = (event: PointerEvent) => {
@@ -9284,14 +9499,6 @@ export function FloorplanPanel({
9284
9499
  setCursorPoint((previousPoint) =>
9285
9500
  previousPoint && pointsEqual(previousPoint, snappedPoint) ? previousPoint : snappedPoint,
9286
9501
  )
9287
-
9288
- if (roofDraftStart) {
9289
- setRoofDraftEnd((previousPoint) =>
9290
- previousPoint && pointsEqual(previousPoint, snappedPoint)
9291
- ? previousPoint
9292
- : snappedPoint,
9293
- )
9294
- }
9295
9502
  return
9296
9503
  }
9297
9504
 
@@ -9506,41 +9713,82 @@ export function FloorplanPanel({
9506
9713
  return
9507
9714
  }
9508
9715
 
9509
- // Wall draft: grid + magnetic snap, then Figma-style alignment.
9510
- // While a draft is open the segment locks to 15° rays from its start.
9511
- // Snapping is governed by the snapping mode (`'off'` is the bypass);
9512
- // there is no Shift hold-to-bypass.
9513
- const wallAngleSnap = draftStart !== null && isAngleSnapActive()
9514
- const wallSnap = snapWallDraftPointDetailed({
9515
- point: planPoint,
9516
- walls: [...walls, ...wallPolygonDraftWalls()],
9517
- start: draftStart ?? undefined,
9518
- angleSnap: wallAngleSnap,
9519
- magnetic: isMagneticSnapActive(),
9520
- })
9521
- const wallSnapped = wallSnap.point
9522
- // Locked onto existing geometry (corner / midpoint / crossing / edge) →
9523
- // that snap wins, so skip Figma alignment and stand the beacon there.
9524
- const lockedToWall = wallSnap.snap !== null
9525
- let snappedPoint = wallSnapped
9526
- if (lockedToWall) {
9527
- useAlignmentGuides.getState().clear()
9716
+ // Wall draft: grid + magnetic snap, then Figma-style alignment. A Walls
9717
+ // segment locks to 15° rays from its start; a run's segment (a Polygon
9718
+ // side, a pulled run) holds square to the last wall and the house's axes,
9719
+ // with projection guides and a labelled snap marker
9720
+ // (`snapWallRunDraftPoint`). Snapping is governed by the snapping mode
9721
+ // (`'off'` is the bypass); Alt frees a run's segment. An invisible wall
9722
+ // draft catches the invisible walls already drawn as well.
9723
+ let snappedPoint: WallPlanPoint
9724
+ const draftSnapWalls = [
9725
+ ...walls,
9726
+ ...wallPolygonDraftWalls(),
9727
+ ...invisibleWallDraftSnapLines(),
9728
+ ]
9729
+ if (draftStart && wallDraftIsRun()) {
9730
+ const runSnap = snapWallRunDraftPoint({
9731
+ point: planPoint,
9732
+ start: draftStart,
9733
+ runStart: wallChainFirstVertex ?? draftStart,
9734
+ reference: wallRunReferenceRef.current,
9735
+ walls: draftSnapWalls,
9736
+ free: isWallRunFree(event),
9737
+ })
9738
+ snappedPoint = runSnap.point
9739
+ publishWallRunFeedback(runSnap)
9528
9740
  } else {
9529
- // Alignment lines show in every mode; the pull applies only when
9530
- // magnetic ('lines') and the segment isn't angle-locked.
9531
- snappedPoint = alignFloorplanDraftPoint(wallSnapped, {
9532
- applySnap: isMagneticSnapActive() && !wallAngleSnap,
9741
+ const wallAngleSnap = draftStart !== null && isAngleSnapActive()
9742
+ const wallSnap = snapWallDraftPointDetailed({
9743
+ point: planPoint,
9744
+ walls: draftSnapWalls,
9745
+ start: draftStart ?? undefined,
9746
+ angleSnap: wallAngleSnap,
9747
+ magnetic: isMagneticSnapActive(),
9748
+ // Same as the click: an end aimed into a wall's body joins it.
9749
+ captureWallBodies: true,
9533
9750
  })
9751
+ const wallSnapped = wallSnap.point
9752
+ // Locked onto existing geometry (corner / midpoint / crossing / edge) →
9753
+ // that snap wins, so skip Figma alignment and stand the beacon there.
9754
+ const lockedToWall = wallSnap.snap !== null
9755
+ snappedPoint = wallSnapped
9756
+ if (lockedToWall) {
9757
+ useAlignmentGuides.getState().clear()
9758
+ } else {
9759
+ // Alignment lines show in every mode; the pull applies only when
9760
+ // magnetic ('lines') and the segment isn't angle-locked.
9761
+ snappedPoint = alignFloorplanDraftPoint(wallSnapped, {
9762
+ applySnap: isMagneticSnapActive() && !wallAngleSnap,
9763
+ })
9764
+ }
9765
+ useWallSnapIndicator
9766
+ .getState()
9767
+ .set(
9768
+ wallSnap.snap ? { x: snappedPoint[0], z: snappedPoint[1], kind: wallSnap.snap } : null,
9769
+ )
9534
9770
  }
9535
- useWallSnapIndicator
9536
- .getState()
9537
- .set(wallSnap.snap ? { x: snappedPoint[0], z: snappedPoint[1], kind: wallSnap.snap } : null)
9538
9771
 
9539
9772
  // Emit `grid:move` so the registry-driven wall tool's 3D preview
9540
9773
  // tracks the cursor. The local draftEnd update below is what
9541
9774
  // drives the 2D draft polygon — both views update in parallel.
9775
+ // An open typed session re-projects the pointer onto what was typed;
9776
+ // the 3D tool gets the pointer itself and projects it the same way.
9777
+ wallDraftPointerRef.current = draftStart ? snappedPoint : null
9778
+ const wallTyping = usePlacementTyping.getState()
9779
+ const draftEndPoint =
9780
+ (draftStart &&
9781
+ wallTyping.isActive &&
9782
+ resolveTypedWallEnd(
9783
+ draftStart,
9784
+ snappedPoint,
9785
+ wallTyping.fields,
9786
+ placementTypingLengthOptions(unit, metricNotation),
9787
+ getSegmentGridStep(),
9788
+ )) ||
9789
+ snappedPoint
9542
9790
  emitFloorplanGridEvent('move', snappedPoint, event)
9543
- setCursorPoint(snappedPoint)
9791
+ setCursorPoint(draftEndPoint)
9544
9792
 
9545
9793
  if (!draftStart) {
9546
9794
  return
@@ -9549,13 +9797,13 @@ export function FloorplanPanel({
9549
9797
  setDraftEnd((previousEnd) => {
9550
9798
  if (
9551
9799
  !previousEnd ||
9552
- previousEnd[0] !== snappedPoint[0] ||
9553
- previousEnd[1] !== snappedPoint[1]
9800
+ previousEnd[0] !== draftEndPoint[0] ||
9801
+ previousEnd[1] !== draftEndPoint[1]
9554
9802
  ) {
9555
9803
  sfxEmitter.emit('sfx:grid-snap')
9556
9804
  }
9557
9805
 
9558
- return snappedPoint
9806
+ return draftEndPoint
9559
9807
  })
9560
9808
  },
9561
9809
  [
@@ -9590,17 +9838,21 @@ export function FloorplanPanel({
9590
9838
  isRoofBuildActive,
9591
9839
  isWallBuildActive,
9592
9840
  levelId,
9841
+ // `unit` / `metricNotation` feed the typed-length projection helpers
9842
+ // shared with the commit path; listed so a unit change re-binds the
9843
+ // move preview to the new bare-unit parse.
9844
+ metricNotation,
9845
+ unit,
9593
9846
  publishFloorplanNavigationPose,
9594
9847
  referenceScaleDraft,
9595
- roofDraftStart,
9596
9848
  siteVertexDragState,
9597
9849
  surfaceSize.height,
9598
9850
  surfaceSize.width,
9599
9851
  svgAspectRatio,
9600
9852
  walls,
9853
+ wallChainFirstVertex,
9601
9854
  setCursorPoint,
9602
9855
  setDraftEnd,
9603
- setRoofDraftEnd,
9604
9856
  setFenceDraftEnd,
9605
9857
  ],
9606
9858
  )
@@ -9776,7 +10028,21 @@ export function FloorplanPanel({
9776
10028
  )
9777
10029
 
9778
10030
  const handleWallPlacementPoint = useCallback(
9779
- (point: WallPlanPoint) => {
10031
+ (rawPoint: WallPlanPoint) => {
10032
+ // An open typed session commits what its preview shows: the pointer
10033
+ // re-projected onto what was typed, never re-snapped.
10034
+ const typing = usePlacementTyping.getState()
10035
+ const typedEnd =
10036
+ draftStart && typing.isActive
10037
+ ? resolveTypedWallEnd(
10038
+ draftStart,
10039
+ rawPoint,
10040
+ typing.fields,
10041
+ placementTypingLengthOptions(unit, metricNotation),
10042
+ getSegmentGridStep(),
10043
+ )
10044
+ : null
10045
+ const point: WallPlanPoint = typedEnd ?? rawPoint
9780
10046
  if (!draftStart) {
9781
10047
  wallConstructionOptionsRef.current = levelId
9782
10048
  ? resolveTerrainWallConstructionOptions(
@@ -9794,6 +10060,7 @@ export function FloorplanPanel({
9794
10060
  ) {
9795
10061
  startWallPolygonDraft(levelId as AnyNodeId, point, wallConstructionOptionsRef.current)
9796
10062
  }
10063
+ wallRunReferenceRef.current = null
9797
10064
  setDraftStart(point)
9798
10065
  setWallChainFirstVertex(point)
9799
10066
  setDraftEnd(point)
@@ -9804,6 +10071,8 @@ export function FloorplanPanel({
9804
10071
  if (!isSegmentLongEnough(draftStart, point)) {
9805
10072
  return
9806
10073
  }
10074
+ // The run's next segment holds square to this one.
10075
+ const placedDirection = wallRunDirection(draftStart, point)
9807
10076
 
9808
10077
  // The 3D wall tool's `grid:click` listener
9809
10078
  // (`packages/nodes/src/wall/tool.tsx`) owns the wall-create
@@ -9820,7 +10089,7 @@ export function FloorplanPanel({
9820
10089
  // ceiling 2D-only committers: create locally here, gated on the
9821
10090
  // view, so split / 3D keep their single-owner tool commit.
9822
10091
  const viewIs2DOnly = visibleScene(useEditor.getState()) === '2d'
9823
- let createdWall: WallNode | null = null
10092
+ let createdWall: WallNode | SeparatorNode | null = null
9824
10093
  // 2D-only Polygon room: the corner joins the draft; nothing is written
9825
10094
  // until it closes, seals or tees (then every wall at once).
9826
10095
  if (viewIs2DOnly && getWallDrawVariant() === 'polygon') {
@@ -9830,17 +10099,19 @@ export function FloorplanPanel({
9830
10099
  setCursorPoint(null)
9831
10100
  return
9832
10101
  }
10102
+ wallRunReferenceRef.current = placedDirection
9833
10103
  setDraftStart(point)
9834
10104
  setDraftEnd(point)
9835
10105
  setCursorPoint(point)
10106
+ usePlacementTyping.getState().clear()
9836
10107
  return
9837
10108
  }
9838
10109
  if (viewIs2DOnly) {
9839
- createdWall = createWallOnCurrentLevel(
9840
- draftStart,
9841
- point,
9842
- wallConstructionOptionsRef.current,
9843
- )
10110
+ // The Walls variant's Invisible draws room separators, not walls.
10111
+ createdWall =
10112
+ getWallLineKind() === 'invisible'
10113
+ ? createInvisibleWallOnCurrentLevel(draftStart, point)
10114
+ : createWallOnCurrentLevel(draftStart, point, wallConstructionOptionsRef.current)
9844
10115
  }
9845
10116
  if (createdWall) {
9846
10117
  wallChainWallIdsRef.current.push(createdWall.id)
@@ -9856,7 +10127,7 @@ export function FloorplanPanel({
9856
10127
  : (publishedNextStart ?? point)
9857
10128
 
9858
10129
  if (
9859
- useEditor.getState().getContinuation('wall') === 'single' ||
10130
+ !wallDraftIsRun() ||
9860
10131
  (wallChainFirstVertex && isWithinWallJoinSnapRadius(nextStart, wallChainFirstVertex))
9861
10132
  ) {
9862
10133
  clearWallPlacementDraft()
@@ -9878,7 +10149,7 @@ export function FloorplanPanel({
9878
10149
  levelWalls,
9879
10150
  wallChainWallIdsRef.current,
9880
10151
  ) ||
9881
- wallClosesRoom(levelWalls, createdWall)
10152
+ (createdWall.type === 'wall' && wallClosesRoom(levelWalls, createdWall))
9882
10153
  ) {
9883
10154
  clearWallPlacementDraft()
9884
10155
  setCursorPoint(null)
@@ -9894,19 +10165,30 @@ export function FloorplanPanel({
9894
10165
  return
9895
10166
  }
9896
10167
 
10168
+ wallRunReferenceRef.current = placedDirection
9897
10169
  setDraftStart(nextStart)
9898
10170
  setDraftEnd(nextStart)
9899
10171
  setCursorPoint(nextStart)
10172
+ // What was typed belongs to this segment; the next starts afresh.
10173
+ usePlacementTyping.getState().clear()
9900
10174
  },
9901
10175
  [
9902
10176
  clearWallPlacementDraft,
9903
10177
  draftStart,
9904
10178
  levelId,
10179
+ metricNotation,
10180
+ unit,
9905
10181
  wallChainFirstVertex,
9906
10182
  setDraftEnd,
9907
10183
  setCursorPoint,
9908
10184
  ],
9909
10185
  )
10186
+ // Latest-ref for the Enter-commit path: assigning the ref inside the
10187
+ // callback itself froze the first-render closure (draftStart === null), so
10188
+ // a typed Enter re-started the draft instead of committing the wall.
10189
+ useEffect(() => {
10190
+ wallPlacementPointRef.current = handleWallPlacementPoint
10191
+ }, [handleWallPlacementPoint])
9910
10192
  const { getFloorplanHitIdAtPoint, getFloorplanSelectionIdsInBounds } = useFloorplanHitTesting({
9911
10193
  sceneRef: floorplanSceneRef,
9912
10194
  })
@@ -9924,6 +10206,11 @@ export function FloorplanPanel({
9924
10206
  }) => snapWallDraftPoint({ ...args, magnetic: isMagneticSnapActive() }),
9925
10207
  [],
9926
10208
  )
10209
+ // What the click needs to snap an open run's segment as the move did.
10210
+ const getWallRunContext = useCallback(
10211
+ () => ({ runStart: wallChainFirstVertex, reference: wallRunReferenceRef.current }),
10212
+ [wallChainFirstVertex],
10213
+ )
9927
10214
  const { handleBackgroundPlacementClick } = useFloorplanBackgroundPlacement({
9928
10215
  activePolygonDraftPoints,
9929
10216
  ceilingDraftPoints,
@@ -9936,6 +10223,7 @@ export function FloorplanPanel({
9936
10223
  findClosestWallPoint,
9937
10224
  floorplanOpeningLocalY,
9938
10225
  getSnappedFloorplanPoint,
10226
+ getWallRunContext,
9939
10227
  handleCeilingItemPlacementClick,
9940
10228
  handleCeilingPlacementPoint,
9941
10229
  handleSlabPlacementPoint,
@@ -9959,20 +10247,16 @@ export function FloorplanPanel({
9959
10247
  isWallBuildActive,
9960
10248
  isZoneBuildActive,
9961
10249
  levelId,
9962
- roofDraftStart,
9963
10250
  setCursorPoint,
9964
10251
  setFenceDraftEnd,
9965
10252
  setFenceDraftStart,
9966
- setRoofDraftEnd,
9967
- setRoofDraftStart,
9968
10253
  snapPolygonDraftPoint,
9969
10254
  snapWallDraftPoint: snapWallDraftPointMagnetic,
9970
10255
  toPoint2D,
9971
10256
  walls,
9972
- // World-axis grid snap so drafts land on the visible grid even
9973
- // when the active building is rotated. The helper resolves the
9974
- // active building's pose internally; this hook stays oblivious to
9975
- // building rotation / position.
10257
+ // Building-grid snap: the lattice this plan draws inside its rotated
10258
+ // scene group, so drafts land on the visible grid however the
10259
+ // building is turned, and where the 3D tool would put them.
9976
10260
  worldGridSnap: snapBuildingLocalToWorldGrid,
9977
10261
  })
9978
10262
 
@@ -10021,10 +10305,11 @@ export function FloorplanPanel({
10021
10305
  // confirming without editing is a no-op instead of a surprise
10022
10306
  // rescale (plan units are meters; convert when defaulting to feet).
10023
10307
  setReferenceScaleValue(
10024
- formatNumber(
10308
+ formatFloorplanNumber(
10025
10309
  unit === 'imperial'
10026
10310
  ? measuredLengthUnits / linearUnitToMeters(1, 'imperial')
10027
10311
  : measuredLengthUnits,
10312
+ locale,
10028
10313
  2,
10029
10314
  ),
10030
10315
  )
@@ -10133,6 +10418,7 @@ export function FloorplanPanel({
10133
10418
  unit,
10134
10419
  emitFloorplanGridEvent,
10135
10420
  setCursorPoint,
10421
+ locale,
10136
10422
  ],
10137
10423
  )
10138
10424
  const handleSvgClick = useCallback(
@@ -10174,6 +10460,16 @@ export function FloorplanPanel({
10174
10460
  )
10175
10461
  const handleSvgPointerUp = useCallback(
10176
10462
  (event: ReactPointerEvent<SVGSVGElement>) => {
10463
+ // Letting go of a wall end's pull grip places the pulled wall, as a click
10464
+ // there would; the click the browser fires next is eaten.
10465
+ const pull = useWallPullRun.getState().run
10466
+ if (event.button === 0 && pull?.dragging && pull.origin === '2d') {
10467
+ releaseWallPullDrag()
10468
+ if (isWallBuildActive && draftStart) {
10469
+ swallowNextClick()
10470
+ handleBackgroundClick(event)
10471
+ }
10472
+ }
10177
10473
  if (
10178
10474
  isFenceBuildActive &&
10179
10475
  useEditor.getState().getContinuation('fence') === 'freehand' &&
@@ -10185,14 +10481,30 @@ export function FloorplanPanel({
10185
10481
  endFloorplanNavigation(event)
10186
10482
  },
10187
10483
  [
10484
+ draftStart,
10188
10485
  emitFloorplanGridEvent,
10189
10486
  endFloorplanNavigation,
10190
10487
  getPlanPointFromClientPoint,
10488
+ handleBackgroundClick,
10191
10489
  isFenceBuildActive,
10490
+ isWallBuildActive,
10192
10491
  ],
10193
10492
  )
10194
10493
  const handleBackgroundDoubleClick = useCallback(
10195
10494
  (event: ReactMouseEvent<SVGSVGElement>) => {
10495
+ // A double-click ends a wall run as the 3D tool's detail-2 click does:
10496
+ // the walls already drawn stay, an open Polygon room is kept, and the
10497
+ // draft goes. The click path does this on its second click; this also
10498
+ // covers one the plan never routed there (the walls the run drew sit
10499
+ // under the pointer by then).
10500
+ if (isWallBuildActive) {
10501
+ if (draftStart) {
10502
+ commitWallPolygonDraft()
10503
+ clearWallPlacementDraft()
10504
+ setCursorPoint(null)
10505
+ }
10506
+ return
10507
+ }
10196
10508
  if (!(isPolygonDraftBuildActive && !isRoofBuildActive)) {
10197
10509
  return
10198
10510
  }
@@ -10238,6 +10550,8 @@ export function FloorplanPanel({
10238
10550
  },
10239
10551
  [
10240
10552
  activePolygonDraftPoints,
10553
+ clearWallPlacementDraft,
10554
+ draftStart,
10241
10555
  emitFloorplanGridEvent,
10242
10556
  handleCeilingPlacementConfirm,
10243
10557
  getPlanPointFromClientPoint,
@@ -10246,8 +10560,10 @@ export function FloorplanPanel({
10246
10560
  isCeilingBuildActive,
10247
10561
  isPolygonDraftBuildActive,
10248
10562
  isRoofBuildActive,
10563
+ isWallBuildActive,
10249
10564
  isZoneBuildActive,
10250
10565
  levelId,
10566
+ setCursorPoint,
10251
10567
  ],
10252
10568
  )
10253
10569
 
@@ -11205,9 +11521,9 @@ export function FloorplanPanel({
11205
11521
  isPanning || isRotatingFloorplan ? 'grabbing' : isSpacePanPressed ? 'grab' : null
11206
11522
  const isFloorplanNavigationOverlayVisible = isSpacePanPressed || isPanning || isRotatingFloorplan
11207
11523
  const pendingReferenceDisplayLength =
11208
- parseReferenceScaleLength(referenceScaleValue, referenceScaleUnit) ?? Number.NaN
11524
+ parseReferenceScaleLength(referenceScaleValue, referenceScaleUnit, locale) ?? Number.NaN
11209
11525
  const pendingReferenceRealLengthMeters =
11210
- pendingReferenceScale && pendingReferenceDisplayLength > 0
11526
+ pendingReferenceScale && isValidReferenceScaleDisplayLength(pendingReferenceDisplayLength)
11211
11527
  ? convertReferenceLengthToMeters(pendingReferenceDisplayLength, referenceScaleUnit)
11212
11528
  : null
11213
11529
  const pendingReferenceMetersPerUnit =
@@ -11218,17 +11534,14 @@ export function FloorplanPanel({
11218
11534
  pendingReferenceScale && pendingReferenceRealLengthMeters
11219
11535
  ? pendingReferenceRealLengthMeters / pendingReferenceScale.measuredLengthUnits
11220
11536
  : null
11221
- const referenceScaleInputError =
11222
- referenceScaleValue.trim() === ''
11223
- ? 'Enter the real length of the line.'
11224
- : Number.isNaN(pendingReferenceDisplayLength)
11225
- ? `Enter a length like 3.5, 180cm or 5'11".`
11226
- : pendingReferenceDisplayLength > 0
11227
- ? null
11228
- : 'Length must be greater than 0.'
11537
+ const referenceScaleInputError = getReferenceScaleInputError(
11538
+ referenceScaleValue,
11539
+ referenceScaleUnit,
11540
+ locale,
11541
+ )
11229
11542
  const referenceScaleHint = referenceScaleInputError
11230
11543
  ? null
11231
- : referenceScaleLengthHint(referenceScaleValue, referenceScaleUnit)
11544
+ : referenceScaleLengthHint(referenceScaleValue, referenceScaleUnit, locale)
11232
11545
  return (
11233
11546
  <div
11234
11547
  className="pointer-events-auto flex h-full w-full flex-col overflow-hidden bg-background/95"
@@ -11383,8 +11696,8 @@ export function FloorplanPanel({
11383
11696
 
11384
11697
  <div className="mt-3 rounded-lg bg-muted/45 px-3 py-2 text-muted-foreground text-xs">
11385
11698
  {pendingReferenceImageScaleFactor
11386
- ? `Image will scale ${formatNumber(pendingReferenceImageScaleFactor, 3)}x from the first point.`
11387
- : 'Enter a length greater than 0.'}
11699
+ ? `Image will scale ${formatFloorplanNumber(pendingReferenceImageScaleFactor, locale, 3)}x from the first point.`
11700
+ : 'Enter a valid length to preview the scale.'}
11388
11701
  </div>
11389
11702
 
11390
11703
  <div className="mt-4 flex justify-end gap-2">
@@ -11599,6 +11912,11 @@ export function FloorplanPanel({
11599
11912
  `floorplan-wall-move-ghost-layer.tsx`. */}
11600
11913
  <FloorplanWallMoveGhostLayer />
11601
11914
  </g>
11915
+ {/* Dashed walls of the level below / above, over the plan but
11916
+ outside the measured group so they never change the fit. */}
11917
+ {activeDrawingType !== 'site-plan' && (
11918
+ <FloorplanAdjacentWallsLayer levelId={levelNode?.id} />
11919
+ )}
11602
11920
  {activeDrawingType !== 'site-plan' && <FloorplanOpenWallEndsLayer />}
11603
11921
  <FloorplanMeasurementToolLayer />
11604
11922
  <FloorplanRegisteredToolLayer />
@@ -11614,7 +11932,7 @@ export function FloorplanPanel({
11614
11932
  overlay during a free-translate drag. Sits above the
11615
11933
  registry layer so the red lines and distance pills
11616
11934
  paint on top of node geometry. */}
11617
- <FloorplanAlignmentGuideLayer />
11935
+ <FloorplanAlignmentGuideLayer unitsPerPixel={floorplanUnitsPerPixel} />
11618
11936
 
11619
11937
  {/* The property line, its vertex handles and its edge labels are
11620
11938
  drawn in BUILDING-LOCAL metres (the frame the level's walls
@@ -11668,7 +11986,7 @@ export function FloorplanPanel({
11668
11986
  {/* "Magnetic" wall-snap beacon — per-kind glyph at the active
11669
11987
  draft / endpoint-move snap point. Same store + coord space as
11670
11988
  the alignment guides. */}
11671
- <FloorplanSnapBeaconLayer />
11989
+ <FloorplanSnapBeaconLayer unitsPerPixel={floorplanUnitsPerPixel} />
11672
11990
 
11673
11991
  <FloorplanMarqueeOverlay cursorColor={palette.cursor} />
11674
11992