@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
@@ -4,19 +4,22 @@ import {
4
4
  type AnyNode,
5
5
  type AnyNodeId,
6
6
  type FloorFoundationPatch,
7
- floorFootprintName,
8
7
  floorFootprintSupportClass,
9
8
  upperFloorHeightControl,
10
- getLevelDisplayName,
11
9
  getLevelElevations,
12
10
  getStoredLevelHeight,
13
11
  type LevelNode,
14
12
  MIN_GROUND_FLOOR_THICKNESS,
15
13
  MIN_SLAB_THICKNESS,
14
+ type PascalI18n,
16
15
  type SlabNode,
16
+ useLocale,
17
+ usePascalI18n,
17
18
  useScene,
18
19
  } from '@pascal-app/core'
19
20
  import { useViewer } from '@pascal-app/viewer'
21
+ import { localizedFloorFootprintName } from '../../../i18n/floor-footprint-name'
22
+ import { localizedLevelName } from '../../../i18n/level-name'
20
23
  import { useRef, useState } from 'react'
21
24
  import {
22
25
  applyFloorFoundation,
@@ -33,7 +36,7 @@ import {
33
36
  THICK_FLOOR_HINT,
34
37
  thickFloorAdvice,
35
38
  } from '../../../lib/floor-footprints'
36
- import { formatLinearMeasurement } from '../../../lib/measurements'
39
+ import { formatLinearMeasurement } from '@pascal-app/core'
37
40
  import { FinishSwatch } from '../controls/finish-swatch'
38
41
  import { MetricControl } from '../controls/metric-control'
39
42
  import { SegmentedControl } from '../controls/segmented-control'
@@ -78,6 +81,7 @@ export function FloorSection({
78
81
  thickness: number
79
82
  thicknessLabel: string
80
83
  }) {
84
+ const i18n = usePascalI18n()
81
85
  const base = 100
82
86
  const foundation = !supported && raised ? Math.min(40, 14 + foundationHeight * 30) : 0
83
87
  const band = Math.max(7, Math.min(16, thickness * 60))
@@ -98,32 +102,67 @@ export function FloorSection({
98
102
  const left = 64
99
103
  const right = 164
100
104
  const labels: { y: number; text: string; key: string }[] = [
101
- { key: 'top', y: bandTop - 9, text: 'Floor top' },
105
+ {
106
+ key: 'top',
107
+ y: bandTop - 9,
108
+ text: i18n._({ id: 'editor.panels.floorFoundationPanel.section.floorTop', message: 'Floor top' }),
109
+ },
102
110
  {
103
111
  key: 'plate',
104
112
  y: bandTop + band / 2,
105
- text: supported ? `Floor plate · ${thicknessLabel}` : `Slab · ${thicknessLabel}`,
113
+ text: supported
114
+ ? i18n._({
115
+ id: 'editor.panels.floorFoundationPanel.section.floorPlate',
116
+ message: 'Floor plate · {thickness}',
117
+ values: { thickness: thicknessLabel },
118
+ })
119
+ : i18n._({
120
+ id: 'editor.panels.floorFoundationPanel.section.slab',
121
+ message: 'Slab · {thickness}',
122
+ values: { thickness: thicknessLabel },
123
+ }),
106
124
  },
107
125
  ...(foundation
108
126
  ? [
109
127
  {
110
128
  key: 'foundation',
111
129
  y: bandBottom + foundation / 2,
112
- text: foundationLabel ? `Foundation · ${foundationLabel}` : 'Foundation',
130
+ text: foundationLabel
131
+ ? i18n._({
132
+ id: 'editor.panels.floorFoundationPanel.section.foundationKind',
133
+ message: 'Foundation · {foundation}',
134
+ values: { foundation: foundationLabel },
135
+ })
136
+ : i18n._({ id: 'editor.panels.floorFoundationPanel.section.foundation', message: 'Foundation' }),
113
137
  },
114
138
  ]
115
139
  : []),
116
- { key: 'base', y: base + 9, text: supported ? 'Walls below' : 'Ground' },
140
+ {
141
+ key: 'base',
142
+ y: base + 9,
143
+ text: supported
144
+ ? i18n._({ id: 'editor.panels.floorFoundationPanel.section.wallsBelow', message: 'Walls below' })
145
+ : i18n._({ id: 'editor.panels.floorFoundationPanel.section.ground', message: 'Ground' }),
146
+ },
117
147
  ]
118
148
  return (
119
149
  <figure className="flex flex-col gap-1" data-floor-section>
120
150
  <svg
121
151
  aria-label={
122
152
  supported
123
- ? `Floor plate ${thicknessLabel} thick, resting on the walls below`
153
+ ? i18n._({ id: 'editor.panels.floorFoundationPanel.floorPlateThicknesslabelThickRestingOn', message: 'Floor plate {thicknessLabel} thick, resting on the walls below', values: { thicknessLabel } })
124
154
  : raised
125
- ? `Slab ${thicknessLabel} thick on a ${foundationLabel} foundation, floor top ${topLabel ?? ''} above the ground`
126
- : `Slab ${thicknessLabel} thick on the ground, floor top ${topLabel ?? ''} above the ground`
155
+ ? i18n._({
156
+ id: 'editor.panels.floorFoundationPanel.sectionRaised',
157
+ message:
158
+ 'Slab {thickness} thick on a {foundation} foundation, floor top {top} above the ground',
159
+ values: { thickness: thicknessLabel, foundation: foundationLabel, top: topLabel ?? '' },
160
+ })
161
+ : i18n._({
162
+ id: 'editor.panels.floorFoundationPanel.sectionOnGround',
163
+ message: 'Slab {thickness} thick on the ground, floor top {top} above the ground',
164
+ values: { thickness: thicknessLabel, top: topLabel ?? '' },
165
+ })
127
166
  }
128
167
  className="h-auto w-full text-muted-foreground"
129
168
  role="img"
@@ -259,7 +298,7 @@ export function FloorSection({
259
298
  ))}
260
299
  </svg>
261
300
  <figcaption className="text-right text-[10px] text-muted-foreground/70">
262
- Not to scale
301
+ {i18n._({ id: 'editor.panels.floorFoundationPanel.notToScale', message: 'Not to scale' })}
263
302
  </figcaption>
264
303
  </figure>
265
304
  )
@@ -271,15 +310,29 @@ type Notice = {
271
310
  together: { ids: string[]; patch: FloorFoundationPatch; lower: boolean } | null
272
311
  }
273
312
 
274
- export function moveTogetherLabel(count: number, lower: boolean) {
275
- const verb = lower ? 'Lower' : 'Raise'
276
- return count === 2 ? `${verb} both` : `${verb} all ${count}`
313
+ export function moveTogetherLabel(count: number, lower: boolean, i18n: PascalI18n) {
314
+ const values = { count, number: String(count) }
315
+ return lower
316
+ ? i18n._({
317
+ id: 'editor.panels.floorFoundationPanel.lowerTogether',
318
+ message: '{count, plural, =2 {Lower both} other {Lower all {number}}}',
319
+ values,
320
+ })
321
+ : i18n._({
322
+ id: 'editor.panels.floorFoundationPanel.raiseTogether',
323
+ message: '{count, plural, =2 {Raise both} other {Raise all {number}}}',
324
+ values,
325
+ })
277
326
  }
278
327
 
279
328
  /** "Floor plate · Ground floor", upstairs "Floor plate · Floor 1 · rests on Ground floor". */
280
- export function floorPlateKicker(nodes: Record<string, AnyNode>, plate: SlabNode): string {
329
+ export function floorPlateKicker(
330
+ nodes: Record<string, AnyNode>,
331
+ plate: SlabNode,
332
+ i18n: PascalI18n,
333
+ locale: string,
334
+ ): string {
281
335
  const level = plate.parentId ? nodes[plate.parentId] : undefined
282
- const levelName = level?.type === 'level' ? ` · ${getLevelDisplayName(level)}` : ''
283
336
  // A supported floor rests on the storey right below it.
284
337
  let below: LevelNode | null = null
285
338
  if (level?.type === 'level' && floorFootprintSupportClass(nodes, plate) === 'supported')
@@ -291,8 +344,20 @@ export function floorPlateKicker(nodes: Record<string, AnyNode>, plate: SlabNode
291
344
  (!below || node.level > below.level)
292
345
  )
293
346
  below = node
294
- const restsOn = below ? ` · rests on ${getLevelDisplayName(below)}` : ''
295
- return `Floor plate${levelName}${restsOn}`
347
+ if (level?.type !== 'level')
348
+ return i18n._({ id: 'editor.panels.floorFoundationPanel.kicker', message: 'Floor plate' })
349
+ const levelName = localizedLevelName(level, i18n, locale)
350
+ return below
351
+ ? i18n._({
352
+ id: 'editor.panels.floorFoundationPanel.kickerRestsOn',
353
+ message: 'Floor plate · {level} · rests on {below}',
354
+ values: { level: levelName, below: localizedLevelName(below, i18n, locale) },
355
+ })
356
+ : i18n._({
357
+ id: 'editor.panels.floorFoundationPanel.kickerLevel',
358
+ message: 'Floor plate · {level}',
359
+ values: { level: levelName },
360
+ })
296
361
  }
297
362
 
298
363
  /**
@@ -311,7 +376,6 @@ export function floorFoundationModel(nodes: Record<string, AnyNode>, plate: Slab
311
376
  : 0
312
377
  return {
313
378
  upper,
314
- kicker: floorPlateKicker(nodes, plate),
315
379
  preset: upper ? ('ground' as const) : footprintPreset(nodes, plate),
316
380
  /** Upstairs: the floor top (level-local). On the ground: the foundation height. */
317
381
  height,
@@ -332,6 +396,8 @@ export function floorFoundationModel(nodes: Record<string, AnyNode>, plate: Slab
332
396
  * the building live and lands as one undo step.
333
397
  */
334
398
  export function FloorFoundationPanel({ node, onClose }: { node: SlabNode; onClose: () => void }) {
399
+ const i18n = usePascalI18n()
400
+ const locale = useLocale()
335
401
  const nodes = useScene((state) => state.nodes)
336
402
  const unit = useViewer((s) => s.unit)
337
403
  const metricNotation = useViewer((s) => s.metricNotation)
@@ -341,7 +407,8 @@ export function FloorFoundationPanel({ node, onClose }: { node: SlabNode; onClos
341
407
  const plate = nodes[node.id as AnyNodeId]
342
408
  if (plate?.type !== 'slab' || plate.plateRole !== 'base') return null
343
409
  const length = (meters: number) => formatLinearMeasurement(meters, unit, metricNotation)
344
- const { upper, kicker, preset, height, floorTop, minHeight, minThickness, underFloor, thickFloor } =
410
+ const kicker = floorPlateKicker(nodes, plate, i18n, locale)
411
+ const { upper, preset, height, floorTop, minHeight, minThickness, underFloor, thickFloor } =
345
412
  floorFoundationModel(nodes, plate)
346
413
  const level = plate.parentId ? nodes[plate.parentId as AnyNodeId] : undefined
347
414
  const maxHeight = level?.type === 'level' ? getStoredLevelHeight(level) - 0.5 : 3
@@ -389,7 +456,7 @@ export function FloorFoundationPanel({ node, onClose }: { node: SlabNode; onClos
389
456
  icon="/icons/floor.webp"
390
457
  kicker={kicker}
391
458
  onClose={onClose}
392
- title={floorFootprintName(nodes, plate)}
459
+ title={localizedFloorFootprintName(nodes, plate, i18n)}
393
460
  >
394
461
  <div
395
462
  className="flex flex-col gap-3 px-4 pt-3 pb-4 text-sm"
@@ -407,13 +474,13 @@ export function FloorFoundationPanel({ node, onClose }: { node: SlabNode; onClos
407
474
  />
408
475
  <section className="flex flex-col gap-1.5" data-floor-plate-group>
409
476
  <h3 className="font-medium text-muted-foreground text-xs">
410
- {upper ? 'Floor plate' : 'Slab'}
477
+ {upper ? i18n._({ id: 'editor.panels.floorFoundationPanel.floorPlate', message: 'Floor plate' }) : i18n._({ id: 'editor.panels.floorFoundationPanel.slab', message: 'Slab' })}
411
478
  </h3>
412
479
  {upper ? (
413
- <div title="A thicker floor grows upward: the rooms below keep their height.">
480
+ <div title={i18n._({ id: 'editor.panels.floorFoundationPanel.aThickerFloorGrowsUpwardThe', message: 'A thicker floor grows upward: the rooms below keep their height.' })}>
414
481
  <MetricControl
415
482
  className="h-9 text-xs"
416
- label="Floor thickness"
483
+ label={i18n._({ id: 'editor.panels.floorFoundationPanel.floorThickness', message: 'Floor thickness' })}
417
484
  max={Math.max(MIN_SLAB_THICKNESS, maxHeight - underside)}
418
485
  min={Math.max(MIN_SLAB_THICKNESS, minHeight - underside)}
419
486
  onChange={(next) => previewHeight(underside + next)}
@@ -426,10 +493,10 @@ export function FloorFoundationPanel({ node, onClose }: { node: SlabNode; onClos
426
493
  />
427
494
  </div>
428
495
  ) : (
429
- <div title="The slab sits on the foundation (or the ground): a thicker slab raises the floor top and everything on it.">
496
+ <div title={i18n._({ id: 'editor.panels.floorFoundationPanel.theSlabSitsOnTheFoundation', message: 'The slab sits on the foundation (or the ground): a thicker slab raises the floor top and everything on it.' })}>
430
497
  <MetricControl
431
498
  className="h-9 text-xs"
432
- label="Slab thickness"
499
+ label={i18n._({ id: 'editor.panels.floorFoundationPanel.slabThickness', message: 'Slab thickness' })}
433
500
  max={1}
434
501
  min={minThickness}
435
502
  onChange={(next) => {
@@ -454,15 +521,19 @@ export function FloorFoundationPanel({ node, onClose }: { node: SlabNode; onClos
454
521
  )}
455
522
  {upper && (
456
523
  <p className="px-0.5 text-muted-foreground text-xs" data-floor-top>
457
- Floor top +{length(floorTop)}
524
+ {i18n._({
525
+ id: 'editor.panels.floorFoundationPanel.floorTopAbove',
526
+ message: 'Floor top +{height}',
527
+ values: { height: length(floorTop) },
528
+ })}
458
529
  </p>
459
530
  )}
460
531
  <FinishSwatch
461
532
  fallback={EDGE_DEFAULT}
462
- hint="The floor's outside edge, all around the footprint."
463
- label="Edge finish"
533
+ hint={i18n._({ id: 'editor.panels.floorFoundationPanel.theFloorSOutsideEdgeAll', message: 'The floor\'s outside edge, all around the footprint.' })}
534
+ label={i18n._({ id: 'editor.panels.floorFoundationPanel.edgeFinish', message: 'Edge finish' })}
464
535
  node={plate}
465
- role="edge"
536
+ paintRole="edge"
466
537
  value={plate.slots?.edge}
467
538
  />
468
539
  {thickFloor && (
@@ -473,7 +544,7 @@ export function FloorFoundationPanel({ node, onClose }: { node: SlabNode; onClos
473
544
  </section>
474
545
  {underFloor && (
475
546
  <section className="flex flex-col gap-1.5" data-under-floor-group>
476
- <h3 className="font-medium text-muted-foreground text-xs">Under the floor</h3>
547
+ <h3 className="font-medium text-muted-foreground text-xs">{i18n._({ id: 'editor.panels.floorFoundationPanel.underTheFloor', message: 'Under the floor' })}</h3>
477
548
  <SegmentedControl
478
549
  onChange={(next: FootprintPreset) => {
479
550
  if (next === preset) return
@@ -481,17 +552,17 @@ export function FloorFoundationPanel({ node, onClose }: { node: SlabNode; onClos
481
552
  report(applyFloorFoundation(plate.id, patch), patch)
482
553
  }}
483
554
  options={[
484
- { label: 'On the ground', value: 'ground' },
485
- { label: 'Raised on a foundation', value: 'raised' },
555
+ { label: i18n._({ id: 'editor.panels.floorFoundationPanel.onTheGround', message: 'On the ground' }), value: 'ground' },
556
+ { label: i18n._({ id: 'editor.panels.floorFoundationPanel.raisedOnAFoundation', message: 'Raised on a foundation' }), value: 'raised' },
486
557
  ]}
487
558
  value={preset}
488
559
  />
489
560
  {preset === 'raised' && (
490
561
  <>
491
- <div title="The foundation stands on the ground; the slab sits on it.">
562
+ <div title={i18n._({ id: 'editor.panels.floorFoundationPanel.theFoundationStandsOnTheGround', message: 'The foundation stands on the ground; the slab sits on it.' })}>
492
563
  <MetricControl
493
564
  className="h-9 text-xs"
494
- label="Foundation height"
565
+ label={i18n._({ id: 'editor.panels.floorFoundationPanel.foundationHeight', message: 'Foundation height' })}
495
566
  max={Math.max(0.05, maxHeight)}
496
567
  min={minHeight}
497
568
  onChange={previewHeight}
@@ -505,9 +576,9 @@ export function FloorFoundationPanel({ node, onClose }: { node: SlabNode; onClos
505
576
  </div>
506
577
  <FinishSwatch
507
578
  fallback={FOUNDATION_DEFAULT}
508
- label="Foundation finish"
579
+ label={i18n._({ id: 'editor.panels.floorFoundationPanel.foundationFinish', message: 'Foundation finish' })}
509
580
  node={plate}
510
- role="foundation"
581
+ paintRole="foundation"
511
582
  value={
512
583
  typeof plate.foundation?.material === 'string'
513
584
  ? plate.foundation.material
@@ -517,7 +588,11 @@ export function FloorFoundationPanel({ node, onClose }: { node: SlabNode; onClos
517
588
  </>
518
589
  )}
519
590
  <p className="px-0.5 text-muted-foreground text-xs" data-floor-top>
520
- Floor top {length(floorTop)}
591
+ {i18n._({
592
+ id: 'editor.panels.floorFoundationPanel.floorTopValue',
593
+ message: 'Floor top {height}',
594
+ values: { height: length(floorTop) },
595
+ })}
521
596
  </p>
522
597
  </section>
523
598
  )}
@@ -540,7 +615,7 @@ export function FloorFoundationPanel({ node, onClose }: { node: SlabNode; onClos
540
615
  type="button"
541
616
  variant="outline"
542
617
  >
543
- {moveTogetherLabel(notice.together.ids.length, notice.together.lower)}
618
+ {moveTogetherLabel(notice.together.ids.length, notice.together.lower, i18n)}
544
619
  </Button>
545
620
  )}
546
621
  </div>
@@ -5,8 +5,8 @@ import {
5
5
  adjacentLevelId,
6
6
  area,
7
7
  floorOpeningHints,
8
- getLevelDisplayName,
9
- useScene,
8
+ useScene, usePascalI18n,
9
+ useLocale,
10
10
  } from '@pascal-app/core'
11
11
  import { useViewer } from '@pascal-app/viewer'
12
12
  import {
@@ -14,10 +14,13 @@ import {
14
14
  finishFloorOpening,
15
15
  openingRoom,
16
16
  } from '../../../lib/floor-edit-session'
17
- import { formatAreaLabel } from '../../../lib/measurements'
17
+ import { formatAreaLabel } from '@pascal-app/core'
18
18
  import { Button } from '../primitives/button'
19
19
  import { Switch } from '../primitives/switch'
20
20
  import { PanelWrapper } from './panel-wrapper'
21
+ import { localizedLevelName } from '../../../i18n/level-name'
22
+ import { nodeLabel } from '../../../i18n/node-messages'
23
+ import { displayRoomName } from '../../../lib/room-name-messages'
21
24
 
22
25
  /**
23
26
  * A floor (or ceiling) opening: where it is, what it cuts, and the switch for
@@ -27,6 +30,8 @@ import { PanelWrapper } from './panel-wrapper'
27
30
  * handles in the scene. Done and Delete return to the room.
28
31
  */
29
32
  export function FloorOpeningPanel({ openingId }: { openingId: string }) {
33
+ const i18n = usePascalI18n()
34
+ const locale = useLocale()
30
35
  const nodes = useScene((state) => state.nodes)
31
36
  const unit = useViewer((s) => s.unit)
32
37
  const readOnly = useScene((state) => state.readOnly)
@@ -35,7 +40,13 @@ export function FloorOpeningPanel({ openingId }: { openingId: string }) {
35
40
  const onCeiling = opening.drawnOn === 'ceiling'
36
41
  const zoneId = openingRoom(nodes, opening)
37
42
  const zone = zoneId ? nodes[zoneId as AnyNodeId] : undefined
38
- const roomName = zone?.type === 'zone' ? zone.name?.trim() || 'Room' : null
43
+ const storedRoomName = zone?.type === 'zone' ? zone.name?.trim() : undefined
44
+ const roomName =
45
+ zone?.type !== 'zone'
46
+ ? null
47
+ : storedRoomName
48
+ ? displayRoomName(storedRoomName, i18n)
49
+ : i18n._({ id: 'editor.panels.floorOpeningPanel.room', message: 'Room' })
39
50
  const level = opening.parentId ? nodes[opening.parentId as AnyNodeId] : undefined
40
51
  const adjacentId = opening.parentId
41
52
  ? adjacentLevelId(nodes, opening.parentId, onCeiling ? 1 : -1)
@@ -47,19 +58,35 @@ export function FloorOpeningPanel({ openingId }: { openingId: string }) {
47
58
  !!opening.ownerId &&
48
59
  nodes[opening.ownerId as AnyNodeId]?.type === opening.source
49
60
  const hints = floorOpeningHints(nodes, opening)
50
- const title = opening.source === 'stair'
51
- ? 'Stair opening'
52
- : opening.source === 'elevator'
53
- ? 'Elevator opening'
54
- : onCeiling ? 'Ceiling opening' : 'Floor opening'
55
- const kicker = [
56
- 'Opening',
57
- roomName ? `in ${roomName}` : null,
58
- level?.type === 'level' ? getLevelDisplayName(level) : null,
59
- ]
60
- .filter(Boolean)
61
- .join(' · ')
62
- const switchLabel = onCeiling ? 'Also cut the floor above' : 'Also cut the ceiling below'
61
+ const title = i18n._({
62
+ id: 'editor.panels.floorOpeningPanel.title',
63
+ message:
64
+ '{source, select, stair {Stair opening} elevator {Elevator opening} ceiling {Ceiling opening} other {Floor opening}}',
65
+ values: { source: opening.source === 'stair' || opening.source === 'elevator' ? opening.source : onCeiling ? 'ceiling' : 'floor' },
66
+ })
67
+ const levelName = level?.type === 'level' ? localizedLevelName(level, i18n, locale) : null
68
+ const kicker = roomName
69
+ ? levelName
70
+ ? i18n._({
71
+ id: 'editor.panels.floorOpeningPanel.kickerRoomLevel',
72
+ message: 'Opening · in {room} · {level}',
73
+ values: { room: roomName, level: levelName },
74
+ })
75
+ : i18n._({
76
+ id: 'editor.panels.floorOpeningPanel.kickerRoom',
77
+ message: 'Opening · in {room}',
78
+ values: { room: roomName },
79
+ })
80
+ : levelName
81
+ ? i18n._({
82
+ id: 'editor.panels.floorOpeningPanel.kickerLevel',
83
+ message: 'Opening · {level}',
84
+ values: { level: levelName },
85
+ })
86
+ : i18n._({ id: 'editor.panels.floorOpeningPanel.kicker', message: 'Opening' })
87
+ const switchLabel = onCeiling
88
+ ? i18n._({ id: 'editor.panels.floorOpeningPanel.alsoCutTheFloorAbove', message: 'Also cut the floor above' })
89
+ : i18n._({ id: 'editor.panels.floorOpeningPanel.alsoCutTheCeilingBelow', message: 'Also cut the ceiling below' })
63
90
  const setAdjacent = (next: boolean) =>
64
91
  useScene.getState().updateNode(opening.id, { cutsAdjacent: next })
65
92
 
@@ -72,8 +99,23 @@ export function FloorOpeningPanel({ openingId }: { openingId: string }) {
72
99
  >
73
100
  <div className="flex flex-col gap-3 px-4 pt-3 pb-4 text-sm" data-floor-opening-panel={opening.id}>
74
101
  <p className="text-muted-foreground text-xs">
75
- {formatAreaLabel(area([{ outer: opening.polygon, holes: [] }]), unit, 2)}
76
- {owned ? ' · Shape follows its owner.' : ' · Drag its corners to change the shape.'}
102
+ {owned
103
+ ? opening.source === 'stair'
104
+ ? i18n._({
105
+ id: 'editor.panels.floorOpeningPanel.areaStairResize',
106
+ message: '{area} · Drag its sides or corners to resize it; it moves with the stair.',
107
+ values: { area: formatAreaLabel(area([{ outer: opening.polygon, holes: [] }]), unit, 2) },
108
+ })
109
+ : i18n._({
110
+ id: 'editor.panels.floorOpeningPanel.areaOwned',
111
+ message: '{area} · Shape follows its owner.',
112
+ values: { area: formatAreaLabel(area([{ outer: opening.polygon, holes: [] }]), unit, 2) },
113
+ })
114
+ : i18n._({
115
+ id: 'editor.panels.floorOpeningPanel.areaDragCorners',
116
+ message: '{area} · Drag its corners to change the shape.',
117
+ values: { area: formatAreaLabel(area([{ outer: opening.polygon, holes: [] }]), unit, 2) },
118
+ })}
77
119
  </p>
78
120
  {owned && (
79
121
  <button
@@ -82,12 +124,14 @@ export function FloorOpeningPanel({ openingId }: { openingId: string }) {
82
124
  onClick={() => useViewer.getState().setSelection({ selectedIds: [opening.ownerId!] })}
83
125
  type="button"
84
126
  >
85
- Owned by {opening.source === 'stair' ? 'Staircase' : 'Elevator'}
127
+ {opening.source === 'stair'
128
+ ? i18n._({ id: 'editor.panels.floorOpeningPanel.ownedByStaircase', message: 'Owned by Staircase' })
129
+ : i18n._({ id: 'editor.panels.floorOpeningPanel.ownedByElevator', message: 'Owned by Elevator' })}
86
130
  </button>
87
131
  )}
88
132
  {hosted ? (
89
133
  <p className="text-muted-foreground text-xs" data-opening-hosted>
90
- A hatch in the mezzanine: it cuts only the mezzanine’s floor.
134
+ {i18n._({ id: 'editor.panels.floorOpeningPanel.aHatchInTheMezzanineIt', message: 'A hatch in the mezzanine: it cuts only the mezzanine’s floor.' })}
91
135
  </p>
92
136
  ) : owned ? null : (
93
137
  <label className="flex items-start gap-3" data-opening-adjacent>
@@ -95,10 +139,14 @@ export function FloorOpeningPanel({ openingId }: { openingId: string }) {
95
139
  <span className="font-medium text-[13px] text-foreground">{switchLabel}</span>
96
140
  <span className="text-muted-foreground text-xs">
97
141
  {adjacent?.type === 'level'
98
- ? `On ${getLevelDisplayName(adjacent)}, as one void with this opening.`
142
+ ? i18n._({
143
+ id: 'editor.panels.floorOpeningPanel.oneVoidWith',
144
+ message: 'On {level}, as one void with this opening.',
145
+ values: { level: localizedLevelName(adjacent, i18n, locale) },
146
+ })
99
147
  : onCeiling
100
- ? 'There is no floor above this storey.'
101
- : 'There is no storey below.'}
148
+ ? i18n._({ id: 'editor.panels.floorOpeningPanel.thereIsNoFloorAboveThis', message: 'There is no floor above this storey.' })
149
+ : i18n._({ id: 'editor.panels.floorOpeningPanel.thereIsNoStoreyBelow', message: 'There is no storey below.' })}
102
150
  </span>
103
151
  </span>
104
152
  <Switch
@@ -123,7 +171,7 @@ export function FloorOpeningPanel({ openingId }: { openingId: string }) {
123
171
  type="button"
124
172
  variant="outline"
125
173
  >
126
- Done
174
+ {i18n._({ id: 'editor.panels.floorOpeningPanel.done', message: 'Done' })}
127
175
  </Button>
128
176
  {!owned && <Button
129
177
  className="flex-1 rounded-full"
@@ -134,7 +182,7 @@ export function FloorOpeningPanel({ openingId }: { openingId: string }) {
134
182
  type="button"
135
183
  variant="outline"
136
184
  >
137
- Delete
185
+ {i18n._({ id: 'editor.panels.floorOpeningPanel.delete', message: 'Delete' })}
138
186
  </Button>}
139
187
  </div>
140
188
  </div>
@@ -0,0 +1,66 @@
1
+ 'use client'
2
+
3
+ import { Icon } from '@iconify/react'
4
+ import {
5
+ type AnyNodeId,
6
+ getInspectorExtensions,
7
+ type IconRef,
8
+ useRegistryVersion,
9
+ useScene,
10
+ } from '@pascal-app/core'
11
+ import { useViewer } from '@pascal-app/viewer'
12
+ import { lazy, Suspense, useMemo } from 'react'
13
+ import { IconRefImage } from '../icon-ref'
14
+
15
+ /**
16
+ * The plugin inspector extensions (`Plugin.inspectorExtensions`) for the single selected node,
17
+ * gated on their plugin being installed, and the one that owns the card body for it
18
+ * (`primaryWhen`). Read by the inspector card and by the action menu that opens them.
19
+ */
20
+ export function useInspectorExtensions() {
21
+ const registryVersion = useRegistryVersion()
22
+ const selectedId = useViewer((s) =>
23
+ s.selection.selectedIds.length === 1 ? s.selection.selectedIds[0] : undefined,
24
+ ) as AnyNodeId | undefined
25
+ // Subscribe to the selected node's *type* only — a string primitive that
26
+ // doesn't change as fields are edited (same trick as ParametricInspector).
27
+ const selectedType = useScene((s) => (selectedId ? (s.nodes[selectedId]?.type ?? null) : null))
28
+ const installedPlugins = useScene((s) => s.installedPlugins)
29
+ const extensions = useMemo(() => {
30
+ // re-derive when plugin extensions register after mount (async plugin load)
31
+ void registryVersion
32
+ if (!selectedType) return []
33
+ return getInspectorExtensions(selectedType).filter(
34
+ (extension) => !extension.pluginId || installedPlugins.includes(extension.pluginId),
35
+ )
36
+ }, [selectedType, installedPlugins, registryVersion])
37
+ const primaryExtensionId = useScene((state) => {
38
+ const node = selectedId ? state.nodes[selectedId] : undefined
39
+ return node ? extensions.find((extension) => extension.primaryWhen?.(node))?.id : undefined
40
+ })
41
+ const primaryExtension = extensions.find((extension) => extension.id === primaryExtensionId)
42
+ return { selectedId, extensions, primaryExtension }
43
+ }
44
+
45
+ /** An extension's 16px icon — mirrors the parametric inspector's `renderIcon` (plain <img> so no next/image server deps). */
46
+ export function renderExtensionIcon(ref: IconRef): React.ReactNode {
47
+ if (ref.kind === 'url') {
48
+ return <IconRefImage className="h-4 w-4 shrink-0" src={ref.src} />
49
+ }
50
+ if (ref.kind === 'iconify') {
51
+ return <Icon height={16} icon={ref.name} width={16} />
52
+ }
53
+ if (ref.kind === 'svg') {
54
+ return (
55
+ <svg height={16} viewBox={ref.viewBox} width={16}>
56
+ <path d={ref.path} fill="currentColor" />
57
+ </svg>
58
+ )
59
+ }
60
+ const LazyIcon = lazy(ref.module)
61
+ return (
62
+ <Suspense fallback={null}>
63
+ <LazyIcon />
64
+ </Suspense>
65
+ )
66
+ }
@@ -1,5 +1,6 @@
1
1
  'use client'
2
2
 
3
+ import { usePascalI18n } from '@pascal-app/core'
3
4
  import { X } from 'lucide-react'
4
5
  import { AnimatePresence, motion } from 'motion/react'
5
6
  import { type ReactNode, useEffect, useState } from 'react'
@@ -19,6 +20,7 @@ const HEIGHT_VH = 50
19
20
  const DRAG_CLOSE_THRESHOLD_PX = 120
20
21
 
21
22
  export function MobilePanelSheet({ open, onClose, icon, title, children }: MobilePanelSheetProps) {
23
+ const i18n = usePascalI18n()
22
24
  const [mounted, setMounted] = useState(false)
23
25
  const setMobilePanelSheetHeight = useEditor((s) => s.setMobilePanelSheetHeight)
24
26
 
@@ -88,7 +90,7 @@ export function MobilePanelSheet({ open, onClose, icon, title, children }: Mobil
88
90
  </h2>
89
91
  </div>
90
92
  <button
91
- aria-label="Close"
93
+ aria-label={i18n._({ id: 'editor.panels.mobilePanelSheet.close', message: 'Close' })}
92
94
  className="flex h-8 w-8 items-center justify-center rounded-md bg-black/4 dark:bg-[#2C2C2E] text-muted-foreground transition-colors hover:bg-black/8 dark:hover:bg-[#3e3e3e] hover:text-foreground"
93
95
  onClick={onClose}
94
96
  type="button"