@pascal-app/editor 1.0.5 → 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 +20 -17
  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
@@ -6,6 +6,7 @@
6
6
  import {
7
7
  getWallCurveFrameAt,
8
8
  getWallCurveLength,
9
+ getWallFaceOffsets,
9
10
  isCurvedWall,
10
11
  type WallNode,
11
12
  } from '@pascal-app/core'
@@ -124,6 +125,142 @@ export function findWallSnapTarget(
124
125
  return bestTarget
125
126
  }
126
127
 
128
+ // A drafted segment closer than this to a wall's direction (sine of the angle
129
+ // between them) runs alongside it: the wall topology stacks such walls rather
130
+ // than T-joining them, so the body snap leaves them alone. Two walls meeting
131
+ // closer than this aren't a corner either.
132
+ const WALL_BODY_SNAP_MIN_SINE = 0.5
133
+
134
+ /** A straight wall's reference line as a frame, with the faces its body spans. */
135
+ type WallBodyFrame = {
136
+ wall: WallNode
137
+ length: number
138
+ ux: number
139
+ uz: number
140
+ faces: { a: number; b: number }
141
+ /** Offset along the wall's left normal, the side its face offsets are measured on. */
142
+ across: (q: WallPlanPoint) => number
143
+ /** Station along the reference line from `start`. */
144
+ along: (q: WallPlanPoint) => number
145
+ }
146
+
147
+ function wallBodyFrame(wall: WallNode): WallBodyFrame | null {
148
+ if (isCurvedWall(wall)) return null
149
+ const [x1, z1] = wall.start
150
+ const length = Math.hypot(wall.end[0] - x1, wall.end[1] - z1)
151
+ if (length < 1e-9) return null
152
+ const ux = (wall.end[0] - x1) / length
153
+ const uz = (wall.end[1] - z1) / length
154
+ return {
155
+ wall,
156
+ length,
157
+ ux,
158
+ uz,
159
+ faces: getWallFaceOffsets(wall),
160
+ across: (q) => (q[1] - z1) * ux - (q[0] - x1) * uz,
161
+ along: (q) => (q[0] - x1) * ux + (q[1] - z1) * uz,
162
+ }
163
+ }
164
+
165
+ /**
166
+ * The corner a join at `target` on `host`'s reference line falls into: where
167
+ * `host` meets another straight wall, at an L or a T, with `target` inside that
168
+ * wall's drawn body. A T-join there would split `host` into a stub buried in
169
+ * the corner block, so the end joins the junction instead. A shared end comes
170
+ * back exactly; walls meeting at a shallow angle aren't a corner.
171
+ */
172
+ function cornerJunctionAt(
173
+ target: WallPlanPoint,
174
+ host: WallBodyFrame,
175
+ frames: WallBodyFrame[],
176
+ ): WallPlanPoint | null {
177
+ for (const other of frames) {
178
+ if (other === host) continue
179
+ if (Math.abs(host.ux * other.uz - host.uz * other.ux) < WALL_BODY_SNAP_MIN_SINE) continue
180
+ const offset = other.across(target)
181
+ if (offset < other.faces.b || offset > other.faces.a) continue
182
+ const ends = [host.wall.start, host.wall.end, other.wall.start, other.wall.end]
183
+ const crossing = segmentIntersection(ends[0]!, ends[1]!, ends[2]!, ends[3]!)
184
+ if (!crossing) continue
185
+ const sharedEnd = ends.find((end) => distanceSquared(end, crossing) <= 1e-12)
186
+ return sharedEnd ? [sharedEnd[0], sharedEnd[1]] : crossing
187
+ }
188
+ return null
189
+ }
190
+
191
+ /**
192
+ * Where a drawn wall end joins a wall it is aimed into: the point on a straight
193
+ * wall's reference line (`start` → `end`, the line the wall topology T-joins a
194
+ * partition end on, whatever the wall's justification) for an end inside that
195
+ * wall's drawn body or within `margin` of either face.
196
+ *
197
+ * `point` is the raw cursor and `placed` its grid / angle placement; either one
198
+ * inside a body captures, so the grid can't leave an end a fraction short of a
199
+ * face. Given the drafted segment's `start`, the end slides along start →
200
+ * `placed` onto the line, so a square or angle-locked segment keeps its
201
+ * direction, and a wall the segment runs alongside (or starts on) is skipped.
202
+ * Without a start the end drops square onto the line, keeping its position
203
+ * along the wall. A join that lands inside a corner block, where the wall
204
+ * meets another at an L or a T, takes the junction itself (`cornerJunctionAt`).
205
+ * Curved walls are left to the edge snap (`findWallSnapTarget`).
206
+ */
207
+ export function findWallBodySnapTarget(
208
+ point: WallPlanPoint,
209
+ walls: WallNode[],
210
+ options?: {
211
+ ignoreWallIds?: string[]
212
+ placed?: WallPlanPoint
213
+ start?: WallPlanPoint
214
+ margin?: number
215
+ },
216
+ ): WallPlanPoint | null {
217
+ const ignored = new Set(options?.ignoreWallIds ?? [])
218
+ const placed = options?.placed ?? point
219
+ const start = options?.start
220
+ const margin = options?.margin ?? WALL_CONNECT_SNAP_RADIUS
221
+ const frames = walls.flatMap((wall) => {
222
+ const frame = ignored.has(wall.id) ? null : wallBodyFrame(wall)
223
+ return frame ? [frame] : []
224
+ })
225
+ let best: WallPlanPoint | null = null
226
+ let bestOutside = Number.POSITIVE_INFINITY
227
+
228
+ for (const frame of frames) {
229
+ const { length, ux, uz, faces, across, along } = frame
230
+ const [x1, z1] = frame.wall.start
231
+ const onWall = (q: WallPlanPoint) => along(q) > 0 && along(q) < length
232
+ const outsideBody = (q: WallPlanPoint) =>
233
+ onWall(q) ? Math.max(0, faces.b - across(q), across(q) - faces.a) : Number.POSITIVE_INFINITY
234
+ const outside = Math.min(outsideBody(point), outsideBody(placed))
235
+ if (outside > margin || outside >= bestOutside) continue
236
+
237
+ let target: WallPlanPoint | null
238
+ if (Math.abs(across(point)) <= 1e-9 && onWall(point)) {
239
+ // Already on the line: a joined end snapped again (the plan view hands
240
+ // the 3D tool its resolved point) stays put.
241
+ target = [point[0], point[1]]
242
+ } else if (start) {
243
+ const dx = placed[0] - start[0]
244
+ const dz = placed[1] - start[1]
245
+ const run = Math.hypot(dx, dz)
246
+ if (run < 1e-9 || Math.abs(dx * uz - dz * ux) / run < WALL_BODY_SNAP_MIN_SINE) continue
247
+ const startAcross = across(start)
248
+ const t = startAcross / (startAcross - across(placed))
249
+ if (!(t > 1e-6)) continue
250
+ target = [start[0] + dx * t, start[1] + dz * t]
251
+ if (!onWall(target)) continue
252
+ } else {
253
+ const station = along(placed)
254
+ if (!(station > 0 && station < length)) continue
255
+ target = [x1 + ux * station, z1 + uz * station]
256
+ }
257
+ best = cornerJunctionAt(target, frame, frames) ?? target
258
+ bestOutside = outside
259
+ }
260
+
261
+ return best
262
+ }
263
+
127
264
  /**
128
265
  * Wall ids that contain an already-resolved snap point.
129
266
  *
@@ -184,9 +184,9 @@ export const ZoneTool: React.FC = () => {
184
184
  updateLines()
185
185
  }
186
186
 
187
- // World-grid snap projected into building-local; rotated buildings
188
- // used to pull the snap off the visible grid lines. Grid quantize only
189
- // in grid mode; off / lines / angles leave the raw cursor.
187
+ // Snap on the building's own grid lattice, the one its walls are drafted
188
+ // on (square to a turned house). Grid quantize only in grid mode; off /
189
+ // lines / angles leave the raw cursor.
190
190
  const gridPointOf = (event: GridEvent): [number, number] =>
191
191
  isGridSnapActive()
192
192
  ? snapWorldXZForActiveBuilding(
@@ -44,6 +44,8 @@ export const ActionButton = React.forwardRef<HTMLButtonElement, ActionButtonProp
44
44
  // These are toggles, not commands: the armed/unarmed state has to be
45
45
  // readable without decoding the active-state styling.
46
46
  aria-pressed={isActive}
47
+ // Icon-only: the tooltip text is the button's name.
48
+ aria-label={label}
47
49
  className={cn('relative h-11 w-11 transition-all', className)}
48
50
  onClick={(event) => {
49
51
  triggerSFX('sfx:menu-click')
@@ -1,6 +1,6 @@
1
1
  'use client'
2
2
 
3
- import { emitter } from '@pascal-app/core'
3
+ import { emitter, usePascalI18n } from '@pascal-app/core'
4
4
  import Image from 'next/image'
5
5
  import useEditor from '../../../store/use-editor'
6
6
  import { visibleScene } from '../../../store/view-layout'
@@ -8,6 +8,7 @@ import { ActionButton } from './action-button'
8
8
  import { FollowPascalToggle } from './follow-pascal-toggle'
9
9
 
10
10
  export function CameraActions({ hideOrbit = false }: { hideOrbit?: boolean }) {
11
+ const i18n = usePascalI18n()
11
12
  // Orbit stays useful in 2D-only (it spins the synced floorplan view), but
12
13
  // top view only tilts the hidden 3D camera — pointless without the canvas.
13
14
  const is2dOnly = useEditor((s) => visibleScene(s) === '2d')
@@ -31,13 +32,13 @@ export function CameraActions({ hideOrbit = false }: { hideOrbit?: boolean }) {
31
32
  {/* Orbit CCW */}
