@pascal-app/editor 1.0.5 → 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 +20 -17
  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
@@ -12,12 +12,14 @@ 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
+ formatNumber as formatLocalizedNumber,
21
23
  type GeometryContext,
22
24
  type GuideNode,
23
25
  getRenderableSlabPolygon,
@@ -37,6 +39,7 @@ import {
37
39
  resolveSlabPlacementElevation,
38
40
  resolveTerrainWallConstructionOptions,
39
41
  runAsSingleSceneHistoryStep,
42
+ type SeparatorNode,
40
43
  type SiteNode,
41
44
  type SlabNode,
42
45
  SlabNode as SlabNodeSchema,
@@ -50,6 +53,7 @@ import {
50
53
  useInteractive,
51
54
  useLiveNodeOverrides,
52
55
  useLiveTransforms,
56
+ useLocale,
53
57
  useScene,
54
58
  type WallNode,
55
59
  WallNode as WallNodeSchema,
@@ -121,6 +125,13 @@ import {
121
125
  } from '../../lib/keyboard-pan'
122
126
  import { measurementHint, parseMeasurement } from '../../lib/measurement-parser'
123
127
  import { formatLinearMeasurement, linearUnitToMeters } from '../../lib/measurements'
128
+ import {
129
+ createGuideScaleReference,
130
+ formatReferenceScaleLabel,
131
+ isValidReferenceScaleDisplayLength,
132
+ MAX_REFERENCE_SCALE_DISPLAY_LENGTH,
133
+ type ReferenceScaleUnit,
134
+ } from '../../lib/reference-scale'
124
135
  import { snapRegisteredDraftPoint } from '../../lib/registered-draft-snap'
125
136
  import { selectRoomFromHit } from '../../lib/room-selection-commands'
126
137
  import { enterBuildingFromPlanHit } from '../../lib/selection-routing'
@@ -135,7 +146,13 @@ import {
135
146
  } from '../../lib/units'
136
147
  import { cn } from '../../lib/utils'
137
148
  import { framingAnimates } from '../../lib/viewer-selection'
138
- import { getWallDrawVariant } from '../../lib/wall-draw-variant'
149
+ import {
150
+ getWallDrawVariant,
151
+ getWallLineKind,
152
+ invisibleWallDraftSnapLines,
153
+ useWallDraftThickness,
154
+ } from '../../lib/wall-draw-variant'
155
+ import { wallSideOfPlanPoint } from '../../lib/wall-placement-pointer'
139
156
  import {
140
157
  addWallPolygonDraftCorner,
141
158
  commitWallPolygonDraft,
@@ -143,6 +160,18 @@ import {
143
160
  startWallPolygonDraft,
144
161
  wallPolygonDraftWalls,
145
162
  } from '../../lib/wall-polygon-draft'
163
+ import {
164
+ endWallPullRun,
165
+ releaseWallPullDrag,
166
+ useWallPullRun,
167
+ wallDraftIsRun,
168
+ } from '../../lib/wall-pull'
169
+ import {
170
+ isWallRunFree,
171
+ publishWallRunFeedback,
172
+ snapWallRunDraftPoint,
173
+ wallRunDirection,
174
+ } from '../../lib/wall-run-snap'
146
175
  import { snapBuildingLocalToWorldGrid } from '../../lib/world-grid-snap'
147
176
  import { nextZoneName } from '../../lib/zone-name'
148
177
  import { subscribeNavigationSyncPose } from '../../store/navigation-sync-pose-store'
@@ -173,6 +202,7 @@ import {
173
202
  VIEW_3D,
174
203
  visibleScene,
175
204
  } from '../../store/view-layout'
205
+ import { FloorplanAdjacentWallsLayer } from '../editor-2d/floorplan-adjacent-walls-layer'
176
206
  import { FloorplanAlignmentGuideLayer } from '../editor-2d/floorplan-alignment-guide-layer'
177
207
  import { FloorplanCursorIndicatorOverlay as Editor2dFloorplanCursorIndicatorOverlay } from '../editor-2d/floorplan-cursor-indicator-overlay'
178
208
  import { FloorplanGroupActionMenu } from '../editor-2d/floorplan-group-action-menu'
@@ -230,6 +260,7 @@ import {
230
260
  } from '../tools/shared/segment-angle'
231
261
  import {
232
262
  chainEndJoinsExistingWall,
263
+ createInvisibleWallOnCurrentLevel,
233
264
  createWallOnCurrentLevel,
234
265
  isSegmentLongEnough,
235
266
  snapWallDraftPoint,
@@ -258,6 +289,7 @@ import {
258
289
  resolveFloorplanPresentationViewBox,
259
290
  setFloorplanCompassRotation,
260
291
  } from './floorplan-navigation-presentation'
292
+ import { swallowNextClick } from './handles/use-handle-drag'
261
293
  import { useFloorplanBackgroundPlacement } from './use-floorplan-background-placement'
262
294
  import { useFloorplanHitTesting } from './use-floorplan-hit-testing'
263
295
  import { useFloorplanSceneData } from './use-floorplan-scene-data'
@@ -266,6 +298,9 @@ import { sceneSpan } from './views/scene-region'
266
298
  const FALLBACK_VIEW_SIZE = 12
267
299
  const FLOORPLAN_PADDING = 2
268
300
  const MIN_VIEWPORT_WIDTH_RATIO = 0.08
301
+ // However large the fitted plan (a whole lot), zooming in goes down to a view this wide (m): a
302
+ // casing leg or a stud's edge fills the screen.
303
+ const MIN_VIEWPORT_WIDTH = 0.6
269
304
  const MAX_VIEWPORT_WIDTH_RATIO = 40
270
305
  const PANEL_MIN_WIDTH = 420
271
306
  const PANEL_MIN_HEIGHT = 320
@@ -526,8 +561,6 @@ type GuideTransformDraft = {
526
561
  rotation: number
527
562
  }
528
563
 
529
- type ReferenceScaleUnit = 'meters' | 'centimeters' | 'feet' | 'inches'
530
-
531
564
  // The in-flight reference-scale measurement. Only the per-CLICK fields live
532
565
  // here (guide + start anchor); the rubber-band's moving END is the shared
533
566
  // `useFloorplanDraftPreview.cursorPoint` (set on every move anyway), so it
@@ -835,7 +868,7 @@ function resolveFloorplanViewWidth(
835
868
  return Math.max(0.001, requestedWidth)
836
869
  }
837
870
 
838
- const minWidth = fittedViewport.width * MIN_VIEWPORT_WIDTH_RATIO
871
+ const minWidth = Math.min(fittedViewport.width * MIN_VIEWPORT_WIDTH_RATIO, MIN_VIEWPORT_WIDTH)
839
872
  const maxWidth = fittedViewport.width * MAX_VIEWPORT_WIDTH_RATIO
840
873
 
841
874
  return clamp(requestedWidth, Math.min(minWidth, currentWidth), Math.max(maxWidth, currentWidth))
@@ -2281,7 +2314,12 @@ function getWallHoverSidePaths(polygon: Point2D[], wall: WallNode): [string, str
2281
2314
  return [rightSidePath, leftSidePath]
2282
2315
  }
2283
2316
 
2284
- function buildDraftWall(levelId: string, start: WallPlanPoint, end: WallPlanPoint): WallNode {
2317
+ function buildDraftWall(
2318
+ levelId: string,
2319
+ start: WallPlanPoint,
2320
+ end: WallPlanPoint,
2321
+ thickness?: number,
2322
+ ): WallNode {
2285
2323
  return {
2286
2324
  object: 'node',
2287
2325
  id: 'wall_draft' as WallNode['id'],
@@ -2293,6 +2331,7 @@ function buildDraftWall(levelId: string, start: WallPlanPoint, end: WallPlanPoin
2293
2331
  children: [],
2294
2332
  start,
2295
2333
  end,
2334
+ thickness,
2296
2335
  frontSide: 'unknown',
2297
2336
  backSide: 'unknown',
2298
2337
  }
@@ -2438,8 +2477,8 @@ function formatMeasurement(
2438
2477
  return formatLinearMeasurement(measuredValue, unit, metricNotation)
2439
2478
  }
2440
2479
 
2441
- function formatNumber(value: number, fractionDigits = 2) {
2442
- return Number.parseFloat(value.toFixed(fractionDigits)).toString()
2480
+ export function formatFloorplanNumber(value: number, locale: string, fractionDigits = 2) {
2481
+ return formatLocalizedNumber(value, { locale, max: fractionDigits })
2443
2482
  }
2444
2483
 
2445
2484
  function convertReferenceLengthToMeters(value: number, unit: ReferenceScaleUnit) {
@@ -2465,22 +2504,52 @@ const REFERENCE_SCALE_LINGO_UNIT: Record<ReferenceScaleUnit, 'm' | 'cm' | 'ft' |
2465
2504
  /** Lingo-parse the free-text real-length input in the dropdown's unit — a
2466
2505
  * bare number means the dropdown unit, while `180cm`, `1m80` or `5'11"`
2467
2506
  * override it. Returns `null` when the text isn't a readable length. */
2468
- function parseReferenceScaleLength(raw: string, unit: ReferenceScaleUnit): number | null {
2507
+ export function parseReferenceScaleLength(
2508
+ raw: string,
2509
+ unit: ReferenceScaleUnit,
2510
+ locale: string,
2511
+ ): number | null {
2469
2512
  const unitId = REFERENCE_SCALE_LINGO_UNIT[unit]
2470
2513
  return parseMeasurement(
2471
2514
  raw,
2472
2515
  { kind: 'length', unitId },
2473
- { bareUnit: unitId, system: unit === 'feet' || unit === 'inches' ? 'us' : 'metric' },
2516
+ {
2517
+ bareUnit: unitId,
2518
+ locale,
2519
+ system: unit === 'feet' || unit === 'inches' ? 'us' : 'metric',
2520
+ },
2474
2521
  )
2475
2522
  }
2476
2523
 
2477
- function referenceScaleLengthHint(raw: string, unit: ReferenceScaleUnit): string | null {
2524
+ export function getReferenceScaleInputError(
2525
+ raw: string,
2526
+ unit: ReferenceScaleUnit,
2527
+ locale: string,
2528
+ ): string | null {
2529
+ if (raw.trim() === '') return 'Enter the real length of the line.'
2530
+ const displayLength = parseReferenceScaleLength(raw, unit, locale)
2531
+ if (displayLength === null || Number.isNaN(displayLength)) {
2532
+ return `Enter a length like 3.5, 180cm or 5'11".`
2533
+ }
2534
+ if (!(displayLength > 0)) return 'Length must be greater than 0.'
2535
+ if (!isValidReferenceScaleDisplayLength(displayLength)) {
2536
+ return `Length must be at most ${formatFloorplanNumber(MAX_REFERENCE_SCALE_DISPLAY_LENGTH, locale)} ${REFERENCE_SCALE_LINGO_UNIT[unit]}.`
2537
+ }
2538
+ return null
2539
+ }
2540
+
2541
+ function referenceScaleLengthHint(
2542
+ raw: string,
2543
+ unit: ReferenceScaleUnit,
2544
+ locale: string,
2545
+ ): string | null {
2478
2546
  const unitId = REFERENCE_SCALE_LINGO_UNIT[unit]
2479
2547
  return measurementHint(
2480
2548
  raw,
2481
2549
  { kind: 'length', unitId },
2482
2550
  {
2483
2551
  bareUnit: unitId,
2552
+ locale,
2484
2553
  system: unit === 'feet' || unit === 'inches' ? 'us' : 'metric',
2485
2554
  displayUnit: unitId,
2486
2555
  precision: 2,
@@ -2488,23 +2557,6 @@ function referenceScaleLengthHint(raw: string, unit: ReferenceScaleUnit): string
2488
2557
  )
2489
2558
  }
2490
2559
 
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
2560
  function getPolygonAreaAndCentroid(polygon: Point2D[]) {
2509
2561
  let cx = 0
2510
2562
  let cy = 0
@@ -2810,8 +2862,10 @@ function findClosestWallPoint(
2810
2862
  const distSq = (point[0] - px) ** 2 + (point[1] - pz) ** 2
2811
2863
  if (distSq < bestDistSq) {
2812
2864
  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] }
2865
+ // The face the cursor is on, as a 3D hover's normal reports it: a door
2866
+ // swings and a window faces toward the cursor's side, in 2D as in 3D.
2867
+ const side = wallSideOfPlanPoint(wall, [point[0], point[1]])
2868
+ best = { wall, point: [px, pz], t, normal: [0, 0, side === 'front' ? 1 : -1] }
2815
2869
  }
2816
2870
  }
2817
2871
 
@@ -3281,6 +3335,7 @@ function FloorplanReferenceScaleLayer({
3281
3335
  unit: 'metric' | 'imperial'
3282
3336
  unitsPerPixel: number
3283
3337
  }) {
3338
+ const locale = useLocale()
3284
3339
  const visibleReferences = guides
3285
3340
  .filter((guide) => guideUi[guide.id]?.scaleReferenceVisible !== false)
3286
3341
  .map((guide) => guide.scaleReference)
@@ -3290,16 +3345,19 @@ function FloorplanReferenceScaleLayer({
3290
3345
 
3291
3346
  return (
3292
3347
  <>
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
- ))}
3348
+ {visibleReferences.map((reference, index) => {
3349
+ const label = formatReferenceScaleLabel(reference, locale)
3350
+ return (
3351
+ <FloorplanReferenceScaleLine
3352
+ end={reference.end}
3353
+ key={`${index}-${reference.start.join(',')}-${reference.end.join(',')}`}
3354
+ label={label}
3355
+ palette={palette}
3356
+ start={reference.start}
3357
+ unitsPerPixel={unitsPerPixel}
3358
+ />
3359
+ )
3360
+ })}
3303
3361
  {draft?.start && (
3304
3362
  <FloorplanReferenceScaleDraftLine
3305
3363
  palette={palette}
@@ -4442,6 +4500,8 @@ function FloorplanLinearDraftLayer({
4442
4500
  sceneRotationDeg: number
4443
4501
  }) {
4444
4502
  const metricNotation = useViewer((state) => state.metricNotation)
4503
+ // The thickness the wall commits at, as the 3D draft shows it.
4504
+ const wallDraftThickness = useWallDraftThickness(DEFAULT_WALL_THICKNESS)
4445
4505
  const wallDraftEnd = useFloorplanDraftPreview((s) => s.wallDraftEnd)
4446
4506
  const fenceDraftEnd = useFloorplanDraftPreview((s) => s.fenceDraftEnd)
4447
4507
  const roofDraftEnd = useFloorplanDraftPreview((s) => s.roofDraftEnd)
@@ -4465,10 +4525,12 @@ function FloorplanLinearDraftLayer({
4465
4525
  ) {
4466
4526
  return null
4467
4527
  }
4468
- const draftWall = getSharedFloorplanWall(buildDraftWall(levelId, wallDraftStart, wallDraftEnd))
4528
+ const draftWall = getSharedFloorplanWall(
4529
+ buildDraftWall(levelId, wallDraftStart, wallDraftEnd, wallDraftThickness),
4530
+ )
4469
4531
  // Keep the live draft preview cheap; full level-wide mitering here runs on every mouse move.
4470
4532
  return getWallPlanFootprint(draftWall, EMPTY_WALL_MITER_DATA)
4471
- }, [levelId, wallDraftStart, wallDraftEnd])
4533
+ }, [levelId, wallDraftStart, wallDraftEnd, wallDraftThickness])
4472
4534
 
4473
4535
  // Rectangle mode drafts four walls at once; they read exactly like a line
4474
4536
  // draft — same mitered footprints, measurement plates and axis guides.
@@ -4838,6 +4900,7 @@ export function FloorplanPanel({
4838
4900
  floorplanSceneSlot?: ReactNode
4839
4901
  }) {
4840
4902
  useFloorplanCameraSyncBridge()
4903
+ const locale = useLocale()
4841
4904
  const viewportHostRef = useRef<HTMLDivElement>(null)
4842
4905
  const svgRef = useRef<SVGSVGElement>(null)
4843
4906
  const floorplanBackgroundRef = useRef<SVGRectElement>(null)
@@ -5042,6 +5105,9 @@ export function FloorplanPanel({
5042
5105
  // Walls committed by the current 2D-only chain — exclusion set for the
5043
5106
  // T-junction chain-termination test (mirrors the 3D tool's `chainWallIds`).
5044
5107
  const wallChainWallIdsRef = useRef<string[]>([])
5108
+ // Direction of the run's last wall (a pulled run's source first): the next
5109
+ // holds square to it. Null before the run has a wall.
5110
+ const wallRunReferenceRef = useRef<WallPlanPoint | null>(null)
5045
5111
  const setDraftEnd = useCallback(
5046
5112
  (next: WallPlanPoint | null | ((prev: WallPlanPoint | null) => WallPlanPoint | null)) => {
5047
5113
  const store = useFloorplanDraftPreview.getState()
@@ -5057,14 +5123,10 @@ export function FloorplanPanel({
5057
5123
  },
5058
5124
  [],
5059
5125
  )
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
- )
5126
+ // The roof tool that owns the input (the 3D tool, or the plan's own in 2D-only view) writes the
5127
+ // roof draft from the corners it snaps and commits; the plan only draws it, so its rectangle is
5128
+ // the roof the next click makes.
5129
+ const roofDraftStart = useFloorplanDraftPreview((s) => s.roofDraftStart)
5068
5130
  const [ceilingDraftPoints, setCeilingDraftPoints] = useState<WallPlanPoint[]>([])
5069
5131
  const [slabDraftPoints, setSlabDraftPoints] = useState<WallPlanPoint[]>([])
5070
5132
  const slabPlacementBaseRef = useRef<number | null>(null)
@@ -5077,9 +5139,6 @@ export function FloorplanPanel({
5077
5139
  useEffect(() => {
5078
5140
  useFloorplanDraftPreview.getState().setFenceDraftStart(fenceDraftStart)
5079
5141
  }, [fenceDraftStart])
5080
- useEffect(() => {
5081
- useFloorplanDraftPreview.getState().setRoofDraftStart(roofDraftStart)
5082
- }, [roofDraftStart])
5083
5142
  const [zoneDraftPoints, setZoneDraftPoints] = useState<WallPlanPoint[]>([])
5084
5143
  const [siteBoundaryDraft, setSiteBoundaryDraft] = useState<SiteBoundaryDraft | null>(null)
5085
5144
  const [siteVertexDragState, setSiteVertexDragState] = useState<SiteVertexDragState | null>(null)
@@ -7161,8 +7220,8 @@ export function FloorplanPanel({
7161
7220
  return
7162
7221
  }
7163
7222
 
7164
- const displayLength = parseReferenceScaleLength(referenceScaleValue, referenceScaleUnit)
7165
- if (!(displayLength && displayLength > 0)) {
7223
+ const displayLength = parseReferenceScaleLength(referenceScaleValue, referenceScaleUnit, locale)
7224
+ if (displayLength === null || !isValidReferenceScaleDisplayLength(displayLength)) {
7166
7225
  return
7167
7226
  }
7168
7227
 
@@ -7199,14 +7258,17 @@ export function FloorplanPanel({
7199
7258
  {
7200
7259
  position: nextGuidePosition,
7201
7260
  scale: nextGuideScale,
7202
- scaleReference: {
7203
- start: pendingReferenceScale.start,
7204
- end: scaledEnd,
7205
- realLengthMeters,
7206
- measuredLengthUnits: scaledMeasuredLengthUnits,
7207
- metersPerUnit,
7208
- label: formatReferenceScaleLabel(displayLength, referenceScaleUnit),
7209
- },
7261
+ scaleReference: createGuideScaleReference(
7262
+ {
7263
+ start: pendingReferenceScale.start,
7264
+ end: scaledEnd,
7265
+ realLengthMeters,
7266
+ measuredLengthUnits: scaledMeasuredLengthUnits,
7267
+ metersPerUnit,
7268
+ },
7269
+ displayLength,
7270
+ referenceScaleUnit,
7271
+ ),
7210
7272
  } as Partial<GuideNode>,
7211
7273
  )
7212
7274
  setGuideLocked(pendingReferenceScale.guideId, true)
@@ -7215,6 +7277,7 @@ export function FloorplanPanel({
7215
7277
  setPendingReferenceScale(null)
7216
7278
  }, [
7217
7279
  guideById,
7280
+ locale,
7218
7281
  pendingReferenceScale,
7219
7282
  referenceScaleUnit,
7220
7283
  referenceScaleValue,
@@ -7861,20 +7924,29 @@ export function FloorplanPanel({
7861
7924
  setWallChainFirstVertex(null)
7862
7925
  wallConstructionOptionsRef.current = undefined
7863
7926
  wallChainWallIdsRef.current = []
7927
+ wallRunReferenceRef.current = null
7864
7928
  setDraftEnd(null)
7865
7929
  useSegmentDraftChain.getState().clear('wall')
7930
+ // A run pulled from a wall end ends with the plan's draft: back to Select.
7931
+ endWallPullRun()
7866
7932
  }, [setDraftEnd])
7867
7933
  const clearFencePlacementDraft = useCallback(() => {
7868
7934
  setFenceDraftStart(null)
7869
7935
  setFenceDraftEnd(null)
7870
7936
  }, [setFenceDraftEnd])
7871
7937
  const clearRoofPlacementDraft = useCallback(() => {
7872
- setRoofDraftStart(null)
7873
- setRoofDraftEnd(null)
7874
- }, [setRoofDraftEnd])
7938
+ const draft = useFloorplanDraftPreview.getState()
7939
+ draft.setRoofDraftStart(null)
7940
+ draft.setRoofDraftEnd(null)
7941
+ }, [])
7875
7942
  const clearCeilingPlacementDraft = useCallback(() => {
7876
7943
  setCeilingDraftPoints([])
7877
7944
  }, [])
7945
+ // Beside the 3D view its ceiling tool commits; in 2D-only view the plan does. Each keeps its own
7946
+ // corners, so a polygon begun under one owner must not be closed against the other's.
7947
+ useEffect(() => {
7948
+ if (shownScene) clearCeilingPlacementDraft()
7949
+ }, [clearCeilingPlacementDraft, shownScene])
7878
7950
  const clearSlabPlacementDraft = useCallback(() => {
7879
7951
  setSlabDraftPoints([])
7880
7952
  slabPlacementBaseRef.current = null
@@ -7970,6 +8042,30 @@ export function FloorplanPanel({
7970
8042
  }
7971
8043
  }, [clearDraft])
7972
8044
 
8045
+ // A wall end's pull grip pressed in the plan opens the Walls run at that
8046
+ // end: start the plan's draft there, as if the end had been clicked. A pull
8047
+ // from the 3D view drafts there (the plan beside it follows its moves).
8048
+ const wallPullRun = useWallPullRun((state) => state.run)
8049
+ const seededWallPullSeqRef = useRef<number | null>(null)
8050
+ useEffect(() => {
8051
+ if (!(wallPullRun && wallPullRun.origin === '2d' && isWallBuildActive && levelId)) return
8052
+ if (seededWallPullSeqRef.current === wallPullRun.seq) return
8053
+ seededWallPullSeqRef.current = wallPullRun.seq
8054
+ const start = wallPullRun.start
8055
+ wallConstructionOptionsRef.current = resolveTerrainWallConstructionOptions(
8056
+ useScene.getState().nodes,
8057
+ levelId,
8058
+ start,
8059
+ useEditor.getState().toolDefaults.wall,
8060
+ )
8061
+ wallChainWallIdsRef.current = []
8062
+ wallRunReferenceRef.current = wallPullRun.reference
8063
+ setDraftStart(start)
8064
+ setWallChainFirstVertex(start)
8065
+ setDraftEnd(start)
8066
+ setCursorPoint(start)
8067
+ }, [isWallBuildActive, levelId, setCursorPoint, setDraftEnd, wallPullRun])
8068
+
7973
8069
  const createZoneOnCurrentLevel = useCallback(
7974
8070
  (points: WallPlanPoint[]) => {
7975
8071
  if (!levelId) {
@@ -9284,14 +9380,6 @@ export function FloorplanPanel({
9284
9380
  setCursorPoint((previousPoint) =>
9285
9381
  previousPoint && pointsEqual(previousPoint, snappedPoint) ? previousPoint : snappedPoint,
9286
9382
  )
9287
-
9288
- if (roofDraftStart) {
9289
- setRoofDraftEnd((previousPoint) =>
9290
- previousPoint && pointsEqual(previousPoint, snappedPoint)
9291
- ? previousPoint
9292
- : snappedPoint,
9293
- )
9294
- }
9295
9383
  return
9296
9384
  }
9297
9385
 
@@ -9506,35 +9594,61 @@ export function FloorplanPanel({
9506
9594
  return
9507
9595
  }
9508
9596
 
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()
9597
+ // Wall draft: grid + magnetic snap, then Figma-style alignment. A Walls
9598
+ // segment locks to 15° rays from its start; a run's segment (a Polygon
9599
+ // side, a pulled run) holds square to the last wall and the house's axes,
9600
+ // with projection guides and a labelled snap marker
9601
+ // (`snapWallRunDraftPoint`). Snapping is governed by the snapping mode
9602
+ // (`'off'` is the bypass); Alt frees a run's segment. An invisible wall
9603
+ // draft catches the invisible walls already drawn as well.
9604
+ let snappedPoint: WallPlanPoint
9605
+ const draftSnapWalls = [
9606
+ ...walls,
9607
+ ...wallPolygonDraftWalls(),
9608
+ ...invisibleWallDraftSnapLines(),
9609
+ ]
9610
+ if (draftStart && wallDraftIsRun()) {
9611
+ const runSnap = snapWallRunDraftPoint({
9612
+ point: planPoint,
9613
+ start: draftStart,
9614
+ runStart: wallChainFirstVertex ?? draftStart,
9615
+ reference: wallRunReferenceRef.current,
9616
+ walls: draftSnapWalls,
9617
+ free: isWallRunFree(event),
9618
+ })
9619
+ snappedPoint = runSnap.point
9620
+ publishWallRunFeedback(runSnap)
9528
9621
  } 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,
9622
+ const wallAngleSnap = draftStart !== null && isAngleSnapActive()
9623
+ const wallSnap = snapWallDraftPointDetailed({
9624
+ point: planPoint,
9625
+ walls: draftSnapWalls,
9626
+ start: draftStart ?? undefined,
9627
+ angleSnap: wallAngleSnap,
9628
+ magnetic: isMagneticSnapActive(),
9629
+ // Same as the click: an end aimed into a wall's body joins it.
9630
+ captureWallBodies: true,
9533
9631
  })
9632
+ const wallSnapped = wallSnap.point
9633
+ // Locked onto existing geometry (corner / midpoint / crossing / edge) →
9634
+ // that snap wins, so skip Figma alignment and stand the beacon there.
9635
+ const lockedToWall = wallSnap.snap !== null
9636
+ snappedPoint = wallSnapped
9637
+ if (lockedToWall) {
9638
+ useAlignmentGuides.getState().clear()
9639
+ } else {
9640
+ // Alignment lines show in every mode; the pull applies only when
9641
+ // magnetic ('lines') and the segment isn't angle-locked.
9642
+ snappedPoint = alignFloorplanDraftPoint(wallSnapped, {
9643
+ applySnap: isMagneticSnapActive() && !wallAngleSnap,
9644
+ })
9645
+ }
9646
+ useWallSnapIndicator
9647
+ .getState()
9648
+ .set(
9649
+ wallSnap.snap ? { x: snappedPoint[0], z: snappedPoint[1], kind: wallSnap.snap } : null,
9650
+ )
9534
9651
  }
9535
- useWallSnapIndicator
9536
- .getState()
9537
- .set(wallSnap.snap ? { x: snappedPoint[0], z: snappedPoint[1], kind: wallSnap.snap } : null)
9538
9652
 
9539
9653
  // Emit `grid:move` so the registry-driven wall tool's 3D preview
9540
9654
  // tracks the cursor. The local draftEnd update below is what
@@ -9592,15 +9706,14 @@ export function FloorplanPanel({
9592
9706
  levelId,
9593
9707
  publishFloorplanNavigationPose,
9594
9708
  referenceScaleDraft,
9595
- roofDraftStart,
9596
9709
  siteVertexDragState,
9597
9710
  surfaceSize.height,
9598
9711
  surfaceSize.width,
9599
9712
  svgAspectRatio,
9600
9713
  walls,
9714
+ wallChainFirstVertex,
9601
9715
  setCursorPoint,
9602
9716
  setDraftEnd,
9603
- setRoofDraftEnd,
9604
9717
  setFenceDraftEnd,
9605
9718
  ],
9606
9719
  )
@@ -9794,6 +9907,7 @@ export function FloorplanPanel({
9794
9907
  ) {
9795
9908
  startWallPolygonDraft(levelId as AnyNodeId, point, wallConstructionOptionsRef.current)
9796
9909
  }
9910
+ wallRunReferenceRef.current = null
9797
9911
  setDraftStart(point)
9798
9912
  setWallChainFirstVertex(point)
9799
9913
  setDraftEnd(point)
@@ -9804,6 +9918,8 @@ export function FloorplanPanel({
9804
9918
  if (!isSegmentLongEnough(draftStart, point)) {
9805
9919
  return
9806
9920
  }
9921
+ // The run's next segment holds square to this one.
9922
+ const placedDirection = wallRunDirection(draftStart, point)
9807
9923
 
9808
9924
  // The 3D wall tool's `grid:click` listener
9809
9925
  // (`packages/nodes/src/wall/tool.tsx`) owns the wall-create
@@ -9820,7 +9936,7 @@ export function FloorplanPanel({
9820
9936
  // ceiling 2D-only committers: create locally here, gated on the
9821
9937
  // view, so split / 3D keep their single-owner tool commit.
9822
9938
  const viewIs2DOnly = visibleScene(useEditor.getState()) === '2d'
9823
- let createdWall: WallNode | null = null
9939
+ let createdWall: WallNode | SeparatorNode | null = null
9824
9940
  // 2D-only Polygon room: the corner joins the draft; nothing is written
9825
9941
  // until it closes, seals or tees (then every wall at once).
9826
9942
  if (viewIs2DOnly && getWallDrawVariant() === 'polygon') {
@@ -9830,17 +9946,18 @@ export function FloorplanPanel({
9830
9946
  setCursorPoint(null)
9831
9947
  return
9832
9948
  }
9949
+ wallRunReferenceRef.current = placedDirection
9833
9950
  setDraftStart(point)
9834
9951
  setDraftEnd(point)
9835
9952
  setCursorPoint(point)
9836
9953
  return
9837
9954
  }
9838
9955
  if (viewIs2DOnly) {
9839
- createdWall = createWallOnCurrentLevel(
9840
- draftStart,
9841
- point,
9842
- wallConstructionOptionsRef.current,
9843
- )
9956
+ // The Walls variant's Invisible draws room separators, not walls.
9957
+ createdWall =
9958
+ getWallLineKind() === 'invisible'
9959
+ ? createInvisibleWallOnCurrentLevel(draftStart, point)
9960
+ : createWallOnCurrentLevel(draftStart, point, wallConstructionOptionsRef.current)
9844
9961
  }
9845
9962
  if (createdWall) {
9846
9963
  wallChainWallIdsRef.current.push(createdWall.id)
@@ -9856,7 +9973,7 @@ export function FloorplanPanel({
9856
9973
  : (publishedNextStart ?? point)
9857
9974
 
9858
9975
  if (
9859
- useEditor.getState().getContinuation('wall') === 'single' ||
9976
+ !wallDraftIsRun() ||
9860
9977
  (wallChainFirstVertex && isWithinWallJoinSnapRadius(nextStart, wallChainFirstVertex))
9861
9978
  ) {
9862
9979
  clearWallPlacementDraft()
@@ -9878,7 +9995,7 @@ export function FloorplanPanel({
9878
9995
  levelWalls,
9879
9996
  wallChainWallIdsRef.current,
9880
9997
  ) ||
9881
- wallClosesRoom(levelWalls, createdWall)
9998
+ (createdWall.type === 'wall' && wallClosesRoom(levelWalls, createdWall))
9882
9999
  ) {
9883
10000
  clearWallPlacementDraft()
9884
10001
  setCursorPoint(null)
@@ -9894,6 +10011,7 @@ export function FloorplanPanel({
9894
10011
  return
9895
10012
  }
9896
10013
 
10014
+ wallRunReferenceRef.current = placedDirection
9897
10015
  setDraftStart(nextStart)
9898
10016
  setDraftEnd(nextStart)
9899
10017
  setCursorPoint(nextStart)
@@ -9924,6 +10042,11 @@ export function FloorplanPanel({
9924
10042
  }) => snapWallDraftPoint({ ...args, magnetic: isMagneticSnapActive() }),
9925
10043
  [],
9926
10044
  )
10045
+ // What the click needs to snap an open run's segment as the move did.
10046
+ const getWallRunContext = useCallback(
10047
+ () => ({ runStart: wallChainFirstVertex, reference: wallRunReferenceRef.current }),
10048
+ [wallChainFirstVertex],
10049
+ )
9927
10050
  const { handleBackgroundPlacementClick } = useFloorplanBackgroundPlacement({
9928
10051
  activePolygonDraftPoints,
9929
10052
  ceilingDraftPoints,
@@ -9936,6 +10059,7 @@ export function FloorplanPanel({
9936
10059
  findClosestWallPoint,
9937
10060
  floorplanOpeningLocalY,
9938
10061
  getSnappedFloorplanPoint,
10062
+ getWallRunContext,
9939
10063
  handleCeilingItemPlacementClick,
9940
10064
  handleCeilingPlacementPoint,
9941
10065
  handleSlabPlacementPoint,
@@ -9959,20 +10083,16 @@ export function FloorplanPanel({
9959
10083
  isWallBuildActive,
9960
10084
  isZoneBuildActive,
9961
10085
  levelId,
9962
- roofDraftStart,
9963
10086
  setCursorPoint,
9964
10087
  setFenceDraftEnd,
9965
10088
  setFenceDraftStart,
9966
- setRoofDraftEnd,
9967
- setRoofDraftStart,
9968
10089
  snapPolygonDraftPoint,
9969
10090
  snapWallDraftPoint: snapWallDraftPointMagnetic,
9970
10091
  toPoint2D,
9971
10092
  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.
10093
+ // Building-grid snap: the lattice this plan draws inside its rotated
10094
+ // scene group, so drafts land on the visible grid however the
10095
+ // building is turned, and where the 3D tool would put them.
9976
10096
  worldGridSnap: snapBuildingLocalToWorldGrid,
9977
10097
  })
9978
10098
 
@@ -10021,10 +10141,11 @@ export function FloorplanPanel({
10021
10141
  // confirming without editing is a no-op instead of a surprise
10022
10142
  // rescale (plan units are meters; convert when defaulting to feet).
10023
10143
  setReferenceScaleValue(
10024
- formatNumber(
10144
+ formatFloorplanNumber(
10025
10145
  unit === 'imperial'
10026
10146
  ? measuredLengthUnits / linearUnitToMeters(1, 'imperial')
10027
10147
  : measuredLengthUnits,
10148
+ locale,
10028
10149
  2,
10029
10150
  ),
10030
10151
  )
@@ -10174,6 +10295,16 @@ export function FloorplanPanel({
10174
10295
  )
10175
10296
  const handleSvgPointerUp = useCallback(
10176
10297
  (event: ReactPointerEvent<SVGSVGElement>) => {
10298
+ // Letting go of a wall end's pull grip places the pulled wall, as a click
10299
+ // there would; the click the browser fires next is eaten.
10300
+ const pull = useWallPullRun.getState().run
10301
+ if (event.button === 0 && pull?.dragging && pull.origin === '2d') {
10302
+ releaseWallPullDrag()
10303
+ if (isWallBuildActive && draftStart) {
10304
+ swallowNextClick()
10305
+ handleBackgroundClick(event)
10306
+ }
10307
+ }
10177
10308
  if (
10178
10309
  isFenceBuildActive &&
10179
10310
  useEditor.getState().getContinuation('fence') === 'freehand' &&
@@ -10185,14 +10316,30 @@ export function FloorplanPanel({
10185
10316
  endFloorplanNavigation(event)
10186
10317
  },
10187
10318
  [
10319
+ draftStart,
10188
10320
  emitFloorplanGridEvent,
10189
10321
  endFloorplanNavigation,
10190
10322
  getPlanPointFromClientPoint,
10323
+ handleBackgroundClick,
10191
10324
  isFenceBuildActive,
10325
+ isWallBuildActive,
10192
10326
  ],
10193
10327
  )
10194
10328
  const handleBackgroundDoubleClick = useCallback(
10195
10329
  (event: ReactMouseEvent<SVGSVGElement>) => {
10330
+ // A double-click ends a wall run as the 3D tool's detail-2 click does:
10331
+ // the walls already drawn stay, an open Polygon room is kept, and the
10332
+ // draft goes. The click path does this on its second click; this also
10333
+ // covers one the plan never routed there (the walls the run drew sit
10334
+ // under the pointer by then).
10335
+ if (isWallBuildActive) {
10336
+ if (draftStart) {
10337
+ commitWallPolygonDraft()
10338
+ clearWallPlacementDraft()
10339
+ setCursorPoint(null)
10340
+ }
10341
+ return
10342
+ }
10196
10343
  if (!(isPolygonDraftBuildActive && !isRoofBuildActive)) {
10197
10344
  return
10198
10345
  }
@@ -10238,6 +10385,8 @@ export function FloorplanPanel({
10238
10385
  },
10239
10386
  [
10240
10387
  activePolygonDraftPoints,
10388
+ clearWallPlacementDraft,
10389
+ draftStart,
10241
10390
  emitFloorplanGridEvent,
10242
10391
  handleCeilingPlacementConfirm,
10243
10392
  getPlanPointFromClientPoint,
@@ -10246,8 +10395,10 @@ export function FloorplanPanel({
10246
10395
  isCeilingBuildActive,
10247
10396
  isPolygonDraftBuildActive,
10248
10397
  isRoofBuildActive,
10398
+ isWallBuildActive,
10249
10399
  isZoneBuildActive,
10250
10400
  levelId,
10401
+ setCursorPoint,
10251
10402
  ],
10252
10403
  )
10253
10404
 
@@ -11205,9 +11356,9 @@ export function FloorplanPanel({
11205
11356
  isPanning || isRotatingFloorplan ? 'grabbing' : isSpacePanPressed ? 'grab' : null
11206
11357
  const isFloorplanNavigationOverlayVisible = isSpacePanPressed || isPanning || isRotatingFloorplan
11207
11358
  const pendingReferenceDisplayLength =
11208
- parseReferenceScaleLength(referenceScaleValue, referenceScaleUnit) ?? Number.NaN
11359
+ parseReferenceScaleLength(referenceScaleValue, referenceScaleUnit, locale) ?? Number.NaN
11209
11360
  const pendingReferenceRealLengthMeters =
11210
- pendingReferenceScale && pendingReferenceDisplayLength > 0
11361
+ pendingReferenceScale && isValidReferenceScaleDisplayLength(pendingReferenceDisplayLength)
11211
11362
  ? convertReferenceLengthToMeters(pendingReferenceDisplayLength, referenceScaleUnit)
11212
11363
  : null
11213
11364
  const pendingReferenceMetersPerUnit =
@@ -11218,17 +11369,14 @@ export function FloorplanPanel({
11218
11369
  pendingReferenceScale && pendingReferenceRealLengthMeters
11219
11370
  ? pendingReferenceRealLengthMeters / pendingReferenceScale.measuredLengthUnits
11220
11371
  : 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.'
11372
+ const referenceScaleInputError = getReferenceScaleInputError(
11373
+ referenceScaleValue,
11374
+ referenceScaleUnit,
11375
+ locale,
11376
+ )
11229
11377
  const referenceScaleHint = referenceScaleInputError
11230
11378
  ? null
11231
- : referenceScaleLengthHint(referenceScaleValue, referenceScaleUnit)
11379
+ : referenceScaleLengthHint(referenceScaleValue, referenceScaleUnit, locale)
11232
11380
  return (
11233
11381
  <div
11234
11382
  className="pointer-events-auto flex h-full w-full flex-col overflow-hidden bg-background/95"
@@ -11383,8 +11531,8 @@ export function FloorplanPanel({
11383
11531
 
11384
11532
  <div className="mt-3 rounded-lg bg-muted/45 px-3 py-2 text-muted-foreground text-xs">
11385
11533
  {pendingReferenceImageScaleFactor
11386
- ? `Image will scale ${formatNumber(pendingReferenceImageScaleFactor, 3)}x from the first point.`
11387
- : 'Enter a length greater than 0.'}
11534
+ ? `Image will scale ${formatFloorplanNumber(pendingReferenceImageScaleFactor, locale, 3)}x from the first point.`
11535
+ : 'Enter a valid length to preview the scale.'}
11388
11536
  </div>
11389
11537
 
11390
11538
  <div className="mt-4 flex justify-end gap-2">
@@ -11599,6 +11747,11 @@ export function FloorplanPanel({
11599
11747
  `floorplan-wall-move-ghost-layer.tsx`. */}
11600
11748
  <FloorplanWallMoveGhostLayer />
11601
11749
  </g>
11750
+ {/* Dashed walls of the level below / above, over the plan but
11751
+ outside the measured group so they never change the fit. */}
11752
+ {activeDrawingType !== 'site-plan' && (
11753
+ <FloorplanAdjacentWallsLayer levelId={levelNode?.id} />
11754
+ )}
11602
11755
  {activeDrawingType !== 'site-plan' && <FloorplanOpenWallEndsLayer />}
11603
11756
  <FloorplanMeasurementToolLayer />
11604
11757
  <FloorplanRegisteredToolLayer />
@@ -11614,7 +11767,7 @@ export function FloorplanPanel({
11614
11767
  overlay during a free-translate drag. Sits above the
11615
11768
  registry layer so the red lines and distance pills
11616
11769
  paint on top of node geometry. */}
11617
- <FloorplanAlignmentGuideLayer />
11770
+ <FloorplanAlignmentGuideLayer unitsPerPixel={floorplanUnitsPerPixel} />
11618
11771
 
11619
11772
  {/* The property line, its vertex handles and its edge labels are
11620
11773
  drawn in BUILDING-LOCAL metres (the frame the level's walls
@@ -11668,7 +11821,7 @@ export function FloorplanPanel({
11668
11821
  {/* "Magnetic" wall-snap beacon — per-kind glyph at the active
11669
11822
  draft / endpoint-move snap point. Same store + coord space as
11670
11823
  the alignment guides. */}
11671
- <FloorplanSnapBeaconLayer />
11824
+ <FloorplanSnapBeaconLayer unitsPerPixel={floorplanUnitsPerPixel} />
11672
11825
 
11673
11826
  <FloorplanMarqueeOverlay cursorColor={palette.cursor} />
11674
11827