@pascal-app/editor 1.0.6 → 1.0.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (317) hide show
  1. package/package.json +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 +20 -8
  6. package/src/components/editor/elevation-3d-guide-layer.tsx +1 -1
  7. package/src/components/editor/first-person-controls.tsx +72 -61
  8. package/src/components/editor/floating-action-menu.tsx +27 -9
  9. package/src/components/editor/floating-building-action-menu.tsx +1 -1
  10. package/src/components/editor/floorplan-panel.test.ts +220 -0
  11. package/src/components/editor/floorplan-panel.tsx +285 -132
  12. package/src/components/editor/footprint-height-handle.tsx +1 -1
  13. package/src/components/editor/grid.tsx +24 -15
  14. package/src/components/editor/group-actions.ts +19 -1
  15. package/src/components/editor/group-floating-action-menu.tsx +1 -1
  16. package/src/components/editor/group-move-3d.ts +27 -2
  17. package/src/components/editor/group-selection-box-3d.tsx +4 -1
  18. package/src/components/editor/group-transform-shared.ts +20 -0
  19. package/src/components/editor/index.tsx +130 -20
  20. package/src/components/editor/inspector-extension-actions.tsx +36 -0
  21. package/src/components/editor/localized-html.tsx +22 -0
  22. package/src/components/editor/node-action-menu.tsx +46 -12
  23. package/src/components/editor/node-arrow-handles.tsx +1 -2
  24. package/src/components/editor/open-wall-ends-3d-layer.tsx +1 -1
  25. package/src/components/editor/opening-guides-3d-layer.tsx +1 -1
  26. package/src/components/editor/registry-action-contributions.test.tsx +67 -6
  27. package/src/components/editor/registry-action-contributions.tsx +115 -15
  28. package/src/components/editor/riser-diagram-panel.tsx +59 -9
  29. package/src/components/editor/room-controls.tsx +1 -1
  30. package/src/components/editor/room-handles-2d.tsx +156 -6
  31. package/src/components/editor/room-handles.tsx +241 -51
  32. package/src/components/editor/room-highlight.tsx +2 -0
  33. package/src/components/editor/room-labels.tsx +29 -15
  34. package/src/components/editor/selection-manager.tsx +15 -102
  35. package/src/components/editor/separator-end-grip.tsx +74 -0
  36. package/src/components/editor/site-edge-labels.tsx +1 -1
  37. package/src/components/editor/thumbnail-generator.test.ts +15 -6
  38. package/src/components/editor/thumbnail-generator.tsx +26 -1
  39. package/src/components/editor/use-floorplan-background-placement.ts +55 -52
  40. package/src/components/editor/views/view-bar.tsx +111 -26
  41. package/src/components/editor/wall-measurement-label.tsx +1 -1
  42. package/src/components/editor/wall-move-side-handles.tsx +5 -1
  43. package/src/components/editor/wall-pull-grip.tsx +193 -0
  44. package/src/components/editor/wall-snap-beacon-layer.tsx +81 -1
  45. package/src/components/editor-2d/floorplan-adjacent-walls-layer.test.ts +76 -0
  46. package/src/components/editor-2d/floorplan-adjacent-walls-layer.tsx +186 -0
  47. package/src/components/editor-2d/floorplan-alignment-guide-layer.tsx +63 -3
  48. package/src/components/editor-2d/floorplan-group-move.tsx +31 -3
  49. package/src/components/editor-2d/floorplan-measurement-tool-layer.test.ts +6 -0
  50. package/src/components/editor-2d/floorplan-measurement-tool-layer.tsx +22 -13
  51. package/src/components/editor-2d/floorplan-open-wall-ends-layer.tsx +11 -3
  52. package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +7 -1
  53. package/src/components/editor-2d/floorplan-snap-beacon-layer.tsx +96 -4
  54. package/src/components/editor-2d/renderers/floorplan-dimension-edit-overlay.tsx +52 -12
  55. package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +12 -1
  56. package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +197 -37
  57. package/src/components/procedural-items/inspector.tsx +59 -12
  58. package/src/components/systems/ceiling/ceiling-bracket-batch.test.ts +2 -0
  59. package/src/components/systems/ceiling/ceiling-system.test.ts +141 -0
  60. package/src/components/systems/ceiling/ceiling-system.tsx +53 -18
  61. package/src/components/tools/distribution/distribution-run-tool.tsx +50 -26
  62. package/src/components/tools/item/placement-history.test.tsx +1 -1
  63. package/src/components/tools/item/placement-strategies.test.ts +64 -1
  64. package/src/components/tools/item/placement-strategies.ts +5 -9
  65. package/src/components/tools/item/use-placement-coordinator.tsx +30 -34
  66. package/src/components/tools/registry/item-surface-move.test.ts +11 -0
  67. package/src/components/tools/select/select-candidates.test.ts +45 -0
  68. package/src/components/tools/select/select-candidates.ts +13 -2
  69. package/src/components/tools/shared/cursor-sphere.tsx +10 -3
  70. package/src/components/tools/shared/draft-measurement-label.tsx +1 -1
  71. package/src/components/tools/shared/placement-box.tsx +1 -1
  72. package/src/components/tools/shared/placement-dimension-guides.tsx +1 -1
  73. package/src/components/tools/site/terrain-brush-cursor.tsx +1 -1
  74. package/src/components/tools/wall/wall-drafting.test.ts +299 -23
  75. package/src/components/tools/wall/wall-drafting.ts +132 -24
  76. package/src/components/tools/wall/wall-pull-snap.test.ts +274 -0
  77. package/src/components/tools/wall/wall-pull-snap.ts +563 -0
  78. package/src/components/tools/wall/wall-snap-geometry.test.ts +113 -1
  79. package/src/components/tools/wall/wall-snap-geometry.ts +137 -0
  80. package/src/components/tools/zone/zone-tool.tsx +3 -3
  81. package/src/components/ui/action-menu/action-button.tsx +2 -0
  82. package/src/components/ui/action-menu/camera-actions.tsx +8 -7
  83. package/src/components/ui/action-menu/control-modes.tsx +9 -5
  84. package/src/components/ui/action-menu/follow-pascal-pill.tsx +3 -1
  85. package/src/components/ui/action-menu/follow-pascal-toggle.tsx +7 -1
  86. package/src/components/ui/action-menu/furnish-tools.tsx +7 -6
  87. package/src/components/ui/action-menu/index.tsx +1 -0
  88. package/src/components/ui/action-menu/measurement-control.tsx +35 -29
  89. package/src/components/ui/action-menu/point-ask-button.tsx +4 -3
  90. package/src/components/ui/action-menu/structure-tools.tsx +25 -23
  91. package/src/components/ui/action-menu/view-toggles.test.ts +18 -0
  92. package/src/components/ui/action-menu/view-toggles.tsx +256 -51
  93. package/src/components/ui/build-panel/build-panel.tsx +163 -25
  94. package/src/components/ui/command-palette/editor-commands.tsx +38 -11
  95. package/src/components/ui/controls/finish-swatch.tsx +45 -19
  96. package/src/components/ui/controls/material-picker.tsx +24 -17
  97. package/src/components/ui/controls/metric-control.test.ts +52 -0
  98. package/src/components/ui/controls/metric-control.tsx +62 -24
  99. package/src/components/ui/controls/segmented-control.tsx +5 -2
  100. package/src/components/ui/controls/slider-control.test.tsx +23 -0
  101. package/src/components/ui/controls/slider-control.tsx +30 -14
  102. package/src/components/ui/floating-level-selector.tsx +100 -48
  103. package/src/components/ui/helpers/building-helper.tsx +5 -3
  104. package/src/components/ui/helpers/contextual-helper-panel.tsx +97 -37
  105. package/src/components/ui/helpers/helper-manager.test.tsx +53 -3
  106. package/src/components/ui/helpers/helper-manager.tsx +82 -57
  107. package/src/components/ui/helpers/item-helper.tsx +6 -4
  108. package/src/components/ui/helpers/registered-tool-helper.tsx +3 -1
  109. package/src/components/ui/level-duplicate-dialog.tsx +32 -23
  110. package/src/components/ui/level-wall-copy-prompt.tsx +124 -0
  111. package/src/components/ui/panels/collections/collections-popover.tsx +11 -11
  112. package/src/components/ui/panels/deck-boards-fields.tsx +136 -0
  113. package/src/components/ui/panels/floor-foundation-panel.tsx +113 -38
  114. package/src/components/ui/panels/floor-opening-panel.tsx +73 -25
  115. package/src/components/ui/panels/inspector-extensions.tsx +66 -0
  116. package/src/components/ui/panels/mobile-panel-sheet.tsx +3 -1
  117. package/src/components/ui/panels/mobile-selection-bar.tsx +8 -6
  118. package/src/components/ui/panels/multi-field-value.test.ts +27 -0
  119. package/src/components/ui/panels/multi-field-value.ts +20 -1
  120. package/src/components/ui/panels/multi-height-mode.tsx +8 -7
  121. package/src/components/ui/panels/multi-parametric-inspector.tsx +122 -34
  122. package/src/components/ui/panels/multi-selection-panel.tsx +75 -35
  123. package/src/components/ui/panels/node-display.ts +35 -24
  124. package/src/components/ui/panels/panel-manager.tsx +5 -3
  125. package/src/components/ui/panels/panel-wrapper.tsx +22 -109
  126. package/src/components/ui/panels/parametric-field-control.tsx +13 -10
  127. package/src/components/ui/panels/parametric-inspector.tsx +18 -12
  128. package/src/components/ui/panels/reference-panel.tsx +51 -35
  129. package/src/components/ui/panels/room-construction-rows.tsx +464 -65
  130. package/src/components/ui/panels/room-name-options.test.ts +36 -3
  131. package/src/components/ui/panels/room-name-options.ts +110 -11
  132. package/src/components/ui/panels/room-openings-field.tsx +21 -12
  133. package/src/components/ui/panels/room-panel.tsx +206 -99
  134. package/src/components/ui/panels/shape-choice.tsx +7 -5
  135. package/src/components/ui/panels/wall-region-list.tsx +7 -7
  136. package/src/components/ui/primitives/shortcut-token.tsx +10 -6
  137. package/src/components/ui/sidebar/icon-rail.tsx +11 -9
  138. package/src/components/ui/sidebar/panels/items-panel/function-tree-panel.tsx +10 -10
  139. package/src/components/ui/sidebar/panels/items-panel/index.tsx +10 -9
  140. package/src/components/ui/sidebar/panels/settings-panel/index.tsx +3 -0
  141. package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +102 -32
  142. package/src/components/ui/sidebar/panels/site-panel/building-tree-node.tsx +4 -3
  143. package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +4 -2
  144. package/src/components/ui/sidebar/panels/site-panel/chimney-tree-node.tsx +4 -2
  145. package/src/components/ui/sidebar/panels/site-panel/collection-tree-node.tsx +7 -5
  146. package/src/components/ui/sidebar/panels/site-panel/column-tree-node.tsx +4 -2
  147. package/src/components/ui/sidebar/panels/site-panel/default-names.ts +75 -0
  148. package/src/components/ui/sidebar/panels/site-panel/door-tree-node.tsx +4 -2
  149. package/src/components/ui/sidebar/panels/site-panel/dormer-tree-node.tsx +4 -2
  150. package/src/components/ui/sidebar/panels/site-panel/elevator-tree-node.tsx +4 -2
  151. package/src/components/ui/sidebar/panels/site-panel/fence-tree-node.tsx +4 -2
  152. package/src/components/ui/sidebar/panels/site-panel/gutter-tree-node.tsx +4 -2
  153. package/src/components/ui/sidebar/panels/site-panel/index.tsx +117 -64
  154. package/src/components/ui/sidebar/panels/site-panel/inline-rename-input.tsx +36 -3
  155. package/src/components/ui/sidebar/panels/site-panel/item-tree-node.tsx +4 -2
  156. package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +7 -4
  157. package/src/components/ui/sidebar/panels/site-panel/registry-tree-node.tsx +8 -2
  158. package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +8 -5
  159. package/src/components/ui/sidebar/panels/site-panel/shelf-tree-node.tsx +4 -2
  160. package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +8 -3
  161. package/src/components/ui/sidebar/panels/site-panel/solar-panel-tree-node.tsx +4 -2
  162. package/src/components/ui/sidebar/panels/site-panel/spawn-tree-node.tsx +4 -2
  163. package/src/components/ui/sidebar/panels/site-panel/stair-tree-node.tsx +8 -7
  164. package/src/components/ui/sidebar/panels/site-panel/tree-node-actions.tsx +7 -6
  165. package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +16 -1
  166. package/src/components/ui/sidebar/panels/site-panel/unit-tree-node.tsx +15 -8
  167. package/src/components/ui/sidebar/panels/site-panel/wall-tree-node.tsx +4 -2
  168. package/src/components/ui/sidebar/panels/site-panel/window-tree-node.tsx +4 -2
  169. package/src/components/ui/sidebar/panels/site-panel/zone-membership-checkbox.tsx +3 -1
  170. package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +18 -8
  171. package/src/components/ui/sidebar/panels/zone-panel/index.tsx +25 -14
  172. package/src/components/ui/sidebar/tab-bar.tsx +4 -1
  173. package/src/components/ui/sidebar/use-plugin-panels.tsx +19 -10
  174. package/src/components/ui/snap-target-badge.tsx +7 -6
  175. package/src/components/ui/wall-type/wall-type-editor-dialog.tsx +721 -0
  176. package/src/components/ui/wall-type/wall-type-menu.tsx +167 -0
  177. package/src/components/viewer/floorplan-compass-button.tsx +12 -3
  178. package/src/components/viewer/floorplan-preview.tsx +27 -19
  179. package/src/components/viewer/render-settings-menu.tsx +294 -0
  180. package/src/components/viewer/sky-control.tsx +710 -0
  181. package/src/components/viewer/use-viewer-ui-sounds.test.ts +15 -0
  182. package/src/components/viewer/use-viewer-ui-sounds.ts +46 -0
  183. package/src/components/viewer/viewer-controls-bar.tsx +278 -260
  184. package/src/components/viewer/viewer-hover-label.tsx +12 -4
  185. package/src/components/viewer/viewer-rooms.tsx +61 -4
  186. package/src/components/viewer/viewer-scene-header.tsx +28 -17
  187. package/src/components/viewer/viewer-stage-switcher.tsx +4 -2
  188. package/src/components/viewer-overlay.tsx +8 -0
  189. package/src/components/walkthrough-hud.tsx +75 -2
  190. package/src/hooks/use-keyboard.test.ts +85 -4
  191. package/src/hooks/use-keyboard.ts +65 -7
  192. package/src/hooks/use-selected-room.ts +12 -1
  193. package/src/i18n/floor-footprint-name.ts +30 -0
  194. package/src/i18n/level-name.ts +39 -0
  195. package/src/i18n/locales/en/messages.po +15117 -0
  196. package/src/i18n/locales/en/messages.ts +1 -0
  197. package/src/i18n/locales/en-XA/messages.ts +1 -0
  198. package/src/i18n/locales/es/messages.po +37501 -0
  199. package/src/i18n/locales/es/messages.ts +1 -0
  200. package/src/i18n/locales/fr/messages.po +37501 -0
  201. package/src/i18n/locales/fr/messages.ts +1 -0
  202. package/src/i18n/locales/ja/messages.po +37501 -0
  203. package/src/i18n/locales/ja/messages.ts +1 -0
  204. package/src/i18n/locales/zh-CN/messages.po +37501 -0
  205. package/src/i18n/locales/zh-CN/messages.ts +1 -0
  206. package/src/i18n/locales.test.ts +63 -0
  207. package/src/i18n/locales.ts +32 -0
  208. package/src/i18n/message-slot.tsx +25 -0
  209. package/src/i18n/node-messages.ts +169 -0
  210. package/src/i18n/pseudo-locale.ts +17 -0
  211. package/src/i18n/published-entry.test.ts +17 -0
  212. package/src/i18n/scene-theme-name.ts +16 -0
  213. package/src/index.tsx +156 -4
  214. package/src/lib/add-storey.test.ts +3 -3
  215. package/src/lib/contextual-help.test.ts +20 -1
  216. package/src/lib/contextual-help.ts +95 -19
  217. package/src/lib/continuation.ts +38 -11
  218. package/src/lib/floor-footprints.test.ts +2 -1
  219. package/src/lib/floor-foundation-support.test.ts +7 -3
  220. package/src/lib/floorplan/dimension-drive.test.ts +36 -2
  221. package/src/lib/floorplan/dimension-drive.ts +68 -3
  222. package/src/lib/floorplan/floorplan-export.test.ts +16 -5
  223. package/src/lib/floorplan/floorplan-export.tsx +13 -16
  224. package/src/lib/floorplan/floorplan-extension.test.ts +11 -12
  225. package/src/lib/floorplan/floorplan-extension.ts +37 -18
  226. package/src/lib/floorplan/floorplan-mode.test.ts +13 -6
  227. package/src/lib/floorplan/floorplan-mode.ts +14 -12
  228. package/src/lib/floorplan/floorplan-plugin-install.test.tsx +1 -1
  229. package/src/lib/floorplan/floorplan-readonly.ts +3 -3
  230. package/src/lib/floorplan/schedules.ts +10 -6
  231. package/src/lib/floorplan/site-plan/site-annotations.test.ts +46 -1
  232. package/src/lib/floorplan/site-plan/site-annotations.ts +3 -0
  233. package/src/lib/floorplan/site-plan/site-parts.ts +6 -3
  234. package/src/lib/geometry-script/author.test.ts +114 -0
  235. package/src/lib/geometry-script/author.ts +55 -23
  236. package/src/lib/geometry-script/client.ts +3 -0
  237. package/src/lib/geometry-script/finish-scripts.ts +7 -7
  238. package/src/lib/glb-export.test.ts +327 -0
  239. package/src/lib/glb-export.ts +70 -2
  240. package/src/lib/grid-frame.test.ts +56 -3
  241. package/src/lib/grid-frame.ts +28 -1
  242. package/src/lib/history.test.ts +2 -1
  243. package/src/lib/hud-title.ts +48 -24
  244. package/src/lib/ifc-export.test.ts +41 -1
  245. package/src/lib/inspector-card-mode.test.ts +3 -31
  246. package/src/lib/inspector-card-mode.ts +5 -10
  247. package/src/lib/inspector-expanded.test.ts +13 -0
  248. package/src/lib/inspector-expanded.ts +13 -1
  249. package/src/lib/keyboard-pan.test.ts +7 -0
  250. package/src/lib/keyboard-pan.ts +9 -2
  251. package/src/lib/layer-thickness.test.ts +22 -0
  252. package/src/lib/layer-thickness.ts +79 -0
  253. package/src/lib/lot/drop-in.test.ts +59 -1
  254. package/src/lib/lot/drop-in.ts +42 -50
  255. package/src/lib/material-swatch-style.ts +24 -0
  256. package/src/lib/measurement-parser.ts +7 -3
  257. package/src/lib/measurements.test.ts +17 -0
  258. package/src/lib/measurements.ts +20 -5
  259. package/src/lib/mezzanine-selection.test.ts +3 -1
  260. package/src/lib/orbit-near-plane.test.ts +12 -0
  261. package/src/lib/orbit-near-plane.ts +13 -0
  262. package/src/lib/paint-regions.test.ts +8 -5
  263. package/src/lib/paint-scope.test.ts +53 -0
  264. package/src/lib/paint-scope.ts +18 -4
  265. package/src/lib/panel-rows.ts +17 -8
  266. package/src/lib/reference-scale.ts +83 -0
  267. package/src/lib/room-built-on.test.ts +26 -12
  268. package/src/lib/room-built-on.ts +20 -2
  269. package/src/lib/room-construction.test.ts +28 -1
  270. package/src/lib/room-construction.ts +14 -0
  271. package/src/lib/room-controls-phase6.test.ts +140 -0
  272. package/src/lib/room-handle-drag.ts +557 -42
  273. package/src/lib/room-name-catalog.test.ts +8 -0
  274. package/src/lib/room-name-catalog.ts +8 -1
  275. package/src/lib/room-name-messages.test.ts +132 -0
  276. package/src/lib/room-name-messages.ts +147 -0
  277. package/src/lib/room-name-storage.test.ts +334 -0
  278. package/src/lib/room-push-dimensions.test.ts +37 -0
  279. package/src/lib/room-push-dimensions.ts +226 -2
  280. package/src/lib/room-selection.test.ts +23 -6
  281. package/src/lib/scene-clipboard.test.ts +70 -49
  282. package/src/lib/scene-clipboard.ts +9 -4
  283. package/src/lib/selection-cycle.ts +63 -0
  284. package/src/lib/separator-edit.test.ts +431 -0
  285. package/src/lib/separator-edit.ts +179 -0
  286. package/src/lib/separator-lines.ts +38 -0
  287. package/src/lib/sfx-bus.ts +27 -1
  288. package/src/lib/sfx-player.ts +16 -0
  289. package/src/lib/tool-cursor-icon.ts +5 -1
  290. package/src/lib/tool-shortcuts.ts +130 -0
  291. package/src/lib/viewer-selection.test.ts +28 -24
  292. package/src/lib/viewer-selection.ts +20 -5
  293. package/src/lib/wall-draw-variant.test.ts +7 -6
  294. package/src/lib/wall-draw-variant.ts +107 -6
  295. package/src/lib/wall-nudge.test.ts +44 -0
  296. package/src/lib/wall-nudge.ts +131 -0
  297. package/src/lib/wall-parallel-snap.test.ts +83 -0
  298. package/src/lib/wall-parallel-snap.ts +176 -0
  299. package/src/lib/wall-placement-pointer.test.ts +23 -0
  300. package/src/lib/wall-placement-pointer.ts +18 -0
  301. package/src/lib/wall-pull.test.ts +245 -0
  302. package/src/lib/wall-pull.ts +209 -0
  303. package/src/lib/wall-push-line-up.test.ts +132 -0
  304. package/src/lib/wall-region-list.ts +14 -5
  305. package/src/lib/wall-run-snap.test.ts +188 -0
  306. package/src/lib/wall-run-snap.ts +83 -0
  307. package/src/lib/wall-type.test.ts +125 -0
  308. package/src/lib/wall-type.ts +119 -0
  309. package/src/lib/window-interaction.test.ts +42 -0
  310. package/src/lib/window-interaction.ts +47 -5
  311. package/src/lib/world-grid-snap.test.ts +139 -2
  312. package/src/lib/world-grid-snap.ts +102 -105
  313. package/src/store/use-alignment-guides.ts +27 -1
  314. package/src/store/use-editor.tsx +48 -0
  315. package/src/store/use-floorplan-annotation-visibility.test.ts +64 -0
  316. package/src/store/use-floorplan-annotation-visibility.ts +47 -22
  317. package/src/store/use-wall-snap-indicator.ts +15 -2
