@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
@@ -1,11 +1,22 @@
1
1
  'use client'
2
2
 
3
- import { type FloorplanGeometry, type Point, roomDrawnFloor, useScene } from '@pascal-app/core'
3
+ import {
4
+ type AnyNode,
5
+ type AnyNodeId,
6
+ type FloorplanGeometry,
7
+ formatLinearMeasurement,
8
+ nodeRegistry,
9
+ type Point,
10
+ roomDrawnFloor,
11
+ useScene,
12
+ } from '@pascal-app/core'
4
13
  import { useViewer } from '@pascal-app/viewer'
5
14
  import { Minus, Plus } from 'lucide-react'
6
15
  import { type PointerEvent as ReactPointerEvent, useMemo, useState } from 'react'
7
- import { useSelectedRoom } from '../../hooks/use-selected-room'
8
- import { formatLinearMeasurement, MEASUREMENT_FLOORPLAN_COLOR } from '../../lib/measurements'
16
+ import { useShallow } from 'zustand/react/shallow'
17
+ import { useRoomRecords, useSelectedRoom } from '../../hooks/use-selected-room'
18
+ import { getFloorplanNodeExtension } from '../../lib/floorplan/floorplan-extension'
19
+ import { MEASUREMENT_FLOORPLAN_COLOR } from '../../lib/measurements'
9
20
  import {
10
21
  commitRoomFloorElevation,
11
22
  roomRelativeFloorHeight,
@@ -21,8 +32,13 @@ import {
21
32
  useRoomHandleDrag,
22
33
  type WallPushHandle,
23
34
  } from '../../lib/room-handle-drag'
24
- import type { RoomDimension } from '../../lib/room-push-dimensions'
35
+ import {
36
+ mergeSelectionDimensions,
37
+ type OwnedSelectionDimension,
38
+ type RoomDimension,
39
+ } from '../../lib/room-push-dimensions'
25
40
  import useEditor from '../../store/use-editor'
41
+ import useFloorplanAnnotationVisibility from '../../store/use-floorplan-annotation-visibility'
26
42
  import useInteractionScope from '../../store/use-interaction-scope'
27
43
  import { useFloorplanRender } from '../editor-2d/floorplan-render-context'
28
44
  import { FloorplanGeometryRenderer } from '../editor-2d/renderers/floorplan-geometry-renderer'
@@ -158,9 +174,10 @@ function startArrow(event: ReactPointerEvent<SVGPathElement>, run: () => unknown
158
174
 
159
175
  /**
160
176
  * The selected room's arrows on the plan: one push/pull chevron per boundary
161
- * span (on the far face, along the normal the handle supplies) and, for a
162
- * mezzanine, one per plate edge. Mounted after every node's handles so a
163
- * wall outline never takes the press. The drags are the 3D arrows' own.
177
+ * wall span (on the far face, along the normal the handle supplies), one per
178
+ * straight edge of invisible walls and, for a mezzanine, one per plate edge.
179
+ * Mounted after every node's handles so a wall outline never takes the press.
180
+ * The drags are the 3D arrows' own.
164
181
  */
165
182
  export function RoomHandles2D({ levelId }: { levelId: string | null }) {
166
183
  const room = useRoomControls()
@@ -327,6 +344,13 @@ export function RoomHandleDragPreview2D({ levelId }: { levelId: string | null })
327
344
  const unit = useViewer((s) => s.unit)
328
345
  const metricNotation = useViewer((s) => s.metricNotation)
329
346
  if (!drag || drag.levelId !== levelId || drag.kind === 'elevation') return null
347
+ if (drag.kind === 'wall-group-push')
348
+ return (
349
+ <g data-room-drag-preview-2d pointerEvents="none">
350
+ <PlanRoomDimensions dimensions={drag.dimensions} />
351
+ {drag.message && <PlanReadout at={drag.anchor} refused text={drag.message} />}
352
+ </g>
353
+ )
330
354
  const plan = drag as PlanDrag
331
355
  const at: Point = [
332
356
  plan.anchor[0] + plan.outward[0] * plan.distance,
@@ -352,3 +376,130 @@ export function RoomHandleDragPreview2D({ levelId }: { levelId: string | null })
352
376
  </g>
353
377
  )
354
378
  }
379
+
380
+ function selectionDimensionsOf(node: AnyNode | undefined) {
381
+ return node
382
+ ? getFloorplanNodeExtension(nodeRegistry.get(node.type))?.selectionDimensions
383
+ : undefined
384
+ }
385
+
386
+ const NO_NODES: readonly AnyNode[] = []
387
+
388
+ /**
389
+ * The selected nodes that show their temporary dimensions now: Select mode,
390
+ * nothing in progress, Display → Dimensions on, kinds with `selectionDimensions`
391
+ * only (a wall among other selected things still shows its own), on the first
392
+ * one's level. A new array only when one of them changes.
393
+ */
394
+ export function useSelectionDimensionNodes(): readonly AnyNode[] {
395
+ const selectedIds = useViewer((s) => s.selection.selectedIds)
396
+ const enabled = useEditor((s) => s.mode === 'select')
397
+ const idle = useInteractionScope((s) => s.scope.kind === 'idle')
398
+ const dragging = useRoomHandleDrag((s) => s.drag !== null)
399
+ const shown = useFloorplanAnnotationVisibility((s) => s.showDimensions)
400
+ const active = enabled && idle && !dragging && shown
401
+ return useScene(
402
+ useShallow((s) => {
403
+ if (!active) return NO_NODES
404
+ const found: AnyNode[] = []
405
+ for (const id of selectedIds) {
406
+ const node = s.nodes[id as AnyNodeId]
407
+ if (!node || !selectionDimensionsOf(node)) continue
408
+ if (found.length && node.parentId !== found[0]!.parentId) continue
409
+ found.push(node)
410
+ }
411
+ return found
412
+ }),
413
+ )
414
+ }
415
+
416
+ /**
417
+ * The selected nodes' temporary dimensions (`useSelectionDimensionNodes`),
418
+ * from each kind's `selectionDimensions` (a wall: the room widths its push
419
+ * shows and, for an exterior wall, the outside size, as Display → Measure
420
+ * walls asks). Several selected walls show each one's, a room two of them
421
+ * bound measured once. Shared by the plan and the 3D view; typing into one
422
+ * drives the node it runs from. Recomputed only when a measured node or its
423
+ * level's rooms change (the room index keeps its records while unrelated
424
+ * nodes edit).
425
+ */
426
+ export function useSelectedWallDimensions(): {
427
+ levelId: string
428
+ dimensions: readonly OwnedSelectionDimension[]
429
+ } | null {
430
+ const measured = useSelectionDimensionNodes()
431
+ const faces = useFloorplanAnnotationVisibility((s) => s.wallDimensionFaces)
432
+ const levelId = measured[0]?.parentId ?? null
433
+ const rooms = useRoomRecords(levelId)
434
+ return useMemo(() => {
435
+ // `rooms` is the invalidation key: it changes when the level's room
436
+ // topology, slabs, ceilings or zones do.
437
+ if (!levelId || !rooms) return null
438
+ const nodes = useScene.getState().nodes
439
+ const dimensions = mergeSelectionDimensions(
440
+ measured.map((node) => ({
441
+ ownerId: node.id,
442
+ dimensions: selectionDimensionsOf(node)?.({ node, nodes, faces }) ?? [],
443
+ })),
444
+ )
445
+ return dimensions.length ? { levelId, dimensions } : null
446
+ }, [faces, levelId, measured, rooms])
447
+ }
448
+
449
+ /**
450
+ * The selected walls' temporary dimensions on the plan, drawn as the push
451
+ * drag draws its room widths (indigo line, end dots). Each is a `dimension`
452
+ * owned by the wall it runs from, so a click on its value opens the plan's
453
+ * type-a-value box (`FloorplanDimensionEditOverlay`) and the typed size slides
454
+ * that wall.
455
+ */
456
+ export function SelectedWallDimensions2D({ levelId }: { levelId: string | null }) {
457
+ const selection = useSelectedWallDimensions()
458
+ const context = useFloorplanRender()
459
+ const unit = useViewer((s) => s.unit)
460
+ const metricNotation = useViewer((s) => s.metricNotation)
461
+ const owners = useMemo(() => {
462
+ const byOwner = new Map<string, FloorplanGeometry[]>()
463
+ for (const { from, to, distance, ownerId } of selection?.dimensions ?? []) {
464
+ const length = Math.hypot(to[0] - from[0], to[1] - from[1]) || 1
465
+ const geometry: FloorplanGeometry = {
466
+ kind: 'dimension',
467
+ start: from,
468
+ end: to,
469
+ offsetNormal: [-(to[1] - from[1]) / length, (to[0] - from[0]) / length],
470
+ offsetDistance: 0,
471
+ extensionOvershoot: 0,
472
+ extensionStartGap: 0,
473
+ terminator: 'dot',
474
+ text: formatLinearMeasurement(distance, unit, metricNotation),
475
+ stroke: MEASUREMENT_FLOORPLAN_COLOR,
476
+ }
477
+ const list = byOwner.get(ownerId)
478
+ if (list) list.push(geometry)
479
+ else byOwner.set(ownerId, [geometry])
480
+ }
481
+ return [...byOwner].map(([ownerId, children]) => ({
482
+ ownerId,
483
+ geometry: { kind: 'group', children } satisfies FloorplanGeometry,
484
+ }))
485
+ }, [metricNotation, selection, unit])
486
+ if (!selection || selection.levelId !== levelId) return null
487
+ return (
488
+ <g
489
+ data-selected-wall-dimensions-2d=""
490
+ // A press on a value is the type-a-value box's, not the plan's marquee or pan.
491
+ onPointerDown={(event) => event.stopPropagation()}
492
+ >
493
+ {owners.map(({ ownerId, geometry }) => (
494
+ <g data-node-id={ownerId} key={ownerId}>
495
+ <FloorplanGeometryRenderer
496
+ geometry={geometry}
497
+ pointerEventsOverride="none"
498
+ sceneRotationDeg={context?.sceneRotationDeg ?? 0}
499
+ screenUnitsPerPixel={context?.unitsPerPixel}
500
+ />
501
+ </g>
502
+ ))}
503
+ </g>
504
+ )
505
+ }
@@ -1,14 +1,17 @@
1
1
  'use client'
2
2
 
3
3
  import {
4
+ type AnyNodeId,
4
5
  clampRoomFloorHandle,
6
+ formatLinearMeasurement,
5
7
  type Point,
6
8
  roomDrawnFloor,
9
+ roomFloorElevation,
7
10
  sceneRegistry,
11
+ usePascalI18n,
8
12
  useScene,
9
13
  } from '@pascal-app/core'
10
- import { useViewer } from '@pascal-app/viewer'
11
- import { Html } from '@react-three/drei'
14
+ import { MEASUREMENT_ACTIVE_COLOR, useViewer } from '@pascal-app/viewer'
12
15
  import { type ThreeEvent, useFrame, useThree } from '@react-three/fiber'
13
16
  import { useEffect, useMemo, useRef, useState } from 'react'
14
17
  import {
@@ -33,7 +36,7 @@ import {
33
36
  publishStructuralElevationGuide,
34
37
  resolveStructuralElevationSnap,
35
38
  } from '../../lib/elevation-guides'
36
- import { formatLinearMeasurement, MEASUREMENT_ACTIVE_COLOR } from '../../lib/measurements'
39
+ import { parseDimensionInput } from '../../lib/floorplan/dimension-drive'
37
40
  import { commitRoomFloorElevation, roomFloorBase } from '../../lib/room-construction-commands'
38
41
  import {
39
42
  type MezzanineEdgeHandle,
@@ -42,7 +45,6 @@ import {
42
45
  ownFloorElevationBounds,
43
46
  ROOM_ELEVATION_DRAG_LABEL,
44
47
  type RoomHandleDrag,
45
- roomFloorElevation,
46
48
  roomPushHandles,
47
49
  runMezzanineEdgeDrag,
48
50
  runRoomHandleDrag,
@@ -51,10 +53,16 @@ import {
51
53
  import type { RoomDimension } from '../../lib/room-push-dimensions'
52
54
  import type { RoomSelectionRecord } from '../../lib/room-selection'
53
55
  import useEditor, { isGridSnapActive, isMagneticSnapActive } from '../../store/use-editor'
56
+ import {
57
+ commitDimensionValue,
58
+ useDimensionEditAllowed,
59
+ } from '../editor-2d/renderers/floorplan-dimension-edit-overlay'
54
60
  import { suppressBoxSelectForPointer } from '../tools/select/box-select-state'
55
61
  import { ARROW_COLOR, ARROW_SCALE, HandleArrow } from './handles/handle-arrow'
56
62
  import { resolveResizeSnapValue } from './handles/resize-snap'
63
+ import { Html } from './localized-html'
57
64
  import { formatMeasurement } from './measurement-pill'
65
+ import { useSelectedWallDimensions } from './room-handles-2d'
58
66
  import { WallPushArrow } from './wall-push-arrow'
59
67
 
60
68
  const PILL =
@@ -295,8 +303,9 @@ export function ElevationArrows({
295
303
 
296
304
  /**
297
305
  * A selected room's 3D handles: the floor elevation tracker at its centroid
298
- * and one push/pull arrow outside every boundary wall. Rendered in the level
299
- * frame by `RoomControls3D`, only while nothing else is in progress.
306
+ * and one push/pull arrow outside every boundary wall and every straight edge
307
+ * of invisible walls. Rendered in the level frame by `RoomControls3D`, only
308
+ * while nothing else is in progress.
300
309
  */
301
310
  export function RoomHandles3D({ room }: { room: RoomSelectionRecord }) {
302
311
  const nodes = useScene((s) => s.nodes)
@@ -307,7 +316,11 @@ export function RoomHandles3D({ room }: { room: RoomSelectionRecord }) {
307
316
  () => outlineCentroid([room.polygon as Point[], ...(room.holes as Point[][])]),
308
317
  [room.polygon, room.holes],
309
318
  )
310
- const handles = useMemo(() => roomPushHandles(nodes, room.spans), [nodes, room.spans])
319
+ // Its invisible walls' arrows lie on its floor, where their dashed lines are drawn.
320
+ const handles = useMemo(
321
+ () => roomPushHandles(nodes, room.spans, elevation),
322
+ [nodes, room.spans, elevation],
323
+ )
311
324
  // A mezzanine has no walls: its plate edges carry the push arrows instead.
312
325
  const edges = useMemo(
313
326
  () => (room.mezzanine ? mezzanineEdgeHandles(nodes, room.zoneId) : []),
@@ -414,9 +427,20 @@ const DIMENSION_LABEL_OUTLINE = [
414
427
  * above its room's floor and drawn over the walls, closed by cross ticks, with
415
428
  * the measurement's outlined label — apart from the moved-distance pill.
416
429
  */
417
- function RoomDimensionLines3D({ dimensions }: { dimensions: RoomDimension[] }) {
430
+ function RoomDimensionLines3D({
431
+ dimensions,
432
+ hidden = false,
433
+ }: {
434
+ /** With an `ownerId` (a selected wall's), a value can be typed to move that wall. */
435
+ dimensions: readonly (RoomDimension & { ownerId?: string })[]
436
+ /** Hides the labels while keeping their drei `Html` roots mounted. */
437
+ hidden?: boolean
438
+ }) {
439
+ const i18n = usePascalI18n()
418
440
  const unit = useViewer((s) => s.unit)
419
441
  const metricNotation = useViewer((s) => s.metricNotation)
442
+ const editAllowed = useDimensionEditAllowed()
443
+ const [editing, setEditing] = useState<string | null>(null)
420
444
  const geometry = useMemo(() => {
421
445
  if (!dimensions.length) return null
422
446
  const positions: number[] = []
@@ -441,27 +465,175 @@ function RoomDimensionLines3D({ dimensions }: { dimensions: RoomDimension[] }) {
441
465
  raycast={noRaycast}
442
466
  renderOrder={1001}
443
467
  />
444
- {dimensions.map((dimension) => (
445
- <Html
446
- center
447
- key={dimension.key}
448
- position={[
449
- (dimension.from[0] + dimension.to[0]) / 2,
450
- dimension.elevation,
451
- (dimension.from[1] + dimension.to[1]) / 2,
452
- ]}
453
- style={{ pointerEvents: 'none' }}
454
- zIndexRange={[20, 0]}
455
- >
456
- <div
457
- className="-translate-y-3 whitespace-nowrap font-medium text-base text-white"
458
- data-room-dimension-3d
459
- style={{ textShadow: DIMENSION_LABEL_OUTLINE }}
468
+ {dimensions.map(({ ownerId: wallId, ...dimension }) => {
469
+ const editable = editAllowed && !!wallId
470
+ return (
471
+ <Html
472
+ center
473
+ key={dimension.key}
474
+ position={[
475
+ (dimension.from[0] + dimension.to[0]) / 2,
476
+ dimension.elevation,
477
+ (dimension.from[1] + dimension.to[1]) / 2,
478
+ ]}
479
+ style={{ display: hidden ? 'none' : undefined, pointerEvents: 'none' }}
480
+ zIndexRange={[20, 0]}
460
481
  >
461
- {formatMeasurement(dimension.distance, unit, metricNotation)}
462
- </div>
463
- </Html>
464
- ))}
482
+ {editable && wallId && !hidden && editing === dimension.key ? (
483
+ <DimensionValueInput
484
+ dimension={dimension}
485
+ initial={formatMeasurement(dimension.distance, unit, metricNotation)}
486
+ onClose={() => setEditing(null)}
487
+ unit={unit}
488
+ wallId={wallId}
489
+ />
490
+ ) : (
491
+ <div
492
+ className={`-translate-y-3 whitespace-nowrap font-medium text-base text-white ${editable ? 'pointer-events-auto cursor-text' : ''}`}
493
+ data-room-dimension-3d
494
+ onClick={editable ? () => setEditing(dimension.key) : undefined}
495
+ onKeyDown={
496
+ editable
497
+ ? (event) => {
498
+ if (event.key === 'Enter') setEditing(dimension.key)
499
+ }
500
+ : undefined
501
+ }
502
+ onPointerDown={editable ? (event) => event.stopPropagation() : undefined}
503
+ role={editable ? 'button' : undefined}
504
+ style={{ textShadow: DIMENSION_LABEL_OUTLINE }}
505
+ tabIndex={editable ? 0 : undefined}
506
+ title={
507
+ editable
508
+ ? i18n._({
509
+ id: 'editor.roomHandles.typeASizeToMoveThe',
510
+ message: 'Type a size to move the wall',
511
+ })
512
+ : undefined
513
+ }
514
+ >
515
+ {formatMeasurement(dimension.distance, unit, metricNotation)}
516
+ </div>
517
+ )}
518
+ </Html>
519
+ )
520
+ })}
521
+ </group>
522
+ )
523
+ }
524
+
525
+ /**
526
+ * A temporary dimension's value, typed: Enter moves the selected wall so the
527
+ * dimension reads it (`commitDimensionValue`, one undo step), Esc or a click
528
+ * away leaves it. Feet-inches, metric and bare numbers parse as in the plan.
529
+ */
530
+ function DimensionValueInput({
531
+ dimension,
532
+ initial,
533
+ onClose,
534
+ unit,
535
+ wallId,
536
+ }: {
537
+ dimension: RoomDimension
538
+ initial: string
539
+ onClose: () => void
540
+ unit: 'metric' | 'imperial'
541
+ wallId: string
542
+ }) {
543
+ const i18n = usePascalI18n()
544
+ const [draft, setDraft] = useState(initial)
545
+ const [error, setError] = useState<string | null>(null)
546
+ const commit = () => {
547
+ const nextLength = parseDimensionInput(draft, unit)
548
+ if (nextLength === null) {
549
+ setError(
550
+ i18n._({
551
+ id: 'editor.roomHandles.cannotReadInput',
552
+ message: 'Cannot read "{input}"',
553
+ values: { input: draft.trim() },
554
+ }),
555
+ )
556
+ return
557
+ }
558
+ const outcome = commitDimensionValue({
559
+ target: {
560
+ ownerNodeId: wallId as AnyNodeId,
561
+ witnessStart: dimension.from,
562
+ witnessEnd: dimension.to,
563
+ value: dimension.distance,
564
+ },
565
+ nextLength,
566
+ unit,
567
+ })
568
+ if (outcome.ok) onClose()
569
+ else setError(outcome.reason)
570
+ }
571
+ return (
572
+ <div
573
+ className="pointer-events-auto -translate-y-3 w-24"
574
+ onPointerDown={(event) => event.stopPropagation()}
575
+ >
576
+ <input
577
+ // biome-ignore lint/a11y/noAutofocus: the box opens on a click to be typed in
578
+ autoFocus
579
+ aria-label={i18n._({ id: 'editor.roomHandles.dimensionValue', message: 'Dimension value' })}
580
+ className="w-full rounded-sm border border-primary bg-card px-1 py-0.5 text-center font-mono text-foreground text-xs shadow-sm outline-none"
581
+ onBlur={onClose}
582
+ onChange={(event) => {
583
+ setDraft(event.target.value)
584
+ setError(null)
585
+ }}
586
+ onFocus={(event) => event.target.select()}
587
+ onKeyDown={(event) => {
588
+ event.stopPropagation()
589
+ if (event.key === 'Enter') {
590
+ event.preventDefault()
591
+ commit()
592
+ } else if (event.key === 'Escape') {
593
+ event.preventDefault()
594
+ onClose()
595
+ }
596
+ }}
597
+ spellCheck={false}
598
+ value={draft}
599
+ />
600
+ {error ? (
601
+ <div className="mt-0.5 whitespace-nowrap rounded-sm bg-card px-1 text-[10px] text-destructive shadow-sm">
602
+ {error}
603
+ </div>
604
+ ) : null}
605
+ </div>
606
+ )
607
+ }
608
+
609
+ /**
610
+ * The selected walls' temporary dimensions in 3D, the plan's twin
611
+ * (`SelectedWallDimensions2D`): the room widths their pushes show and the
612
+ * outside sizes, drawn as the push draws them, in the walls' level frame.
613
+ */
614
+ export function SelectedWallDimensions3D() {
615
+ const selection = useSelectedWallDimensions()
616
+ const isFloorplanHovered = useEditor((s) => s.isFloorplanHovered)
617
+ const levelId = selection?.levelId ?? null
618
+ const root = useRef<Group>(null)
619
+ useFrame(() => {
620
+ const level = levelId ? sceneRegistry.nodes.get(levelId) : null
621
+ if (!root.current) return
622
+ root.current.visible = !!level && !isFloorplanHovered
623
+ if (level) {
624
+ level.updateWorldMatrix(true, false)
625
+ root.current.matrix.copy(level.matrixWorld)
626
+ }
627
+ })
628
+ // The labels are DOM (drei `Html`); with no document (a headless
629
+ // renderer) there is nothing to draw them into.
630
+ if (!selection || typeof document === 'undefined') return null
631
+ // Hovering the plan hides the dimensions rather than unmounting them: drei
632
+ // `Html` unmounts its DOM root synchronously, which React reports as an error
633
+ // when it happens inside a render.
634
+ return (
635
+ <group matrixAutoUpdate={false} ref={root}>
636
+ <RoomDimensionLines3D dimensions={selection.dimensions} hidden={isFloorplanHovered} />
465
637
  </group>
466
638
  )
467
639
  }
@@ -524,6 +696,7 @@ function MezzanineEdgeArrow({
524
696
  }
525
697
 
526
698
  export function RoomHandleDragPreview3D() {
699
+ const i18n = usePascalI18n()
527
700
  const drag = useRoomHandleDrag((s) => s.drag)
528
701
  const unit = useViewer((s) => s.unit)
529
702
  const metricNotation = useViewer((s) => s.metricNotation)
@@ -614,7 +787,9 @@ export function RoomHandleDragPreview3D() {
614
787
  />
615
788
  <Html center position={[ax, drag.value + 0.35, az]} zIndexRange={[25, 0]}>
616
789
  <div className={PILL}>
617
- <span className="font-medium text-foreground">Floor</span>
790
+ <span className="font-medium text-foreground">
791
+ {i18n._({ id: 'editor.roomHandles.floor', message: 'Floor' })}
792
+ </span>
618
793
  <span className="text-muted-foreground">
619
794
  {formatMeasurement(
620
795
  drag.value - roomFloorBase(useScene.getState().nodes, drag.zoneId),
@@ -627,6 +802,45 @@ export function RoomHandleDragPreview3D() {
627
802
  </group>
628
803
  )
629
804
  }
805
+ const ghosts = drag.ghosts.map((ghost, index) => {
806
+ const length = Math.hypot(ghost.end[0] - ghost.start[0], ghost.end[1] - ghost.start[1])
807
+ if (length < 1e-3) return null
808
+ return (
809
+ <mesh
810
+ key={index}
811
+ layers={EDITOR_LAYER}
812
+ material={materials.fill}
813
+ position={[
814
+ (ghost.start[0] + ghost.end[0]) / 2,
815
+ drag.base + ghost.height / 2,
816
+ (ghost.start[1] + ghost.end[1]) / 2,
817
+ ]}
818
+ raycast={noRaycast}
819
+ renderOrder={99}
820
+ rotation={[0, -Math.atan2(ghost.end[1] - ghost.start[1], ghost.end[0] - ghost.start[0]), 0]}
821
+ >
822
+ <boxGeometry args={[length, ghost.height, ghost.thickness]} />
823
+ </mesh>
824
+ )
825
+ })
826
+ if (drag.kind === 'wall-group-push')
827
+ return (
828
+ <group>
829
+ <RoomDimensionLines3D dimensions={drag.dimensions} />
830
+ {ghosts}
831
+ {drag.message && (
832
+ <Html
833
+ center
834
+ position={[drag.anchor[0], drag.height + 0.45, drag.anchor[1]]}
835
+ zIndexRange={[25, 0]}
836
+ >
837
+ <div className={PILL}>
838
+ <span className="font-medium text-red-500">{drag.message}</span>
839
+ </div>
840
+ </Html>
841
+ )}
842
+ </group>
843
+ )
630
844
  const at: Point = [
631
845
  drag.anchor[0] + drag.outward[0] * drag.distance,
632
846
  drag.anchor[1] + drag.outward[1] * drag.distance,
@@ -636,31 +850,7 @@ export function RoomHandleDragPreview3D() {
636
850
  return (
637
851
  <group>
638
852
  <RoomDimensionLines3D dimensions={drag.dimensions} />
639
- {drag.ghosts.map((ghost, index) => {
640
- const length = Math.hypot(ghost.end[0] - ghost.start[0], ghost.end[1] - ghost.start[1])
641
- if (length < 1e-3) return null
642
- return (
643
- <mesh
644
- key={index}
645
- layers={EDITOR_LAYER}
646
- material={materials.fill}
647
- position={[
648
- (ghost.start[0] + ghost.end[0]) / 2,
649
- drag.base + ghost.height / 2,
650
- (ghost.start[1] + ghost.end[1]) / 2,
651
- ]}
652
- raycast={noRaycast}
653
- renderOrder={99}
654
- rotation={[
655
- 0,
656
- -Math.atan2(ghost.end[1] - ghost.start[1], ghost.end[0] - ghost.start[0]),
657
- 0,
658
- ]}
659
- >
660
- <boxGeometry args={[length, ghost.height, ghost.thickness]} />
661
- </mesh>
662
- )
663
- })}
853
+ {ghosts}
664
854
  <Html center position={[at[0], drag.height + 0.45, at[1]]} zIndexRange={[25, 0]}>
665
855
  <div className={PILL}>
666
856
  {drag.message ? (
@@ -25,6 +25,7 @@ import { describeZoneOutline, type RoomSelectionRecord } from '../../lib/room-se
25
25
  import useEditor from '../../store/use-editor'
26
26
  import useInteractionScope from '../../store/use-interaction-scope'
27
27
  import { RoomControls2D, RoomControls3D } from './room-controls'
28
+ import { SelectedWallDimensions3D } from './room-handles'
28
29
 
29
30
  /**
30
31
  * A zone reached as a zone (hovered or selected through its area or pill, not
@@ -188,6 +189,7 @@ export function RoomHighlight3D() {
188
189
  return (
189
190
  <>
190
191
  <RoomControls3D />
192
+ <SelectedWallDimensions3D />
191
193
  {room ? <RoomMesh room={room} /> : null}
192
194
  </>
193
195
  )