@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
@@ -1,3 +1,4 @@
1
+ import { formatLength, type MessageDescriptor, usePascalI18n } from '@pascal-app/core'
1
2
  import { Icon } from '@iconify/react'
2
3
  import type { ToolHint } from '@pascal-app/core'
3
4
  import { Fragment, useEffect, useId, useState, useSyncExternalStore } from 'react'
@@ -6,6 +7,7 @@ import {
6
7
  type ContinuationContext,
7
8
  } from '../../../lib/continuation'
8
9
  import type { ContextualShortcutHint } from '../../../lib/contextual-help'
10
+ import { hintText, nodeLabel, toolHintChipLabel, toolHintLabel } from '../../../i18n/node-messages'
9
11
  import type { HudTitle } from '../../../lib/hud-title'
10
12
  import { hasActivePaintMaterial } from '../../../lib/material-paint'
11
13
  import { usePaintRegionMode } from '../../../lib/paint-region-mode'
@@ -169,10 +171,10 @@ const SNAPPING_MODE_ICONS = {
169
171
  } as const
170
172
 
171
173
  const SNAPPING_MODE_LABELS = {
172
- grid: 'Grid',
173
- lines: 'Lines',
174
- angles: 'Angles',
175
- off: 'Off',
174
+ grid: /* i18n */ { id: 'editor.helpers.contextualHelperPanel.snappingGrid', message: 'Snapping: Grid' },
175
+ lines: /* i18n */ { id: 'editor.helpers.contextualHelperPanel.snappingLines', message: 'Snapping: Lines' },
176
+ angles: /* i18n */ { id: 'editor.helpers.contextualHelperPanel.snappingAngles', message: 'Snapping: Angles' },
177
+ off: /* i18n */ { id: 'editor.helpers.contextualHelperPanel.snappingOff', message: 'Snapping: Off' },
176
178
  } as const
177
179
 
178
180
  const GRID_SNAP_STEPS: GridSnapStep[] = [0.5, 0.25, 0.1, 0.05]
