@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
@@ -9,6 +9,7 @@ import {
9
9
  createAngleAccumulator,
10
10
  createSceneApi,
11
11
  DEFAULT_ANGLE_STEP,
12
+ formatAngleRadians,
12
13
  type HandleDescriptor,
13
14
  type HandlePortal,
14
15
  type LatchHandle,
@@ -21,7 +22,6 @@ import {
21
22
  useScene,
22
23
  } from '@pascal-app/core'
23
24
  import { useViewer } from '@pascal-app/viewer'
24
- import { Html } from '@react-three/drei'
25
25
  import { createPortal, type ThreeEvent, useFrame, useThree } from '@react-three/fiber'
26
26
  import { useEffect, useMemo, useRef, useState } from 'react'
27
27
  import {
@@ -41,7 +41,6 @@ import {
41
41
  Vector3,
42
42
  } from 'three'
43
43
  import { mergeGeometries } from 'three/examples/jsm/utils/BufferGeometryUtils.js'
44
-
45
44
  import { MeshBasicNodeMaterial } from 'three/webgpu'
46
45
  import { useCeilingEditCeilingId } from '../../lib/ceiling-edit-session'
47
46
  import { EDITOR_LAYER } from '../../lib/constants'
@@ -61,7 +60,6 @@ import useInteractionScope, {
61
60
  useMovingNode,
62
61
  } from '../../store/use-interaction-scope'
63
62
  import useOpeningGuides from '../../store/use-opening-guides'
64
- import { formatAngleRadians } from '../tools/shared/segment-angle'
65
63
  import {
66
64
  ARROW_COLOR,
67
65
  ARROW_HOVER_COLOR,
@@ -78,6 +76,7 @@ import {
78
76
  import { createLinearResizeDragBinding, linearResizeFactor } from './handles/linear-resize-drag'
79
77
  import { resolveResizeSnapValue } from './handles/resize-snap'
80
78
  import { type HandleDragControls, useHandleDrag } from './handles/use-handle-drag'
79
+ import { Html } from './localized-html'
81
80
 
82
81
  // Pooled scratch for the handle rig's world-relative pose mapping.
83
82
  const _rigRelative = new Matrix4()
@@ -1,8 +1,7 @@
1
1
  'use client'
2
2
 
3
3
  import { type OpenWallEnd, sceneRegistry } from '@pascal-app/core'
4
- import { useViewer } from '@pascal-app/viewer'
5
- import { Html } from '@react-three/drei'
4
+ import { MEASUREMENT_DANGLING_COLOR, useViewer } from '@pascal-app/viewer'
6
5
  import { useFrame } from '@react-three/fiber'
7
6
  import { memo, useEffect, useMemo, useRef } from 'react'
8
7
  import { BoxGeometry, CircleGeometry, type Group, RingGeometry } from 'three'
@@ -14,11 +13,11 @@ import {
14
13
  } from '../../hooks/use-open-wall-ends'
15
14
  import { EDITOR_LAYER } from '../../lib/constants'
16
15
  import { openWallEndKey, openWallEndLabel } from '../../lib/floorplan/open-wall-ends'
17
- import { MEASUREMENT_DANGLING_COLOR } from '../../lib/measurements'
18
16
  import { cn } from '../../lib/utils'
19
17
  import useEditor from '../../store/use-editor'
20
18
  import { isViewVisible, VIEW_3D, visibleScene } from '../../store/view-layout'
21
19
  import { JoinWallsPill } from './join-walls-pill'
20
+ import { Html } from './localized-html'
22
21
  import { OpenWallEndsHint } from './open-wall-ends-hint'
23
22
 
24
23
  /**
@@ -1,10 +1,10 @@
1
1
  'use client'
2
2
 
3
3
  import { type AnyNodeId, emitter, type OpenWallEnd } from '@pascal-app/core'
4
+ import { MEASUREMENT_DANGLING_COLOR } from '@pascal-app/viewer'
4
5
  import { useEffect, useState } from 'react'
5
6
  import { createPortal } from 'react-dom'
6
7
  import { openWallEndsSummary } from '../../lib/floorplan/open-wall-ends'
7
- import { MEASUREMENT_DANGLING_COLOR } from '../../lib/measurements'
8
8
 
9
9
  // The viewer area holds the floor plan and the 3D view side by side, so its
10
10
  // top centre clears both views' corner controls in 2D, 3D and split alike.
@@ -1,7 +1,6 @@
1
1
  'use client'
2
2
 
3
3
  import { useViewer } from '@pascal-app/viewer'
4
- import { Html } from '@react-three/drei'
5
4
  import { memo, useEffect, useLayoutEffect, useMemo } from 'react'
6
5
  import { BufferGeometry, Float32BufferAttribute, Line as ThreeLine } from 'three'
7
6
  import { LineBasicNodeMaterial } from 'three/webgpu'
@@ -10,6 +9,7 @@ import useOpeningGuides, {
10
9
  type OpeningGuide3D,
11
10
  type OpeningGuideVec3,
12
11
  } from '../../store/use-opening-guides'
12
+ import { Html } from './localized-html'
13
13
  import { formatMeasurement } from './measurement-pill'
14
14
 
15
15
  const DIMENSION_COLOR = 0x81_8c_f8 // indigo — a neutral measurement
@@ -1,9 +1,9 @@
1
1
  'use client'
2
2
 
3
3
  import type { QuickMeasurementMetric, QuickMeasurementReport } from '@pascal-app/core'
4
+ import { formatAreaLabel, formatLinearMeasurement, formatVolumeLabel } from '@pascal-app/core'
4
5
  import { Crosshair, MapPin } from 'lucide-react'
5
6
  import { memo } from 'react'
6
- import { formatAreaLabel, formatLinearMeasurement, formatVolumeLabel } from '../../lib/measurements'
7
7
 
8
8
  function formatMetric(
9
9
  metric: QuickMeasurementMetric,
@@ -1,7 +1,13 @@
1
1
  import { afterEach, describe, expect, test } from 'bun:test'
2
- import { type AnyNode, registerNode, useInteractive } from '@pascal-app/core'
2
+ import { type AnyNode, type MechanismPart, registerNode, useInteractive } from '@pascal-app/core'
3
+ import type { MouseEvent, ReactElement } from 'react'
3
4
  import { renderToStaticMarkup } from 'react-dom/server'
4
- import { MechanismButton, selectedMechanismNode } from './registry-action-contributions'
5
+ import {
6
+ MechanismButton,
7
+ MechanismPartButton,
8
+ NodeMechanismActions,
9
+ soleSelectedNode,
10
+ } from './registry-action-contributions'
5
11
 
6
12
  const switchMechanism = {
7
13
  has: (node: AnyNode) => (node as unknown as { joints: number }).joints > 0,
@@ -23,6 +29,30 @@ function articulated(joints: number): AnyNode {
23
29
  return { object: 'node', id: `${kind}_1`, type: kind, joints } as unknown as AnyNode
24
30
  }
25
31
 
32
+ const shutters: MechanismPart = {
33
+ id: 'shutters',
34
+ label: 'Shutters',
35
+ icon: 'shutters',
36
+ verb: 'open',
37
+ has: () => true,
38
+ isOn: (node, state) => Boolean(state.mechanisms[`${node.id}:shutters` as never]),
39
+ set: (node, on) => useInteractive.getState().setMechanism(`${node.id}:shutters` as never, on),
40
+ }
41
+
42
+ function shuttered(): AnyNode {
43
+ const kind = `test:shuttered-${crypto.randomUUID()}`
44
+ registerNode({
45
+ kind,
46
+ schemaVersion: 1,
47
+ category: 'structure',
48
+ defaults: () => ({}),
49
+ capabilities: {
50
+ mechanism: { ...switchMechanism, verb: 'open', icon: 'window', parts: [shutters] },
51
+ },
52
+ } as never)
53
+ return { object: 'node', id: `${kind}_1`, type: kind, joints: 1 } as unknown as AnyNode
54
+ }
55
+
26
56
  const byId = (...nodes: AnyNode[]) => Object.fromEntries(nodes.map((node) => [node.id, node]))
27
57
 
28
58
  afterEach(() => {
@@ -33,17 +63,20 @@ afterEach(() => {
33
63
  describe('mechanism action in the action menu', () => {
34
64
  test('the only selected node gets Play when its kind declares a mechanism', () => {
35
65
  const node = articulated(2)
36
- expect(selectedMechanismNode([node.id], byId(node))).toBe(node)
66
+ expect(soleSelectedNode([node.id], byId(node))).toBe(node)
67
+ expect(renderToStaticMarkup(<NodeMechanismActions node={node} />)).toContain(
68
+ 'aria-label="Play"',
69
+ )
37
70
  })
38
71
 
39
72
  test('no Play without moving parts, for a multi-selection, or without the capability', () => {
40
73
  const still = articulated(0)
41
- expect(selectedMechanismNode([still.id], byId(still))).toBeUndefined()
74
+ expect(renderToStaticMarkup(<NodeMechanismActions node={still} />)).toBe('')
42
75
 
43
76
  const node = articulated(2)
44
77
  const other = { object: 'node', id: 'other_1', type: 'other' } as unknown as AnyNode
45
- expect(selectedMechanismNode([node.id, other.id], byId(node, other))).toBeUndefined()
46
- expect(selectedMechanismNode([other.id], byId(other))).toBeUndefined()
78
+ expect(soleSelectedNode([node.id, other.id], byId(node, other))).toBeUndefined()
79
+ expect(renderToStaticMarkup(<NodeMechanismActions node={other} />)).toBe('')
47
80
  })
48
81
 
49
82
  test('the button reads Play, then Stop while the mechanism runs', () => {
@@ -61,3 +94,31 @@ describe('mechanism action in the action menu', () => {
61
94
  expect(running).toContain('aria-pressed="true"')
62
95
  })
63
96
  })
97
+
98
+ describe('mechanism parts in the action menu', () => {
99
+ test('a window with shutters gets Open shutters beside Open', () => {
100
+ const markup = renderToStaticMarkup(<NodeMechanismActions node={shuttered()} />)
101
+ expect(markup.match(/aria-label="[^"]*"/g)).toEqual([
102
+ 'aria-label="Open"',
103
+ 'aria-label="Open shutters"',
104
+ ])
105
+ })
106
+
107
+ test('the shutters button opens them alone, then reads Close shutters', () => {
108
+ const node = shuttered()
109
+ let button: ReactElement<{ onClick: (event: MouseEvent) => void }> | undefined
110
+ // Rendering the button as a hook call hands over the element it returns, onClick included.
111
+ function Probe() {
112
+ button = MechanismPartButton({ node, part: shutters, running: false }) as typeof button
113
+ return null
114
+ }
115
+ renderToStaticMarkup(<Probe />)
116
+ button?.props.onClick({ stopPropagation: () => {} } as MouseEvent)
117
+ expect(shutters.isOn(node, useInteractive.getState())).toBe(true)
118
+ expect(switchMechanism.isOn(node, useInteractive.getState())).toBe(false)
119
+
120
+ const open = renderToStaticMarkup(<MechanismPartButton node={node} part={shutters} running />)
121
+ expect(open).toContain('aria-label="Close shutters"')
122
+ expect(open).toContain('aria-pressed="true"')
123
+ })
124
+ })
@@ -4,10 +4,14 @@ import {
4
4
  type AnyNode,
5
5
  type AnyNodeId,
6
6
  type MechanismCapability,
7
+ type MechanismPart,
7
8
  nodeMechanism,
9
+ nodeMechanismParts,
8
10
  nodeRegistry,
9
11
  toggleMechanism,
12
+ toggleMechanismPart,
10
13
  useInteractive,
14
+ usePascalI18n,
11
15
  useScene,
12
16
  } from '@pascal-app/core'
13
17
  import { useViewer } from '@pascal-app/viewer'
@@ -16,6 +20,7 @@ import { type ComponentType, lazy, Suspense } from 'react'
16
20
  import { useShallow } from 'zustand/react/shallow'
17
21
  import { getFloorplanNodeExtension } from '../../lib/floorplan/floorplan-extension'
18
22
  import { ActionMenuButton } from './action-menu-button'
23
+ import { InspectorExtensionActions } from './inspector-extension-actions'
19
24
 
20
25
  type Loader = () => Promise<{ default: ComponentType }>
21
26
  const lazyCache = new WeakMap<Loader, ComponentType>()
@@ -30,13 +35,12 @@ function contribution(kind: string): ComponentType | null {
30
35
  return component
31
36
  }
32
37
 
33
- /** The node the action menu's Play/Stop runs: the only selected node, when its kind declares a mechanism. */
34
- export function selectedMechanismNode(
38
+ /** The node the action menu's mechanism buttons run: the only selected node. */
39
+ export function soleSelectedNode(
35
40
  selectedIds: readonly string[],
36
41
  nodes: Readonly<Record<string, AnyNode>>,
37
42
  ): AnyNode | undefined {
38
- const node = selectedIds.length === 1 ? nodes[selectedIds[0]!] : undefined
39
- return nodeMechanism(node) ? node : undefined
43
+ return selectedIds.length === 1 ? nodes[selectedIds[0]!] : undefined
40
44
  }
41
45
 
42
46
  function MechanismGlyph({
@@ -61,6 +65,34 @@ function MechanismGlyph({
61
65
  return <Glyph className="h-4 w-4" />
62
66
  }
63
67
 
68
+ /** Two louvred leaves: shut across the window, or swung out to its sides. */
69
+ function ShuttersGlyph({ open }: { open: boolean }) {
70
+ return (
71
+ <svg
72
+ aria-hidden="true"
73
+ className="h-4 w-4"
74
+ fill="none"
75
+ stroke="currentColor"
76
+ strokeLinecap="round"
77
+ strokeLinejoin="round"
78
+ strokeWidth={2}
79
+ viewBox="0 0 24 24"
80
+ >
81
+ {open ? (
82
+ <>
83
+ <rect height="16" rx="1" width="8" x="8" y="4" />
84
+ <path d="M8 4 3 6v12l5 2M16 4l5 2v12l-5 2" />
85
+ </>
86
+ ) : (
87
+ <>
88
+ <rect height="18" rx="1" width="16" x="4" y="3" />
89
+ <path d="M12 3v18M7 8h2M7 12h2M7 16h2M15 8h2M15 12h2M15 16h2" />
90
+ </>
91
+ )}
92
+ </svg>
93
+ )
94
+ }
95
+
64
96
  export function MechanismButton({
65
97
  node,
66
98
  mechanism,
@@ -70,7 +102,12 @@ export function MechanismButton({
70
102
  mechanism: MechanismCapability
71
103
  running: boolean
72
104
  }) {
73
- const label = mechanism.icon ? (running ? 'Close' : 'Open') : running ? 'Stop' : 'Play'
105
+ const i18n = usePascalI18n()
106
+ const label = i18n._({
107
+ id: 'editor.mechanismAction',
108
+ message: '{action, select, close {Close} open {Open} stop {Stop} other {Play}}',
109
+ values: { action: mechanism.icon ? (running ? 'close' : 'open') : running ? 'stop' : 'play' },
110
+ })
74
111
  return (
75
112
  <ActionMenuButton
76
113
  keys={['E']}
@@ -86,21 +123,83 @@ export function MechanismButton({
86
123
  )
87
124
  }
88
125
 
89
- /** Play/Stop for a single selected node whose kind declares `capabilities.mechanism`. */
90
- function MechanismAction() {
91
- const selected = useViewer((s) => s.selection.selectedIds)
92
- const node = useScene((s) => selectedMechanismNode(selected, s.nodes))
126
+ /** A second control beside the main one (a window's shutters), with its own running state. */
127
+ export function MechanismPartButton({
128
+ node,
129
+ part,
130
+ running,
131
+ }: {
132
+ node: AnyNode
133
+ part: MechanismPart
134
+ running: boolean
135
+ }) {
136
+ const i18n = usePascalI18n()
137
+ const label =
138
+ part.icon === 'shutters'
139
+ ? i18n._({
140
+ id: 'editor.mechanismPartAction.shutters',
141
+ message: '{action, select, close {Close shutters} other {Open shutters}}',
142
+ values: { action: running ? 'close' : 'open' },
143
+ })
144
+ : i18n._({
145
+ id: 'editor.mechanismAction',
146
+ message: '{action, select, close {Close} open {Open} stop {Stop} other {Play}}',
147
+ values: {
148
+ action: part.verb === 'open' ? (running ? 'close' : 'open') : running ? 'stop' : 'play',
149
+ },
150
+ })
151
+ return (
152
+ <ActionMenuButton
153
+ label={label}
154
+ onClick={(event) => {
155
+ event.stopPropagation()
156
+ toggleMechanismPart(part, node)
157
+ }}
158
+ pressed={running}
159
+ >
160
+ {part.icon === 'shutters' ? (
161
+ <ShuttersGlyph open={!running} />
162
+ ) : (
163
+ <MechanismGlyph icon={undefined} running={running} />
164
+ )}
165
+ </ActionMenuButton>
166
+ )
167
+ }
168
+
169
+ function MechanismPartAction({ node, part }: { node: AnyNode; part: MechanismPart }) {
170
+ const running = useInteractive((s) => part.isOn(node, s))
171
+ return <MechanismPartButton node={node} part={part} running={running} />
172
+ }
173
+
174
+ /**
175
+ * Play/Stop for a kind declaring `capabilities.mechanism`, then one button per
176
+ * part the node has. Parts don't need the main mechanism: a fixed window can
177
+ * still have shutters.
178
+ */
179
+ export function NodeMechanismActions({ node }: { node: AnyNode }) {
93
180
  const mechanism = nodeMechanism(node)
94
- const running = useInteractive((s) => (node && mechanism ? mechanism.isOn(node, s) : false))
95
- return node && mechanism ? (
96
- <MechanismButton mechanism={mechanism} node={node} running={running} />
97
- ) : null
181
+ const running = useInteractive((s) => (mechanism ? mechanism.isOn(node, s) : false))
182
+ return (
183
+ <>
184
+ {mechanism ? <MechanismButton mechanism={mechanism} node={node} running={running} /> : null}
185
+ {nodeMechanismParts(node).map((part) => (
186
+ <MechanismPartAction key={part.id} node={node} part={part} />
187
+ ))}
188
+ </>
189
+ )
190
+ }
191
+
192
+ function MechanismActions() {
193
+ const selected = useViewer((s) => s.selection.selectedIds)
194
+ const node = useScene((s) => soleSelectedNode(selected, s.nodes))
195
+ return node ? <NodeMechanismActions node={node} /> : null
98
196
  }
99
197
 
100
198
  /**
101
199
  * The buttons kinds add to the action menu for selections holding them
102
200
  * (`extensions['pascal:editor/floorplan'].actionMenu.actions`), in 2D and 3D
103
- * alike. Each contribution reads the selection and decides its own visibility.
201
+ * alike, then one per plugin inspector extension of the selected node. Each
202
+ * contribution reads the selection and decides its own visibility.
104
203
  */
105
204
  export function RegistryActionContributions() {
106
205
  const selectedIds = useViewer((s) => s.selection.selectedIds)
@@ -118,7 +217,7 @@ export function RegistryActionContributions() {
118
217
  )
119
218
  return (
120
219
  <>
121
- <MechanismAction />
220
+ <MechanismActions />
122
221
  {kinds.map((kind) => {
123
222
  const Contribution = contribution(kind)
124
223
  return Contribution ? (
@@ -127,6 +226,7 @@ export function RegistryActionContributions() {
127
226
  </Suspense>
128
227
  ) : null
129
228
  })}
229
+ <InspectorExtensionActions />
130
230
  </>
131
231
  )
132
232
  }
@@ -1,9 +1,18 @@
1
1
  'use client'
2
2
 
3
- import { type AnyNodeId, buildRiserDiagram, useScene } from '@pascal-app/core'
3
+ import {
4
+ type AnyNode,
5
+ type AnyNodeId,
6
+ buildRiserDiagram,
7
+ type PascalI18n,
8
+ type RiserMarker,
9
+ usePascalI18n,
10
+ useScene,
11
+ } from '@pascal-app/core'
4
12
  import { useViewer } from '@pascal-app/viewer'
5
13
  import { X } from 'lucide-react'
6
14
  import { useMemo } from 'react'
15
+ import { inspectorOptionDescriptor } from '../../i18n/node-messages'
7
16
  import useEditor from '../../store/use-editor'
8
17
 
9
18
  // Set per theme on the panel root; the light values are darker so the 9-unit
@@ -33,6 +42,7 @@ export function RiserDiagramPanel() {
33
42
  }
34
43
 
35
44
  function RiserDiagramContent() {
45
+ const i18n = usePascalI18n()
36
46
  const setRiserOpen = useEditor((s) => s.setRiserOpen)
37
47
  const nodes = useScene((s) => s.nodes)
38
48
  const selectedIds = useViewer((s) => s.selection.selectedIds)
@@ -47,15 +57,30 @@ function RiserDiagramContent() {
47
57
  const ty = diagram ? -diagram.bounds.minY * SCALE + PADDING : 0
48
58
 
49
59
  return (
50
- <div className="pointer-events-auto absolute top-4 right-4 z-30 flex max-h-[80vh] w-[26rem] flex-col overflow-hidden rounded-2xl border border-border/40 bg-background/95 text-foreground shadow-lg backdrop-blur-xl [--riser-vent:#9333ea] [--riser-waste:#0369a1] dark:[--riser-vent:#a855f7] dark:[--riser-waste:#0ea5e9]">
60
+ <div
61
+ data-surface="riser-diagram"
62
+ className="pointer-events-auto absolute top-4 right-4 z-30 flex max-h-[80vh] w-[26rem] flex-col overflow-hidden rounded-2xl border border-border/40 bg-background/95 text-foreground shadow-lg backdrop-blur-xl [--riser-vent:#9333ea] [--riser-waste:#0369a1] dark:[--riser-vent:#a855f7] dark:[--riser-waste:#0ea5e9]"
63
+ >
51
64
  <div className="flex items-center justify-between border-border/40 border-b px-4 py-2.5">
52
65
  <div className="flex flex-col">
53
- <span className="font-medium text-sm">Riser Diagram</span>
54
- <span className="text-muted-foreground text-xs">DWV plumbing isometric</span>
66
+ <span className="font-medium text-sm">
67
+ {i18n._({ id: 'editor.riserDiagramPanel.riserDiagram', message: 'Riser Diagram' })}
68
+ </span>
69
+ <span className="text-muted-foreground text-xs">
70
+ {i18n._({
71
+ id: 'editor.riserDiagramPanel.dwvPlumbingIsometric',
72
+ message: 'DWV plumbing isometric',
73
+ })}
74
+ </span>
55
75
  </div>
56
76
  <button
77
+ aria-label={i18n._({
78
+ id: 'editor.riserDiagramPanel.close',
79
+ message: 'Close riser diagram',
80
+ })}
57
81
  className="flex h-7 w-7 items-center justify-center rounded-md transition-colors hover:bg-black/6 dark:hover:bg-white/10"
58
82
  onClick={() => setRiserOpen(false)}
83
+ title={i18n._({ id: 'editor.riserDiagramPanel.close', message: 'Close riser diagram' })}
59
84
  >
60
85
  <X className="h-4 w-4 text-muted-foreground" />
61
86
  </button>
@@ -63,11 +88,12 @@ function RiserDiagramContent() {
63
88
 
64
89
  <div className="flex items-center gap-3 border-border/40 border-b px-4 py-2 text-xs">
65
90
  <span className="flex items-center gap-1.5">
66
- <span className="h-0.5 w-4" style={{ background: WASTE_COLOR }} /> Waste
91
+ <span className="h-0.5 w-4" style={{ background: WASTE_COLOR }} />{' '}
92
+ {i18n._({ id: 'editor.riserDiagramPanel.waste', message: 'Waste' })}
67
93
  </span>
68
94
  <span className="flex items-center gap-1.5">
69
95
  <span className="h-0 w-4 border-t-2 border-dashed" style={{ borderColor: VENT_COLOR }} />{' '}
70
- Vent
96
+ {i18n._({ id: 'editor.riserDiagramPanel.vent', message: 'Vent' })}
71
97
  </span>
72
98
  </div>
73
99
 
@@ -76,7 +102,10 @@ function RiserDiagramContent() {
76
102
  <svg
77
103
  height={Math.max(height, 120)}
78
104
  role="img"
79
- aria-label="DWV riser diagram"
105
+ aria-label={i18n._({
106
+ id: 'editor.riserDiagramPanel.dwvRiserDiagram',
107
+ message: 'DWV riser diagram',
108
+ })}
80
109
  viewBox={`0 0 ${Math.max(width, 200)} ${Math.max(height, 120)}`}
81
110
  width="100%"
82
111
  >
@@ -127,7 +156,7 @@ function RiserDiagramContent() {
127
156
  <circle fill={MARKER_COLOR} r={3} stroke={MARKER_COLOR} strokeWidth={1.5} />
128
157
  )}
129
158
  <text fill={MARKER_COLOR} fontSize={9} x={8} y={3}>
130
- {marker.label}
159
+ {markerLabel(marker, nodes, i18n)}
131
160
  </text>
132
161
  </g>
133
162
  ))}
@@ -135,10 +164,31 @@ function RiserDiagramContent() {
135
164
  </svg>
136
165
  ) : (
137
166
  <div className="flex h-32 items-center justify-center px-6 text-center text-muted-foreground text-sm">
138
- No drain, waste, or vent pipes yet. Draw plumbing to see the riser diagram.
167
+ {i18n._({
168
+ id: 'editor.riserDiagramPanel.noDrainWasteOrVentPipes',
169
+ message:
170
+ 'No drain, waste, or vent pipes yet. Draw plumbing to see the riser diagram.',
171
+ })}
139
172
  </div>
140
173
  )}
141
174
  </div>
142
175
  </div>
143
176
  )
144
177
  }
178
+
179
+ /** Core's marker labels are English ("2\" P-trap"); the panel words them in the UI language. */
180
+ function markerLabel(marker: RiserMarker, nodes: Record<string, AnyNode>, i18n: PascalI18n) {
181
+ const node = nodes[marker.nodeId]
182
+ if (marker.kind === 'fitting' && node?.type === 'pipe-fitting')
183
+ return i18n._(inspectorOptionDescriptor(node.fittingType))
184
+ if (!(node && 'diameter' in node && typeof node.diameter === 'number')) return marker.label
185
+ const values = { diameter: String(node.diameter) }
186
+ return marker.kind === 'trap'
187
+ ? i18n._({ id: 'editor.riserDiagramPanel.trap', message: '{diameter}" P-trap', values })
188
+ : i18n._({
189
+ id: 'editor.riserDiagramPanel.ventTermination',
190
+ message: '{diameter}" VTR',
191
+ comment: 'Vent through roof: where a plumbing vent stack exits the roof.',
192
+ values,
193
+ })
194
+ }
@@ -6,13 +6,14 @@ import {
6
6
  DEFAULT_LEVEL_HEIGHT,
7
7
  deleteZone,
8
8
  emitter,
9
+ formatLinearMeasurement,
9
10
  type Point,
11
+ roomFloorElevation,
10
12
  type StructureNodes,
11
13
  sceneRegistry,
12
14
  useScene,
13
15
  } from '@pascal-app/core'
14
16
  import { getSceneTheme, setSurfaceRaycastLayers, useViewer } from '@pascal-app/viewer'
15
- import { Html } from '@react-three/drei'
16
17
  import { useFrame, useThree } from '@react-three/fiber'
17
18
  import { SquareSplitHorizontal } from 'lucide-react'
18
19
  import Image from 'next/image'
@@ -36,7 +37,6 @@ import { getRoomSelectionIndex } from '../../hooks/use-selected-room'
36
37
  import { EDITOR_LAYER } from '../../lib/constants'
37
38
  import { getFloatingMenuScale } from '../../lib/floating-menu-scale'
38
39
  import { clientToPlan } from '../../lib/floorplan/plan-coords'
39
- import { formatLinearMeasurement } from '../../lib/measurements'
40
40
  import { addMezzanineStairs } from '../../lib/mezzanine-stairs'
41
41
  import {
42
42
  cancelRoomDivide,
@@ -47,7 +47,7 @@ import {
47
47
  roomDivideContains,
48
48
  startRoomDivide,
49
49
  } from '../../lib/room-divide-session'
50
- import { roomFloorElevation, useRoomHandleDrag } from '../../lib/room-handle-drag'
50
+ import { useRoomHandleDrag } from '../../lib/room-handle-drag'
51
51
  import type { RoomSelectionRecord } from '../../lib/room-selection'
52
52
  import { requestRoomDeletion } from '../../lib/room-structure-commands'
53
53
  import {
@@ -69,6 +69,7 @@ import {
69
69
  } from '../tools/shared/draft-measurement-label'
70
70
  import { ActionMenuButton } from './action-menu-button'
71
71
  import { getMenuYOffset } from './floating-action-menu'
72
+ import { Html } from './localized-html'
72
73
  import { NodeActionMenu } from './node-action-menu'
73
74
  import { RoomFloorHighlight3D } from './room-floor-highlight'
74
75
  import { RoomHandleDragPreview3D, RoomHandles3D } from './room-handles'