@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,31 +1,52 @@
1
1
  'use client'
2
2
 
3
3
  import {
4
+ type AnyNode,
4
5
  type AnyNodeId,
5
6
  type BuildingNode,
6
7
  type GuideNode,
7
8
  type LevelNode,
8
9
  type ScanNode,
10
+ type MessageDescriptor,
9
11
  useScene,
12
+ usePascalI18n,
10
13
  } from '@pascal-app/core'
11
14
  import { useViewer } from '@pascal-app/viewer'
12
- import { Check, ChevronDown, Eye, EyeOff, Layers2, Plus, Trash2, Waypoints } from 'lucide-react'
15
+ import {
16
+ Check,
17
+ ChevronDown,
18
+ Eye,
19
+ EyeOff,
20
+ Layers2,
21
+ Plus,
22
+ SquareDashed,
23
+ Trash2,
24
+ Waypoints,
25
+ } from 'lucide-react'
13
26
  import { useCallback, useRef, useState } from 'react'
14
27
  import { useShallow } from 'zustand/react/shallow'
15
- import { getLevelDisplayName } from '@pascal-app/core'
16
28
  import { createLocalGuideImage } from '../../../lib/local-guide-image'
17
29
  import { cn } from '../../../lib/utils'
18
30
  import useEditor from '../../../store/use-editor'
19
31
  import { useUploadStore } from '../../../store/use-upload'
32
+ import {
33
+ ADJACENT_WALL_COLORS,
34
+ adjacentLevelIds,
35
+ useWallsBelowSetting,
36
+ } from '../../editor-2d/floorplan-adjacent-walls-layer'
20
37
  import { SliderControl } from '../controls/slider-control'
21
38
  import { Popover, PopoverContent, PopoverTrigger } from '../primitives/popover'
39
+ import { Switch } from '../primitives/switch'
22
40
  import { ActionButton } from './action-button'
23
41
  import { useActionMenuPopupSide } from './placement'
42
+ import { localizedLevelName } from '../../../i18n/level-name'
24
43
 
25
44
  const MAX_FILE_SIZE = 200 * 1024 * 1024 // 200MB
26
45
  const ACCEPTED_FILE_TYPES = '.glb,.gltf,image/jpeg,image/png,image/webp,image/gif'
27
- const REFERENCES_EMPTY_TEXT =
28
- 'Upload GLB meshes as scan references or blueprint images as guide references.'
46
+ const REFERENCES_EMPTY_TEXT = /* i18n */ {
47
+ id: 'editor.actionMenu.viewToggles.referencesEmpty',
48
+ message: 'Upload GLB meshes as scan references or blueprint images as guide references.',
49
+ }
29
50
 
30
51
  // ── Helper: get guide images for the current level ──────────────────────────
31
52
 
