@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
@@ -388,7 +388,13 @@ export function FloorplanRegistryMoveOverlay() {
388
388
  endTransientMarkers()
389
389
  })
390
390
  sfxEmitter.emit('sfx:item-place')
391
- useViewer.getState().setSelection({ selectedIds: initialAffectedIds })
391
+ // The moved node stays selected, as after a 3D move: the joined walls and
392
+ // rooms a wall move reshapes are part of its write, not a pick.
393
+ useViewer.getState().setSelection({
394
+ selectedIds: useScene.getState().nodes[movingNode.id]
395
+ ? [movingNode.id]
396
+ : initialAffectedIds,
397
+ })
392
398
  return
393
399
  }
394
400
 
@@ -844,14 +850,25 @@ export function FloorplanRegistryMoveOverlay() {
844
850
  }
845
851
  boxEl.setAttribute('transform', transform)
846
852
  const oldY = originalPosition[1]
853
+ const position = nodeRegistry.get(movingNode.type)?.capabilities?.movable?.resolvePosition?.({
854
+ node: useScene.getState().nodes[movingNode.id] ?? movingNode,
855
+ position: [finalX, oldY, finalZ],
856
+ nodes: useScene.getState().nodes,
857
+ }) ?? [finalX, oldY, finalZ]
858
+ if (def?.capabilities?.movable?.resolvePosition) {
859
+ useLiveTransforms.getState().set(movingNode.id, {
860
+ position: position as [number, number, number],
861
+ rotation: currentRotation,
862
+ })
863
+ }
847
864
  lastPositionValid = movableValidityConfig?.isValidPosition
