@pascal-app/editor 1.0.5 → 1.0.7

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 (317) hide show
  1. package/package.json +20 -17
  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 +20 -8
  6. package/src/components/editor/elevation-3d-guide-layer.tsx +1 -1
  7. package/src/components/editor/first-person-controls.tsx +72 -61
  8. package/src/components/editor/floating-action-menu.tsx +27 -9
  9. package/src/components/editor/floating-building-action-menu.tsx +1 -1
  10. package/src/components/editor/floorplan-panel.test.ts +220 -0
  11. package/src/components/editor/floorplan-panel.tsx +285 -132
  12. package/src/components/editor/footprint-height-handle.tsx +1 -1
  13. package/src/components/editor/grid.tsx +24 -15
  14. package/src/components/editor/group-actions.ts +19 -1
  15. package/src/components/editor/group-floating-action-menu.tsx +1 -1
  16. package/src/components/editor/group-move-3d.ts +27 -2
  17. package/src/components/editor/group-selection-box-3d.tsx +4 -1
  18. package/src/components/editor/group-transform-shared.ts +20 -0
  19. package/src/components/editor/index.tsx +130 -20
  20. package/src/components/editor/inspector-extension-actions.tsx +36 -0
  21. package/src/components/editor/localized-html.tsx +22 -0
  22. package/src/components/editor/node-action-menu.tsx +46 -12
  23. package/src/components/editor/node-arrow-handles.tsx +1 -2
  24. package/src/components/editor/open-wall-ends-3d-layer.tsx +1 -1
  25. package/src/components/editor/opening-guides-3d-layer.tsx +1 -1
  26. package/src/components/editor/registry-action-contributions.test.tsx +67 -6
  27. package/src/components/editor/registry-action-contributions.tsx +115 -15
  28. package/src/components/editor/riser-diagram-panel.tsx +59 -9
  29. package/src/components/editor/room-controls.tsx +1 -1
  30. package/src/components/editor/room-handles-2d.tsx +156 -6
  31. package/src/components/editor/room-handles.tsx +241 -51
  32. package/src/components/editor/room-highlight.tsx +2 -0
  33. package/src/components/editor/room-labels.tsx +29 -15
  34. package/src/components/editor/selection-manager.tsx +15 -102
  35. package/src/components/editor/separator-end-grip.tsx +74 -0
  36. package/src/components/editor/site-edge-labels.tsx +1 -1
  37. package/src/components/editor/thumbnail-generator.test.ts +15 -6
  38. package/src/components/editor/thumbnail-generator.tsx +26 -1
  39. package/src/components/editor/use-floorplan-background-placement.ts +55 -52
  40. package/src/components/editor/views/view-bar.tsx +111 -26
  41. package/src/components/editor/wall-measurement-label.tsx +1 -1
  42. package/src/components/editor/wall-move-side-handles.tsx +5 -1
  43. package/src/components/editor/wall-pull-grip.tsx +193 -0
  44. package/src/components/editor/wall-snap-beacon-layer.tsx +81 -1
  45. package/src/components/editor-2d/floorplan-adjacent-walls-layer.test.ts +76 -0
  46. package/src/components/editor-2d/floorplan-adjacent-walls-layer.tsx +186 -0
  47. package/src/components/editor-2d/floorplan-alignment-guide-layer.tsx +63 -3
  48. package/src/components/editor-2d/floorplan-group-move.tsx +31 -3
  49. package/src/components/editor-2d/floorplan-measurement-tool-layer.test.ts +6 -0
  50. package/src/components/editor-2d/floorplan-measurement-tool-layer.tsx +22 -13
  51. package/src/components/editor-2d/floorplan-open-wall-ends-layer.tsx +11 -3
  52. package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +7 -1
  53. package/src/components/editor-2d/floorplan-snap-beacon-layer.tsx +96 -4
  54. package/src/components/editor-2d/renderers/floorplan-dimension-edit-overlay.tsx +52 -12
  55. package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +12 -1
  56. package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +197 -37
  57. package/src/components/procedural-items/inspector.tsx +59 -12
  58. package/src/components/systems/ceiling/ceiling-bracket-batch.test.ts +2 -0
  59. package/src/components/systems/ceiling/ceiling-system.test.ts +141 -0
  60. package/src/components/systems/ceiling/ceiling-system.tsx +53 -18
  61. package/src/components/tools/distribution/distribution-run-tool.tsx +50 -26
  62. package/src/components/tools/item/placement-history.test.tsx +1 -1
  63. package/src/components/tools/item/placement-strategies.test.ts +64 -1
  64. package/src/components/tools/item/placement-strategies.ts +5 -9
  65. package/src/components/tools/item/use-placement-coordinator.tsx +30 -34
  66. package/src/components/tools/registry/item-surface-move.test.ts +11 -0
  67. package/src/components/tools/select/select-candidates.test.ts +45 -0
  68. package/src/components/tools/select/select-candidates.ts +13 -2
  69. package/src/components/tools/shared/cursor-sphere.tsx +10 -3
  70. package/src/components/tools/shared/draft-measurement-label.tsx +1 -1
  71. package/src/components/tools/shared/placement-box.tsx +1 -1
  72. package/src/components/tools/shared/placement-dimension-guides.tsx +1 -1
  73. package/src/components/tools/site/terrain-brush-cursor.tsx +1 -1
  74. package/src/components/tools/wall/wall-drafting.test.ts +299 -23
  75. package/src/components/tools/wall/wall-drafting.ts +132 -24
  76. package/src/components/tools/wall/wall-pull-snap.test.ts +274 -0
  77. package/src/components/tools/wall/wall-pull-snap.ts +563 -0
  78. package/src/components/tools/wall/wall-snap-geometry.test.ts +113 -1
  79. package/src/components/tools/wall/wall-snap-geometry.ts +137 -0
  80. package/src/components/tools/zone/zone-tool.tsx +3 -3
  81. package/src/components/ui/action-menu/action-button.tsx +2 -0
  82. package/src/components/ui/action-menu/camera-actions.tsx +8 -7
  83. package/src/components/ui/action-menu/control-modes.tsx +9 -5
  84. package/src/components/ui/action-menu/follow-pascal-pill.tsx +3 -1
  85. package/src/components/ui/action-menu/follow-pascal-toggle.tsx +7 -1
  86. package/src/components/ui/action-menu/furnish-tools.tsx +7 -6
  87. package/src/components/ui/action-menu/index.tsx +1 -0
  88. package/src/components/ui/action-menu/measurement-control.tsx +35 -29
  89. package/src/components/ui/action-menu/point-ask-button.tsx +4 -3
  90. package/src/components/ui/action-menu/structure-tools.tsx +25 -23
  91. package/src/components/ui/action-menu/view-toggles.test.ts +18 -0
  92. package/src/components/ui/action-menu/view-toggles.tsx +256 -51
  93. package/src/components/ui/build-panel/build-panel.tsx +163 -25
  94. package/src/components/ui/command-palette/editor-commands.tsx +38 -11
  95. package/src/components/ui/controls/finish-swatch.tsx +45 -19
  96. package/src/components/ui/controls/material-picker.tsx +24 -17
  97. package/src/components/ui/controls/metric-control.test.ts +52 -0
  98. package/src/components/ui/controls/metric-control.tsx +62 -24
  99. package/src/components/ui/controls/segmented-control.tsx +5 -2
  100. package/src/components/ui/controls/slider-control.test.tsx +23 -0
  101. package/src/components/ui/controls/slider-control.tsx +30 -14
  102. package/src/components/ui/floating-level-selector.tsx +100 -48
  103. package/src/components/ui/helpers/building-helper.tsx +5 -3
  104. package/src/components/ui/helpers/contextual-helper-panel.tsx +97 -37
  105. package/src/components/ui/helpers/helper-manager.test.tsx +53 -3
  106. package/src/components/ui/helpers/helper-manager.tsx +82 -57
  107. package/src/components/ui/helpers/item-helper.tsx +6 -4
  108. package/src/components/ui/helpers/registered-tool-helper.tsx +3 -1
  109. package/src/components/ui/level-duplicate-dialog.tsx +32 -23
  110. package/src/components/ui/level-wall-copy-prompt.tsx +124 -0
  111. package/src/components/ui/panels/collections/collections-popover.tsx +11 -11
  112. package/src/components/ui/panels/deck-boards-fields.tsx +136 -0
  113. package/src/components/ui/panels/floor-foundation-panel.tsx +113 -38
  114. package/src/components/ui/panels/floor-opening-panel.tsx +73 -25
  115. package/src/components/ui/panels/inspector-extensions.tsx +66 -0
  116. package/src/components/ui/panels/mobile-panel-sheet.tsx +3 -1
  117. package/src/components/ui/panels/mobile-selection-bar.tsx +8 -6
  118. package/src/components/ui/panels/multi-field-value.test.ts +27 -0
  119. package/src/components/ui/panels/multi-field-value.ts +20 -1
  120. package/src/components/ui/panels/multi-height-mode.tsx +8 -7
  121. package/src/components/ui/panels/multi-parametric-inspector.tsx +122 -34
  122. package/src/components/ui/panels/multi-selection-panel.tsx +75 -35
  123. package/src/components/ui/panels/node-display.ts +35 -24
  124. package/src/components/ui/panels/panel-manager.tsx +5 -3
  125. package/src/components/ui/panels/panel-wrapper.tsx +22 -109
  126. package/src/components/ui/panels/parametric-field-control.tsx +13 -10
  127. package/src/components/ui/panels/parametric-inspector.tsx +18 -12
  128. package/src/components/ui/panels/reference-panel.tsx +51 -35
  129. package/src/components/ui/panels/room-construction-rows.tsx +464 -65
  130. package/src/components/ui/panels/room-name-options.test.ts +36 -3
  131. package/src/components/ui/panels/room-name-options.ts +110 -11
  132. package/src/components/ui/panels/room-openings-field.tsx +21 -12
  133. package/src/components/ui/panels/room-panel.tsx +206 -99
  134. package/src/components/ui/panels/shape-choice.tsx +7 -5
  135. package/src/components/ui/panels/wall-region-list.tsx +7 -7
  136. package/src/components/ui/primitives/shortcut-token.tsx +10 -6
  137. package/src/components/ui/sidebar/icon-rail.tsx +11 -9
  138. package/src/components/ui/sidebar/panels/items-panel/function-tree-panel.tsx +10 -10
  139. package/src/components/ui/sidebar/panels/items-panel/index.tsx +10 -9
  140. package/src/components/ui/sidebar/panels/settings-panel/index.tsx +3 -0
  141. package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +102 -32
  142. package/src/components/ui/sidebar/panels/site-panel/building-tree-node.tsx +4 -3
  143. package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +4 -2
  144. package/src/components/ui/sidebar/panels/site-panel/chimney-tree-node.tsx +4 -2
  145. package/src/components/ui/sidebar/panels/site-panel/collection-tree-node.tsx +7 -5
  146. package/src/components/ui/sidebar/panels/site-panel/column-tree-node.tsx +4 -2
  147. package/src/components/ui/sidebar/panels/site-panel/default-names.ts +75 -0
  148. package/src/components/ui/sidebar/panels/site-panel/door-tree-node.tsx +4 -2
  149. package/src/components/ui/sidebar/panels/site-panel/dormer-tree-node.tsx +4 -2
  150. package/src/components/ui/sidebar/panels/site-panel/elevator-tree-node.tsx +4 -2
  151. package/src/components/ui/sidebar/panels/site-panel/fence-tree-node.tsx +4 -2
  152. package/src/components/ui/sidebar/panels/site-panel/gutter-tree-node.tsx +4 -2
  153. package/src/components/ui/sidebar/panels/site-panel/index.tsx +117 -64
  154. package/src/components/ui/sidebar/panels/site-panel/inline-rename-input.tsx +36 -3
  155. package/src/components/ui/sidebar/panels/site-panel/item-tree-node.tsx +4 -2
  156. package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +7 -4
  157. package/src/components/ui/sidebar/panels/site-panel/registry-tree-node.tsx +8 -2
  158. package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +8 -5
  159. package/src/components/ui/sidebar/panels/site-panel/shelf-tree-node.tsx +4 -2
  160. package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +8 -3
  161. package/src/components/ui/sidebar/panels/site-panel/solar-panel-tree-node.tsx +4 -2
  162. package/src/components/ui/sidebar/panels/site-panel/spawn-tree-node.tsx +4 -2
  163. package/src/components/ui/sidebar/panels/site-panel/stair-tree-node.tsx +8 -7
  164. package/src/components/ui/sidebar/panels/site-panel/tree-node-actions.tsx +7 -6
  165. package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +16 -1
  166. package/src/components/ui/sidebar/panels/site-panel/unit-tree-node.tsx +15 -8
  167. package/src/components/ui/sidebar/panels/site-panel/wall-tree-node.tsx +4 -2
  168. package/src/components/ui/sidebar/panels/site-panel/window-tree-node.tsx +4 -2
  169. package/src/components/ui/sidebar/panels/site-panel/zone-membership-checkbox.tsx +3 -1
  170. package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +18 -8
  171. package/src/components/ui/sidebar/panels/zone-panel/index.tsx +25 -14
  172. package/src/components/ui/sidebar/tab-bar.tsx +4 -1
  173. package/src/components/ui/sidebar/use-plugin-panels.tsx +19 -10
  174. package/src/components/ui/snap-target-badge.tsx +7 -6
  175. package/src/components/ui/wall-type/wall-type-editor-dialog.tsx +721 -0
  176. package/src/components/ui/wall-type/wall-type-menu.tsx +167 -0
  177. package/src/components/viewer/floorplan-compass-button.tsx +12 -3
  178. package/src/components/viewer/floorplan-preview.tsx +27 -19
  179. package/src/components/viewer/render-settings-menu.tsx +294 -0
  180. package/src/components/viewer/sky-control.tsx +710 -0
  181. package/src/components/viewer/use-viewer-ui-sounds.test.ts +15 -0
  182. package/src/components/viewer/use-viewer-ui-sounds.ts +46 -0
  183. package/src/components/viewer/viewer-controls-bar.tsx +278 -260
  184. package/src/components/viewer/viewer-hover-label.tsx +12 -4
  185. package/src/components/viewer/viewer-rooms.tsx +61 -4
  186. package/src/components/viewer/viewer-scene-header.tsx +28 -17
  187. package/src/components/viewer/viewer-stage-switcher.tsx +4 -2
  188. package/src/components/viewer-overlay.tsx +8 -0
  189. package/src/components/walkthrough-hud.tsx +75 -2
  190. package/src/hooks/use-keyboard.test.ts +85 -4
  191. package/src/hooks/use-keyboard.ts +65 -7
  192. package/src/hooks/use-selected-room.ts +12 -1
  193. package/src/i18n/floor-footprint-name.ts +30 -0
  194. package/src/i18n/level-name.ts +39 -0
  195. package/src/i18n/locales/en/messages.po +15117 -0
  196. package/src/i18n/locales/en/messages.ts +1 -0
  197. package/src/i18n/locales/en-XA/messages.ts +1 -0
  198. package/src/i18n/locales/es/messages.po +37501 -0
  199. package/src/i18n/locales/es/messages.ts +1 -0
  200. package/src/i18n/locales/fr/messages.po +37501 -0
  201. package/src/i18n/locales/fr/messages.ts +1 -0
  202. package/src/i18n/locales/ja/messages.po +37501 -0
  203. package/src/i18n/locales/ja/messages.ts +1 -0
  204. package/src/i18n/locales/zh-CN/messages.po +37501 -0
  205. package/src/i18n/locales/zh-CN/messages.ts +1 -0
  206. package/src/i18n/locales.test.ts +63 -0
  207. package/src/i18n/locales.ts +32 -0
  208. package/src/i18n/message-slot.tsx +25 -0
  209. package/src/i18n/node-messages.ts +169 -0
  210. package/src/i18n/pseudo-locale.ts +17 -0
  211. package/src/i18n/published-entry.test.ts +17 -0
  212. package/src/i18n/scene-theme-name.ts +16 -0
  213. package/src/index.tsx +156 -4
  214. package/src/lib/add-storey.test.ts +3 -3
  215. package/src/lib/contextual-help.test.ts +20 -1
  216. package/src/lib/contextual-help.ts +95 -19
  217. package/src/lib/continuation.ts +38 -11
  218. package/src/lib/floor-footprints.test.ts +2 -1
  219. package/src/lib/floor-foundation-support.test.ts +7 -3
  220. package/src/lib/floorplan/dimension-drive.test.ts +36 -2
  221. package/src/lib/floorplan/dimension-drive.ts +68 -3
  222. package/src/lib/floorplan/floorplan-export.test.ts +16 -5
  223. package/src/lib/floorplan/floorplan-export.tsx +13 -16
  224. package/src/lib/floorplan/floorplan-extension.test.ts +11 -12
  225. package/src/lib/floorplan/floorplan-extension.ts +37 -18
  226. package/src/lib/floorplan/floorplan-mode.test.ts +13 -6
  227. package/src/lib/floorplan/floorplan-mode.ts +14 -12
  228. package/src/lib/floorplan/floorplan-plugin-install.test.tsx +1 -1
  229. package/src/lib/floorplan/floorplan-readonly.ts +3 -3
  230. package/src/lib/floorplan/schedules.ts +10 -6
  231. package/src/lib/floorplan/site-plan/site-annotations.test.ts +46 -1
  232. package/src/lib/floorplan/site-plan/site-annotations.ts +3 -0
  233. package/src/lib/floorplan/site-plan/site-parts.ts +6 -3
  234. package/src/lib/geometry-script/author.test.ts +114 -0
  235. package/src/lib/geometry-script/author.ts +55 -23
  236. package/src/lib/geometry-script/client.ts +3 -0
  237. package/src/lib/geometry-script/finish-scripts.ts +7 -7
  238. package/src/lib/glb-export.test.ts +327 -0
  239. package/src/lib/glb-export.ts +70 -2
  240. package/src/lib/grid-frame.test.ts +56 -3
  241. package/src/lib/grid-frame.ts +28 -1
  242. package/src/lib/history.test.ts +2 -1
  243. package/src/lib/hud-title.ts +48 -24
  244. package/src/lib/ifc-export.test.ts +41 -1
  245. package/src/lib/inspector-card-mode.test.ts +3 -31
  246. package/src/lib/inspector-card-mode.ts +5 -10
  247. package/src/lib/inspector-expanded.test.ts +13 -0
  248. package/src/lib/inspector-expanded.ts +13 -1
  249. package/src/lib/keyboard-pan.test.ts +7 -0
  250. package/src/lib/keyboard-pan.ts +9 -2
  251. package/src/lib/layer-thickness.test.ts +22 -0
  252. package/src/lib/layer-thickness.ts +79 -0
  253. package/src/lib/lot/drop-in.test.ts +59 -1
  254. package/src/lib/lot/drop-in.ts +42 -50
  255. package/src/lib/material-swatch-style.ts +24 -0
  256. package/src/lib/measurement-parser.ts +7 -3
  257. package/src/lib/measurements.test.ts +17 -0
  258. package/src/lib/measurements.ts +20 -5
  259. package/src/lib/mezzanine-selection.test.ts +3 -1
  260. package/src/lib/orbit-near-plane.test.ts +12 -0
  261. package/src/lib/orbit-near-plane.ts +13 -0
  262. package/src/lib/paint-regions.test.ts +8 -5
  263. package/src/lib/paint-scope.test.ts +53 -0
  264. package/src/lib/paint-scope.ts +18 -4
  265. package/src/lib/panel-rows.ts +17 -8
  266. package/src/lib/reference-scale.ts +83 -0
  267. package/src/lib/room-built-on.test.ts +26 -12
  268. package/src/lib/room-built-on.ts +20 -2
  269. package/src/lib/room-construction.test.ts +28 -1
  270. package/src/lib/room-construction.ts +14 -0
  271. package/src/lib/room-controls-phase6.test.ts +140 -0
  272. package/src/lib/room-handle-drag.ts +557 -42
  273. package/src/lib/room-name-catalog.test.ts +8 -0
  274. package/src/lib/room-name-catalog.ts +8 -1
  275. package/src/lib/room-name-messages.test.ts +132 -0
  276. package/src/lib/room-name-messages.ts +147 -0
  277. package/src/lib/room-name-storage.test.ts +334 -0
  278. package/src/lib/room-push-dimensions.test.ts +37 -0
  279. package/src/lib/room-push-dimensions.ts +226 -2
  280. package/src/lib/room-selection.test.ts +23 -6
  281. package/src/lib/scene-clipboard.test.ts +70 -49
  282. package/src/lib/scene-clipboard.ts +9 -4
  283. package/src/lib/selection-cycle.ts +63 -0
  284. package/src/lib/separator-edit.test.ts +431 -0
  285. package/src/lib/separator-edit.ts +179 -0
  286. package/src/lib/separator-lines.ts +38 -0
  287. package/src/lib/sfx-bus.ts +27 -1
  288. package/src/lib/sfx-player.ts +16 -0
  289. package/src/lib/tool-cursor-icon.ts +5 -1
  290. package/src/lib/tool-shortcuts.ts +130 -0
  291. package/src/lib/viewer-selection.test.ts +28 -24
  292. package/src/lib/viewer-selection.ts +20 -5
  293. package/src/lib/wall-draw-variant.test.ts +7 -6
  294. package/src/lib/wall-draw-variant.ts +107 -6
  295. package/src/lib/wall-nudge.test.ts +44 -0
  296. package/src/lib/wall-nudge.ts +131 -0
  297. package/src/lib/wall-parallel-snap.test.ts +83 -0
  298. package/src/lib/wall-parallel-snap.ts +176 -0
  299. package/src/lib/wall-placement-pointer.test.ts +23 -0
  300. package/src/lib/wall-placement-pointer.ts +18 -0
  301. package/src/lib/wall-pull.test.ts +245 -0
  302. package/src/lib/wall-pull.ts +209 -0
  303. package/src/lib/wall-push-line-up.test.ts +132 -0
  304. package/src/lib/wall-region-list.ts +14 -5
  305. package/src/lib/wall-run-snap.test.ts +188 -0
  306. package/src/lib/wall-run-snap.ts +83 -0
  307. package/src/lib/wall-type.test.ts +125 -0
  308. package/src/lib/wall-type.ts +119 -0
  309. package/src/lib/window-interaction.test.ts +42 -0
  310. package/src/lib/window-interaction.ts +47 -5
  311. package/src/lib/world-grid-snap.test.ts +139 -2
  312. package/src/lib/world-grid-snap.ts +102 -105
  313. package/src/store/use-alignment-guides.ts +27 -1
  314. package/src/store/use-editor.tsx +48 -0
  315. package/src/store/use-floorplan-annotation-visibility.test.ts +64 -0
  316. package/src/store/use-floorplan-annotation-visibility.ts +47 -22
  317. package/src/store/use-wall-snap-indicator.ts +15 -2
