@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
@@ -6,16 +6,18 @@ import {
6
6
  floorPlacedCollides,
7
7
  nodeRegistry,
8
8
  type TerrainVerb,
9
- useScene,
9
+ useScene, usePascalI18n,
10
10
  } from '@pascal-app/core'
11
11
  import { useViewer } from '@pascal-app/viewer'
12
12
  import { useEffect, useMemo, useState, useSyncExternalStore } from 'react'
13
13
  import { useShallow } from 'zustand/react/shallow'
14
14
  import { useIsMobile } from '../../../hooks/use-mobile'
15
15
  import {
16
+ AXIS_MOVE_DRAG_LABEL,
16
17
  type ContextualShortcutHint,
17
18
  GROUP_MOVE_DRAG_LABEL,
18
19
  GROUP_ROTATE_DRAG_LABEL,
20
+ PATH_POINT_DRAG_LABEL,
19
21
  ROTATE_HANDLE_DRAG_LABEL,
20
22
  resolveRotateHandleHelpHints,
21
23
  resolveSelectModeHelpHints,
@@ -42,7 +44,7 @@ import {
42
44
  terrainHudTitle,
43
45
  toolHudTitle,
44
46
  } from '../../../lib/hud-title'
45
- import type { ReshapeKind } from '../../../lib/interaction/scope'
47
+ import type { InteractionScope, ReshapeKind } from '../../../lib/interaction/scope'
46
48
  import {
47
49
  type MezzanineShape,
48
50
  useMezzanineDraft,
@@ -94,7 +96,7 @@ function reshapingHints(reshape: ReshapeKind): ContextualShortcutHint[] {
94
96
  : 'Move corner'
95
97
  return [
96
98
  { keys: ['Drag'], label: action },
97
- { keys: ['Esc'], label: 'Cancel' },
99
+ { keys: ['Esc'], label: /* i18n */ { id: 'editor.helpers.helperManager.cancel', message: 'Cancel' } },
98
100
  ]
99
101
  }
100
102
 
@@ -106,8 +108,8 @@ function reshapingHints(reshape: ReshapeKind): ContextualShortcutHint[] {
106
108
  function terrainSculptHints(verb: TerrainVerb, sampling: boolean): ContextualShortcutHint[] {
107
109
  if (sampling) {
108
110
  return [
109
- { keys: ['Click'], label: 'Pick target height' },
110
- { keys: ['Esc'], label: 'Cancel picking' },
111
+ { keys: ['Click'], label: /* i18n */ { id: 'editor.helpers.helperManager.pickTargetHeight', message: 'Pick target height' } },
112
+ { keys: ['Esc'], label: /* i18n */ { id: 'editor.helpers.helperManager.cancelPicking', message: 'Cancel picking' } },
111
113
  ]
112
114
  }
113
115
  const action =
@@ -122,8 +124,8 @@ function terrainSculptHints(verb: TerrainVerb, sampling: boolean): ContextualSho
122
124
  { keys: ['Drag'], label: action },
123
125
  // Nested, so the two render as alternatives ("[ / ]") rather than a chord —
124
126
  // a flat `['[', ']']` joins with "+" and would read as "press both".
125
- { keys: [['[', ']']], label: 'Brush size' },
126
- { keys: ['Esc'], label: 'Cancel stroke' },
127
+ { keys: [['[', ']']], label: /* i18n */ { id: 'editor.helpers.helperManager.brushSize', message: 'Brush size' } },
128
+ { keys: ['Esc'], label: /* i18n */ { id: 'editor.helpers.helperManager.cancelStroke', message: 'Cancel stroke' } },
127
129
  ]
128
130
  }
129
131
 
@@ -141,12 +143,12 @@ const EMPTY_CONTEXTUAL_HINTS: ContextualShortcutHint[] = []
141
143
  export function paintHints(mode: PaintMode): ContextualShortcutHint[] {
142
144
  if (mode === 'pick')
143
145
  return [
144
- { keys: ['Left click'], label: 'Paint with this material' },
145
- { keys: ['Esc'], label: 'Cancel' },
146
+ { keys: ['Left click'], label: /* i18n */ { id: 'editor.helpers.helperManager.paintWithThisMaterial', message: 'Paint with this material' } },
147
+ { keys: ['Esc'], label: /* i18n */ { id: 'editor.helpers.helperManager.cancel', message: 'Cancel' } },
146
148
  ]
147
149
  if (mode === 'erase')
148
- return [{ keys: ['Left click'], label: 'Remove a painted part, or reset a surface' }]
149
- if (mode === 'surface') return [{ keys: [['Alt', 'Option']], label: 'Hold to pick a material' }]
150
+ return [{ keys: ['Left click'], label: /* i18n */ { id: 'editor.helpers.helperManager.removeAPaintedPartOrReset', message: 'Remove a painted part, or reset a surface' } }]
151
+ if (mode === 'surface') return [{ keys: [['Alt', 'Option']], label: /* i18n */ { id: 'editor.helpers.helperManager.holdToPickAMaterial', message: 'Hold to pick a material' } }]
150
152
  return []
151
153
  }
152
154
 
@@ -156,43 +158,45 @@ export function paintRegionHints(
156
158
  hovering = true,
157
159
  ): ContextualShortcutHint[] {
158
160
  const gesture: Record<typeof mode, ContextualShortcutHint> = {
159
- rectangle: { keys: ['Drag'], label: 'Press anywhere, drag to the opposite corner' },
160
- polygon: { keys: ['Left click'], label: 'Add point · click the first to close' },
161
+ rectangle: { keys: ['Drag'], label: /* i18n */ { id: 'editor.helpers.helperManager.pressAnywhereDragToTheOpposite', message: 'Press anywhere, drag to the opposite corner' } },
162
+ polygon: { keys: ['Left click'], label: /* i18n */ { id: 'editor.helpers.helperManager.addPointClickTheFirstTo', message: 'Add point · click the first to close' } },
161
163
  }
162
164
  // Off anything the sub-mode draws on, the HUD says where to go instead.
163
165
  if (!hovering)
164
166
  return [
165
167
  ...(notice ? [{ keys: ['!'], label: notice, active: true }] : []),
166
168
  { keys: ['Hover'], label: paintRegionHoverHint(mode), active: true },
167
- { keys: ['Esc'], label: 'Cancel' },
169
+ { keys: ['Esc'], label: /* i18n */ { id: 'editor.helpers.helperManager.cancel', message: 'Cancel' } },
168
170
  ]
169
171
  return [
170
172
  ...(notice ? [{ keys: ['!'], label: notice, active: true }] : []),
171
173
  gesture[mode],
172
- ...(mode === 'polygon' ? [{ keys: ['Backspace'], label: 'Remove last point' }] : []),
173
- { keys: ['Esc'], label: 'Cancel' },
174
+ ...(mode === 'polygon' ? [{ keys: ['Backspace'], label: /* i18n */ { id: 'editor.helpers.helperManager.removeLastPoint', message: 'Remove last point' } }] : []),
175
+ { keys: ['Esc'], label: /* i18n */ { id: 'editor.helpers.helperManager.cancel', message: 'Cancel' } },
174
176
  ]
175
177
  }
178
+ const DRAW_MEZZANINE = /* i18n */ { id: 'editor.helpers.helperManager.drawTheMezzanine', message: 'Draw the mezzanine' }
179
+ const DRAW_OPENING = /* i18n */ { id: 'editor.helpers.helperManager.drawTheOpening', message: 'Draw the opening' }
176
180
  // "Add mezzanine": the outline gesture for the shape picked in the room panel,
177
181
  // then Esc; the snapping chip sits below.
178
182
  export function mezzanineHints(shape: MezzanineShape): ContextualShortcutHint[] {
179
183
  return shape === 'rectangle'
180
184
  ? [
181
- { keys: ['Left click'], label: 'Set one corner, then the opposite' },
182
- { keys: ['Drag'], label: 'Draw the mezzanine' },
183
- { keys: ['Esc'], label: 'Cancel' },
185
+ { keys: ['Left click'], label: /* i18n */ { id: 'editor.helpers.helperManager.setOneCornerThenTheOpposite', message: 'Set one corner, then the opposite' } },
186
+ { keys: ['Drag'], label: DRAW_MEZZANINE },
187
+ { keys: ['Esc'], label: /* i18n */ { id: 'editor.helpers.helperManager.cancel', message: 'Cancel' } },
184
188
  ]
185
189
  : [
186
- { keys: ['Left click'], label: 'Add point · click the first to close' },
187
- { keys: ['Backspace'], label: 'Remove last point' },
188
- { keys: ['Enter'], label: 'Finish' },
189
- { keys: ['Esc'], label: 'Cancel' },
190
+ { keys: ['Left click'], label: /* i18n */ { id: 'editor.helpers.helperManager.addPointClickTheFirstTo', message: 'Add point · click the first to close' } },
191
+ { keys: ['Backspace'], label: /* i18n */ { id: 'editor.helpers.helperManager.removeLastPoint', message: 'Remove last point' } },
192
+ { keys: ['Enter'], label: /* i18n */ { id: 'editor.helpers.helperManager.finish', message: 'Finish' } },
193
+ { keys: ['Esc'], label: /* i18n */ { id: 'editor.helpers.helperManager.cancel', message: 'Cancel' } },
190
194
  ]
191
195
  }
192
196
  /** "Cut opening": the outline gesture for the chosen shape, then Esc. */
193
197
  export function openingHints(shape: OpeningShape): ContextualShortcutHint[] {
194
198
  return mezzanineHints(shape).map((hint) =>
195
- hint.label === 'Draw the mezzanine' ? { ...hint, label: 'Draw the opening' } : hint,
199
+ typeof hint.label !== 'string' && hint.label.id === DRAW_MEZZANINE.id ? { ...hint, label: DRAW_OPENING } : hint,
196
200
  )
197
201
  }
198
202
  const TERRACE_CHIP_HINTS = [TERRACE_SHAPE_HINT]
@@ -200,22 +204,22 @@ const TERRACE_CHIP_HINTS = [TERRACE_SHAPE_HINT]
200
204
  export function terraceHints(shape: TerraceShape): ContextualShortcutHint[] {
201
205
  return shape === 'rectangle'
202
206
  ? [
203
- { keys: ['Left click'], label: 'Set one corner, then the opposite' },
204
- { keys: ['Drag'], label: 'Draw the terrace' },
205
- { keys: ['Esc'], label: 'Done' },
207
+ { keys: ['Left click'], label: /* i18n */ { id: 'editor.helpers.helperManager.setOneCornerThenTheOpposite', message: 'Set one corner, then the opposite' } },
208
+ { keys: ['Drag'], label: /* i18n */ { id: 'editor.helpers.helperManager.drawTheTerrace', message: 'Draw the terrace' } },
209
+ { keys: ['Esc'], label: /* i18n */ { id: 'editor.helpers.helperManager.done', message: 'Done' } },
206
210
  ]
207
211
  : [
208
- { keys: ['Left click'], label: 'Add point · click the first to close' },
209
- { keys: ['Backspace'], label: 'Remove last point' },
210
- { keys: ['Enter'], label: 'Finish' },
211
- { keys: ['Esc'], label: 'Done' },
212
+ { keys: ['Left click'], label: /* i18n */ { id: 'editor.helpers.helperManager.addPointClickTheFirstTo', message: 'Add point · click the first to close' } },
213
+ { keys: ['Backspace'], label: /* i18n */ { id: 'editor.helpers.helperManager.removeLastPoint', message: 'Remove last point' } },
214
+ { keys: ['Enter'], label: /* i18n */ { id: 'editor.helpers.helperManager.finish', message: 'Finish' } },
215
+ { keys: ['Esc'], label: /* i18n */ { id: 'editor.helpers.helperManager.done', message: 'Done' } },
212
216
  ]
213
217
  }
214
218
  export const ROOM_DIVIDE_HINTS: ContextualShortcutHint[] = [
215
- { keys: ['Left click'], label: 'Add point' },
216
- { keys: ['Backspace'], label: 'Remove last point' },
217
- { keys: ['Enter'], label: 'Finish at the edge' },
218
- { keys: ['Esc'], label: 'Cancel' },
219
+ { keys: ['Left click'], label: /* i18n */ { id: 'editor.helpers.helperManager.addPoint', message: 'Add point' } },
220
+ { keys: ['Backspace'], label: /* i18n */ { id: 'editor.helpers.helperManager.removeLastPoint', message: 'Remove last point' } },
221
+ { keys: ['Enter'], label: /* i18n */ { id: 'editor.helpers.helperManager.finishAtTheEdge', message: 'Finish at the edge' } },
222
+ { keys: ['Esc'], label: /* i18n */ { id: 'editor.helpers.helperManager.cancel', message: 'Cancel' } },
219
223
  ]
220
224
  const NO_CONTEXTUAL_HELP_SUBSCRIPTION = () => () => {}
221
225
 
@@ -277,22 +281,76 @@ export function mezzanineGesture(
277
281
 
278
282
  export const MEZZANINE_GESTURE_HINTS: Record<MezzanineGesture, ContextualShortcutHint[]> = {
279
283
  raise: [
280
- { keys: ['Drag'], label: 'Raise or lower' },
281
- { keys: ['Esc'], label: 'Cancel' },
284
+ { keys: ['Drag'], label: /* i18n */ { id: 'editor.helpers.helperManager.raiseOrLower', message: 'Raise or lower' } },
285
+ { keys: ['Esc'], label: /* i18n */ { id: 'editor.helpers.helperManager.cancel', message: 'Cancel' } },
282
286
  ],
283
287
  resize: [
284
- { keys: ['Drag'], label: 'Resize' },
285
- { keys: ['Esc'], label: 'Cancel' },
288
+ { keys: ['Drag'], label: /* i18n */ { id: 'editor.helpers.helperManager.resize', message: 'Resize' } },
289
+ { keys: ['Esc'], label: /* i18n */ { id: 'editor.helpers.helperManager.cancel', message: 'Cancel' } },
286
290
  ],
287
291
  move: [
288
- { keys: ['Left click'], label: 'Place inside the room' },
289
- { keys: [['R', 'T']], label: 'Rotate ±45°' },
290
- { keys: ['Alt'], label: 'Free move' },
291
- { keys: ['Esc'], label: 'Cancel' },
292
+ { keys: ['Left click'], label: /* i18n */ { id: 'editor.helpers.helperManager.placeInsideTheRoom', message: 'Place inside the room' } },
293
+ { keys: [['R', 'T']], label: /* i18n */ { id: 'editor.helpers.helperManager.rotate45', message: 'Rotate ±45°' } },
294
+ { keys: ['Alt'], label: /* i18n */ { id: 'editor.helpers.helperManager.freeMove', message: 'Free move' } },
295
+ { keys: ['Esc'], label: /* i18n */ { id: 'editor.helpers.helperManager.cancel', message: 'Cancel' } },
292
296
  ],
293
297
  }
294
298
 
299
+ export type MepHandleGesture = 'edit-path' | 'move'
300
+
301
+ /**
302
+ * A drag on an MEP handle — a run point, where Alt detaches the joint, or a
303
+ * fitting, run or run segment slid along an axis — or null. Its HUD names the
304
+ * kind and the gesture, not "Resize".
305
+ */
306
+ export function mepHandleGesture(
307
+ drag: { nodeId: string; label: string } | null,
308
+ ): MepHandleGesture | null {
309
+ if (drag?.label === PATH_POINT_DRAG_LABEL) return 'edit-path'
310
+ if (drag?.label === AXIS_MOVE_DRAG_LABEL) return 'move'
311
+ return null
312
+ }
313
+
314
+ export function mepHandleGestureHints(
315
+ gesture: MepHandleGesture,
316
+ altPressed: boolean,
317
+ ): ContextualShortcutHint[] {
318
+ const cancel: ContextualShortcutHint = {
319
+ keys: ['Esc'],
320
+ label: /* i18n */ { id: 'editor.helpers.helperManager.cancel', message: 'Cancel' },
321
+ }
322
+ if (gesture === 'move') return [cancel]
323
+ return [
324
+ {
325
+ keys: ['Alt'],
326
+ label: /* i18n */ { id: 'editor.helpers.helperManager.detachTheJoint', message: 'Detach the joint' },
327
+ active: altPressed,
328
+ },
329
+ cancel,
330
+ ]
331
+ }
332
+
333
+ /**
334
+ * Whether the armed tool's own hints (`toolHints`) win over the node in hand: a registry tool that
335
+ * holds its draft in a placing scope (the roof's footprint, a fitting, a block) says what its clicks
336
+ * do, where the generic rows misnamed it ("Move roof · Left click: Place" while a click draws a
337
+ * footprint).
338
+ */
339
+ export function toolHintsOwnPlacing(
340
+ scope: InteractionScope,
341
+ tool: string | null,
342
+ toolHintCount: number,
343
+ ): boolean {
344
+ return (
345
+ scope.kind === 'placing' &&
346
+ scope.driver === 'registry-tool' &&
347
+ scope.nodeType === tool &&
348
+ toolHintCount > 0
349
+ )
350
+ }
351
+
295
352
  export function HelperManager() {
353
+ const i18n = usePascalI18n()
296
354
  const mode = useEditor((s) => s.mode)
297
355
  const tool = useEditor((s) => s.tool)
298
356
  const terrainVerb = useEditor((s) => s.terrainVerb)
@@ -318,6 +376,10 @@ export function HelperManager() {
318
376
  const reshapingNode = useReshapingNode()
319
377
  const activeHandleDrag = useActiveHandleDrag()
320
378
  const mezzanineHandle = useScene((s) => mezzanineGesture(activeHandleDrag, s.nodes))
379
+ const mepHandle = mepHandleGesture(activeHandleDrag)
380
+ const mepHandleKind = useScene((s) =>
381
+ mepHandle && activeHandleDrag ? s.nodes[activeHandleDrag.nodeId as AnyNodeId]?.type : undefined,
382
+ )
321
383
  const selectedIds = useViewer((s) => s.selection.selectedIds)
322
384
  const isMobile = useIsMobile()
323
385
  const modifiers = useActiveModifierKeys()
@@ -417,7 +479,7 @@ export function HelperManager() {
417
479
  if (activeHandleDrag?.label === GROUP_MOVE_DRAG_LABEL) {
418
480
  return (
419
481
  <ContextualHelperPanel
420
- hints={[{ keys: ['R / T'], label: 'Rotate the selection ±45°' }]}
482
+ hints={[{ keys: ['R / T'], label: i18n._({ id: 'editor.helpers.helperManager.rotateTheSelection45', message: 'Rotate the selection ±45°' }) }]}
421
483
  snapContext={snapContext}
422
484
  title={MOVE_SELECTION_HUD_TITLE}
423
485
  />
@@ -440,10 +502,10 @@ export function HelperManager() {
440
502
  return (
441
503
  <ContextualHelperPanel
442
504
  hints={[
443
- { keys: ['Left click'], label: 'Place' },
444
- { keys: [['R', 'T']], label: 'Rotate ±45°' },
445
- { keys: ['Alt'], label: 'Free move · slide doors and windows' },
446
- { keys: ['Esc'], label: 'Cancel' },
505
+ { keys: ['Left click'], label: i18n._({ id: 'editor.helpers.helperManager.place', message: 'Place' }) },
506
+ { keys: [['R', 'T']], label: i18n._({ id: 'editor.helpers.helperManager.rotate45', message: 'Rotate ±45°' }) },
507
+ { keys: ['Alt'], label: i18n._({ id: 'editor.helpers.helperManager.freeMoveSlideDoorsAndWindows', message: 'Free move · slide doors and windows' }) },
508
+ { keys: ['Esc'], label: i18n._({ id: 'editor.helpers.helperManager.cancel', message: 'Cancel' }) },
447
509
  ]}
448
510
  snapContext={snapContext}
449
511
  title={MOVE_ROOM_HUD_TITLE}
@@ -496,6 +558,20 @@ export function HelperManager() {
496
558
  )
497
559
  }
498
560
 
561
+ if (mepHandle) {
562
+ return (
563
+ <ContextualHelperPanel
564
+ hints={mepHandleGestureHints(mepHandle, modifiers.alt)}
565
+ snapContext={snapContext}
566
+ title={
567
+ mepHandleKind
568
+ ? nodeKindHudTitle(mepHandleKind, mepHandle === 'move' ? 'move' : 'edit')
569
+ : null
570
+ }
571
+ />
572
+ )
573
+ }
574
+
499
575
  // A single-node resize arrow is still an active snapping interaction. Roof
500
576
  // width/depth handles opt into grid snapping, so keep the mode and grid-step
501
577
  // controls visible for the whole drag instead of falling through to idle
@@ -504,8 +580,8 @@ export function HelperManager() {
504
580
  return (
505
581
  <ContextualHelperPanel
506
582
  hints={[
507
- { keys: ['Drag'], label: 'Resize' },
508
- { keys: ['Esc'], label: 'Cancel' },
583
+ { keys: ['Drag'], label: i18n._({ id: 'editor.helpers.helperManager.resize', message: 'Resize' }) },
584
+ { keys: ['Esc'], label: i18n._({ id: 'editor.helpers.helperManager.cancel', message: 'Cancel' }) },
509
585
  ]}
510
586
  snapContext={snapContext}
511
587
  title={RESIZE_HUD_TITLE}
@@ -522,7 +598,7 @@ export function HelperManager() {
522
598
  ? nodeRegistry.get(reshapingNode.type)?.affordanceHints?.[scope.reshape]
523
599
  : undefined
524
600
  const title = reshapingNode
525
- ? nodeKindHudTitle(reshapingNode.type, scope.reshape === 'split' ? 'Split' : 'Edit')
601
+ ? nodeKindHudTitle(reshapingNode.type, scope.reshape === 'split' ? 'split' : 'edit')
526
602
  : null
527
603
  if (hints) {
528
604
  return (
@@ -543,12 +619,15 @@ export function HelperManager() {
543
619
  )
544
620
  }
545
621
 
546
- if (movingNode) {
622
+ if (
623
+ movingNode &&
624
+ !toolHintsOwnPlacing(scope, tool, tool ? (nodeRegistry.get(tool)?.toolHints?.length ?? 0) : 0)
625
+ ) {
547
626
  // A fresh placement (e.g. a positioned preset like a shelf) advertises its
548
627
  // once/repeat continuation, exactly like the GLB item tool — but an existing
549
628
  // node being *moved* is not a placement, so it gets no continuation chip.
550
629
  const isFreshPlacement = isFreshPlacementMetadata(movingNode.metadata)
551
- const movingTitle = nodeKindHudTitle(movingNode.type, isFreshPlacement ? undefined : 'Move')
630
+ const movingTitle = nodeKindHudTitle(movingNode.type, isFreshPlacement ? undefined : 'move')
552
631
  if (movingNode.type === 'building') return <BuildingHelper showRotate title={movingTitle} />
553
632
  const movingContinuationContext = isFreshPlacement
554
633
  ? keyCyclableContinuationContext(continuationContextOf(movingNode.type))
@@ -611,7 +690,7 @@ export function HelperManager() {
611
690
  <ContextualHelperPanel
612
691
  hints={contextualEditHints}
613
692
  snapContext={snapContext}
614
- title={contextualHelpNode ? nodeKindHudTitle(contextualHelpNode.type, 'Edit') : null}
693
+ title={contextualHelpNode ? nodeKindHudTitle(contextualHelpNode.type, 'edit') : null}
615
694
  />
616
695
  )
617
696
  }
@@ -639,9 +718,9 @@ export function HelperManager() {
639
718
  return (
640
719
  <ContextualHelperPanel
641
720
  hints={[
642
- { keys: ['Hover'], label: 'Inspect surface dimensions' },
643
- { keys: ['Click'], label: 'Pin measurement lens' },
644
- { keys: ['Esc'], label: 'Exit smart measure' },
721
+ { keys: ['Hover'], label: i18n._({ id: 'editor.helpers.helperManager.inspectSurfaceDimensions', message: 'Inspect surface dimensions' }) },
722
+ { keys: ['Click'], label: i18n._({ id: 'editor.helpers.helperManager.pinMeasurementLens', message: 'Pin measurement lens' }) },
723
+ { keys: ['Esc'], label: i18n._({ id: 'editor.helpers.helperManager.exitSmartMeasure', message: 'Exit smart measure' }) },
645
724
  ]}
646
725
  title={SMART_MEASURE_HUD_TITLE}
647
726
  />
@@ -1,3 +1,4 @@
1
+ import { usePascalI18n } from '@pascal-app/core'
1
2
  import type { ContinuationContext } from '../../../lib/continuation'
2
3
  import type { HudTitle } from '../../../lib/hud-title'
3
4
  import type { SnapContext } from '../../../lib/snapping-mode'
@@ -33,17 +34,18 @@ export function ItemHelper({
33
34
  extraHints = [],
34
35
  replacesPlace = false,
35
36
  }: ItemHelperProps) {
37
+ const i18n = usePascalI18n()
36
38
  return (
37
39
  <ContextualHelperPanel
38
40
  continuationContext={continuationContext}
39
41
  notice={notice}
40
42
  hints={[
41
43
  // Where a click does something else (joining a corner), that row says so instead.
42
- ...(replacesPlace ? [] : [{ keys: ['Left click'], label: 'Place' }]),
44
+ ...(replacesPlace ? [] : [{ keys: ['Left click'], label: i18n._({ id: 'editor.helpers.itemHelper.place', message: 'Place' }) }]),
43
45
  ...extraHints,
44
- { keys: ['R', 'T'], label: 'Rotate' },
45
- ...(showForce ? [{ keys: ['Alt'], label: 'Force place' }] : []),
46
- { keys: [showEsc ? 'Esc' : 'Right click'], label: 'Cancel' },
46
+ { keys: ['R', 'T'], label: i18n._({ id: 'editor.helpers.itemHelper.rotate', message: 'Rotate' }) },
47
+ ...(showForce ? [{ keys: ['Alt'], label: i18n._({ id: 'editor.helpers.itemHelper.forcePlace', message: 'Force place' }) }] : []),
48
+ { keys: [showEsc ? 'Esc' : 'Right click'], label: i18n._({ id: 'editor.helpers.itemHelper.cancel', message: 'Cancel' }) },
47
49
  ]}
48
50
  snapContext={snapContext}
49
51
  title={title}
@@ -1,3 +1,4 @@
1
+ import { usePascalI18n } from '@pascal-app/core'
1
2
  import type { ToolHint } from '@pascal-app/core'
2
3
  import type { ContinuationContext } from '../../../lib/continuation'
3
4
  import type { HudTitle } from '../../../lib/hud-title'
@@ -29,6 +30,7 @@ export function RegisteredToolHelper({
29
30
  title?: HudTitle | null
30
31
  notice?: string | null
31
32
  }) {
33
+ const i18n = usePascalI18n()
32
34
  const visible = useVisibleToolHints(hints)
33
35
  if (visible.length === 0 && !snapContext && !continuationContext && !notice) return null
34
36
  // Hints carrying a live-state `chip` render as mode chips next to the
@@ -44,7 +46,7 @@ export function RegisteredToolHelper({
44
46
  const isBypassHint = hint.key === 'Shift'
45
47
  return {
46
48
  keys: [hint.key],
47
- label: shiftPressed && isBypassHint ? 'Guided constraints bypassed' : hint.label,
49
+ label: shiftPressed && isBypassHint ? i18n._({ id: 'editor.helpers.registeredToolHelper.guidedConstraintsBypassed', message: 'Guided constraints bypassed' }) : hint.label,
48
50
  active: shiftPressed && isBypassHint,
49
51
  }
50
52
  })}
@@ -1,9 +1,10 @@
1
1
  'use client'
2
2
 
3
- import type { LevelNode } from '@pascal-app/core'
3
+ import type { LevelNode, MessageDescriptor } from '@pascal-app/core'
4
4
  import { useEffect, useState } from 'react'
5
5
  import type { LevelDuplicatePreset } from '@pascal-app/core/building'
6
- import { getLevelDisplayName } from '@pascal-app/core'
6
+ import { useLocale, usePascalI18n } from '@pascal-app/core'
7
+ import { localizedLevelName } from '../../i18n/level-name'
7
8
  import { cn } from '../../lib/utils'
8
9
  import {
9
10
  Dialog,
@@ -16,36 +17,31 @@ import {
16
17
 
17
18
  const DUPLICATE_PRESETS: Array<{
18
19
  id: LevelDuplicatePreset
19
- label: string
20
- description: string
20
+ label: MessageDescriptor
21
+ description: MessageDescriptor
21
22
  }> = [
22
23
  {
23
24
  id: 'everything',
24
- label: 'Everything',
25
- description: 'Structure, materials, furniture, and references.',
25
+ label: /* i18n */ { id: 'editor.levelDuplicateDialog.everything', message: 'Everything' },
26
+ description: /* i18n */ { id: 'editor.levelDuplicateDialog.structureMaterialsFurnitureAndReferences', message: 'Structure, materials, furniture, and references.' },
26
27
  },
27
28
  {
28
29
  id: 'structure',
29
- label: 'Structure only',
30
- description: 'Walls, slabs, roofs, stairs, windows, and doors without finishes.',
30
+ label: /* i18n */ { id: 'editor.levelDuplicateDialog.structureOnly', message: 'Structure only' },
31
+ description: /* i18n */ { id: 'editor.levelDuplicateDialog.wallsSlabsRoofsStairsWindowsAnd', message: 'Walls, slabs, roofs, stairs, windows, and doors without finishes.' },
31
32
  },
32
33
  {
33
34
  id: 'structure-materials',
34
- label: 'Structure + materials',
35
- description: 'Structure with the current material and finish assignments.',
35
+ label: /* i18n */ { id: 'editor.levelDuplicateDialog.structureMaterials', message: 'Structure + materials' },
36
+ description: /* i18n */ { id: 'editor.levelDuplicateDialog.structureWithTheCurrentMaterialAnd', message: 'Structure with the current material and finish assignments.' },
36
37
  },
37
38
  {
38
39
  id: 'structure-furniture',
39
- label: 'Structure + furniture',
40
- description: 'Structure, finishes, and placed items, without guide references.',
40
+ label: /* i18n */ { id: 'editor.levelDuplicateDialog.structureFurniture', message: 'Structure + furniture' },
41
+ description: /* i18n */ { id: 'editor.levelDuplicateDialog.structureFinishesAndPlacedItemsWithout', message: 'Structure, finishes, and placed items, without guide references.' },
41
42
  },
42
43
  ]
43
44
 
44
- function getLevelLabel(level: LevelNode | null) {
45
- if (!level) return 'this level'
46
- return getLevelDisplayName(level)
47
- }
48
-
49
45
  export function LevelDuplicateDialog({
50
46
  open,
51
47
  level,
@@ -57,6 +53,8 @@ export function LevelDuplicateDialog({
57
53
  onConfirm: (preset: LevelDuplicatePreset) => void
58
54
  onOpenChange: (open: boolean) => void
59
55
  }) {
56
+ const i18n = usePascalI18n()
57
+ const locale = useLocale()
60
58
  const [preset, setPreset] = useState<LevelDuplicatePreset>('everything')
61
59
 
62
60
  useEffect(() => {
@@ -69,8 +67,19 @@ export function LevelDuplicateDialog({
69
67
  <Dialog onOpenChange={onOpenChange} open={open}>
70
68
  <DialogContent className="sm:max-w-md" showCloseButton={false}>
71
69
  <DialogHeader>
72
- <DialogTitle>Duplicate Level</DialogTitle>
73
- <DialogDescription>Choose what to copy from {getLevelLabel(level)}.</DialogDescription>
70
+ <DialogTitle>{i18n._({ id: 'editor.levelDuplicateDialog.duplicateLevel', message: 'Duplicate Level' })}</DialogTitle>
71
+ <DialogDescription>
72
+ {level
73
+ ? i18n._({
74
+ id: 'editor.levelDuplicateDialog.chooseWhatToCopyFromLevel',
75
+ message: 'Choose what to copy from {levelName}.',
76
+ values: { levelName: localizedLevelName(level, i18n, locale) },
77
+ })
78
+ : i18n._({
79
+ id: 'editor.levelDuplicateDialog.chooseWhatToCopyFromThisLevel',
80
+ message: 'Choose what to copy from this level.',
81
+ })}
82
+ </DialogDescription>
74
83
  </DialogHeader>
75
84
 
76
85
  <div className="grid gap-2">
@@ -86,8 +95,8 @@ export function LevelDuplicateDialog({
86
95
  onClick={() => setPreset(option.id)}
87
96
  type="button"
88
97
  >
89
- <div className="font-medium text-sm">{option.label}</div>
90
- <div className="mt-1 text-muted-foreground text-xs">{option.description}</div>
98
+ <div className="font-medium text-sm">{i18n._(option.label)}</div>
99
+ <div className="mt-1 text-muted-foreground text-xs">{i18n._(option.description)}</div>
91
100
  </button>
92
101
  ))}
93
102
  </div>
@@ -98,14 +107,14 @@ export function LevelDuplicateDialog({
98
107
  onClick={() => onOpenChange(false)}
99
108
  type="button"
100
109
  >
101
- Cancel
110
+ {i18n._({ id: 'editor.levelDuplicateDialog.cancel', message: 'Cancel' })}
102
111
  </button>
103
112
  <button
104
113
  className="cursor-pointer rounded-md bg-primary px-4 py-2 text-primary-foreground text-sm transition-opacity hover:opacity-90"
105
114
  onClick={() => onConfirm(preset)}
106
115
  type="button"
107
116
  >
108
- Duplicate
117
+ {i18n._({ id: 'editor.levelDuplicateDialog.duplicate', message: 'Duplicate' })}
109
118
  </button>
110
119
  </DialogFooter>
111
120
  </DialogContent>