@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
@@ -1,13 +1,18 @@
1
1
  'use client'
2
2
 
3
+ import { type PascalI18n, usePascalI18n } from '@pascal-app/core'
3
4
  import { ChevronRight } from 'lucide-react'
4
5
  import { type ComponentPropsWithoutRef, type ReactNode, useEffect, useState } from 'react'
5
6
  import { cn } from '../../../lib/utils'
6
7
  import {
7
- WALL_DRAW_IDLE_CAPTION,
8
+ selectWallKind,
9
+ useWallKind,
8
10
  WALL_DRAW_VARIANTS,
11
+ WALL_KINDS,
9
12
  type WallDrawVariant,
13
+ type WallKind,
10
14
  } from '../../../lib/wall-draw-variant'
15
+ import { SegmentedControl } from '../controls/segmented-control'
11
16
 
12
17
  /**
13
18
  * The Build panel's grouped layout (Rooms / Add to rooms / Outdoor / Advanced),
@@ -70,6 +75,7 @@ export function BuildPanelAdvancedSection({
70
75
  description?: string
71
76
  hint?: string
72
77
  }) {
78
+ const i18n = usePascalI18n()
73
79
  const [open, setOpen] = useState(false)
74
80
  useEffect(() => setOpen(readAdvancedOpen()), [])
75
81
  useEffect(() => {
@@ -94,7 +100,9 @@ export function BuildPanelAdvancedSection({
94
100
  open && 'rotate-90',
95
101
  )}
96
102
  />
97
- <h3 className="font-medium text-foreground text-xs">Advanced</h3>
103
+ <h3 className="font-medium text-foreground text-xs">
104
+ {i18n._({ id: 'editor.buildPanel.advanced', message: 'Advanced' })}
105
+ </h3>
98
106
  {hint ? (
99
107
  <span className="ml-auto truncate text-[11px] text-muted-foreground">{hint}</span>
100
108
  ) : null}
@@ -188,19 +196,32 @@ export function BuildToolTile({
188
196
  )
189
197
  }
190
198
 
199
+ const CAPTION_LEAD_MARKER = '__PASCAL_CAPTION_LEAD__'
200
+
191
201
  /** The line under the Rooms tiles: what the armed shape does, lead in bold. */