32
33
  <ActionButton
33
34
  className="group hover:bg-black/4"
34
- label="Orbit Left"
35
+ label={i18n._({ id: 'editor.actionMenu.cameraActions.orbitLeft', message: 'Orbit Left' })}
35
36
  onClick={orbitCCW}
36
37
  size="icon"
37
38
  variant="ghost"
38
39
  >
39
40
  <Image
40
- alt="Orbit Left"
41
+ alt={i18n._({ id: 'editor.actionMenu.cameraActions.orbitLeft', message: 'Orbit Left' })}
41
42
  className="h-[28px] w-[28px] -scale-x-100 object-contain opacity-70 transition-opacity group-hover:opacity-100"
42
43
  height={28}
43
44
  src="/icons/rotate.webp"
@@ -48,13 +49,13 @@ export function CameraActions({ hideOrbit = false }: { hideOrbit?: boolean }) {
48
49
  {/* Orbit CW */}
49
50
  <ActionButton
50
51
  className="group hover:bg-black/4"
51
- label="Orbit Right"
52
+ label={i18n._({ id: 'editor.actionMenu.cameraActions.orbitRight', message: 'Orbit Right' })}
52
53
  onClick={orbitCW}
53
54
  size="icon"
54
55
  variant="ghost"
55
56
  >
56
57
  <Image
57
- alt="Orbit Right"
58
+ alt={i18n._({ id: 'editor.actionMenu.cameraActions.orbitRight', message: 'Orbit Right' })}
58
59
  className="h-[28px] w-[28px] object-contain opacity-70 transition-opacity group-hover:opacity-100"
59
60
  height={28}
60
61
  src="/icons/rotate.webp"
@@ -71,13 +72,13 @@ export function CameraActions({ hideOrbit = false }: { hideOrbit?: boolean }) {
71
72
  {!is2dOnly && (
72
73
  <ActionButton
73
74
  className="group hover:bg-black/4"
74
- label="Top View"
75
+ label={i18n._({ id: 'editor.actionMenu.cameraActions.topView', message: 'Top View' })}
75
76
  onClick={goToTopView}
76
77
  size="icon"
77
78
  variant="ghost"
78
79
  >
79
80
  <Image
80
- alt="Top View"
81
+ alt={i18n._({ id: 'editor.actionMenu.cameraActions.topView', message: 'Top View' })}
81
82
  className="h-[28px] w-[28px] object-contain opacity-70 transition-opacity group-hover:opacity-100"
82
83
  height={28}
83
84
  src="/icons/topview.webp"
@@ -1,5 +1,8 @@
1
1
  'use client'
2
2
 
3
+ import { usePascalI18n } from '@pascal-app/core'
4
+ import type { MessageDescriptor } from '@pascal-app/core'
5
+
3
6
  import { Icon } from '@iconify/react'
4
7
  import { type LucideIcon, Trash2 } from 'lucide-react'
5
8
  import Image from 'next/image'
@@ -17,7 +20,7 @@ type ControlConfig = {
17
20
  icon?: LucideIcon
18
21
  iconifyIcon?: string
19
22
  imageSrc?: string
20
- label: string
23
+ label: MessageDescriptor
21
24
  shortcut?: string
22
25
  color: string
23
26
  activeColor: string
@@ -28,7 +31,7 @@ const controls: ControlConfig[] = [
28
31
  {
29
32
  id: 'select',
30
33
  imageSrc: '/icons/select.webp',
31
- label: 'Select',
34
+ label: /* i18n */ { id: 'editor.actionMenu.controlModes.select', message: 'Select' },
32
35
  shortcut: 'V',
33
36
  color: 'hover:bg-blue-500/20 hover:text-blue-600 dark:hover:text-blue-400',
34
37
  activeColor: 'bg-blue-500/20 text-blue-600 dark:text-blue-400 dark:hover:text-accent-foreground',
@@ -36,7 +39,7 @@ const controls: ControlConfig[] = [
36
39
  {
37
40
  id: 'delete',
38
41
  icon: Trash2,
39
- label: 'Delete',
42
+ label: /* i18n */ { id: 'editor.actionMenu.controlModes.delete', message: 'Delete' },
40
43
  shortcut: 'X',
41
44
  color: 'hover:bg-red-500/20 hover:text-red-600 dark:hover:text-red-400',
42
45
  activeColor: 'bg-red-500/20 text-red-600 dark:text-red-400 dark:hover:text-accent-foreground',
@@ -44,6 +47,7 @@ const controls: ControlConfig[] = [
44
47
  ]
45
48
 
46
49
  export function ControlModes() {
50
+ const i18n = usePascalI18n()
47
51
  const mode = useEditor((state) => state.mode)
48
52
  const selectionTool = useEditor((state) => state.floorplanSelectionTool)
49
53
  const armToolMode = useEditor((state) => state.armToolMode)
@@ -94,7 +98,7 @@ export function ControlModes() {
94
98
  // tour at this button. Nothing here reads it.
95
99
  data-guide-target={c.id === 'select' ? 'mode-select' : undefined}
96
100
  isActive={isActive}
97
- label={c.label}
101
+ label={i18n._(c.label)}
98
102
  onClick={() => handleClick(c.id)}
99
103
  shortcut={c.shortcut}
100
104
  size="icon"
@@ -102,7 +106,7 @@ export function ControlModes() {
102
106
  >
103
107
  {c.imageSrc ? (
104
108
  <Image
105
- alt={c.label}
109
+ alt={i18n._(c.label)}
106
110
  className={cn(
107
111
  'h-[28px] w-[28px] object-contain transition-[opacity,filter] duration-200',
108
112
  isActive
@@ -1,5 +1,6 @@
1
1
  'use client'
2
2
 
3
+ import { usePascalI18n } from '@pascal-app/core'
3
4
  import { AnimatePresence, motion } from 'motion/react'
4
5
  import { useReducedMotion } from '../../../hooks/use-reduced-motion'
5
6
  import useFollowPascal from '../../../store/use-follow-pascal'
@@ -9,6 +10,7 @@ import useFollowPascal from '../../../store/use-follow-pascal'
9
10
  * following. It comes up from where the toolbar is (origin-aware) and goes the same way.
10
11
  */
11
12
  export function FollowPascalPill() {
13
+ const i18n = usePascalI18n()
12
14
  const shows = useFollowPascal((state) => state.on && state.paused && state.building)
13
15
  const resume = useFollowPascal((state) => state.resume)
14
16
  const reduced = useReducedMotion()
@@ -24,7 +26,7 @@ export function FollowPascalPill() {
24
26
  transition={reduced ? { duration: 0.12 } : { type: 'spring', bounce: 0, duration: 0.3 }}
25
27
  type="button"
26
28
  >
27
- Resume following
29
+ {i18n._({ id: 'editor.actionMenu.followPascalPill.resumeFollowing', message: 'Resume following' })}
28
30
  </motion.button>
29
31
  ) : null}
30
32
  </AnimatePresence>
@@ -1,5 +1,6 @@
1
1
  'use client'
2
2
 
3
+ import { usePascalI18n } from '@pascal-app/core'
3
4
  import { Video } from 'lucide-react'
4
5
  import { cn } from '../../../lib/utils'
5
6
  import useFollowPascal from '../../../store/use-follow-pascal'
@@ -13,6 +14,7 @@ export function FollowPascalToggle({
13
14
  className?: string
14
15
  tooltipClassName?: string
15
16
  }) {
17
+ const i18n = usePascalI18n()
16
18
  const on = useFollowPascal((state) => state.on)
17
19
  const setOn = useFollowPascal((state) => state.setOn)
18
20
  return (
@@ -24,7 +26,11 @@ export function FollowPascalToggle({
24
26
  : 'hover:bg-black/4 hover:text-violet-600 dark:hover:text-violet-300',
25
27
  className,
26
28
  )}
27
- label={`Follow Pascal: ${on ? 'On' : 'Off'}`}
29
+ label={
30
+ on
31
+ ? i18n._({ id: 'editor.actionMenu.followPascalToggle.on', message: 'Follow Pascal: On' })
32
+ : i18n._({ id: 'editor.actionMenu.followPascalToggle.off', message: 'Follow Pascal: Off' })
33
+ }
28
34
  onClick={() => setOn(!on)}
29
35
  size="icon"
30
36
  tooltipClassName={tooltipClassName}
@@ -1,16 +1,17 @@
1
+ import type { MessageDescriptor } from '@pascal-app/core'
1
2
  import type { CatalogCategory } from './../../../store/use-editor'
2
3
 
3
4
  export type FurnishToolConfig = {
4
5
  id: 'item'
5
6
  iconSrc: string
6
- label: string
7
+ label: MessageDescriptor
7
8
  catalogCategory: CatalogCategory
8
9
  }
9
10
 
10
11
  export const furnishTools: FurnishToolConfig[] = [
11
- { id: 'item', iconSrc: '/icons/couch.webp', label: 'Furniture', catalogCategory: 'furniture' },
12
- { id: 'item', iconSrc: '/icons/appliance.webp', label: 'Appliance', catalogCategory: 'appliance' },
13
- { id: 'item', iconSrc: '/icons/kitchen.webp', label: 'Kitchen', catalogCategory: 'kitchen' },
14
- { id: 'item', iconSrc: '/icons/bathroom.webp', label: 'Bathroom', catalogCategory: 'bathroom' },
15
- { id: 'item', iconSrc: '/icons/tree.webp', label: 'Outdoor', catalogCategory: 'outdoor' },
12
+ { id: 'item', iconSrc: '/icons/couch.webp', label: /* i18n */ { id: 'editor.actionMenu.furnishTools.furniture', message: 'Furniture' }, catalogCategory: 'furniture' },
13
+ { id: 'item', iconSrc: '/icons/appliance.webp', label: /* i18n */ { id: 'editor.actionMenu.furnishTools.appliance', message: 'Appliance' }, catalogCategory: 'appliance' },
14
+ { id: 'item', iconSrc: '/icons/kitchen.webp', label: /* i18n */ { id: 'editor.actionMenu.furnishTools.kitchen', message: 'Kitchen' }, catalogCategory: 'kitchen' },
15
+ { id: 'item', iconSrc: '/icons/bathroom.webp', label: /* i18n */ { id: 'editor.actionMenu.furnishTools.bathroom', message: 'Bathroom' }, catalogCategory: 'bathroom' },
16
+ { id: 'item', iconSrc: '/icons/tree.webp', label: /* i18n */ { id: 'editor.actionMenu.furnishTools.outdoor', message: 'Outdoor' }, catalogCategory: 'outdoor' },
16
17
  ]
@@ -66,6 +66,7 @@ export function ActionMenu({
66
66
  <TooltipProvider>
67
67
  <FollowPascalPill />
68
68
  <motion.div
69
+ data-surface="tool-dock"
69
70
  className={cn(
70
71
  'z-50',
71
72
  isMobile
@@ -1,5 +1,8 @@
1
1
  'use client'
2
2
 
3
+ import type { MessageDescriptor } from '@pascal-app/core'
4
+
5
+ import { usePascalI18n } from '@pascal-app/core'
3
6
  import type {
4
7
  ConstructionDimensionChainMode,
5
8
  ConstructionDimensionMode,
@@ -29,40 +32,41 @@ import { ActionButton } from './action-button'
29
32
  import { useActionMenuPopupSide } from './placement'
30
33
 
31
34
  const measurementOptions = [
32
- { kind: 'distance', label: 'Distance', icon: Ruler },
33
- { kind: 'angle', label: 'Angle', icon: Triangle },
34
- { kind: 'area', label: 'Area', icon: Square },
35
- { kind: 'perimeter', label: 'Perimeter', icon: Waypoints },
36
- { kind: 'volume', label: 'Volume', icon: Box },
35
+ { kind: 'distance', label: /* i18n */ { id: 'editor.actionMenu.measurementControl.distance', message: 'Distance' }, icon: Ruler },
36
+ { kind: 'angle', label: /* i18n */ { id: 'editor.actionMenu.measurementControl.angle', message: 'Angle' }, icon: Triangle },
37
+ { kind: 'area', label: /* i18n */ { id: 'editor.actionMenu.measurementControl.area', message: 'Area' }, icon: Square },
38
+ { kind: 'perimeter', label: /* i18n */ { id: 'editor.actionMenu.measurementControl.perimeter', message: 'Perimeter' }, icon: Waypoints },
39
+ { kind: 'volume', label: /* i18n */ { id: 'editor.actionMenu.measurementControl.volume', message: 'Volume' }, icon: Box },
37
40
  ] as const satisfies readonly {
38
41
  kind: CreatableMeasurementKind
39
- label: string
42
+ label: MessageDescriptor
40
43
  icon: typeof Ruler
41
44
  }[]
42
45
 
43
46
  const measurementMenuOptions = [
44
- { kind: 'smart', label: 'Smart', icon: ScanSearch },
47
+ { kind: 'smart', label: /* i18n */ { id: 'editor.actionMenu.measurementControl.smart', message: 'Smart' }, icon: ScanSearch },
45
48
  ...measurementOptions,
46
49
  ] as const
47
50
 
48
51
  const constructionDimensionOptions = [
49
- { mode: 'linear', chainMode: 'point-to-point', label: 'Linear dimension', icon: Ruler },
50
- { mode: 'linear', chainMode: 'continuous', label: 'Continuous dimension', icon: Waypoints },
51
- { mode: 'radius', chainMode: 'point-to-point', label: 'Radius dimension', icon: CircleIcon },
52
- { mode: 'diameter', chainMode: 'point-to-point', label: 'Diameter dimension', icon: CircleIcon },
53
- { mode: 'center-mark', chainMode: 'point-to-point', label: 'Center mark', icon: Crosshair },
54
- { mode: 'chord', chainMode: 'point-to-point', label: 'Chord dimension', icon: Minus },
55
- { mode: 'arc-length', chainMode: 'point-to-point', label: 'Arc length', icon: CircleIcon },
56
- { mode: 'angular', chainMode: 'point-to-point', label: 'Angular dimension', icon: Triangle },
57
- { mode: 'coordinate', chainMode: 'continuous', label: 'Coordinate dimensions', icon: Grid2X2 },
52
+ { mode: 'linear', chainMode: 'point-to-point', label: /* i18n */ { id: 'editor.actionMenu.measurementControl.linearDimension', message: 'Linear dimension' }, icon: Ruler },
53
+ { mode: 'linear', chainMode: 'continuous', label: /* i18n */ { id: 'editor.actionMenu.measurementControl.continuousDimension', message: 'Continuous dimension' }, icon: Waypoints },
54
+ { mode: 'radius', chainMode: 'point-to-point', label: /* i18n */ { id: 'editor.actionMenu.measurementControl.radiusDimension', message: 'Radius dimension' }, icon: CircleIcon },
55
+ { mode: 'diameter', chainMode: 'point-to-point', label: /* i18n */ { id: 'editor.actionMenu.measurementControl.diameterDimension', message: 'Diameter dimension' }, icon: CircleIcon },
56
+ { mode: 'center-mark', chainMode: 'point-to-point', label: /* i18n */ { id: 'editor.actionMenu.measurementControl.centerMark', message: 'Center mark' }, icon: Crosshair },
57
+ { mode: 'chord', chainMode: 'point-to-point', label: /* i18n */ { id: 'editor.actionMenu.measurementControl.chordDimension', message: 'Chord dimension' }, icon: Minus },
58
+ { mode: 'arc-length', chainMode: 'point-to-point', label: /* i18n */ { id: 'editor.actionMenu.measurementControl.arcLength', message: 'Arc length' }, icon: CircleIcon },
59
+ { mode: 'angular', chainMode: 'point-to-point', label: /* i18n */ { id: 'editor.actionMenu.measurementControl.angularDimension', message: 'Angular dimension' }, icon: Triangle },
60
+ { mode: 'coordinate', chainMode: 'continuous', label: /* i18n */ { id: 'editor.actionMenu.measurementControl.coordinateDimensions', message: 'Coordinate dimensions' }, icon: Grid2X2 },
58
61
  ] as const satisfies readonly {
59
62
  mode: ConstructionDimensionMode
60
63
  chainMode: ConstructionDimensionChainMode
61
- label: string
64
+ label: MessageDescriptor
62
65
  icon: typeof Ruler
63
66
  }[]
64
67
 
65
68
  export function MeasurementControl() {
69
+ const i18n = usePascalI18n()
66
70
  const popupSide = useActionMenuPopupSide()
67
71
  const [isOpen, setIsOpen] = useState(false)
68
72
  const mode = useEditor((state) => state.mode)
@@ -98,11 +102,13 @@ export function MeasurementControl() {
98
102
  : isSmartActive
99
103
  ? ScanSearch
100
104
  : selectedOption.icon
101
- const selectedLabel = isConstructionDimensionActive
102
- ? (activeConstructionDimensionOption?.label ?? 'Linear dimension')
103
- : isSmartActive
104
- ? 'Smart'
105
- : selectedOption.label
105
+ const selectedLabel = i18n._(
106
+ isConstructionDimensionActive
107
+ ? (activeConstructionDimensionOption ?? constructionDimensionOptions[0]).label
108
+ : isSmartActive
109
+ ? measurementMenuOptions[0].label
110
+ : selectedOption.label,
111
+ )
106
112
 
107
113
  const activateMeasurement = (kind: CreatableMeasurementKind) => {
108
114
  setLastMeasurementKind(kind)
@@ -143,7 +149,7 @@ export function MeasurementControl() {
143
149
  <Popover onOpenChange={setIsOpen} open={isOpen}>
144
150
  <div className="flex items-center">
145
151
  <ActionButton
146
- aria-label={`Measure: ${selectedLabel}`}
152
+ aria-label={i18n._({ id: 'editor.actionMenu.measurementControl.measureSelectedlabel', message: 'Measure: {selectedLabel}', values: { selectedLabel } })}
147
153
  aria-pressed={isControlActive}
148
154
  className={cn(
149
155
  'rounded-r-none p-0 text-muted-foreground',
@@ -151,7 +157,7 @@ export function MeasurementControl() {
151
157
  ? 'bg-cyan-500/20 text-cyan-700 hover:bg-cyan-500/20 dark:text-cyan-400 dark:hover:text-accent-foreground'
152
158
  : 'hover:bg-cyan-500/15 hover:text-cyan-700 dark:hover:text-cyan-400',
153
159
  )}
154
- label={`Measure: ${selectedLabel}`}
160
+ label={i18n._({ id: 'editor.actionMenu.measurementControl.measureSelectedlabel', message: 'Measure: {selectedLabel}', values: { selectedLabel } })}
155
161
  onClick={handlePrimaryClick}
156
162
  shortcut="M"
157
163
  size="icon"
@@ -164,7 +170,7 @@ export function MeasurementControl() {
164
170
  <button
165
171
  aria-expanded={isOpen}
166
172
  aria-haspopup="menu"
167
- aria-label="Measurement options"
173
+ aria-label={i18n._({ id: 'editor.actionMenu.measurementControl.measurementOptions', message: 'Measurement options' })}
168
174
  className={cn(
169
175
  'flex h-11 w-6 items-center justify-center rounded-r-lg text-muted-foreground transition-colors',
170
176
  isOpen
@@ -187,7 +193,7 @@ export function MeasurementControl() {
187
193
  side={popupSide}
188
194
  sideOffset={14}
189
195
  >
190
- <div aria-label="Measurement type" className="space-y-1" role="menu">
196
+ <div aria-label={i18n._({ id: 'editor.actionMenu.measurementControl.measurementType', message: 'Measurement type' })} className="space-y-1" role="menu">
191
197
  {measurementMenuOptions.map((option) => {
192
198
  const OptionIcon = option.icon
193
199
  const isSmart = option.kind === 'smart'
@@ -213,7 +219,7 @@ export function MeasurementControl() {
213
219
  type="button"
214
220
  >
215
221
  <OptionIcon aria-hidden="true" className="h-4 w-4" />
216
- <span>{option.label}</span>
222
+ <span>{i18n._(option.label)}</span>
217
223
  {isSelected ? <Check aria-hidden="true" className="ml-auto h-4 w-4" /> : null}
218
224
  </button>
219
225
  )
@@ -223,7 +229,7 @@ export function MeasurementControl() {
223
229
  <>
224
230
  <div className="my-1.5 h-px bg-border/60" />
225
231
  <div className="px-2.5 pt-1 pb-0.5 font-semibold text-[10px] text-muted-foreground uppercase tracking-wider">
226
- Floor plan
232
+ {i18n._({ id: 'editor.actionMenu.measurementControl.floorPlan', message: 'Floor plan' })}
227
233
  </div>
228
234
 
229
235
  {constructionDimensionOptions.map((option) => {
@@ -248,7 +254,7 @@ export function MeasurementControl() {
248
254
  type="button"
249
255
  >
250
256
  <OptionIcon aria-hidden="true" className="h-4 w-4" />
251
- <span>{option.label}</span>
257
+ <span>{i18n._(option.label)}</span>
252
258
  {isSelected ? <Check aria-hidden="true" className="ml-auto h-4 w-4" /> : null}
253
259
  </button>
254
260
  )
@@ -1,6 +1,6 @@
1
1
  'use client'
2
2
 
3
- import { useHasPointAskHandler } from '@pascal-app/core'
3
+ import { useHasPointAskHandler, usePascalI18n } from '@pascal-app/core'
4
4
  import { canPoint, enterPointMode, leavePointMode } from '../../../lib/point-ask/session'
5
5
  import { cn } from '../../../lib/utils'
6
6
  import useEditor from '../../../store/use-editor'
@@ -14,6 +14,7 @@ import { ActionButton } from './action-button'
14
14
  * the 2D plan and the walkthrough it is disabled and says so (v1 works in the 3D view).
15
15
  */
16
16
  export function PointAskButton() {
17
+ const i18n = usePascalI18n()
17
18
  const available = useHasPointAskHandler()
18
19
  const active = usePointAsk((s) => s.active)
19
20
  const sceneVisible = useEditor((s) => isViewVisible(s, VIEW_3D))
@@ -31,7 +32,7 @@ export function PointAskButton() {
31
32
  data-guide-target="mode-point-ask"
32
33
  disabled={unavailable}
33
34
  isActive={active}
34
- label="Point and ask"
35
+ label={i18n._({ id: 'editor.actionMenu.pointAskButton.pointAndAsk', message: 'Point and ask' })}
35
36
  onClick={() => {
36
37
  if (active) leavePointMode()
37
38
  else if (canPoint()) enterPointMode(true)
@@ -39,7 +40,7 @@ export function PointAskButton() {
39
40
  shortcut="C"
40
41
  size="icon"
41
42
  tooltipContent={
42
- unavailable ? <p>Point and ask works in the 3D view</p> : <p>Point and ask (C)</p>
43
+ unavailable ? <p>{i18n._({ id: 'editor.actionMenu.pointAskButton.pointAndAskWorksInThe', message: 'Point and ask works in the 3D view' })}</p> : <p>{i18n._({ id: 'editor.actionMenu.pointAskButton.pointAndAskC', message: 'Point and ask (C)' })}</p>
43
44
  }
44
45
  variant="ghost"
45
46
  >
@@ -1,9 +1,10 @@
1
+ import type { MessageDescriptor } from '@pascal-app/core'
1
2
  import type { CatalogCategory, StructureTool } from '../../../store/use-editor'
2
3
 
3
4
  export type ToolConfig = {
4
5
  id: StructureTool
5
6
  iconSrc: string
6
- label: string
7
+ label: MessageDescriptor
7
8
  catalogCategory?: CatalogCategory
8
9
  }
9
10
 
@@ -12,26 +13,27 @@ export type ToolConfig = {
12
13
  // for cursor/floorplan indicators. Roof-mounted accessories are intentionally
13
14
  // absent — they're placed from the roof inspector's "Add element" section.
14
15
  export const tools: ToolConfig[] = [
15
- { id: 'wall', iconSrc: '/icons/wall.webp', label: 'Wall' },
16
- { id: 'door', iconSrc: '/icons/door.webp', label: 'Door' },
17
- { id: 'window', iconSrc: '/icons/window.webp', label: 'Window' },
18
- { id: 'stair', iconSrc: '/icons/stairs.webp', label: 'Stairs' },
19
- { id: 'roof', iconSrc: '/icons/roof.webp', label: 'Gable Roof' },
20
- { id: 'fence', iconSrc: '/icons/fence.webp', label: 'Fence' },
21
- { id: 'column', iconSrc: '/icons/column.webp', label: 'Column' },
22
- { id: 'elevator', iconSrc: '/icons/elevator.webp', label: 'Elevator' },
23
- { id: 'slab', iconSrc: '/icons/floor.webp', label: 'Slab' },
24
- { id: 'ceiling', iconSrc: '/icons/ceiling.webp', label: 'Ceiling' },
25
- { id: 'zone', iconSrc: '/icons/zone.webp', label: 'Zone' },
26
- { id: 'spawn', iconSrc: '/icons/spawn-point.webp', label: 'Spawn Point' },
27
- { id: 'shelf', iconSrc: '/icons/shelf.webp', label: 'Shelf' },
28
- { id: 'duct-segment', iconSrc: '/icons/duct.webp', label: 'Duct' },
29
- { id: 'duct-fitting', iconSrc: '/icons/duct-fitting.webp', label: 'Duct Fitting' },
30
- { id: 'duct-terminal', iconSrc: '/icons/registers.webp', label: 'Register' },
31
- { id: 'hvac-equipment', iconSrc: '/icons/HVAC.webp', label: 'HVAC Unit' },
32
- { id: 'pipe-segment', iconSrc: '/icons/dwv-pipes.webp', label: 'DWV Pipe' },
33
- { id: 'pipe-trap', iconSrc: '/icons/dwv-pipes.webp', label: 'Trap' },
34
- { id: 'pipe-fitting', iconSrc: '/icons/duct-fitting.webp', label: 'Pipe Fitting' },
35
- { id: 'lineset', iconSrc: '/icons/lineset.webp', label: 'Lineset' },
36
- { id: 'liquid-line', iconSrc: '/icons/lineset.webp', label: 'Liquid Line' },
16
+ { id: 'wall', iconSrc: '/icons/wall.webp', label: /* i18n */ { id: 'editor.actionMenu.structureTools.wall', message: 'Wall' } },
17
+ { id: 'door', iconSrc: '/icons/door.webp', label: /* i18n */ { id: 'editor.actionMenu.structureTools.door', message: 'Door' } },
18
+ { id: 'window', iconSrc: '/icons/window.webp', label: /* i18n */ { id: 'editor.actionMenu.structureTools.window', message: 'Window' } },
19
+ { id: 'stair', iconSrc: '/icons/stairs.webp', label: /* i18n */ { id: 'editor.actionMenu.structureTools.stairs', message: 'Stairs' } },
20
+ { id: 'roof', iconSrc: '/icons/roof.webp', label: /* i18n */ { id: 'editor.actionMenu.structureTools.gableRoof', message: 'Gable Roof' } },
21
+ { id: 'fence', iconSrc: '/icons/fence.webp', label: /* i18n */ { id: 'editor.actionMenu.structureTools.fence', message: 'Fence' } },
22
+ { id: 'column', iconSrc: '/icons/column.webp', label: /* i18n */ { id: 'editor.actionMenu.structureTools.column', message: 'Column' } },
23
+ { id: 'beam', iconSrc: '/icons/beam.svg', label: /* i18n */ { id: 'editor.actionMenu.structureTools.beam', message: 'Beam' } },
24
+ { id: 'elevator', iconSrc: '/icons/elevator.webp', label: /* i18n */ { id: 'editor.actionMenu.structureTools.elevator', message: 'Elevator' } },
25
+ { id: 'slab', iconSrc: '/icons/floor.webp', label: /* i18n */ { id: 'editor.actionMenu.structureTools.slab', message: 'Slab' } },
26
+ { id: 'ceiling', iconSrc: '/icons/ceiling.webp', label: /* i18n */ { id: 'editor.actionMenu.structureTools.ceiling', message: 'Ceiling' } },
27
+ { id: 'zone', iconSrc: '/icons/zone.webp', label: /* i18n */ { id: 'editor.actionMenu.structureTools.zone', message: 'Zone' } },
28
+ { id: 'spawn', iconSrc: '/icons/spawn-point.webp', label: /* i18n */ { id: 'editor.actionMenu.structureTools.spawnPoint', message: 'Spawn Point' } },
29
+ { id: 'shelf', iconSrc: '/icons/shelf.webp', label: /* i18n */ { id: 'editor.actionMenu.structureTools.shelf', message: 'Shelf' } },
30
+ { id: 'duct-segment', iconSrc: '/icons/duct.webp', label: /* i18n */ { id: 'editor.actionMenu.structureTools.duct', message: 'Duct' } },
31
+ { id: 'duct-fitting', iconSrc: '/icons/duct-fitting.webp', label: /* i18n */ { id: 'editor.actionMenu.structureTools.ductFitting', message: 'Duct Fitting' } },
32
+ { id: 'duct-terminal', iconSrc: '/icons/registers.webp', label: /* i18n */ { id: 'editor.actionMenu.structureTools.register', message: 'Register' } },
33
+ { id: 'hvac-equipment', iconSrc: '/icons/HVAC.webp', label: /* i18n */ { id: 'editor.actionMenu.structureTools.hvacUnit', message: 'HVAC Unit' } },
34
+ { id: 'pipe-segment', iconSrc: '/icons/dwv-pipes.webp', label: /* i18n */ { id: 'editor.actionMenu.structureTools.dwvPipe', message: 'DWV Pipe' } },
35
+ { id: 'pipe-trap', iconSrc: '/icons/dwv-pipes.webp', label: /* i18n */ { id: 'editor.actionMenu.structureTools.trap', message: 'Trap' } },
36
+ { id: 'pipe-fitting', iconSrc: '/icons/duct-fitting.webp', label: /* i18n */ { id: 'editor.actionMenu.structureTools.pipeFitting', message: 'Pipe Fitting' } },
37
+ { id: 'lineset', iconSrc: '/icons/lineset.webp', label: /* i18n */ { id: 'editor.actionMenu.structureTools.lineset', message: 'Lineset' } },
38
+ { id: 'liquid-line', iconSrc: '/icons/lineset.webp', label: /* i18n */ { id: 'editor.actionMenu.structureTools.liquidLine', message: 'Liquid Line' } },
37
39
  ]