@@ -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 {
@@ -18,6 +18,9 @@ import { formatAreaLabel } from '../../../lib/measurements'
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"
@@ -1,5 +1,6 @@
1
1
  'use client'
2
2
 
3
+ import { usePascalI18n } from '@pascal-app/core'
3
4
  import type { AnyNode } from '@pascal-app/core'
4
5
  import { Copy, Move, SlidersHorizontal, Trash2 } from 'lucide-react'
5
6
  import type { MouseEventHandler } from 'react'
@@ -29,7 +30,8 @@ export function MobileSelectionBar({
29
30
  onDelete,
30
31
  onEdit,
31
32
  }: MobileSelectionBarProps) {
32
- const display = getNodeDisplay(node)
33
+ const i18n = usePascalI18n()
34
+ const display = getNodeDisplay(node, i18n)
33
35
  const resolvedLabel = label ?? display.label
34
36
  const resolvedIcon = icon ?? display.icon
35
37
 
@@ -38,7 +40,7 @@ export function MobileSelectionBar({
38
40
  return (
39
41
  <div className="pointer-events-auto absolute right-3 bottom-6 left-3 z-50 flex h-12 items-stretch gap-1 rounded-2xl border border-border/50 bg-background/95 px-2 shadow-2xl backdrop-blur-xl">
40
42
  <button
41
- aria-label={`Edit ${resolvedLabel}`}
43
+ aria-label={i18n._({ id: 'editor.panels.mobileSelectionBar.editResolvedlabel', message: 'Edit {resolvedLabel}', values: { resolvedLabel } })}
42
44
  className={cn(
43
45
  'flex min-w-0 flex-1 items-center gap-2 rounded-lg px-2 text-left transition-colors hover:bg-black/5 dark:hover:bg-white/8',
44
46
  )}
@@ -57,7 +59,7 @@ export function MobileSelectionBar({
57
59
 
58
60
  <div className="flex items-center gap-0.5 border-border/40 border-l pl-1">
59
61
  <button
60
- aria-label="Move"
62
+ aria-label={i18n._({ id: 'editor.panels.mobileSelectionBar.move', message: 'Move' })}
61
63
  className={ACTION_BTN}
62
64
  onClick={(e) => {
63
65
  stop(e)
@@ -68,7 +70,7 @@ export function MobileSelectionBar({
68
70
  <Move className="h-4 w-4" />
69
71
  </button>
70
72
  <button
71
- aria-label="Duplicate"
73
+ aria-label={i18n._({ id: 'editor.panels.mobileSelectionBar.duplicate', message: 'Duplicate' })}
72
74
  className={ACTION_BTN}
73
75
  onClick={(e) => {
74
76
  stop(e)
@@ -79,7 +81,7 @@ export function MobileSelectionBar({
79
81
  <Copy className="h-4 w-4" />
80
82
  </button>
81
83
  <button
82
- aria-label="Delete"
84
+ aria-label={i18n._({ id: 'editor.panels.mobileSelectionBar.delete', message: 'Delete' })}
83
85
  className={cn(
84
86
  ACTION_BTN,
85
87
  'hover:bg-red-500/15 hover:text-red-600 dark:hover:bg-red-500/15 dark:hover:text-red-400',
@@ -93,7 +95,7 @@ export function MobileSelectionBar({
93
95
  <Trash2 className="h-4 w-4" />
94
96
  </button>
95
97
  <button
96
- aria-label="Edit properties"
98
+ aria-label={i18n._({ id: 'editor.panels.mobileSelectionBar.editProperties', message: 'Edit properties' })}
97
99
  className={ACTION_BTN}
98
100
  onClick={(e) => {
99
101
  stop(e)
@@ -15,6 +15,7 @@ import {
15
15
  import {
16
16
  buildMultiNodePatches,
17
17
  commitMultiNodeFields,
18
+ fieldTargetIds,
18
19
  fieldVisibleForAll,
19
20
  reduceFieldValue,
20
21
  reduceHeightBoundMode,
@@ -57,6 +58,32 @@ describe('reduceFieldValue', () => {
57
58
  expect(reduceFieldValue(['a', 'b'], 'thickness', nodes)).toEqual({ kind: 'mixed' })
58
59
  })
59
60
 
61
+ test('an omitted value reads as the field default, not as zero', () => {
62
+ const nodes = {
63
+ a: makeNode('a', 'wall'),
64
+ b: makeNode('b', 'wall', { thickness: 0.1 }),
65
+ c: makeNode('c', 'wall', { thickness: 0.2 }),
66
+ }
67
+ expect(reduceFieldValue(['a', 'b'], 'thickness', nodes, 0.1)).toEqual({
68
+ kind: 'same',
69
+ value: 0.1,
70
+ })
71
+ expect(reduceFieldValue(['a', 'c'], 'thickness', nodes, 0.1)).toEqual({ kind: 'mixed' })
72
+ })
73
+
74
+ test('a field that names the nodes it applies to edits only those', () => {
75
+ const nodes = {
76
+ a: makeNode('a', 'wall', { thickness: 0.2 }),
77
+ b: makeNode('b', 'wall', { thickness: 0.18, assembly: { layers: [] } }),
78
+ c: makeNode('c', 'wall', { thickness: 0.2 }),
79
+ }
80
+ const plain = (node: AnyNode) => !(node as { assembly?: unknown }).assembly
81
+ const targets = fieldTargetIds(['a', 'b', 'c'], plain, nodes)
82
+ expect(targets).toEqual(['a', 'c'])
83
+ expect(reduceFieldValue(targets, 'thickness', nodes)).toEqual({ kind: 'same', value: 0.2 })
84
+ expect(fieldTargetIds(['a', 'b'], undefined, nodes)).toEqual(['a', 'b'])
85
+ })
86
+
60
87
  test('vec3 compares elementwise', () => {
61
88
  const nodes = {
62
89
  a: makeNode('a', 'item', { position: [1, 0, 2] }),
@@ -20,17 +20,23 @@ function fieldValuesEqual(a: unknown, b: unknown): boolean {
20
20
  return false
21
21
  }
22
22
 
23
+ /**
24
+ * The value every node holds for `key`, or mixed. `fallback` is what a node
25
+ * that omits the key shows (the field's default), so an absent value and an
26
+ * explicit default read as the same.
27
+ */
23
28
  export function reduceFieldValue(
24
29
  nodeIds: readonly string[],
25
30
  key: string,
26
31
  nodes: Readonly<Record<string, AnyNode | undefined>>,
32
+ fallback?: unknown,
27
33
  ): ReducedFieldValue {
28
34
  let seen = false
29
35
  let shared: unknown
30
36
  for (const id of nodeIds) {
31
37
  const node = nodes[id]
32
38
  if (!node) continue
33
- const value = (node as Record<string, unknown>)[key]
39
+ const value = (node as Record<string, unknown>)[key] ?? fallback
34
40
  if (!seen) {
35
41
  seen = true
36
42
  shared = value
@@ -78,6 +84,19 @@ export function fieldVisibleForAll(
78
84
  return true
79
85
  }
80
86
 
87
+ /** The selected nodes a field edits: every one, unless it names those it `appliesTo`. */
88
+ export function fieldTargetIds<T extends string>(
89
+ nodeIds: readonly T[],
90
+ appliesTo: ((node: AnyNode) => boolean) | undefined,
91
+ nodes: Readonly<Record<string, AnyNode | undefined>>,
92
+ ): T[] {
93
+ if (!appliesTo) return [...nodeIds]
94
+ return nodeIds.filter((id) => {
95
+ const node = nodes[id]
96
+ return node ? appliesTo(node) : false
97
+ })
98
+ }
99
+
81
100
  export function firstNumericFieldValue(
82
101
  nodeIds: readonly string[],
83
102
  key: string,
@@ -11,7 +11,7 @@ import {
11
11
  resolveCeilingHeight,
12
12
  terrainSupportLift,
13
13
  useScene,
14
- type WallNode,
14
+ type WallNode, usePascalI18n,
15
15
  } from '@pascal-app/core'
16
16
  import { useViewer } from '@pascal-app/viewer'
17
17
  import { useCallback } from 'react'
@@ -99,6 +99,7 @@ export function MultiHeightModeField({
99
99
  max?: number
100
100
  step?: number
101
101
  }) {
102
+ const i18n = usePascalI18n()
102
103
  const unit = useViewer((s) => s.unit)
103
104
  const metricNotation = useViewer((s) => s.metricNotation)
104
105
  const mode = useScene(useShallow((s) => reduceHeightBoundMode(nodeIds, s.nodes)))
@@ -151,29 +152,29 @@ export function MultiHeightModeField({
151
152
  const sliderMixed = storedHeight.kind === 'mixed' || mixedMode
152
153
  const currentLabel = Number.isFinite(liveHeight)
153
154
  ? formatLinearMeasurement(liveHeight, unit, metricNotation)
154
- : 'Mixed'
155
+ : i18n._({ id: 'editor.panels.mixed', message: 'Mixed' })
155
156
 
156
157
  return (
157
158
  <>
158
159
  {nodeType === 'wall' && (
159
160
  <div className="px-1 font-medium text-[10px] text-muted-foreground/80 uppercase tracking-wider">
160
- Top
161
+ {i18n._({ id: 'editor.panels.multiHeightMode.top', message: 'Top' })}
161
162
  </div>
162
163
  )}
163
164
  <SegmentedControl
164
165
  mixed={mixedMode}
165
166
  onChange={applyMode}
166
167
  options={[
167
- { label: 'Follows level', value: 'storey' },
168
- { label: 'Custom height', value: 'custom' },
168
+ { label: i18n._({ id: 'editor.panels.multiHeightMode.followsLevel', message: 'Follows level' }), value: 'storey' },
169
+ { label: i18n._({ id: 'editor.panels.multiHeightMode.customHeight', message: 'Custom height' }), value: 'custom' },
169
170
  ]}
170
171
  value={mode.kind === 'same' ? mode.value : 'storey'}
171
172
  />
172
173
  {isFollows ? (
173
- <div className="px-1 text-[11px] text-muted-foreground">Currently {currentLabel}</div>
174
+ <div className="px-1 text-[11px] text-muted-foreground">{i18n._({ id: 'editor.panels.multiHeightMode.currentlyCurrentlabel', message: 'Currently {currentLabel}', values: { currentLabel } })}</div>
174
175
  ) : isCustom ? (
175
176
  <SliderControl
176
- label="Height"
177
+ label={i18n._({ id: 'editor.panels.multiHeightMode.height', message: 'Height' })}
177
178
  max={max}
178
179
  min={min}
179
180
  mixed={sliderMixed}