848
865
  ? movableValidityConfig.isValidPosition({
849
866
  node: {
850
867
  ...movingNode,
851
- position: [finalX, oldY, finalZ],
868
+ position,
852
869
  rotation: currentRotation,
853
870
  } as AnyNode,
854
- position: [finalX, oldY, finalZ],
871
+ position: position as [number, number, number],
855
872
  rotation: currentRotation,
856
873
  levelId:
857
874
  (useViewer.getState().selection.levelId as AnyNodeId | null) ??
@@ -879,6 +896,11 @@ export function FloorplanRegistryMoveOverlay() {
879
896
  (storedRotation as [number?, number?, number?])[2] ?? 0,
880
897
  ]
881
898
  : snapped.rotation
899
+ const position = nodeRegistry.get(movingNode.type)?.capabilities?.movable?.resolvePosition?.({
900
+ node: useScene.getState().nodes[movingNode.id] ?? movingNode,
901
+ position: [sx, oldY, sz],
902
+ nodes: useScene.getState().nodes,
903
+ }) ?? [sx, oldY, sz]
882
904
  const rotationPatch = 'rotation' in movingNode ? { rotation } : {}
883
905
  const atomicPreview =
884
906
  isFreshPlacement &&
@@ -931,7 +953,7 @@ export function FloorplanRegistryMoveOverlay() {
931
953
  const committedId = commitFreshPlacementSubtree(
932
954
  movingNode.id as AnyNodeId,
933
955
  {
934
- position: [sx, oldY, sz],
956
+ position,
935
957
  ...rotationPatch,
936
958
  metadata: stripPlacementMetadataFlags((movingNode as { metadata?: unknown }).metadata),
937
959
  visible: true,
@@ -947,7 +969,7 @@ export function FloorplanRegistryMoveOverlay() {
947
969
  useScene.getState().updateNode(
948
970
  movingNode.id as AnyNodeId,
949
971
  {
950
- position: [sx, oldY, sz],
972
+ position,
951
973
  ...rotationPatch,
952
974
  } as Partial<AnyNode>,
953
975
  )
@@ -999,6 +1021,8 @@ export function FloorplanRegistryMoveOverlay() {
999
1021
  for (const relatedEntry of relatedEntries) {
1000
1022
  relatedEntry.style.visibility = ''
1001
1023
  }
1024
+ if (def?.capabilities?.movable?.resolvePosition)
1025
+ useLiveTransforms.getState().clear(movingNode.id)
1002
1026
  boxEl.remove()
1003
1027
  useAlignmentGuides.getState().clear()
1004
1028
  }
@@ -13,33 +13,92 @@ import { useFloorplanRender } from './floorplan-render-context'
13
13
  *
14
14
  * endpoint (corner) → square midpoint → triangle
15
15
  * intersection → ✕ cross wall body (edge) → circle
16
+ * perpendicular → ⊥ corner projection guide → diamond
17
+ * grid length → small +
16
18
  *
17
19
  * Indigo to match the 3D beacon, except the corner (endpoint) square which is
18
- * green to match the alignment guides. Sizes are pixel-budgeted via
19
- * `unitsPerPixel` so the marker stays a
20
+ * green to match the alignment guides. A point that carries a `label` (a wall
21
+ * pulled from a wall end) also names what it caught in a pill beside the
22
+ * glyph. Sizes are pixel-budgeted via `unitsPerPixel` so the marker stays a
20
23
  * constant size on screen at any zoom. Mounted inside the `data-floorplan-scene`
21
24
  * group so coordinates are world meters (XZ) 1:1, like the alignment guides.
22
25
  */
23
26
  const COLOR = '#6366f1' // indigo-500 — matches the 3D beacon
24
27
  const ENDPOINT_COLOR = '#22c55e' // green-500 — corner (endpoint) snap accent
25
28
 
26
- export const FloorplanSnapBeaconLayer = memo(function FloorplanSnapBeaconLayer() {
29
+ export const FloorplanSnapBeaconLayer = memo(function FloorplanSnapBeaconLayer({
30
+ unitsPerPixel,
31
+ }: {
32
+ /** The plan's live scale, which keeps a labelled marker screen-sized. */
33
+ unitsPerPixel?: number
34
+ }) {
27
35
  const point = useWallSnapIndicator((s) => s.point)
28
36
  const ctx = useFloorplanRender()
29
37
 
30
38
  if (!point) return null
31
39
 
32
- const upp = ctx?.unitsPerPixel ?? 0.01
40
+ // A labelled point (a wall pulled from a wall end) is sized on screen.
41
+ const upp = (point.label ? unitsPerPixel : undefined) ?? ctx?.unitsPerPixel ?? 0.01
33
42
  const m = 6 * upp // base half-size of the glyph in world meters
34
43
  const stroke = 1.5 * upp
35
44
 
36
45
  return (
37
46
  <g pointerEvents="none">
38
47
  <SnapMarker color={COLOR} kind={point.kind} m={m} stroke={stroke} x={point.x} z={point.z} />
48
+ {point.label ? (
49
+ <SnapLabel
50
+ color={point.kind === 'endpoint' ? ENDPOINT_COLOR : COLOR}
51
+ sceneRotationDeg={ctx?.sceneRotationDeg ?? 0}
52
+ text={point.label}
53
+ upp={upp}
54
+ x={point.x}
55
+ z={point.z}
56
+ />
57
+ ) : null}
39
58
  </g>
40
59
  )
41
60
  })
42
61
 
62
+ /** What the snap caught, in a pill down and to the right of the glyph, kept upright. */
63
+ function SnapLabel({
64
+ color,
65
+ sceneRotationDeg,
66
+ text,
67
+ upp,
68
+ x,
69
+ z,
70
+ }: {
71
+ color: string
72
+ sceneRotationDeg: number
73
+ text: string
74
+ upp: number
75
+ x: number
76
+ z: number
77
+ }) {
78
+ const fontSize = 10.5 * upp
79
+ const padX = 5 * upp
80
+ const width = text.length * fontSize * 0.56 + padX * 2
81
+ const height = fontSize + 6 * upp
82
+ return (
83
+ <g
84
+ transform={`translate(${x} ${z}) rotate(${-sceneRotationDeg}) translate(${12 * upp} ${16 * upp})`}
85
+ >
86
+ <rect fill={color} height={height} rx={3 * upp} width={width} x={0} y={-height / 2} />
87
+ <text
88
+ dominantBaseline="middle"
89
+ fill="#ffffff"
90
+ fontFamily="-apple-system, system-ui, sans-serif"
91
+ fontSize={fontSize}
92
+ fontWeight={600}
93
+ x={padX}
94
+ y={0}
95
+ >
96
+ {text}
97
+ </text>
98
+ </g>
99
+ )
100
+ }
101
+
43
102
  function SnapMarker({
44
103
  color,
45
104
  kind,
@@ -72,6 +131,39 @@ function SnapMarker({
72
131
  </g>
73
132
  )
74
133
  }
134
+ if (kind === 'perpendicular') {
135
+ // ⊥ — a base line with an upright from its middle, and the square corner.
136
+ const c = m * 1.3
137
+ return (
138
+ <g fill="none" stroke={color} strokeLinecap="round" strokeWidth={stroke * 1.6}>
139
+ <line x1={x - c} x2={x + c} y1={z} y2={z} />
140
+ <line x1={x} x2={x} y1={z} y2={z - c * 1.4} />
141
+ <polyline
142
+ points={`${x + c * 0.5},${z} ${x + c * 0.5},${z - c * 0.5} ${x},${z - c * 0.5}`}
143
+ />
144
+ </g>
145
+ )
146
+ }
147
+ if (kind === 'alignment') {
148
+ const c = m * 1.2
149
+ return (
150
+ <polygon
151
+ fill="#ffffff"
152
+ points={`${x},${z - c} ${x + c},${z} ${x},${z + c} ${x - c},${z}`}
153
+ stroke={color}
154
+ strokeWidth={stroke * 1.4}
155
+ />
156
+ )
157
+ }
158
+ if (kind === 'grid') {
159
+ const c = m * 0.9
160
+ return (
161
+ <g stroke={color} strokeLinecap="round" strokeWidth={stroke * 1.4}>
162
+ <line x1={x - c} x2={x + c} y1={z} y2={z} />
163
+ <line x1={x} x2={x} y1={z - c} y2={z + c} />
164
+ </g>
165
+ )
166
+ }
75
167
  // 'wall' (edge / along-wall) → circle
76
168
  return <circle cx={x} cy={z} fill={color} r={m} />
77
169
  }
@@ -1,7 +1,7 @@
1
1
  'use client'
2
2
 
3
3
  import type { AnyNode, AnyNodeId } from '@pascal-app/core'
4
- import { useScene } from '@pascal-app/core'
4
+ import { useLocale, useScene } from '@pascal-app/core'
5
5
  import { useViewer } from '@pascal-app/viewer'
6
6
  import { useCallback, useEffect, useRef, useState } from 'react'
7
7
  import { createPortal } from 'react-dom'
@@ -11,6 +11,7 @@ import {
11
11
  resolveDimensionDrive,
12
12
  } from '../../../lib/floorplan/dimension-drive'
13
13
  import { formatScheduleLength } from '../../../lib/floorplan/schedules'
14
+ import { commitRoomPush } from '../../../lib/room-handle-drag'
14
15
  import useEditor from '../../../store/use-editor'
15
16
 
16
17
  /**
@@ -20,7 +21,8 @@ import useEditor from '../../../store/use-editor'
20
21
  * plate, pre-filled with the current text. Enter commits, Esc cancels.
21
22
  * Committing DRIVES geometry: the anchor the dimension runs toward moves
22
23
  * along the dimension direction by the delta, and connected wall junctions
23
- * follow (`planDimensionDrive`). When nothing bindable is under the far
24
+ * follow (`planDimensionDrive`); a dimension across a wall slides the whole
25
+ * wall, as its push arrow does (`commitRoomPush`). When nothing bindable is under the far
24
26
  * anchor the value is written as `textOverride` on a construction-dimension
25
27
  * node instead, and the label renders an "override" badge.
26
28
  *
@@ -60,6 +62,32 @@ export function useDimensionEditAllowed(): boolean {
60
62
  return useEditor((state) => isDimensionEditAllowed(state, { readOnly }))
61
63
  }
62
64
 
65
+ /**
66
+ * The typeable dimension value under a press, with the node typing it drives:
67
+ * the pressed element's or, when the plan's dashed group box lies over it, the
68
+ * one beneath (a multi-selection's walls keep their dimensions inside it).
69
+ */
70
+ export function typeableDimensionAt(
71
+ target: EventTarget | null,
72
+ clientX: number,
73
+ clientY: number,
74
+ ): { label: Element; ownerNodeId: AnyNodeId } | null {
75
+ const resolve = (element: Element | null | undefined) => {
76
+ const hit = element?.closest?.('[data-floorplan-dimension-hit]')
77
+ const label = hit?.closest('[data-floorplan-annotation-label]')
78
+ const ownerNodeId = hit?.closest('[data-node-id]')?.getAttribute('data-node-id')
79
+ return label && ownerNodeId ? { label, ownerNodeId: ownerNodeId as AnyNodeId } : null
80
+ }
81
+ const element = target as Element | null
82
+ const direct = resolve(element)
83
+ if (direct || !element?.closest?.('[data-group-selection-box]')) return direct
84
+ for (const below of document.elementsFromPoint(clientX, clientY)) {
85
+ const found = resolve(below)
86
+ if (found) return found
87
+ }
88
+ return null
89
+ }
90
+
63
91
  type EditTarget = {
64
92
  ownerNodeId: AnyNodeId
65
93
  text: string
@@ -78,6 +106,7 @@ export function FloorplanDimensionEditOverlay(): React.ReactElement | null {
78
106
  const [error, setError] = useState<string | null>(null)
79
107
  const inputRef = useRef<HTMLInputElement | null>(null)
80
108
  const unit = useViewer((state) => state.unit)
109
+ const locale = useLocale()
81
110
 
82
111
  useEffect(() => {
83
112
  let down: { x: number; y: number } | null = null
@@ -92,13 +121,9 @@ export function FloorplanDimensionEditOverlay(): React.ReactElement | null {
92
121
  return
93
122
  }
94
123
  if (!isDimensionEditAllowed(useEditor.getState(), useScene.getState())) return
95
- const origin = event.target as Element | null
96
- const hit = origin?.closest?.('[data-floorplan-dimension-hit]')
97
- if (!hit) return
98
- const label = hit.closest('[data-floorplan-annotation-label]')
99
- const owner = hit.closest('[data-node-id]')
100
- const ownerNodeId = owner?.getAttribute('data-node-id')
101
- if (!label || !ownerNodeId) return
124
+ const found = typeableDimensionAt(event.target, event.clientX, event.clientY)
125
+ if (!found) return
126
+ const { label, ownerNodeId } = found
102
127
 
103
128
  const value = number(label.getAttribute('data-floorplan-dimension-value'))
104
129
  const witnessStart = point(
@@ -116,7 +141,7 @@ export function FloorplanDimensionEditOverlay(): React.ReactElement | null {
116
141
  const box = label.getBoundingClientRect()
117
142
  const text = label.getAttribute('data-floorplan-dimension-text') ?? ''
118
143
  setTarget({
119
- ownerNodeId: ownerNodeId as AnyNodeId,
144
+ ownerNodeId,
120
145
  text,
121
146
  value,
122
147
  witnessStart,
@@ -154,7 +179,7 @@ export function FloorplanDimensionEditOverlay(): React.ReactElement | null {
154
179
 
155
180
  const commit = useCallback(() => {
156
181
  if (!target) return
157
- const nextLength = parseDimensionInput(draft, unit)
182
+ const nextLength = parseDimensionInput(draft, unit, locale)
158
183
  if (nextLength === null) {
159
184
  setError(`Cannot read "${draft.trim()}"`)
160
185
  return
@@ -166,7 +191,7 @@ export function FloorplanDimensionEditOverlay(): React.ReactElement | null {
166
191
  }
167
192
  setTarget(null)
168
193
  setError(null)
169
- }, [draft, target, unit])
194
+ }, [draft, locale, target, unit])
170
195
 
171
196
  if (!target) return null
172
197
  if (typeof document === 'undefined') return null
@@ -246,6 +271,21 @@ export function commitDimensionValue(args: {
246
271
  return { ok: true }
247
272
  }
248
273
 
274
+ if (resolution.target.kind === 'wall-offset') {
275
+ // The wall's own push: the whole wall slides along its normal, connected
276
+ // walls follow through the junction planner, rooms and floors re-form —
277
+ // one undo step, exactly as dragging its side arrow that far.
278
+ const delta = args.nextLength - args.target.value
279
+ if (Math.abs(delta) < 1e-9) return { ok: true }
280
+ return commitRoomPush(
281
+ { wallId: resolution.target.wallId, t0: 0, t1: 1 },
282
+ resolution.target.direction,
283
+ delta,
284
+ )
285
+ ? { ok: true }
286
+ : { ok: false, reason: "Can't move this wall there" }
287
+ }
288
+
249
289
  const plan = planDimensionDrive({
250
290
  nodes,
251
291
  target: resolution.target,
@@ -19,6 +19,7 @@ export type DraftMeasurement = {
19
19
  }
20
20
 
21
21
  export function FloorplanDraftMeasurement({
22
+ lengthHidden = false,
22
23
  measurement,
23
24
  measurementStroke,
24
25
  labelBackground,
@@ -26,6 +27,8 @@ export function FloorplanDraftMeasurement({
26
27
  sceneRotationDeg,
27
28
  unitsPerPixel,
28
29
  }: {
30
+ /** Leave the length plate to something drawn in its place (the typed-entry HUD). */
31
+ lengthHidden?: boolean
29
32
  measurement: DraftMeasurement
30
33
  measurementStroke: string
31
34
  labelBackground: string
@@ -68,7 +71,10 @@ export function FloorplanDraftMeasurement({
68
71
 
69
72
  return (
70
73
  <g pointerEvents="none">
71
- <g transform={`translate(${cx} ${cy}) rotate(${labelAngleDeg})`}>
74
+ <g
75
+ display={lengthHidden ? 'none' : undefined}
76
+ transform={`translate(${cx} ${cy}) rotate(${labelAngleDeg})`}
77
+ >
72
78
  <rect
73
79
  fill={labelBg}
74
80
  height={lengthPlateH}
@@ -4,17 +4,19 @@ import {
4
4
  type AnyNode,
5
5
  type AnyNodeId,
6
6
  type FloorplanGeometry,
7
+ formatLinearMeasurement,
7
8
  type GeometryContext,
8
9
  nodeRegistry,
9
10
  useScene,
10
11
  } from '@pascal-app/core'
11
12
  import { useViewer } from '@pascal-app/viewer'
12
13
  import { memo, useMemo } from 'react'
13
- import { formatLinearMeasurement } from '../../../lib/measurements'
14
14
  import usePlacementPreview from '../../../store/use-placement-preview'
15
+ import { usePlacementTyping } from '../../../store/use-placement-typing'
15
16
  import { useFloorplanRender, useFloorplanSceneRotation } from '../floorplan-render-context'
16
17
  import { FloorplanDimensionRenderer } from './floorplan-dimension-renderer'
17
18
  import { FloorplanGeometryRenderer } from './floorplan-geometry-renderer'
19
+ import { FloorplanPlacementTypingHud } from './floorplan-placement-typing-hud'
18
20
 
19
21
  export interface FloorplanNodePreviewProps {
20
22
  node: AnyNode
@@ -139,14 +141,24 @@ export const FloorplanPlacementPreviewLayer = memo(function FloorplanPlacementPr
139
141
  const dimensions = usePlacementPreview((s) => s.dimensions)
140
142
  const activeDimensionId = usePlacementPreview((s) => s.activeDimensionId)
141
143
  const dimensionInput = usePlacementPreview((s) => s.dimensionInput)
144
+ const typingProjectedPosition = usePlacementTyping((s) => s.projectedPosition)
142
145
  const unit = useViewer((s) => s.unit)
143
146
  const metricNotation = useViewer((s) => s.metricNotation)
144
147
  const sceneRotationDeg = useFloorplanSceneRotation()
148
+ const unitsPerPixel = useFloorplanRender()?.unitsPerPixel ?? 1
145
149
  if (!node) return null
146
150
 
147
151
  return (
148
152
  <g data-floorplan-placement-preview>
149
153
  <FloorplanNodePreview contextNodes={contextNodes} node={node} parentNode={parentNode} />
154
+ {typingProjectedPosition ? (
155
+ <FloorplanPlacementTypingHud
156
+ sceneRotationDeg={sceneRotationDeg}
157
+ unitsPerPixel={unitsPerPixel}
158
+ x={typingProjectedPosition[0]}
159
+ y={typingProjectedPosition[2]}
160
+ />
161
+ ) : null}
150
162
  <g data-floorplan-placement-dimensions>
151
163
  {dimensions
152
164
  .filter((dimension) => dimension.renderInFloorplan !== false)
@@ -0,0 +1,35 @@
1
+ import { afterEach, expect, test } from 'bun:test'
2
+ import {
3
+ PLACEMENT_TYPING_LENGTH_ANGLE,
4
+ usePlacementTyping,
5
+ } from '../../../store/use-placement-typing'
6
+ import { renderCurrentMarkup } from '../../../test-utils/render-current-markup'
7
+ import { FloorplanPlacementTypingHud } from './floorplan-placement-typing-hud'
8
+
9
+ afterEach(() => {
10
+ usePlacementTyping.getState().clear()
11
+ })
12
+
13
+ test('sizes and counter-rotates the HUD from the props it is given', () => {
14
+ usePlacementTyping.getState().begin({ labels: PLACEMENT_TYPING_LENGTH_ANGLE, activeField: 0 })
15
+
16
+ const markup = renderCurrentMarkup(
17
+ <svg>
18
+ <FloorplanPlacementTypingHud sceneRotationDeg={30} unitsPerPixel={0.02} x={1} y={2} />
19
+ </svg>,
20
+ )
21
+
22
+ expect(markup).toContain('transform="translate(1 2) rotate(-30)"')
23
+ expect(markup).toMatch(/<foreignObject[^>]*width="4.72"/)
24
+ expect(markup).toContain('scale(0.02)')
25
+ })
26
+
27
+ test('renders nothing without a typing session', () => {
28
+ const markup = renderCurrentMarkup(
29
+ <svg>
30
+ <FloorplanPlacementTypingHud sceneRotationDeg={0} unitsPerPixel={0.02} x={0} y={0} />
31
+ </svg>,
32
+ )
33
+
34
+ expect(markup).not.toContain('foreignObject')
35
+ })
@@ -0,0 +1,65 @@
1
+ 'use client'
2
+
3
+ import { usePlacementTyping } from '../../../store/use-placement-typing'
4
+ import { PlacementCoordinateInput } from '../../tools/shared/placement-coordinate-input'
5
+
6
+ const HUD_WIDTH_PX = 236
7
+ const HUD_HEIGHT_PX = 64
8
+ const HUD_LIFT_PX = 14
9
+
10
+ /**
11
+ * The typed-entry HUD placed in the plan at `x`/`y` (level-local metres).
12
+ * HTML inside a `foreignObject` is laid out in CSS pixels equal to SVG user
13
+ * units, and the plan's `<g>` is zoomed and rotated, so the box is scaled by
14
+ * `unitsPerPixel` and counter-rotated to stay screen-sized and upright. Both
15
+ * come from the caller: the wall draft renders outside `FloorplanRenderProvider`.
16
+ * It sits just above its anchor, and only the HUD itself takes the pointer, so
17
+ * clicks on the ghost or the draft under it still land on the plan.
18
+ */
19
+ export function FloorplanPlacementTypingHud({
20
+ placeholders,
21
+ sceneRotationDeg,
22
+ unitsPerPixel,
23
+ x,
24
+ y,
25
+ }: {
26
+ placeholders?: readonly string[]
27
+ sceneRotationDeg: number
28
+ unitsPerPixel: number
29
+ x: number
30
+ y: number
31
+ }) {
32
+ const isActive = usePlacementTyping((state) => state.isActive)
33
+ const scale = Math.max(unitsPerPixel, 1e-6)
34
+ if (!isActive) return null
35
+
36
+ return (
37
+ <g transform={`translate(${x} ${y}) rotate(${-sceneRotationDeg})`}>
38
+ <foreignObject
39
+ height={HUD_HEIGHT_PX * scale}
40
+ overflow="visible"
41
+ pointerEvents="none"
42
+ style={{ overflow: 'visible' }}
43
+ width={HUD_WIDTH_PX * scale}
44
+ x={(-HUD_WIDTH_PX / 2) * scale}
45
+ y={-(HUD_HEIGHT_PX + HUD_LIFT_PX) * scale}
46
+ >
47
+ <div
48
+ style={{
49
+ alignItems: 'flex-end',
50
+ display: 'flex',
51
+ height: HUD_HEIGHT_PX,
52
+ justifyContent: 'center',
53
+ overflow: 'visible',
54
+ pointerEvents: 'none',
55
+ transform: `scale(${scale})`,
56
+ transformOrigin: 'top left',
57
+ width: HUD_WIDTH_PX,
58
+ }}
59
+ >
60
+ <PlacementCoordinateInput placeholders={placeholders} />
61
+ </div>
62
+ </foreignObject>
63
+ </g>
64
+ )
65
+ }
@@ -11,11 +11,13 @@ import { type AnyNodeDefinition, emitter, nodeRegistry, registerNode } from '@pa
11
11
  import { createElement } from 'react'
12
12
  import { renderToStaticMarkup } from 'react-dom/server'
13
13
  import { z } from 'zod'
14
+ import { AXIS_MOVE_DRAG_LABEL, PATH_POINT_DRAG_LABEL } from '../../../lib/contextual-help'
14
15
  import {
15
16
  FLOORPLAN_NODE_EXTENSION_KEY,
16
17
  floorplanGeometryMetadata,
17
18
  } from '../../../lib/floorplan/floorplan-extension'
18
19
  import type { SessionWrites } from '../../../lib/session-writes'
20
+ import { snapContextOf } from '../../../lib/snapping-mode'
19
21
  import {
20
22
  buildFloorplanEntryGeometry,
21
23
  cancelFloorplanAffordanceDrag,
@@ -24,6 +26,7 @@ import {
24
26
  collectFloorplanLinkedLevelNodes,
25
27
  computeAffectedSiblingIds,
26
28
  floorplanAffordanceReshapeScope,
29
+ floorplanEntryStripsHandles,
27
30
  floorplanEntryYieldsToTool,
28
31
  floorplanHandleDoubleClickAffordance,
29
32
  InteractiveGeometry,
@@ -162,7 +165,6 @@ describe('site-scoped floorplan discovery', () => {
162
165
  siblingEpoch: 0,
163
166
  unit: 'metric' as const,
164
167
  metricNotation: 'meters' as const,
165
- wallDimensionReference: 'finished-faces' as const,
166
168
  visibilityRootId: site.id,
167
169
  }
168
170
  const committedSnapshot = structuredClone(overlay)
@@ -276,6 +278,26 @@ describe('floorplan affordance ownership', () => {
276
278
  driver: 'floorplan',
277
279
  })
278
280
  })
281
+
282
+ // With no scope, Select mode reads every snap mode as off, so these drags
283
+ // never snapped to the grid on the plan.
284
+ test('gives the MEP run point and segment drags a handle-drag scope in the run snap context', () => {
285
+ const point = floorplanAffordanceReshapeScope('move-path-point', 'duct_1', { pointIndex: 0 })
286
+ const segment = floorplanAffordanceReshapeScope('move-segment', 'duct_1', { segmentIndex: 0 })
287
+ expect(point).toEqual({ kind: 'handle-drag', nodeId: 'duct_1', handle: PATH_POINT_DRAG_LABEL })
288
+ expect(segment).toEqual({ kind: 'handle-drag', nodeId: 'duct_1', handle: AXIS_MOVE_DRAG_LABEL })
289
+ for (const scope of [point, segment]) {
290
+ expect(
291
+ snapContextOf({
292
+ scope: scope!,
293
+ mode: 'select',
294
+ tool: null,
295
+ profileOf: () => undefined,
296
+ profileOfNode: () => 'structural',
297
+ }),
298
+ ).toBe('polygon')
299
+ }
300
+ })
279
301
  })
280
302
 
281
303
  function cabinetRun(id: string, children: string[] = [], parentId: string | null = 'level_test') {
@@ -793,6 +815,17 @@ describe('floorplan entry routing while a tool is active', () => {
793
815
  expect(floorplanEntryYieldsToTool({ mode: 'select', openingPlacement: false })).toBe(false)
794
816
  expect(floorplanEntryYieldsToTool({ mode: 'delete', openingPlacement: false })).toBe(false)
795
817
  })
818
+
819
+ // A roof plane selected before arming the roof tool kept its live corner and edge handles: a
820
+ // click near its eave resized it instead of drawing.
821
+ test('a selected node shows no edit handles while a build tool is armed', () => {
822
+ const entry = { multiSelected: false, ceilingWithoutSession: false }
823
+ expect(floorplanEntryStripsHandles({ ...entry, mode: 'select' })).toBe(false)
824
+ expect(floorplanEntryStripsHandles({ ...entry, mode: 'build' })).toBe(true)
825
+ expect(floorplanEntryStripsHandles({ ...entry, mode: 'select', multiSelected: true })).toBe(
826
+ true,
827
+ )
828
+ })
796
829
  })
797
830
 
798
831
  describe('isFloorplanHierarchyVisible', () => {