@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
@@ -59,7 +59,11 @@ import {
59
59
  useRoomRecords,
60
60
  } from '../../../hooks/use-selected-room'
61
61
  import { useCeilingEditSession } from '../../../lib/ceiling-edit-session'
62
- import { ROTATE_HANDLE_DRAG_LABEL } from '../../../lib/contextual-help'
62
+ import {
63
+ AXIS_MOVE_DRAG_LABEL,
64
+ PATH_POINT_DRAG_LABEL,
65
+ ROTATE_HANDLE_DRAG_LABEL,
66
+ } from '../../../lib/contextual-help'
63
67
  import {
64
68
  canDirectRotateNode,
65
69
  resolveDirectManipulationNode,
@@ -78,7 +82,7 @@ import { resolveNodeForDrawingType } from '../../../lib/floorplan/drawing-coordi
78
82
  import {
79
83
  createFloorplanContextExtensions,
80
84
  type FloorplanAnnotationRole,
81
- type FloorplanWallDimensionReference,
85
+ type FloorplanWallDimensionFaces,
82
86
  getFloorplanNodeExtension,
83
87
  readFloorplanGeometryMetadata,
84
88
  withFloorplanGeometryMetadata,
@@ -86,7 +90,6 @@ import {
86
90
  import {
87
91
  type FloorplanMode,
88
92
  resolveFloorplanAnnotationVisibility,
89
- resolveFloorplanWallDimensionReference,
90
93
  } from '../../../lib/floorplan/floorplan-mode'
91
94
  import {
92
95
  buildFloorplanContext,
@@ -115,6 +118,11 @@ import { createSessionWrites, type SessionWrites } from '../../../lib/session-wr
115
118
  import { sfxEmitter } from '../../../lib/sfx-bus'
116
119
  import { clearSurfacePlanSnapFeedback } from '../../../lib/surface-plan-snap'
117
120
  import { paintZoneMembership } from '../../../lib/units'
121
+ import {
122
+ startWallPull,
123
+ WALL_PULL_AFFORDANCE,
124
+ type WallPullGripPayload,
125
+ } from '../../../lib/wall-pull'
118
126
  import useDirectManipulationFeedback from '../../../store/use-direct-manipulation-feedback'
119
127
  import useDrawingView from '../../../store/use-drawing-view'
120
128
  import useEditor, { isAngleSnapActive, type Mode } from '../../../store/use-editor'
@@ -130,7 +138,12 @@ import { isViewVisible, VIEW_2D } from '../../../store/view-layout'
130
138
  import { FloorRegionControls2D } from '../../editor/floor-region-controls'
131
139
  import { startGroupPickUp } from '../../editor/group-actions'
132
140
  import { classifyParticipant } from '../../editor/group-transform-shared'
133
- import { RoomHandleDragPreview2D, RoomHandles2D } from '../../editor/room-handles-2d'
141
+ import {
142
+ RoomHandleDragPreview2D,
143
+ RoomHandles2D,
144
+ SelectedWallDimensions2D,
145
+ useSelectionDimensionNodes,
146
+ } from '../../editor/room-handles-2d'
134
147
  import { RoomHighlight2D } from '../../editor/room-highlight'
135
148
  import { RoomLabels2D } from '../../editor/room-labels'
136
149
  import { suppressBoxSelectForPointer } from '../../tools/select/box-select-state'
@@ -150,7 +163,10 @@ import {
150
163
  resolveSvgAnnotationCollisions,
151
164
  svgAnnotationLabelId,
152
165
  } from './floorplan-annotation-layout'
153
- import { FloorplanDimensionEditOverlay } from './floorplan-dimension-edit-overlay'
166
+ import {
167
+ FloorplanDimensionEditOverlay,
168
+ typeableDimensionAt,
169
+ } from './floorplan-dimension-edit-overlay'
154
170
  import { FloorplanDimensionRenderer } from './floorplan-dimension-renderer'
155
171
  import { FloorplanGeometryRenderer } from './floorplan-geometry-renderer'
156
172
  import {
@@ -199,6 +215,15 @@ const DIRECT_DRAG_THRESHOLD_PX = 4
199
215
  const DIRECT_ROTATE_EPSILON = 1e-6
200
216
  const DIRECT_ROTATE_RADIANS_PER_PIXEL = Math.PI / 180
201
217
  const MAX_HANDLE_UNITS_PER_PIXEL = 0.015
218
+ // A wall end's pull grip, in screen pixels out from the end along the wall:
219
+ // clear of the end's move grip (its hit ring reaches ~17 px), a short dashed
220
+ // leader, then a round button with an outward arrow.
221
+ const PULL_GRIP_LEADER_START_PX = 9
222
+ const PULL_GRIP_CENTER_PX = 30
223
+ const PULL_GRIP_RADIUS_PX = 10
224
+ const PULL_GRIP_HIT_RADIUS_PX = 13
225
+ const PULL_GRIP_FILL = '#10b981' // emerald-500
226
+ const PULL_GRIP_HOVER_FILL = '#059669' // emerald-600
202
227
 
203
228
  export function resolveFloorplanHandleUnitsPerPixel(unitsPerPixel: number): number {
204
229
  return Math.min(unitsPerPixel, MAX_HANDLE_UNITS_PER_PIXEL)
@@ -300,7 +325,7 @@ export function subscribeFloorplanAffordanceToolCancel(
300
325
  // edits that set a direction/shape map to a scope; resize / rotate / body-move
301
326
  // affordances return `null` (no polygon/wall snapping chip). Keyed off the
302
327
  // affordance name the kinds register (`move-vertex` / `move-edge` / `add-vertex`
303
- // / `curve` / `move-endpoint`).
328
+ // / `curve` / `move-endpoint` / `move-path-point` / `move-segment`).
304
329
  export function floorplanAffordanceReshapeScope(
305
330
  affordance: string,
306
331
  nodeId: string,
@@ -341,6 +366,14 @@ export function floorplanAffordanceReshapeScope(
341
366
  if (affordance.includes('rotate')) {
342
367
  return { kind: 'handle-drag', nodeId, handle: ROTATE_HANDLE_DRAG_LABEL }
343
368
  }
369
+ // MEP run point and run segment drags: the same handle-drag scopes as their
370
+ // 3D arrows, so they snap in the kind's own context (polygon for runs).
371
+ if (affordance === 'move-path-point') {
372
+ return { kind: 'handle-drag', nodeId, handle: PATH_POINT_DRAG_LABEL }
373
+ }
374
+ if (affordance === 'move-segment') {
375
+ return { kind: 'handle-drag', nodeId, handle: AXIS_MOVE_DRAG_LABEL }
376
+ }
344
377
  return null
345
378
  }
346
379
 
@@ -387,7 +420,7 @@ type NodeDeps = {
387
420
  live: LiveTransform | undefined
388
421
  unit: 'metric' | 'imperial'
389
422
  metricNotation: 'meters' | 'millimeters'
390
- wallDimensionReference: FloorplanWallDimensionReference
423
+ wallDimensionFaces?: FloorplanWallDimensionFaces
391
424
  selected: boolean
392
425
  highlighted: boolean
393
426
  hovered: boolean
@@ -497,6 +530,20 @@ export function floorplanEntryYieldsToTool(state: {
497
530
  return state.mode === 'build' || state.openingPlacement
498
531
  }
499
532
 
533
+ /**
534
+ * Whether an entry draws its node without edit handles or dimension chrome: a member of a
535
+ * multi-selection, a ceiling outside its edit session, or anything while a build tool is armed (a
536
+ * selected roof keeps its selection for a new plane to join it, but a press near its eave must
537
+ * draw, not resize it).
538
+ */
539
+ export function floorplanEntryStripsHandles(state: {
540
+ mode: Mode
541
+ multiSelected: boolean
542
+ ceilingWithoutSession: boolean
543
+ }): boolean {
544
+ return state.multiSelected || state.ceilingWithoutSession || state.mode === 'build'
545
+ }
546
+
500
547
  function floorplanEntryYieldsToToolNow(): boolean {
501
548
  const { mode, tool } = useEditor.getState()
502
549
  const movingNode = getMovingNode()
@@ -596,6 +643,11 @@ export const FloorplanRegistryLayer = memo(function FloorplanRegistryLayer() {
596
643
  // `handleClickStop` / `handleSelect`, so the tool never sees it. Pass
597
644
  // clicks through in that case (`floorplanEntryYieldsToToolNow`).
598
645
  const editorMode = useEditor((s) => s.mode)
646
+ const selectionDimensionNodes = useSelectionDimensionNodes()
647
+ const selectionDimensionIds = useMemo(
648
+ () => new Set(selectionDimensionNodes.map((node) => node.id)),
649
+ [selectionDimensionNodes],
650
+ )
599
651
  const floorplanSelectionTool = useEditor((s) => s.floorplanSelectionTool)
600
652
  const endpointReshape = useEndpointReshape()
601
653
  const isMarqueeSelectionActive =
@@ -609,12 +661,9 @@ export const FloorplanRegistryLayer = memo(function FloorplanRegistryLayer() {
609
661
  const floorplanVisible = useEditor((s) => isViewVisible(s, VIEW_2D))
610
662
  const drawingType = useDrawingView((s) => s.drawingType)
611
663
  const annotationVisibility = useFloorplanAnnotationVisibility((s) => s.visibility)
612
- const wallDimensionReference = useFloorplanAnnotationVisibility((s) => s.wallDimensionReference)
664
+ const showDimensions = useFloorplanAnnotationVisibility((s) => s.showDimensions)
665
+ const wallDimensionFaces = useFloorplanAnnotationVisibility((s) => s.wallDimensionFaces)
613
666
  const floorplanMode = useFloorplanMode((s) => s.mode)
614
- const effectiveWallDimensionReference = resolveFloorplanWallDimensionReference(
615
- floorplanMode,
616
- wallDimensionReference,
617
- )
618
667
  // Elevator builders read runtime state imperatively, so entries include this
619
668
  // rare-changing ref in their cache deps.
620
669
  const interactiveElevators = useInteractive((s) => s.elevators)
@@ -981,6 +1030,12 @@ export const FloorplanRegistryLayer = memo(function FloorplanRegistryLayer() {
981
1030
  if (event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) return false
982
1031
  if (movingNode) return false
983
1032
  if (useEditor.getState().mode === 'delete') return false
1033
+ // A press on a member's dimension value is the type-a-value box's: its click opens it.
1034
+ if (!immediate && typeableDimensionAt(event.target, event.clientX, event.clientY)) {
1035
+ event.stopPropagation()
1036
+ suppressBoxSelectForPointer(event)
1037
+ return true
1038
+ }
984
1039
  const started = startFloorplanGroupMove(id, event, {
985
1040
  immediate,
986
1041
  onClickFallthrough: immediate ? undefined : () => startGroupPickUp(),
@@ -1299,6 +1354,16 @@ export const FloorplanRegistryLayer = memo(function FloorplanRegistryLayer() {
1299
1354
  runFloorplanWallPush(payload, event.clientX, event.clientY)
1300
1355
  return
1301
1356
  }
1357
+ // A wall end's pull grip opens the Walls run at that end; the plan's
1358
+ // wall draft then follows the pointer and the release places the wall.
1359
+ if (affordance === WALL_PULL_AFFORDANCE) {
1360
+ event.preventDefault()
1361
+ event.stopPropagation()
1362
+ suppressBoxSelectForPointer(event)
1363
+ const grip = payload as WallPullGripPayload
1364
+ startWallPull({ wallId: grip.wallId, endpoint: grip.endpoint, origin: '2d' })
1365
+ return
1366
+ }
1302
1367
 
1303
1368
  const sceneNodes = useScene.getState().nodes
1304
1369
  const node = sceneNodes[nodeId]
@@ -1590,6 +1655,7 @@ export const FloorplanRegistryLayer = memo(function FloorplanRegistryLayer() {
1590
1655
  <FloorplanRegistryEntry
1591
1656
  activeDragId={handleIdForNode(activeDragId, entry.id)}
1592
1657
  annotationVisibility={annotationVisibility}
1658
+ showDimensions={showDimensions}
1593
1659
  floorplanMode={floorplanMode}
1594
1660
  floorplanVisible={floorplanVisible}
1595
1661
  geometryCacheRef={geometryCacheRef}
@@ -1597,6 +1663,7 @@ export const FloorplanRegistryLayer = memo(function FloorplanRegistryLayer() {
1597
1663
  hoveredHandleId={handleIdForNode(hoveredHandleId, entry.id)}
1598
1664
  interactiveElevators={interactiveElevators}
1599
1665
  isMarqueeSelectionActive={isMarqueeSelectionActive}
1666
+ keepsSelectionDimensions={selectionDimensionIds.has(entry.id)}
1600
1667
  key={`${pass}-${entry.id}`}
1601
1668
  levelDataCacheRef={levelDataCacheRef}
1602
1669
  levelNodeIdsByType={floorplanData.levelNodeIdsByType}
@@ -1619,7 +1686,7 @@ export const FloorplanRegistryLayer = memo(function FloorplanRegistryLayer() {
1619
1686
  siblingEpoch={entry.dependsOnSiblingInputs ? (siblingEpochs.get(entry.id) ?? 0) : 0}
1620
1687
  unit={unit}
1621
1688
  metricNotation={metricNotation}
1622
- wallDimensionReference={effectiveWallDimensionReference}
1689
+ wallDimensionFaces={wallDimensionFaces}
1623
1690
  visibilityRootId={
1624
1691
  entry.visibilityRootId ?? (entry.ctxOverrides ? undefined : (levelId as AnyNodeId))
1625
1692
  }
@@ -1669,6 +1736,7 @@ export const FloorplanRegistryLayer = memo(function FloorplanRegistryLayer() {
1669
1736
  <g className="floorplan-registry-handles">{renderEntries('handles')}</g>
1670
1737
  <RoomHandles2D levelId={levelId} />
1671
1738
  <RoomHandleDragPreview2D levelId={levelId} />
1739
+ <SelectedWallDimensions2D levelId={levelId} />
1672
1740
  <FloorplanAnnotationLayoutResolver active={floorplanVisible} />
1673
1741
  {/* Click-to-type dimension labels. Portals its HTML input to
1674
1742
  document.body, so mounting it inside the SVG tree is safe. */}
@@ -1707,9 +1775,8 @@ function FloorplanAnnotationLayoutResolver({ active }: { active: boolean }) {
1707
1775
  const drawingType = useDrawingView((state) => state.drawingType)
1708
1776
  const annotationLayoutOverrides = useDrawingView((state) => state.annotationLayoutOverrides)
1709
1777
  const annotationVisibility = useFloorplanAnnotationVisibility((state) => state.visibility)
1710
- const wallDimensionReference = useFloorplanAnnotationVisibility(
1711
- (state) => state.wallDimensionReference,
1712
- )
1778
+ const showDimensions = useFloorplanAnnotationVisibility((state) => state.showDimensions)
1779
+ const wallDimensionFaces = useFloorplanAnnotationVisibility((state) => state.wallDimensionFaces)
1713
1780
  const floorplanMode = useFloorplanMode((state) => state.mode)
1714
1781
  const layoutInputs = useMemo(
1715
1782
  () => ({
@@ -1718,7 +1785,8 @@ function FloorplanAnnotationLayoutResolver({ active }: { active: boolean }) {
1718
1785
  drawingType,
1719
1786
  interactionIdle,
1720
1787
  settledLayoutEpoch,
1721
- wallDimensionReference,
1788
+ showDimensions,
1789
+ wallDimensionFaces,
1722
1790
  floorplanMode,
1723
1791
  }),
1724
1792
  [
@@ -1727,7 +1795,8 @@ function FloorplanAnnotationLayoutResolver({ active }: { active: boolean }) {
1727
1795
  drawingType,
1728
1796
  interactionIdle,
1729
1797
  settledLayoutEpoch,
1730
- wallDimensionReference,
1798
+ showDimensions,
1799
+ wallDimensionFaces,
1731
1800
  floorplanMode,
1732
1801
  ],
1733
1802
  )
@@ -1955,6 +2024,7 @@ function readFloorplanAnnotationLayoutOffset(label: SVGGElement) {
1955
2024
  type FloorplanRegistryEntryProps = {
1956
2025
  activeDragId: string | null
1957
2026
  annotationVisibility: FloorplanAnnotationVisibility
2027
+ showDimensions: boolean
1958
2028
  floorplanMode: FloorplanMode
1959
2029
  ctxOverrides: FloorplanContextOverrides | undefined
1960
2030
  floorplanVisible: boolean
@@ -1963,6 +2033,8 @@ type FloorplanRegistryEntryProps = {
1963
2033
  hoveredHandleId: string | null
1964
2034
  interactiveElevators: unknown
1965
2035
  isMarqueeSelectionActive: boolean
2036
+ /** A wall in a multi-selection still shows its own length (`useSelectionDimensionNodes`). */
2037
+ keepsSelectionDimensions: boolean
1966
2038
  levelDataCacheRef: { current: Map<string, LevelDataCacheEntry> }
1967
2039
  levelNodeIdsByType: ReadonlyMap<string, readonly AnyNodeId[]>
1968
2040
  moving: boolean
@@ -1996,13 +2068,14 @@ type FloorplanRegistryEntryProps = {
1996
2068
  siblingEpoch: number
1997
2069
  unit: 'metric' | 'imperial'
1998
2070
  metricNotation: 'meters' | 'millimeters'
1999
- wallDimensionReference: FloorplanWallDimensionReference
2071
+ wallDimensionFaces: FloorplanWallDimensionFaces
2000
2072
  visibilityRootId: AnyNodeId | undefined
2001
2073
  }
2002
2074
 
2003
2075
  const FloorplanRegistryEntry = memo(function FloorplanRegistryEntry({
2004
2076
  activeDragId,
2005
2077
  annotationVisibility,
2078
+ showDimensions,
2006
2079
  floorplanMode,
2007
2080
  ctxOverrides,
2008
2081
  floorplanVisible,
@@ -2011,6 +2084,7 @@ const FloorplanRegistryEntry = memo(function FloorplanRegistryEntry({
2011
2084
  hoveredHandleId,
2012
2085
  interactiveElevators,
2013
2086
  isMarqueeSelectionActive,
2087
+ keepsSelectionDimensions,
2014
2088
  levelDataCacheRef,
2015
2089
  levelNodeIdsByType,
2016
2090
  moving,
@@ -2032,7 +2106,7 @@ const FloorplanRegistryEntry = memo(function FloorplanRegistryEntry({
2032
2106
  siblingEpoch,
2033
2107
  unit,
2034
2108
  metricNotation,
2035
- wallDimensionReference,
2109
+ wallDimensionFaces,
2036
2110
  visibilityRootId,
2037
2111
  }: FloorplanRegistryEntryProps): React.ReactElement | null {
2038
2112
  // A node an agent just built is drawn on its construction-reveal turn, as in 3D.
@@ -2049,7 +2123,12 @@ const FloorplanRegistryEntry = memo(function FloorplanRegistryEntry({
2049
2123
  const ceilingWithoutSession = useCeilingEditSession(
2050
2124
  (state) => node.type === 'ceiling' && state.session?.ceilingId !== nodeId,
2051
2125
  )
2052
- const suppressHandles = multiSelected || ceilingWithoutSession
2126
+ const mode = useEditor((state) => state.mode)
2127
+ const suppressHandles = floorplanEntryStripsHandles({
2128
+ mode,
2129
+ multiSelected,
2130
+ ceilingWithoutSession,
2131
+ })
2053
2132
  const selectedLevelId = useViewer((state) => state.selection.levelId)
2054
2133
  const selectionProxyId = resolveSelectionProxyId(
2055
2134
  node,
@@ -2089,6 +2168,7 @@ const FloorplanRegistryEntry = memo(function FloorplanRegistryEntry({
2089
2168
  referencedAnnotationRole,
2090
2169
  selected,
2091
2170
  target: 'editor',
2171
+ dimensions: showDimensions,
2092
2172
  },
2093
2173
  )
2094
2174
 
@@ -2220,7 +2300,7 @@ const FloorplanRegistryEntry = memo(function FloorplanRegistryEntry({
2220
2300
  siblingEpoch,
2221
2301
  unit,
2222
2302
  metricNotation,
2223
- wallDimensionReference,
2303
+ wallDimensionFaces,
2224
2304
  visibilityRootId,
2225
2305
  })
2226
2306
  const rawGeometry = cacheEntry ? cacheEntry[pass] : null
@@ -2229,10 +2309,10 @@ const FloorplanRegistryEntry = memo(function FloorplanRegistryEntry({
2229
2309
  : null
2230
2310
  // Multi-selection shows highlight only: strip this member's edit handles /
2231
2311
  // dimension chrome (all of which live in the overlay pass) while keeping
2232
- // its highlighted body geometry.
2312
+ // its highlighted body geometry and, for a wall, its own length.
2233
2313
  const geometry =
2234
2314
  visibleGeometry && suppressHandles && pass !== 'base'
2235
- ? stripHandleChrome(visibleGeometry)
2315
+ ? stripHandleChrome(visibleGeometry, keepsSelectionDimensions)
2236
2316
  : visibleGeometry
2237
2317
  useFloorplanReveal(nodeId, entryRef, cacheEntry, revealWaiting, revealActive)
2238
2318
  if (revealWaiting || !geometry) return null
@@ -2293,7 +2373,7 @@ type BuildFloorplanEntryGeometryArgs = {
2293
2373
  siblingEpoch: number
2294
2374
  unit: 'metric' | 'imperial'
2295
2375
  metricNotation: 'meters' | 'millimeters'
2296
- wallDimensionReference: FloorplanWallDimensionReference
2376
+ wallDimensionFaces?: FloorplanWallDimensionFaces
2297
2377
  visibilityRootId: AnyNodeId | undefined
2298
2378
  }
2299
2379
 
@@ -2353,7 +2433,7 @@ export function buildFloorplanEntryGeometry({
2353
2433
  siblingEpoch,
2354
2434
  unit,
2355
2435
  metricNotation,
2356
- wallDimensionReference,
2436
+ wallDimensionFaces,
2357
2437
  visibilityRootId,
2358
2438
  }: BuildFloorplanEntryGeometryArgs): CacheEntry | null {
2359
2439
  const def = nodeRegistry.get(node.type)
@@ -2381,7 +2461,7 @@ export function buildFloorplanEntryGeometry({
2381
2461
  live,
2382
2462
  unit,
2383
2463
  metricNotation,
2384
- wallDimensionReference,
2464
+ wallDimensionFaces,
2385
2465
  selected,
2386
2466
  highlighted,
2387
2467
  hovered,
@@ -2468,7 +2548,7 @@ export function buildFloorplanEntryGeometry({
2468
2548
  unit,
2469
2549
  metricNotation,
2470
2550
  roomLabelOverlay: true,
2471
- wallDimensionReference,
2551
+ wallDimensionFaces,
2472
2552
  highlighted,
2473
2553
  hovered,
2474
2554
  moving,
@@ -2494,7 +2574,7 @@ export function buildFloorplanEntryGeometry({
2494
2574
  metricNotation,
2495
2575
  purpose: 'edit',
2496
2576
  roomLabelOverlay: true,
2497
- wallDimensionReference,
2577
+ wallDimensionFaces,
2498
2578
  }),
2499
2579
  viewState: palette
2500
2580
  ? {
@@ -2642,6 +2722,87 @@ export const InteractiveGeometry = memo(function InteractiveGeometry({
2642
2722
 
2643
2723
  return renderInteractive(geometry, 0)
2644
2724
 
2725
+ /**
2726
+ * A wall end's "pull to continue" grip: screen-sized, just past the end
2727
+ * along the wall, emerald so it reads apart from the end's move grip.
2728
+ */
2729
+ function renderPullGrip(
2730
+ g: Extract<FloorplanGeometry, { kind: 'move-arrow' }>,
2731
+ keyHint: number,
2732
+ ): React.ReactElement {
2733
+ const handleId = `${makeHandleId(nodeId, g.payload)}:pull`
2734
+ const isHovered = hoveredHandleId === handleId
2735
+ const k = handleUnitsPerPixel
2736
+ const c = PULL_GRIP_CENTER_PX * k
2737
+ const r = PULL_GRIP_RADIUS_PX * (isHovered ? 1.12 : 1) * k
2738
+ // Outward arrow inside the button: shaft then head, tip along +x.
2739
+ const arrowD = [
2740
+ `M ${c - 5.5 * k},${-1.4 * k}`,
2741
+ `L ${c + 0.5 * k},${-1.4 * k}`,
2742
+ `L ${c + 0.5 * k},${-4.6 * k}`,
2743
+ `L ${c + 6 * k},0`,
2744
+ `L ${c + 0.5 * k},${4.6 * k}`,
2745
+ `L ${c + 0.5 * k},${1.4 * k}`,
2746
+ `L ${c - 5.5 * k},${1.4 * k}`,
2747
+ 'Z',
2748
+ ].join(' ')
2749
+ const affordance = g.affordance
2750
+ const payload = g.payload
2751
+ return (
2752
+ <g
2753
+ data-wall-pull-grip={
2754
+ typeof payload === 'object' && payload && 'endpoint' in payload
2755
+ ? String((payload as { endpoint: unknown }).endpoint)
2756
+ : ''
2757
+ }
2758
+ key={keyHint}
2759
+ onClick={(e) => e.stopPropagation()}
2760
+ transform={`translate(${g.point[0]} ${g.point[1]}) rotate(${(g.angle * 180) / Math.PI})`}
2761
+ >
2762
+ <line
2763
+ pointerEvents="none"
2764
+ stroke={PULL_GRIP_FILL}
2765
+ strokeDasharray={`${3 * k} ${2.5 * k}`}
2766
+ strokeLinecap="round"
2767
+ strokeWidth={1.5 * k}
2768
+ x1={PULL_GRIP_LEADER_START_PX * k}
2769
+ x2={c - r}
2770
+ y1={0}
2771
+ y2={0}
2772
+ />
2773
+ <circle
2774
+ cx={c}
2775
+ cy={0}
2776
+ fill={isHovered ? PULL_GRIP_HOVER_FILL : PULL_GRIP_FILL}
2777
+ pointerEvents="none"
2778
+ r={r}
2779
+ stroke="#ffffff"
2780
+ strokeWidth={1.5 * k}
2781
+ />
2782
+ <path d={arrowD} fill="#ffffff" pointerEvents="none" />
2783
+ <circle
2784
+ cx={c}
2785
+ cy={0}
2786
+ fill="transparent"
2787
+ onPointerDown={(e) => {
2788
+ if (affordance) {
2789
+ onHandlePointerDown(affordance, payload, e as ReactPointerEvent<SVGGElement>)
2790
+ }
2791
+ }}
2792
+ onPointerEnter={() => onHandleHoverChange(handleId)}
2793
+ onPointerLeave={() => onHandleHoverChange(null)}
2794
+ // Fill only: the default stroke is a plan metre wide, and would take
2795
+ // the press meant for the end's move grip.
2796
+ pointerEvents="fill"
2797
+ r={PULL_GRIP_HIT_RADIUS_PX * k}
2798
+ style={{ cursor: 'grab' }}
2799
+ >
2800
+ {g.title ? <title>{g.title}</title> : null}
2801
+ </circle>
2802
+ </g>
2803
+ )
2804
+ }
2805
+
2645
2806
  function renderInteractive(g: FloorplanGeometry, keyHint: number): React.ReactElement {
2646
2807
  switch (g.kind) {
2647
2808
  case 'group': {
@@ -2914,7 +3075,8 @@ export const InteractiveGeometry = memo(function InteractiveGeometry({
2914
3075
  const head1 = `M ${t1x} ${t1y} L ${b1ax} ${b1ay} L ${b1bx} ${b1by} Z`
2915
3076
  const head2 = `M ${t2x} ${t2y} L ${b2ax} ${b2ay} L ${b2bx} ${b2by} Z`
2916
3077
  const fill = isHovered ? '#a5b4fc' : '#8381ed'
2917
- const strokeWidthPx = isHovered ? 2.4 : 1.8
3078
+ const scale = g.scale ?? 1
3079
+ const strokeWidthPx = (isHovered ? 2.4 : 1.8) * Math.sqrt(scale)
2918
3080
  const angleDeg = (g.angle * 180) / Math.PI
2919
3081
  const affordance = g.affordance
2920
3082
  const payload = g.payload
@@ -2923,7 +3085,7 @@ export const InteractiveGeometry = memo(function InteractiveGeometry({
2923
3085
  <g
2924
3086
  key={keyHint}
2925
3087
  onClick={(e) => e.stopPropagation()}
2926
- transform={`translate(${g.point[0]} ${g.point[1]}) rotate(${angleDeg})`}
3088
+ transform={`translate(${g.point[0]} ${g.point[1]}) rotate(${angleDeg}) scale(${scale})`}
2927
3089
  >
2928
3090
  <path
2929
3091
  d={arcPath}
@@ -2978,6 +3140,7 @@ export const InteractiveGeometry = memo(function InteractiveGeometry({
2978
3140
  }
2979
3141
  case 'move-arrow': {
2980
3142
  if (!palette) return <></>
3143
+ if (g.variant === 'pull') return renderPullGrip(g, keyHint)
2981
3144
  // Affordance-routed arrows (door width-resize) get a per-payload
2982
3145
  // handle id so each side can hover independently; default
2983
3146
  // (move-flow) arrows share the node's :move id like the dot.
@@ -2987,13 +3150,14 @@ export const InteractiveGeometry = memo(function InteractiveGeometry({
2987
3150
  // so it shrinks on zoom-out and grows on zoom-in, matching the
2988
3151
  // wall it accompanies. Composed of a rectangular shaft + triangular
2989
3152
  // head, drawn as a single path for a clean fill + stroke outline.
2990
- const sl = 0.1 // shaft length (shortened body)
2991
- const hl = 0.12 // head length
2992
- const sh = 0.04 // shaft half-height
2993
- const hh = 0.1 // head half-height
3153
+ const k = g.scale ?? 1
3154
+ const sl = 0.1 * k // shaft length (shortened body)
3155
+ const hl = 0.12 * k // head length
3156
+ const sh = 0.04 * k // shaft half-height
3157
+ const hh = 0.1 * k // head half-height
2994
3158
  // Inset the shaft start so the arrow sits a little off the wall
2995
3159
  // body (matches the 3D `HANDLE_OFFSET`).
2996
- const bi = 0.03 // base inset
3160
+ const bi = 0.03 * k // base inset
2997
3161
  const arrowD = `M ${bi},${-sh} L ${bi + sl},${-sh} L ${bi + sl},${-hh} L ${bi + sl + hl},0 L ${bi + sl},${hh} L ${bi + sl},${sh} L ${bi},${sh} Z`
2998
3162
  // Indigo palette to match the 3D wall push arrows
2999
3163
  // (`ARROW_COLOR` / `ARROW_HOVER_COLOR`) and the corner-sphere
@@ -3642,11 +3806,19 @@ const HANDLE_CHROME_KINDS = new Set<FloorplanGeometry['kind']>([
3642
3806
  'equal-spacing-badge',
3643
3807
  ])
3644
3808
 
3645
- function stripHandleChrome(g: FloorplanGeometry): FloorplanGeometry | null {
3809
+ function stripHandleChrome(
3810
+ g: FloorplanGeometry,
3811
+ keepContextualDimensions = false,
3812
+ ): FloorplanGeometry | null {
3813
+ if (
3814
+ keepContextualDimensions &&
3815
+ readFloorplanGeometryMetadata(g).annotationRole === 'contextual-dimension'
3816
+ )
3817
+ return g
3646
3818
  if (HANDLE_CHROME_KINDS.has(g.kind)) return null
3647
3819
  if (g.kind === 'group') {
3648
3820
  const children = g.children
3649
- .map(stripHandleChrome)
3821
+ .map((child) => stripHandleChrome(child, keepContextualDimensions))
3650
3822
  .filter((c): c is FloorplanGeometry => c !== null)
3651
3823
  if (children.length === 0) return null
3652
3824
  return { kind: 'group', children, transform: g.transform }
@@ -3763,7 +3935,7 @@ function nodeDepsEqual(a: NodeDeps, b: NodeDeps): boolean {
3763
3935
  'live',
3764
3936
  'unit',
3765
3937
  'metricNotation',
3766
- 'wallDimensionReference',
3938
+ 'wallDimensionFaces',
3767
3939
  'selected',
3768
3940
  'highlighted',
3769
3941
  'hovered',