@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
@@ -19,6 +19,8 @@ import { resolveGenericFloorplanGridEventPoint } from '../../lib/floorplan-grid-
19
19
  import { snapRegisteredDraftPoint } from '../../lib/registered-draft-snap'
20
20
  import { resolveSlabPlanPointSnap } from '../../lib/slab-plan-snap'
21
21
  import { commitWallPolygonDraft, wallPolygonDraftWalls } from '../../lib/wall-polygon-draft'
22
+ import { wallDraftIsRun } from '../../lib/wall-pull'
23
+ import { isWallRunFree, snapWallRunDraftPoint } from '../../lib/wall-run-snap'
22
24
  import useAlignmentGuides from '../../store/use-alignment-guides'
23
25
  import useEditor, { isAngleSnapActive, isMagneticSnapActive } from '../../store/use-editor'
24
26
  import usePlacementPreview from '../../store/use-placement-preview'
@@ -52,6 +54,8 @@ type UseFloorplanBackgroundPlacementArgs = {
52
54
  wall: WallNode
53
55
  } | null
54
56
  floorplanOpeningLocalY: number
57
+ /** An open wall run's start and the direction of its last wall (null before one). */
58
+ getWallRunContext: () => { runStart: WallPlanPoint | null; reference: WallPlanPoint | null }
55
59
  getSnappedFloorplanPoint: (point: WallPlanPoint) => WallPlanPoint
