@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,3 +1,5 @@
1
+ 'use client'
2
+
1
3
  import {
2
4
  type AnyNode,
3
5
  type AnyNodeId,
@@ -9,6 +11,8 @@ import {
9
11
  type SiteNode,
10
12
  useScene,
11
13
  type ZoneNode,
14
+ useLocale,
15
+ usePascalI18n,
12
16
  } from '@pascal-app/core'
13
17
  import { markPerfAction, useViewer } from '@pascal-app/viewer'
14
18
  import {
@@ -53,24 +57,16 @@ import {
53
57
  buildLevelDuplicateCreateOps,
54
58
  type LevelDuplicatePreset,
55
59
  } from '@pascal-app/core/building'
56
- import { getDefaultLevelName, getLevelDisplayName } from '@pascal-app/core'
57
60
  import { addStoreyOnTop } from './../../../../../lib/add-storey'
61
+ import { offerExteriorWallCopy } from './../../../../../components/ui/level-wall-copy-prompt'
58
62
  import { deleteLevelWithFallbackSelection } from './../../../../../lib/level-selection'
59
- import {
60
- formatAreaLabel,
61
- getAreaUnitLabel,
62
- getLinearUnitLabel,
63
- linearUnitToMeters,
64
- metersToLinearUnit,
65
- squareMetersToAreaUnit,
66
- } from './../../../../../lib/measurements'
63
+ import { formatAreaLabel, getAreaUnitLabel, getLinearUnitLabel, linearUnitToMeters, metersToLinearUnit, squareMetersToAreaUnit } from '@pascal-app/core'
67
64
  import { createLocalGuideImage, createLocalScan } from './../../../../../lib/local-guide-image'
68
65
  import { editorHostTreeChildrenRegistry } from './../../../../../lib/host-tree-children'
69
66
  import { requestRoomDeletion } from './../../../../../lib/room-structure-commands'
70
67
  import {
71
68
  selectZoneOrRoom,
72
69
  useZoneSelected,
73
- zoneKindLabel,
74
70
  } from './../../../../../lib/room-zone-routing'
75
71
  import { createUnitInBuilding, toggleZoneMembership } from './../../../../../lib/units'
76
72
  import { cn } from './../../../../../lib/utils'
@@ -79,10 +75,13 @@ import { useUploadStore } from '../../../../../store/use-upload'
79
75
  import { MetricControl } from '../../../controls/metric-control'
80
76
  import { LevelDuplicateDialog } from '../../../level-duplicate-dialog'
81
77
  import { CollectionsSection } from './collection-tree-node'
82
- import { InlineRenameInput } from './inline-rename-input'
78
+ import { InlineRenameInput, RoomRenameInput } from './inline-rename-input'
83
79
  import { ZoneMembershipCheckbox } from './zone-membership-checkbox'
84
80
  import { focusTreeNode, TreeNode, TreeNodeWrapper } from './tree-node'
85
81
  import { TreeNodeDragProvider } from './tree-node-drag'
82
+ import { zoneDefaultName } from './default-names'
83
+ import { nodeLabelDescriptor } from '../../../../../i18n/node-messages'
84
+ import { authoredLevelName, localizedLevelName } from '../../../../../i18n/level-name'
86
85
 
87
86
  // ============================================================================
88
87
  // PROPERTY LINE SECTION
@@ -126,6 +125,7 @@ function useSiteNode(): SiteNode | null {
126
125
  }
127
126
 
128
127
  const PropertyLineSection = memo(function PropertyLineSection() {
128
+ const i18n = usePascalI18n()
129
129
  const siteNode = useSiteNode()
130
130
  const updateNode = useScene((state) => state.updateNode)
131
131
  const mode = useEditor((state) => state.mode)
@@ -194,7 +194,7 @@ const PropertyLineSection = memo(function PropertyLineSection() {
194
194
 
195
195
  <div className="flex items-center gap-2">
196
196
  <Pentagon className="h-4 w-4 text-muted-foreground" />
197
- <span className="font-medium text-sm">Property Line</span>
197
+ <span className="font-medium text-sm">{i18n._({ id: 'editor.sidebar.panels.sitePanel.propertyLine', message: 'Property Line' })}</span>
198
198
  </div>
199
199
  <button
200
200
  className={cn(
@@ -212,13 +212,13 @@ const PropertyLineSection = memo(function PropertyLineSection() {
212
212
  {/* Measurements */}
213
213
  <div className="relative flex gap-3 pr-3 pb-2 pl-10">
214
214
  <div className="text-muted-foreground text-xs">
215
- Area:{' '}
215
+ {i18n._({ id: 'editor.sidebar.panels.sitePanel.area', message: 'Area:' })}{' '}
216
216
  <span className="text-foreground">
217
217
  {displayArea.toFixed(1)} {getAreaUnitLabel(viewerUnit)}
218
218
  </span>
219
219
  </div>
220
220
  <div className="text-muted-foreground text-xs">
221
- Perimeter:{' '}
221
+ {i18n._({ id: 'editor.sidebar.panels.sitePanel.perimeter', message: 'Perimeter:' })}{' '}
222
222
  <span className="text-foreground">
223
223
  {displayPerimeter.toFixed(1)} {linearLabel}
224
224
  </span>
@@ -272,7 +272,7 @@ const PropertyLineSection = memo(function PropertyLineSection() {
272
272
  onClick={handleAddPoint}
273
273
  >
274
274
  <Plus className="h-3 w-3" />
275
- Add point
275
+ {i18n._({ id: 'editor.sidebar.panels.sitePanel.addPoint', message: 'Add point' })}
276
276
  </button>
277
277
  </div>
278
278
  )}
@@ -297,6 +297,7 @@ const CameraPopover = memo(function CameraPopover({
297
297
  onOpenChange: (open: boolean) => void
298
298
  buttonClassName?: string
299
299
  }) {
300
+ const i18n = usePascalI18n()
300
301
  const updateNode = useScene((state) => state.updateNode)
301
302
  return (
302
303
  <Popover onOpenChange={onOpenChange} open={open}>
@@ -307,7 +308,7 @@ const CameraPopover = memo(function CameraPopover({
307
308
  buttonClassName,
308
309
  )}
309
310
  onClick={(e) => e.stopPropagation()}
310
- title="Camera snapshot"
311
+ title={i18n._({ id: 'editor.sidebar.panels.sitePanel.cameraSnapshot', message: 'Camera snapshot' })}
311
312
  >
312
313
  <Camera className="h-3.5 w-3.5" />
313
314
  {hasCamera && (
@@ -332,7 +333,7 @@ const CameraPopover = memo(function CameraPopover({
332
333
  }}
333
334
  >
334
335
  <Camera className="h-3.5 w-3.5" />
335
- View snapshot
336
+ {i18n._({ id: 'editor.sidebar.panels.sitePanel.viewSnapshot', message: 'View snapshot' })}
336
337
  </button>
337
338
  )}
338
339
  <button
@@ -344,7 +345,7 @@ const CameraPopover = memo(function CameraPopover({
344
345
  }}
345
346
  >
346
347
  <Camera className="h-3.5 w-3.5" />
347
- {hasCamera ? 'Update snapshot' : 'Take snapshot'}
348
+ {hasCamera ? i18n._({ id: 'editor.sidebar.panels.sitePanel.updateSnapshot', message: 'Update snapshot' }) : i18n._({ id: 'editor.sidebar.panels.sitePanel.takeSnapshot', message: 'Take snapshot' })}
348
349
  </button>
349
350
  {hasCamera && (
350
351
  <button
@@ -356,7 +357,7 @@ const CameraPopover = memo(function CameraPopover({
356
357
  }}
357
358
  >
358
359
  <Trash2 className="h-3.5 w-3.5" />
359
- Clear snapshot
360
+ {i18n._({ id: 'editor.sidebar.panels.sitePanel.clearSnapshot', message: 'Clear snapshot' })}
360
361
  </button>
361
362
  )}
362
363
  </div>
@@ -376,6 +377,7 @@ const ReferenceItem = memo(function ReferenceItem({
376
377
  setSelectedReferenceId: (id: string) => void
377
378
  handleDelete: (id: string, e: React.MouseEvent) => void
378
379
  }) {
380
+ const i18n = usePascalI18n()
379
381
  const [isEditing, setIsEditing] = useState(false)
380
382
  const [isExpanded, setIsExpanded] = useState(true)
381
383
  const updateNode = useScene((state) => state.updateNode)
@@ -450,13 +452,13 @@ const ReferenceItem = memo(function ReferenceItem({
450
452
  >
451
453
  {isCapture ? (
452
454
  <img
453
- alt="Capture"
455
+ alt={i18n._({ id: 'editor.sidebar.panels.sitePanel.capture', message: 'Capture' })}
454
456
  className="h-3.5 w-3.5 shrink-0 object-contain opacity-70 transition-opacity group-hover/ref:opacity-100"
455
457
  src="/icons/mesh.webp"
456
458
  />
457
459
  ) : (
458
460
  <img
459
- alt="Guide"
461
+ alt={i18n._({ id: 'editor.sidebar.panels.sitePanel.guide', message: 'Guide' })}
460
462
  className="h-3.5 w-3.5 shrink-0 object-contain opacity-70 transition-opacity group-hover/ref:opacity-100"
461
463
  src="/icons/floorplan.webp"
462
464
  />
@@ -477,7 +479,7 @@ const ReferenceItem = memo(function ReferenceItem({
477
479
  event.stopPropagation()
478
480
  updateNode(refNode.id, { visible: !isVisible })
479
481
  }}
480
- title={isVisible ? 'Hide' : 'Show'}
482
+ title={isVisible ? i18n._({ id: 'editor.sidebar.panels.sitePanel.hide', message: 'Hide' }) : i18n._({ id: 'editor.sidebar.panels.sitePanel.show', message: 'Show' })}
481
483
  type="button"
482
484
  >
483
485
  {isVisible ? <Eye className="h-3 w-3" /> : <EyeOff className="h-3 w-3" />}
@@ -486,7 +488,7 @@ const ReferenceItem = memo(function ReferenceItem({
486
488
  <button
487
489
  className="z-20 flex h-5 w-5 shrink-0 cursor-pointer items-center justify-center rounded-md text-muted-foreground opacity-0 transition-colors hover:bg-black/5 hover:text-foreground group-hover/ref:opacity-100 dark:hover:bg-white/10"
488
490
  onClick={(e) => handleDelete(refNode.id, e)}
489
- title="Delete"
491
+ title={i18n._({ id: 'editor.sidebar.panels.sitePanel.delete', message: 'Delete' })}
490
492
  type="button"
491
493
  >
492
494
  <Trash2 className="h-3 w-3" />
@@ -516,6 +518,7 @@ const LevelReferences = memo(function LevelReferences({
516
518
  onUploadAsset,
517
519
  onDeleteAsset,
518
520
  }: LevelReferencesProps) {
521
+ const i18n = usePascalI18n()
519
522
  const createNode = useScene((s) => s.createNode)
520
523
  const deleteNode = useScene((s) => s.deleteNode)
521
524
  const setSelection = useViewer((s) => s.setSelection)
@@ -676,7 +679,12 @@ const LevelReferences = memo(function LevelReferences({
676
679
  ) : (
677
680
  <Plus className="h-3.5 w-3.5" />
678
681
  )}
679
- {uploading ? `Uploading ${uploadingType}... ${progress}%` : 'Upload scan/floorplan'}
682
+ {uploading ? i18n._({
683
+ id: 'editor.sidebar.panels.sitePanel.uploadingProgress',
684
+ message:
685
+ '{assetType, select, guide {Uploading guide... {progress}%} other {Uploading scan... {progress}%}}',
686
+ values: { assetType: uploadingType, progress },
687
+ }) : i18n._({ id: 'editor.sidebar.panels.sitePanel.uploadScanFloorplan', message: 'Upload scan/floorplan' })}
680
688
  </button>
681
689
 
682
690
  <input
@@ -736,6 +744,8 @@ const LevelItem = memo(function LevelItem({
736
744
  onUploadAsset?: (projectId: string, levelId: string, file: File, type: 'scan' | 'guide') => void
737
745
  onDeleteAsset?: (projectId: string, url: string) => void
738
746
  }) {
747
+ const i18n = usePascalI18n()
748
+ const locale = useLocale()
739
749
  const [cameraPopoverOpen, setCameraPopoverOpen] = useState(false)
740
750
  const [duplicateDialogOpen, setDuplicateDialogOpen] = useState(false)
741
751
  const [isEditing, setIsEditing] = useState(false)
@@ -820,7 +830,14 @@ const LevelItem = memo(function LevelItem({
820
830
 
821
831
  <div className="relative z-20 flex h-8 items-center pr-1 pl-[28px]">
822
832
  <button
833
+ aria-expanded={isExpanded}
834
+ aria-label={
835
+ isExpanded
836
+ ? i18n._({ id: 'editor.layers.collapse', message: 'Collapse' })
837
+ : i18n._({ id: 'editor.layers.expand', message: 'Expand' })
838
+ }
823
839
  className="z-20 flex h-4 w-4 shrink-0 cursor-pointer items-center justify-center bg-inherit"
840
+ type="button"
824
841
  onClick={(e) => {
825
842
  e.stopPropagation()
826
843
  if (isSelected) {
@@ -840,7 +857,7 @@ const LevelItem = memo(function LevelItem({
840
857
 
841
858
  <div className="flex h-8 min-w-0 flex-1 cursor-pointer items-center gap-2 py-0 pl-0.5 text-sm">
842
859
  <img
843
- alt="Level"
860
+ alt={i18n._({ id: 'editor.sidebar.panels.sitePanel.level', message: 'Level' })}
844
861
  className={cn(
845
862
  'h-4 w-4 shrink-0 object-contain transition-all duration-200',
846
863
  !isSelected && 'opacity-60 grayscale',
@@ -848,8 +865,9 @@ const LevelItem = memo(function LevelItem({
848
865
  src="/icons/level.webp"
849
866
  />
850
867
  <InlineRenameInput
851
- defaultName={getDefaultLevelName(level.level)}
852
- displayName={getLevelDisplayName(level)}
868
+ defaultName={localizedLevelName({ level: level.level }, i18n, locale)}
869
+ displayName={localizedLevelName(level, i18n, locale)}
870
+ editableName={(name) => authoredLevelName({ name, level: level.level })}
853
871
  isEditing={isEditing}
854
872
  nodeId={level.id}
855
873
  onStartEditing={() => setIsEditing(true)}
@@ -867,7 +885,7 @@ const LevelItem = memo(function LevelItem({
867
885
  : 'text-muted-foreground hover:bg-accent hover:text-foreground',
868
886
  )}
869
887
  onClick={(e) => e.stopPropagation()}
870
- title="Camera snapshot"
888
+ title={i18n._({ id: 'editor.sidebar.panels.sitePanel.cameraSnapshot', message: 'Camera snapshot' })}
871
889
  >
872
890
  <Camera className="h-3.5 w-3.5" />
873
891
  {level.camera && (
@@ -892,7 +910,7 @@ const LevelItem = memo(function LevelItem({
892
910
  }}
893
911
  >
894
912
  <Camera className="h-3.5 w-3.5" />
895
- View snapshot
913
+ {i18n._({ id: 'editor.sidebar.panels.sitePanel.viewSnapshot', message: 'View snapshot' })}
896
914
  </button>
897
915
  )}
898
916
  <button
@@ -904,7 +922,7 @@ const LevelItem = memo(function LevelItem({
904
922
  }}
905
923
  >
906
924
  <Camera className="h-3.5 w-3.5" />
907
- {level.camera ? 'Update snapshot' : 'Take snapshot'}
925
+ {level.camera ? i18n._({ id: 'editor.sidebar.panels.sitePanel.updateSnapshot', message: 'Update snapshot' }) : i18n._({ id: 'editor.sidebar.panels.sitePanel.takeSnapshot', message: 'Take snapshot' })}
908
926
  </button>
909
927
  {level.camera && (
910
928
  <button
@@ -916,7 +934,7 @@ const LevelItem = memo(function LevelItem({
916
934
  }}
917
935
  >
918
936
  <Trash2 className="h-3.5 w-3.5" />
919
- Clear snapshot
937
+ {i18n._({ id: 'editor.sidebar.panels.sitePanel.clearSnapshot', message: 'Clear snapshot' })}
920
938
  </button>
921
939
  )}
922
940
  </div>
@@ -925,6 +943,11 @@ const LevelItem = memo(function LevelItem({
925
943
  <Popover>
926
944
  <PopoverTrigger asChild>
927
945
  <button
946
+ aria-label={i18n._({
947
+ id: 'editor.layers.levelOptions',
948
+ message: '{levelName} options',
949
+ values: { levelName: localizedLevelName(level, i18n, locale) },
950
+ })}
928
951
  className={cn(
929
952
  'mr-1 flex h-6 w-6 shrink-0 cursor-pointer items-center justify-center rounded-md opacity-0 transition-colors group-hover/level:opacity-100',
930
953
  selectedLevelId === level.id
@@ -936,31 +959,31 @@ const LevelItem = memo(function LevelItem({
936
959
  <MoreHorizontal className="h-3.5 w-3.5" />
937
960
  </button>
938
961
  </PopoverTrigger>
939
- <PopoverContent align="start" className="w-48 p-1" side="right">
962
+ <PopoverContent align="start" className="w-max min-w-48 max-w-72 p-1" side="right">
940
963
  <button
941
964
  className="flex w-full cursor-pointer items-center gap-2 rounded px-3 py-1.5 text-left text-sm transition-colors hover:bg-accent"
942
965
  onClick={() => handleDuplicateLevel()}
943
- title="Duplicate level"
966
+ title={i18n._({ id: 'editor.sidebar.panels.sitePanel.duplicateLevel', message: 'Duplicate level' })}
944
967
  >
945
968
  <Copy className="h-3.5 w-3.5" />
946
- Duplicate
969
+ {i18n._({ id: 'editor.sidebar.panels.sitePanel.duplicate', message: 'Duplicate' })}
947
970
  </button>
948
971
  <button
949
972
  className="flex w-full cursor-pointer items-center gap-2 rounded px-3 py-1.5 text-left text-sm transition-colors hover:bg-accent"
950
973
  onClick={() => setDuplicateDialogOpen(true)}
951
- title="Duplicate level with options"
974
+ title={i18n._({ id: 'editor.sidebar.panels.sitePanel.duplicateLevelWithOptions', message: 'Duplicate level with options' })}
952
975
  >
953
976
  <Copy className="h-3.5 w-3.5" />
954
- Duplicate with options...
977
+ {i18n._({ id: 'editor.sidebar.panels.sitePanel.duplicateWithOptions', message: 'Duplicate with options...' })}
955
978
  </button>
956
979
  <button
957
980
  className="flex w-full items-center gap-2 rounded px-3 py-1.5 text-left text-sm transition-colors enabled:cursor-pointer enabled:hover:bg-accent enabled:hover:text-red-600 disabled:cursor-not-allowed disabled:opacity-50"
958
981
  disabled={!canDeleteLevel}
959
982
  onClick={() => deleteLevelWithFallbackSelection(level.id)}
960
- title={canDeleteLevel ? 'Delete level' : 'The ground level cannot be deleted'}
983
+ title={canDeleteLevel ? i18n._({ id: 'editor.sidebar.panels.sitePanel.deleteLevel', message: 'Delete level' }) : i18n._({ id: 'editor.sidebar.panels.sitePanel.theGroundLevelCannotBeDeleted', message: 'The ground level cannot be deleted' })}
961
984
  >
962
985
  <Trash2 className="h-3.5 w-3.5" />
963
- Delete
986
+ {i18n._({ id: 'editor.sidebar.panels.sitePanel.delete', message: 'Delete' })}
964
987
  </button>
965
988
  </PopoverContent>
966
989
  </Popover>
@@ -977,7 +1000,7 @@ const LevelItem = memo(function LevelItem({
977
1000
  <div className="relative border-border/50 border-b py-2 pr-3 pl-[60px]">
978
1001
  <div className="pointer-events-none absolute top-0 bottom-0 left-[45px] z-10 w-px bg-border/50" />
979
1002
  <MetricControl
980
- label="Base elevation"
1003
+ label={i18n._({ id: 'editor.sidebar.panels.sitePanel.baseElevation', message: 'Base elevation' })}
981
1004
  onChange={(value) => updateNode(level.id, { baseElevation: value })}
982
1005
  precision={2}
983
1006
  step={0.05}
@@ -1014,6 +1037,7 @@ const LevelsSection = memo(function LevelsSection({
1014
1037
  onUploadAsset?: (projectId: string, levelId: string, file: File, type: 'scan' | 'guide') => void
1015
1038
  onDeleteAsset?: (projectId: string, url: string) => void
1016
1039
  } = {}) {
1040
+ const i18n = usePascalI18n()
1017
1041
  const updateNode = useScene((state) => state.updateNode)
1018
1042
  const selectedBuildingId = useViewer((state) => state.selection.buildingId)
1019
1043
  const selectedLevelId = useViewer((state) => state.selection.levelId)
@@ -1038,6 +1062,7 @@ const LevelsSection = memo(function LevelsSection({
1038
1062
  const handleAddLevel = () => {
1039
1063
  const newLevel = addStoreyOnTop(building.id)
1040
1064
  setSelection({ buildingId: building.id, levelId: newLevel.id })
1065
+ offerExteriorWallCopy(newLevel.id)
1041
1066
  }
1042
1067
 
1043
1068
  return (
@@ -1056,7 +1081,7 @@ const LevelsSection = memo(function LevelsSection({
1056
1081
  <div className="relative z-10 flex items-center pr-1 pl-[38px]">
1057
1082
  <Plus className="h-3.5 w-3.5" />
1058
1083
  </div>
1059
- <span className="truncate">Add level</span>
1084
+ <span className="truncate">{i18n._({ id: 'editor.sidebar.panels.sitePanel.addLevel', message: 'Add level' })}</span>
1060
1085
  </button>
1061
1086
  {levels.length === 0 && (
1062
1087
  <div className="relative flex h-8 select-none items-center border-border/50 border-b py-0 pr-2 pl-[38px] text-muted-foreground text-xs">
@@ -1064,7 +1089,7 @@ const LevelsSection = memo(function LevelsSection({
1064
1089
  <div className="pointer-events-none absolute top-0 bottom-1/2 left-[21px] w-px bg-border/50" />
1065
1090
  {/* Horizontal branch line */}
1066
1091
  <div className="pointer-events-none absolute top-1/2 left-[21px] h-px w-[11px] bg-border/50" />
1067
- No levels yet
1092
+ {i18n._({ id: 'editor.sidebar.panels.sitePanel.noLevelsYet', message: 'No levels yet' })}
1068
1093
  </div>
1069
1094
  )}
1070
1095
  {[...levels].reverse().map((level, index) => (
@@ -1091,6 +1116,7 @@ const UnitsSection = memo(function UnitsSection({
1091
1116
  }: {
1092
1117
  buildingId: BuildingNode['id']
1093
1118
  }) {
1119
+ const i18n = usePascalI18n()
1094
1120
  const unitIds = useScene(
1095
1121
  useShallow((s) => {
1096
1122
  const building = s.nodes[buildingId] as BuildingNode | undefined
@@ -1117,7 +1143,7 @@ const UnitsSection = memo(function UnitsSection({
1117
1143
  <button
1118
1144
  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"
1119
1145
  onClick={handleNewUnit}
1120
- title="New unit"
1146
+ title={i18n._({ id: 'editor.sidebar.panels.sitePanel.newUnit', message: 'New unit' })}
1121
1147
  type="button"
1122
1148
  >
1123
1149
  <Plus className="h-3 w-3" />
@@ -1129,7 +1155,7 @@ const UnitsSection = memo(function UnitsSection({
1129
1155
  icon={<Group className="h-3.5 w-3.5" />}
1130
1156
  label={
1131
1157
  <span className="flex items-center gap-1.5">
1132
- Units
1158
+ {i18n._({ id: 'editor.sidebar.panels.sitePanel.units', message: 'Units' })}
1133
1159
  {hasUnits && <span className="text-muted-foreground text-xs">{unitIds.length}</span>}
1134
1160
  </span>
1135
1161
  }
@@ -1145,7 +1171,7 @@ const UnitsSection = memo(function UnitsSection({
1145
1171
  hasChildren={false}
1146
1172
  icon={<Plus className="h-3.5 w-3.5" />}
1147
1173
  isLast
1148
- label="New unit"
1174
+ label={i18n._({ id: 'editor.sidebar.panels.sitePanel.newUnit', message: 'New unit' })}
1149
1175
  onClick={handleNewUnit}
1150
1176
  onToggle={() => {}}
1151
1177
  />
@@ -1163,6 +1189,7 @@ const ZoneItem = memo(function ZoneItem({
1163
1189
  depth: number
1164
1190
  isLast?: boolean
1165
1191
  }) {
1192
+ const i18n = usePascalI18n()
1166
1193
  const [isEditing, setIsEditing] = useState(false)
1167
1194
  const [cameraPopoverOpen, setCameraPopoverOpen] = useState(false)
1168
1195
  const deleteNode = useScene((state) => state.deleteNode)
@@ -1190,7 +1217,7 @@ const ZoneItem = memo(function ZoneItem({
1190
1217
  }
1191
1218
  }, [isSelected])
1192
1219
 
1193
- const defaultName = `${zoneKindLabel(zone)} (${formatAreaLabel(calculatePolygonArea(zone.polygon), unit)})`
1220
+ const defaultName = zoneDefaultName(i18n, zone, formatAreaLabel(calculatePolygonArea(zone.polygon), unit))
1194
1221
 
1195
1222
  const handleClick = () => {
1196
1223
  setMode('select')
@@ -1250,14 +1277,22 @@ const ZoneItem = memo(function ZoneItem({
1250
1277
  <ZoneMembershipCheckbox
1251
1278
  checked={focusedUnit.members.includes(zone.id)}
1252
1279
  onToggle={() => toggleZoneMembership(focusedUnit.id, zone.id)}
1253
- unitName={focusedUnit.name || 'Unit'}
1280
+ unitName={focusedUnit.name || i18n._(nodeLabelDescriptor('unit', 'Unit'))}
1254
1281
  />
1255
1282
  )}
1256
1283
  <span className={cn('mr-2', !isSelected && 'opacity-40')}>
1257
- <ColorDot color={zone.color} onChange={handleColorChange} />
1284
+ <ColorDot
1285
+ color={zone.color}
1286
+ label={i18n._({
1287
+ id: 'editor.layers.spaceColour',
1288
+ message: '{role, select, room {Room colour} other {Zone colour}}',
1289
+ values: { role: zone.spaceRole ?? 'generic' },
1290
+ })}
1291
+ onChange={handleColorChange}
1292
+ />
1258
1293
  </span>
1259
1294
  <div className="min-w-0 flex-1 pr-1">
1260
- <InlineRenameInput
1295
+ <RoomRenameInput
1261
1296
  defaultName={defaultName}
1262
1297
  isEditing={isEditing}
1263
1298
  nodeId={zone.id}
@@ -1272,7 +1307,7 @@ const ZoneItem = memo(function ZoneItem({
1272
1307
  <button
1273
1308
  className="relative flex h-6 w-6 cursor-pointer items-center justify-center rounded-md text-muted-foreground opacity-0 transition-colors hover:bg-black/5 hover:text-foreground group-hover/row:opacity-100 dark:hover:bg-white/10"
1274
1309
  onClick={(e) => e.stopPropagation()}
1275
- title="Camera snapshot"
1310
+ title={i18n._({ id: 'editor.sidebar.panels.sitePanel.cameraSnapshot', message: 'Camera snapshot' })}
1276
1311
  >
1277
1312
  <Camera className="h-3 w-3" />
1278
1313
  {zone.camera && (
@@ -1297,7 +1332,7 @@ const ZoneItem = memo(function ZoneItem({
1297
1332
  }}
1298
1333
  >
1299
1334
  <Camera className="h-3.5 w-3.5" />
1300
- View snapshot
1335
+ {i18n._({ id: 'editor.sidebar.panels.sitePanel.viewSnapshot', message: 'View snapshot' })}
1301
1336
  </button>
1302
1337
  )}
1303
1338
  <button
@@ -1309,7 +1344,7 @@ const ZoneItem = memo(function ZoneItem({
1309
1344
  }}
1310
1345
  >
1311
1346
  <Camera className="h-3.5 w-3.5" />
1312
- {zone.camera ? 'Update snapshot' : 'Take snapshot'}
1347
+ {zone.camera ? i18n._({ id: 'editor.sidebar.panels.sitePanel.updateSnapshot', message: 'Update snapshot' }) : i18n._({ id: 'editor.sidebar.panels.sitePanel.takeSnapshot', message: 'Take snapshot' })}
1313
1348
  </button>
1314
1349
  {zone.camera && (
1315
1350
  <button
@@ -1321,15 +1356,17 @@ const ZoneItem = memo(function ZoneItem({
1321
1356
  }}
1322
1357
  >
1323
1358
  <Trash2 className="h-3.5 w-3.5" />
1324
- Clear snapshot
1359
+ {i18n._({ id: 'editor.sidebar.panels.sitePanel.clearSnapshot', message: 'Clear snapshot' })}
1325
1360
  </button>
1326
1361
  )}
1327
1362
  </div>
1328
1363
  </PopoverContent>
1329
1364
  </Popover>
1330
1365
  <button
1331
- className="flex h-6 w-6 cursor-pointer items-center justify-center rounded-md text-muted-foreground opacity-0 transition-colors hover:bg-black/5 hover:text-foreground group-hover/row:opacity-100 dark:hover:bg-white/10"
1366
+ aria-label={i18n._({ id: 'editor.sidebar.panels.sitePanel.delete', message: 'Delete' })}
1367
+ className="flex h-6 w-6 cursor-pointer items-center justify-center rounded-md text-muted-foreground opacity-0 transition-colors hover:bg-black/5 hover:text-foreground group-hover/row:opacity-100 focus-visible:opacity-100 dark:hover:bg-white/10"
1332
1368
  onClick={handleDelete}
1369
+ type="button"
1333
1370
  >
1334
1371
  <Trash2 className="h-3 w-3" />
1335
1372
  </button>
@@ -1339,6 +1376,7 @@ const ZoneItem = memo(function ZoneItem({
1339
1376
  })
1340
1377
 
1341
1378
  const MultiSelectionBadge = memo(function MultiSelectionBadge() {
1379
+ const i18n = usePascalI18n()
1342
1380
  const selectedIds = useViewer((state) => state.selection.selectedIds)
1343
1381
  const setSelection = useViewer((state) => state.setSelection)
1344
1382
 
@@ -1347,11 +1385,15 @@ const MultiSelectionBadge = memo(function MultiSelectionBadge() {
1347
1385
  return (
1348
1386
  <div className="pointer-events-none sticky top-4 z-50 flex h-0 w-full justify-center overflow-visible">
1349
1387
  <div className="pointer-events-auto flex items-center gap-2.5 rounded-full border border-primary/20 bg-primary px-0.5 py-4 pl-2 font-medium text-primary-foreground text-xs shadow-black/10 shadow-lg backdrop-blur-md">
1350
- <span>{selectedIds.length} objects selected</span>
1388
+ <span>{i18n._({
1389
+ id: 'editor.sidebar.panels.sitePanel.objectsSelected',
1390
+ message: '{count, plural, one {# object selected} other {# objects selected}}',
1391
+ values: { count: selectedIds.length },
1392
+ })}</span>
1351
1393
  <button
1352
1394
  className="cursor-pointer rounded-full p-1.5 transition-colors hover:bg-primary-foreground/20"
1353
1395
  onClick={() => setSelection({ selectedIds: [] })}
1354
- title="Clear selection"
1396
+ title={i18n._({ id: 'editor.sidebar.panels.sitePanel.clearSelection', message: 'Clear selection' })}
1355
1397
  >
1356
1398
  <X className="h-4 w-4" />
1357
1399
  </button>
@@ -1361,6 +1403,7 @@ const MultiSelectionBadge = memo(function MultiSelectionBadge() {
1361
1403
  })
1362
1404
 
1363
1405
  const RoomsSection = memo(function RoomsSection() {
1406
+ const i18n = usePascalI18n()
1364
1407
  const selectedLevelId = useViewer((state) => state.selection.levelId)
1365
1408
  const armToolMode = useEditor((state) => state.armToolMode)
1366
1409
  const levelZones = useScene(
@@ -1390,7 +1433,7 @@ const RoomsSection = memo(function RoomsSection() {
1390
1433
  <button
1391
1434
  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"
1392
1435
  onClick={handleDrawZone}
1393
- title="Draw a zone"
1436
+ title={i18n._({ id: 'editor.sidebar.panels.sitePanel.drawAZone', message: 'Draw a zone' })}
1394
1437
  type="button"
1395
1438
  >
1396
1439
  <Plus className="h-3 w-3" />
@@ -1402,7 +1445,7 @@ const RoomsSection = memo(function RoomsSection() {
1402
1445
  icon={<LayoutGrid className="h-3.5 w-3.5" />}
1403
1446
  label={
1404
1447
  <span className="flex items-center gap-1.5">
1405
- Rooms
1448
+ {i18n._({ id: 'editor.sidebar.panels.sitePanel.rooms', message: 'Rooms' })}
1406
1449
  {rooms.length > 0 && (
1407
1450
  <span className="text-muted-foreground text-xs">{rooms.length}</span>
1408
1451
  )}
@@ -1413,7 +1456,7 @@ const RoomsSection = memo(function RoomsSection() {
1413
1456
  >
1414
1457
  {levelZones.length === 0 && (
1415
1458
  <div className="border-border/50 border-b py-2 pr-3 pl-[52px] text-muted-foreground text-xs">
1416
- Close walls around a space to make a room.
1459
+ {i18n._({ id: 'editor.sidebar.panels.sitePanel.closeWallsAroundASpaceTo', message: 'Close walls around a space to make a room.' })}
1417
1460
  </div>
1418
1461
  )}
1419
1462
  {rooms.map((zone, index) => (
@@ -1430,7 +1473,7 @@ const RoomsSection = memo(function RoomsSection() {
1430
1473
  className="pt-3 pb-1 pl-[52px] font-medium text-[11px] text-muted-foreground"
1431
1474
  data-other-zones
1432
1475
  >
1433
- Other zones
1476
+ {i18n._({ id: 'editor.sidebar.panels.sitePanel.otherZones', message: 'Other zones' })}
1434
1477
  </div>
1435
1478
  {others.map((zone, index) => (
1436
1479
  <ZoneItem depth={2} isLast={index === others.length - 1} key={zone.id} zone={zone} />
@@ -1443,6 +1486,7 @@ const RoomsSection = memo(function RoomsSection() {
1443
1486
  })
1444
1487
 
1445
1488
  const ContentSection = memo(function ContentSection() {
1489
+ const i18n = usePascalI18n()
1446
1490
  const selectedLevelId = useViewer((state) => state.selection.levelId)
1447
1491
  const level = useScene((s) =>
1448
1492
  selectedLevelId ? ((s.nodes[selectedLevelId] as LevelNode | undefined) ?? null) : null,
@@ -1462,7 +1506,7 @@ const ContentSection = memo(function ContentSection() {
1462
1506
 
1463
1507
  if (!level) {
1464
1508
  return (
1465
- <div className="px-3 py-4 text-muted-foreground text-sm">Select a level to view content</div>
1509
+ <div className="px-3 py-4 text-muted-foreground text-sm">{i18n._({ id: 'editor.sidebar.panels.sitePanel.selectALevelToViewContent', message: 'Select a level to view content' })}</div>
1466
1510
  )
1467
1511
  }
1468
1512
 
@@ -1474,7 +1518,7 @@ const ContentSection = memo(function ContentSection() {
1474
1518
  icon={<Boxes className="h-3.5 w-3.5" />}
1475
1519
  label={
1476
1520
  <span className="flex items-center gap-1.5">
1477
- Elements
1521
+ {i18n._({ id: 'editor.sidebar.panels.sitePanel.elements', message: 'Elements' })}
1478
1522
  {elementChildren.length > 0 && (
1479
1523
  <span className="text-muted-foreground text-xs">{elementChildren.length}</span>
1480
1524
  )}
@@ -1485,7 +1529,7 @@ const ContentSection = memo(function ContentSection() {
1485
1529
  >
1486
1530
  {elementChildren.length === 0 ? (
1487
1531
  <div className="border-border/50 border-b py-2 pr-3 pl-[52px] text-muted-foreground text-xs">
1488
- No elements on this level
1532
+ {i18n._({ id: 'editor.sidebar.panels.sitePanel.noElementsOnThisLevel', message: 'No elements on this level' })}
1489
1533
  </div>
1490
1534
  ) : (
1491
1535
  <TreeNodeDragProvider>
@@ -1520,6 +1564,7 @@ const BuildingItem = memo(function BuildingItem({
1520
1564
  onUploadAsset?: (projectId: string, levelId: string, file: File, type: 'scan' | 'guide') => void
1521
1565
  onDeleteAsset?: (projectId: string, url: string) => void
1522
1566
  }) {
1567
+ const i18n = usePascalI18n()
1523
1568
  const setSelection = useViewer((state) => state.setSelection)
1524
1569
  const phase = useEditor((state) => state.phase)
1525
1570
  const setPhase = useEditor((state) => state.setPhase)
@@ -1560,14 +1605,14 @@ const BuildingItem = memo(function BuildingItem({
1560
1605
  >
1561
1606
  <div className="flex h-full min-w-0 flex-1 cursor-pointer items-center gap-2 py-2 pl-3">
1562
1607
  <img
1563
- alt="Building"
1608
+ alt={i18n._({ id: 'editor.sidebar.panels.sitePanel.building', message: 'Building' })}
1564
1609
  className={cn(
1565
1610
  'h-5 w-5 object-contain transition-all',
1566
1611
  !isBuildingActive && 'opacity-60 grayscale',
1567
1612
  )}
1568
1613
  src="/icons/building.webp"
1569
1614
  />
1570
- <span className="truncate font-medium text-sm">{building.name || 'Building'}</span>
1615
+ <span className="truncate font-medium text-sm">{building.name || i18n._({ id: 'editor.sidebar.panels.sitePanel.building', message: 'Building' })}</span>
1571
1616
  </div>
1572
1617
  <Popover
1573
1618
  onOpenChange={(open) => setBuildingCameraOpen(open ? building.id : null)}
@@ -1582,7 +1627,7 @@ const BuildingItem = memo(function BuildingItem({
1582
1627
  : 'text-muted-foreground hover:bg-accent hover:text-foreground',
1583
1628
  )}
1584
1629
  onClick={(e) => e.stopPropagation()}
1585
- title="Camera snapshot"
1630
+ title={i18n._({ id: 'editor.sidebar.panels.sitePanel.cameraSnapshot', message: 'Camera snapshot' })}
1586
1631
  >
1587
1632
  <Camera className="h-4 w-4" />
1588
1633
  {building.camera && (
@@ -1607,7 +1652,7 @@ const BuildingItem = memo(function BuildingItem({
1607
1652
  }}
1608
1653
  >
1609
1654
  <Camera className="h-3.5 w-3.5" />
1610
- View snapshot
1655
+ {i18n._({ id: 'editor.sidebar.panels.sitePanel.viewSnapshot', message: 'View snapshot' })}
1611
1656
  </button>
1612
1657
  )}
1613
1658
  <button
@@ -1619,7 +1664,7 @@ const BuildingItem = memo(function BuildingItem({
1619
1664
  }}
1620
1665
  >
1621
1666
  <Camera className="h-3.5 w-3.5" />
1622
- {building.camera ? 'Update snapshot' : 'Take snapshot'}
1667
+ {building.camera ? i18n._({ id: 'editor.sidebar.panels.sitePanel.updateSnapshot', message: 'Update snapshot' }) : i18n._({ id: 'editor.sidebar.panels.sitePanel.takeSnapshot', message: 'Take snapshot' })}
1623
1668
  </button>
1624
1669
  {building.camera && (
1625
1670
  <button
@@ -1631,7 +1676,7 @@ const BuildingItem = memo(function BuildingItem({
1631
1676
  }}
1632
1677
  >
1633
1678
  <Trash2 className="h-3.5 w-3.5" />
1634
- Clear snapshot
1679
+ {i18n._({ id: 'editor.sidebar.panels.sitePanel.clearSnapshot', message: 'Clear snapshot' })}
1635
1680
  </button>
1636
1681
  )}
1637
1682
  </div>
@@ -1679,6 +1724,7 @@ export interface SitePanelProps {
1679
1724
  }
1680
1725
 
1681
1726
  export function SitePanel({ projectId, onUploadAsset, onDeleteAsset }: SitePanelProps = {}) {
1727
+ const i18n = usePascalI18n()
1682
1728
  const rootNodeIds = useScene((state) => state.rootNodeIds)
1683
1729
  const updateNode = useScene((state) => state.updateNode)
1684
1730
  const selectedBuildingId = useViewer((state) => state.selection.buildingId)
@@ -1703,7 +1749,7 @@ export function SitePanel({ projectId, onUploadAsset, onDeleteAsset }: SitePanel
1703
1749
 
1704
1750
  return (
1705
1751
  <LayoutGroup>
1706
- <div className="flex h-full flex-col">
1752
+ <div className="flex h-full flex-col" data-surface="site-panel">
1707
1753
  {/* Site Header */}
1708
1754
  {siteNode && (
1709
1755
  <motion.div
@@ -1725,14 +1771,14 @@ export function SitePanel({ projectId, onUploadAsset, onDeleteAsset }: SitePanel
1725
1771
  >
1726
1772
  <div className="flex items-center gap-2">
1727
1773
  <img
1728
- alt="Site"
1774
+ alt={i18n._({ id: 'editor.sidebar.panels.sitePanel.site', message: 'Site' })}
1729
1775
  className={cn(
1730
1776
  'h-5 w-5 object-contain transition-all',
1731
1777
  phase !== 'site' && 'opacity-60 grayscale',
1732
1778
  )}
1733
1779
  src="/icons/site-flag.webp"
1734
1780
  />
1735
- <span className="font-medium text-sm">{siteNode.name || 'Site'}</span>
1781
+ <span className="font-medium text-sm">{siteNode.name || i18n._({ id: 'editor.sidebar.panels.sitePanel.site', message: 'Site' })}</span>
1736
1782
  </div>
1737
1783
  <CameraPopover
1738
1784
  buttonClassName={cn(
@@ -1776,7 +1822,7 @@ export function SitePanel({ projectId, onUploadAsset, onDeleteAsset }: SitePanel
1776
1822
  {/* Buildings List */}
1777
1823
  {buildings.length === 0 ? (
1778
1824
  <motion.div className="px-3 py-4 text-muted-foreground text-sm" layout="position">
1779
- No buildings yet
1825
+ {i18n._({ id: 'editor.sidebar.panels.sitePanel.noBuildingsYet', message: 'No buildings yet' })}
1780
1826
  </motion.div>
1781
1827
  ) : (
1782
1828
  <div className="flex min-h-0 flex-1 flex-col">