192
- export function BuildPanelCaption({ lead, rest }: { lead?: string; rest: string }) {
202
+ export function BuildPanelCaption({ caption, lead }: { caption: string; lead?: string }) {
203
+ const markerIndex = caption.indexOf(CAPTION_LEAD_MARKER)
204
+ const hasMarkedLead = lead && markerIndex >= 0
193
205
  return (
194
206
  <p className="mt-2 min-h-[2lh] px-0.5 text-[11px] text-muted-foreground leading-relaxed">
195
- {lead ? <span className="font-medium text-foreground">{lead} </span> : null}
196
- {rest}
207
+ {hasMarkedLead ? (
208
+ <>
209
+ {caption.slice(0, markerIndex)}
210
+ <span className="font-medium text-foreground">{lead}</span>
211
+ {caption.slice(markerIndex + CAPTION_LEAD_MARKER.length)}
212
+ </>
213
+ ) : (
214
+ caption
215
+ )}
197
216
  </p>
198
217
  )
199
218
  }
200
219
 
201
220
  /**
202
221
  * The Rooms group: one large tile per wall drawing variant, and the armed
203
- * variant's caption. `activeVariant` is null while the wall tool is not armed.
222
+ * variant's caption. While Walls is armed the caption gives way to its kinds
223
+ * (Standard, Invisible, Railing) and a line on the one picked.
224
+ * `activeVariant` is null while the wall tool is not armed.
204
225
  */
205
226
  export function BuildPanelRoomsSection({
206
227
  activeVariant,
@@ -214,29 +235,146 @@ export function BuildPanelRoomsSection({
214
235
  /** Extra attributes per tile (e.g. a host's guide-target hook). */
215
236
  tileProps?: (variant: WallDrawVariant) => Record<string, string | undefined>
216
237
  }) {
238
+ const i18n = usePascalI18n()
217
239
  const active = WALL_DRAW_VARIANTS.find((variant) => variant.id === activeVariant)
240
+ const activeMessages = active ? roomVariantMessages(active.id, i18n) : null
218
241
  return (
219
- <BuildPanelSection hint="walls · floor · ceiling" id="rooms" title="Rooms">
242
+ <BuildPanelSection
243
+ hint={i18n._({
244
+ id: 'editor.buildPanel.rooms.hint',
245
+ message: 'walls · floor · ceiling',
246
+ })}
247
+ id="rooms"
248
+ title={i18n._({ id: 'editor.buildPanel.rooms.title', message: 'Rooms' })}
249
+ >
220
250
  <BuildToolGrid columns={3}>
221
- {WALL_DRAW_VARIANTS.map((variant) => (
222
- <BuildToolTile
223
- active={variant.id === activeVariant}
224
- data-build-tool={`room-${variant.id}`}
225
- iconSrc={variant.iconSrc}
226
- key={variant.id}
227
- label={variant.label}
228
- onClick={() => onSelect(variant.id)}
229
- onMouseEnter={() => onHover?.(variant.id)}
230
- size="lg"
231
- title={variant.title}
232
- {...tileProps?.(variant.id)}
233
- />
234
- ))}
251
+ {WALL_DRAW_VARIANTS.map((variant) => {
252
+ const messages = roomVariantMessages(variant.id, i18n)
253
+ return (
254
+ <BuildToolTile
255
+ active={variant.id === activeVariant}
256
+ data-build-tool={`room-${variant.id}`}
257
+ iconSrc={variant.iconSrc}
258
+ key={variant.id}
259
+ label={messages.label}
260
+ onClick={() => onSelect(variant.id)}
261
+ onMouseEnter={() => onHover?.(variant.id)}
262
+ size="lg"
263
+ title={messages.title}
264
+ {...tileProps?.(variant.id)}
265
+ />
266
+ )
267
+ })}
235
268
  </BuildToolGrid>
236
- <BuildPanelCaption
237
- lead={active?.caption.lead}
238
- rest={active ? active.caption.rest : WALL_DRAW_IDLE_CAPTION}
239
- />
269
+ {activeVariant === 'walls' ? (
270
+ <BuildPanelWallKinds />
271
+ ) : (
272
+ <BuildPanelCaption
273
+ caption={
274
+ activeMessages?.caption ??
275
+ i18n._({
276
+ id: 'editor.buildPanel.rooms.idle',
277
+ message:
278
+ 'Pick a shape to start. Every room brings its own walls, floor and ceiling.',
279
+ })
280
+ }
281
+ lead={activeMessages?.lead}
282
+ />
283
+ )}
240
284
  </BuildPanelSection>
241
285
  )
242
286
  }
287
+
288
+ function roomVariantMessages(variant: WallDrawVariant, i18n: PascalI18n) {
289
+ if (variant === 'rectangle') {
290
+ return {
291
+ label: i18n._({
292
+ id: 'editor.buildPanel.rooms.rectangle.label',
293
+ message: 'Rectangle',
294
+ }),
295
+ title: i18n._({
296
+ id: 'editor.buildPanel.rooms.rectangle.title',
297
+ message: 'Rectangle room',
298
+ }),
299
+ lead: i18n._({
300
+ id: 'editor.buildPanel.rooms.rectangle.lead',
301
+ message: 'Click two corners',
302
+ }),
303
+ caption: i18n._({
304
+ id: 'editor.buildPanel.rooms.rectangle.caption',
305
+ message: '{lead} on the grid. Walls, floor and ceiling are created together.',
306
+ values: { lead: CAPTION_LEAD_MARKER },
307
+ }),
308
+ }
309
+ }
310
+ if (variant === 'walls') {
311
+ // Armed, Walls shows its kinds in place of a caption (`BuildPanelWallKinds`).
312
+ return {
313
+ label: i18n._({ id: 'editor.buildPanel.rooms.walls.label', message: 'Walls' }),
314
+ title: i18n._({ id: 'editor.buildPanel.rooms.walls.title', message: 'Walls' }),
315
+ lead: undefined,
316
+ caption: undefined,
317
+ }
318
+ }
319
+ return {
320
+ label: i18n._({ id: 'editor.buildPanel.rooms.polygon.label', message: 'Polygon' }),
321
+ title: i18n._({
322
+ id: 'editor.buildPanel.rooms.polygon.title',
323
+ message: 'Polygon room',
324
+ }),
325
+ lead: i18n._({
326
+ id: 'editor.buildPanel.rooms.polygon.lead',
327
+ message: 'Click each corner,',
328
+ }),
329
+ caption: i18n._({
330
+ id: 'editor.buildPanel.rooms.polygon.caption',
331
+ message: '{lead} then the first one again to close. For L-shapes and angled rooms.',
332
+ values: { lead: CAPTION_LEAD_MARKER },
333
+ }),
334
+ }
335
+ }
336
+
337
+ function wallKindHelp(kind: WallKind, i18n: PascalI18n): string {
338
+ if (kind === 'invisible') {
339
+ return i18n._({
340
+ id: 'editor.buildPanel.rooms.wallKinds.invisibleHelp',
341
+ message:
342
+ 'Divides a room or closes off a porch without building a wall; rooms close on it. Change any wall later from its controls.',
343
+ })
344
+ }
345
+ if (kind === 'railing') {
346
+ return i18n._({
347
+ id: 'editor.buildPanel.rooms.wallKinds.railingHelp',
348
+ message:
349
+ 'A guard for stairs, balconies and mezzanines; rooms close on it. Change any wall later from its controls.',
350
+ })
351
+ }
352
+ return i18n._({
353
+ id: 'editor.buildPanel.rooms.wallKinds.standardHelp',
354
+ message:
355
+ 'One wall per two clicks. Its sides become exterior or interior from the rooms it bounds. Change any wall later from its controls.',
356
+ })
357
+ }
358
+
359
+ /** What the armed Walls variant draws: a plain wall, an invisible wall or a railing. */
360
+ function BuildPanelWallKinds() {
361
+ const i18n = usePascalI18n()
362
+ const kind = useWallKind()
363
+ return (
364
+ <div
365
+ aria-label={i18n._({ id: 'editor.buildPanel.rooms.wallKinds.label', message: 'Wall kind' })}
366
+ className="mt-2 flex flex-col gap-2 rounded-xl border border-border/50 bg-black/4 p-2 dark:bg-muted/40"
367
+ data-build-group="wall-kinds"
368
+ role="group"
369
+ >
370
+ <SegmentedControl
371
+ onChange={selectWallKind}
372
+ options={WALL_KINDS.map((entry) => ({ value: entry.id, label: i18n._(entry.label) }))}
373
+ value={kind}
374
+ />
375
+ <p className="px-0.5 text-[11px] text-muted-foreground leading-relaxed">
376
+ {wallKindHelp(kind, i18n)}
377
+ </p>
378
+ </div>
379
+ )
380
+ }
@@ -1,6 +1,12 @@
1
1
  'use client'
2
2
 
3
- import { type AnyNodeId, type LevelNode, type UnitNode, useScene } from '@pascal-app/core'
3
+ import {
4
+ type AnyNodeId,
5
+ type LevelNode,
6
+ type UnitNode,
7
+ usePascalI18n,
8
+ useScene,
9
+ } from '@pascal-app/core'
4
10
  import { useViewer } from '@pascal-app/viewer'
5
11
  import {
6
12
  AppWindow,
@@ -13,6 +19,7 @@ import {
13
19
  Eye,
14
20
  EyeOff,
15
21
  FileJson,
22
+ Gauge,
16
23
  Grid3X3,
17
24
  Group,
18
25
  Hexagon,
@@ -21,8 +28,8 @@ import {
21
28
  Maximize2,
22
29
  Minimize2,
23
30
  Moon,
24
- MousePointer2,
25
31
  Mountain,
32
+ MousePointer2,
26
33
  Package,
27
34
  PaintBucket,
28
35
  PencilLine,
@@ -37,6 +44,7 @@ import {
37
44
  } from 'lucide-react'
38
45
  import { useEffect } from 'react'
39
46
  import { addStoreyOnTop } from '../../../lib/add-storey'
47
+ import { offerExteriorWallCopy } from '../level-wall-copy-prompt'
40
48
  import { getHistoryCommandState, runRedo, runUndo } from '../../../lib/history'
41
49
  import { deleteLevelWithFallbackSelection } from '../../../lib/level-selection'
42
50
  import { getSavedSceneDocument } from '../../../lib/scene'
@@ -47,6 +55,7 @@ import useEditor from '../../../store/use-editor'
47
55
  import { useCommandPalette } from './index'
48
56
 
49
57
  export function EditorCommands() {
58
+ const i18n = usePascalI18n()
50
59
  const register = useCommandRegistry((s) => s.register)
51
60
  const { navigateTo, setInputValue, setOpen } = useCommandPalette()
52
61
 
@@ -205,6 +214,7 @@ export function EditorCommands() {
205
214
  if (!building) return
206
215
  const newLevel = addStoreyOnTop(building.id)
207
216
  useViewer.getState().setSelection({ levelId: newLevel.id })
217
+ offerExteriorWallCopy(newLevel.id)
208
218
  }),
209
219
  },
210
220
  {
@@ -311,20 +321,36 @@ export function EditorCommands() {
311
321
  }),
312
322
  },
313
323
  {
314
- id: 'editor.viewer.shading-solid',
315
- label: 'Switch to Solid',
324
+ id: 'editor.viewer.quality-auto',
325
+ label: i18n._({ id: 'editor.commands.qualityAuto', message: 'Quality: Auto' }),
326
+ group: 'Viewer Controls',
327
+ icon: <Gauge className="h-4 w-4" />,
328
+ keywords: ['quality', 'auto', 'device', 'render'],
329
+ execute: () => run(() => useViewer.getState().setRenderQuality('auto')),
330
+ },
331
+ {
332
+ id: 'editor.viewer.quality-low',
333
+ label: i18n._({ id: 'editor.commands.qualityLow', message: 'Quality: Low' }),
334
+ group: 'Viewer Controls',
335
+ icon: <Gauge className="h-4 w-4" />,
336
+ keywords: ['quality', 'low', 'fast', 'performance', 'solid', 'render'],
337
+ execute: () => run(() => useViewer.getState().setRenderQuality('low')),
338
+ },
339
+ {
340
+ id: 'editor.viewer.quality-medium',
341
+ label: i18n._({ id: 'editor.commands.qualityMedium', message: 'Quality: Medium' }),
316
342
  group: 'Viewer Controls',
317
- icon: <Box className="h-4 w-4" />,
318
- keywords: ['solid', 'shading', 'render', 'mode', 'performance'],
319
- execute: () => run(() => useViewer.getState().setShading('solid')),
343
+ icon: <Gauge className="h-4 w-4" />,
344
+ keywords: ['quality', 'medium', 'render'],
345
+ execute: () => run(() => useViewer.getState().setRenderQuality('medium')),
320
346
  },
321
347
  {
322
- id: 'editor.viewer.shading-rendered',
323
- label: 'Switch to Rendered',
348
+ id: 'editor.viewer.quality-high',
349
+ label: i18n._({ id: 'editor.commands.qualityHigh', message: 'Quality: High' }),
324
350
  group: 'Viewer Controls',
325
351
  icon: <Sparkles className="h-4 w-4" />,
326
- keywords: ['rendered', 'shading', 'render', 'mode', 'quality'],
327
- execute: () => run(() => useViewer.getState().setShading('rendered')),
352
+ keywords: ['quality', 'high', 'rendered', 'render'],
353
+ execute: () => run(() => useViewer.getState().setRenderQuality('high')),
328
354
  },
329
355
  {
330
356
  id: 'editor.viewer.camera-snapshot',
@@ -449,6 +475,7 @@ export function EditorCommands() {
449
475
  setPreviewMode,
450
476
  exportScene,
451
477
  unitListKey,
478
+ i18n,
452
479
  ])
453
480
 
454
481
  return null
@@ -4,36 +4,52 @@ import {
4
4
  type AnyNode,
5
5
  getCatalogMaterialById,
6
6
  getLibraryMaterialIdFromRef,
7
+ getMaterialSwatch,
7
8
  getSceneMaterialIdFromRef,
8
9
  nodeRegistry,
10
+ type PascalI18n,
11
+ usePascalI18n,
9
12
  useScene,
10
13
  } from '@pascal-app/core'
11
14
  import { useState } from 'react'
15
+ import { materialSwatchStyle } from '../../../lib/material-swatch-style'
12
16
  import { Popover, PopoverContent, PopoverTrigger } from '../primitives/popover'
13
17
  import { MaterialPicker } from './material-picker'
14
18
 
15
- type Swatch = { label: string; color: string | null; image: string | null }
19
+ type Swatch = { label: string; color: string | null; image: string | null; tinted?: boolean }
16
20
 
17
21
  /** How a finish reference reads in a panel: its catalog tile, its colour, or its scene material. */
18
22
  function useFinishSwatch(ref: string | undefined, fallback: string): Swatch {
23
+ const i18n = usePascalI18n()
19
24
  const materials = useScene((state) => state.materials)
20
- if (!ref) return { ...describe(fallback, materials), label: 'Default' }
21
- return describe(ref, materials)
25
+ if (!ref) return { ...describe(fallback, materials, i18n), label: defaultLabel(i18n) }
26
+ return describe(ref, materials, i18n)
22
27
  }
23
28
 
24
- function describe(ref: string, materials: ReturnType<typeof useScene.getState>['materials']) {
29
+ const defaultLabel = (i18n: PascalI18n) =>
30
+ i18n._({ id: 'editor.controls.finishSwatch.default', message: 'Default' })
31
+
32
+ function describe(
33
+ ref: string,
34
+ materials: ReturnType<typeof useScene.getState>['materials'],
35
+ i18n: PascalI18n,
36
+ ) {
25
37
  const library = getCatalogMaterialById(getLibraryMaterialIdFromRef(ref) ?? undefined)
26
- if (library)
38
+ if (library) {
39
+ const swatch = getMaterialSwatch(library)
27
40
  return {
28
41
  label: library.label,
29
- color: library.previewColor ?? null,
30
- image: library.previewThumbnailUrl ?? null,
42
+ color: swatch.color,
43
+ image: swatch.imageUrl,
44
+ tinted: swatch.tinted,
31
45
  }
46
+ }
32
47
  const sceneId = getSceneMaterialIdFromRef(ref)
33
48
  const scene = sceneId ? materials[sceneId as keyof typeof materials] : undefined
34
- if (scene) return { label: scene.name, color: scene.material.properties?.color ?? null, image: null }
49
+ if (scene)
50
+ return { label: scene.name, color: scene.material.properties?.color ?? null, image: null }
35
51
  if (ref.startsWith('#')) return { label: ref.toUpperCase(), color: ref, image: null }
36
- return { label: 'Default', color: null, image: null }
52
+ return { label: defaultLabel(i18n), color: null, image: null }
37
53
  }
38
54
 
39
55
  /**
@@ -55,21 +71,22 @@ export function paintSurface(node: AnyNode, role: string, materialPreset: string
55
71
  export function FinishSwatch({
56
72
  label,
57
73
  node,
58
- role,
74
+ paintRole,
59
75
  value,
60
76
  fallback,
61
77
  hint,
62
78
  }: {
63
79
  label: string
64
80
  node: AnyNode
65
- /** The paint role of the surface (`edge`, `foundation`, …). */
66
- role: string
81
+ /** The paint role of the surface (`edge`, `foundation`, …); not an ARIA role. */
82
+ paintRole: string
67
83
  /** The authored finish reference; absent shows `fallback` as "Default". */
68
84
  value: string | undefined
69
85
  fallback: string
70
86
  /** Tooltip on the row. */
71
87
  hint?: string
72
88
  }) {
89
+ const i18n = usePascalI18n()
73
90
  const [open, setOpen] = useState(false)
74
91
  const swatch = useFinishSwatch(value, fallback)
75
92
  return (
@@ -78,7 +95,11 @@ export function FinishSwatch({
78
95
  <Popover onOpenChange={setOpen} open={open}>
79
96
  <PopoverTrigger asChild>
80
97
  <button
81
- aria-label={`${label}: ${swatch.label}`}
98
+ aria-label={i18n._({
99
+ id: 'editor.controls.finishSwatch.ariaLabel',
100
+ message: '{label}: {finish}',
101
+ values: { label, finish: swatch.label },
102
+ })}
82
103
  className="flex min-w-0 items-center gap-2 rounded-full border border-border/50 py-0.5 pr-2.5 pl-1 text-xs transition-colors hover:bg-accent"
83
104
  data-finish-swatch={label}
84
105
  type="button"
@@ -86,18 +107,23 @@ export function FinishSwatch({
86
107
  <span
87
108
  aria-hidden
88
109
  className="size-4 shrink-0 overflow-hidden rounded-full border border-border/50"
89
- style={{ backgroundColor: swatch.color ?? 'transparent' }}
90
- >
91
- {swatch.image && <img alt="" className="size-full object-cover" src={swatch.image} />}
92
- </span>
110
+ style={materialSwatchStyle({
111
+ color: swatch.color ?? 'transparent',
112
+ imageUrl: swatch.image,
113
+ tinted: swatch.tinted,
114
+ })}
115
+ />
93
116
  <span className="truncate">{swatch.label}</span>
94
117
  </button>
95
118
  </PopoverTrigger>
96
119
  <PopoverContent align="end" className="flex h-96 w-80 flex-col p-2" side="left">
97
120
  <MaterialPicker
98
121
  onSelectMaterialPreset={(ref) => {
99
- const live = useScene.getState().nodes[node.id as keyof ReturnType<typeof useScene.getState>['nodes']]
100
- if (live) paintSurface(live, role, ref)
122
+ const live =
123
+ useScene.getState().nodes[
124
+ node.id as keyof ReturnType<typeof useScene.getState>['nodes']
125
+ ]
126
+ if (live) paintSurface(live, paintRole, ref)
101
127
  setOpen(false)
102
128
  }}
103
129
  selectOnCategoryChange={false}
@@ -1,8 +1,9 @@
1
1
  'use client'
2
2
 
3
- import { type MaterialTarget, toLibraryMaterialRef } from '@pascal-app/core'
3
+ import { getMaterialSwatch, type MaterialTarget, toLibraryMaterialRef } from '@pascal-app/core'
4
4
  import { Plus } from 'lucide-react'
5
- import { useMaterialCatalogModel, type MaterialSourceFilter } from '../../../lib/material-catalog-model'
5
+ import { useMaterialCatalogModel } from '../../../lib/material-catalog-model'
6
+ import { materialSwatchStyle } from '../../../lib/material-swatch-style'
6
7
  import { triggerSFX } from '../../../lib/sfx-bus'
7
8
 
8
9
  export type { MaterialSourceFilter } from '../../../lib/material-catalog-model'
@@ -35,7 +36,21 @@ export function MaterialPicker({
35
36
  onCreateMaterialRequest,
36
37
  selectOnCategoryChange = true,
37
38
  }: MaterialPickerProps) {
38
- const { selectedCategory, setSelectedCategory, sourceFilter, setSourceFilter, visibleSourceFilters, availableCategories, catalogItems, select: handleCatalogSelect } = useMaterialCatalogModel(selectedMaterialPreset, onSelectMaterialPreset, disabled, selectOnCategoryChange)
39
+ const {
40
+ selectedCategory,
41
+ setSelectedCategory,
42
+ sourceFilter,
43
+ setSourceFilter,
44
+ visibleSourceFilters,
45
+ availableCategories,
46
+ catalogItems,
47
+ select: handleCatalogSelect,
48
+ } = useMaterialCatalogModel(
49
+ selectedMaterialPreset,
50
+ onSelectMaterialPreset,
51
+ disabled,
52
+ selectOnCategoryChange,
53
+ )
39
54
 
40
55
  return (
41
56
  <div
@@ -121,20 +136,12 @@ export function MaterialPicker({
121
136
  onMouseEnter={() => triggerSFX('sfx:menu-hover')}
122
137
  type="button"
123
138
  >
124
- <div className="relative aspect-square w-full overflow-hidden rounded-lg">
125
- {item.previewThumbnailUrl ? (
126
- <img
127
- alt={item.label}
128
- className="h-full w-full object-cover"
129
- src={item.previewThumbnailUrl}
130
- />
131
- ) : (
132
- <div
133
- className="h-full w-full"
134
- style={{ backgroundColor: item.previewColor ?? '#f3f4f6' }}
135
- />
136
- )}
137
- </div>
139
+ <div
140
+ aria-label={item.label}
141
+ className="relative aspect-square w-full overflow-hidden rounded-lg"
142
+ role="img"
143
+ style={materialSwatchStyle(getMaterialSwatch(item))}
144
+ />
138
145
  <span className="truncate px-0.5 text-left font-medium text-[11px] text-muted-foreground group-hover:text-foreground">
139
146
  {item.label}
140
147
  </span>
@@ -0,0 +1,52 @@
1
+ import { expect, test } from 'bun:test'
2
+ import {
3
+ formatMetricInputValue,
4
+ parseMetricInput,
5
+ roundMetricValueForDisplay,
6
+ } from './metric-control'
7
+
8
+ test('metric controls parse stair dimensions with the scoped locale', () => {
9
+ expect(
10
+ parseMetricInput('1,000', 'm', {
11
+ isImperial: false,
12
+ locale: 'fr',
13
+ parseUnit: 'm',
14
+ }),
15
+ ).toBe(1)
16
+ expect(
17
+ parseMetricInput('1,000', 'm', {
18
+ isImperial: false,
19
+ locale: 'en',
20
+ parseUnit: 'm',
21
+ }),
22
+ ).toBe(1000)
23
+ })
24
+
25
+ test('metric controls write values back with the scoped decimal separator', () => {
26
+ expect(formatMetricInputValue(2.75, 2, 'fr')).toBe('2,75')
27
+ expect(formatMetricInputValue(2.75, 2, 'en')).toBe('2.75')
28
+ })
29
+
30
+ test('metric write-back rounds negative halves away from zero like the display', () => {
31
+ expect(roundMetricValueForDisplay(-1.25, 1)).toBe(-1.3)
32
+ expect(formatMetricInputValue(-1.25, 1, 'en')).toBe('-1.3')
33
+ expect(roundMetricValueForDisplay(1.25, 1)).toBe(1.3)
34
+ expect(formatMetricInputValue(1.25, 1, 'en')).toBe('1.3')
35
+ })
36
+
37
+ test('metric write-back and display agree for values immediately below a half', () => {
38
+ const positive = 1.2499999999999998
39
+ const negative = -positive
40
+
41
+ expect(roundMetricValueForDisplay(negative, 1)).toBe(-1.2)
42
+ expect(formatMetricInputValue(negative, 1, 'en')).toBe('-1.2')
43
+ expect(roundMetricValueForDisplay(positive, 1)).toBe(1.2)
44
+ expect(formatMetricInputValue(positive, 1, 'en')).toBe('1.2')
45
+ })
46
+
47
+ test('metric write-back preserves Intl rounding for binary-inexact decimal halves', () => {
48
+ expect(roundMetricValueForDisplay(-1.005, 2)).toBe(-1.01)
49
+ expect(formatMetricInputValue(-1.005, 2, 'en')).toBe('-1.01')
50
+ expect(roundMetricValueForDisplay(1.005, 2)).toBe(1.01)
51
+ expect(formatMetricInputValue(1.005, 2, 'en')).toBe('1.01')
52
+ })