@pascal-app/editor 1.0.6 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (382) hide show
  1. package/package.json +18 -15
  2. package/src/components/editor/action-menu-button.tsx +4 -1
  3. package/src/components/editor/alignment-3d-guide-layer.tsx +102 -11
  4. package/src/components/editor/bake-thumbnail.tsx +3 -2
  5. package/src/components/editor/custom-camera-controls.tsx +110 -42
  6. package/src/components/editor/duplicate-node.ts +2 -1
  7. package/src/components/editor/elevation-3d-guide-layer.tsx +1 -1
  8. package/src/components/editor/first-person-controls.tsx +77 -68
  9. package/src/components/editor/fit-scene-framing.test.ts +318 -0
  10. package/src/components/editor/fit-scene-framing.ts +142 -0
  11. package/src/components/editor/floating-action-menu.tsx +31 -11
  12. package/src/components/editor/floating-building-action-menu.tsx +1 -1
  13. package/src/components/editor/floor-openings-overlay.tsx +1 -1
  14. package/src/components/editor/floor-region-controls.tsx +2 -2
  15. package/src/components/editor/floorplan-panel.test.ts +220 -0
  16. package/src/components/editor/floorplan-panel.tsx +458 -140
  17. package/src/components/editor/floorplan-wall-typing-keys.test.tsx +152 -0
  18. package/src/components/editor/floorplan-wall-typing-keys.ts +79 -0
  19. package/src/components/editor/footprint-height-handle.tsx +2 -2
  20. package/src/components/editor/grid.tsx +24 -15
  21. package/src/components/editor/group-actions.ts +19 -1
  22. package/src/components/editor/group-floating-action-menu.tsx +1 -1
  23. package/src/components/editor/group-move-3d.ts +27 -2
  24. package/src/components/editor/group-selection-box-3d.tsx +4 -1
  25. package/src/components/editor/group-transform-shared.ts +23 -1
  26. package/src/components/editor/index.tsx +150 -20
  27. package/src/components/editor/inspector-extension-actions.tsx +36 -0
  28. package/src/components/editor/localized-html.tsx +22 -0
  29. package/src/components/editor/measurement-pill.tsx +1 -1
  30. package/src/components/editor/node-action-menu.tsx +46 -12
  31. package/src/components/editor/node-arrow-handles.tsx +2 -3
  32. package/src/components/editor/open-wall-ends-3d-layer.tsx +2 -3
  33. package/src/components/editor/open-wall-ends-hint.tsx +1 -1
  34. package/src/components/editor/opening-guides-3d-layer.tsx +1 -1
  35. package/src/components/editor/quick-measurement-card.tsx +1 -1
  36. package/src/components/editor/registry-action-contributions.test.tsx +67 -6
  37. package/src/components/editor/registry-action-contributions.tsx +115 -15
  38. package/src/components/editor/riser-diagram-panel.tsx +59 -9
  39. package/src/components/editor/room-controls.tsx +4 -3
  40. package/src/components/editor/room-handles-2d.tsx +158 -7
  41. package/src/components/editor/room-handles.tsx +244 -54
  42. package/src/components/editor/room-highlight.tsx +2 -0
  43. package/src/components/editor/room-labels.tsx +33 -17
  44. package/src/components/editor/selection-manager.tsx +15 -102
  45. package/src/components/editor/separator-end-grip.tsx +74 -0
  46. package/src/components/editor/site-edge-labels.tsx +7 -3
  47. package/src/components/editor/thumbnail-generator.test.ts +15 -6
  48. package/src/components/editor/thumbnail-generator.tsx +26 -1
  49. package/src/components/editor/use-floorplan-background-placement.ts +55 -52
  50. package/src/components/editor/views/view-bar.tsx +111 -26
  51. package/src/components/editor/wall-measurement-label.tsx +31 -15
  52. package/src/components/editor/wall-move-side-handles.tsx +15 -7
  53. package/src/components/editor/wall-pull-grip.tsx +193 -0
  54. package/src/components/editor/wall-region-handles.tsx +2 -2
  55. package/src/components/editor/wall-snap-beacon-layer.tsx +134 -33
  56. package/src/components/editor-2d/floorplan-adjacent-walls-layer.test.ts +76 -0
  57. package/src/components/editor-2d/floorplan-adjacent-walls-layer.tsx +186 -0
  58. package/src/components/editor-2d/floorplan-alignment-guide-layer.tsx +63 -3
  59. package/src/components/editor-2d/floorplan-group-move.tsx +32 -4
  60. package/src/components/editor-2d/floorplan-measurement-tool-layer.test.ts +6 -0
  61. package/src/components/editor-2d/floorplan-measurement-tool-layer.tsx +33 -24
  62. package/src/components/editor-2d/floorplan-open-wall-ends-layer.tsx +12 -5
  63. package/src/components/editor-2d/floorplan-registry-action-menu.tsx +2 -1
  64. package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +29 -5
  65. package/src/components/editor-2d/floorplan-snap-beacon-layer.tsx +96 -4
  66. package/src/components/editor-2d/renderers/floorplan-dimension-edit-overlay.tsx +52 -12
  67. package/src/components/editor-2d/renderers/floorplan-draft-measurement.tsx +7 -1
  68. package/src/components/editor-2d/renderers/floorplan-placement-preview-layer.tsx +13 -1
  69. package/src/components/editor-2d/renderers/floorplan-placement-typing-hud.test.tsx +35 -0
  70. package/src/components/editor-2d/renderers/floorplan-placement-typing-hud.tsx +65 -0
  71. package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +34 -1
  72. package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +211 -39
  73. package/src/components/procedural-items/inspector.tsx +59 -12
  74. package/src/components/systems/ceiling/ceiling-bracket-batch.test.ts +2 -0
  75. package/src/components/systems/ceiling/ceiling-system.test.ts +141 -0
  76. package/src/components/systems/ceiling/ceiling-system.tsx +53 -18
  77. package/src/components/systems/roof/roof-edit-system.test.tsx +15 -1
  78. package/src/components/systems/roof/roof-edit-system.tsx +30 -19
  79. package/src/components/tools/distribution/distribution-run-tool.test.tsx +371 -3
  80. package/src/components/tools/distribution/distribution-run-tool.tsx +200 -58
  81. package/src/components/tools/item/move-tool.tsx +1 -6
  82. package/src/components/tools/item/placement-history.test.tsx +1 -1
  83. package/src/components/tools/item/placement-strategies.test.ts +64 -1
  84. package/src/components/tools/item/placement-strategies.ts +5 -9
  85. package/src/components/tools/item/use-placement-coordinator.tsx +31 -35
  86. package/src/components/tools/paint-region/wall-paint-region-tool.tsx +1 -1
  87. package/src/components/tools/registry/item-surface-move.test.ts +11 -0
  88. package/src/components/tools/registry/move-registry-node-tool.test.ts +42 -1
  89. package/src/components/tools/registry/move-registry-node-tool.tsx +38 -14
  90. package/src/components/tools/select/select-candidates.test.ts +45 -0
  91. package/src/components/tools/select/select-candidates.ts +13 -2
  92. package/src/components/tools/shared/cursor-sphere.tsx +10 -3
  93. package/src/components/tools/shared/draft-measurement-label.tsx +1 -1
  94. package/src/components/tools/shared/placement-box.tsx +2 -6
  95. package/src/components/tools/shared/placement-coordinate-input.tsx +94 -0
  96. package/src/components/tools/shared/placement-dimension-guides.tsx +1 -1
  97. package/src/components/tools/shared/segment-angle.ts +0 -4
  98. package/src/components/tools/site/terrain-brush-cursor.tsx +1 -1
  99. package/src/components/tools/tool-manager.tsx +1 -24
  100. package/src/components/tools/wall/wall-drafting.test.ts +299 -23
  101. package/src/components/tools/wall/wall-drafting.ts +132 -24
  102. package/src/components/tools/wall/wall-pull-snap.test.ts +274 -0
  103. package/src/components/tools/wall/wall-pull-snap.ts +563 -0
  104. package/src/components/tools/wall/wall-snap-geometry.test.ts +113 -1
  105. package/src/components/tools/wall/wall-snap-geometry.ts +137 -0
  106. package/src/components/tools/wall/wall-typed-entry.test.ts +99 -0
  107. package/src/components/tools/wall/wall-typed-entry.ts +70 -0
  108. package/src/components/tools/zone/zone-tool.tsx +3 -3
  109. package/src/components/ui/action-menu/action-button.tsx +2 -0
  110. package/src/components/ui/action-menu/camera-actions.tsx +8 -7
  111. package/src/components/ui/action-menu/control-modes.tsx +9 -5
  112. package/src/components/ui/action-menu/follow-pascal-pill.tsx +3 -1
  113. package/src/components/ui/action-menu/follow-pascal-toggle.tsx +7 -1
  114. package/src/components/ui/action-menu/furnish-tools.tsx +7 -6
  115. package/src/components/ui/action-menu/index.tsx +1 -0
  116. package/src/components/ui/action-menu/measurement-control.tsx +35 -29
  117. package/src/components/ui/action-menu/point-ask-button.tsx +4 -3
  118. package/src/components/ui/action-menu/structure-tools.tsx +25 -23
  119. package/src/components/ui/action-menu/view-toggles.test.ts +18 -0
  120. package/src/components/ui/action-menu/view-toggles.tsx +256 -51
  121. package/src/components/ui/build-panel/build-panel.tsx +163 -25
  122. package/src/components/ui/command-palette/editor-commands.tsx +38 -11
  123. package/src/components/ui/controls/finish-swatch.tsx +45 -19
  124. package/src/components/ui/controls/material-picker.tsx +24 -17
  125. package/src/components/ui/controls/metric-control.test.ts +52 -0
  126. package/src/components/ui/controls/metric-control.tsx +62 -24
  127. package/src/components/ui/controls/segmented-control.tsx +5 -2
  128. package/src/components/ui/controls/slider-control.test.tsx +23 -0
  129. package/src/components/ui/controls/slider-control.tsx +47 -19
  130. package/src/components/ui/controls/tool-options-panel.tsx +74 -44
  131. package/src/components/ui/floating-level-selector.tsx +100 -48
  132. package/src/components/ui/helpers/building-helper.tsx +5 -3
  133. package/src/components/ui/helpers/contextual-helper-panel.tsx +97 -37
  134. package/src/components/ui/helpers/helper-manager.test.tsx +106 -3
  135. package/src/components/ui/helpers/helper-manager.tsx +136 -57
  136. package/src/components/ui/helpers/item-helper.tsx +6 -4
  137. package/src/components/ui/helpers/registered-tool-helper.tsx +3 -1
  138. package/src/components/ui/level-duplicate-dialog.tsx +32 -23
  139. package/src/components/ui/level-wall-copy-prompt.tsx +124 -0
  140. package/src/components/ui/panels/collections/collections-popover.tsx +11 -11
  141. package/src/components/ui/panels/deck-boards-fields.tsx +136 -0
  142. package/src/components/ui/panels/floor-foundation-panel.tsx +114 -39
  143. package/src/components/ui/panels/floor-opening-panel.tsx +74 -26
  144. package/src/components/ui/panels/inspector-extensions.tsx +66 -0
  145. package/src/components/ui/panels/mobile-panel-sheet.tsx +3 -1
  146. package/src/components/ui/panels/mobile-selection-bar.tsx +34 -28
  147. package/src/components/ui/panels/multi-field-value.test.ts +27 -0
  148. package/src/components/ui/panels/multi-field-value.ts +20 -1
  149. package/src/components/ui/panels/multi-height-mode.tsx +11 -10
  150. package/src/components/ui/panels/multi-parametric-inspector.tsx +122 -34
  151. package/src/components/ui/panels/multi-selection-panel.tsx +76 -36
  152. package/src/components/ui/panels/node-display.ts +35 -24
  153. package/src/components/ui/panels/panel-manager.tsx +9 -5
  154. package/src/components/ui/panels/panel-wrapper.tsx +63 -127
  155. package/src/components/ui/panels/parametric-field-control.tsx +13 -10
  156. package/src/components/ui/panels/parametric-inspector.tsx +18 -12
  157. package/src/components/ui/panels/reference-panel.tsx +51 -35
  158. package/src/components/ui/panels/room-construction-rows.tsx +465 -66
  159. package/src/components/ui/panels/room-name-options.test.ts +36 -3
  160. package/src/components/ui/panels/room-name-options.ts +110 -11
  161. package/src/components/ui/panels/room-openings-field.tsx +22 -13
  162. package/src/components/ui/panels/room-panel.tsx +207 -100
  163. package/src/components/ui/panels/shape-choice.tsx +7 -5
  164. package/src/components/ui/panels/wall-region-list.tsx +7 -7
  165. package/src/components/ui/primitives/shortcut-token.tsx +10 -6
  166. package/src/components/ui/right-stack.test.ts +13 -6
  167. package/src/components/ui/right-stack.tsx +21 -10
  168. package/src/components/ui/sidebar/icon-rail.tsx +11 -9
  169. package/src/components/ui/sidebar/panels/items-panel/function-tree-panel.tsx +10 -10
  170. package/src/components/ui/sidebar/panels/items-panel/index.tsx +10 -9
  171. package/src/components/ui/sidebar/panels/settings-panel/index.tsx +3 -0
  172. package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +102 -32
  173. package/src/components/ui/sidebar/panels/settings-panel/load-build-dialog.tsx +1 -5
  174. package/src/components/ui/sidebar/panels/site-panel/building-tree-node.tsx +4 -3
  175. package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +5 -3
  176. package/src/components/ui/sidebar/panels/site-panel/chimney-tree-node.tsx +4 -2
  177. package/src/components/ui/sidebar/panels/site-panel/collection-tree-node.tsx +7 -5
  178. package/src/components/ui/sidebar/panels/site-panel/column-tree-node.tsx +4 -2
  179. package/src/components/ui/sidebar/panels/site-panel/default-names.ts +75 -0
  180. package/src/components/ui/sidebar/panels/site-panel/door-tree-node.tsx +4 -2
  181. package/src/components/ui/sidebar/panels/site-panel/dormer-tree-node.tsx +4 -2
  182. package/src/components/ui/sidebar/panels/site-panel/elevator-tree-node.tsx +4 -2
  183. package/src/components/ui/sidebar/panels/site-panel/fence-tree-node.tsx +4 -2
  184. package/src/components/ui/sidebar/panels/site-panel/gutter-tree-node.tsx +4 -2
  185. package/src/components/ui/sidebar/panels/site-panel/index.tsx +118 -72
  186. package/src/components/ui/sidebar/panels/site-panel/inline-rename-input.tsx +36 -3
  187. package/src/components/ui/sidebar/panels/site-panel/item-tree-node.tsx +4 -2
  188. package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +7 -4
  189. package/src/components/ui/sidebar/panels/site-panel/registry-tree-node.tsx +8 -2
  190. package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +8 -5
  191. package/src/components/ui/sidebar/panels/site-panel/shelf-tree-node.tsx +4 -2
  192. package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +9 -4
  193. package/src/components/ui/sidebar/panels/site-panel/solar-panel-tree-node.tsx +4 -2
  194. package/src/components/ui/sidebar/panels/site-panel/spawn-tree-node.tsx +4 -2
  195. package/src/components/ui/sidebar/panels/site-panel/stair-tree-node.tsx +8 -7
  196. package/src/components/ui/sidebar/panels/site-panel/tree-node-actions.tsx +7 -6
  197. package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +16 -1
  198. package/src/components/ui/sidebar/panels/site-panel/unit-tree-node.tsx +15 -8
  199. package/src/components/ui/sidebar/panels/site-panel/wall-tree-node.tsx +4 -2
  200. package/src/components/ui/sidebar/panels/site-panel/window-tree-node.tsx +4 -2
  201. package/src/components/ui/sidebar/panels/site-panel/zone-membership-checkbox.tsx +3 -1
  202. package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +19 -9
  203. package/src/components/ui/sidebar/panels/zone-panel/index.tsx +25 -14
  204. package/src/components/ui/sidebar/tab-bar.tsx +4 -1
  205. package/src/components/ui/sidebar/use-plugin-panels.tsx +19 -10
  206. package/src/components/ui/snap-target-badge.tsx +7 -6
  207. package/src/components/ui/wall-type/wall-type-editor-dialog.tsx +721 -0
  208. package/src/components/ui/wall-type/wall-type-menu.tsx +167 -0
  209. package/src/components/viewer/floorplan-compass-button.tsx +12 -3
  210. package/src/components/viewer/floorplan-preview.tsx +27 -19
  211. package/src/components/viewer/render-settings-menu.tsx +294 -0
  212. package/src/components/viewer/sky-control.tsx +710 -0
  213. package/src/components/viewer/use-viewer-ui-sounds.test.ts +15 -0
  214. package/src/components/viewer/use-viewer-ui-sounds.ts +46 -0
  215. package/src/components/viewer/viewer-controls-bar.tsx +278 -260
  216. package/src/components/viewer/viewer-hover-label.tsx +12 -4
  217. package/src/components/viewer/viewer-rooms.tsx +62 -5
  218. package/src/components/viewer/viewer-scene-header.tsx +28 -17
  219. package/src/components/viewer/viewer-stage-switcher.tsx +4 -2
  220. package/src/components/viewer-overlay.tsx +8 -0
  221. package/src/components/walkthrough-hud.tsx +75 -2
  222. package/src/hooks/use-keyboard.test.ts +85 -4
  223. package/src/hooks/use-keyboard.ts +65 -7
  224. package/src/hooks/use-selected-room.ts +12 -1
  225. package/src/i18n/floor-footprint-name.ts +30 -0
  226. package/src/i18n/level-name.ts +39 -0
  227. package/src/i18n/locales/en/messages.po +15849 -0
  228. package/src/i18n/locales/en/messages.ts +1 -0
  229. package/src/i18n/locales/en-XA/messages.ts +1 -0
  230. package/src/i18n/locales/es/messages.po +39331 -0
  231. package/src/i18n/locales/es/messages.ts +1 -0
  232. package/src/i18n/locales/fr/messages.po +39331 -0
  233. package/src/i18n/locales/fr/messages.ts +1 -0
  234. package/src/i18n/locales/ja/messages.po +39331 -0
  235. package/src/i18n/locales/ja/messages.ts +1 -0
  236. package/src/i18n/locales/zh-CN/messages.po +39331 -0
  237. package/src/i18n/locales/zh-CN/messages.ts +1 -0
  238. package/src/i18n/locales.test.ts +63 -0
  239. package/src/i18n/locales.ts +32 -0
  240. package/src/i18n/message-slot.tsx +25 -0
  241. package/src/i18n/node-messages.ts +169 -0
  242. package/src/i18n/pseudo-locale.ts +17 -0
  243. package/src/i18n/published-entry.test.ts +17 -0
  244. package/src/i18n/scene-theme-name.ts +16 -0
  245. package/src/index.tsx +224 -45
  246. package/src/lib/add-storey.test.ts +3 -3
  247. package/src/lib/contextual-help.test.ts +20 -1
  248. package/src/lib/contextual-help.ts +101 -19
  249. package/src/lib/continuation.ts +38 -11
  250. package/src/lib/elevation-guides.ts +21 -6
  251. package/src/lib/elevator-support.ts +1 -72
  252. package/src/lib/floor-footprints.test.ts +3 -2
  253. package/src/lib/floor-footprints.ts +1 -9
  254. package/src/lib/floor-foundation-support.test.ts +15 -6
  255. package/src/lib/floor-opening-draft.ts +1 -1
  256. package/src/lib/floor-region-session.ts +1 -1
  257. package/src/lib/floorplan/dimension-drive.test.ts +36 -2
  258. package/src/lib/floorplan/dimension-drive.ts +68 -3
  259. package/src/lib/floorplan/floorplan-export.test.ts +16 -5
  260. package/src/lib/floorplan/floorplan-export.tsx +13 -16
  261. package/src/lib/floorplan/floorplan-extension.test.ts +11 -12
  262. package/src/lib/floorplan/floorplan-extension.ts +37 -18
  263. package/src/lib/floorplan/floorplan-mode.test.ts +13 -6
  264. package/src/lib/floorplan/floorplan-mode.ts +14 -12
  265. package/src/lib/floorplan/floorplan-plugin-install.test.tsx +1 -1
  266. package/src/lib/floorplan/floorplan-readonly.ts +3 -3
  267. package/src/lib/floorplan/open-wall-ends.ts +2 -1
  268. package/src/lib/floorplan/schedules.ts +10 -6
  269. package/src/lib/floorplan/site-plan/site-annotations.test.ts +46 -1
  270. package/src/lib/floorplan/site-plan/site-annotations.ts +3 -0
  271. package/src/lib/floorplan/site-plan/site-parts.ts +6 -3
  272. package/src/lib/geometry-script/author.test.ts +114 -0
  273. package/src/lib/geometry-script/author.ts +55 -23
  274. package/src/lib/geometry-script/client.ts +3 -0
  275. package/src/lib/geometry-script/finish-scripts.ts +7 -7
  276. package/src/lib/gesture-lifecycle.ts +3 -1
  277. package/src/lib/glb-export.test.ts +327 -0
  278. package/src/lib/glb-export.ts +70 -2
  279. package/src/lib/grid-frame.test.ts +56 -3
  280. package/src/lib/grid-frame.ts +28 -1
  281. package/src/lib/history.test.ts +2 -1
  282. package/src/lib/hud-title.ts +48 -24
  283. package/src/lib/ifc-export.test.ts +41 -1
  284. package/src/lib/inspector-card-mode.test.ts +3 -31
  285. package/src/lib/inspector-card-mode.ts +5 -10
  286. package/src/lib/inspector-expanded.test.ts +13 -0
  287. package/src/lib/inspector-expanded.ts +13 -1
  288. package/src/lib/interaction/scope.ts +1 -0
  289. package/src/lib/keyboard-pan.test.ts +7 -0
  290. package/src/lib/keyboard-pan.ts +9 -2
  291. package/src/lib/layer-thickness.test.ts +22 -0
  292. package/src/lib/layer-thickness.ts +79 -0
  293. package/src/lib/level-selection.test.ts +18 -2
  294. package/src/lib/level-selection.ts +31 -8
  295. package/src/lib/linear-display.ts +1 -1
  296. package/src/lib/live-gesture-round-trip.test.ts +1 -1
  297. package/src/lib/lot/drop-in.test.ts +59 -1
  298. package/src/lib/lot/drop-in.ts +42 -50
  299. package/src/lib/material-swatch-style.ts +24 -0
  300. package/src/lib/measurement-label.test.ts +1 -1
  301. package/src/lib/measurement-parser.ts +7 -3
  302. package/src/lib/measurements.test.ts +24 -6
  303. package/src/lib/measurements.ts +1 -203
  304. package/src/lib/mezzanine-selection.test.ts +3 -1
  305. package/src/lib/node-action-movement.test.tsx +115 -0
  306. package/src/lib/node-action-movement.ts +2 -0
  307. package/src/lib/orbit-near-plane.test.ts +12 -0
  308. package/src/lib/orbit-near-plane.ts +13 -0
  309. package/src/lib/paint-regions.test.ts +8 -5
  310. package/src/lib/paint-scope.test.ts +53 -0
  311. package/src/lib/paint-scope.ts +18 -4
  312. package/src/lib/panel-rows.ts +17 -8
  313. package/src/lib/placement-metadata.ts +4 -0
  314. package/src/lib/point-ask/describe-target.ts +6 -8
  315. package/src/lib/point-ask/region.ts +1 -1
  316. package/src/lib/print-shell-compiler.ts +12 -1
  317. package/src/lib/print-wall-solids.test.ts +11 -0
  318. package/src/lib/print-wall-solids.ts +9 -0
  319. package/src/lib/reference-scale.ts +83 -0
  320. package/src/lib/room-assembly-overlay.ts +1 -1
  321. package/src/lib/room-built-on.test.ts +28 -17
  322. package/src/lib/room-built-on.ts +21 -17
  323. package/src/lib/room-construction-commands.ts +3 -2
  324. package/src/lib/room-construction.test.ts +28 -1
  325. package/src/lib/room-construction.ts +14 -0
  326. package/src/lib/room-controls-phase6.test.ts +141 -1
  327. package/src/lib/room-handle-drag.ts +554 -58
  328. package/src/lib/room-name-catalog.test.ts +8 -0
  329. package/src/lib/room-name-catalog.ts +8 -1
  330. package/src/lib/room-name-messages.test.ts +132 -0
  331. package/src/lib/room-name-messages.ts +147 -0
  332. package/src/lib/room-name-storage.test.ts +334 -0
  333. package/src/lib/room-push-dimensions.test.ts +38 -1
  334. package/src/lib/room-push-dimensions.ts +226 -2
  335. package/src/lib/room-selection.test.ts +23 -6
  336. package/src/lib/scene-bounds.test.ts +144 -0
  337. package/src/lib/scene-bounds.ts +79 -15
  338. package/src/lib/scene-clipboard.test.ts +70 -49
  339. package/src/lib/scene-clipboard.ts +11 -5
  340. package/src/lib/selection-cycle.ts +63 -0
  341. package/src/lib/separator-edit.test.ts +431 -0
  342. package/src/lib/separator-edit.ts +179 -0
  343. package/src/lib/separator-lines.ts +38 -0
  344. package/src/lib/sfx-bus.ts +27 -1
  345. package/src/lib/sfx-player.ts +16 -0
  346. package/src/lib/snapping-mode.test.ts +23 -1
  347. package/src/lib/tool-cursor-icon.ts +5 -1
  348. package/src/lib/tool-shortcuts.ts +130 -0
  349. package/src/lib/viewer-selection.test.ts +28 -24
  350. package/src/lib/viewer-selection.ts +21 -6
  351. package/src/lib/wall-draw-variant.test.ts +7 -6
  352. package/src/lib/wall-draw-variant.ts +107 -6
  353. package/src/lib/wall-nudge.test.ts +44 -0
  354. package/src/lib/wall-nudge.ts +131 -0
  355. package/src/lib/wall-parallel-snap.test.ts +83 -0
  356. package/src/lib/wall-parallel-snap.ts +176 -0
  357. package/src/lib/wall-placement-pointer.test.ts +23 -0
  358. package/src/lib/wall-placement-pointer.ts +18 -0
  359. package/src/lib/wall-pull.test.ts +245 -0
  360. package/src/lib/wall-pull.ts +209 -0
  361. package/src/lib/wall-push-line-up.test.ts +132 -0
  362. package/src/lib/wall-region-list.ts +17 -6
  363. package/src/lib/wall-run-snap.test.ts +188 -0
  364. package/src/lib/wall-run-snap.ts +83 -0
  365. package/src/lib/wall-type.test.ts +125 -0
  366. package/src/lib/wall-type.ts +119 -0
  367. package/src/lib/window-interaction.test.ts +44 -0
  368. package/src/lib/world-grid-snap.test.ts +139 -2
  369. package/src/lib/world-grid-snap.ts +102 -105
  370. package/src/store/use-alignment-guides.ts +27 -1
  371. package/src/store/use-editor.tsx +53 -0
  372. package/src/store/use-floorplan-annotation-visibility.test.ts +64 -0
  373. package/src/store/use-floorplan-annotation-visibility.ts +47 -22
  374. package/src/store/use-placement-typing.test.ts +165 -0
  375. package/src/store/use-placement-typing.ts +186 -0
  376. package/src/store/use-wall-snap-indicator.ts +15 -2
  377. package/src/components/tools/elevator/elevator-defaults.ts +0 -8
  378. package/src/components/tools/elevator/elevator-tool.tsx +0 -372
  379. package/src/components/tools/elevator/move-elevator-tool.tsx +0 -262
  380. package/src/lib/door-interaction.ts +0 -102
  381. package/src/lib/measurement-label.ts +0 -68
  382. package/src/lib/window-interaction.ts +0 -100