@@ -78,7 +78,7 @@ import { resolveNodeForDrawingType } from '../../../lib/floorplan/drawing-coordi
78
78
  import {
79
79
  createFloorplanContextExtensions,
80
80
  type FloorplanAnnotationRole,
81
- type FloorplanWallDimensionReference,
81
+ type FloorplanWallDimensionFaces,
82
82
  getFloorplanNodeExtension,
83
83
  readFloorplanGeometryMetadata,
84
84
  withFloorplanGeometryMetadata,
@@ -86,7 +86,6 @@ import {
86
86
  import {
87
87
  type FloorplanMode,
88
88
  resolveFloorplanAnnotationVisibility,
89
- resolveFloorplanWallDimensionReference,
90
89
  } from '../../../lib/floorplan/floorplan-mode'
91
90
  import {
92
91
  buildFloorplanContext,
@@ -115,6 +114,11 @@ import { createSessionWrites, type SessionWrites } from '../../../lib/session-wr
115
114
  import { sfxEmitter } from '../../../lib/sfx-bus'
116
115
  import { clearSurfacePlanSnapFeedback } from '../../../lib/surface-plan-snap'
117
116
  import { paintZoneMembership } from '../../../lib/units'
117
+ import {
118
+ startWallPull,
119
+ WALL_PULL_AFFORDANCE,
120
+ type WallPullGripPayload,
121
+ } from '../../../lib/wall-pull'
118
122
  import useDirectManipulationFeedback from '../../../store/use-direct-manipulation-feedback'
119
123
  import useDrawingView from '../../../store/use-drawing-view'
120
124
  import useEditor, { isAngleSnapActive, type Mode } from '../../../store/use-editor'
@@ -130,7 +134,12 @@ import { isViewVisible, VIEW_2D } from '../../../store/view-layout'
130
134
  import { FloorRegionControls2D } from '../../editor/floor-region-controls'
131
135
  import { startGroupPickUp } from '../../editor/group-actions'
132
136
  import { classifyParticipant } from '../../editor/group-transform-shared'
133
- import { RoomHandleDragPreview2D, RoomHandles2D } from '../../editor/room-handles-2d'
137
+ import {
138
+ RoomHandleDragPreview2D,
139
+ RoomHandles2D,
140
+ SelectedWallDimensions2D,
141
+ useSelectionDimensionNodes,
142
+ } from '../../editor/room-handles-2d'
134
143
  import { RoomHighlight2D } from '../../editor/room-highlight'
135
144
  import { RoomLabels2D } from '../../editor/room-labels'
136
145
  import { suppressBoxSelectForPointer } from '../../tools/select/box-select-state'
@@ -150,7 +159,10 @@ import {
150
159
  resolveSvgAnnotationCollisions,
151
160
  svgAnnotationLabelId,
152
161
  } from './floorplan-annotation-layout'
153
- import { FloorplanDimensionEditOverlay } from './floorplan-dimension-edit-overlay'
162
+ import {
163
+ FloorplanDimensionEditOverlay,
164
+ typeableDimensionAt,
165
+ } from './floorplan-dimension-edit-overlay'
154
166
  import { FloorplanDimensionRenderer } from './floorplan-dimension-renderer'
155
167
  import { FloorplanGeometryRenderer } from './floorplan-geometry-renderer'
156
168
  import {
@@ -199,6 +211,15 @@ const DIRECT_DRAG_THRESHOLD_PX = 4
199
211
  const DIRECT_ROTATE_EPSILON = 1e-6
200
212
  const DIRECT_ROTATE_RADIANS_PER_PIXEL = Math.PI / 180
201
213
  const MAX_HANDLE_UNITS_PER_PIXEL = 0.015
214
+ // A wall end's pull grip, in screen pixels out from the end along the wall:
215
+ // clear of the end's move grip (its hit ring reaches ~17 px), a short dashed
216
+ // leader, then a round button with an outward arrow.
217
+ const PULL_GRIP_LEADER_START_PX = 9
218
+ const PULL_GRIP_CENTER_PX = 30
219
+ const PULL_GRIP_RADIUS_PX = 10
220
+ const PULL_GRIP_HIT_RADIUS_PX = 13
221
+ const PULL_GRIP_FILL = '#10b981' // emerald-500
222
+ const PULL_GRIP_HOVER_FILL = '#059669' // emerald-600
202
223
 
203
224
  export function resolveFloorplanHandleUnitsPerPixel(unitsPerPixel: number): number {
204
225
  return Math.min(unitsPerPixel, MAX_HANDLE_UNITS_PER_PIXEL)
@@ -387,7 +408,7 @@ type NodeDeps = {
387
408
  live: LiveTransform | undefined
388
409
  unit: 'metric' | 'imperial'
389
410
  metricNotation: 'meters' | 'millimeters'
390
- wallDimensionReference: FloorplanWallDimensionReference
411
+ wallDimensionFaces?: FloorplanWallDimensionFaces
391
412
  selected: boolean
392
413
  highlighted: boolean
393
414
  hovered: boolean
@@ -497,6 +518,20 @@ export function floorplanEntryYieldsToTool(state: {
497
518
  return state.mode === 'build' || state.openingPlacement
498
519
  }
499
520
 
521
+ /**
522
+ * Whether an entry draws its node without edit handles or dimension chrome: a member of a
523
+ * multi-selection, a ceiling outside its edit session, or anything while a build tool is armed (a
524
+ * selected roof keeps its selection for a new plane to join it, but a press near its eave must
525
+ * draw, not resize it).
526
+ */
527
+ export function floorplanEntryStripsHandles(state: {
528
+ mode: Mode
529
+ multiSelected: boolean
530
+ ceilingWithoutSession: boolean
531
+ }): boolean {
532
+ return state.multiSelected || state.ceilingWithoutSession || state.mode === 'build'
533
+ }
534
+
500
535
  function floorplanEntryYieldsToToolNow(): boolean {
501
536
  const { mode, tool } = useEditor.getState()
502
537
  const movingNode = getMovingNode()
@@ -596,6 +631,11 @@ export const FloorplanRegistryLayer = memo(function FloorplanRegistryLayer() {
596
631
  // `handleClickStop` / `handleSelect`, so the tool never sees it. Pass
597
632
  // clicks through in that case (`floorplanEntryYieldsToToolNow`).
598
633
  const editorMode = useEditor((s) => s.mode)
634
+ const selectionDimensionNodes = useSelectionDimensionNodes()
635
+ const selectionDimensionIds = useMemo(
636
+ () => new Set(selectionDimensionNodes.map((node) => node.id)),
637
+ [selectionDimensionNodes],
638
+ )
599
639
  const floorplanSelectionTool = useEditor((s) => s.floorplanSelectionTool)
600
640
  const endpointReshape = useEndpointReshape()
601
641
  const isMarqueeSelectionActive =
@@ -609,12 +649,9 @@ export const FloorplanRegistryLayer = memo(function FloorplanRegistryLayer() {
609
649
  const floorplanVisible = useEditor((s) => isViewVisible(s, VIEW_2D))
610
650
  const drawingType = useDrawingView((s) => s.drawingType)
611
651
  const annotationVisibility = useFloorplanAnnotationVisibility((s) => s.visibility)
612
- const wallDimensionReference = useFloorplanAnnotationVisibility((s) => s.wallDimensionReference)
652
+ const showDimensions = useFloorplanAnnotationVisibility((s) => s.showDimensions)
653
+ const wallDimensionFaces = useFloorplanAnnotationVisibility((s) => s.wallDimensionFaces)
613
654
  const floorplanMode = useFloorplanMode((s) => s.mode)
614
- const effectiveWallDimensionReference = resolveFloorplanWallDimensionReference(
615
- floorplanMode,
616
- wallDimensionReference,
617
- )
618
655
  // Elevator builders read runtime state imperatively, so entries include this
619
656
  // rare-changing ref in their cache deps.
620
657
  const interactiveElevators = useInteractive((s) => s.elevators)
@@ -981,6 +1018,12 @@ export const FloorplanRegistryLayer = memo(function FloorplanRegistryLayer() {
981
1018
  if (event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) return false
982
1019
  if (movingNode) return false
983
1020
  if (useEditor.getState().mode === 'delete') return false
1021
+ // A press on a member's dimension value is the type-a-value box's: its click opens it.
1022
+ if (!immediate && typeableDimensionAt(event.target, event.clientX, event.clientY)) {
1023
+ event.stopPropagation()
1024
+ suppressBoxSelectForPointer(event)
1025
+ return true
1026
+ }
984
1027
  const started = startFloorplanGroupMove(id, event, {
985
1028
  immediate,
986
1029
  onClickFallthrough: immediate ? undefined : () => startGroupPickUp(),
@@ -1299,6 +1342,16 @@ export const FloorplanRegistryLayer = memo(function FloorplanRegistryLayer() {
1299
1342
  runFloorplanWallPush(payload, event.clientX, event.clientY)
1300
1343
  return
1301
1344
  }
1345
+ // A wall end's pull grip opens the Walls run at that end; the plan's
1346
+ // wall draft then follows the pointer and the release places the wall.
1347
+ if (affordance === WALL_PULL_AFFORDANCE) {
1348
+ event.preventDefault()
1349
+ event.stopPropagation()
1350
+ suppressBoxSelectForPointer(event)
1351
+ const grip = payload as WallPullGripPayload
1352
+ startWallPull({ wallId: grip.wallId, endpoint: grip.endpoint, origin: '2d' })
1353
+ return
1354
+ }
1302
1355
 
1303
1356
  const sceneNodes = useScene.getState().nodes
1304
1357
  const node = sceneNodes[nodeId]
@@ -1590,6 +1643,7 @@ export const FloorplanRegistryLayer = memo(function FloorplanRegistryLayer() {
1590
1643
  <FloorplanRegistryEntry
1591
1644
  activeDragId={handleIdForNode(activeDragId, entry.id)}
1592
1645
  annotationVisibility={annotationVisibility}
1646
+ showDimensions={showDimensions}
1593
1647
  floorplanMode={floorplanMode}
1594
1648
  floorplanVisible={floorplanVisible}
1595
1649
  geometryCacheRef={geometryCacheRef}
@@ -1597,6 +1651,7 @@ export const FloorplanRegistryLayer = memo(function FloorplanRegistryLayer() {
1597
1651
  hoveredHandleId={handleIdForNode(hoveredHandleId, entry.id)}
1598
1652
  interactiveElevators={interactiveElevators}
1599
1653
  isMarqueeSelectionActive={isMarqueeSelectionActive}
1654
+ keepsSelectionDimensions={selectionDimensionIds.has(entry.id)}
1600
1655
  key={`${pass}-${entry.id}`}
1601
1656
  levelDataCacheRef={levelDataCacheRef}
1602
1657
  levelNodeIdsByType={floorplanData.levelNodeIdsByType}
@@ -1619,7 +1674,7 @@ export const FloorplanRegistryLayer = memo(function FloorplanRegistryLayer() {
1619
1674
  siblingEpoch={entry.dependsOnSiblingInputs ? (siblingEpochs.get(entry.id) ?? 0) : 0}
1620
1675
  unit={unit}
1621
1676
  metricNotation={metricNotation}
1622
- wallDimensionReference={effectiveWallDimensionReference}
1677
+ wallDimensionFaces={wallDimensionFaces}
1623
1678
  visibilityRootId={
1624
1679
  entry.visibilityRootId ?? (entry.ctxOverrides ? undefined : (levelId as AnyNodeId))
1625
1680
  }
@@ -1669,6 +1724,7 @@ export const FloorplanRegistryLayer = memo(function FloorplanRegistryLayer() {
1669
1724
  <g className="floorplan-registry-handles">{renderEntries('handles')}</g>
1670
1725
  <RoomHandles2D levelId={levelId} />
1671
1726
  <RoomHandleDragPreview2D levelId={levelId} />
1727
+ <SelectedWallDimensions2D levelId={levelId} />
1672
1728
  <FloorplanAnnotationLayoutResolver active={floorplanVisible} />
1673
1729
  {/* Click-to-type dimension labels. Portals its HTML input to
1674
1730
  document.body, so mounting it inside the SVG tree is safe. */}
@@ -1707,9 +1763,8 @@ function FloorplanAnnotationLayoutResolver({ active }: { active: boolean }) {
1707
1763
  const drawingType = useDrawingView((state) => state.drawingType)
1708
1764
  const annotationLayoutOverrides = useDrawingView((state) => state.annotationLayoutOverrides)
1709
1765
  const annotationVisibility = useFloorplanAnnotationVisibility((state) => state.visibility)
1710
- const wallDimensionReference = useFloorplanAnnotationVisibility(
1711
- (state) => state.wallDimensionReference,
1712
- )
1766
+ const showDimensions = useFloorplanAnnotationVisibility((state) => state.showDimensions)
1767
+ const wallDimensionFaces = useFloorplanAnnotationVisibility((state) => state.wallDimensionFaces)
1713
1768
  const floorplanMode = useFloorplanMode((state) => state.mode)
1714
1769
  const layoutInputs = useMemo(
1715
1770
  () => ({
@@ -1718,7 +1773,8 @@ function FloorplanAnnotationLayoutResolver({ active }: { active: boolean }) {
1718
1773
  drawingType,
1719
1774
  interactionIdle,
1720
1775
  settledLayoutEpoch,
1721
- wallDimensionReference,
1776
+ showDimensions,
1777
+ wallDimensionFaces,
1722
1778
  floorplanMode,
1723
1779
  }),
1724
1780
  [
@@ -1727,7 +1783,8 @@ function FloorplanAnnotationLayoutResolver({ active }: { active: boolean }) {
1727
1783
  drawingType,
1728
1784
  interactionIdle,
1729
1785
  settledLayoutEpoch,
1730
- wallDimensionReference,
1786
+ showDimensions,
1787
+ wallDimensionFaces,
1731
1788
  floorplanMode,
1732
1789
  ],
1733
1790
  )
@@ -1955,6 +2012,7 @@ function readFloorplanAnnotationLayoutOffset(label: SVGGElement) {
1955
2012
  type FloorplanRegistryEntryProps = {
1956
2013
  activeDragId: string | null
1957
2014
  annotationVisibility: FloorplanAnnotationVisibility
2015
+ showDimensions: boolean
1958
2016
  floorplanMode: FloorplanMode
1959
2017
  ctxOverrides: FloorplanContextOverrides | undefined
1960
2018
  floorplanVisible: boolean
@@ -1963,6 +2021,8 @@ type FloorplanRegistryEntryProps = {
1963
2021
  hoveredHandleId: string | null
1964
2022
  interactiveElevators: unknown
1965
2023
  isMarqueeSelectionActive: boolean
2024
+ /** A wall in a multi-selection still shows its own length (`useSelectionDimensionNodes`). */
2025
+ keepsSelectionDimensions: boolean
1966
2026
  levelDataCacheRef: { current: Map<string, LevelDataCacheEntry> }
1967
2027
  levelNodeIdsByType: ReadonlyMap<string, readonly AnyNodeId[]>
1968
2028
  moving: boolean
@@ -1996,13 +2056,14 @@ type FloorplanRegistryEntryProps = {
1996
2056
  siblingEpoch: number
1997
2057
  unit: 'metric' | 'imperial'
1998
2058
  metricNotation: 'meters' | 'millimeters'
1999
- wallDimensionReference: FloorplanWallDimensionReference
2059
+ wallDimensionFaces: FloorplanWallDimensionFaces
2000
2060
  visibilityRootId: AnyNodeId | undefined
2001
2061
  }
2002
2062
 
2003
2063
  const FloorplanRegistryEntry = memo(function FloorplanRegistryEntry({
2004
2064
  activeDragId,
2005
2065
  annotationVisibility,
2066
+ showDimensions,
2006
2067
  floorplanMode,
2007
2068
  ctxOverrides,
2008
2069
  floorplanVisible,
@@ -2011,6 +2072,7 @@ const FloorplanRegistryEntry = memo(function FloorplanRegistryEntry({
2011
2072
  hoveredHandleId,
2012
2073
  interactiveElevators,
2013
2074
  isMarqueeSelectionActive,
2075
+ keepsSelectionDimensions,
2014
2076
  levelDataCacheRef,
2015
2077
  levelNodeIdsByType,
2016
2078
  moving,
@@ -2032,7 +2094,7 @@ const FloorplanRegistryEntry = memo(function FloorplanRegistryEntry({
2032
2094
  siblingEpoch,
2033
2095
  unit,
2034
2096
  metricNotation,
2035
- wallDimensionReference,
2097
+ wallDimensionFaces,
2036
2098
  visibilityRootId,
2037
2099
  }: FloorplanRegistryEntryProps): React.ReactElement | null {
2038
2100
  // A node an agent just built is drawn on its construction-reveal turn, as in 3D.
@@ -2049,7 +2111,12 @@ const FloorplanRegistryEntry = memo(function FloorplanRegistryEntry({
2049
2111
  const ceilingWithoutSession = useCeilingEditSession(
2050
2112
  (state) => node.type === 'ceiling' && state.session?.ceilingId !== nodeId,
2051
2113
  )
2052
- const suppressHandles = multiSelected || ceilingWithoutSession
2114
+ const mode = useEditor((state) => state.mode)
2115
+ const suppressHandles = floorplanEntryStripsHandles({
2116
+ mode,
2117
+ multiSelected,
2118
+ ceilingWithoutSession,
2119
+ })
2053
2120
  const selectedLevelId = useViewer((state) => state.selection.levelId)
2054
2121
  const selectionProxyId = resolveSelectionProxyId(
2055
2122
  node,
@@ -2089,6 +2156,7 @@ const FloorplanRegistryEntry = memo(function FloorplanRegistryEntry({
2089
2156
  referencedAnnotationRole,
2090
2157
  selected,
2091
2158
  target: 'editor',
2159
+ dimensions: showDimensions,
2092
2160
  },
2093
2161
  )
2094
2162
 
@@ -2220,7 +2288,7 @@ const FloorplanRegistryEntry = memo(function FloorplanRegistryEntry({
2220
2288
  siblingEpoch,
2221
2289
  unit,
2222
2290
  metricNotation,
2223
- wallDimensionReference,
2291
+ wallDimensionFaces,
2224
2292
  visibilityRootId,
2225
2293
  })
2226
2294
  const rawGeometry = cacheEntry ? cacheEntry[pass] : null
@@ -2229,10 +2297,10 @@ const FloorplanRegistryEntry = memo(function FloorplanRegistryEntry({
2229
2297
  : null
2230
2298
  // Multi-selection shows highlight only: strip this member's edit handles /
2231
2299
  // dimension chrome (all of which live in the overlay pass) while keeping
2232
- // its highlighted body geometry.
2300
+ // its highlighted body geometry and, for a wall, its own length.
2233
2301
  const geometry =
2234
2302
  visibleGeometry && suppressHandles && pass !== 'base'
2235
- ? stripHandleChrome(visibleGeometry)
2303
+ ? stripHandleChrome(visibleGeometry, keepsSelectionDimensions)
2236
2304
  : visibleGeometry
2237
2305
  useFloorplanReveal(nodeId, entryRef, cacheEntry, revealWaiting, revealActive)
2238
2306
  if (revealWaiting || !geometry) return null
@@ -2293,7 +2361,7 @@ type BuildFloorplanEntryGeometryArgs = {
2293
2361
  siblingEpoch: number
2294
2362
  unit: 'metric' | 'imperial'
2295
2363
  metricNotation: 'meters' | 'millimeters'
2296
- wallDimensionReference: FloorplanWallDimensionReference
2364
+ wallDimensionFaces?: FloorplanWallDimensionFaces
2297
2365
  visibilityRootId: AnyNodeId | undefined
2298
2366
  }
2299
2367
 
@@ -2353,7 +2421,7 @@ export function buildFloorplanEntryGeometry({
2353
2421
  siblingEpoch,
2354
2422
  unit,
2355
2423
  metricNotation,
2356
- wallDimensionReference,
2424
+ wallDimensionFaces,
2357
2425
  visibilityRootId,
2358
2426
  }: BuildFloorplanEntryGeometryArgs): CacheEntry | null {
2359
2427
  const def = nodeRegistry.get(node.type)
@@ -2381,7 +2449,7 @@ export function buildFloorplanEntryGeometry({
2381
2449
  live,
2382
2450
  unit,
2383
2451
  metricNotation,
2384
- wallDimensionReference,
2452
+ wallDimensionFaces,
2385
2453
  selected,
2386
2454
  highlighted,
2387
2455
  hovered,
@@ -2468,7 +2536,7 @@ export function buildFloorplanEntryGeometry({
2468
2536
  unit,
2469
2537
  metricNotation,
2470
2538
  roomLabelOverlay: true,
2471
- wallDimensionReference,
2539
+ wallDimensionFaces,
2472
2540
  highlighted,
2473
2541
  hovered,
2474
2542
  moving,
@@ -2494,7 +2562,7 @@ export function buildFloorplanEntryGeometry({
2494
2562
  metricNotation,
2495
2563
  purpose: 'edit',
2496
2564
  roomLabelOverlay: true,
2497
- wallDimensionReference,
2565
+ wallDimensionFaces,
2498
2566
  }),
2499
2567
  viewState: palette
2500
2568
  ? {
@@ -2642,6 +2710,87 @@ export const InteractiveGeometry = memo(function InteractiveGeometry({
2642
2710
 
2643
2711
  return renderInteractive(geometry, 0)
2644
2712
 
2713
+ /**
2714
+ * A wall end's "pull to continue" grip: screen-sized, just past the end
2715
+ * along the wall, emerald so it reads apart from the end's move grip.
2716
+ */
2717
+ function renderPullGrip(
2718
+ g: Extract<FloorplanGeometry, { kind: 'move-arrow' }>,
2719
+ keyHint: number,
2720
+ ): React.ReactElement {
2721
+ const handleId = `${makeHandleId(nodeId, g.payload)}:pull`
2722
+ const isHovered = hoveredHandleId === handleId
2723
+ const k = handleUnitsPerPixel
2724
+ const c = PULL_GRIP_CENTER_PX * k
2725
+ const r = PULL_GRIP_RADIUS_PX * (isHovered ? 1.12 : 1) * k
2726
+ // Outward arrow inside the button: shaft then head, tip along +x.
2727
+ const arrowD = [
2728
+ `M ${c - 5.5 * k},${-1.4 * k}`,
2729
+ `L ${c + 0.5 * k},${-1.4 * k}`,
2730
+ `L ${c + 0.5 * k},${-4.6 * k}`,
2731
+ `L ${c + 6 * k},0`,
2732
+ `L ${c + 0.5 * k},${4.6 * k}`,
2733
+ `L ${c + 0.5 * k},${1.4 * k}`,
2734
+ `L ${c - 5.5 * k},${1.4 * k}`,
2735
+ 'Z',
2736
+ ].join(' ')
2737
+ const affordance = g.affordance
2738
+ const payload = g.payload
2739
+ return (
2740
+ <g
2741
+ data-wall-pull-grip={
2742
+ typeof payload === 'object' && payload && 'endpoint' in payload
2743
+ ? String((payload as { endpoint: unknown }).endpoint)
2744
+ : ''
2745
+ }
2746
+ key={keyHint}
2747
+ onClick={(e) => e.stopPropagation()}
2748
+ transform={`translate(${g.point[0]} ${g.point[1]}) rotate(${(g.angle * 180) / Math.PI})`}
2749
+ >
2750
+ <line
2751
+ pointerEvents="none"
2752
+ stroke={PULL_GRIP_FILL}
2753
+ strokeDasharray={`${3 * k} ${2.5 * k}`}
2754
+ strokeLinecap="round"
2755
+ strokeWidth={1.5 * k}
2756
+ x1={PULL_GRIP_LEADER_START_PX * k}
2757
+ x2={c - r}
2758
+ y1={0}
2759
+ y2={0}
2760
+ />
2761
+ <circle
2762
+ cx={c}
2763
+ cy={0}
2764
+ fill={isHovered ? PULL_GRIP_HOVER_FILL : PULL_GRIP_FILL}
2765
+ pointerEvents="none"
2766
+ r={r}
2767
+ stroke="#ffffff"
2768
+ strokeWidth={1.5 * k}
2769
+ />
2770
+ <path d={arrowD} fill="#ffffff" pointerEvents="none" />
2771
+ <circle
2772
+ cx={c}
2773
+ cy={0}
2774
+ fill="transparent"
2775
+ onPointerDown={(e) => {
2776
+ if (affordance) {
2777
+ onHandlePointerDown(affordance, payload, e as ReactPointerEvent<SVGGElement>)
2778
+ }
2779
+ }}
2780
+ onPointerEnter={() => onHandleHoverChange(handleId)}
2781
+ onPointerLeave={() => onHandleHoverChange(null)}
2782
+ // Fill only: the default stroke is a plan metre wide, and would take
2783
+ // the press meant for the end's move grip.
2784
+ pointerEvents="fill"
2785
+ r={PULL_GRIP_HIT_RADIUS_PX * k}
2786
+ style={{ cursor: 'grab' }}
2787
+ >
2788
+ {g.title ? <title>{g.title}</title> : null}
2789
+ </circle>
2790
+ </g>
2791
+ )
2792
+ }
2793
+
2645
2794
  function renderInteractive(g: FloorplanGeometry, keyHint: number): React.ReactElement {
2646
2795
  switch (g.kind) {
2647
2796
  case 'group': {
@@ -2914,7 +3063,8 @@ export const InteractiveGeometry = memo(function InteractiveGeometry({
2914
3063
  const head1 = `M ${t1x} ${t1y} L ${b1ax} ${b1ay} L ${b1bx} ${b1by} Z`
2915
3064
  const head2 = `M ${t2x} ${t2y} L ${b2ax} ${b2ay} L ${b2bx} ${b2by} Z`
2916
3065
  const fill = isHovered ? '#a5b4fc' : '#8381ed'
2917
- const strokeWidthPx = isHovered ? 2.4 : 1.8
3066
+ const scale = g.scale ?? 1
3067
+ const strokeWidthPx = (isHovered ? 2.4 : 1.8) * Math.sqrt(scale)
2918
3068
  const angleDeg = (g.angle * 180) / Math.PI
2919
3069
  const affordance = g.affordance
2920
3070
  const payload = g.payload
@@ -2923,7 +3073,7 @@ export const InteractiveGeometry = memo(function InteractiveGeometry({
2923
3073
  <g
2924
3074
  key={keyHint}
2925
3075
  onClick={(e) => e.stopPropagation()}
2926
- transform={`translate(${g.point[0]} ${g.point[1]}) rotate(${angleDeg})`}
3076
+ transform={`translate(${g.point[0]} ${g.point[1]}) rotate(${angleDeg}) scale(${scale})`}
2927
3077
  >
2928
3078
  <path
2929
3079
  d={arcPath}
@@ -2978,6 +3128,7 @@ export const InteractiveGeometry = memo(function InteractiveGeometry({
2978
3128
  }
2979
3129
  case 'move-arrow': {
2980
3130
  if (!palette) return <></>
3131
+ if (g.variant === 'pull') return renderPullGrip(g, keyHint)
2981
3132
  // Affordance-routed arrows (door width-resize) get a per-payload
2982
3133
  // handle id so each side can hover independently; default
2983
3134
  // (move-flow) arrows share the node's :move id like the dot.
@@ -2987,13 +3138,14 @@ export const InteractiveGeometry = memo(function InteractiveGeometry({
2987
3138
  // so it shrinks on zoom-out and grows on zoom-in, matching the
2988
3139
  // wall it accompanies. Composed of a rectangular shaft + triangular
2989
3140
  // 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
3141
+ const k = g.scale ?? 1
3142
+ const sl = 0.1 * k // shaft length (shortened body)
3143
+ const hl = 0.12 * k // head length
3144
+ const sh = 0.04 * k // shaft half-height
3145
+ const hh = 0.1 * k // head half-height
2994
3146
  // Inset the shaft start so the arrow sits a little off the wall
2995
3147
  // body (matches the 3D `HANDLE_OFFSET`).
2996
- const bi = 0.03 // base inset
3148
+ const bi = 0.03 * k // base inset
2997
3149
  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
3150
  // Indigo palette to match the 3D wall push arrows
2999
3151
  // (`ARROW_COLOR` / `ARROW_HOVER_COLOR`) and the corner-sphere
@@ -3642,11 +3794,19 @@ const HANDLE_CHROME_KINDS = new Set<FloorplanGeometry['kind']>([
3642
3794
  'equal-spacing-badge',
3643
3795
  ])
3644
3796
 
3645
- function stripHandleChrome(g: FloorplanGeometry): FloorplanGeometry | null {
3797
+ function stripHandleChrome(
3798
+ g: FloorplanGeometry,
3799
+ keepContextualDimensions = false,
3800
+ ): FloorplanGeometry | null {
3801
+ if (
3802
+ keepContextualDimensions &&
3803
+ readFloorplanGeometryMetadata(g).annotationRole === 'contextual-dimension'
3804
+ )
3805
+ return g
3646
3806
  if (HANDLE_CHROME_KINDS.has(g.kind)) return null
3647
3807
  if (g.kind === 'group') {
3648
3808
  const children = g.children
3649
- .map(stripHandleChrome)
3809
+ .map((child) => stripHandleChrome(child, keepContextualDimensions))
3650
3810
  .filter((c): c is FloorplanGeometry => c !== null)
3651
3811
  if (children.length === 0) return null
3652
3812
  return { kind: 'group', children, transform: g.transform }
@@ -3763,7 +3923,7 @@ function nodeDepsEqual(a: NodeDeps, b: NodeDeps): boolean {
3763
3923
  'live',
3764
3924
  'unit',
3765
3925
  'metricNotation',
3766
- 'wallDimensionReference',
3926
+ 'wallDimensionFaces',
3767
3927
  'selected',
3768
3928
  'highlighted',
3769
3929
  'hovered',
@@ -1,5 +1,5 @@
1
1
  'use client'
2
- import { type AnyNodeId, useLiveNodeOverrides, useScene } from '@pascal-app/core'
2
+ import { type AnyNodeId, useLiveNodeOverrides, usePascalI18n, useScene } from '@pascal-app/core'
3
3
  import {
4
4
  ProceduralItemNode,
5
5
  snapParameters,
@@ -22,6 +22,7 @@ export function ProceduralInspector({
22
22
  partId: string | null
23
23
  onPartChange: (id: string | null) => void
24
24
  }) {
25
+ const i18n = usePascalI18n()
25
26
  const committed = useScene((s) => s.nodes[nodeId as AnyNodeId]) as unknown as
26
27
  | ProceduralItemNode
27
28
  | undefined
@@ -42,14 +43,24 @@ export function ProceduralInspector({
42
43
  const parameters = commit ? snapParameters(node.recipe, values) : values
43
44
  const parsed = ProceduralItemNode.safeParse({ ...node, parameters })
44
45
  if (!parsed.success) {
45
- setError(parsed.error.issues[0]?.message ?? 'Invalid edit')
46
+ setError(
47
+ parsed.error.issues[0]?.message ??
48
+ i18n._({ id: 'editor.proceduralItems.inspector.invalidEdit', message: 'Invalid edit' }),
49
+ )
46
50
  if (commit) useLiveNodeOverrides.getState().clear(nodeId as AnyNodeId)
47
51
  return
48
52
  }
49
53
  try {
50
54
  validateProceduralRelations({ ...node, parameters }, useScene.getState().nodes)
51
55
  } catch (e) {
52
- setError(e instanceof Error ? e.message : 'Invalid attachment')
56
+ setError(
57
+ e instanceof Error
58
+ ? e.message
59
+ : i18n._({
60
+ id: 'editor.proceduralItems.inspector.invalidAttachment',
61
+ message: 'Invalid attachment',
62
+ }),
63
+ )
53
64
  if (commit) useLiveNodeOverrides.getState().clear(nodeId as AnyNodeId)
54
65
  return
55
66
  }
@@ -64,7 +75,12 @@ export function ProceduralInspector({
64
75
  <div className="space-y-5">
65
76
  <div className="flex items-center justify-between">
66
77
  <h2 className="font-semibold text-lg">
67
- {partId ? node.recipe.parts.find((p) => p.id === partId)?.label : 'Adjust design'}
78
+ {partId
79
+ ? node.recipe.parts.find((p) => p.id === partId)?.label
80
+ : i18n._({
81
+ id: 'editor.proceduralItems.inspector.adjustDesign',
82
+ message: 'Adjust design',
83
+ })}
68
84
  </h2>
69
85
  {partId && (
70
86
  <button
@@ -72,7 +88,7 @@ export function ProceduralInspector({
72
88
  onClick={() => onPartChange(null)}
73
89
  type="button"
74
90
  >
75
- Back
91
+ {i18n._({ id: 'editor.proceduralItems.inspector.back', message: 'Back' })}
76
92
  </button>
77
93
  )}
78
94
  </div>
@@ -128,11 +144,21 @@ export function ProceduralInspector({
128
144
  useScene.getState().updateNode(nodeId as AnyNodeId, { parameters } as never)
129
145
  setError('')
130
146
  } catch (e) {
131
- setError(e instanceof Error ? e.message : 'Invalid reset')
147
+ setError(
148
+ e instanceof Error
149
+ ? e.message
150
+ : i18n._({
151
+ id: 'editor.proceduralItems.inspector.invalidReset',
152
+ message: 'Invalid reset',
153
+ }),
154
+ )
132
155
  }
133
156
  }}
134
157
  >
135
- Reset this part
158
+ {i18n._({
159
+ id: 'editor.proceduralItems.inspector.resetThisPart',
160
+ message: 'Reset this part',
161
+ })}
136
162
  </button>
137
163
  )}
138
164
  {error && (
@@ -142,7 +168,9 @@ export function ProceduralInspector({
142
168
  )}
143
169
  {!partId && node.recipe.parameters.some((p) => p.part) && (
144
170
  <div>
145
- <h3 className="mb-2 font-medium text-sm">Edit parts</h3>
171
+ <h3 className="mb-2 font-medium text-sm">
172
+ {i18n._({ id: 'editor.proceduralItems.inspector.editParts', message: 'Edit parts' })}
173
+ </h3>
146
174
  <div className="flex flex-wrap gap-2">
147
175
  {node.recipe.parts
148
176
  .filter((part) => node.recipe.parameters.some((p) => p.part === part.id))
@@ -188,13 +216,21 @@ function ProceduralPanel({ node }: { node: ProceduralItemNode }) {
188
216
  }
189
217
 
190
218
  function ProceduralPlacementControls({ node }: { node: ProceduralItemNode }) {
219
+ const i18n = usePascalI18n()
191
220
  const [error, setError] = useState('')
192
221
  const update = (patch: Partial<ProceduralItemNode>) => {
193
222
  try {
194
223
  useScene.getState().updateNode(node.id as AnyNodeId, patch as never)
195
224
  setError('')
196
225
  } catch (e) {
197
- setError(e instanceof Error ? e.message : 'Invalid placement')
226
+ setError(
227
+ e instanceof Error
228
+ ? e.message
229
+ : i18n._({
230
+ id: 'editor.proceduralItems.inspector.invalidPlacement',
231
+ message: 'Invalid placement',
232
+ }),
233
+ )
198
234
  }
199
235
  }
200
236
  const changePosition = (index: number, value: number, commit: boolean) => {
@@ -213,18 +249,29 @@ function ProceduralPlacementControls({ node }: { node: ProceduralItemNode }) {
213
249
  setError('')
214
250
  } catch (e) {
215
251
  if (commit) useLiveNodeOverrides.getState().clear(node.id as AnyNodeId)
216
- setError(e instanceof Error ? e.message : 'Invalid position')
252
+ setError(
253
+ e instanceof Error
254
+ ? e.message
255
+ : i18n._({
256
+ id: 'editor.proceduralItems.inspector.invalidPosition',
257
+ message: 'Invalid position',
258
+ }),
259
+ )
217
260
  }
218
261
  }
219
262
  return (
220
- <PanelSection title="Position">
263
+ <PanelSection
264
+ title={i18n._({ id: 'editor.proceduralItems.inspector.position', message: 'Position' })}
265
+ >
221
266
  {(['X', 'Y', 'Z'] as const).map((axis, index) => (
222
267
  <SliderControl
223
268
  key={axis}
224
269
  label={
225
270
  <>
226
271
  {axis}
227
- <sub className="ml-[1px] text-[11px] opacity-70">pos</sub>
272
+ <sub className="ml-[1px] text-[11px] opacity-70">
273
+ {i18n._({ id: 'editor.proceduralItems.inspector.pos', message: 'pos' })}
274
+ </sub>
228
275
  </>
229
276
  }
230
277
  value={node.position[index]!}
@@ -29,6 +29,7 @@ import {
29
29
  type AttributesBackend = {
30
30
  createAttribute(): void
31
31
  updateAttribute(attribute: BufferAttribute): void
32
+ destroyAttribute(attribute: BufferAttribute): void
32
33
  }
33
34
  const Attributes = UntypedAttributes as new (
34
35
  backend: AttributesBackend,
@@ -371,6 +372,7 @@ test('WebGPU attribute updates skip resting frames and upload only written slots
371
372
  const attributes = new Attributes(
372
373
  {
373
374
  createAttribute() {},
375
+ destroyAttribute() {},
374
376
  updateAttribute(attribute: BufferAttribute) {
375
377
  uploads.push({ attribute, ranges: attribute.updateRanges.map((range) => ({ ...range })) })
376
378
  attribute.clearUpdateRanges()