56
60
  handleCeilingItemPlacementClick: (
57
61
  planPoint: WallPlanPoint,
@@ -72,12 +76,9 @@ type UseFloorplanBackgroundPlacementArgs = {
72
76
  isZoneBuildActive: boolean
73
77
  levelId: string | null
74
78
  registryToolOwnsSnapping: boolean
75
- roofDraftStart: WallPlanPoint | null
76
79
  setCursorPoint: React.Dispatch<React.SetStateAction<WallPlanPoint | null>>
77
80
  setFenceDraftEnd: React.Dispatch<React.SetStateAction<WallPlanPoint | null>>
78
81
  setFenceDraftStart: React.Dispatch<React.SetStateAction<WallPlanPoint | null>>
79
- setRoofDraftEnd: React.Dispatch<React.SetStateAction<WallPlanPoint | null>>
80
- setRoofDraftStart: React.Dispatch<React.SetStateAction<WallPlanPoint | null>>
81
82
  snapWallDraftPoint: (args: {
82
83
  point: WallPlanPoint
83
84
  walls: WallNode[]
@@ -86,6 +87,7 @@ type UseFloorplanBackgroundPlacementArgs = {
86
87
  bypassSnap?: boolean
87
88
  step?: number
88
89
  gridSnap?: (point: WallPlanPoint) => WallPlanPoint
90
+ captureWallBodies?: boolean
89
91
  }) => WallPlanPoint
90
92
  snapPolygonDraftPoint: (args: {
91
93
  point: WallPlanPoint
@@ -95,10 +97,9 @@ type UseFloorplanBackgroundPlacementArgs = {
95
97
  toPoint2D: (point: WallPlanPoint) => { x: number; y: number }
96
98
  walls: WallNode[]
97
99
  /**
98
- * Snap a building-local plan point to the world XZ grid at `step`.
99
- * Injected so the hook doesn't have to know the building's rotation
100
- * or position — used by wall / fence branches that snap at variable
101
- * step.
100
+ * Snap a building-local plan point onto the building's grid lattice at
101
+ * `step` (the lattice the plan draws, which turns with the building).
102
+ * Injected — used by wall / fence branches that snap at variable step.
102
103
  */
103
104
  worldGridSnap: (point: WallPlanPoint, step: number) => WallPlanPoint
104
105
  }
@@ -115,6 +116,7 @@ export function useFloorplanBackgroundPlacement({
115
116
  findClosestWallPoint,
116
117
  floorplanOpeningLocalY,
117
118
  getSnappedFloorplanPoint,
119
+ getWallRunContext,
118
120
  handleCeilingItemPlacementClick,
119
121
  handleCeilingPlacementPoint,
120
122
  handleSlabPlacementPoint,
@@ -131,12 +133,9 @@ export function useFloorplanBackgroundPlacement({
131
133
  isZoneBuildActive,
132
134
  levelId,
133
135
  registryToolOwnsSnapping,
134
- roofDraftStart,
135
136
  setCursorPoint,
136
137
  setFenceDraftEnd,
137
138
  setFenceDraftStart,
138
- setRoofDraftEnd,
139
- setRoofDraftStart,
140
139
  snapWallDraftPoint,
141
140
  snapPolygonDraftPoint,
142
141
  toPoint2D,
@@ -219,24 +218,14 @@ export function useFloorplanBackgroundPlacement({
219
218
  if (isRoofBuildActive) {
220
219
  // Footprint placement (polygon context: grid / lines / off, no angle),
221
220
  // mode-driven to match the chip. Alt is force/free at commit time;
222
- // alignment display/pull follows the active magnetic mode.
221
+ // alignment display/pull follows the active magnetic mode. The roof
222
+ // tool snaps the corner it commits and writes the draft rectangle from
223
+ // it, so the plan shows exactly the roof the click makes.
223
224
  const snappedPoint = alignFloorplanDraftPoint(getSnappedFloorplanPoint(planPoint), {
224
225
  applySnap: isMagneticSnapActive(),
225
226
  })
226
227
  emitFloorplanGridEvent('click', snappedPoint, event)
227
228
  setCursorPoint(snappedPoint)
228
-
229
- if (roofFootprintSource !== 'draw') {
230
- clearRoofPlacementDraft()
231
- return true
232
- }
233
-
234
- if (roofDraftStart) {
235
- clearRoofPlacementDraft()
236
- } else {
237
- setRoofDraftStart(snappedPoint)
238
- setRoofDraftEnd(snappedPoint)
239
- }
240
229
  return true
241
230
  }
242
231
 
@@ -254,8 +243,8 @@ export function useFloorplanBackgroundPlacement({
254
243
 
255
244
  if (isFenceBuildActive) {
256
245
  // Fence draft: mode-driven (matches the chip), same as the move
257
- // preview. `grid` snaps to the world XZ grid (rotation-safe via the
258
- // `gridSnap` callback), `angles` locks 15° rays from the start, `lines`
246
+ // preview. `grid` snaps to the building's grid lattice (the `gridSnap`
247
+ // callback), `angles` locks 15° rays from the start, `lines`
259
248
  // pulls onto walls / fences / alignment, `off` is free.
260
249
  const fenceStep = getSegmentGridStep()
261
250
  const fenceAngleSnap = fenceDraftStart !== null && isAngleSnapActive()
@@ -363,30 +352,48 @@ export function useFloorplanBackgroundPlacement({
363
352
  // / draftEnd state in the floor plan would never update, leaving
364
353
  // the dashed-line draft preview invisible.
365
354
  if (isWallBuildActive) {
366
- // Wall draft: mode-driven (matches the chip + the move-preview branch).
367
- // `grid` snaps to the world XZ grid (rotation-safe via `gridSnap`),
368
- // `angles` locks 15° rays from the start, `lines` pulls the endpoint
369
- // onto existing wall corners / edges + alignment, `off` is free.
370
- const wallStep = getSegmentGridStep()
371
- const wallAngleSnap = draftStart !== null && isAngleSnapActive()
372
- const wallSnapped = snapWallDraftPoint({
373
- point: planPoint,
374
- walls: [...walls, ...wallPolygonDraftWalls()],
375
- start: draftStart ?? undefined,
376
- angleSnap: wallAngleSnap,
377
- gridSnap: (p) => worldGridSnap(p, wallStep),
378
- })
379
- const wallGridBase = worldGridSnap(planPoint, wallStep)
380
- const wallLocked = wallSnapped[0] !== wallGridBase[0] || wallSnapped[1] !== wallGridBase[1]
381
- let snappedPoint = wallSnapped
382
- if (wallLocked) {
383
- useAlignmentGuides.getState().clear()
355
+ // Wall draft, matching the move-preview branch: mode-driven (matches
356
+ // the chip). `grid` snaps to the building's grid lattice (`gridSnap`),
357
+ // `angles` locks a Walls segment to 15° rays from its start, `lines`
358
+ // pulls the point onto existing wall corners / edges + alignment, `off`
359
+ // is free, and in every mode but `lines` an end aimed into a wall's body
360
+ // joins that wall. A run's segment (a Polygon side, a pulled run) lands
361
+ // where the move showed it (`snapWallRunDraftPoint`).
362
+ let snappedPoint: WallPlanPoint
363
+ if (draftStart && wallDraftIsRun()) {
364
+ const run = getWallRunContext()
365
+ snappedPoint = snapWallRunDraftPoint({
366
+ point: planPoint,
367
+ start: draftStart,
368
+ runStart: run.runStart ?? draftStart,
369
+ reference: run.reference,
370
+ walls: [...walls, ...wallPolygonDraftWalls()],
371
+ free: isWallRunFree(event),
372
+ }).point
384
373
  } else {
385
- // Alignment lines are shown in every mode; the pull applies only when
386
- // magnetic ('lines') and the segment isn't angle-locked.
387
- snappedPoint = alignFloorplanDraftPoint(wallSnapped, {
388
- applySnap: isMagneticSnapActive() && !wallAngleSnap,
374
+ const wallStep = getSegmentGridStep()
375
+ const wallAngleSnap = draftStart !== null && isAngleSnapActive()
376
+ const wallSnapped = snapWallDraftPoint({
377
+ point: planPoint,
378
+ walls: [...walls, ...wallPolygonDraftWalls()],
379
+ start: draftStart ?? undefined,
380
+ angleSnap: wallAngleSnap,
381
+ gridSnap: (p) => worldGridSnap(p, wallStep),
382
+ captureWallBodies: true,
389
383
  })
384
+ const wallGridBase = worldGridSnap(planPoint, wallStep)
385
+ const wallLocked =
386
+ wallSnapped[0] !== wallGridBase[0] || wallSnapped[1] !== wallGridBase[1]
387
+ snappedPoint = wallSnapped
388
+ if (wallLocked) {
389
+ useAlignmentGuides.getState().clear()
390
+ } else {
391
+ // Alignment lines are shown in every mode; the pull applies only when
392
+ // magnetic ('lines') and the segment isn't angle-locked.
393
+ snappedPoint = alignFloorplanDraftPoint(wallSnapped, {
394
+ applySnap: isMagneticSnapActive() && !wallAngleSnap,
395
+ })
396
+ }
390
397
  }
391
398
 
392
399
  emitFloorplanGridEvent('click', snappedPoint, event)
@@ -437,12 +444,12 @@ export function useFloorplanBackgroundPlacement({
437
444
  activePolygonDraftPoints,
438
445
  ceilingDraftPoints,
439
446
  clearFencePlacementDraft,
440
- clearRoofPlacementDraft,
441
447
  clearWallPlacementDraft,
442
448
  emitFloorplanGridEvent,
443
449
  fenceDraftStart,
444
450
  fences,
445
451
  findClosestWallPoint,
452
+ getWallRunContext,
446
453
  floorplanOpeningLocalY,
447
454
  getSnappedFloorplanPoint,
448
455
  handleCeilingItemPlacementClick,
@@ -459,14 +466,10 @@ export function useFloorplanBackgroundPlacement({
459
466
  isWallBuildActive,
460
467
  isZoneBuildActive,
461
468
  levelId,
462
- roofDraftStart,
463
469
  registryToolOwnsSnapping,
464
- roofFootprintSource,
465
470
  setCursorPoint,
466
471
  setFenceDraftEnd,
467
472
  setFenceDraftStart,
468
- setRoofDraftEnd,
469
- setRoofDraftStart,
470
473
  snapWallDraftPoint,
471
474
  snapPolygonDraftPoint,
472
475
  toPoint2D,
@@ -1,5 +1,6 @@
1
1
  'use client'
2
2
 
3
+ import { type MessageDescriptor, type PascalI18n, usePascalI18n } from '@pascal-app/core'
3
4
  import { type GroundMode, useViewer } from '@pascal-app/viewer'
4
5
  import { ArrowLeftRight, Check, Columns2, MoreHorizontal, Pin, PinOff, X } from 'lucide-react'
5
6
  import { type ReactNode, useEffect, useRef } from 'react'
@@ -7,7 +8,7 @@ import { useIsMobile } from '../../../hooks/use-mobile'
7
8
  import { type EditorViewDescriptor, isSceneView, isViewPinned } from '../../../lib/editor-views'
8
9
  import { cn } from '../../../lib/utils'
9
10
  import useEditor from '../../../store/use-editor'
10
- import type { ViewPaneIndex } from '../../../store/view-layout'
11
+ import { VIEW_2D, VIEW_3D, type ViewPaneIndex } from '../../../store/view-layout'
11
12
  import { IconRefGlyph } from '../../ui/icon-ref'
12
13
  import {
13
14
  DropdownMenu,
@@ -30,7 +31,16 @@ const IDLE =
30
31
  'text-muted-foreground/70 hover:bg-black/5 dark:hover:bg-white/8 hover:text-muted-foreground'
31
32
 
32
33
  /** The views of one pane: its pinned views, the one it shows, and a menu with the rest. */
34
+ /** Built-in views are named by the editor; host and plugin views bring their own label. */
35
+ function viewLabel(view: EditorViewDescriptor, i18n: PascalI18n): string {
36
+ if (view.id === VIEW_3D) return i18n._({ id: 'editor.views.view3d', message: '3D' })
37
+ if (view.id === VIEW_2D) return i18n._({ id: 'editor.views.view2d', message: '2D' })
38
+ if (view.id === PLUGINS_VIEW_ID) return i18n._({ id: 'editor.views.plugins', message: 'Plugins' })
39
+ return view.label
40
+ }
41
+
33
42
  export function ViewTabs({ pane, className }: { pane: ViewPaneIndex; className?: string }) {
43
+ const i18n = usePascalI18n()
34
44
  const layout = useActiveViewLayout()
35
45
  const views = useEditorViews()
36
46
  const pinnedViews = useEditor((s) => s.pinnedViews)
@@ -58,7 +68,11 @@ export function ViewTabs({ pane, className }: { pane: ViewPaneIndex; className?:
58
68
  return (
59
69
  <div className={cn(GROUP, 'min-w-0 shrink', className)}>
60
70
  <div
61
- aria-label={`Pane ${pane + 1} views`}
71
+ aria-label={i18n._({
72
+ id: 'editor.views.viewBar.paneViews',
73
+ message: 'Pane {pane} views',
74
+ values: { pane: pane + 1 },
75
+ })}
62
76
  className="flex min-w-0 overflow-x-auto [scrollbar-width:none]"
63
77
  role="tablist"
64
78
  ref={tabList}
@@ -102,16 +116,20 @@ export function ViewTabs({ pane, className }: { pane: ViewPaneIndex; className?:
102
116
  }
103
117
  }}
104
118
  tabIndex={view.id === current ? 0 : -1}
105
- title={view.label}
119
+ title={viewLabel(view, i18n)}
106
120
  role="tab"
107
121
  type="button"
108
122
  >
109
123
  <IconRefGlyph icon={view.icon} size={14} />
110
- <span className="max-w-36 truncate">{view.label}</span>
124
+ <span className="max-w-36 truncate">{viewLabel(view, i18n)}</span>
111
125
  </button>
112
126
  {view.onClose && (
113
127
  <button
114
- aria-label={`Close ${view.label}`}
128
+ aria-label={i18n._({
129
+ id: 'editor.views.viewBar.closeLabel',
130
+ message: 'Close {label}',
131
+ values: { label: viewLabel(view, i18n) },
132
+ })}
115
133
  className="mr-1 flex size-6 shrink-0 items-center justify-center rounded-full text-muted-foreground hover:bg-accent hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring"
116
134
  onClick={view.onClose}
117
135
  type="button"
@@ -128,9 +146,27 @@ export function ViewTabs({ pane, className }: { pane: ViewPaneIndex; className?:
128
146
  }
129
147
 
130
148
  const GROUND_MODES = [
131
- { mode: 'auto', label: 'Auto', title: 'Veil the ground over a level below grade' },
132
- { mode: 'solid', label: 'Solid', title: 'Keep the ground solid' },
133
- ] as const satisfies ReadonlyArray<{ mode: GroundMode; label: string; title: string }>
149
+ {
150
+ mode: 'auto',
151
+ label: /* i18n */ { id: 'editor.views.viewBar.auto', message: 'Auto' },
152
+ title: /* i18n */ {
153
+ id: 'editor.views.viewBar.veilTheGroundOverALevel',
154
+ message: 'Veil the ground over a level below grade',
155
+ },
156
+ },
157
+ {
158
+ mode: 'solid',
159
+ label: /* i18n */ { id: 'editor.views.viewBar.solid', message: 'Solid' },
160
+ title: /* i18n */ {
161
+ id: 'editor.views.viewBar.keepTheGroundSolid',
162
+ message: 'Keep the ground solid',
163
+ },
164
+ },
165
+ ] as const satisfies ReadonlyArray<{
166
+ mode: GroundMode
167
+ label: MessageDescriptor
168
+ title: MessageDescriptor
169
+ }>
134
170
 
135
171
  /**
136
172
  * Over a level below grade the ground is veiled so its rooms show, and the bar says so: the
@@ -139,6 +175,7 @@ const GROUND_MODES = [
139
175
  * a level at grade or only partly below it.
140
176
  */
141
177
  export function GroundModeToggle() {
178
+ const i18n = usePascalI18n()
142
179
  const belowGrade = useViewer((s) => s.belowGrade)
143
180
  const gradeStanding = useViewer((s) => s.gradeStanding)
144
181
  const groundWindowVeiled = useViewer((s) => s.groundWindowVeiled)
@@ -147,15 +184,31 @@ export function GroundModeToggle() {
147
184
  const veiled =
148
185
  groundMode === 'auto' &&
149
186
  ((belowGrade && gradeStanding !== 'partly-below-grade') || groundWindowVeiled)
150
- const ground = veiled ? 'veiled' : 'solid'
151
- const label =
187
+ const standing =
152
188
  gradeStanding === 'partly-below-grade'
153
- ? `Partly below grade · ground ${ground}`
189
+ ? 'partly'
154
190
  : gradeStanding === 'below-grade'
155
- ? `Below grade · ground ${ground}`
156
- : `Ground ${ground}`
191
+ ? 'below'
192
+ : 'other'
193
+ const label = veiled
194
+ ? i18n._({
195
+ id: 'editor.views.viewBar.groundVeiled',
196
+ message:
197
+ '{standing, select, partly {Partly below grade · ground veiled} below {Below grade · ground veiled} other {Ground veiled}}',
198
+ values: { standing },
199
+ })
200
+ : i18n._({
201
+ id: 'editor.views.viewBar.groundSolid',
202
+ message:
203
+ '{standing, select, partly {Partly below grade · ground solid} below {Below grade · ground solid} other {Ground solid}}',
204
+ values: { standing },
205
+ })
157
206
  return (
158
- <div aria-label="Ground" className={GROUP} role="group">
207
+ <div
208
+ aria-label={i18n._({ id: 'editor.views.viewBar.ground', message: 'Ground' })}
209
+ className={GROUP}
210
+ role="group"
211
+ >
159
212
  <span className="flex items-center px-2.5 text-muted-foreground text-xs">{label}</span>
160
213
  {GROUND_MODES.map(({ mode, label, title }) => (
161
214
  <button
@@ -163,10 +216,10 @@ export function GroundModeToggle() {
163
216
  className={cn(SEGMENT, groundMode === mode ? ACTIVE : IDLE)}
164
217
  key={mode}
165
218
  onClick={() => useViewer.getState().setGroundMode(mode)}
166
- title={title}
219
+ title={i18n._(title)}
167
220
  type="button"
168
221
  >
169
- {label}
222
+ {i18n._(label)}
170
223
  </button>
171
224
  ))}
172
225
  </div>
@@ -175,15 +228,16 @@ export function GroundModeToggle() {
175
228
 
176
229
  /** Split toggle and, split, the swap — at the right end of the last pane's bar. */
177
230
  function SplitControls() {
231
+ const i18n = usePascalI18n()
178
232
  const split = useActiveViewLayout().split
179
233
  return (
180
234
  <div className={GROUP}>
181
235
  {split && (
182
236
  <button
183
- aria-label="Swap panes"
237
+ aria-label={i18n._({ id: 'editor.views.viewBar.swapPanes', message: 'Swap panes' })}
184
238
  className={cn(SEGMENT, IDLE, 'px-2')}
185
239
  onClick={() => useEditor.getState().swapPanes()}
186
- title="Swap panes"
240
+ title={i18n._({ id: 'editor.views.viewBar.swapPanes', message: 'Swap panes' })}
187
241
  type="button"
188
242
  >
189
243
  <ArrowLeftRight className="h-3 w-3" />
@@ -193,11 +247,15 @@ function SplitControls() {
193
247
  aria-pressed={split}
194
248
  className={cn(SEGMENT, split ? ACTIVE : IDLE)}
195
249
  onClick={() => useEditor.getState().toggleSplit()}
196
- title={split ? 'Close split (\\)' : 'Split (\\)'}
250
+ title={
251
+ split
252
+ ? i18n._({ id: 'editor.views.viewBar.closeSplit', message: 'Close split (\\)' })
253
+ : i18n._({ id: 'editor.views.viewBar.split', message: 'Split (\\)' })
254
+ }
197
255
  type="button"
198
256
  >
199
257
  <Columns2 className="h-3 w-3" />
200
- <span>Split</span>
258
+ <span>{i18n._({ id: 'editor.views.viewBar.split2', message: 'Split' })}</span>
201
259
  </button>
202
260
  </div>
203
261
  )
@@ -272,6 +330,7 @@ function MoreViewsMenu({
272
330
  current: string
273
331
  views: EditorViewDescriptor[]
274
332
  }) {
333
+ const i18n = usePascalI18n()
275
334
  const pinnedViews = useEditor((s) => s.pinnedViews)
276
335
  const setPaneView = useEditor((s) => s.setPaneView)
277
336
  const setViewPinned = useEditor((s) => s.setViewPinned)
@@ -282,7 +341,7 @@ function MoreViewsMenu({
282
341
  <DropdownMenu>
283
342
  <DropdownMenuTrigger asChild>
284
343
  <button
285
- aria-label="More views"
344
+ aria-label={i18n._({ id: 'editor.views.viewBar.moreViews', message: 'More views' })}
286
345
  className={cn(SEGMENT, IDLE, 'shrink-0 px-2')}
287
346
  type="button"
288
347
  >
@@ -290,7 +349,9 @@ function MoreViewsMenu({
290
349
  </button>
291
350
  </DropdownMenuTrigger>
292
351
  <DropdownMenuContent align="start" className="w-64">
293
- <DropdownMenuLabel className="text-muted-foreground text-xs">Views</DropdownMenuLabel>
352
+ <DropdownMenuLabel className="text-muted-foreground text-xs">
353
+ {i18n._({ id: 'editor.views.viewBar.views', message: 'Views' })}
354
+ </DropdownMenuLabel>
294
355
  {listed.map((view) => {
295
356
  const pinned = isViewPinned(view, pinnedViews)
296
357
  return (
@@ -300,11 +361,23 @@ function MoreViewsMenu({
300
361
  onSelect={() => setPaneView(pane, view.id)}
301
362
  >
302
363
  <IconRefGlyph icon={view.icon} size={16} />
303
- <span className="min-w-0 flex-1 truncate">{view.label}</span>
364
+ <span className="min-w-0 flex-1 truncate">{viewLabel(view, i18n)}</span>
304
365
  {view.id === current && <Check className="h-3.5 w-3.5 text-muted-foreground" />}
305
366
  {!isSceneView(view.id) && !view.onClose && !view.alwaysPinned && (
306
367
  <button
307
- aria-label={pinned ? `Unpin ${view.label}` : `Pin ${view.label}`}
368
+ aria-label={
369
+ pinned
370
+ ? i18n._({
371
+ id: 'editor.views.viewBar.unpinLabel',
372
+ message: 'Unpin {label}',
373
+ values: { label: viewLabel(view, i18n) },
374
+ })
375
+ : i18n._({
376
+ id: 'editor.views.viewBar.pinLabel',
377
+ message: 'Pin {label}',
378
+ values: { label: viewLabel(view, i18n) },
379
+ })
380
+ }
308
381
  aria-pressed={pinned}
309
382
  className={cn(
310
383
  'flex h-6 w-6 items-center justify-center rounded-md transition-colors hover:bg-black/6 dark:hover:bg-white/10',
@@ -316,7 +389,17 @@ function MoreViewsMenu({
316
389
  setViewPinned(view.id, !pinned)
317
390
  }}
318
391
  onPointerDown={(event) => event.stopPropagation()}
319
- title={pinned ? 'Unpin from the view bar' : 'Pin to the view bar'}
392
+ title={
393
+ pinned
394
+ ? i18n._({
395
+ id: 'editor.views.viewBar.unpinFromTheViewBar',
396
+ message: 'Unpin from the view bar',
397
+ })
398
+ : i18n._({
399
+ id: 'editor.views.viewBar.pinToTheViewBar',
400
+ message: 'Pin to the view bar',
401
+ })
402
+ }
320
403
  type="button"
321
404
  >
322
405
  {pinned ? <PinOff className="h-3.5 w-3.5" /> : <Pin className="h-3.5 w-3.5" />}
@@ -333,7 +416,9 @@ function MoreViewsMenu({
333
416
  onSelect={() => setPaneView(pane, PLUGINS_VIEW_ID)}
334
417
  >
335
418
  <img alt="" className="h-4 w-4 object-contain" src="/icons/plugins.webp" />
336
- <span>Browse plugins</span>
419
+ <span>
420
+ {i18n._({ id: 'editor.views.viewBar.browsePlugins', message: 'Browse plugins' })}
421
+ </span>
337
422
  </DropdownMenuItem>
338
423
  </>
339
424
  )}
@@ -4,6 +4,7 @@ import {
4
4
  type AnyNode,
5
5
  type AnyNodeId,
6
6
  calculateLevelMiters,
7
+ formatLinearMeasurement,
7
8
  getWallCurveLength,
8
9
  getWallEffectiveHeightForNodes,
9
10
  getWallMiterBoundaryPoints,
@@ -20,11 +21,10 @@ import {
20
21
  type WallNode,
21
22
  } from '@pascal-app/core'
22
23
  import { getSceneTheme, useViewer } from '@pascal-app/viewer'
23
- import { Html } from '@react-three/drei'
24
24
  import { createPortal, useFrame } from '@react-three/fiber'
25
25
  import { useMemo, useState } from 'react'
26
26
  import * as THREE from 'three'
27
- import { formatLinearMeasurement } from '../../lib/measurements'
27
+ import { Html } from './localized-html'
28
28
 
29
29
  const GUIDE_Y_OFFSET = 0.08
30
30
  const LABEL_LIFT = 0.08
@@ -315,16 +315,25 @@ function buildMeasurementGuide(
315
315
  const measurementPoints = measurementLine ?? fallbackMiddlePoints
316
316
  if (!measurementPoints) return null
317
317
 
318
- const height = getWallEffectiveHeightForNodes(wall, nodes)
318
+ const heightStart = getWallEffectiveHeightForNodes(wall, nodes, 0)
319
+ const heightEnd = getWallEffectiveHeightForNodes(wall, nodes, 1)
319
320
  const startLocal = worldPointToWallLocal(wall, measurementPoints.start)
320
321
  const endLocal = worldPointToWallLocal(wall, measurementPoints.end)
321
322
  const curvedMeasurementPath = isCurvedWall(wall)
322
323
  ? getCurvedWallMeasurementPath(wall, miterData, levelWalls)
323
324
  : null
325
+ const dx = wall.end[0] - wall.start[0]
326
+ const dz = wall.end[1] - wall.start[1]
327
+ const wallChordLength = Math.hypot(dx, dz)
328
+ const getChordT = (localX: number) =>
329
+ wallChordLength > 1e-6 ? Math.max(0, Math.min(1, localX / wallChordLength)) : 0
330
+
324
331
  const guidePath: Vec3[] = curvedMeasurementPath
325
332
  ? curvedMeasurementPath.map((point) => {
326
333
  const localPoint = worldPointToWallLocal(wall, point)
327
- return [localPoint[0], height + GUIDE_Y_OFFSET, localPoint[2]]
334
+ const t = getChordT(localPoint[0])
335
+ const h = getWallEffectiveHeightForNodes(wall, nodes, t)
336
+ return [localPoint[0], h + GUIDE_Y_OFFSET, localPoint[2]]
328
337
  })
329
338
  : isCurvedWall(wall)
330
339
  ? sampleWallCenterline(wall, 24).map((point, index, points) => {
@@ -334,12 +343,14 @@ function buildMeasurementGuide(
334
343
  : index === points.length - 1
335
344
  ? endLocal
336
345
  : worldPointToWallLocal(wall, point)
346
+ const t = getChordT(localPoint[0])
347
+ const h = getWallEffectiveHeightForNodes(wall, nodes, t)
337
348
 
338
- return [localPoint[0], height + GUIDE_Y_OFFSET, localPoint[2]]
349
+ return [localPoint[0], h + GUIDE_Y_OFFSET, localPoint[2]]
339
350
  })
340
351
  : [
341
- [startLocal[0], height + GUIDE_Y_OFFSET, startLocal[2]],
342
- [endLocal[0], height + GUIDE_Y_OFFSET, endLocal[2]],
352
+ [startLocal[0], heightStart + GUIDE_Y_OFFSET, startLocal[2]],
353
+ [endLocal[0], heightEnd + GUIDE_Y_OFFSET, endLocal[2]],
343
354
  ]
344
355
 
345
356
  if (guidePath.length < 2) return null
@@ -397,26 +408,30 @@ function buildMeasurementGuide(
397
408
  ],
398
409
  })
399
410
  const bottomHeightTick = getHorizontalHeightTick(0)
400
- const topHeightTick = getHorizontalHeightTick(height)
411
+ const topHeightTick = getHorizontalHeightTick(heightEnd)
401
412
 
402
413
  return {
403
414
  guidePath,
404
- extStartStart: [extensionStartBase[0], height, extensionStartBase[2]],
415
+ extStartStart: [extensionStartBase[0], heightStart, extensionStartBase[2]],
405
416
  extStartEnd: [
406
417
  extensionStartBase[0],
407
- height + GUIDE_Y_OFFSET + extOvershoot,
418
+ heightStart + GUIDE_Y_OFFSET + extOvershoot,
408
419
  extensionStartBase[2],
409
420
  ],
410
- extEndStart: [extensionEndBase[0], height, extensionEndBase[2]],
411
- extEndEnd: [extensionEndBase[0], height + GUIDE_Y_OFFSET + extOvershoot, extensionEndBase[2]],
421
+ extEndStart: [extensionEndBase[0], heightEnd, extensionEndBase[2]],
422
+ extEndEnd: [
423
+ extensionEndBase[0],
424
+ heightEnd + GUIDE_Y_OFFSET + extOvershoot,
425
+ extensionEndBase[2],
426
+ ],
412
427
  labelPosition: [midpoint[0], midpoint[1] + LABEL_LIFT, midpoint[2]],
413
428
  heightStart: [heightGuidePosition[0], 0, heightGuidePosition[2]],
414
- heightEnd: [heightGuidePosition[0], height, heightGuidePosition[2]],
429
+ heightEnd: [heightGuidePosition[0], heightEnd, heightGuidePosition[2]],
415
430
  heightBottomTickStart: bottomHeightTick.start,
416
431
  heightBottomTickEnd: bottomHeightTick.end,
417
432
  heightTopTickStart: topHeightTick.start,
418
433
  heightTopTickEnd: topHeightTick.end,
419
- heightLabelPosition: [heightGuidePosition[0], height / 2, heightGuidePosition[2]],
434
+ heightLabelPosition: [heightGuidePosition[0], heightEnd / 2, heightGuidePosition[2]],
420
435
  }
421
436
  }
422
437
 
@@ -530,7 +545,8 @@ function WallMeasurementAnnotation({ wall }: { wall: WallNode }) {
530
545
  return total
531
546
  }, [guide, wall])
532
547
  const label = formatLinearMeasurement(length, unit, metricNotation)
533
- const height = useMemo(() => getWallEffectiveHeightForNodes(wall, nodes), [nodes, wall])
548
+ // Height annotation uses t=1 (wall end) because the vertical guide is drawn at the endpoint
549
+ const height = useMemo(() => getWallEffectiveHeightForNodes(wall, nodes, 1), [nodes, wall])
534
550
  const heightLabel = `H ${formatLinearMeasurement(height, unit, metricNotation)}`
535
551
 
536
552
  if (!(guide && Number.isFinite(length) && length >= 0.01)) return null