@@ -1,7 +1,9 @@
1
- import { type AnyNodeId, useScene } from '@pascal-app/core'
1
+ import { type AnyNodeId, usePascalI18n, useScene } from '@pascal-app/core'
2
2
  import { Pencil } from 'lucide-react'
3
3
  import { memo, useCallback, useEffect, useRef, useState } from 'react'
4
+ import { displayRoomName } from './../../../../../lib/room-name-messages'
4
5
  import { cn } from './../../../../../lib/utils'
6
+ import { commitTreeRoomRename } from './../../../panels/room-name-options'
5
7
 
6
8
  interface InlineRenameFieldProps {
7
9
  name: string | undefined
@@ -18,15 +20,19 @@ interface InlineRenameFieldProps {
18
20
 
19
21
  type InlineRenameInputProps = Omit<InlineRenameFieldProps, 'name' | 'onRename'> & {
20
22
  nodeId: AnyNodeId
23
+ /** The part of the stored name the user edits; a generated default edits as empty. */
24
+ editableName?: (name: string | undefined) => string | undefined
21
25
  }
22
26
 
23
27
  /** Renames a scene node through `updateNode`. */
24
28
  export const InlineRenameInput = memo(function InlineRenameInput({
25
29
  nodeId,
30
+ editableName,
26
31
  ...props
27
32
  }: InlineRenameInputProps) {
28
33
  const updateNode = useScene((s) => s.updateNode)
29
- const name = useScene((s) => s.nodes[nodeId]?.name)
34
+ const stored = useScene((s) => s.nodes[nodeId]?.name)
35
+ const name = editableName ? editableName(stored) : stored
30
36
  const handleRename = useCallback(
31
37
  (next: string | undefined) => updateNode(nodeId, { name: next }),
32
38
  [nodeId, updateNode],
@@ -34,6 +40,30 @@ export const InlineRenameInput = memo(function InlineRenameInput({
34
40
  return <InlineRenameField {...props} name={name} onRename={handleRename} />
35
41
  })
36
42
 
43
+ /**
44
+ * Renames a room or zone from the layer tree. The field edits the name in the
45
+ * UI language, as the room panel shows it, and stores what an edit there
46
+ * stores: a catalog name stays English ("Salon 3" on a Lounge stores "Lounge 3").
47
+ */
48
+ export const RoomRenameInput = memo(function RoomRenameInput({
49
+ nodeId,
50
+ ...props
51
+ }: Omit<InlineRenameInputProps, 'editableName' | 'displayName'>) {
52
+ const i18n = usePascalI18n()
53
+ const stored = useScene((s) => s.nodes[nodeId]?.name)
54
+ const handleRename = useCallback(
55
+ (next: string | undefined) => commitTreeRoomRename(nodeId, next, i18n),
56
+ [nodeId, i18n],
57
+ )
58
+ return (
59
+ <InlineRenameField
60
+ {...props}
61
+ name={stored ? displayRoomName(stored, i18n) : stored}
62
+ onRename={handleRename}
63
+ />
64
+ )
65
+ })
66
+
37
67
  export const InlineRenameField = memo(function InlineRenameField({
38
68
  name,
39
69
  onRename,
@@ -44,6 +74,7 @@ export const InlineRenameField = memo(function InlineRenameField({
44
74
  className,
45
75
  onStartEditing,
46
76
  }: InlineRenameFieldProps) {
77
+ const i18n = usePascalI18n()
47
78
  const [value, setValue] = useState(name || '')
48
79
  const inputRef = useRef<HTMLInputElement>(null)
49
80
  const inputSize = Math.max((value || defaultName).length, 1)
@@ -63,7 +94,7 @@ export const InlineRenameField = memo(function InlineRenameField({
63
94
 
64
95
  const handleSave = useCallback(() => {
65
96
  const trimmed = value.trim()
66
- if (trimmed !== name) {
97
+ if ((trimmed || undefined) !== (name || undefined)) {
67
98
  onRename(trimmed || undefined)
68
99
  }
69
100
  onStopEditing()
@@ -87,6 +118,7 @@ export const InlineRenameField = memo(function InlineRenameField({
87
118
  </span>
88
119
  {onStartEditing && (
89
120
  <button
121
+ aria-label={i18n._({ id: 'editor.layers.rename', message: 'Rename' })}
90
122
  className="shrink-0 text-muted-foreground opacity-0 transition-opacity hover:text-foreground group-hover/rename:opacity-100"
91
123
  onClick={(e) => {
92
124
  e.stopPropagation()
@@ -102,6 +134,7 @@ export const InlineRenameField = memo(function InlineRenameField({
102
134
 
103
135
  return (
104
136
  <input
137
+ aria-label={i18n._({ id: 'editor.layers.rename', message: 'Rename' })}
105
138
  className={cn(
106
139
  'm-0 h-5 min-w-[1ch] max-w-full flex-none rounded-none border-primary/50 border-b bg-transparent px-0 py-0 text-foreground text-sm outline-none focus:border-primary',
107
140
  className,
@@ -1,4 +1,4 @@
1
- import { type AnyNodeId, type ItemNode, useScene } from '@pascal-app/core'
1
+ import { type AnyNodeId, type ItemNode, useScene, usePascalI18n } from '@pascal-app/core'
2
2
  import { useViewer } from '@pascal-app/viewer'
3
3
  import Image from 'next/image'
4
4
  import { memo, useCallback, useEffect, useState } from 'react'
@@ -12,6 +12,7 @@ import {
12
12
  TreeNodeWrapper,
13
13
  } from './tree-node'
14
14
  import { TreeNodeActions } from './tree-node-actions'
15
+ import { nodeLabelDescriptor } from '../../../../../i18n/node-messages'
15
16
 
16
17
  const CATEGORY_ICONS: Record<string, string> = {
17
18
  door: '/icons/door.webp',
@@ -34,6 +35,7 @@ export const ItemTreeNode = memo(function ItemTreeNode({
34
35
  depth,
35
36
  isLast,
36
37
  }: ItemTreeNodeProps) {
38
+ const i18n = usePascalI18n()
37
39
  const [isEditing, setIsEditing] = useState(false)
38
40
  const [expanded, setExpanded] = useState(true)
39
41
  const isVisible = useScene((s) => s.nodes[nodeId]?.visible !== false)
@@ -83,7 +85,7 @@ export const ItemTreeNode = memo(function ItemTreeNode({
83
85
 
84
86
  const iconSrc = CATEGORY_ICONS[asset?.category ?? ''] || '/icons/couch.webp'
85
87
  const snapTarget = resolveNodeSnapTarget(node)
86
- const defaultName = asset?.name || 'Item'
88
+ const defaultName = asset?.name || i18n._(nodeLabelDescriptor('item', 'Item'))
87
89
  const hasChildren = children.length > 0
88
90
 
89
91
  return (
@@ -1,12 +1,12 @@
1
- import { type LevelNode, useScene } from '@pascal-app/core'
1
+ import { type LevelNode, useLocale, usePascalI18n, useScene } from '@pascal-app/core'
2
2
  import { markPerfAction, useViewer } from '@pascal-app/viewer'
3
3
  import { Layers } from 'lucide-react'
4
4
  import { memo, useCallback, useState } from 'react'
5
5
  import { useShallow } from 'zustand/react/shallow'
6
- import { getDefaultLevelName, getLevelDisplayName } from '@pascal-app/core'
7
6
  import { InlineRenameInput } from './inline-rename-input'
8
7
  import { focusTreeNode, TreeNode, TreeNodeWrapper } from './tree-node'
9
8
  import { TreeNodeActions } from './tree-node-actions'
9
+ import { authoredLevelName, localizedLevelName } from '../../../../../i18n/level-name'
10
10
 
11
11
  interface LevelTreeNodeProps {
12
12
  nodeId: LevelNode['id']
@@ -19,6 +19,8 @@ export const LevelTreeNode = memo(function LevelTreeNode({
19
19
  depth,
20
20
  isLast,
21
21
  }: LevelTreeNodeProps) {
22
+ const i18n = usePascalI18n()
23
+ const locale = useLocale()
22
24
  const [expanded, setExpanded] = useState(true)
23
25
  const [isEditing, setIsEditing] = useState(false)
24
26
  const isVisible = useScene((s) => s.nodes[nodeId]?.visible !== false)
@@ -40,8 +42,8 @@ export const LevelTreeNode = memo(function LevelTreeNode({
40
42
  const handleStartEditing = useCallback(() => setIsEditing(true), [])
41
43
  const handleStopEditing = useCallback(() => setIsEditing(false), [])
42
44
 
43
- const defaultName = getDefaultLevelName(level)
44
- const displayName = getLevelDisplayName({ name: levelName, level })
45
+ const defaultName = localizedLevelName({ level }, i18n, locale)
46
+ const displayName = localizedLevelName({ name: levelName, level }, i18n, locale)
45
47
 
46
48
  return (
47
49
  <TreeNodeWrapper
@@ -58,6 +60,7 @@ export const LevelTreeNode = memo(function LevelTreeNode({
58
60
  <InlineRenameInput
59
61
  defaultName={defaultName}
60
62
  displayName={displayName}
63
+ editableName={(name) => authoredLevelName({ name, level })}
61
64
  isEditing={isEditing}
62
65
  nodeId={nodeId}
63
66
  onStartEditing={handleStartEditing}
@@ -1,5 +1,5 @@
1
1
  import { Icon as IconifyIcon } from '@iconify/react'
2
- import { type AnyNodeId, nodeRegistry, useScene } from '@pascal-app/core'
2
+ import { type AnyNodeId, nodeRegistry, useScene, usePascalI18n} from '@pascal-app/core'
3
3
  import { useViewer } from '@pascal-app/viewer'
4
4
  import Image from 'next/image'
5
5
  import { memo, useCallback, useEffect, useState, useSyncExternalStore } from 'react'
@@ -15,6 +15,7 @@ import {
15
15
  } from './tree-node'
16
16
  import { TreeNodeActions } from './tree-node-actions'
17
17
  import { resolveTreeChildIds, treeContainsDescendant } from './tree-structure'
18
+ import { nodeLabelDescriptor } from '../../../../../i18n/node-messages'
18
19
 
19
20
  interface RegistryTreeNodeProps {
20
21
  nodeId: AnyNodeId
@@ -34,6 +35,7 @@ export const RegistryTreeNode = memo(function RegistryTreeNode({
34
35
  depth,
35
36
  isLast,
36
37
  }: RegistryTreeNodeProps) {
38
+ const i18n = usePascalI18n()
37
39
  const [isEditing, setIsEditing] = useState(false)
38
40
  const [expanded, setExpanded] = useState(true)
39
41
  const isVisible = useScene((s) => s.nodes[nodeId]?.visible !== false)
@@ -72,7 +74,11 @@ export const RegistryTreeNode = memo(function RegistryTreeNode({
72
74
  )
73
75
  const snapTarget = resolveNodeSnapTarget(node)
74
76
  const defaultName =
75
- node ? tree?.label?.(node, useScene.getState().nodes) || node.name || presentation?.label || 'Node' : 'Node'
77
+ (node &&
78
+ (tree?.label?.(node, useScene.getState().nodes) ||
79
+ node.name ||
80
+ (presentation?.label && i18n._(nodeLabelDescriptor(node.type, presentation.label))))) ||
81
+ i18n._({ id: 'editor.layers.defaultName.node', message: 'Node' })
76
82
  const hasChildren = children.length > 0 || hasHostChildren
77
83
 
78
84
  useEffect(() => {
@@ -1,4 +1,4 @@
1
- import { type AnyNodeId, type RoofNode, type RoofSegmentNode, useScene } from '@pascal-app/core'
1
+ import { type AnyNodeId, type RoofNode, type RoofSegmentNode, useScene, usePascalI18n} from '@pascal-app/core'
2
2
  import { useViewer } from '@pascal-app/viewer'
3
3
  import { AnimatePresence } from 'motion/react'
4
4
  import Image from 'next/image'
@@ -9,6 +9,7 @@ import { InlineRenameInput } from './inline-rename-input'
9
9
  import { focusTreeNode, handleTreeSelection, TreeNode, TreeNodeWrapper } from './tree-node'
10
10
  import { TreeNodeActions } from './tree-node-actions'
11
11
  import { DropIndicatorLine, useTreeNodeDrag } from './tree-node-drag'
12
+ import { roofDefaultName, roofSegmentDefaultName } from './default-names'
12
13
 
13
14
  interface RoofTreeNodeProps {
14
15
  nodeId: AnyNodeId
@@ -21,6 +22,7 @@ export const RoofTreeNode = memo(function RoofTreeNode({
21
22
  depth,
22
23
  isLast,
23
24
  }: RoofTreeNodeProps) {
25
+ const i18n = usePascalI18n()
24
26
  const [isEditing, setIsEditing] = useState(false)
25
27
  const [expanded, setExpanded] = useState(false)
26
28
  const isVisible = useScene((s) => s.nodes[nodeId]?.visible !== false)
@@ -84,7 +86,7 @@ export const RoofTreeNode = memo(function RoofTreeNode({
84
86
  }, [isDropTarget, expanded])
85
87
 
86
88
  const segmentCount = segments.length
87
- const defaultName = `Roof (${segmentCount} segment${segmentCount !== 1 ? 's' : ''})`
89
+ const defaultName = roofDefaultName(i18n, segmentCount)
88
90
 
89
91
  // Hide the dragged segment from every roof while dragging
90
92
  const visibleSegments = drag ? segments.filter((seg) => seg.id !== drag.nodeId) : segments
@@ -163,6 +165,7 @@ function RoofSegmentTreeNode({
163
165
  depth: number
164
166
  isLast?: boolean
165
167
  }) {
168
+ const i18n = usePascalI18n()
166
169
  const [isEditing, setIsEditing] = useState(false)
167
170
  const [expanded, setExpanded] = useState(true)
168
171
  const accessoryIds = useScene(
@@ -206,16 +209,16 @@ function RoofSegmentTreeNode({
206
209
  const handlePointerDown = useCallback(
207
210
  (e: React.PointerEvent) => {
208
211
  if (e.button !== 0) return
209
- const label = `${node.roofType.charAt(0).toUpperCase() + node.roofType.slice(1)} (${node.width.toFixed(1)}×${node.depth.toFixed(1)}m)`
212
+ const label = roofSegmentDefaultName(i18n, node.roofType, node.width, node.depth)
210
213
  startDrag(node.id, node.type, node.parentId as string, label, e.clientX, e.clientY)
211
214
  },
212
- [node.id, node.type, node.parentId, node.roofType, node.width, node.depth, startDrag],
215
+ [node.id, node.type, node.parentId, node.roofType, node.width, node.depth, startDrag, i18n],
213
216
  )
214
217
 
215
218
  const handleStartEditing = useCallback(() => setIsEditing(true), [])
216
219
  const handleStopEditing = useCallback(() => setIsEditing(false), [])
217
220
 
218
- const defaultName = `${node.roofType.charAt(0).toUpperCase() + node.roofType.slice(1)} (${node.width.toFixed(1)}x${node.depth.toFixed(1)}m)`
221
+ const defaultName = roofSegmentDefaultName(i18n, node.roofType, node.width, node.depth)
219
222
 
220
223
  const hasAccessories = accessoryIds.length > 0
221
224
 
@@ -1,6 +1,6 @@
1
1
  'use client'
2
2
 
3
- import { type AnyNodeId, type ShelfNode, useScene } from '@pascal-app/core'
3
+ import { type AnyNodeId, type ShelfNode, useScene, usePascalI18n } from '@pascal-app/core'
4
4
  import { useViewer } from '@pascal-app/viewer'
5
5
  import Image from 'next/image'
6
6
  import { memo, useCallback, useEffect, useState } from 'react'
@@ -13,6 +13,7 @@ import {
13
13
  TreeNodeWrapper,
14
14
  } from './tree-node'
15
15
  import { TreeNodeActions } from './tree-node-actions'
16
+ import { nodeLabelDescriptor } from '../../../../../i18n/node-messages'
16
17
 
17
18
  interface ShelfTreeNodeProps {
18
19
  nodeId: ShelfNode['id']
@@ -32,6 +33,7 @@ export const ShelfTreeNode = memo(function ShelfTreeNode({
32
33
  depth,
33
34
  isLast,
34
35
  }: ShelfTreeNodeProps) {
36
+ const i18n = usePascalI18n()
35
37
  const [isEditing, setIsEditing] = useState(false)
36
38
  const [expanded, setExpanded] = useState(true)
37
39
  const isVisible = useScene((s) => s.nodes[nodeId]?.visible !== false)
@@ -97,7 +99,7 @@ export const ShelfTreeNode = memo(function ShelfTreeNode({
97
99
  isVisible={isVisible}
98
100
  label={
99
101
  <InlineRenameInput
100
- defaultName="Shelf"
102
+ defaultName={i18n._(nodeLabelDescriptor('shelf', 'Shelf'))}
101
103
  isEditing={isEditing}
102
104
  nodeId={nodeId}
103
105
  onStartEditing={handleStartEditing}
@@ -1,11 +1,13 @@
1
- import { type AnyNodeId, floorFootprintName, type SlabNode, useScene } from '@pascal-app/core'
1
+ import { type AnyNodeId, type SlabNode, usePascalI18n, useScene } from '@pascal-app/core'
2
2
  import { useViewer } from '@pascal-app/viewer'
3
3
  import Image from 'next/image'
4
4
  import { memo, useCallback, useState } from 'react'
5
- import { formatAreaLabel } from './../../../../../lib/measurements'
5
+ import { formatAreaLabel } from '@pascal-app/core'
6
6
  import { InlineRenameInput } from './inline-rename-input'
7
7
  import { focusTreeNode, handleTreeSelection, TreeNodeWrapper } from './tree-node'
8
8
  import { TreeNodeActions } from './tree-node-actions'
9
+ import { localizedFloorFootprintName } from '../../../../../i18n/floor-footprint-name'
10
+ import { slabDefaultName } from './default-names'
9
11
 
10
12
  interface SlabTreeNodeProps {
11
13
  nodeId: AnyNodeId
@@ -18,6 +20,7 @@ export const SlabTreeNode = memo(function SlabTreeNode({
18
20
  depth,
19
21
  isLast,
20
22
  }: SlabTreeNodeProps) {
23
+ const i18n = usePascalI18n()
21
24
  const [isEditing, setIsEditing] = useState(false)
22
25
  const isVisible = useScene((s) => s.nodes[nodeId]?.visible !== false)
23
26
  const polygon = useScene((s) => (s.nodes[nodeId] as SlabNode | undefined)?.polygon ?? [])
@@ -30,7 +33,9 @@ export const SlabTreeNode = memo(function SlabTreeNode({
30
33
  // footprint's name ("Shared floor", "Lanai floor"), opening that panel.
31
34
  const footprint = useScene((s) => {
32
35
  const node = s.nodes[nodeId]
33
- return node?.type === 'slab' && node.plateRole === 'base' ? floorFootprintName(s.nodes, node) : null
36
+ return node?.type === 'slab' && node.plateRole === 'base'
37
+ ? localizedFloorFootprintName(s.nodes, node, i18n)
38
+ : null
34
39
  })
35
40
 
36
41
  const handleClick = useCallback(
@@ -49,7 +54,7 @@ export const SlabTreeNode = memo(function SlabTreeNode({
49
54
  const handleStartEditing = useCallback(() => setIsEditing(true), [])
50
55
  const handleStopEditing = useCallback(() => setIsEditing(false), [])
51
56
 
52
- const defaultName = `Slab (${formatAreaLabel(calculatePolygonArea(polygon), unit)})`
57
+ const defaultName = slabDefaultName(i18n, formatAreaLabel(calculatePolygonArea(polygon), unit))
53
58
 
54
59
  return (
55
60
  <TreeNodeWrapper
@@ -1,4 +1,4 @@
1
- import { type AnyNodeId, type SolarPanelNode, useScene } from '@pascal-app/core'
1
+ import { type AnyNodeId, type SolarPanelNode, useScene, usePascalI18n } from '@pascal-app/core'
2
2
  import { useViewer } from '@pascal-app/viewer'
3
3
  import Image from 'next/image'
4
4
  import { memo, useCallback, useState } from 'react'
@@ -6,6 +6,7 @@ import { SnapTargetIcon } from '../../../snap-target-badge'
6
6
  import { InlineRenameInput } from './inline-rename-input'
7
7
  import { focusTreeNode, handleTreeSelection, TreeNodeWrapper } from './tree-node'
8
8
  import { TreeNodeActions } from './tree-node-actions'
9
+ import { nodeLabelDescriptor } from '../../../../../i18n/node-messages'
9
10
 
10
11
  interface SolarPanelTreeNodeProps {
11
12
  nodeId: AnyNodeId
@@ -18,6 +19,7 @@ export const SolarPanelTreeNode = memo(function SolarPanelTreeNode({
18
19
  depth,
19
20
  isLast,
20
21
  }: SolarPanelTreeNodeProps) {
22
+ const i18n = usePascalI18n()
21
23
  const [isEditing, setIsEditing] = useState(false)
22
24
  const isVisible = useScene((s) => s.nodes[nodeId]?.visible !== false)
23
25
  const node = useScene((s) => s.nodes[nodeId] as SolarPanelNode | undefined)
@@ -39,7 +41,7 @@ export const SolarPanelTreeNode = memo(function SolarPanelTreeNode({
39
41
  [nodeId, setSelection],
40
42
  )
41
43
 
42
- const defaultName = node?.name || 'Solar Panel'
44
+ const defaultName = node?.name || i18n._(nodeLabelDescriptor('solar-panel', 'Solar Panel'))
43
45
 
44
46
  return (
45
47
  <TreeNodeWrapper
@@ -1,12 +1,13 @@
1
1
  'use client'
2
2
 
3
- import { type SpawnNode, useScene } from '@pascal-app/core'
3
+ import { type SpawnNode, useScene, usePascalI18n } from '@pascal-app/core'
4
4
  import { useViewer } from '@pascal-app/viewer'
5
5
  import Image from 'next/image'
6
6
  import { memo, useCallback, useState } from 'react'
7
7
  import { InlineRenameInput } from './inline-rename-input'
8
8
  import { focusTreeNode, handleTreeSelection, TreeNodeWrapper } from './tree-node'
9
9
  import { TreeNodeActions } from './tree-node-actions'
10
+ import { nodeLabelDescriptor } from '../../../../../i18n/node-messages'
10
11
 
11
12
  interface SpawnTreeNodeProps {
12
13
  nodeId: SpawnNode['id']
@@ -19,6 +20,7 @@ export const SpawnTreeNode = memo(function SpawnTreeNode({
19
20
  depth,
20
21
  isLast,
21
22
  }: SpawnTreeNodeProps) {
23
+ const i18n = usePascalI18n()
22
24
  const [isEditing, setIsEditing] = useState(false)
23
25
  const isVisible = useScene((s) => s.nodes[nodeId]?.visible !== false)
24
26
  const isSelected = useViewer((state) => state.selection.selectedIds.includes(nodeId))
@@ -60,7 +62,7 @@ export const SpawnTreeNode = memo(function SpawnTreeNode({
60
62
  isVisible={isVisible}
61
63
  label={
62
64
  <InlineRenameInput
63
- defaultName="Spawn Point"
65
+ defaultName={i18n._(nodeLabelDescriptor('spawn', 'Spawn Point'))}
64
66
  isEditing={isEditing}
65
67
  nodeId={nodeId}
66
68
  onStartEditing={() => setIsEditing(true)}
@@ -1,4 +1,4 @@
1
- import { type AnyNodeId, type StairNode, type StairSegmentNode, useScene } from '@pascal-app/core'
1
+ import { type AnyNodeId, type StairNode, type StairSegmentNode, useScene, usePascalI18n} from '@pascal-app/core'
2
2
  import { useViewer } from '@pascal-app/viewer'
3
3
  import { AnimatePresence } from 'motion/react'
4
4
  import Image from 'next/image'
@@ -8,6 +8,7 @@ import { InlineRenameInput } from './inline-rename-input'
8
8
  import { focusTreeNode, handleTreeSelection, TreeNodeWrapper } from './tree-node'
9
9
  import { TreeNodeActions } from './tree-node-actions'
10
10
  import { DropIndicatorLine, useTreeNodeDrag } from './tree-node-drag'
11
+ import { staircaseDefaultName, stairSegmentDefaultName } from './default-names'
11
12
 
12
13
  interface StairTreeNodeProps {
13
14
  nodeId: AnyNodeId
@@ -20,6 +21,7 @@ export const StairTreeNode = memo(function StairTreeNode({
20
21
  depth,
21
22
  isLast,
22
23
  }: StairTreeNodeProps) {
24
+ const i18n = usePascalI18n()
23
25
  const [isEditing, setIsEditing] = useState(false)
24
26
  const [expanded, setExpanded] = useState(false)
25
27
  const isVisible = useScene((s) => s.nodes[nodeId]?.visible !== false)
@@ -79,7 +81,7 @@ export const StairTreeNode = memo(function StairTreeNode({
79
81
  }, [isDropTarget, expanded])
80
82
 
81
83
  const segmentCount = segments.length
82
- const defaultName = `Staircase (${segmentCount} segment${segmentCount !== 1 ? 's' : ''})`
84
+ const defaultName = staircaseDefaultName(i18n, segmentCount)
83
85
 
84
86
  // Hide the dragged segment from every stair while dragging
85
87
  const visibleSegments = drag ? segments.filter((seg) => seg.id !== drag.nodeId) : segments
@@ -158,6 +160,7 @@ function StairSegmentTreeNode({
158
160
  depth: number
159
161
  isLast?: boolean
160
162
  }) {
163
+ const i18n = usePascalI18n()
161
164
  const [isEditing, setIsEditing] = useState(false)
162
165
  const isSelected = useViewer((state) => state.selection.selectedIds.includes(node.id))
163
166
  const isHovered = useViewer((state) => state.hoveredId === node.id)
@@ -177,18 +180,16 @@ function StairSegmentTreeNode({
177
180
  const handlePointerDown = useCallback(
178
181
  (e: React.PointerEvent) => {
179
182
  if (e.button !== 0) return
180
- const typeLabel = node.segmentType === 'stair' ? 'Flight' : 'Landing'
181
- const label = `${typeLabel} (${node.width.toFixed(1)}×${node.length.toFixed(1)}m)`
183
+ const label = stairSegmentDefaultName(i18n, node.segmentType, node.width, node.length)
182
184
  startDrag(node.id, node.type, node.parentId as string, label, e.clientX, e.clientY)
183
185
  },
184
- [node.id, node.type, node.parentId, node.segmentType, node.width, node.length, startDrag],
186
+ [node.id, node.type, node.parentId, node.segmentType, node.width, node.length, startDrag, i18n],
185
187
  )
186
188
 
187
189
  const handleStartEditing = useCallback(() => setIsEditing(true), [])
188
190
  const handleStopEditing = useCallback(() => setIsEditing(false), [])
189
191
 
190
- const typeLabel = node.segmentType === 'stair' ? 'Flight' : 'Landing'
191
- const defaultName = `${typeLabel} (${node.width.toFixed(1)}×${node.length.toFixed(1)}m)`
192
+ const defaultName = stairSegmentDefaultName(i18n, node.segmentType, node.width, node.length)
192
193
 
193
194
  return (
194
195
  <div data-drop-child={node.id}>
@@ -1,4 +1,4 @@
1
- import { type AnyNodeId, emitter, useScene } from '@pascal-app/core'
1
+ import { type AnyNodeId, emitter, useScene, usePascalI18n } from '@pascal-app/core'
2
2
  import { useViewer } from '@pascal-app/viewer'
3
3
  import { Camera, Eye, EyeOff, Trash2 } from 'lucide-react'
4
4
  import { memo, useState } from 'react'
@@ -13,6 +13,7 @@ interface TreeNodeActionsProps {
13
13
  }
14
14
 
15
15
  export const TreeNodeActions = memo(function TreeNodeActions({ nodeId }: TreeNodeActionsProps) {
16
+ const i18n = usePascalI18n()
16
17
  const [open, setOpen] = useState(false)
17
18
  const updateNode = useScene((state) => state.updateNode)
18
19
  const updateNodes = useScene((state) => state.updateNodes)
@@ -57,7 +58,7 @@ export const TreeNodeActions = memo(function TreeNodeActions({ nodeId }: TreeNod
57
58
  <button
58
59
  className="flex h-6 w-6 cursor-pointer items-center justify-center rounded-md text-muted-foreground transition-colors hover:bg-black/5 hover:text-foreground dark:hover:bg-white/10"
59
60
  onClick={toggleVisibility}
60
- title={isVisible ? 'Hide' : 'Show'}
61
+ title={isVisible ? i18n._({ id: 'editor.sidebar.panels.sitePanel.treeNodeActions.hide', message: 'Hide' }) : i18n._({ id: 'editor.sidebar.panels.sitePanel.treeNodeActions.show', message: 'Show' })}
61
62
  >
62
63
  {isVisible ? <Eye className="h-3 w-3" /> : <EyeOff className="h-3 w-3 opacity-50" />}
63
64
  </button>
@@ -67,7 +68,7 @@ export const TreeNodeActions = memo(function TreeNodeActions({ nodeId }: TreeNod
67
68
  <button
68
69
  className="relative flex h-6 w-6 cursor-pointer items-center justify-center rounded-md text-muted-foreground transition-colors hover:bg-black/5 hover:text-foreground dark:hover:bg-white/10"
69
70
  onClick={(e) => e.stopPropagation()}
70
- title="Camera snapshot"
71
+ title={i18n._({ id: 'editor.sidebar.panels.sitePanel.treeNodeActions.cameraSnapshot', message: 'Camera snapshot' })}
71
72
  >
72
73
  <Camera className="h-3 w-3" />
73
74
  {hasCamera && (
@@ -88,7 +89,7 @@ export const TreeNodeActions = memo(function TreeNodeActions({ nodeId }: TreeNod
88
89
  onClick={handleViewCamera}
89
90
  >
90
91
  <Camera className="h-3.5 w-3.5" />
91
- View snapshot
92
+ {i18n._({ id: 'editor.sidebar.panels.sitePanel.treeNodeActions.viewSnapshot', message: 'View snapshot' })}
92
93
  </button>
93
94
  )}
94
95
  <button
@@ -96,7 +97,7 @@ export const TreeNodeActions = memo(function TreeNodeActions({ nodeId }: TreeNod
96
97
  onClick={handleCaptureCamera}
97
98
  >
98
99
  <Camera className="h-3.5 w-3.5" />
99
- {hasCamera ? 'Update snapshot' : 'Take snapshot'}
100
+ {hasCamera ? i18n._({ id: 'editor.sidebar.panels.sitePanel.treeNodeActions.updateSnapshot', message: 'Update snapshot' }) : i18n._({ id: 'editor.sidebar.panels.sitePanel.treeNodeActions.takeSnapshot', message: 'Take snapshot' })}
100
101
  </button>
101
102
  {hasCamera && (
102
103
  <button
@@ -104,7 +105,7 @@ export const TreeNodeActions = memo(function TreeNodeActions({ nodeId }: TreeNod
104
105
  onClick={handleClearCamera}
105
106
  >
106
107
  <Trash2 className="h-3.5 w-3.5" />
107
- Clear snapshot
108
+ {i18n._({ id: 'editor.sidebar.panels.sitePanel.treeNodeActions.clearSnapshot', message: 'Clear snapshot' })}
108
109
  </button>
109
110
  )}
110
111
  </div>
@@ -1,4 +1,11 @@
1
- import { type AnyNode, type AnyNodeId, emitter, nodeRegistry, useScene } from '@pascal-app/core'
1
+ import {
2
+ type AnyNode,
3
+ type AnyNodeId,
4
+ emitter,
5
+ nodeRegistry,
6
+ usePascalI18n,
7
+ useScene,
8
+ } from '@pascal-app/core'
2
9
  import { useViewer } from '@pascal-app/viewer'
3
10
  import { ChevronRight } from 'lucide-react'
4
11
  import { AnimatePresence, motion } from 'motion/react'
@@ -231,6 +238,7 @@ export const TreeNodeWrapper = forwardRef<HTMLDivElement, TreeNodeWrapperProps>(
231
238
  },
232
239
  ref,
233
240
  ) {
241
+ const i18n = usePascalI18n()
234
242
  const rowRef = useRef<HTMLDivElement>(null)
235
243
 
236
244
  useEffect(() => {
@@ -286,7 +294,14 @@ export const TreeNodeWrapper = forwardRef<HTMLDivElement, TreeNodeWrapperProps>(
286
294
  {/* A row with nothing to fold keeps the space, and a click there selects the row. */}
287
295
  {hasChildren ? (
288
296
  <button
297
+ aria-expanded={expanded}
298
+ aria-label={
299
+ expanded
300
+ ? i18n._({ id: 'editor.layers.collapse', message: 'Collapse' })
301
+ : i18n._({ id: 'editor.layers.expand', message: 'Expand' })
302
+ }
289
303
  className="z-10 flex h-4 w-4 shrink-0 items-center justify-center bg-inherit"
304
+ type="button"
290
305
  onClick={(e) => {
291
306
  e.stopPropagation()
292
307
  onToggle()
@@ -1,11 +1,11 @@
1
1
  import {
2
2
  type AnyNodeId,
3
- getLevelDisplayName,
4
3
  type LevelNode,
5
4
  type UnitNode,
6
5
  unitWarnings,
7
6
  useScene,
8
- type ZoneNode,
7
+ type ZoneNode, usePascalI18n,
8
+ useLocale,
9
9
  } from '@pascal-app/core'
10
10
  import { useViewer } from '@pascal-app/viewer'
11
11
  import { AlertTriangle, Trash2, X } from 'lucide-react'
@@ -15,6 +15,9 @@ import { ColorDot } from './../../../../../components/ui/primitives/color-dot'
15
15
  import { assignZoneToUnit, enterUnitFocus } from './../../../../../lib/units'
16
16
  import { InlineRenameInput } from './inline-rename-input'
17
17
  import { TreeNodeWrapper } from './tree-node'
18
+ import { nodeLabelDescriptor } from '../../../../../i18n/node-messages'
19
+ import { localizedLevelName } from '../../../../../i18n/level-name'
20
+ import { displayRoomName } from '../../../../../lib/room-name-messages'
18
21
 
19
22
  type UnitWarningCode = ReturnType<typeof unitWarnings>[number]['code']
20
23
 
@@ -40,10 +43,12 @@ export const UnitZoneRow = memo(function UnitZoneRow({
40
43
  isLast,
41
44
  onRemove,
42
45
  }: UnitZoneRowProps) {
46
+ const i18n = usePascalI18n()
47
+ const locale = useLocale()
43
48
  const zone = useScene((s) => s.nodes[zoneId] as ZoneNode | undefined)
44
49
  const levelLabel = useScene((s) => {
45
50
  const level = zone?.parentId ? (s.nodes[zone.parentId as AnyNodeId] as LevelNode | undefined) : undefined
46
- return level?.type === 'level' ? getLevelDisplayName(level) : null
51
+ return level?.type === 'level' ? localizedLevelName(level, i18n, locale) : null
47
52
  })
48
53
  const isSelected = useViewer((s) => s.selection.zoneId === zoneId)
49
54
  const isHovered = useViewer((s) => s.hoveredId === zoneId)
@@ -71,7 +76,7 @@ export const UnitZoneRow = memo(function UnitZoneRow({
71
76
  event.stopPropagation()
72
77
  onRemove(zoneId)
73
78
  }}
74
- title="Remove from unit"
79
+ title={i18n._({ id: 'editor.sidebar.panels.sitePanel.unitTreeNode.removeFromUnit', message: 'Remove from unit' })}
75
80
  type="button"
76
81
  >
77
82
  <X className="h-3 w-3" />
@@ -93,7 +98,7 @@ export const UnitZoneRow = memo(function UnitZoneRow({
93
98
  keepIconColor
94
99
  label={
95
100
  <span className="flex min-w-0 items-baseline gap-1.5">
96
- <span className="truncate">{zone.name || (zone.spaceRole === 'room' ? 'Room' : 'Zone')}</span>
101
+ <span className="truncate">{(zone.name && displayRoomName(zone.name, i18n)) || (zone.spaceRole === 'room' ? i18n._({ id: 'editor.sidebar.panels.sitePanel.unitTreeNode.room', message: 'Room' }) : i18n._({ id: 'editor.sidebar.panels.sitePanel.unitTreeNode.zone', message: 'Zone' }))}</span>
97
102
  {levelLabel && (
98
103
  <span className="shrink-0 text-muted-foreground text-xs">{levelLabel}</span>
99
104
  )}
@@ -119,6 +124,7 @@ export const UnitTreeNode = memo(function UnitTreeNode({
119
124
  depth,
120
125
  isLast,
121
126
  }: UnitTreeNodeProps) {
127
+ const i18n = usePascalI18n()
122
128
  const unitId = nodeId as UnitNode['id']
123
129
  const [isEditing, setIsEditing] = useState(false)
124
130
  const [expanded, setExpanded] = useState(false)
@@ -163,7 +169,7 @@ export const UnitTreeNode = memo(function UnitTreeNode({
163
169
  event.stopPropagation()
164
170
  deleteNode(unitId)
165
171
  }}
166
- title="Delete unit"
172
+ title={i18n._({ id: 'editor.sidebar.panels.sitePanel.unitTreeNode.deleteUnit', message: 'Delete unit' })}
167
173
  type="button"
168
174
  >
169
175
  <Trash2 className="h-3 w-3" />
@@ -175,6 +181,7 @@ export const UnitTreeNode = memo(function UnitTreeNode({
175
181
  icon={
176
182
  <ColorDot
177
183
  color={color ?? '#f59e0b'}
184
+ label={i18n._({ id: 'editor.layers.unitColour', message: 'Unit colour' })}
178
185
  onChange={(next) => updateNode(unitId, { color: next })}
179
186
  />
180
187
  }
@@ -185,7 +192,7 @@ export const UnitTreeNode = memo(function UnitTreeNode({
185
192
  label={
186
193
  <span className="flex min-w-0 items-center gap-1.5">
187
194
  <InlineRenameInput
188
- defaultName="Unit"
195
+ defaultName={i18n._(nodeLabelDescriptor('unit', 'Unit'))}
189
196
  isEditing={isEditing}
190
197
  nodeId={nodeId}
191
198
  onStartEditing={() => setIsEditing(true)}
@@ -193,7 +200,7 @@ export const UnitTreeNode = memo(function UnitTreeNode({
193
200
  />
194
201
  {isFocused && (
195
202
  <span className="shrink-0 rounded-full bg-primary/15 px-1.5 text-[10px] text-primary leading-4">
196
- Focused
203
+ {i18n._({ id: 'editor.sidebar.panels.sitePanel.unitTreeNode.focused', message: 'Focused' })}
197
204
  </span>
198
205
  )}
199
206
  {warningCodes.length > 0 && (