@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
@@ -6,6 +6,7 @@
6
6
  import {
7
7
  getWallCurveFrameAt,
8
8
  getWallCurveLength,
9
+ getWallFaceOffsets,
9
10
  isCurvedWall,
10
11
  type WallNode,
11
12
  } from '@pascal-app/core'
@@ -124,6 +125,142 @@ export function findWallSnapTarget(
124
125
  return bestTarget
125
126
  }
126
127
 
128
+ // A drafted segment closer than this to a wall's direction (sine of the angle
129
+ // between them) runs alongside it: the wall topology stacks such walls rather
130
+ // than T-joining them, so the body snap leaves them alone. Two walls meeting
131
+ // closer than this aren't a corner either.
132
+ const WALL_BODY_SNAP_MIN_SINE = 0.5
133
+
134
+ /** A straight wall's reference line as a frame, with the faces its body spans. */
135
+ type WallBodyFrame = {
136
+ wall: WallNode
137
+ length: number
138
+ ux: number
139
+ uz: number
140
+ faces: { a: number; b: number }
141
+ /** Offset along the wall's left normal, the side its face offsets are measured on. */
142
+ across: (q: WallPlanPoint) => number
143
+ /** Station along the reference line from `start`. */
144
+ along: (q: WallPlanPoint) => number
145
+ }
146
+
147
+ function wallBodyFrame(wall: WallNode): WallBodyFrame | null {
148
+ if (isCurvedWall(wall)) return null
149
+ const [x1, z1] = wall.start
150
+ const length = Math.hypot(wall.end[0] - x1, wall.end[1] - z1)
151
+ if (length < 1e-9) return null
152
+ const ux = (wall.end[0] - x1) / length
153
+ const uz = (wall.end[1] - z1) / length
154
+ return {
155
+ wall,
156
+ length,
157
+ ux,
158
+ uz,
159
+ faces: getWallFaceOffsets(wall),
160
+ across: (q) => (q[1] - z1) * ux - (q[0] - x1) * uz,
161
+ along: (q) => (q[0] - x1) * ux + (q[1] - z1) * uz,
162
+ }
163
+ }
164
+
165
+ /**
166
+ * The corner a join at `target` on `host`'s reference line falls into: where
167
+ * `host` meets another straight wall, at an L or a T, with `target` inside that
168
+ * wall's drawn body. A T-join there would split `host` into a stub buried in
169
+ * the corner block, so the end joins the junction instead. A shared end comes
170
+ * back exactly; walls meeting at a shallow angle aren't a corner.
171
+ */
172
+ function cornerJunctionAt(
173
+ target: WallPlanPoint,
174
+ host: WallBodyFrame,
175
+ frames: WallBodyFrame[],
176
+ ): WallPlanPoint | null {
177
+ for (const other of frames) {
178
+ if (other === host) continue
179
+ if (Math.abs(host.ux * other.uz - host.uz * other.ux) < WALL_BODY_SNAP_MIN_SINE) continue
180
+ const offset = other.across(target)
181
+ if (offset < other.faces.b || offset > other.faces.a) continue
182
+ const ends = [host.wall.start, host.wall.end, other.wall.start, other.wall.end]
183
+ const crossing = segmentIntersection(ends[0]!, ends[1]!, ends[2]!, ends[3]!)
184
+ if (!crossing) continue
185
+ const sharedEnd = ends.find((end) => distanceSquared(end, crossing) <= 1e-12)
186
+ return sharedEnd ? [sharedEnd[0], sharedEnd[1]] : crossing
187
+ }
188
+ return null
189
+ }
190
+
191
+ /**
192
+ * Where a drawn wall end joins a wall it is aimed into: the point on a straight
193
+ * wall's reference line (`start` → `end`, the line the wall topology T-joins a
194
+ * partition end on, whatever the wall's justification) for an end inside that
195
+ * wall's drawn body or within `margin` of either face.
196
+ *
197
+ * `point` is the raw cursor and `placed` its grid / angle placement; either one
198
+ * inside a body captures, so the grid can't leave an end a fraction short of a
199
+ * face. Given the drafted segment's `start`, the end slides along start →
200
+ * `placed` onto the line, so a square or angle-locked segment keeps its
201
+ * direction, and a wall the segment runs alongside (or starts on) is skipped.
202
+ * Without a start the end drops square onto the line, keeping its position
203
+ * along the wall. A join that lands inside a corner block, where the wall
204
+ * meets another at an L or a T, takes the junction itself (`cornerJunctionAt`).
205
+ * Curved walls are left to the edge snap (`findWallSnapTarget`).
206
+ */
207
+ export function findWallBodySnapTarget(
208
+ point: WallPlanPoint,
209
+ walls: WallNode[],
210
+ options?: {
211
+ ignoreWallIds?: string[]
212
+ placed?: WallPlanPoint
213
+ start?: WallPlanPoint
214
+ margin?: number
215
+ },
216
+ ): WallPlanPoint | null {
217
+ const ignored = new Set(options?.ignoreWallIds ?? [])
218
+ const placed = options?.placed ?? point
219
+ const start = options?.start
220
+ const margin = options?.margin ?? WALL_CONNECT_SNAP_RADIUS
221
+ const frames = walls.flatMap((wall) => {
222
+ const frame = ignored.has(wall.id) ? null : wallBodyFrame(wall)
223
+ return frame ? [frame] : []
224
+ })
225
+ let best: WallPlanPoint | null = null
226
+ let bestOutside = Number.POSITIVE_INFINITY
227
+
228
+ for (const frame of frames) {
229
+ const { length, ux, uz, faces, across, along } = frame
230
+ const [x1, z1] = frame.wall.start
231
+ const onWall = (q: WallPlanPoint) => along(q) > 0 && along(q) < length
232
+ const outsideBody = (q: WallPlanPoint) =>
233
+ onWall(q) ? Math.max(0, faces.b - across(q), across(q) - faces.a) : Number.POSITIVE_INFINITY
234
+ const outside = Math.min(outsideBody(point), outsideBody(placed))
235
+ if (outside > margin || outside >= bestOutside) continue
236
+
237
+ let target: WallPlanPoint | null
238
+ if (Math.abs(across(point)) <= 1e-9 && onWall(point)) {
239
+ // Already on the line: a joined end snapped again (the plan view hands
240
+ // the 3D tool its resolved point) stays put.
241
+ target = [point[0], point[1]]
242
+ } else if (start) {
243
+ const dx = placed[0] - start[0]
244
+ const dz = placed[1] - start[1]
245
+ const run = Math.hypot(dx, dz)
246
+ if (run < 1e-9 || Math.abs(dx * uz - dz * ux) / run < WALL_BODY_SNAP_MIN_SINE) continue
247
+ const startAcross = across(start)
248
+ const t = startAcross / (startAcross - across(placed))
249
+ if (!(t > 1e-6)) continue
250
+ target = [start[0] + dx * t, start[1] + dz * t]
251
+ if (!onWall(target)) continue
252
+ } else {
253
+ const station = along(placed)
254
+ if (!(station > 0 && station < length)) continue
255
+ target = [x1 + ux * station, z1 + uz * station]
256
+ }
257
+ best = cornerJunctionAt(target, frame, frames) ?? target
258
+ bestOutside = outside
259
+ }
260
+
261
+ return best
262
+ }
263
+
127
264
  /**
128
265
  * Wall ids that contain an already-resolved snap point.
129
266
  *
@@ -0,0 +1,99 @@
1
+ import { describe, expect, test } from 'bun:test'
2
+ import {
3
+ formatWallDraftHeading,
4
+ resolveTypedWallEnd,
5
+ wallDraftHeadingDegrees,
6
+ } from './wall-typed-entry'
7
+
8
+ const metric = { bareUnit: 'm', system: 'metric' } as const
9
+
10
+ function expectPoint(actual: [number, number] | null, expected: [number, number]) {
11
+ expect(actual).not.toBeNull()
12
+ expect(actual![0]).toBeCloseTo(expected[0], 10)
13
+ expect(actual![1]).toBeCloseTo(expected[1], 10)
14
+ }
15
+
16
+ describe('resolveTypedWallEnd — length only', () => {
17
+ test('keeps the pointer direction and replaces the distance', () => {
18
+ expectPoint(resolveTypedWallEnd([0, 0], [3, 4], ['1', ''], metric), [0.6, 0.8])
19
+ expectPoint(resolveTypedWallEnd([0, 0], [-30, -40], ['5', ''], metric), [-3, -4])
20
+ })
21
+
22
+ test('reads the whole field, not the length the pointer last showed', () => {
23
+ expectPoint(resolveTypedWallEnd([0, 0], [1, 0], ['12', ''], metric), [12, 0])
24
+ })
25
+
26
+ test('honours unit suffixes and the bare unit', () => {
27
+ expectPoint(resolveTypedWallEnd([1, 1], [11, 1], ['250cm', ''], metric), [3.5, 1])
28
+ expectPoint(
29
+ resolveTypedWallEnd([0, 0], [0, 9], ['2500', ''], { bareUnit: 'mm', system: 'metric' }),
30
+ [0, 2.5],
31
+ )
32
+ expectPoint(
33
+ resolveTypedWallEnd([0, 0], [9, 0], ["10'", ''], { bareUnit: 'in', system: 'imperial' }),
34
+ [3.048, 0],
35
+ )
36
+ })
37
+
38
+ test('has no endpoint without a usable length or a direction', () => {
39
+ expect(resolveTypedWallEnd([0, 0], [1, 0], ['', ''], metric)).toBeNull()
40
+ expect(resolveTypedWallEnd([0, 0], [1, 0], ['abc', ''], metric)).toBeNull()
41
+ expect(resolveTypedWallEnd([0, 0], [1, 0], ['0', ''], metric)).toBeNull()
42
+ expect(resolveTypedWallEnd([2, 2], [2, 2], ['5', ''], metric)).toBeNull()
43
+ })
44
+ })
45
+
46
+ describe('resolveTypedWallEnd — angle', () => {
47
+ test('angle only locks the heading; the pointer projection sets the length', () => {
48
+ // 90° runs along +Z; a pointer at (1, 3) projects to 3 m along it.
49
+ expectPoint(resolveTypedWallEnd([0, 0], [1, 3], ['', '90'], metric), [0, 3])
50
+ // (3, 4) projects to 7/√2 along 45°, landing 3.5 out on each axis.
51
+ expectPoint(resolveTypedWallEnd([1, 1], [4, 5], ['', '45'], metric), [4.5, 4.5])
52
+ })
53
+
54
+ test('angle only quantises the projected length to the step', () => {
55
+ expectPoint(resolveTypedWallEnd([0, 0], [0.2, 2.37], ['', '90'], metric, 0.5), [0, 2.5])
56
+ })
57
+
58
+ test('angle only never flips the wall behind its start', () => {
59
+ expectPoint(resolveTypedWallEnd([0, 0], [-4, 0.1], ['', '0'], metric), [0, 0])
60
+ })
61
+
62
+ test('length and angle fix the endpoint whatever the pointer does', () => {
63
+ expectPoint(resolveTypedWallEnd([2, 1], [-50, 80], ['4', '180'], metric), [-2, 1])
64
+ expectPoint(resolveTypedWallEnd([0, 0], [0, 0], ['2', '270'], metric), [0, -2])
65
+ expectPoint(resolveTypedWallEnd([0, 0], [5, 5], ['1', '-90'], metric), [0, -1])
66
+ })
67
+
68
+ test('accepts degree and radian suffixes', () => {
69
+ expectPoint(resolveTypedWallEnd([0, 0], [9, 9], ['2', '90°'], metric), [0, 2])
70
+ expectPoint(resolveTypedWallEnd([0, 0], [9, 9], ['2', `${Math.PI}rad`], metric), [-2, 0])
71
+ })
72
+
73
+ test('an unreadable angle leaves the direction to the pointer', () => {
74
+ expectPoint(resolveTypedWallEnd([0, 0], [0, 4], ['2', 'x'], metric), [0, 2])
75
+ expect(resolveTypedWallEnd([0, 0], [0, 4], ['', 'x'], metric)).toBeNull()
76
+ })
77
+ })
78
+
79
+ describe('wallDraftHeadingDegrees', () => {
80
+ test('measures from +X towards +Z in [0, 360)', () => {
81
+ expect(wallDraftHeadingDegrees([0, 0], [1, 0])).toBeCloseTo(0, 10)
82
+ expect(wallDraftHeadingDegrees([0, 0], [0, 1])).toBeCloseTo(90, 10)
83
+ expect(wallDraftHeadingDegrees([0, 0], [-1, 0])).toBeCloseTo(180, 10)
84
+ expect(wallDraftHeadingDegrees([0, 0], [0, -1])).toBeCloseTo(270, 10)
85
+ expect(wallDraftHeadingDegrees([1, 1], [1, 1])).toBeNull()
86
+ })
87
+
88
+ test('round-trips through a typed angle', () => {
89
+ const heading = wallDraftHeadingDegrees([0, 0], [-3, -4])!
90
+ expectPoint(resolveTypedWallEnd([0, 0], [0, 0], ['5', String(heading)], metric), [-3, -4])
91
+ })
92
+
93
+ test('labels the live heading in the same convention, wrapping 360° to 0°', () => {
94
+ expect(formatWallDraftHeading([0, 0], [0, 1])).toBe('90°')
95
+ expect(formatWallDraftHeading([0, 0], [1, -1])).toBe('315°')
96
+ expect(formatWallDraftHeading([0, 0], [1, -0.001])).toBe('0°')
97
+ expect(formatWallDraftHeading([0, 0], [0, 0])).toBe('')
98
+ })
99
+ })
@@ -0,0 +1,70 @@
1
+ import { formatAngleRadians } from '@pascal-app/core'
2
+ import { type ParseMeasurementOptions, parseMeasurement } from '../../../lib/measurement-parser'
3
+ import type { PlacementTypingValues } from '../../../store/use-placement-typing'
4
+ import type { WallPlanPoint } from './wall-drafting'
5
+
6
+ /**
7
+ * Heading of a wall draft in degrees, the way the 15° angle snap measures it:
8
+ * from the building's +X axis towards +Z (`atan2(dz, dx)`), in [0, 360).
9
+ * On an unrotated plan +Z points down the screen, so 90 runs straight down.
10
+ */
11
+ export function wallDraftHeadingDegrees(start: WallPlanPoint, end: WallPlanPoint): number | null {
12
+ const dx = end[0] - start[0]
13
+ const dz = end[1] - start[1]
14
+ if (Math.hypot(dx, dz) <= 1e-6) return null
15
+ const degrees = (Math.atan2(dz, dx) * 180) / Math.PI
16
+ return ((degrees % 360) + 360) % 360
17
+ }
18
+
19
+ /** The draft's live heading as the angle field's placeholder, e.g. `90°`. */
20
+ export function formatWallDraftHeading(start: WallPlanPoint, end: WallPlanPoint): string {
21
+ const degrees = wallDraftHeadingDegrees(start, end)
22
+ if (degrees === null) return ''
23
+ return formatAngleRadians(((Math.round(degrees) % 360) * Math.PI) / 180)
24
+ }
25
+
26
+ /**
27
+ * The endpoint a typed wall draft commits to. Field 0 is the length, field 1
28
+ * the heading (see `wallDraftHeadingDegrees`); an empty field is left to the
29
+ * pointer:
30
+ * - length only: the pointer steers the direction, the length is exact;
31
+ * - angle only: the direction is locked, the pointer's projection onto it sets
32
+ * the length (quantised to `lengthStep` when one is given);
33
+ * - both: the endpoint is fully determined and the pointer is ignored.
34
+ *
35
+ * Returns null when no field holds a usable value or the pointer gives no
36
+ * direction to follow, so the caller keeps the pointer's own endpoint.
37
+ */
38
+ export function resolveTypedWallEnd(
39
+ start: WallPlanPoint,
40
+ pointer: WallPlanPoint,
41
+ fields: Readonly<PlacementTypingValues>,
42
+ lengthOptions: ParseMeasurementOptions,
43
+ lengthStep = 0,
44
+ ): WallPlanPoint | null {
45
+ const typedLength = fields[0].trim()
46
+ ? parseMeasurement(fields[0], { kind: 'length', unitId: 'm' }, lengthOptions)
47
+ : null
48
+ const length = typedLength !== null && typedLength > 0 ? typedLength : null
49
+ const typedAngle = fields[1].trim()
50
+ ? parseMeasurement(fields[1], { kind: 'angle', unitId: 'rad' }, { bareUnit: 'deg' })
51
+ : null
52
+
53
+ const dx = pointer[0] - start[0]
54
+ const dz = pointer[1] - start[1]
55
+ if (typedAngle === null) {
56
+ if (length === null) return null
57
+ const pointerLength = Math.hypot(dx, dz)
58
+ if (pointerLength <= 1e-6) return null
59
+ return [start[0] + (dx / pointerLength) * length, start[1] + (dz / pointerLength) * length]
60
+ }
61
+
62
+ const dirX = Math.cos(typedAngle)
63
+ const dirZ = Math.sin(typedAngle)
64
+ let distance = length
65
+ if (distance === null) {
66
+ const projected = Math.max(0, dx * dirX + dz * dirZ)
67
+ distance = lengthStep > 0 ? Math.round(projected / lengthStep) * lengthStep : projected
68
+ }
69
+ return [start[0] + dirX * distance, start[1] + dirZ * distance]
70
+ }
@@ -184,9 +184,9 @@ export const ZoneTool: React.FC = () => {
184
184
  updateLines()
185
185
  }
