@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
@@ -3,12 +3,18 @@
3
3
  import { Icon } from '@iconify/react'
4
4
  import {
5
5
  acquireSceneReadOnlyLease,
6
+ createI18n,
7
+ emitter,
6
8
  getCatalogMaterialById,
7
9
  getLibraryMaterialIdFromRef,
10
+ getMaterialSwatch,
8
11
  getSceneMaterialIdFromRef,
9
12
  initSpaceDetectionSync,
10
13
  initSpatialGridSync,
14
+ type MessageDescriptor,
15
+ PascalI18nProvider,
11
16
  spatialGridManager,
17
+ usePascalI18n,
12
18
  useScene,
13
19
  } from '@pascal-app/core'
14
20
  import {
@@ -32,15 +38,18 @@ import {
32
38
  useCallback,
33
39
  useEffect,
34
40
  useLayoutEffect,
41
+ useMemo,
35
42
  useRef,
36
43
  useState,
37
44
  } from 'react'
38
45
  import { ViewerOverlay } from '../../components/viewer-overlay'
39
46
  import { ViewerZoneSystem } from '../../components/viewer-zone-system'
47
+ import { useAutoFrame } from '../../hooks/use-auto-frame'
40
48
  import { type SaveStatus, useAutoSave } from '../../hooks/use-auto-save'
41
49
  import { useKeyboard } from '../../hooks/use-keyboard'
42
50
  import { useIsMobile } from '../../hooks/use-mobile'
43
51
  import { useSaveShortcut } from '../../hooks/use-save-shortcut'
52
+ import { type HINT_KEY_NAMES, toolHintKey, toolHintKeyDescriptor } from '../../i18n/node-messages'
44
53
  import { useCeilingEditSessionOwner } from '../../lib/ceiling-edit-session'
45
54
  import { showsWholeBuilding, useEditorLevelDisplay } from '../../lib/editor-level-display'
46
55
  import { isSceneView } from '../../lib/editor-views'
@@ -60,6 +69,7 @@ import {
60
69
  type SceneGraph,
61
70
  writePersistedSelection,
62
71
  } from '../../lib/scene'
72
+ import { computeSceneBoundsXZ } from '../../lib/scene-bounds'
63
73
  import { disposeSFXBus, initSFXBus } from '../../lib/sfx-bus'
64
74
  import { useUnitFocusRules } from '../../lib/units'
65
75
  import { cn } from '../../lib/utils'
@@ -71,7 +81,7 @@ import useSessionGroups from '../../store/use-session-groups'
71
81
  import { paneOfView, VIEW_2D, VIEW_3D, type ViewPaneIndex } from '../../store/view-layout'
72
82
  import { PointAskLayer } from '../point-ask/point-ask-layer'
73
83
  import { CeilingSelectionAffordanceSystem } from '../systems/ceiling/ceiling-selection-affordance-system'
74
- import { CeilingSystem } from '../systems/ceiling/ceiling-system'
84
+ import { CeilingSystem, CeilingTopsSync } from '../systems/ceiling/ceiling-system'
75
85
  import { RoofEditSystem } from '../systems/roof/roof-edit-system'
76
86
  import { SelectionAffordanceManager } from '../systems/selection-affordance-manager'
77
87
  import { StairEditSystem } from '../systems/stair/stair-edit-system'
@@ -96,6 +106,8 @@ import { SettingsPanel, type SettingsPanelProps } from '../ui/sidebar/panels/set
96
106
  import { SitePanel, type SitePanelProps } from '../ui/sidebar/panels/site-panel'
97
107
  import type { SidebarTab } from '../ui/sidebar/tab-bar'
98
108
  import { useHostPanels } from '../ui/sidebar/use-plugin-panels'
109
+ import { SKY_DOCK_HEIGHT, SkyControl } from '../viewer/sky-control'
110
+ import { useViewerUiSounds } from '../viewer/use-viewer-ui-sounds'
99
111
  import { ViewerHoverLabel } from '../viewer/viewer-hover-label'
100
112
  import { ParametricViewerRooms } from '../viewer/viewer-rooms'
101
113
  import { ViewerSelectionManager } from '../viewer/viewer-selection-manager'
@@ -202,6 +214,8 @@ function initializeEditorRuntime(): () => void {
202
214
  }
203
215
  }