@@ -185,6 +187,7 @@ function nextGridSnapStep(step: GridSnapStep): GridSnapStep {
185
187
  // The active interaction's snapping controls, scoped to its context (wall / item
186
188
  // / polygon) so each action shows only the modes that make sense for it.
187
189
  function SnappingChips({ context }: { context: SnapContext }) {
190
+ const i18n = usePascalI18n()
188
191
  const snappingMode = useEditor((s) => s.snappingModeByContext[context])
189
192
  const setSnappingMode = useEditor((s) => s.setSnappingMode)
190
193
  const gridSnapStep = useEditor((s) => s.gridSnapStep)
@@ -195,28 +198,36 @@ function SnappingChips({ context }: { context: SnapContext }) {
195
198
  return (
196
199
  <>
197
200
  <ChipRow
198
- ariaLabel={`Snapping: ${SNAPPING_MODE_LABELS[snappingMode]}`}
201
+ ariaLabel={i18n._(SNAPPING_MODE_LABELS[snappingMode])}
199
202
  guideTarget="snap-mode"
200
203
  icon={SNAPPING_MODE_ICONS[snappingMode]}
201
- label={`Snapping: ${SNAPPING_MODE_LABELS[snappingMode]}`}
204
+ label={i18n._(SNAPPING_MODE_LABELS[snappingMode])}
202
205
  onClick={() => {
203
206
  setSnappingMode(context, cycleSnappingModeIn(context, snappingMode))
204
207
  sfxEmitter.emit('sfx:grid-snap')
205
208
  }}
206
209
  shortcut="Shift"
207
- tooltip="Snapping mode — click or press Shift to cycle"
210
+ tooltip={i18n._({ id: 'editor.helpers.contextualHelperPanel.snappingModeClickOrPressShift', message: 'Snapping mode — click or press Shift to cycle' })}
208
211
  />
209
212
  {gridActive ? (
210
213
  <ChipRow
211
- ariaLabel={`Grid step: ${gridSnapStep.toFixed(2)} m`}
214
+ ariaLabel={i18n._({
215
+ id: 'editor.helpers.contextualHelperPanel.gridStepLength',
216
+ message: 'Grid step: {length}',
217
+ values: { length: formatLength(gridSnapStep, 'm', { locale: i18n.getLocale(), min: 2, max: 2 }) },
218
+ })}
212
219
  guideTarget="snap-grid-step"
213
- label={`Grid: ${gridSnapStep.toFixed(2)} m`}
220
+ label={i18n._({
221
+ id: 'editor.helpers.contextualHelperPanel.gridLength',
222
+ message: 'Grid: {length}',
223
+ values: { length: formatLength(gridSnapStep, 'm', { locale: i18n.getLocale(), min: 2, max: 2 }) },
224
+ })}
214
225
  onClick={() => {
215
226
  setGridSnapStep(nextGridSnapStep(gridSnapStep))
216
227
  sfxEmitter.emit('sfx:grid-snap')
217
228
  }}
218
229
  shortcut="Ctrl"
219
- tooltip="Grid step — click or tap Ctrl to cycle"
230
+ tooltip={i18n._({ id: 'editor.helpers.contextualHelperPanel.gridStepClickOrTapCtrl', message: 'Grid step — click or tap Ctrl to cycle' })}
220
231
  />
221
232
  ) : null}
222
233
  </>
@@ -229,8 +240,12 @@ function SnappingChips({ context }: { context: SnapContext }) {
229
240
  // the tool itself) cycles it.
230
241
  function ToolHintChipRow({ hint }: { hint: ToolHint & { chip: NonNullable<ToolHint['chip']> } }) {
231
242
  const { chip } = hint
243
+ const i18n = usePascalI18n()
232
244
  const value = useSyncExternalStore(chip.subscribe, chip.value, chip.value)
233
- const label = chip.labels[value] ?? hint.label
245
+ const english = chip.labels[value]
246
+ const label = english
247
+ ? toolHintChipLabel(value, english, i18n)
248
+ : toolHintLabel(hint.label, i18n)
234
249
  return (
235
250
  <ChipRow
236
251
  ariaLabel={label}
@@ -238,31 +253,38 @@ function ToolHintChipRow({ hint }: { hint: ToolHint & { chip: NonNullable<ToolHi
238
253
  label={label}
239
254
  onClick={chip.cycle}
240
255
  shortcut={hint.key}
241
- tooltip={chip.tooltip}
256
+ tooltip={chip.tooltip ? toolHintLabel(chip.tooltip, i18n) : undefined}
242
257
  />
243
258
  )
244
259
  }
245
260
 
246
261
  function ContinuationChip({ context }: { context: ContinuationContext }) {
262
+ const i18n = usePascalI18n()
247
263
  const mode = useEditor((s) => s.getContinuation(context))
248
264
  const cycleContinuation = useEditor((s) => s.cycleContinuation)
249
265
  const profile = CONTINUATION_PROFILES[context]
250
- const label = profile.labels[mode] ?? mode
266
+ const descriptor = profile.labels[mode]
267
+ const label = descriptor ? i18n._(descriptor) : mode
251
268
  const icon = profile.icons[mode] ?? 'lucide:repeat'
252
269
 
253
270
  return (
254
271
  <ChipRow
255
- ariaLabel={`Continuation: ${label}`}
272
+ ariaLabel={i18n._({
273
+ id: 'editor.helpers.contextualHelperPanel.continuationLabel',
274
+ message: 'Continuation: {label}',
275
+ values: { label },
276
+ })}
256
277
  icon={icon}
257
278
  label={label}
258
279
  onClick={() => cycleContinuation(context)}
259
280
  shortcut="C"
260
- tooltip="Continuation — click or press C to cycle"
281
+ tooltip={i18n._({ id: 'editor.helpers.contextualHelperPanel.continuationClickOrPressCTo', message: 'Continuation — click or press C to cycle' })}
261
282
  />
262
283
  )
263
284
  }
264
285
 
265
286
  function FenceContinuationChips() {
287
+ const i18n = usePascalI18n()
266
288
  const mode = useEditor((s) => s.getContinuation('fence'))
267
289
  const setContinuation = useEditor((s) => s.setContinuation)
268
290
  const curveStarted = useFenceCurveDraft((s) => s.pointCount > 0)
@@ -287,13 +309,13 @@ function FenceContinuationChips() {
287
309
  label={typeLabel}
288
310
  onClick={() => setContinuation('fence', nextType)}
289
311
  shortcut="T"
290
- tooltip="Fence type — click or press T to switch between straight, curved and freehand"
312
+ tooltip={i18n._({ id: 'editor.helpers.contextualHelperPanel.fenceTypeClickOrPressT', message: 'Fence type — click or press T to switch between straight, curved and freehand' })}
291
313
  />
292
314
  <ChipRow
293
315
  ariaLabel={`Fence continuation: ${straightMode === 'single' ? 'Single' : 'Continuous'}`}
294
316
  disabled={isCurved || isFreehand}
295
317
  icon={straightMode === 'single' ? 'lucide:minus' : 'lucide:waypoints'}
296
- label={straightMode === 'single' ? 'Straight: Single' : 'Straight: Continuous'}
318
+ label={straightMode === 'single' ? i18n._({ id: 'editor.helpers.contextualHelperPanel.straightSingle', message: 'Straight: Single' }) : i18n._({ id: 'editor.helpers.contextualHelperPanel.straightContinuous', message: 'Straight: Continuous' })}
297
319
  onClick={
298
320
  isCurved || isFreehand
299
321
  ? undefined
@@ -302,8 +324,8 @@ function FenceContinuationChips() {
302
324
  shortcut="C"
303
325
  tooltip={
304
326
  isCurved || isFreehand
305
- ? 'Straight continuation is unavailable for curved or freehand fences'
306
- : 'Straight fence continuation — click or press C to toggle'
327
+ ? i18n._({ id: 'editor.helpers.contextualHelperPanel.straightContinuationIsUnavailableForCurved', message: 'Straight continuation is unavailable for curved or freehand fences' })
328
+ : i18n._({ id: 'editor.helpers.contextualHelperPanel.straightFenceContinuationClickOrPress', message: 'Straight fence continuation — click or press C to toggle' })
307
329
  }
308
330
  />
309
331
  {/* Curved fences are committed by a closing gesture rather than per-click,
@@ -312,7 +334,7 @@ function FenceContinuationChips() {
312
334
  {(isCurved || isFreehand) && curveStarted ? (
313
335
  <ChipRow
314
336
  icon="lucide:circle-check"
315
- label={isFreehand ? 'Drag to draw fence' : 'Finish curve (or double-click)'}
337
+ label={isFreehand ? i18n._({ id: 'editor.helpers.contextualHelperPanel.dragToDrawFence', message: 'Drag to draw fence' }) : i18n._({ id: 'editor.helpers.contextualHelperPanel.finishCurveOrDoubleClick', message: 'Finish curve (or double-click)' })}
316
338
  shortcut={isFreehand ? 'Release' : 'Enter'}
317
339
  />
318
340
  ) : null}
@@ -340,6 +362,7 @@ function PaintNotice() {
340
362
  // derived `paintHover` (scopes + labels), so it works for any kind without a
341
363
  // per-target table.
342
364
  function PaintScopeChip() {
365
+ const i18n = usePascalI18n()
343
366
  // What the cursor is over (that's what the next click paints). `null` when not
344
367
  // over a paintable surface — including an item with no slots.
345
368
  const paintHover = useEditor((s) => s.paintHover)
@@ -349,12 +372,30 @@ function PaintScopeChip() {
349
372
  const paintMode = usePaintRegionMode((s) => s.mode)
350
373
  const paintEraser = paintMode === 'erase'
351
374
  const picking = paintMode === 'pick'
352
- const verb = picking ? 'Pick' : paintEraser ? 'Erase' : 'Paint'
375
+ const verb = picking ? 'pick' : paintEraser ? 'erase' : 'paint'
376
+ const scopeText = (scope: PaintScope) =>
377
+ ({
378
+ pick: i18n._({
379
+ id: 'editor.helpers.contextualHelperPanel.pickScope',
380
+ message: 'Pick: {scope}',
381
+ values: { scope: paintScopeLabel(scope, paintHover!) },
382
+ }),
383
+ erase: i18n._({
384
+ id: 'editor.helpers.contextualHelperPanel.eraseScope',
385
+ message: 'Erase: {scope}',
386
+ values: { scope: paintScopeLabel(scope, paintHover!) },
387
+ }),
388
+ paint: i18n._({
389
+ id: 'editor.helpers.contextualHelperPanel.paintScope',
390
+ message: 'Paint: {scope}',
391
+ values: { scope: paintScopeLabel(scope, paintHover!) },
392
+ }),
393
+ })[verb]
353
394
 
354
395
  // Nothing to paint with yet (no material picked, not erasing) → the first step
355
396
  // is choosing a material, so say that before anything about scope or hovering.
356
397
  if (!(paintEraser || picking || hasActivePaintMaterial(activePaintMaterial))) {
357
- return <ChipRow icon="lucide:palette" label="Select a material to paint" />
398
+ return <ChipRow icon="lucide:palette" label={i18n._({ id: 'editor.helpers.contextualHelperPanel.selectAMaterialToPaint', message: 'Select a material to paint' })} />
358
399
  }
359
400
 
360
401
  // Not over anything paintable → guide the user to hover, still teaching Shift.
@@ -364,10 +405,10 @@ function PaintScopeChip() {
364
405
  icon="lucide:mouse-pointer-click"
365
406
  label={
366
407
  picking
367
- ? 'Hover a surface to pick its material'
408
+ ? i18n._({ id: 'editor.helpers.contextualHelperPanel.hoverASurfaceToPickIts', message: 'Hover a surface to pick its material' })
368
409
  : paintEraser
369
- ? 'Hover a painted surface to erase'
370
- : 'Hover a surface to paint'
410
+ ? i18n._({ id: 'editor.helpers.contextualHelperPanel.hoverAPaintedSurfaceToErase', message: 'Hover a painted surface to erase' })
411
+ : i18n._({ id: 'editor.helpers.contextualHelperPanel.hoverASurfaceToPaint', message: 'Hover a surface to paint' })
371
412
  }
372
413
  shortcut={picking ? undefined : 'Shift'}
373
414
  />
@@ -385,29 +426,43 @@ function PaintScopeChip() {
385
426
  return (
386
427
  <ChipRow
387
428
  icon={picking ? 'lucide:pipette' : PAINT_SCOPE_ICONS[effective]}
388
- label={`${verb}: ${paintScopeLabel(picking ? 'single' : effective, paintHover)}`}
429
+ label={scopeText(picking ? 'single' : effective)}
389
430
  />
390
431
  )
391
432
  }
392
433
 
393
434
  return (
394
435
  <ChipRow
395
- ariaLabel={`${verb} scope: ${paintScopeLabel(effective, paintHover)}`}
436
+ ariaLabel={
437
+ paintEraser
438
+ ? i18n._({ id: 'editor.helpers.contextualHelperPanel.eraseScopeLabel', message: 'Erase scope: {scope}', values: { scope: paintScopeLabel(effective, paintHover) } })
439
+ : i18n._({ id: 'editor.helpers.contextualHelperPanel.paintScopeLabel', message: 'Paint scope: {scope}', values: { scope: paintScopeLabel(effective, paintHover) } })
440
+ }
396
441
  icon={PAINT_SCOPE_ICONS[effective]}
397
- label={`${verb}: ${paintScopeLabel(effective, paintHover)}`}
442
+ label={scopeText(effective)}
398
443
  onClick={() => cyclePaintScope()}
399
444
  shortcut="Shift"
400
- tooltip={`${verb} scope — click or press Shift to cycle`}
445
+ tooltip={
446
+ paintEraser
447
+ ? i18n._({ id: 'editor.helpers.contextualHelperPanel.eraseScopeClickOrPressShift', message: 'Erase scope — click or press Shift to cycle' })
448
+ : i18n._({ id: 'editor.helpers.contextualHelperPanel.paintScopeClickOrPressShift', message: 'Paint scope — click or press Shift to cycle' })
449
+ }
401
450
  />
402
451
  )
403
452
  }
404
453
 
405
454
  // The tool in hand: its icon, name and arming key, over a hairline.
406
455
  function HudHeader({ title }: { title: HudTitle }) {
456
+ const i18n = usePascalI18n()
457
+ const kind = title.kind
458
+ ? nodeLabel(title.kind.kind, title.kind.english, i18n).toLocaleLowerCase(i18n.getLocale())
459
+ : undefined
460
+ const descriptor: MessageDescriptor = { ...title.label, values: { ...title.label.values, kind } }
461
+ const label = i18n._(descriptor)
407
462
  return (
408
463
  <div
409
464
  className="col-span-2 mb-0.5 flex items-center gap-2.5 border-border border-b pb-2.5"
410
- data-hud-title={title.label}
465
+ data-hud-title={label}
411
466
  >
412
467
  {title.icon ? (
413
468
  <span className="flex size-7 shrink-0 items-center justify-center">
@@ -415,7 +470,7 @@ function HudHeader({ title }: { title: HudTitle }) {
415
470
  </span>
416
471
  ) : null}
417
472
  <span className="min-w-0 flex-1 truncate font-medium text-[13px] text-foreground leading-tight">
418
- {title.label}
473
+ {label}
419
474
  </span>
420
475
  {title.shortcut ? <ShortcutToken className={TOKEN_CLASS} value={title.shortcut} /> : null}
421
476
  </div>
@@ -430,6 +485,7 @@ const isEscHint = (hint: ContextualShortcutHint) =>
430
485
  const isNoteHint = (hint: ContextualShortcutHint) => hint.keys.length === 1 && hint.keys[0] === '!'
431
486
 
432
487
  function HintRow({ hint }: { hint: ContextualShortcutHint }) {
488
+ const i18n = usePascalI18n()
433
489
  return (
434
490
  <div className={cn(ROW_CLASS, 'items-start')}>
435
491
  <ShortcutSequence active={hint.active} keys={hint.keys} />
@@ -440,17 +496,20 @@ function HintRow({ hint }: { hint: ContextualShortcutHint }) {
440
496
  hint.active ? 'font-medium text-foreground dark:text-white' : 'text-muted-foreground',
441
497
  )}
442
498
  >
443
- {hint.label}
499
+ {hintText(hint.label, i18n)}
444
500
  </div>
445
501
  {hint.subtitle ? (
446
- <div className="text-[10px] text-muted-foreground/70 leading-snug">{hint.subtitle}</div>
502
+ <div className="text-[10px] text-muted-foreground/70 leading-snug">
503
+ {hintText(hint.subtitle, i18n)}
504
+ </div>
447
505
  ) : null}
448
506
  </div>
449
507
  </div>
450
508
  )
451
509
  }
452
510
 
453
- const hintKey = (hint: ContextualShortcutHint) => `${hint.keys.join('+')}:${hint.label}`
511
+ const hintKey = (hint: ContextualShortcutHint) =>
512
+ `${hint.keys.join('+')}:${typeof hint.label === 'string' ? hint.label : hint.label.id}`
454
513
 
455
514
  export function ContextualHelperPanel({
456
515
  hints,
@@ -475,6 +534,7 @@ export function ContextualHelperPanel({
475
534
  // A warning about what is in hand (a floor item in a door's way), in the "!" row.
476
535
  notice?: string | null
477
536
  }) {
537
+ const i18n = usePascalI18n()
478
538
  const inStack = useInRightStack()
479
539
  const [showAllHints, setShowAllHints] = useState(false)
480
540
  const hintsId = useId()
@@ -486,8 +546,8 @@ export function ContextualHelperPanel({
486
546
  if (fenceFeature === 'gate' || fenceFeature === 'opening') return (
487
547
  <div className={cn(CARD_CLASS, !inStack && FLOATING_CLASS)} data-hud-card>
488
548
  {title ? <HudHeader title={title} /> : null}
489
- <ChipRow shortcut="Left click" label={fenceFeature === 'gate' ? 'Place gate on a fence' : 'Place passage on a fence'} />
490
- <ChipRow shortcut="Esc" label="Cancel placement" />
549
+ <ChipRow shortcut="Left click" label={fenceFeature === 'gate' ? i18n._({ id: 'editor.helpers.contextualHelperPanel.placeGateOnAFence', message: 'Place gate on a fence' }) : i18n._({ id: 'editor.helpers.contextualHelperPanel.placePassageOnAFence', message: 'Place passage on a fence' })} />
550
+ <ChipRow shortcut="Esc" label={i18n._({ id: 'editor.helpers.contextualHelperPanel.cancelPlacement', message: 'Cancel placement' })} />
491
551
  </div>
492
552
  )
493
553
  if (hints.length === 0 && !hasChips && !notice) return null
@@ -518,7 +578,7 @@ export function ContextualHelperPanel({
518
578
  onClick={() => setShowAllHints((value) => !value)}
519
579
  type="button"
520
580
  >
521
- {showAllHints ? 'Fewer shortcuts' : 'More shortcuts'}
581
+ {showAllHints ? i18n._({ id: 'editor.helpers.contextualHelperPanel.fewerShortcuts', message: 'Fewer shortcuts' }) : i18n._({ id: 'editor.helpers.contextualHelperPanel.moreShortcuts', message: 'More shortcuts' })}
522
582
  </button>
523
583
  ) : null}
524
584
  {notice ? <HintRow hint={{ keys: ['!'], label: notice, active: true }} /> : null}
@@ -5,7 +5,12 @@ import { snapContextOf } from '../../../lib/snapping-mode'
5
5
  import { cornerHintRows } from '../../../store/use-corner-snap-hint'
6
6
  import { TooltipProvider } from '../primitives/tooltip'
7
7
  import { ContextualHelperPanel } from './contextual-helper-panel'
8
- import { ROOM_DIVIDE_HINTS } from './helper-manager'
8
+ import {
9
+ mezzanineHints,
10
+ openingHints,
11
+ ROOM_DIVIDE_HINTS,
12
+ toolHintsOwnPlacing,
13
+ } from './helper-manager'
9
14
 
10
15
  describe('Divide HUD', () => {
11
16
  test('uses the wall draft snapping context', () => {
@@ -28,7 +33,7 @@ describe('Divide HUD', () => {
28
33
  </TooltipProvider>,
29
34
  )
30
35
  expect(html).toContain('Snapping: Grid')
31
- expect(html).toContain('Grid: 0.50 m')
36
+ expect(html).toContain('Grid: 0.50\u00a0m')
32
37
  for (const label of ['Add point', 'More shortcuts', 'Cancel'])
33
38
  expect(html).toContain(label)
34
39
  expect(html).toContain('aria-expanded="false"')
@@ -116,6 +121,51 @@ describe('HUD header', () => {
116
121
  })
117
122
 
118
123
  test('select mode is titled Select with its V key', () => {
119
- expect(SELECT_HUD_TITLE).toMatchObject({ label: 'Select', shortcut: 'V' })
124
+ expect(SELECT_HUD_TITLE).toMatchObject({
125
+ label: expect.objectContaining({ message: 'Select' }),
126
+ shortcut: 'V',
127
+ })
128
+ })
129
+ })
130
+
131
+ describe('Cut opening HUD', () => {
132
+ test('names the drag gesture "Draw the opening", not the mezzanine', () => {
133
+ // The drag row folds under "More shortcuts", so read the rows the HUD is given.
134
+ const english = (hints: ReturnType<typeof openingHints>) =>
135
+ hints.map((hint) => (typeof hint.label === 'string' ? hint.label : hint.label.message))
136
+ expect(english(openingHints('rectangle'))).toContain('Draw the opening')
137
+ expect(english(openingHints('rectangle'))).not.toContain('Draw the mezzanine')
138
+ expect(english(mezzanineHints('rectangle'))).toContain('Draw the mezzanine')
139
+ })
140
+ })
141
+
142
+ // The roof tool holds its draft in a placing scope, so the HUD read "Move roof · Left click: Place"
143
+ // while a click set a footprint corner; the tool's own hints say what the click does.
144
+ describe('a registry tool holding its draft', () => {
145
+ const placing = (driver: 'registry-tool' | 'move-tool', nodeType = 'roof') => ({
146
+ kind: 'placing' as const,
147
+ node: { id: 'roof_draft', type: nodeType } as never,
148
+ nodeId: 'roof_draft',
149
+ nodeType,
150
+ view: '3d' as const,
151
+ pressDrag: false,
152
+ driver,
153
+ })
154
+
155
+ test("shows the armed tool's hints", () => {
156
+ expect(toolHintsOwnPlacing(placing('registry-tool'), 'roof', 3)).toBe(true)
157
+ })
158
+
159
+ test('leaves a move, a catalog placement or a tool with no hints to the move rows', () => {
160
+ expect(toolHintsOwnPlacing(placing('move-tool'), 'roof', 3)).toBe(false)
161
+ expect(toolHintsOwnPlacing(placing('registry-tool', 'item'), 'roof', 3)).toBe(false)
162
+ expect(toolHintsOwnPlacing(placing('registry-tool'), 'roof', 0)).toBe(false)
163
+ expect(
164
+ toolHintsOwnPlacing(
165
+ { kind: 'moving', node: { id: 'r', type: 'roof' } as never, nodeId: 'r', nodeType: 'roof', view: '3d' },
166
+ 'roof',
167
+ 3,
168
+ ),
169
+ ).toBe(false)
120
170
  })
121
171
  })