186
186
 
187
- // World-grid snap projected into building-local; rotated buildings
188
- // used to pull the snap off the visible grid lines. Grid quantize only
189
- // in grid mode; off / lines / angles leave the raw cursor.
187
+ // Snap on the building's own grid lattice, the one its walls are drafted
188
+ // on (square to a turned house). Grid quantize only in grid mode; off /
189
+ // lines / angles leave the raw cursor.
190
190
  const gridPointOf = (event: GridEvent): [number, number] =>
191
191
  isGridSnapActive()
192
192
  ? snapWorldXZForActiveBuilding(
@@ -44,6 +44,8 @@ export const ActionButton = React.forwardRef<HTMLButtonElement, ActionButtonProp
44
44
  // These are toggles, not commands: the armed/unarmed state has to be
45
45
  // readable without decoding the active-state styling.
46
46
  aria-pressed={isActive}
47
+ // Icon-only: the tooltip text is the button's name.
48
+ aria-label={label}
47
49
  className={cn('relative h-11 w-11 transition-all', className)}
48
50
  onClick={(event) => {
49
51
  triggerSFX('sfx:menu-click')
@@ -1,6 +1,6 @@
1
1
  'use client'
2
2
 
3
- import { emitter } from '@pascal-app/core'
3
+ import { emitter, usePascalI18n } from '@pascal-app/core'
4
4
  import Image from 'next/image'
5
5
  import useEditor from '../../../store/use-editor'
6
6
  import { visibleScene } from '../../../store/view-layout'
@@ -8,6 +8,7 @@ import { ActionButton } from './action-button'
8
8
  import { FollowPascalToggle } from './follow-pascal-toggle'
9
9
 
10
10
  export function CameraActions({ hideOrbit = false }: { hideOrbit?: boolean }) {
11
+ const i18n = usePascalI18n()
11
12
  // Orbit stays useful in 2D-only (it spins the synced floorplan view), but
12
13
  // top view only tilts the hidden 3D camera — pointless without the canvas.
13
14
  const is2dOnly = useEditor((s) => visibleScene(s) === '2d')
@@ -31,13 +32,13 @@ export function CameraActions({ hideOrbit = false }: { hideOrbit?: boolean }) {
31
32
  {/* Orbit CCW */}
32
33
  <ActionButton
33
34
  className="group hover:bg-black/4"
34
- label="Orbit Left"
35
+ label={i18n._({ id: 'editor.actionMenu.cameraActions.orbitLeft', message: 'Orbit Left' })}
35
36
  onClick={orbitCCW}
36
37
  size="icon"
37
38
  variant="ghost"
38
39
  >
39
40
  <Image
40
- alt="Orbit Left"
41
+ alt={i18n._({ id: 'editor.actionMenu.cameraActions.orbitLeft', message: 'Orbit Left' })}
41
42
  className="h-[28px] w-[28px] -scale-x-100 object-contain opacity-70 transition-opacity group-hover:opacity-100"
42
43
  height={28}
43
44
  src="/icons/rotate.webp"
@@ -48,13 +49,13 @@ export function CameraActions({ hideOrbit = false }: { hideOrbit?: boolean }) {
48
49
  {/* Orbit CW */}
49
50
  <ActionButton
50
51
  className="group hover:bg-black/4"
51
- label="Orbit Right"
52
+ label={i18n._({ id: 'editor.actionMenu.cameraActions.orbitRight', message: 'Orbit Right' })}
52
53
  onClick={orbitCW}
53
54
  size="icon"
54
55
  variant="ghost"
55
56
  >
56
57
  <Image
57
- alt="Orbit Right"
58
+ alt={i18n._({ id: 'editor.actionMenu.cameraActions.orbitRight', message: 'Orbit Right' })}
58
59
  className="h-[28px] w-[28px] object-contain opacity-70 transition-opacity group-hover:opacity-100"
59
60
  height={28}
60
61
  src="/icons/rotate.webp"
@@ -71,13 +72,13 @@ export function CameraActions({ hideOrbit = false }: { hideOrbit?: boolean }) {
71
72
  {!is2dOnly && (
72
73
  <ActionButton
73
74
  className="group hover:bg-black/4"
74
- label="Top View"
75
+ label={i18n._({ id: 'editor.actionMenu.cameraActions.topView', message: 'Top View' })}
75
76
  onClick={goToTopView}
76
77
  size="icon"
77
78
  variant="ghost"
78
79
  >
79
80
  <Image
80
- alt="Top View"
81
+ alt={i18n._({ id: 'editor.actionMenu.cameraActions.topView', message: 'Top View' })}
81
82
  className="h-[28px] w-[28px] object-contain opacity-70 transition-opacity group-hover:opacity-100"
82
83
  height={28}
83
84
  src="/icons/topview.webp"
@@ -1,5 +1,8 @@
1
1
  'use client'
2
2
 
3
+ import { usePascalI18n } from '@pascal-app/core'
4
+ import type { MessageDescriptor } from '@pascal-app/core'
5
+
3
6
  import { Icon } from '@iconify/react'
4
7
  import { type LucideIcon, Trash2 } from 'lucide-react'
5
8
  import Image from 'next/image'
@@ -17,7 +20,7 @@ type ControlConfig = {
17
20
  icon?: LucideIcon
18
21
  iconifyIcon?: string
19
22
  imageSrc?: string
20
- label: string
23
+ label: MessageDescriptor
21
24
  shortcut?: string
22
25
  color: string
23
26
  activeColor: string
@@ -28,7 +31,7 @@ const controls: ControlConfig[] = [
28
31
  {
29
32
  id: 'select',
30
33
  imageSrc: '/icons/select.webp',
31
- label: 'Select',
34
+ label: /* i18n */ { id: 'editor.actionMenu.controlModes.select', message: 'Select' },
32
35
  shortcut: 'V',
33
36
  color: 'hover:bg-blue-500/20 hover:text-blue-600 dark:hover:text-blue-400',
34
37
  activeColor: 'bg-blue-500/20 text-blue-600 dark:text-blue-400 dark:hover:text-accent-foreground',
@@ -36,7 +39,7 @@ const controls: ControlConfig[] = [
36
39
  {
37
40
  id: 'delete',
38
41
  icon: Trash2,
39
- label: 'Delete',
42
+ label: /* i18n */ { id: 'editor.actionMenu.controlModes.delete', message: 'Delete' },
40
43
  shortcut: 'X',
41
44
  color: 'hover:bg-red-500/20 hover:text-red-600 dark:hover:text-red-400',
42
45
  activeColor: 'bg-red-500/20 text-red-600 dark:text-red-400 dark:hover:text-accent-foreground',
@@ -44,6 +47,7 @@ const controls: ControlConfig[] = [
44
47
  ]
45
48
 
46
49
  export function ControlModes() {
50
+ const i18n = usePascalI18n()
47
51
  const mode = useEditor((state) => state.mode)
48
52
  const selectionTool = useEditor((state) => state.floorplanSelectionTool)
49
53
  const armToolMode = useEditor((state) => state.armToolMode)
@@ -94,7 +98,7 @@ export function ControlModes() {
94
98
  // tour at this button. Nothing here reads it.
95
99
  data-guide-target={c.id === 'select' ? 'mode-select' : undefined}
96
100
  isActive={isActive}
97
- label={c.label}
101
+ label={i18n._(c.label)}
98
102
  onClick={() => handleClick(c.id)}
99
103
  shortcut={c.shortcut}
100
104
  size="icon"
@@ -102,7 +106,7 @@ export function ControlModes() {
102
106
  >
103
107
  {c.imageSrc ? (
104
108
  <Image
105
- alt={c.label}
109
+ alt={i18n._(c.label)}
106
110
  className={cn(
107
111
  'h-[28px] w-[28px] object-contain transition-[opacity,filter] duration-200',
108
112
  isActive
@@ -1,5 +1,6 @@
1
1
  'use client'
2
2
 
3
+ import { usePascalI18n } from '@pascal-app/core'
3
4
  import { AnimatePresence, motion } from 'motion/react'
4
5
  import { useReducedMotion } from '../../../hooks/use-reduced-motion'
5
6
  import useFollowPascal from '../../../store/use-follow-pascal'
@@ -9,6 +10,7 @@ import useFollowPascal from '../../../store/use-follow-pascal'
9
10
  * following. It comes up from where the toolbar is (origin-aware) and goes the same way.
10
11
  */
11
12
  export function FollowPascalPill() {
13
+ const i18n = usePascalI18n()
12
14
  const shows = useFollowPascal((state) => state.on && state.paused && state.building)
13
15
  const resume = useFollowPascal((state) => state.resume)
14
16
  const reduced = useReducedMotion()
@@ -24,7 +26,7 @@ export function FollowPascalPill() {
24
26
  transition={reduced ? { duration: 0.12 } : { type: 'spring', bounce: 0, duration: 0.3 }}
25
27
  type="button"
26
28
  >
27
- Resume following
29
+ {i18n._({ id: 'editor.actionMenu.followPascalPill.resumeFollowing', message: 'Resume following' })}
28
30
  </motion.button>
29
31
  ) : null}
30
32
  </AnimatePresence>
@@ -1,5 +1,6 @@
1
1
  'use client'
2
2
 
3
+ import { usePascalI18n } from '@pascal-app/core'
3
4
  import { Video } from 'lucide-react'
4
5
  import { cn } from '../../../lib/utils'
5
6
  import useFollowPascal from '../../../store/use-follow-pascal'
@@ -13,6 +14,7 @@ export function FollowPascalToggle({
13
14
  className?: string
14
15
  tooltipClassName?: string
15
16
  }) {
17
+ const i18n = usePascalI18n()
16
18
  const on = useFollowPascal((state) => state.on)
17
19
  const setOn = useFollowPascal((state) => state.setOn)
18
20
  return (
@@ -24,7 +26,11 @@ export function FollowPascalToggle({
24
26
  : 'hover:bg-black/4 hover:text-violet-600 dark:hover:text-violet-300',
25
27
  className,
26
28
  )}
27
- label={`Follow Pascal: ${on ? 'On' : 'Off'}`}
29
+ label={
30
+ on
31
+ ? i18n._({ id: 'editor.actionMenu.followPascalToggle.on', message: 'Follow Pascal: On' })
32
+ : i18n._({ id: 'editor.actionMenu.followPascalToggle.off', message: 'Follow Pascal: Off' })
33
+ }
28
34
  onClick={() => setOn(!on)}
29
35
  size="icon"
30
36
  tooltipClassName={tooltipClassName}
@@ -1,16 +1,17 @@
1
+ import type { MessageDescriptor } from '@pascal-app/core'
1
2
  import type { CatalogCategory } from './../../../store/use-editor'
2
3
 
3
4
  export type FurnishToolConfig = {
4
5
  id: 'item'
5
6
  iconSrc: string
6
- label: string
7
+ label: MessageDescriptor
7
8
  catalogCategory: CatalogCategory
8
9
  }
9
10
 
10
11
  export const furnishTools: FurnishToolConfig[] = [
11
- { id: 'item', iconSrc: '/icons/couch.webp', label: 'Furniture', catalogCategory: 'furniture' },
12
- { id: 'item', iconSrc: '/icons/appliance.webp', label: 'Appliance', catalogCategory: 'appliance' },
13
- { id: 'item', iconSrc: '/icons/kitchen.webp', label: 'Kitchen', catalogCategory: 'kitchen' },
14
- { id: 'item', iconSrc: '/icons/bathroom.webp', label: 'Bathroom', catalogCategory: 'bathroom' },
15
- { id: 'item', iconSrc: '/icons/tree.webp', label: 'Outdoor', catalogCategory: 'outdoor' },
12
+ { id: 'item', iconSrc: '/icons/couch.webp', label: /* i18n */ { id: 'editor.actionMenu.furnishTools.furniture', message: 'Furniture' }, catalogCategory: 'furniture' },
13
+ { id: 'item', iconSrc: '/icons/appliance.webp', label: /* i18n */ { id: 'editor.actionMenu.furnishTools.appliance', message: 'Appliance' }, catalogCategory: 'appliance' },
14
+ { id: 'item', iconSrc: '/icons/kitchen.webp', label: /* i18n */ { id: 'editor.actionMenu.furnishTools.kitchen', message: 'Kitchen' }, catalogCategory: 'kitchen' },
15
+ { id: 'item', iconSrc: '/icons/bathroom.webp', label: /* i18n */ { id: 'editor.actionMenu.furnishTools.bathroom', message: 'Bathroom' }, catalogCategory: 'bathroom' },
16
+ { id: 'item', iconSrc: '/icons/tree.webp', label: /* i18n */ { id: 'editor.actionMenu.furnishTools.outdoor', message: 'Outdoor' }, catalogCategory: 'outdoor' },
16
17
  ]
@@ -66,6 +66,7 @@ export function ActionMenu({
66
66
  <TooltipProvider>
67
67
  <FollowPascalPill />
68
68
  <motion.div
69
+ data-surface="tool-dock"
69
70
  className={cn(
70
71
  'z-50',
71
72
  isMobile