204
216
  export interface EditorProps {
217
+ /** UI locale. Hosts may also call `setLocale` before mounting. */
218
+ locale?: string
205
219
  // Layout version — 'v1' (default) or 'v2' (navbar + two-column)
206
220
  layoutVersion?: 'v1' | 'v2'
207
221
  // Viewport edge the tool menu docks to (desktop only; mobile stays at the bottom)
@@ -315,12 +329,22 @@ export interface EditorProps {
315
329
  }
316
330
 
317
331
  function EditorSceneCrashFallback() {
332
+ const i18n = usePascalI18n()
318
333
  return (
319
334
  <div className="fixed inset-0 z-80 flex items-center justify-center bg-background/95 p-4 text-foreground">
320
335
  <div className="w-full max-w-md rounded-2xl border border-border/60 bg-background p-6 shadow-xl">
321
- <h2 className="font-semibold text-lg">The editor scene failed to render</h2>
336
+ <h2 className="font-semibold text-lg">
337
+ {i18n._({
338
+ id: 'editor.theEditorSceneFailedToRender',
339
+ message: 'The editor scene failed to render',
340
+ })}
341
+ </h2>
322
342
  <p className="mt-2 text-muted-foreground text-sm">
323
- You can retry the scene or return home without reloading the whole app shell.
343
+ {i18n._({
344
+ id: 'editor.youCanRetryTheSceneOr',
345
+ message:
346
+ 'You can retry the scene or return home without reloading the whole app shell.',
347
+ })}
324
348
  </p>
325
349
  <div className="mt-4 flex items-center gap-2">
326
350
  <button
@@ -328,13 +352,13 @@ function EditorSceneCrashFallback() {
328
352
  onClick={() => window.location.reload()}
329
353
  type="button"
330
354
  >
331
- Reload editor
355
+ {i18n._({ id: 'editor.reloadEditor', message: 'Reload editor' })}
332
356
  </button>
333
357
  <a
334
358
  className="rounded-md border border-border bg-background px-3 py-2 font-medium text-sm hover:bg-accent/40"
335
359
  href="/"
336
360
  >
337
- Back to home
361
+ {i18n._({ id: 'editor.backToHome', message: 'Back to home' })}
338
362
  </a>
339
363
  </div>
340
364
  </div>
@@ -345,6 +369,7 @@ function EditorSceneCrashFallback() {
345
369
  // ── Sidebar slot: in-flow, resizable, collapses to a grab strip ──────────────
346
370
 
347
371
  function SidebarSlot({ children }: { children: ReactNode }) {
372
+ const i18n = usePascalI18n()
348
373
  const width = useSidebarStore((s) => s.width)
349
374
  const isCollapsed = useSidebarStore((s) => s.isCollapsed)
350
375
  const setIsCollapsed = useSidebarStore((s) => s.setIsCollapsed)
@@ -416,7 +441,7 @@ function SidebarSlot({ children }: { children: ReactNode }) {
416
441
  <div
417
442
  className="absolute inset-0 z-10 cursor-col-resize transition-colors hover:bg-primary/20"
418
443
  onPointerDown={handleGrabDown}
419
- title="Expand sidebar"
444
+ title={i18n._({ id: 'editor.expandSidebar', message: 'Expand sidebar' })}
420
445
  />
421
446
  ) : (
422
447
  children
@@ -494,26 +519,29 @@ const CAMERA_CONTROL_HINTS: CameraControlHint[] = [
494
519
  { action: 'Zoom', keys: [{ value: 'Scroll' }] },
495
520
  ]
496
521
 
497
- const CAMERA_SHORTCUT_KEY_META: Record<string, { icon?: string; label: string; text?: string }> = {
522
+ const CAMERA_SHORTCUT_KEY_META: Record<
523
+ string,
524
+ { icon?: string; label: MessageDescriptor; text?: string }
525
+ > = {
498
526
  'Left click': {
499
527
  icon: 'ph:mouse-left-click-fill',
500
- label: 'Left click',
528
+ label: hintKeyName('Left click'),
501
529
  },
502
530
  'Middle click': {
503
531
  icon: 'qlementine-icons:mouse-middle-button-16',
504
- label: 'Middle click',
532
+ label: hintKeyName('Middle click'),
505
533
  },
506
534
  'Right click': {
507
535
  icon: 'ph:mouse-right-click-fill',
508
- label: 'Right click',
536
+ label: hintKeyName('Right click'),
509
537
  },
510
538
  Scroll: {
511
539
  icon: 'qlementine-icons:mouse-middle-button-16',
512
- label: 'Scroll wheel',
540
+ label: hintKeyName('Scroll wheel'),
513
541
  },
514
542
  Space: {
515
543
  icon: 'lucide:space',
516
- label: 'Space',
544
+ label: hintKeyName('Space'),
517
545
  },
518
546
  }
519
547
 
@@ -544,8 +572,21 @@ function writeCameraControlsHintDismissed(dismissed: boolean) {
544
572
  } catch {}
545
573
  }
546
574
 
575
+ /** Key names shared with the tool HUD (`editor.toolHint.key.*`). */
576
+ function hintKeyName(key: (typeof HINT_KEY_NAMES)[number]): MessageDescriptor {
577
+ return toolHintKeyDescriptor(key) ?? { id: key, message: key }
578
+ }
579
+
580
+ const CAMERA_HINT_ACTION_LABELS: Record<CameraHintAction, MessageDescriptor> = {
581
+ Pan: /* i18n */ { id: 'editor.cameraHint.pan', message: 'Pan' },
582
+ Rotate: /* i18n */ { id: 'editor.cameraHint.rotate', message: 'Rotate' },
583
+ Zoom: /* i18n */ { id: 'editor.cameraHint.zoom', message: 'Zoom' },
584
+ }
585
+
547
586
  function InlineShortcutKey({ shortcutKey }: { shortcutKey: ShortcutKey }) {
548
- const meta = CAMERA_SHORTCUT_KEY_META[shortcutKey.value]
587
+ const i18n = usePascalI18n()
588
+ const entry = CAMERA_SHORTCUT_KEY_META[shortcutKey.value]
589
+ const meta = entry ? { ...entry, label: i18n._(entry.label) } : undefined
549
590
 
550
591
  if (meta?.icon) {
551
592
  return (
@@ -563,7 +604,7 @@ function InlineShortcutKey({ shortcutKey }: { shortcutKey: ShortcutKey }) {
563
604
 
564
605
  return (
565
606
  <span className="font-medium text-[11px] text-foreground/90">
566
- {meta?.text ?? shortcutKey.value}
607
+ {meta?.text ?? toolHintKey(shortcutKey.value, i18n)}
567
608
  </span>
568
609
  )
569
610
  }
@@ -582,10 +623,11 @@ function ShortcutSequence({ keys }: { keys: ShortcutKey[] }) {
582
623
  }
583
624
 
584
625
  function CameraControlHintItem({ hint }: { hint: CameraControlHint }) {
626
+ const i18n = usePascalI18n()
585
627
  return (
586
628
  <div className="flex min-w-0 flex-col items-center gap-1.5 px-4 text-center first:pl-0 last:pr-0">
587
629
  <span className="font-medium text-[10px] text-muted-foreground/60 tracking-[0.03em]">
588
- {hint.action}
630
+ {i18n._(CAMERA_HINT_ACTION_LABELS[hint.action])}
589
631
  </span>
590
632
  <div className="flex flex-wrap items-center justify-center gap-1.5">
591
633
  <ShortcutSequence keys={hint.keys} />
@@ -607,6 +649,7 @@ function ViewerCanvasControlsHint({
607
649
  belowTopMenu?: boolean
608
650
  onDismiss: () => void
609
651
  }) {
652
+ const i18n = usePascalI18n()
610
653
  const all = CAMERA_CONTROL_HINTS
611
654
  // A host teaching one gesture at a time narrows this to the one it is asking
612
655
  // for, and to nothing once it is done. Null — the default — is all of them.
@@ -626,7 +669,7 @@ function ViewerCanvasControlsHint({
626
669
  style={belowTopMenu ? { top: TOP_MENU_CLEARANCE } : undefined}
627
670
  >
628
671
  <section
629
- aria-label="Camera controls hint"
672
+ aria-label={i18n._({ id: 'editor.cameraControlsHint', message: 'Camera controls hint' })}
630
673
  className="pointer-events-auto flex items-start gap-3 rounded-2xl border border-border/35 bg-background/90 px-3.5 py-2.5 shadow-elevation-4 backdrop-blur-xl"
631
674
  >
632
675
  <div
@@ -640,7 +683,10 @@ function ViewerCanvasControlsHint({
640
683
  <Tooltip>
641
684
  <TooltipTrigger asChild>
642
685
  <button
643
- aria-label="Dismiss camera controls hint"
686
+ aria-label={i18n._({
687
+ id: 'editor.dismissCameraControlsHint',
688
+ message: 'Dismiss camera controls hint',
689
+ })}
644
690
  className="flex h-5 shrink-0 items-center justify-center self-center border-border/18 border-l pl-3 text-muted-foreground/70 transition-colors hover:text-foreground"
645
691
  onClick={onDismiss}
646
692
  type="button"
@@ -655,7 +701,7 @@ function ViewerCanvasControlsHint({
655
701
  </button>
656
702
  </TooltipTrigger>
657
703
  <TooltipContent side="bottom" sideOffset={8}>
658
- Dismiss
704
+ {i18n._({ id: 'editor.dismiss', message: 'Dismiss' })}
659
705
  </TooltipContent>
660
706
  </Tooltip>
661
707
  </section>
@@ -733,7 +779,17 @@ function getActivePaintMaterialSwatchImageUrl(
733
779
  const catalogId =
734
780
  getLibraryMaterialIdFromRef(material?.materialPreset) ?? material?.material?.id ?? undefined
735
781
  const catalogMaterial = getCatalogMaterialById(catalogId)
736
- return catalogMaterial?.previewThumbnailUrl ?? catalogMaterial?.preset.maps.albedoMap
782
+ return catalogMaterial ? (getMaterialSwatch(catalogMaterial).imageUrl ?? undefined) : undefined
783
+ }
784
+
785
+ /** Whether the badge's image is a white albedo its colour tints (a siding's profile). */
786
+ function isActivePaintMaterialSwatchTinted(material: ActivePaintMaterial | null) {
787
+ if (material?.material?.texture?.url || getSceneMaterialIdFromRef(material?.materialPreset))
788
+ return false
789
+ const catalogId =
790
+ getLibraryMaterialIdFromRef(material?.materialPreset) ?? material?.material?.id ?? undefined
791
+ const catalogMaterial = getCatalogMaterialById(catalogId)
792
+ return catalogMaterial ? getMaterialSwatch(catalogMaterial).tinted : false
737
793
  }
738
794
 
739
795
  function PaintCursorBadge({
@@ -741,6 +797,7 @@ function PaintCursorBadge({
741
797
  state,
742
798
  swatchColor,
743
799
  swatchImageUrl,
800
+ swatchTinted = false,
744
801
  isEraser,
745
802
  isPicker = false,
746
803
  }: {
@@ -748,6 +805,8 @@ function PaintCursorBadge({
748
805
  state: PaintCursorBadgeState
749
806
  swatchColor: string
750
807
  swatchImageUrl?: string
808
+ /** Multiply the image by the colour (a tinted albedo). */
809
+ swatchTinted?: boolean
751
810
  isEraser: boolean
752
811
  /** The eyedropper: the swatch is what a click would take. */
753
812
  isPicker?: boolean
@@ -821,6 +880,7 @@ function PaintCursorBadge({
821
880
  backgroundImage: swatchImageUrl
822
881
  ? `url(${JSON.stringify(swatchImageUrl)})`
823
882
  : undefined,
883
+ backgroundBlendMode: swatchImageUrl && swatchTinted ? 'multiply' : undefined,
824
884
  opacity: state === 'aim' ? 0.55 : 1,
825
885
  }}
826
886
  />
@@ -1084,6 +1144,7 @@ function PaintCursorLayer({
1084
1144
  const swatchMaterial = picking ? picked : activePaintMaterial
1085
1145
  const swatchColor = getActivePaintMaterialSwatchColor(swatchMaterial, sceneMaterials)
1086
1146
  const swatchImageUrl = getActivePaintMaterialSwatchImageUrl(swatchMaterial, sceneMaterials)
1147
+ const swatchTinted = isActivePaintMaterialSwatchTinted(swatchMaterial)
1087
1148
 
1088
1149
  if (!active || !position) return null
1089
1150
 
@@ -1099,6 +1160,7 @@ function PaintCursorLayer({
1099
1160
  state={badgeState}
1100
1161
  swatchColor={swatchColor}
1101
1162
  swatchImageUrl={swatchImageUrl}
1163
+ swatchTinted={swatchTinted}
1102
1164
  />
1103
1165
  </div>
1104
1166
  )
@@ -1150,6 +1212,9 @@ const ViewerCanvas = memo(function ViewerCanvas({
1150
1212
  const views = useEditorViews()
1151
1213
  const isPreviewMode = useEditor((s) => s.isPreviewMode)
1152
1214
  const isCaptureMode = useEditor((s) => s.isCaptureMode)
1215
+ const textures = useViewer((s) => s.textures)
1216
+ const isMobile = useIsMobile()
1217
+ const planOnRight = layout.split && layout.panes[1] === VIEW_2D
1153
1218
  useUnitFocusRules()
1154
1219
  const wholeBuilding = useEditor((s) =>
1155
1220
  showsWholeBuilding({
@@ -1319,6 +1384,22 @@ const ViewerCanvas = memo(function ViewerCanvas({
1319
1384
  viewerSceneSlot={viewerSceneSlot}
1320
1385
  />
1321
1386
  </Viewer>
1387
+ {!showLoader &&
1388
+ textures &&
1389
+ !isMobile &&
1390
+ !(
1391
+ isFirstPersonMode ||
1392
+ isStudioMode ||
1393
+ isVersionPreviewMode ||
1394
+ isCaptureMode ||
1395
+ isPreviewMode
1396
+ ) ? (
1397
+ // Beside the compass (which moves to the plan's corner when the plan is on the right).
1398
+ <SkyControl
1399
+ height={SKY_DOCK_HEIGHT.editor}
1400
+ style={{ left: planOnRight ? 12 : 52 }}
1401
+ />
1402
+ ) : null}
1322
1403
  </div>
1323
1404
  {showLoader && loadingOverlay}
1324
1405
  </div>
@@ -1327,6 +1408,7 @@ const ViewerCanvas = memo(function ViewerCanvas({
1327
1408
  const viewId = layout.panes[pane]
1328
1409
  return (
1329
1410
  <div
1411
+ aria-busy={isSceneDataLoading}
1330
1412
  className="relative min-w-0 overflow-hidden"
1331
1413
  data-view-pane={pane}
1332
1414
  key={viewId}
@@ -1335,6 +1417,9 @@ const ViewerCanvas = memo(function ViewerCanvas({
1335
1417
  style={paneStyle(pane)}
1336
1418
  >
1337
1419
  <HostView key={viewId} view={views.find((view) => view.id === viewId)} />
1420
+ {/* A restored pane's plugin is known only once the project has loaded: until
1421
+ then it would read "This view isn't available". */}
1422
+ {isSceneDataLoading && loadingOverlay}
1338
1423
  </div>
1339
1424
  )
1340
1425
  })}
@@ -1384,6 +1469,7 @@ function PreviewStage({
1384
1469
  )
1385
1470
 
1386
1471
  useViewerFloorDisplay()
1472
+ useViewerUiSounds()
1387
1473
  const stageMode = isFirstPersonMode || !hasFloorplan ? '3d' : mode
1388
1474
  const stageModes = hasFloorplan && !isFirstPersonMode ? undefined : (['3d'] as const)
1389
1475
 
@@ -1487,6 +1573,8 @@ function EditorContent({
1487
1573
  useKeyboard({ isVersionPreviewMode, disabled: isFirstPersonMode || isStudioMode })
1488
1574
  useGestureLifecycleOwner()
1489
1575
 
1576
+ useAutoFrame()
1577
+
1490
1578
  const { beginSceneLoad, completeSceneLoad, saveNow } = useAutoSave({
1491
1579
  guardAgainstSceneWipe,
1492
1580
  onSave,
@@ -1658,6 +1746,21 @@ function EditorContent({
1658
1746
  return () => window.clearTimeout(timer)
1659
1747
  }, [hasLoadedInitialScene, isLoading, isSceneLoading, isViewerSceneReady, sceneReadyKey])
1660
1748
 
1749
+ // The useAutoFrame emit can be clobbered by the level-follow effect's
1750
+ // first-run pivot rest on fast (client-side navigation) loads. Re-emitting
1751
+ // here is the last word after every load-driven camera effect has run.
1752
+ // Once per load: a remounted viewer reports ready again, but only a new
1753
+ // load reframes; remounted controls rest their own pivot on the level.
1754
+ const framedSceneReadyKeyRef = useRef<number | null>(null)
1755
+ useEffect(() => {
1756
+ if (!isViewerSceneReady || framedSceneReadyKeyRef.current === sceneReadyKey) return
1757
+ framedSceneReadyKeyRef.current = sceneReadyKey
1758
+ const nodes = useScene.getState().nodes
1759
+ if (Object.keys(nodes).length === 0) return
1760
+ const bounds = computeSceneBoundsXZ(nodes)
1761
+ emitter.emit('camera-controls:fit-scene', bounds ? { bounds } : {})
1762
+ }, [isViewerSceneReady, sceneReadyKey])
1763
+
1661
1764
  const isSceneDataLoading = isLoading || isSceneLoading || !hasLoadedInitialScene
1662
1765
  const showLoader = isSceneDataLoading || !isViewerSceneReady
1663
1766
  const visibleLoader =
@@ -1714,6 +1817,13 @@ function EditorContent({
1714
1817
  }
1715
1818
  }, [isFirstPersonMode, isPreviewMode])
1716
1819
 
1820
+ // The viewer's walkthrough says how the first-person camera moves, for the
1821
+ // viewer-side consumers that tell a walk from a drone flight.
1822
+ const firstPersonMovementMode = useEditor((s) => s.firstPersonMovementMode)
1823
+ useEffect(() => {
1824
+ if (isFirstPersonMode) useViewer.getState().setWalkthroughMovement(firstPersonMovementMode)
1825
+ }, [isFirstPersonMode, firstPersonMovementMode])
1826
+
1717
1827
  const previewViewerContent = (
1718
1828
  <Viewer
1719
1829
  defaultRender={EDITOR_DEFAULT_RENDER}
@@ -1826,6 +1936,7 @@ function EditorContent({
1826
1936
  return (
1827
1937
  <>
1828
1938
  <FloorplanModeCoordinator />
1939
+ <CeilingTopsSync />
1829
1940
  {!isLoading && isPreviewMode ? (
1830
1941
  <PreviewStage
1831
1942
  isFirstPersonMode={isFirstPersonMode}
@@ -1912,6 +2023,7 @@ function EditorContent({
1912
2023
  return (
1913
2024
  <div className="flex h-full w-full gap-3 bg-neutral-100 p-3 text-foreground">
1914
2025
  <FloorplanModeCoordinator />
2026
+ <CeilingTopsSync />
1915
2027
  {!isLoading && isPreviewMode ? (
1916
2028
  <PreviewStage
1917
2029
  isFirstPersonMode={isFirstPersonMode}
@@ -1960,10 +2072,28 @@ function EditorContent({
1960
2072
  )
1961
2073
  }
1962
2074
 
1963
- export default function Editor(props: EditorProps) {
2075
+ function EditorFrame(props: EditorProps) {
1964
2076
  return (
1965
2077
  <Profiler id="editor" onRender={recordEditorRender}>
1966
2078
  <EditorContent {...props} />
1967
2079
  </Profiler>
1968
2080
  )
1969
2081
  }
2082
+
2083
+ function LocalizedEditor(props: EditorProps & { locale: string }) {
2084
+ const i18n = useMemo(() => createI18n({ locale: props.locale }), [props.locale])
2085
+
2086
+ return (
2087
+ <PascalI18nProvider i18n={i18n}>
2088
+ <EditorFrame {...props} />
2089
+ </PascalI18nProvider>
2090
+ )
2091
+ }
2092
+
2093
+ export default function Editor(props: EditorProps) {
2094
+ return props.locale ? (
2095
+ <LocalizedEditor {...props} locale={props.locale} />
2096
+ ) : (
2097
+ <EditorFrame {...props} />
2098
+ )
2099
+ }
@@ -0,0 +1,36 @@
1
+ 'use client'
2
+
3
+ import { useIsMobile } from '../../hooks/use-mobile'
4
+ import { openExtension } from '../../lib/inspector-card-mode'
5
+ import { useInspectorExpanded } from '../../lib/inspector-expanded'
6
+ import { renderExtensionIcon, useInspectorExtensions } from '../ui/panels/inspector-extensions'
7
+ import { ActionMenuButton } from './action-menu-button'
8
+
9
+ /**
10
+ * One action-menu button per plugin inspector extension of the selected node
11
+ * (`Plugin.inspectorExtensions`, e.g. a wall's Engineering): it opens the
12
+ * inspector card on that extension, or keeps it there. The extension that owns
13
+ * the card body for this node (`primaryWhen`) is already showing and gets none;
14
+ * the mobile sheet lists every extension under the kind's controls instead.
15
+ */
16
+ export function InspectorExtensionActions() {
17
+ const isMobile = useIsMobile()
18
+ const { extensions, primaryExtension } = useInspectorExtensions()
19
+ const active = useInspectorExpanded((state) => (state.expanded ? state.activeExtensionId : null))
20
+ if (isMobile) return null
21
+ return extensions
22
+ .filter((extension) => extension !== primaryExtension)
23
+ .map((extension) => (
24
+ <ActionMenuButton
25
+ key={extension.id}
26
+ label={extension.title}
27
+ onClick={(event) => {
28
+ event.stopPropagation()
29
+ useInspectorExpanded.getState().applyMode(openExtension(extension.id))
30
+ }}
31
+ pressed={active === extension.id}
32
+ >
33
+ {renderExtensionIcon(extension.icon)}
34
+ </ActionMenuButton>
35
+ ))
36
+ }
@@ -0,0 +1,22 @@
1
+ 'use client'
2
+
3
+ import { PascalI18nProvider, usePascalI18n } from '@pascal-app/core'
4
+ import { Html as DreiHtml } from '@react-three/drei'
5
+ import { type ComponentProps, forwardRef } from 'react'
6
+
7
+ /**
8
+ * drei's `Html`, keeping the editor's i18n scope. `Html` renders its children
9
+ * in a separate React root, which sees none of the surrounding context, so a
10
+ * component inside it would translate with the default (English) scope instead
11
+ * of the editor's `?lang=` or `<Editor locale>` one.
12
+ */
13
+ export const Html = forwardRef<HTMLDivElement, Omit<ComponentProps<typeof DreiHtml>, 'ref'>>(
14
+ function Html({ children, ...props }, ref) {
15
+ const i18n = usePascalI18n()
16
+ return (
17
+ <DreiHtml {...props} ref={ref}>
18
+ <PascalI18nProvider i18n={i18n}>{children}</PascalI18nProvider>
19
+ </DreiHtml>
20
+ )
21
+ },
22
+ )
@@ -1,8 +1,8 @@
1
1
  'use client'
2
2
 
3
+ import { formatLinearMeasurement, type MetricNotation } from '@pascal-app/core'
3
4
  import { useViewer } from '@pascal-app/viewer'
4
5
  import { type ForwardedRef, Fragment, forwardRef } from 'react'
5
- import { formatLinearMeasurement, type MetricNotation } from '../../lib/measurements'
6
6
 
7
7
  // Canonical in-world dimension formatter — metric metres or imperial
8
8
  // feet/inches. Shared by every measurement readout so they read the same.
@@ -1,6 +1,7 @@
1
1
  'use client'
2
2
 
3
3
  import { Icon } from '@iconify/react'
4
+ import { usePascalI18n } from '@pascal-app/core'
4
5
  import {
5
6
  Copy,
6
7
  Group,
@@ -65,70 +66,103 @@ export function NodeActionMenu({
65
66
  onPointerEnter,
66
67
  onPointerLeave,
67
68
  children,
68
- deleteLabel = 'Delete',
69
+ deleteLabel,
69
70
  deleteDisabledReason,
70
71
  }: NodeActionMenuProps) {
72
+ const i18n = usePascalI18n()
71
73
  return (
72
74
  <div
73
75
  className="pointer-events-auto flex items-center gap-1 rounded-lg border border-border bg-background/95 p-1 shadow-xl backdrop-blur-md"
76
+ data-node-action-menu
74
77
  onPointerDown={onPointerDown}
75
78
  onPointerEnter={onPointerEnter}
76
79
  onPointerLeave={onPointerLeave}
77
80
  onPointerUp={onPointerUp}
78
81
  >
79
82
  {onFind && (
80
- <ActionMenuButton label="Find in catalog" onClick={onFind}>
83
+ <ActionMenuButton
84
+ label={i18n._({ id: 'editor.nodeActionMenu.findInCatalog', message: 'Find in catalog' })}
85
+ onClick={onFind}
86
+ >
81
87
  <Search className="h-4 w-4" />
82
88
  </ActionMenuButton>
83
89
  )}
84
90
  {onMove && (
85
- <ActionMenuButton label="Move" onClick={onMove}>
91
+ <ActionMenuButton
92
+ label={i18n._({ id: 'editor.nodeActionMenu.move', message: 'Move' })}
93
+ onClick={onMove}
94
+ >
86
95
  <Move className="h-4 w-4" />
87
96
  </ActionMenuButton>
88
97
  )}
89
98
  {onEditMesh && (
90
- <ActionMenuButton label="Edit mesh" onClick={onEditMesh}>
99
+ <ActionMenuButton
100
+ label={i18n._({ id: 'editor.nodeActionMenu.editMesh', message: 'Edit mesh' })}
101
+ onClick={onEditMesh}
102
+ >
91
103
  <PencilRuler className="h-4 w-4" />
92
104
  </ActionMenuButton>
93
105
  )}
94
106
  {onGroup && (
95
- <ActionMenuButton keys={['Cmd/Ctrl', 'G']} label="Group selection" onClick={onGroup}>
107
+ <ActionMenuButton
108
+ keys={['Cmd/Ctrl', 'G']}
109
+ label={i18n._({ id: 'editor.nodeActionMenu.groupSelection', message: 'Group selection' })}
110
+ onClick={onGroup}
111
+ >
96
112
  <Group className="h-4 w-4" />
97
113
  </ActionMenuButton>
98
114
  )}
99
115
  {onUngroup && (
100
116
  <ActionMenuButton
101
117
  keys={['Cmd/Ctrl', 'Shift', 'G']}
102
- label="Ungroup selection"
118
+ label={i18n._({
119
+ id: 'editor.nodeActionMenu.ungroupSelection',
120
+ message: 'Ungroup selection',
121
+ })}
103
122
  onClick={onUngroup}
104
123
  >
105
124
  <Ungroup className="h-4 w-4" />
106
125
  </ActionMenuButton>
107
126
  )}
108
127
  {onCurve && (
109
- <ActionMenuButton label="Curve" onClick={onCurve}>
128
+ <ActionMenuButton
129
+ label={i18n._({ id: 'editor.nodeActionMenu.curve', message: 'Curve' })}
130
+ onClick={onCurve}
131
+ >
110
132
  <Spline className="h-4 w-4" />
111
133
  </ActionMenuButton>
112
134
  )}
113
135
  {onRotateLeft && (
114
- <ActionMenuButton label="Rotate left" onClick={onRotateLeft}>
136
+ <ActionMenuButton
137
+ label={i18n._({ id: 'editor.nodeActionMenu.rotateLeft', message: 'Rotate left' })}
138
+ onClick={onRotateLeft}
139
+ >
115
140
  <RotateCcw className="h-4 w-4" />
116
141
  </ActionMenuButton>
117
142
  )}
118
143
  {onRotateRight && (
119
- <ActionMenuButton label="Rotate right" onClick={onRotateRight}>
144
+ <ActionMenuButton
145
+ label={i18n._({ id: 'editor.nodeActionMenu.rotateRight', message: 'Rotate right' })}
146
+ onClick={onRotateRight}
147
+ >
120
148
  <RotateCw className="h-4 w-4" />
121
149
  </ActionMenuButton>
122
150
  )}
123
151
  <RegistryActionContributions />
124
152
  {children}
125
153
  {onDuplicate && (
126
- <ActionMenuButton label="Duplicate" onClick={onDuplicate}>
154
+ <ActionMenuButton
155
+ label={i18n._({ id: 'editor.nodeActionMenu.duplicate', message: 'Duplicate' })}
156
+ onClick={onDuplicate}
157
+ >
127
158
  <Copy className="h-4 w-4" />
128
159
  </ActionMenuButton>
129
160
  )}
130
161
  {onAddHole && (
131
- <ActionMenuButton label="Cut out" onClick={onAddHole}>
162
+ <ActionMenuButton
163
+ label={i18n._({ id: 'editor.nodeActionMenu.cutOut', message: 'Cut out' })}
164
+ onClick={onAddHole}
165
+ >
132
166
  <Icon height={16} icon="carbon:cut-out" width={16} />
133
167
  </ActionMenuButton>
134
168
  )}
@@ -138,7 +172,7 @@ export function NodeActionMenu({
138
172
  disabled={!!deleteDisabledReason}
139
173
  disabledReason={deleteDisabledReason}
140
174
  keys={['Delete / Backspace']}
141
- label={deleteLabel}
175
+ label={deleteLabel ?? i18n._({ id: 'editor.nodeActionMenu.delete', message: 'Delete' })}
142
176
  onClick={onDelete}
143
177
  >
144
178
  <Trash2 className="h-4 w-4" />