@@ -84,6 +105,7 @@ function useLowerReferenceLevels(): LevelNode[] {
84
105
  // ── Shared upload button for dropdowns ──────────────────────────────────────
85
106
 
86
107
  function UploadButton({ onError }: { onError: (message: string | null) => void }) {
108
+ const i18n = usePascalI18n()
87
109
  const fileInputRef = useRef<HTMLInputElement>(null)
88
110
  const levelId = useViewer((s) => s.selection.levelId)
89
111
  const setSelection = useViewer((s) => s.setSelection)
@@ -149,7 +171,7 @@ function UploadButton({ onError }: { onError: (message: string | null) => void }
149
171
  return (
150
172
  <>
151
173
  <button
152
- aria-label="Upload scan or guide image"
174
+ aria-label={i18n._({ id: 'editor.actionMenu.viewToggles.uploadScanOrGuideImage', message: 'Upload scan or guide image' })}
153
175
  className="flex h-6 w-6 shrink-0 items-center justify-center rounded-full border border-border/40 text-muted-foreground transition-colors hover:bg-black/6 dark:hover:bg-white/10 hover:text-foreground"
154
176
  disabled={isAddingGuide}
155
177
  onClick={() => fileInputRef.current?.click()}
@@ -171,6 +193,7 @@ function UploadButton({ onError }: { onError: (message: string | null) => void }
171
193
  // ── Guides toggle + dropdown ────────────────────────────────────────────────
172
194
 
173
195
  function GuidesControl() {
196
+ const i18n = usePascalI18n()
174
197
  const popupSide = useActionMenuPopupSide()
175
198
  const showGuides = useViewer((state) => state.showGuides)
176
199
  const setShowGuides = useViewer((state) => state.setShowGuides)
@@ -212,14 +235,18 @@ function GuidesControl() {
212
235
  ? 'bg-black/9 hover:bg-black/9 dark:bg-white/15'
213
236
  : 'opacity-60 grayscale hover:bg-black/4 hover:opacity-100 hover:grayscale-0',
214
237
  )}
215
- label={`Guides: ${showGuides ? 'Visible' : 'Hidden'}`}
238
+ label={
239
+ showGuides
240
+ ? i18n._({ id: 'editor.actionMenu.viewToggles.guidesVisible', message: 'Guides: Visible' })
241
+ : i18n._({ id: 'editor.actionMenu.viewToggles.guidesHidden', message: 'Guides: Hidden' })
242
+ }
216
243
  onClick={() => setShowGuides(!showGuides)}
217
244
  size="icon"
218
245
  variant="ghost"
219
246
  >
220
247
  <div className="relative">
221
248
  <img
222
- alt="Guides"
249
+ alt={i18n._({ id: 'editor.actionMenu.viewToggles.guides', message: 'Guides' })}
223
250
  className="h-[28px] w-[28px] object-contain"
224
251
  src="/icons/floorplan.webp"
225
252
  />
@@ -233,7 +260,7 @@ function GuidesControl() {
233
260
  <PopoverTrigger asChild>
234
261
  <button
235
262
  aria-expanded={isOpen}
236
- aria-label="Guide image settings"
263
+ aria-label={i18n._({ id: 'editor.actionMenu.viewToggles.guideImageSettings', message: 'Guide image settings' })}
237
264
  className={cn(
238
265
  'flex h-11 w-6 items-center justify-center rounded-r-lg transition-colors',
239
266
  showGuides
@@ -263,10 +290,10 @@ function GuidesControl() {
263
290
  <img alt="" className="h-4 w-4 object-contain" src="/icons/floorplan.webp" />
264
291
  </span>
265
292
  <div className="min-w-0 flex-1">
266
- <p className="font-medium text-foreground text-sm">Guide images</p>
293
+ <p className="font-medium text-foreground text-sm">{i18n._({ id: 'editor.actionMenu.viewToggles.guideImages', message: 'Guide images' })}</p>
267
294
  {hasGuides && (
268
295
  <p className="text-muted-foreground text-xs">
269
- {guides.length} guide image{guides.length !== 1 ? 's' : ''} on this level
296
+ {i18n._({ id: 'editor.actionMenu.viewToggles.guideCount', message: '{count, plural, one {# guide image on this level} other {# guide images on this level}}', values: { count: guides.length } })}
270
297
  </p>
271
298
  )}
272
299
  </div>
@@ -303,14 +330,15 @@ function GuidesControl() {
303
330
  src="/icons/floorplan.webp"
304
331
  />
305
332
  <p className="truncate font-medium text-foreground text-sm">
306
- {guide.name || `Guide image ${index + 1}`}
333
+ {guide.name ||
334
+ i18n._({ id: 'editor.actionMenu.viewToggles.guideImageNumber', message: 'Guide image {number}', values: { number: index + 1 } })}
307
335
  </p>
308
336
  {selectedReferenceId === guide.id && (
309
337
  <Check className="ml-auto h-3.5 w-3.5 shrink-0 text-foreground/80" />
310
338
  )}
311
339
  </button>
312
340
  <button
313
- aria-label="Delete guide image"
341
+ aria-label={i18n._({ id: 'editor.actionMenu.viewToggles.deleteGuideImage', message: 'Delete guide image' })}
314
342
  className="flex h-5 w-5 shrink-0 items-center justify-center rounded-md text-muted-foreground/50 opacity-0 transition-all hover:bg-destructive/10 hover:text-destructive group-hover/item:opacity-100"
315
343
  onClick={(event) => {
316
344
  event.stopPropagation()
@@ -325,7 +353,7 @@ function GuidesControl() {
325
353
  </button>
326
354
  </div>
327
355
  <SliderControl
328
- label="Opacity"
356
+ label={i18n._({ id: 'editor.actionMenu.viewToggles.opacity', message: 'Opacity' })}
329
357
  max={100}
330
358
  min={0}
331
359
  onChange={(value) => handleOpacityChange(guide.id, value)}
@@ -339,7 +367,7 @@ function GuidesControl() {
339
367
  </div>
340
368
  ) : (
341
369
  <div className="rounded-xl border border-border/45 border-dashed bg-background/60 px-3 py-4 text-muted-foreground text-sm">
342
- {REFERENCES_EMPTY_TEXT}
370
+ {i18n._(REFERENCES_EMPTY_TEXT)}
343
371
  </div>
344
372
  )}
345
373
  </div>
@@ -351,6 +379,7 @@ function GuidesControl() {
351
379
  // ── Scans toggle + dropdown ─────────────────────────────────────────────────
352
380
 
353
381
  function ScansControl() {
382
+ const i18n = usePascalI18n()
354
383
  const popupSide = useActionMenuPopupSide()
355
384
  const showScans = useViewer((state) => state.showScans)
356
385
  const setShowScans = useViewer((state) => state.setShowScans)
@@ -392,13 +421,17 @@ function ScansControl() {
392
421
  ? 'bg-black/9 hover:bg-black/9 dark:bg-white/15'
393
422
  : 'opacity-60 grayscale hover:bg-black/4 hover:opacity-100 hover:grayscale-0',
394
423
  )}
395
- label={`Scans: ${showScans ? 'Visible' : 'Hidden'}`}
424
+ label={
425
+ showScans
426
+ ? i18n._({ id: 'editor.actionMenu.viewToggles.scansVisible', message: 'Scans: Visible' })
427
+ : i18n._({ id: 'editor.actionMenu.viewToggles.scansHidden', message: 'Scans: Hidden' })
428
+ }
396
429
  onClick={() => setShowScans(!showScans)}
397
430
  size="icon"
398
431
  variant="ghost"
399
432
  >
400
433
  <div className="relative">
401
- <img alt="Scans" className="h-[28px] w-[28px] object-contain" src="/icons/mesh.webp" />
434
+ <img alt={i18n._({ id: 'editor.actionMenu.viewToggles.scans', message: 'Scans' })} className="h-[28px] w-[28px] object-contain" src="/icons/mesh.webp" />
402
435
  <span className="absolute -right-1.5 -bottom-1 min-w-[14px] rounded-full bg-black/12 dark:bg-white/20 px-[3px] text-center font-medium text-[9px] text-foreground/70 dark:text-white/70 leading-[14px]">
403
436
  {scans.length}
404
437
  </span>
@@ -409,7 +442,7 @@ function ScansControl() {
409
442
  <PopoverTrigger asChild>
410
443
  <button
411
444
  aria-expanded={isOpen}
412
- aria-label="Scan settings"
445
+ aria-label={i18n._({ id: 'editor.actionMenu.viewToggles.scanSettings', message: 'Scan settings' })}
413
446
  className={cn(
414
447
  'flex h-11 w-6 items-center justify-center rounded-r-lg transition-colors',
415
448
  showScans
@@ -439,10 +472,10 @@ function ScansControl() {
439
472
  <img alt="" className="h-4 w-4 object-contain" src="/icons/mesh.webp" />
440
473
  </span>
441
474
  <div className="min-w-0 flex-1">
442
- <p className="font-medium text-foreground text-sm">Scans</p>
475
+ <p className="font-medium text-foreground text-sm">{i18n._({ id: 'editor.actionMenu.viewToggles.scans', message: 'Scans' })}</p>
443
476
  {hasScans && (
444
477
  <p className="text-muted-foreground text-xs">
445
- {scans.length} scan{scans.length !== 1 ? 's' : ''} on this level
478
+ {i18n._({ id: 'editor.actionMenu.viewToggles.scanCount', message: '{count, plural, one {# scan on this level} other {# scans on this level}}', values: { count: scans.length } })}
446
479
  </p>
447
480
  )}
448
481
  </div>
@@ -479,14 +512,15 @@ function ScansControl() {
479
512
  src="/icons/mesh.webp"
480
513
  />
481
514
  <p className="truncate font-medium text-foreground text-sm">
482
- {scan.name || `Scan ${index + 1}`}
515
+ {scan.name ||
516
+ i18n._({ id: 'editor.actionMenu.viewToggles.scanNumber', message: 'Scan {number}', values: { number: index + 1 } })}
483
517
  </p>
484
518
  {selectedReferenceId === scan.id && (
485
519
  <Check className="ml-auto h-3.5 w-3.5 shrink-0 text-foreground/80" />
486
520
  )}
487
521
  </button>
488
522
  <button
489
- aria-label="Delete scan"
523
+ aria-label={i18n._({ id: 'editor.actionMenu.viewToggles.deleteScan', message: 'Delete scan' })}
490
524
  className="flex h-5 w-5 shrink-0 items-center justify-center rounded-md text-muted-foreground/50 opacity-0 transition-all hover:bg-destructive/10 hover:text-destructive group-hover/item:opacity-100"
491
525
  onClick={(event) => {
492
526
  event.stopPropagation()
@@ -501,7 +535,7 @@ function ScansControl() {
501
535
  </button>
502
536
  </div>
503
537
  <SliderControl
504
- label="Opacity"
538
+ label={i18n._({ id: 'editor.actionMenu.viewToggles.opacity', message: 'Opacity' })}
505
539
  max={100}
506
540
  min={0}
507
541
  onChange={(value) => handleOpacityChange(scan.id, value)}
@@ -515,7 +549,7 @@ function ScansControl() {
515
549
  </div>
516
550
  ) : (
517
551
  <div className="rounded-xl border border-border/45 border-dashed bg-background/60 px-3 py-4 text-muted-foreground text-sm">
518
- {REFERENCES_EMPTY_TEXT}
552
+ {i18n._(REFERENCES_EMPTY_TEXT)}
519
553
  </div>
520
554
  )}
521
555
  </div>
@@ -532,7 +566,7 @@ function ScansControl() {
532
566
  function ReferenceListSection({
533
567
  title,
534
568
  iconSrc,
535
- noun,
569
+ kind,
536
570
  emptyText,
537
571
  nodes,
538
572
  show,
@@ -541,13 +575,14 @@ function ReferenceListSection({
541
575
  }: {
542
576
  title: string
543
577
  iconSrc: string
544
- noun: string
545
- emptyText: string
578
+ kind: 'scan' | 'guide'
579
+ emptyText: MessageDescriptor
546
580
  nodes: (GuideNode | ScanNode)[]
547
581
  show: boolean
548
582
  setShow: (show: boolean) => void
549
583
  onError: (message: string | null) => void
550
584
  }) {
585
+ const i18n = usePascalI18n()
551
586
  const setSelection = useViewer((state) => state.setSelection)
552
587
  const updateNode = useScene((state) => state.updateNode)
553
588
  const deleteNode = useScene((state) => state.deleteNode)
@@ -574,13 +609,22 @@ function ReferenceListSection({
574
609
  <p className="font-medium text-foreground text-sm">{title}</p>
575
610
  {hasItems && (
576
611
  <p className="text-muted-foreground text-xs">
577
- {nodes.length} {noun}
578
- {nodes.length !== 1 ? 's' : ''} on this level
612
+ {kind === 'scan'
613
+ ? i18n._({ id: 'editor.actionMenu.viewToggles.scanCount', message: '{count, plural, one {# scan on this level} other {# scans on this level}}', values: { count: nodes.length } })
614
+ : i18n._({ id: 'editor.actionMenu.viewToggles.guideCount', message: '{count, plural, one {# guide image on this level} other {# guide images on this level}}', values: { count: nodes.length } })}
579
615
  </p>
580
616
  )}
581
617
  </div>
582
618
  <button
583
- aria-label={show ? `Hide ${title.toLowerCase()}` : `Show ${title.toLowerCase()}`}
619
+ aria-label={
620
+ kind === 'scan'
621
+ ? show
622
+ ? i18n._({ id: 'editor.actionMenu.viewToggles.hideScans', message: 'Hide scans' })
623
+ : i18n._({ id: 'editor.actionMenu.viewToggles.showScans', message: 'Show scans' })
624
+ : show
625
+ ? i18n._({ id: 'editor.actionMenu.viewToggles.hideGuideImages', message: 'Hide guide images' })
626
+ : i18n._({ id: 'editor.actionMenu.viewToggles.showGuideImages', message: 'Show guide images' })
627
+ }
584
628
  className="flex h-6 w-6 shrink-0 items-center justify-center rounded-full border border-border/40 text-muted-foreground transition-colors hover:bg-black/6 dark:hover:bg-white/10 hover:text-foreground"
585
629
  onClick={() => setShow(!show)}
586
630
  type="button"
@@ -614,14 +658,21 @@ function ReferenceListSection({
614
658
  src={iconSrc}
615
659
  />
616
660
  <p className="truncate font-medium text-foreground text-sm">
617
- {node.name || `${noun.charAt(0).toUpperCase()}${noun.slice(1)} ${index + 1}`}
661
+ {node.name ||
662
+ (kind === 'scan'
663
+ ? i18n._({ id: 'editor.actionMenu.viewToggles.scanNumber', message: 'Scan {number}', values: { number: index + 1 } })
664
+ : i18n._({ id: 'editor.actionMenu.viewToggles.guideImageNumber', message: 'Guide image {number}', values: { number: index + 1 } }))}
618
665
  </p>
619
666
  {selectedReferenceId === node.id && (
620
667
  <Check className="ml-auto h-3.5 w-3.5 shrink-0 text-foreground/80" />
621
668
  )}
622
669
  </button>
623
670
  <button
624
- aria-label={`Delete ${noun}`}
671
+ aria-label={
672
+ kind === 'scan'
673
+ ? i18n._({ id: 'editor.actionMenu.viewToggles.deleteScan', message: 'Delete scan' })
674
+ : i18n._({ id: 'editor.actionMenu.viewToggles.deleteGuideImage', message: 'Delete guide image' })
675
+ }
625
676
  className="flex h-5 w-5 shrink-0 items-center justify-center rounded-md text-muted-foreground/50 opacity-0 transition-all hover:bg-destructive/10 hover:text-destructive group-hover/item:opacity-100"
626
677
  onClick={(event) => {
627
678
  event.stopPropagation()
@@ -636,7 +687,7 @@ function ReferenceListSection({
636
687
  </button>
637
688
  </div>
638
689
  <SliderControl
639
- label="Opacity"
690
+ label={i18n._({ id: 'editor.actionMenu.viewToggles.opacity', message: 'Opacity' })}
640
691
  max={100}
641
692
  min={0}
642
693
  onChange={(value) =>
@@ -652,7 +703,7 @@ function ReferenceListSection({
652
703
  </div>
653
704
  ) : (
654
705
  <div className="rounded-xl border border-border/45 border-dashed bg-background/60 px-3 py-3 text-muted-foreground text-sm">
655
- {emptyText}
706
+ {i18n._(emptyText)}
656
707
  </div>
657
708
  )}
658
709
  </div>
@@ -660,6 +711,7 @@ function ReferenceListSection({
660
711
  }
661
712
 
662
713
  function ReferencesControl() {
714
+ const i18n = usePascalI18n()
663
715
  const popupSide = useActionMenuPopupSide()
664
716
  const showScans = useViewer((state) => state.showScans)
665
717
  const setShowScans = useViewer((state) => state.setShowScans)
@@ -689,14 +741,18 @@ function ReferencesControl() {
689
741
  ? 'bg-black/9 hover:bg-black/9 dark:bg-white/15'
690
742
  : 'opacity-60 grayscale hover:bg-black/4 hover:opacity-100 hover:grayscale-0',
691
743
  )}
692
- label={`References: ${anyVisible ? 'Visible' : 'Hidden'}`}
744
+ label={
745
+ anyVisible
746
+ ? i18n._({ id: 'editor.actionMenu.viewToggles.referencesVisible', message: 'References: Visible' })
747
+ : i18n._({ id: 'editor.actionMenu.viewToggles.referencesHidden', message: 'References: Hidden' })
748
+ }
693
749
  onClick={toggleAll}
694
750
  size="icon"
695
751
  variant="ghost"
696
752
  >
697
753
  <div className="relative">
698
754
  <img
699
- alt="References"
755
+ alt={i18n._({ id: 'editor.actionMenu.viewToggles.references', message: 'References' })}
700
756
  className="h-[28px] w-[28px] object-contain"
701
757
  src="/icons/floorplan.webp"
702
758
  />
@@ -709,7 +765,7 @@ function ReferencesControl() {
709
765
  <PopoverTrigger asChild>
710
766
  <button
711
767
  aria-expanded={isOpen}
712
- aria-label="Reference settings"
768
+ aria-label={i18n._({ id: 'editor.actionMenu.viewToggles.referenceSettings', message: 'Reference settings' })}
713
769
  className={cn(
714
770
  'flex h-11 w-6 items-center justify-center rounded-r-lg transition-colors',
715
771
  anyVisible
@@ -743,22 +799,22 @@ function ReferencesControl() {
743
799
  emptyText={REFERENCES_EMPTY_TEXT}
744
800
  iconSrc="/icons/mesh.webp"
745
801
  nodes={scans}
746
- noun="scan"
802
+ kind="scan"
747
803
  onError={setUploadError}
748
804
  setShow={setShowScans}
749
805
  show={showScans}
750
- title="Scans"
806
+ title={i18n._({ id: 'editor.actionMenu.viewToggles.scans', message: 'Scans' })}
751
807
  />
752
808
  <div className="h-px bg-border/45" />
753
809
  <ReferenceListSection
754
810
  emptyText={REFERENCES_EMPTY_TEXT}
755
811
  iconSrc="/icons/floorplan.webp"
756
812
  nodes={guides}
757
- noun="guide image"
813
+ kind="guide"
758
814
  onError={setUploadError}
759
815
  setShow={setShowGuides}
760
816
  show={showGuides}
761
- title="Guide images"
817
+ title={i18n._({ id: 'editor.actionMenu.viewToggles.guideImages', message: 'Guide images' })}
762
818
  />
763
819
  </div>
764
820
  </PopoverContent>
@@ -769,6 +825,7 @@ function ReferencesControl() {
769
825
  // ── Reference floor control ────────────────────────────────────────────────────────────────────
770
826
 
771
827
  function ReferenceFloorControl() {
828
+ const i18n = usePascalI18n()
772
829
  const popupSide = useActionMenuPopupSide()
773
830
  const showReferenceFloor = useEditor((state) => state.showReferenceFloor)
774
831
  const toggleReferenceFloor = useEditor((state) => state.toggleReferenceFloor)
@@ -780,7 +837,7 @@ function ReferenceFloorControl() {
780
837
  const lowerLevels = useLowerReferenceLevels()
781
838
  const hasLowerLevels = lowerLevels.length > 0
782
839
  const selectedLevel = lowerLevels[referenceFloorOffset - 1] ?? lowerLevels[0] ?? null
783
- const selectedLevelName = selectedLevel ? getLevelDisplayName(selectedLevel) : null
840
+ const selectedLevelName = selectedLevel ? localizedLevelName(selectedLevel, i18n, i18n.getLocale()) : null
784
841
 
785
842
  return (
786
843
  <Popover onOpenChange={setIsOpen} open={isOpen}>
@@ -795,8 +852,8 @@ function ReferenceFloorControl() {
795
852
  disabled={!hasLowerLevels}
796
853
  label={
797
854
  selectedLevelName && showReferenceFloor
798
- ? `Reference floor: ${selectedLevelName}`
799
- : 'Reference floor'
855
+ ? i18n._({ id: 'editor.actionMenu.viewToggles.referenceFloorSelectedlevelname', message: 'Reference floor: {selectedLevelName}', values: { selectedLevelName } })
856
+ : i18n._({ id: 'editor.actionMenu.viewToggles.referenceFloor', message: 'Reference floor' })
800
857
  }
801
858
  onClick={() => {
802
859
  if (hasLowerLevels) toggleReferenceFloor()
@@ -815,7 +872,7 @@ function ReferenceFloorControl() {
815
872
  <PopoverTrigger asChild>
816
873
  <button
817
874
  aria-expanded={isOpen}
818
- aria-label="Reference floor settings"
875
+ aria-label={i18n._({ id: 'editor.actionMenu.viewToggles.referenceFloorSettings', message: 'Reference floor settings' })}
819
876
  className={cn(
820
877
  'flex h-11 w-6 items-center justify-center rounded-r-lg transition-colors',
821
878
  showReferenceFloor && selectedLevel
@@ -846,13 +903,13 @@ function ReferenceFloorControl() {
846
903
  <Layers2 className="h-4 w-4" />
847
904
  </span>
848
905
  <div className="min-w-0 flex-1">
849
- <p className="font-medium text-foreground text-sm">Reference floor</p>
906
+ <p className="font-medium text-foreground text-sm">{i18n._({ id: 'editor.actionMenu.viewToggles.referenceFloor', message: 'Reference floor' })}</p>
850
907
  {selectedLevelName && (
851
908
  <p className="truncate text-muted-foreground text-xs">{selectedLevelName}</p>
852
909
  )}
853
910
  </div>
854
911
  <button
855
- aria-label={showReferenceFloor ? 'Hide reference floor' : 'Show reference floor'}
912
+ aria-label={showReferenceFloor ? i18n._({ id: 'editor.actionMenu.viewToggles.hideReferenceFloor', message: 'Hide reference floor' }) : i18n._({ id: 'editor.actionMenu.viewToggles.showReferenceFloor', message: 'Show reference floor' })}
856
913
  className="flex h-6 w-6 shrink-0 items-center justify-center rounded-full border border-border/40 text-muted-foreground transition-colors hover:bg-black/6 dark:hover:bg-white/10 hover:text-foreground disabled:pointer-events-none disabled:opacity-40"
857
914
  disabled={!hasLowerLevels}
858
915
  onClick={toggleReferenceFloor}
@@ -871,7 +928,7 @@ function ReferenceFloorControl() {
871
928
  <div className="max-h-44 space-y-1 overflow-y-auto rounded-xl border border-border/45 bg-background/60 p-1.5">
872
929
  {lowerLevels.map((level, index) => {
873
930
  const isSelected = referenceFloorOffset === index + 1
874
- const levelName = getLevelDisplayName(level)
931
+ const levelName = localizedLevelName(level, i18n, i18n.getLocale())
875
932
  return (
876
933
  <button
877
934
  className={cn(
@@ -896,14 +953,14 @@ function ReferenceFloorControl() {
896
953
  )}
897
954
  />
898
955
  <span className="min-w-0 flex-1 truncate">{levelName}</span>
899
- <span className="text-[10px] text-muted-foreground">{index + 1} below</span>
956
+ <span className="text-[10px] text-muted-foreground">{i18n._({ id: 'editor.actionMenu.viewToggles.levelsBelow', message: '{count} below', values: { count: index + 1 } })}</span>
900
957
  </button>
901
958
  )
902
959
  })}
903
960
  </div>
904
961
 
905
962
  <SliderControl
906
- label="Opacity"
963
+ label={i18n._({ id: 'editor.actionMenu.viewToggles.opacity', message: 'Opacity' })}
907
964
  max={0.8}
908
965
  min={0.1}
909
966
  onChange={setReferenceFloorOpacity}
@@ -914,7 +971,7 @@ function ReferenceFloorControl() {
914
971
  </>
915
972
  ) : (
916
973
  <div className="rounded-xl border border-border/45 border-dashed bg-background/60 px-3 py-4 text-muted-foreground text-sm">
917
- No lower floor available.
974
+ {i18n._({ id: 'editor.actionMenu.viewToggles.noLowerFloorAvailable', message: 'No lower floor available.' })}
918
975
  </div>
919
976
  )}
920
977
  </div>
@@ -923,9 +980,156 @@ function ReferenceFloorControl() {
923
980
  )
924
981
  }
925
982
 
983
+ // ── Walls below / above control ─────────────────────────────────────────────
984
+
985
+ function useAdjacentLevelNames() {
986
+ const i18n = usePascalI18n()
987
+ const levelId = useViewer((s) => s.selection.levelId)
988
+ return useScene(
989
+ useShallow((state) => {
990
+ const nodes = state.nodes as Record<string, AnyNode>
991
+ const ids = adjacentLevelIds(nodes, levelId)
992
+ const name = (id: string | null) => {
993
+ const level = id ? nodes[id] : undefined
994
+ return level?.type === 'level' ? localizedLevelName(level, i18n, i18n.getLocale()) : null
995
+ }
996
+ return { below: name(ids.below), above: name(ids.above) }
997
+ }),
998
+ )
999
+ }
1000
+
1001
+ /**
1002
+ * What the "Walls below / above" button turns each side to: the sides that have a level all on, or
1003
+ * all off when one shows. A side with no level keeps its setting, which persists for the levels
1004
+ * that have one: turning the walls below on from the top storey must not trace walls above later.
1005
+ */
1006
+ export function adjacentWallsToggle(
1007
+ active: boolean,
1008
+ levels: { below: string | null; above: string | null },
1009
+ ): { below?: boolean; above?: boolean } {
1010
+ return {
1011
+ ...(levels.below ? { below: !active } : {}),
1012
+ ...(levels.above ? { above: !active } : {}),
1013
+ }
1014
+ }
1015
+
1016
+ /**
1017
+ * "Walls below / above": the plan traces the walls of the neighbouring levels as dashed outlines,
1018
+ * blue for the level below (on a roof level, the top storey), magenta for the level above.
1019
+ */
1020
+ function AdjacentWallsControl() {
1021
+ const i18n = usePascalI18n()
1022
+ const popupSide = useActionMenuPopupSide()
1023
+ const levelId = useViewer((s) => s.selection.levelId)
1024
+ const { shown: showBelow, setShown: setShowBelow } = useWallsBelowSetting(levelId)
1025
+ const showAbove = useEditor((state) => state.showWallsAbove)
1026
+ const setShowAbove = useEditor((state) => state.setShowWallsAbove)
1027
+ const names = useAdjacentLevelNames()
1028
+ const [isOpen, setIsOpen] = useState(false)
1029
+ const active = (showBelow && !!names.below) || (showAbove && !!names.above)
1030
+ const available = !!(names.below || names.above)
1031
+ const rows = [
1032
+ {
1033
+ key: 'below',
1034
+ label: i18n._({ id: 'editor.actionMenu.viewToggles.wallsBelow', message: 'Walls below' }),
1035
+ level: names.below,
1036
+ checked: showBelow,
1037
+ set: setShowBelow,
1038
+ color: ADJACENT_WALL_COLORS.below,
1039
+ },
1040
+ {
1041
+ key: 'above',
1042
+ label: i18n._({ id: 'editor.actionMenu.viewToggles.wallsAbove', message: 'Walls above' }),
1043
+ level: names.above,
1044
+ checked: showAbove,
1045
+ set: setShowAbove,
1046
+ color: ADJACENT_WALL_COLORS.above,
1047
+ },
1048
+ ] as const
1049
+
1050
+ return (
1051
+ <Popover onOpenChange={setIsOpen} open={isOpen}>
1052
+ <div className="flex items-center">
1053
+ <ActionButton
1054
+ className={cn(
1055
+ 'rounded-r-none p-0',
1056
+ active
1057
+ ? 'bg-black/9 hover:bg-black/9 dark:bg-white/15'
1058
+ : 'opacity-60 grayscale hover:bg-black/4 hover:opacity-100 hover:grayscale-0',
1059
+ )}
1060
+ disabled={!available}
1061
+ label={active ? i18n._({ id: 'editor.actionMenu.viewToggles.wallsBelowAndAboveShown', message: 'Walls below and above: shown' }) : i18n._({ id: 'editor.actionMenu.viewToggles.showWallsBelowAndAbove', message: 'Show walls below and above' })}
1062
+ onClick={() => {
1063
+ const next = adjacentWallsToggle(active, names)
1064
+ if (next.below !== undefined) setShowBelow(next.below)
1065
+ if (next.above !== undefined) setShowAbove(next.above)
1066
+ }}
1067
+ size="icon"
1068
+ variant="ghost"
1069
+ >
1070
+ <SquareDashed className="h-4 w-4" />
1071
+ </ActionButton>
1072
+ <PopoverTrigger asChild>
1073
+ <button
1074
+ aria-expanded={isOpen}
1075
+ aria-label={i18n._({ id: 'editor.actionMenu.viewToggles.wallsBelowAndAboveSettings', message: 'Walls below and above settings' })}
1076
+ className={cn(
1077
+ 'flex h-11 w-6 items-center justify-center rounded-r-lg transition-colors',
1078
+ active
1079
+ ? 'bg-black/4 dark:bg-white/5 hover:bg-black/5 dark:hover:bg-white/8'
1080
+ : 'opacity-60 hover:bg-black/4 dark:hover:bg-white/5 hover:opacity-100',
1081
+ )}
1082
+ disabled={!available}
1083
+ type="button"
1084
+ >
1085
+ <ChevronDown className={cn('h-3 w-3 transition-transform', isOpen && 'rotate-180')} />
1086
+ </button>
1087
+ </PopoverTrigger>
1088
+ </div>
1089
+ <PopoverContent
1090
+ align="center"
1091
+ className="w-72 rounded-xl border-border/45 bg-background/96 p-3 shadow-elevation-3 backdrop-blur-xl"
1092
+ side={popupSide}
1093
+ sideOffset={14}
1094
+ >
1095
+ <div className="space-y-3" data-adjacent-walls-settings>
1096
+ <div>
1097
+ <p className="font-medium text-foreground text-sm">{i18n._({ id: 'editor.actionMenu.viewToggles.wallsBelowAndAbove', message: 'Walls below and above' })}</p>
1098
+ <p className="text-muted-foreground text-xs">
1099
+ {i18n._({ id: 'editor.actionMenu.viewToggles.dashedOutlinesOfTheNeighbouringLevels', message: 'Dashed outlines of the neighbouring levels’ walls, to line walls up and check a roof against the walls under it.' })}
1100
+ </p>
1101
+ </div>
1102
+ {rows.map((row) => (
1103
+ <label className="flex items-center gap-3" key={row.key}>
1104
+ <span
1105
+ aria-hidden
1106
+ className="h-0 w-5 shrink-0 border-t-2 border-dashed"
1107
+ style={{ borderColor: row.color }}
1108
+ />
1109
+ <span className="flex min-w-0 flex-1 flex-col">
1110
+ <span className="text-foreground text-sm">{row.label}</span>
1111
+ <span className="truncate text-muted-foreground text-xs">
1112
+ {row.level ?? (row.key === 'below' ? i18n._({ id: 'editor.actionMenu.viewToggles.noLevelBelow', message: 'No level below' }) : i18n._({ id: 'editor.actionMenu.viewToggles.noLevelAbove', message: 'No level above' }))}
1113
+ </span>
1114
+ </span>
1115
+ <Switch
1116
+ aria-label={row.label}
1117
+ checked={row.checked}
1118
+ disabled={!row.level}
1119
+ onCheckedChange={row.set}
1120
+ />
1121
+ </label>
1122
+ ))}
1123
+ </div>
1124
+ </PopoverContent>
1125
+ </Popover>
1126
+ )
1127
+ }
1128
+
926
1129
  // ── Riser diagram control ────────────────────────────────────────────────────
927
1130
 
928
1131
  function RiserControl() {
1132
+ const i18n = usePascalI18n()
929
1133
  const isRiserOpen = useEditor((state) => state.isRiserOpen)
930
1134
  const toggleRiserOpen = useEditor((state) => state.toggleRiserOpen)
931
1135
 
@@ -936,7 +1140,7 @@ function RiserControl() {
936
1140
  ? 'bg-black/9 hover:bg-black/9 dark:bg-white/15'
937
1141
  : 'opacity-60 grayscale hover:bg-black/4 hover:opacity-100 hover:grayscale-0',
938
1142
  )}
939
- label="Riser diagram"
1143
+ label={i18n._({ id: 'editor.actionMenu.viewToggles.riserDiagram', message: 'Riser diagram' })}
940
1144
  onClick={toggleRiserOpen}
941
1145
  size="icon"
942
1146
  variant="ghost"
@@ -962,6 +1166,7 @@ export function ViewToggles() {
962
1166
  <ScansControl />
963
1167
  <GuidesControl />
964
1168
  <ReferenceFloorControl />
1169
+ <AdjacentWallsControl />
965
1170
  <RiserControl />
966
1171
  </div>
967
1172
  )