@pascal-app/editor 1.0.1 → 1.0.4

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 (498) hide show
  1. package/package.json +16 -9
  2. package/src/components/editor/action-menu-button.tsx +83 -0
  3. package/src/components/editor/bake-thumbnail.tsx +315 -4
  4. package/src/components/editor/capture-level-picker.tsx +72 -0
  5. package/src/components/editor/custom-camera-controls.tsx +221 -128
  6. package/src/components/editor/delete-confirmation-dialog.tsx +297 -7
  7. package/src/components/editor/duplicate-node.ts +244 -0
  8. package/src/components/editor/editor-layout-mobile.tsx +37 -25
  9. package/src/components/editor/editor-layout-v2.tsx +131 -58
  10. package/src/components/editor/export-manager.tsx +34 -8
  11. package/src/components/editor/first-person/build-collider-world.test.ts +51 -0
  12. package/src/components/editor/first-person/build-collider-world.ts +6 -2
  13. package/src/components/editor/first-person/mechanism-targets.test.ts +75 -0
  14. package/src/components/editor/first-person/mechanism-targets.ts +36 -0
  15. package/src/components/editor/first-person-controls.tsx +205 -2
  16. package/src/components/editor/floating-action-menu.tsx +63 -207
  17. package/src/components/editor/floor-aware-camera-controls.test.ts +442 -0
  18. package/src/components/editor/floor-aware-camera-controls.ts +324 -0
  19. package/src/components/editor/floor-openings-overlay.tsx +243 -0
  20. package/src/components/editor/floor-region-controls.tsx +846 -0
  21. package/src/components/editor/floor-region-webgpu-materials.test.ts +12 -0
  22. package/src/components/editor/floor-surface.test.ts +165 -0
  23. package/src/components/editor/floor-surface.ts +182 -0
  24. package/src/components/editor/floorplan-background-selection.test.ts +11 -2
  25. package/src/components/editor/floorplan-background-selection.ts +1 -3
  26. package/src/components/editor/floorplan-panel.tsx +536 -487
  27. package/src/components/editor/footprint-height-handle.tsx +204 -0
  28. package/src/components/editor/grid.tsx +31 -7
  29. package/src/components/editor/group-actions.ts +59 -11
  30. package/src/components/editor/group-floating-action-menu.tsx +19 -9
  31. package/src/components/editor/group-move-3d.ts +4 -6
  32. package/src/components/editor/group-rotate-handle.tsx +81 -35
  33. package/src/components/editor/group-selection-box-3d.tsx +25 -17
  34. package/src/components/editor/group-transform-shared.test.ts +126 -1
  35. package/src/components/editor/group-transform-shared.ts +79 -73
  36. package/src/components/editor/handles/handle-arrow.tsx +72 -2
  37. package/src/components/editor/handles/handle-hit-priority.test.tsx +63 -0
  38. package/src/components/editor/handles/handle-hit-priority.tsx +37 -0
  39. package/src/components/editor/handles/swallow-next-click.test.ts +28 -0
  40. package/src/components/editor/handles/use-handle-drag.ts +272 -131
  41. package/src/components/editor/index.tsx +316 -176
  42. package/src/components/editor/join-walls-pill.tsx +44 -0
  43. package/src/components/editor/node-action-menu.tsx +70 -70
  44. package/src/components/editor/node-arrow-handles.tsx +167 -14
  45. package/src/components/editor/open-wall-ends-3d-layer.tsx +273 -0
  46. package/src/components/editor/open-wall-ends-hint.tsx +89 -0
  47. package/src/components/editor/quick-measurement-hud.tsx +3 -2
  48. package/src/components/editor/registry-action-contributions.test.tsx +63 -0
  49. package/src/components/editor/registry-action-contributions.tsx +132 -0
  50. package/src/components/editor/room-controls.tsx +847 -0
  51. package/src/components/editor/room-floor-highlight.tsx +128 -0
  52. package/src/components/editor/room-handles-2d.tsx +354 -0
  53. package/src/components/editor/room-handles.tsx +675 -0
  54. package/src/components/editor/room-highlight.tsx +225 -0
  55. package/src/components/editor/room-labels.test.ts +357 -0
  56. package/src/components/editor/room-labels.tsx +990 -0
  57. package/src/components/editor/room-transform-ghost.tsx +256 -0
  58. package/src/components/editor/room-webgpu-materials.test.ts +27 -0
  59. package/src/components/editor/selection-manager.tsx +451 -317
  60. package/src/components/editor/slab-hole-highlights.tsx +3 -0
  61. package/src/components/editor/snapshot-capture-overlay.tsx +4 -0
  62. package/src/components/editor/snapshot-capture.test.ts +52 -4
  63. package/src/components/editor/snapshot-capture.ts +30 -5
  64. package/src/components/editor/thumbnail-generator.test.ts +308 -0
  65. package/src/components/editor/thumbnail-generator.tsx +1035 -229
  66. package/src/components/editor/use-floorplan-background-placement.ts +31 -11
  67. package/src/components/editor/vector-edge-extractor.tsx +121 -0
  68. package/src/components/editor/views/host-view.tsx +61 -0
  69. package/src/components/editor/views/plugins-view.tsx +670 -0
  70. package/src/components/editor/views/scene-region.tsx +59 -0
  71. package/src/components/editor/views/use-editor-views.ts +57 -0
  72. package/src/components/editor/views/view-bar.tsx +220 -0
  73. package/src/components/editor/wall-justification.test.ts +140 -0
  74. package/src/components/editor/wall-move-side-handles.tsx +114 -163
  75. package/src/components/editor/wall-opening-highlights.tsx +2 -3
  76. package/src/components/editor/wall-push-arrow.tsx +92 -0
  77. package/src/components/editor/wall-region-handles.tsx +509 -0
  78. package/src/components/editor/wall-snap-beacon-layer.tsx +3 -0
  79. package/src/components/editor-2d/floorplan-cursor-indicator-overlay.tsx +8 -4
  80. package/src/components/editor-2d/floorplan-group-move.tsx +17 -24
  81. package/src/components/editor-2d/floorplan-open-wall-ends-layer.test.tsx +86 -0
  82. package/src/components/editor-2d/floorplan-open-wall-ends-layer.tsx +290 -0
  83. package/src/components/editor-2d/floorplan-registered-tool-layer.tsx +33 -12
  84. package/src/components/editor-2d/floorplan-registry-action-menu.tsx +11 -1
  85. package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +242 -126
  86. package/src/components/editor-2d/renderers/floorplan-dimension-edit-overlay.test.ts +156 -0
  87. package/src/components/editor-2d/renderers/floorplan-dimension-edit-overlay.tsx +278 -0
  88. package/src/components/editor-2d/renderers/floorplan-dimension-renderer.tsx +77 -0
  89. package/src/components/editor-2d/renderers/floorplan-draft-measurement.tsx +166 -0
  90. package/src/components/editor-2d/renderers/floorplan-geometry-renderer.tsx +4 -1
  91. package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +87 -8
  92. package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +355 -245
  93. package/src/components/editor-2d/svg-paths.ts +12 -15
  94. package/src/components/procedural-items/inspector.tsx +40 -15
  95. package/src/components/systems/ceiling/ceiling-selection-affordance-system.test.ts +97 -14
  96. package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +18 -18
  97. package/src/components/systems/zone/zone-system.test.ts +47 -0
  98. package/src/components/systems/zone/zone-system.tsx +31 -66
  99. package/src/components/tools/__fixtures__/hosting.ts +10 -0
  100. package/src/components/tools/item/item-surface-placement.test.ts +3 -9
  101. package/src/components/tools/item/item-surface-strategy-resolver.test.ts +3 -9
  102. package/src/components/tools/item/placement-history.test.tsx +474 -0
  103. package/src/components/tools/item/placement-strategies.ts +63 -47
  104. package/src/components/tools/item/shelf-surface-strategy-resolver.test.ts +3 -3
  105. package/src/components/tools/item/use-draft-node.test.tsx +432 -1
  106. package/src/components/tools/item/use-draft-node.ts +207 -97
  107. package/src/components/tools/item/use-placement-coordinator.tsx +251 -111
  108. package/src/components/tools/paint-region/use-paint-tint.ts +37 -0
  109. package/src/components/tools/paint-region/wall-paint-region-tool.tsx +466 -0
  110. package/src/components/tools/registry/item-surface-move.test.ts +10 -2
  111. package/src/components/tools/registry/item-surface-move.ts +46 -24
  112. package/src/components/tools/registry/item-surface-resolver.test.ts +2 -2
  113. package/src/components/tools/registry/move-registry-node-tool.tsx +26 -10
  114. package/src/components/tools/select/box-select-tool.tsx +2 -12
  115. package/src/components/tools/select/plane-box-select-tool.test.tsx +79 -0
  116. package/src/components/tools/select/plane-box-select-tool.tsx +35 -22
  117. package/src/components/tools/select/select-candidates.test.ts +37 -4
  118. package/src/components/tools/select/select-candidates.ts +15 -11
  119. package/src/components/tools/shared/affordance-dispatch.test.ts +24 -0
  120. package/src/components/tools/shared/affordance-dispatch.ts +13 -0
  121. package/src/components/tools/shared/cursor-sphere.tsx +36 -12
  122. package/src/components/tools/shared/draft-measurement-label.tsx +38 -0
  123. package/src/components/tools/shared/pointer-support-cap.test.ts +27 -0
  124. package/src/components/tools/shared/pointer-support-cap.ts +17 -5
  125. package/src/components/tools/shared/surface-rejection.test.tsx +5 -4
  126. package/src/components/tools/shared/surface-rejection.tsx +14 -0
  127. package/src/components/tools/site/site-boundary-editor.tsx +1 -2
  128. package/src/components/tools/stair/stair-defaults.ts +0 -3
  129. package/src/components/tools/stair/stair-tool.tsx +90 -42
  130. package/src/components/tools/tool-manager.tsx +82 -23
  131. package/src/components/tools/wall/wall-drafting.test.ts +19 -4
  132. package/src/components/tools/zone/zone-tool.tsx +4 -12
  133. package/src/components/ui/action-menu/action-button.tsx +3 -0
  134. package/src/components/ui/action-menu/camera-actions.tsx +2 -1
  135. package/src/components/ui/action-menu/control-modes.tsx +2 -35
  136. package/src/components/ui/action-menu/measurement-control.tsx +3 -10
  137. package/src/components/ui/build-panel/build-panel.tsx +235 -0
  138. package/src/components/ui/command-palette/editor-commands.tsx +4 -30
  139. package/src/components/ui/command-palette/index.tsx +5 -24
  140. package/src/components/ui/controls/finish-swatch.tsx +110 -0
  141. package/src/components/ui/controls/material-paint-panel.tsx +101 -91
  142. package/src/components/ui/controls/material-picker.tsx +8 -67
  143. package/src/components/ui/controls/scene-material-list.tsx +4 -32
  144. package/src/components/ui/controls/select-control.tsx +65 -0
  145. package/src/components/ui/controls/slider-control.tsx +76 -31
  146. package/src/components/ui/floating-level-selector.tsx +9 -14
  147. package/src/components/ui/helpers/building-helper.tsx +4 -1
  148. package/src/components/ui/helpers/contextual-helper-panel.tsx +154 -70
  149. package/src/components/ui/helpers/helper-manager.test.tsx +69 -0
  150. package/src/components/ui/helpers/helper-manager.tsx +348 -19
  151. package/src/components/ui/helpers/item-helper.tsx +8 -0
  152. package/src/components/ui/helpers/registered-tool-helper.tsx +10 -33
  153. package/src/components/ui/icon-ref.tsx +45 -1
  154. package/src/components/ui/item-catalog/item-catalog.tsx +5 -30
  155. package/src/components/ui/level-duplicate-dialog.tsx +1 -1
  156. package/src/components/ui/node-icon-image.tsx +16 -0
  157. package/src/components/ui/panels/collections/collections-popover.tsx +20 -14
  158. package/src/components/ui/panels/floor-foundation-panel.tsx +551 -0
  159. package/src/components/ui/panels/floor-opening-panel.tsx +143 -0
  160. package/src/components/ui/panels/mobile-panel-sheet.tsx +2 -2
  161. package/src/components/ui/panels/mobile-selection-bar.tsx +2 -2
  162. package/src/components/ui/panels/multi-field-value.test.ts +26 -3
  163. package/src/components/ui/panels/multi-field-value.ts +6 -3
  164. package/src/components/ui/panels/multi-height-mode.tsx +27 -21
  165. package/src/components/ui/panels/multi-parametric-inspector.tsx +52 -2
  166. package/src/components/ui/panels/multi-selection-panel.tsx +43 -4
  167. package/src/components/ui/panels/node-display.ts +8 -3
  168. package/src/components/ui/panels/panel-manager.tsx +103 -68
  169. package/src/components/ui/panels/panel-wrapper.tsx +78 -36
  170. package/src/components/ui/panels/parametric-field-control.tsx +1 -1
  171. package/src/components/ui/panels/parametric-inspector.tsx +7 -19
  172. package/src/components/ui/panels/reference-panel.tsx +2 -3
  173. package/src/components/ui/panels/room-construction-rows.tsx +774 -0
  174. package/src/components/ui/panels/room-name-options.test.ts +55 -0
  175. package/src/components/ui/panels/room-name-options.ts +47 -0
  176. package/src/components/ui/panels/room-openings-field.tsx +110 -0
  177. package/src/components/ui/panels/room-panel.tsx +725 -0
  178. package/src/components/ui/panels/shape-choice.tsx +58 -0
  179. package/src/components/ui/panels/wall-region-list.tsx +109 -0
  180. package/src/components/ui/primitives/color-dot.tsx +36 -24
  181. package/src/components/ui/primitives/shortcut-token.tsx +4 -0
  182. package/src/components/ui/primitives/sidebar.tsx +2 -0
  183. package/src/components/ui/right-stack.test.ts +29 -0
  184. package/src/components/ui/right-stack.tsx +145 -0
  185. package/src/components/ui/sidebar/icon-rail.tsx +5 -0
  186. package/src/components/ui/sidebar/panels/settings-panel/audio-settings-dialog.tsx +2 -2
  187. package/src/components/ui/sidebar/panels/settings-panel/index.tsx +615 -372
  188. package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +7 -7
  189. package/src/components/ui/sidebar/panels/settings-panel/print-export-button.tsx +1 -1
  190. package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +4 -4
  191. package/src/components/ui/sidebar/panels/site-panel/chimney-tree-node.tsx +1 -5
  192. package/src/components/ui/sidebar/panels/site-panel/collection-tree-node.tsx +197 -0
  193. package/src/components/ui/sidebar/panels/site-panel/column-tree-node.tsx +1 -5
  194. package/src/components/ui/sidebar/panels/site-panel/door-tree-node.tsx +1 -5
  195. package/src/components/ui/sidebar/panels/site-panel/dormer-tree-node.tsx +1 -5
  196. package/src/components/ui/sidebar/panels/site-panel/elevator-tree-node.tsx +1 -5
  197. package/src/components/ui/sidebar/panels/site-panel/fence-tree-node.tsx +24 -11
  198. package/src/components/ui/sidebar/panels/site-panel/gutter-tree-node.tsx +1 -5
  199. package/src/components/ui/sidebar/panels/site-panel/index.tsx +176 -211
  200. package/src/components/ui/sidebar/panels/site-panel/inline-rename-input.tsx +30 -8
  201. package/src/components/ui/sidebar/panels/site-panel/item-tree-node.tsx +1 -3
  202. package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +4 -1
  203. package/src/components/ui/sidebar/panels/site-panel/registry-tree-node.tsx +1 -3
  204. package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +1 -4
  205. package/src/components/ui/sidebar/panels/site-panel/shelf-tree-node.tsx +1 -3
  206. package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +21 -13
  207. package/src/components/ui/sidebar/panels/site-panel/solar-panel-tree-node.tsx +1 -5
  208. package/src/components/ui/sidebar/panels/site-panel/spawn-tree-node.tsx +1 -5
  209. package/src/components/ui/sidebar/panels/site-panel/stair-tree-node.tsx +1 -5
  210. package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +0 -27
  211. package/src/components/ui/sidebar/panels/site-panel/unit-tree-node.tsx +2 -3
  212. package/src/components/ui/sidebar/panels/site-panel/wall-tree-node.tsx +1 -5
  213. package/src/components/ui/sidebar/panels/site-panel/window-tree-node.tsx +1 -5
  214. package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +9 -4
  215. package/src/components/ui/sidebar/panels/zone-panel/index.tsx +0 -2
  216. package/src/components/ui/sidebar/tab-bar.tsx +10 -0
  217. package/src/components/ui/sidebar/use-plugin-panels.tsx +15 -5
  218. package/src/components/viewer/floorplan-compass-button.tsx +9 -1
  219. package/src/components/viewer/floorplan-preview-visibility.test.ts +35 -0
  220. package/src/components/viewer/floorplan-preview-visibility.ts +19 -0
  221. package/src/components/viewer/floorplan-preview.tsx +6 -18
  222. package/src/components/viewer/viewer-hover-label.tsx +34 -0
  223. package/src/components/viewer/viewer-rooms.tsx +175 -0
  224. package/src/components/viewer/viewer-scene-header.tsx +181 -145
  225. package/src/components/viewer/viewer-selection-manager.tsx +124 -0
  226. package/src/components/viewer/viewer-stage.tsx +2 -2
  227. package/src/components/viewer-zone-system.tsx +9 -9
  228. package/src/hooks/use-auto-save.integration.test.tsx +311 -0
  229. package/src/hooks/use-auto-save.ts +55 -14
  230. package/src/hooks/use-ceiling-events.test.ts +1 -0
  231. package/src/hooks/use-ceiling-events.ts +2 -0
  232. package/src/hooks/use-grid-events.test.ts +2 -0
  233. package/src/hooks/use-grid-events.ts +16 -11
  234. package/src/hooks/use-keyboard.test.ts +182 -0
  235. package/src/hooks/use-keyboard.ts +122 -91
  236. package/src/hooks/use-open-wall-ends.test.ts +99 -0
  237. package/src/hooks/use-open-wall-ends.ts +238 -0
  238. package/src/hooks/use-placement-notice.test.ts +48 -0
  239. package/src/hooks/use-placement-notice.ts +42 -0
  240. package/src/hooks/use-selected-room.ts +143 -0
  241. package/src/index.tsx +306 -39
  242. package/src/lib/catalog-panel-model.ts +39 -0
  243. package/src/lib/catalog-selection.test.ts +27 -0
  244. package/src/lib/catalog-selection.ts +8 -0
  245. package/src/lib/ceiling-edit-session.test.ts +321 -0
  246. package/src/lib/ceiling-edit-session.ts +206 -0
  247. package/src/lib/ceiling-region-session.test.ts +179 -0
  248. package/src/lib/contextual-help.test.ts +27 -1
  249. package/src/lib/contextual-help.ts +16 -9
  250. package/src/lib/continuation.test.ts +19 -1
  251. package/src/lib/continuation.ts +25 -4
  252. package/src/lib/door-interaction.ts +14 -0
  253. package/src/lib/editor-level-display.test.ts +96 -0
  254. package/src/lib/editor-level-display.ts +48 -0
  255. package/src/lib/editor-views.ts +144 -0
  256. package/src/lib/export-geometry-wait.test.ts +90 -0
  257. package/src/lib/floor-edit-session.ts +153 -0
  258. package/src/lib/floor-footprints.test.ts +563 -0
  259. package/src/lib/floor-footprints.ts +395 -0
  260. package/src/lib/floor-foundation-support.test.ts +283 -0
  261. package/src/lib/floor-opening-draft.ts +218 -0
  262. package/src/lib/floor-openings.test.ts +216 -0
  263. package/src/lib/floor-region-geometry.ts +191 -0
  264. package/src/lib/floor-region-session.test.ts +485 -0
  265. package/src/lib/floor-region-session.ts +589 -0
  266. package/src/lib/floor-region-snap.ts +101 -0
  267. package/src/lib/floor-step-hover.test.ts +32 -0
  268. package/src/lib/floorplan/annotation-visibility.ts +17 -0
  269. package/src/lib/floorplan/dimension-drive.test.ts +360 -0
  270. package/src/lib/floorplan/dimension-drive.ts +479 -0
  271. package/src/lib/floorplan/floorplan-export.test.ts +15 -9
  272. package/src/lib/floorplan/floorplan-export.tsx +32 -8
  273. package/src/lib/floorplan/floorplan-extension.ts +48 -0
  274. package/src/lib/floorplan/floorplan-mode.test.ts +4 -0
  275. package/src/lib/floorplan/floorplan-mode.ts +2 -0
  276. package/src/lib/floorplan/floorplan-pdfkit-document.ts +339 -18
  277. package/src/lib/floorplan/floorplan-pdfkit-renderer.test.ts +138 -8
  278. package/src/lib/floorplan/floorplan-pdfkit-renderer.ts +56 -25
  279. package/src/lib/floorplan/floorplan-plugin-install.test.tsx +138 -0
  280. package/src/lib/floorplan/floorplan-readonly.ts +6 -0
  281. package/src/lib/floorplan/geometry.ts +13 -6
  282. package/src/lib/floorplan/index.ts +0 -9
  283. package/src/lib/floorplan/items.ts +5 -2
  284. package/src/lib/floorplan/marks.test.ts +213 -0
  285. package/src/lib/floorplan/marks.ts +238 -0
  286. package/src/lib/floorplan/open-wall-ends.test.ts +268 -0
  287. package/src/lib/floorplan/open-wall-ends.ts +162 -0
  288. package/src/lib/floorplan/pdf-fonts/GeistMono-Bold.ttf +0 -0
  289. package/src/lib/floorplan/pdf-fonts/GeistMono-Regular.ttf +0 -0
  290. package/src/lib/floorplan/pdf-fonts/LICENSE-GeistMono.txt +92 -0
  291. package/src/lib/floorplan/pdf-fonts/LICENSE-LiberationSans.txt +102 -0
  292. package/src/lib/floorplan/pdf-fonts/LiberationSans-Bold.ttf +0 -0
  293. package/src/lib/floorplan/pdf-fonts/LiberationSans-Regular.ttf +0 -0
  294. package/src/lib/floorplan/schedules.test.ts +189 -0
  295. package/src/lib/floorplan/schedules.ts +339 -0
  296. package/src/lib/floorplan/sheet-export.test.ts +100 -0
  297. package/src/lib/floorplan/sheet-export.ts +318 -0
  298. package/src/lib/floorplan/site-plan/build-site-plan-drawing.ts +779 -0
  299. package/src/lib/floorplan/site-plan/contours.test.ts +43 -0
  300. package/src/lib/floorplan/site-plan/contributors.ts +97 -0
  301. package/src/lib/floorplan/site-plan/coverage.ts +232 -0
  302. package/src/lib/floorplan/site-plan/drawing-type-switch.test.ts +15 -0
  303. package/src/lib/floorplan/site-plan/drawing-type-switch.tsx +86 -0
  304. package/src/lib/floorplan/site-plan/front-edge.test.ts +103 -0
  305. package/src/lib/floorplan/site-plan/front-edge.ts +156 -0
  306. package/src/lib/floorplan/site-plan/geometry.test.ts +275 -0
  307. package/src/lib/floorplan/site-plan/geometry.ts +557 -0
  308. package/src/lib/floorplan/site-plan/index.ts +71 -0
  309. package/src/lib/floorplan/site-plan/site-annotations.test.ts +366 -0
  310. package/src/lib/floorplan/site-plan/site-annotations.ts +1083 -0
  311. package/src/lib/floorplan/site-plan/site-parts.ts +379 -0
  312. package/src/lib/floorplan/site-plan/site-plan-layer.tsx +138 -0
  313. package/src/lib/floorplan/site-plan/street-name.test.ts +69 -0
  314. package/src/lib/floorplan/site-plan/street-name.ts +302 -0
  315. package/src/lib/floorplan/site-plan/yards.test.ts +69 -0
  316. package/src/lib/floorplan/types.ts +1 -27
  317. package/src/lib/fresh-planar-placement.test.ts +36 -0
  318. package/src/lib/fresh-planar-placement.ts +191 -36
  319. package/src/lib/geometry-script/author.ts +152 -0
  320. package/src/lib/geometry-script/client.ts +152 -0
  321. package/src/lib/geometry-script/protocol.ts +12 -0
  322. package/src/lib/gesture-lifecycle.test.ts +595 -0
  323. package/src/lib/gesture-lifecycle.ts +290 -0
  324. package/src/lib/glb-export-zone-holes.test.ts +41 -0
  325. package/src/lib/glb-export.test.ts +544 -16
  326. package/src/lib/glb-export.ts +256 -125
  327. package/src/lib/grid-frame.test.ts +19 -0
  328. package/src/lib/grid-frame.ts +18 -0
  329. package/src/lib/history.test.ts +6 -0
  330. package/src/lib/history.ts +7 -0
  331. package/src/lib/hud-title.ts +109 -0
  332. package/src/lib/hydration-gated-storage.ts +49 -0
  333. package/src/lib/ifc-export.test.ts +174 -0
  334. package/src/lib/ifc-export.ts +201 -0
  335. package/src/lib/ifc-parts.ts +15 -0
  336. package/src/lib/inspector-expanded.test.ts +44 -0
  337. package/src/lib/inspector-expanded.ts +36 -0
  338. package/src/lib/interaction/scope.ts +27 -1
  339. package/src/lib/keyboard-pan.test.ts +184 -0
  340. package/src/lib/keyboard-pan.ts +238 -0
  341. package/src/lib/live-gesture-round-trip.test.ts +360 -0
  342. package/src/lib/lot/__fixtures__/map-dossier-miami-shores.json +1 -0
  343. package/src/lib/lot/__fixtures__/map-dossier-stpete.json +1 -0
  344. package/src/lib/lot/clean-ring.test.ts +186 -0
  345. package/src/lib/lot/clean-ring.ts +217 -0
  346. package/src/lib/lot/dossier.test.ts +202 -0
  347. package/src/lib/lot/dossier.ts +498 -0
  348. package/src/lib/lot/drop-in.test.ts +326 -0
  349. package/src/lib/lot/drop-in.ts +407 -0
  350. package/src/lib/lot/index.ts +43 -0
  351. package/src/lib/lot/lot-patch.test.ts +340 -0
  352. package/src/lib/lot/lot-patch.ts +394 -0
  353. package/src/lib/lot/parcel-provider.ts +43 -0
  354. package/src/lib/lot/terrain.test.ts +161 -0
  355. package/src/lib/lot/terrain.ts +266 -0
  356. package/src/lib/material-catalog-model.ts +74 -0
  357. package/src/lib/material-paint-panel-model.ts +80 -0
  358. package/src/lib/material-paint.ts +4 -0
  359. package/src/lib/mezzanine-actions.test.ts +342 -0
  360. package/src/lib/mezzanine-draft.test.ts +501 -0
  361. package/src/lib/mezzanine-draft.ts +278 -0
  362. package/src/lib/mezzanine-messages.ts +26 -0
  363. package/src/lib/mezzanine-selection.test.ts +207 -0
  364. package/src/lib/mezzanine-stairs.ts +46 -0
  365. package/src/lib/model-export.ts +3 -1
  366. package/src/lib/node-action-movement.ts +17 -6
  367. package/src/lib/node-display-label.ts +37 -0
  368. package/src/lib/orbit-floor-clearance.test.ts +165 -0
  369. package/src/lib/orbit-floor-clearance.ts +106 -0
  370. package/src/lib/overlay-transform.test.ts +28 -0
  371. package/src/lib/overlay-transform.ts +12 -0
  372. package/src/lib/paint-commit-route.test.ts +37 -0
  373. package/src/lib/paint-commit-route.ts +17 -0
  374. package/src/lib/paint-eyedropper.test.tsx +165 -0
  375. package/src/lib/paint-eyedropper.ts +99 -0
  376. package/src/lib/paint-pass-through.test.ts +111 -0
  377. package/src/lib/paint-pass-through.ts +76 -0
  378. package/src/lib/paint-region-hover.ts +25 -0
  379. package/src/lib/paint-region-mode.ts +154 -0
  380. package/src/lib/paint-regions.test.ts +166 -0
  381. package/src/lib/paint-regions.ts +192 -0
  382. package/src/lib/paint-scope.test.ts +317 -35
  383. package/src/lib/paint-scope.ts +266 -25
  384. package/src/lib/panel-rows.ts +96 -0
  385. package/src/lib/panel-tool-options.test.ts +34 -0
  386. package/src/lib/panel-tool-options.ts +46 -0
  387. package/src/lib/parametric-node-update.ts +37 -0
  388. package/src/lib/plate-paint-affected.test.ts +300 -0
  389. package/src/lib/plate-paint-affected.ts +177 -0
  390. package/src/lib/plugin-chat.ts +50 -0
  391. package/src/lib/plugin-panels.test.ts +25 -0
  392. package/src/lib/plugin-panels.ts +94 -0
  393. package/src/lib/print-shell-compiler.ts +1 -0
  394. package/src/lib/registered-draft-snap.ts +12 -0
  395. package/src/lib/roof-duplication.ts +10 -2
  396. package/src/lib/room-assembly-overlay.test.ts +275 -0
  397. package/src/lib/room-assembly-overlay.ts +266 -0
  398. package/src/lib/room-built-on.test.ts +379 -0
  399. package/src/lib/room-built-on.ts +138 -0
  400. package/src/lib/room-construction-commands.ts +712 -0
  401. package/src/lib/room-construction.test.ts +746 -0
  402. package/src/lib/room-construction.ts +385 -0
  403. package/src/lib/room-controls-phase6.test.ts +630 -0
  404. package/src/lib/room-divide-session.ts +523 -0
  405. package/src/lib/room-handle-drag.ts +1068 -0
  406. package/src/lib/room-name-catalog.test.ts +90 -0
  407. package/src/lib/room-name-catalog.ts +168 -0
  408. package/src/lib/room-push-dimensions.test.ts +311 -0
  409. package/src/lib/room-push-dimensions.ts +251 -0
  410. package/src/lib/room-selection-commands.ts +62 -0
  411. package/src/lib/room-selection.test.ts +973 -0
  412. package/src/lib/room-selection.ts +417 -0
  413. package/src/lib/room-structure-commands.ts +94 -0
  414. package/src/lib/room-structure.test.ts +921 -0
  415. package/src/lib/room-transform-session.ts +570 -0
  416. package/src/lib/room-zone-routing.test.ts +294 -0
  417. package/src/lib/room-zone-routing.ts +197 -0
  418. package/src/lib/scene-clipboard.test.ts +364 -0
  419. package/src/lib/scene-clipboard.ts +229 -48
  420. package/src/lib/scene-material-refs.test.ts +79 -0
  421. package/src/lib/scene-material-refs.ts +79 -0
  422. package/src/lib/scene.test.ts +87 -0
  423. package/src/lib/scene.ts +39 -45
  424. package/src/lib/selection-routing.test.ts +129 -45
  425. package/src/lib/selection-routing.ts +53 -74
  426. package/src/lib/session-writes.test.ts +120 -0
  427. package/src/lib/session-writes.ts +205 -0
  428. package/src/lib/sidebar-panel.ts +62 -0
  429. package/src/lib/site-boundary.test.ts +4 -6
  430. package/src/lib/site-boundary.ts +1 -1
  431. package/src/lib/snapping-mode.test.ts +17 -2
  432. package/src/lib/snapping-mode.ts +19 -7
  433. package/src/lib/spatial-drag-plane.test.ts +53 -0
  434. package/src/lib/spatial-drag-plane.ts +19 -0
  435. package/src/lib/spatial-pointer-input.test.ts +72 -0
  436. package/src/lib/spatial-pointer-input.ts +62 -0
  437. package/src/lib/spatial-snap.test.ts +47 -0
  438. package/src/lib/terrace-draft.test.ts +145 -0
  439. package/src/lib/terrace-draft.ts +213 -0
  440. package/src/lib/terrain-panel-model.ts +117 -0
  441. package/src/lib/tool-cursor-icon.test.ts +16 -0
  442. package/src/lib/tool-cursor-icon.ts +11 -0
  443. package/src/lib/typing-target.ts +9 -0
  444. package/src/lib/units.ts +77 -39
  445. package/src/lib/vector-edges.test.ts +58 -0
  446. package/src/lib/vector-edges.ts +400 -0
  447. package/src/lib/viewer-selection.test.ts +577 -0
  448. package/src/lib/viewer-selection.ts +569 -0
  449. package/src/lib/wall-draw-variant.test.ts +71 -0
  450. package/src/lib/wall-draw-variant.ts +91 -0
  451. package/src/lib/wall-paint-region-session.ts +246 -0
  452. package/src/lib/wall-paint-region.test.ts +373 -0
  453. package/src/lib/wall-polygon-draft.test.ts +262 -0
  454. package/src/lib/wall-polygon-draft.ts +286 -0
  455. package/src/lib/wall-push.test.ts +453 -0
  456. package/src/lib/wall-region-face.ts +352 -0
  457. package/src/lib/wall-region-handles.test.ts +304 -0
  458. package/src/lib/wall-region-handles.ts +368 -0
  459. package/src/lib/wall-region-list.test.ts +119 -0
  460. package/src/lib/wall-region-list.ts +136 -0
  461. package/src/lib/wall-region-snap.ts +110 -0
  462. package/src/lib/window-interaction.ts +14 -0
  463. package/src/lib/world-grid-snap.test.ts +48 -0
  464. package/src/lib/world-grid-snap.ts +9 -5
  465. package/src/lib/zone-content.ts +4 -1
  466. package/src/lib/zone-name.test.ts +17 -0
  467. package/src/lib/zone-name.ts +17 -0
  468. package/src/store/opens-in-select-mode.test.ts +223 -0
  469. package/src/store/paint-mode.test.ts +188 -0
  470. package/src/store/rotation-snapping.test.ts +53 -0
  471. package/src/store/terrain-sculpt-mode.test.ts +19 -21
  472. package/src/store/tool-mode.test.ts +137 -6
  473. package/src/store/use-delete-confirmation.ts +28 -1
  474. package/src/store/use-drawing-view.test.ts +24 -3
  475. package/src/store/use-drawing-view.ts +29 -1
  476. package/src/store/use-editor.tsx +442 -323
  477. package/src/store/use-floorplan-draft-preview.ts +23 -1
  478. package/src/store/use-floorplan-marquee.ts +12 -2
  479. package/src/store/use-interaction-scope.ts +155 -6
  480. package/src/store/use-quick-measurement-hud.ts +3 -3
  481. package/src/store/use-stair-build-preview.ts +13 -5
  482. package/src/store/use-wall-move-ghosts.ts +4 -2
  483. package/src/store/use-wall-region-handles-selection.test.ts +222 -0
  484. package/src/store/use-wall-region-handles-selection.ts +139 -0
  485. package/src/store/view-layout.test.ts +175 -0
  486. package/src/store/view-layout.ts +196 -0
  487. package/src/test-utils/immediate-animation-frames.ts +16 -0
  488. package/src/test-utils/live-gesture-round-trip.ts +224 -0
  489. package/src/test-utils/selection-harness.ts +82 -0
  490. package/src/components/editor-2d/renderers/floorplan-stair-layer.tsx +0 -499
  491. package/src/components/systems/zone/zone-label-editor-system.tsx +0 -333
  492. package/src/components/tools/fence/fence-drafting.test.ts +0 -100
  493. package/src/components/tools/fence/fence-drafting.ts +0 -266
  494. package/src/components/ui/sidebar/panels/plugins-panel.tsx +0 -218
  495. package/src/components/viewer/viewer-units-panel.tsx +0 -116
  496. package/src/lib/floorplan/stairs.ts +0 -489
  497. package/src/lib/level-duplication.test.ts +0 -144
  498. package/src/lib/level-duplication.ts +0 -195
@@ -1,30 +1,22 @@
1
1
  'use client'
2
2
 
3
- import {
4
- type AnyNodeId,
5
- generateSceneMaterialId,
6
- type SceneMaterialId,
7
- toSceneMaterialRef,
8
- useScene,
9
- } from '@pascal-app/core'
10
- import { useViewer } from '@pascal-app/viewer'
11
- import { Eraser, Plus, RotateCcw } from 'lucide-react'
12
- import { useEffect, useState } from 'react'
13
- import {
14
- buildResetSurfaceMaterialUpdates,
15
- resolvePaintTargetFromSelection,
16
- } from './../../../lib/material-paint'
17
- import useEditor from './../../../store/use-editor'
3
+ import { type SceneMaterialId } from '@pascal-app/core'
4
+ import { Eraser, PaintBucket, Pentagon, Pipette, Plus, Square } from 'lucide-react'
5
+ import { useState } from 'react'
6
+ import { useMaterialPaintPanelModel } from '../../../lib/material-paint-panel-model'
7
+ import { type PaintMode, usePaintRegionMode } from '../../../lib/paint-region-mode'
8
+ import useEditor, { armMaterialPaint } from '../../../store/use-editor'
18
9
  import { Button } from '../primitives/button'
19
10
  import { Tooltip, TooltipContent, TooltipTrigger } from '../primitives/tooltip'
20
11
  import { MaterialPicker } from './material-picker'
21
12
  import { SceneMaterialList } from './scene-material-list'
13
+ import { SegmentedControl } from './segmented-control'
22
14
 
23
15
  /**
24
16
  * Material picker for paint mode. Embedders render this wherever paint controls
25
17
  * belong (the community editor places it in the Build sidebar while paint mode
26
18
  * is active). It fills its container's height and lays out as three bands: a
27
- * fixed control/category header, a single scrolling catalog grid, and a fixed
19
+ * fixed mode header, a single scrolling catalog grid, and a fixed
28
20
  * scene-material footer (always visible, with a `+` to add a custom material).
29
21
  */
30
22
  export type MaterialPaintPanelProps = {
@@ -32,94 +24,110 @@ export type MaterialPaintPanelProps = {
32
24
  onCreateMaterialRequest?: () => void
33
25
  }
34
26
 
35
- export function MaterialPaintPanel({ onCreateMaterialRequest }: MaterialPaintPanelProps) {
36
- const activePaintMaterial = useEditor((state) => state.activePaintMaterial)
37
- const activePaintTarget = useEditor((state) => state.activePaintTarget)
38
- const armMaterialPaint = useEditor((state) => state.armMaterialPaint)
39
- const setActivePaintTarget = useEditor((state) => state.setActivePaintTarget)
40
- const paintEraser = useEditor((state) => state.paintEraser)
41
- const setPaintEraser = useEditor((state) => state.setPaintEraser)
42
- // Id of a just-created scene material whose inline editor should open on mount.
43
- const [autoEditMaterialId, setAutoEditMaterialId] = useState<SceneMaterialId | null>(null)
44
- const selectedIds = useViewer((state) => state.selection.selectedIds)
45
- const nodes = useScene((state) => state.nodes)
46
- const materialCount = useScene((state) => Object.keys(state.materials).length)
47
- const selectedId = selectedIds.length === 1 ? (selectedIds[0] ?? null) : null
48
- const selectedNode = selectedId ? nodes[selectedId as AnyNodeId] : null
49
- const canResetSelection =
50
- selectedNode != null && resolvePaintTargetFromSelection({ nodes, selectedId }) != null
27
+ const PAINT_MODE_OPTIONS: { value: PaintMode; label: string; icon: typeof PaintBucket }[] = [
28
+ { value: 'surface', label: 'Whole surface: a click paints the surface under it', icon: PaintBucket },
29
+ {
30
+ value: 'rectangle',
31
+ label: 'Rectangle: press anywhere on a wall, floor or ceiling, drag to the opposite corner',
32
+ icon: Square,
33
+ },
34
+ {
35
+ value: 'polygon',
36
+ label: 'Polygon: click the points of a shape on a floor or ceiling',
37
+ icon: Pentagon,
38
+ },
39
+ {
40
+ value: 'erase',
41
+ label: 'Erase: click a painted part to remove it, or a painted surface to reset it',
42
+ icon: Eraser,
43
+ },
44
+ {
45
+ value: 'pick',
46
+ label: 'Pick: click a surface to paint with its material (or hold Alt / Option)',
47
+ icon: Pipette,
48
+ },
49
+ ]
51
50
 
52
- useEffect(() => {
53
- const selectedPaintTarget = resolvePaintTargetFromSelection({ nodes, selectedId })
54
- if (selectedPaintTarget) {
55
- setActivePaintTarget(selectedPaintTarget)
56
- }
57
- }, [nodes, selectedId, setActivePaintTarget])
58
-
59
- const resetSelection = () => {
60
- if (!selectedNode) return
61
- useScene.getState().updateNodes(buildResetSurfaceMaterialUpdates(nodes, selectedNode))
51
+ /**
52
+ * Paint the whole surface, draw part of a wall, floor or ceiling first, erase, or
53
+ * pick a material off a surface. Erase
54
+ * is the eraser the paint path already knows (a click writes "no finish"), so a
55
+ * painted region goes and a whole-surface paint returns to its default. The row
56
+ * shows a choice only while paint mode is on; any pick (erase included) turns it
57
+ * on, so nothing here needs a colour first.
58
+ */
59
+ function PaintModeControl() {
60
+ const painting = useEditor((s) => s.mode === 'material-paint')
61
+ const mode = usePaintRegionMode((s) => s.mode)
62
+ const select = (next: PaintMode) => {
63
+ if (painting) usePaintRegionMode.getState().setMode(next)
64
+ else armMaterialPaint(undefined, next)
62
65
  }
66
+ return (
67
+ <div
68
+ className="shrink-0 space-y-1.5 pb-2"
69
+ data-paint-mode={painting ? mode : 'off'}
70
+ // A clicked option must not keep focus: the next shortcut (V) would light
71
+ // its focus ring and it would read as still chosen after paint mode ends.
72
+ onPointerUp={(event) => (event.target as HTMLElement).closest('button')?.blur()}
73
+ >
74
+ <div className="px-0.5 font-medium text-muted-foreground text-xs">
75
+ {painting && mode === 'erase' ? 'Erase' : painting && mode === 'pick' ? 'Pick material' : 'Paint'}
76
+ </div>
77
+ <SegmentedControl
78
+ mixed={!painting}
79
+ onChange={select}
80
+ options={PAINT_MODE_OPTIONS.map(({ value, label, icon: Icon }) => ({
81
+ value,
82
+ label: (
83
+ <Tooltip>
84
+ <TooltipTrigger asChild>
85
+ <span aria-label={label} className="flex items-center justify-center">
86
+ <Icon className="h-3.5 w-3.5" />
87
+ </span>
88
+ </TooltipTrigger>
89
+ <TooltipContent>{label}</TooltipContent>
90
+ </Tooltip>
91
+ ),
92
+ }))}
93
+ value={mode}
94
+ />
95
+ </div>
96
+ )
97
+ }
63
98
 
64
- // Create a blank custom scene material, select it as the brush (`scene:` ref so
65
- // edits propagate), and open its inline editor. Available from any category.
66
- const createCustomMaterial = () => {
67
- const id = generateSceneMaterialId()
68
- const count = Object.keys(useScene.getState().materials).length
69
- useScene.getState().addSceneMaterial({
70
- id,
71
- name: `Material ${count + 1}`,
72
- material: {
73
- preset: 'custom',
74
- properties: {
75
- color: '#ffffff',
76
- roughness: 0.5,
77
- metalness: 0,
78
- opacity: 1,
79
- transparent: false,
80
- side: 'front',
81
- },
82
- },
83
- })
84
- armMaterialPaint({ materialPreset: toSceneMaterialRef(id), sourceTarget: activePaintTarget })
85
- setAutoEditMaterialId(id)
86
- }
99
+ export function MaterialPaintPanel({ onCreateMaterialRequest }: MaterialPaintPanelProps) {
100
+ const painting = useEditor((s) => s.mode === 'material-paint')
101
+ const erasing = usePaintRegionMode((s) => s.mode) === 'erase' && painting
102
+ const {
103
+ activePaintMaterial,
104
+ materialCount,
105
+ selectMaterial,
106
+ createCustomMaterial: createMaterial,
107
+ } = useMaterialPaintPanelModel()
108
+ const [autoEditMaterialId, setAutoEditMaterialId] = useState<SceneMaterialId | null>(null)
109
+ const createCustomMaterial = () => setAutoEditMaterialId(createMaterial())
87
110
 
88
111
  return (
89
112
  <div className="flex h-full min-h-0 w-full flex-col">
90
- {/* Fixed: eraser / reset. */}
91
- <div className="flex shrink-0 items-center gap-2 pb-2">
92
- <Button
93
- aria-pressed={paintEraser}
94
- className="flex-1"
95
- onClick={() => setPaintEraser(!paintEraser)}
96
- size="sm"
97
- variant={paintEraser ? 'default' : 'outline'}
98
- >
99
- <Eraser />
100
- Erase
101
- </Button>
102
- <Button
103
- className="flex-1"
104
- disabled={!canResetSelection}
105
- onClick={resetSelection}
106
- size="sm"
107
- variant="outline"
108
- >
109
- <RotateCcw />
110
- Reset all
111
- </Button>
112
- </div>
113
+ {/* Fixed: whole surface, part of a surface, or erase. */}
114
+ <PaintModeControl />
113
115
 
116
+ {/* Erasing paints nothing: no swatches to pick from, just what a click does. */}
117
+ {erasing ? (
118
+ <p className="px-0.5 py-1 text-muted-foreground text-xs" data-paint-erase-note>
119
+ Click a painted part of a wall, floor or ceiling to remove it, or a painted surface to reset it to
120
+ its default.
121
+ </p>
122
+ ) : (
123
+ <>
114
124
  {/* Scrolls: category tabs (fixed inside) + catalog grid (the scroll). */}
115
125
  {/* A stable hook for host-app onboarding to point at. Static, and read
116
126
  only from outside: nothing here depends on it. */}
117
127
  <div className="min-h-0 flex-1" data-guide-target="paint-material">
118
128
  <MaterialPicker
119
129
  onCreateMaterialRequest={onCreateMaterialRequest}
120
- onSelectMaterialPreset={(materialPreset) => {
121
- armMaterialPaint({ materialPreset, sourceTarget: activePaintTarget })
122
- }}
130
+ onSelectMaterialPreset={selectMaterial}
123
131
  selectedMaterialPreset={activePaintMaterial?.materialPreset}
124
132
  />
125
133
  </div>
@@ -155,6 +163,8 @@ export function MaterialPaintPanel({ onCreateMaterialRequest }: MaterialPaintPan
155
163
  )}
156
164
  </div>
157
165
  </div>
166
+ </>
167
+ )}
158
168
  </div>
159
169
  )
160
170
  }
@@ -1,23 +1,11 @@
1
1
  'use client'
2
2
 
3
- import {
4
- getCatalogMaterialById,
5
- getDynamicLibraryMaterials,
6
- getLibraryMaterialIdFromRef,
7
- getLibraryMaterialsVersion,
8
- getMaterialsForCategory,
9
- MATERIAL_CATEGORIES,
10
- type MaterialCatalogItem,
11
- type MaterialSource,
12
- type MaterialTarget,
13
- subscribeLibraryMaterials,
14
- toLibraryMaterialRef,
15
- } from '@pascal-app/core'
3
+ import { type MaterialTarget, toLibraryMaterialRef } from '@pascal-app/core'
16
4
  import { Plus } from 'lucide-react'
17
- import { useEffect, useMemo, useState, useSyncExternalStore } from 'react'
5
+ import { useMaterialCatalogModel, type MaterialSourceFilter } from '../../../lib/material-catalog-model'
18
6
  import { triggerSFX } from '../../../lib/sfx-bus'
19
7
 
20
- export type MaterialSourceFilter = MaterialSource
8
+ export type { MaterialSourceFilter } from '../../../lib/material-catalog-model'
21
9
 
22
10
  export type MaterialPickerProps = {
23
11
  selectedMaterialPreset?: string
@@ -26,25 +14,14 @@ export type MaterialPickerProps = {
26
14
  nodeType?: MaterialTarget
27
15
  hideSideControl?: boolean
28
16
  onCreateMaterialRequest?: () => void
17
+ /** Whether switching category tabs selects the tab's first material (paint mode does). */
18
+ selectOnCategoryChange?: boolean
29
19
  }
30
20
 
31
- // No 'All': the browse surfaces (Items / Rooms / Build) dropped it and default
32
- // to the Pascal library — the combined list buried the curated set.
33
- const SOURCE_FILTERS: { id: MaterialSourceFilter; label: string }[] = [
34
- { id: 'pascal', label: 'Pascal' },
35
- { id: 'mine', label: 'Mine' },
36
- { id: 'workspace', label: 'Workspace' },
37
- { id: 'community', label: 'Community' },
38
- ]
39
-
40
- function getCategoryLabel(category: (typeof MATERIAL_CATEGORIES)[number]) {
21
+ function getCategoryLabel(category: string) {
41
22
  return category.charAt(0).toUpperCase() + category.slice(1)
42
23
  }
43
24
 
44
- function filterBySource(items: MaterialCatalogItem[], filter: MaterialSourceFilter) {
45
- return items.filter((item) => (item.source ?? 'pascal') === filter)
46
- }
47
-
48
25
  /**
49
26
  * Catalog material picker: a fixed row of category tabs and a source filter row
50
27
  * over a scrollable grid of swatches. Scene-material creation lives in the
@@ -56,41 +33,9 @@ export function MaterialPicker({
56
33
  onSelectMaterialPreset,
57
34
  disabled = false,
58
35
  onCreateMaterialRequest,
36
+ selectOnCategoryChange = true,
59
37
  }: MaterialPickerProps) {
60
- const [selectedCategory, setSelectedCategory] = useState<(typeof MATERIAL_CATEGORIES)[number]>(
61
- MATERIAL_CATEGORIES[0],
62
- )
63
- const [sourceFilter, setSourceFilter] = useState<MaterialSourceFilter>('pascal')
64
- // Version counter so host registrations/unregistrations re-render the picker.
65
- const libraryVersion = useSyncExternalStore(
66
- subscribeLibraryMaterials,
67
- getLibraryMaterialsVersion,
68
- getLibraryMaterialsVersion,
69
- )
70
- const hasWorkspaceMaterials = useMemo(
71
- () => getDynamicLibraryMaterials().some((item) => item.source === 'workspace'),
72
- [libraryVersion],
73
- )
74
- const visibleSourceFilters = SOURCE_FILTERS.filter(
75
- (filter) => filter.id !== 'workspace' || hasWorkspaceMaterials,
76
- )
77
- const availableCategories = MATERIAL_CATEGORIES.filter(
78
- (category) => getMaterialsForCategory(category).length > 0,
79
- )
80
- const catalogItems = filterBySource(getMaterialsForCategory(selectedCategory), sourceFilter)
81
-
82
- // Keep the visible category in sync with the externally-selected catalog
83
- // material (a `scene:` ref matches no catalog entry, so the tab stays put).
84
- useEffect(() => {
85
- const catalogId = getLibraryMaterialIdFromRef(selectedMaterialPreset) ?? undefined
86
- const entry = getCatalogMaterialById(catalogId)
87
- if (entry?.category) setSelectedCategory(entry.category)
88
- }, [selectedMaterialPreset])
89
-
90
- const handleCatalogSelect = (materialId: string) => {
91
- if (disabled) return
92
- onSelectMaterialPreset?.(toLibraryMaterialRef(materialId))
93
- }
38
+ const { selectedCategory, setSelectedCategory, sourceFilter, setSourceFilter, visibleSourceFilters, availableCategories, catalogItems, select: handleCatalogSelect } = useMaterialCatalogModel(selectedMaterialPreset, onSelectMaterialPreset, disabled, selectOnCategoryChange)
94
39
 
95
40
  return (
96
41
  <div
@@ -108,10 +53,6 @@ export function MaterialPicker({
108
53
  key={category}
109
54
  onClick={() => {
110
55
  setSelectedCategory(category)
111
- // Auto-select the first material in the category so the brush is
112
- // immediately ready (and the swatch shows as selected).
113
- const first = filterBySource(getMaterialsForCategory(category), sourceFilter)[0]
114
- if (first) handleCatalogSelect(first.id)
115
56
  }}
116
57
  type="button"
117
58
  >
@@ -10,21 +10,13 @@ import {
10
10
  } from '@pascal-app/core'
11
11
  import { Copy, Paintbrush, Pencil, Trash2 } from 'lucide-react'
12
12
  import { useEffect, useMemo, useState } from 'react'
13
+ import { sceneMaterialUsageCounts } from '../../../lib/scene-material-refs'
13
14
  import useEditor from '../../../store/use-editor'
14
15
  import { Button } from '../primitives/button'
15
16
  import { Input } from '../primitives/input'
16
17
  import { Tooltip, TooltipContent, TooltipTrigger } from '../primitives/tooltip'
17
18
  import { MaterialPropertiesEditor } from './material-properties-editor'
18
19
 
19
- type SlotRecord = Record<string, string | undefined>
20
-
21
- function getSlotRecord(node: unknown): SlotRecord | null {
22
- if (!node || typeof node !== 'object' || !('slots' in node)) return null
23
- const slots = (node as { slots?: unknown }).slots
24
- if (!slots || typeof slots !== 'object' || Array.isArray(slots)) return null
25
- return slots as SlotRecord
26
- }
27
-
28
20
  export function SceneMaterialList({ autoEditId }: { autoEditId?: SceneMaterialId | null }) {
29
21
  const materials = useScene((state) => state.materials)
30
22
  const nodes = useScene((state) => state.nodes)
@@ -40,29 +32,9 @@ export function SceneMaterialList({ autoEditId }: { autoEditId?: SceneMaterialId
40
32
  [materials],
41
33
  )
42
34
 
43
- const usageCounts = useMemo(() => {
44
- const counts = new Map<SceneMaterialId, number>()
45
- const refToId = new Map<string, SceneMaterialId>()
46
-
47
- for (const [id] of materialEntries) {
48
- counts.set(id, 0)
49
- refToId.set(toSceneMaterialRef(id), id)
50
- }
51
-
52
- for (const node of Object.values(nodes)) {
53
- const slots = getSlotRecord(node)
54
- if (!slots) continue
55
-
56
- for (const value of Object.values(slots)) {
57
- if (typeof value !== 'string') continue
58
- const materialId = refToId.get(value)
59
- if (!materialId) continue
60
- counts.set(materialId, (counts.get(materialId) ?? 0) + 1)
61
- }
62
- }
63
-
64
- return counts
65
- }, [materialEntries, nodes])
35
+ // Every place a scene material can be used — slots and the room, wall and
36
+ // footprint finishes — counted by the same walker saving and pasting use.
37
+ const usageCounts = useMemo(() => sceneMaterialUsageCounts(nodes), [nodes])
66
38
 
67
39
  return (
68
40
  <div className="space-y-2">
@@ -0,0 +1,65 @@
1
+ 'use client'
2
+
3
+ import { ChevronDown } from 'lucide-react'
4
+ import { cn } from '../../../lib/utils'
5
+ import {
6
+ DropdownMenu,
7
+ DropdownMenuContent,
8
+ DropdownMenuRadioGroup,
9
+ DropdownMenuRadioItem,
10
+ DropdownMenuTrigger,
11
+ } from '../primitives/dropdown-menu'
12
+
13
+ interface SelectControlProps<T extends string> {
14
+ label: string
15
+ value: T
16
+ onChange: (value: T) => void
17
+ options: { label: string; value: T }[]
18
+ className?: string
19
+ mixed?: boolean
20
+ }
21
+
22
+ export function SelectControl<T extends string>({
23
+ label,
24
+ value,
25
+ onChange,
26
+ options,
27
+ className,
28
+ mixed = false,
29
+ }: SelectControlProps<T>) {
30
+ const current = mixed ? 'Mixed' : options.find((option) => option.value === value)?.label
31
+ return (
32
+ <DropdownMenu>
33
+ <DropdownMenuTrigger asChild>
34
+ <button
35
+ aria-label={label}
36
+ className={cn(
37
+ 'group flex h-10 w-full items-center justify-between gap-2 rounded-lg border border-border/50 bg-[#2C2C2E] px-3 text-sm transition-colors hover:bg-[#3e3e3e] data-[state=open]:bg-[#3e3e3e]',
38
+ className,
39
+ )}
40
+ type="button"
41
+ >
42
+ <span className="select-none text-muted-foreground transition-colors group-hover:text-foreground">
43
+ {label}
44
+ </span>
45
+ <span className="flex min-w-0 items-center gap-1.5 text-foreground">
46
+ <span className="truncate">{current}</span>
47
+ <ChevronDown className="size-3.5 shrink-0 text-muted-foreground" />
48
+ </span>
49
+ </button>
50
+ </DropdownMenuTrigger>
51
+ <DropdownMenuContent align="end" className="min-w-(--radix-dropdown-menu-trigger-width)">
52
+ <DropdownMenuRadioGroup
53
+ onValueChange={(next) => onChange(next as T)}
54
+ value={mixed ? '' : value}
55
+ >
56
+ {options.map((option) => (
57
+ <DropdownMenuRadioItem key={option.value} value={option.value}>
58
+ {option.label}
59
+ </DropdownMenuRadioItem>
60
+ ))}
61
+ </DropdownMenuRadioGroup>
62
+ </DropdownMenuContent>
63
+ </DropdownMenu>
64
+ )
65
+ }
@@ -11,6 +11,8 @@ interface SliderControlProps {
11
11
  value: number
12
12
  onChange: (value: number) => void
13
13
  onCommit?: (value: number) => void
14
+ onCancel?: () => void
15
+ previewWhileTyping?: boolean
14
16
  min?: number
15
17
  max?: number
16
18
  precision?: number
@@ -54,6 +56,8 @@ export function SliderControl({
54
56
  value,
55
57
  onChange,
56
58
  onCommit,
59
+ onCancel,
60
+ previewWhileTyping = false,
57
61
  min = Number.NEGATIVE_INFINITY,
58
62
  max = Number.POSITIVE_INFINITY,
59
63
  precision: storedPrecision = 0,
@@ -87,6 +91,9 @@ export function SliderControl({
87
91
  anchorValue: number
88
92
  stepMultiplier: number
89
93
  } | null>(null)
94
+ const cancelRef = useRef(onCancel)
95
+ cancelRef.current = onCancel
96
+ const editingRef = useRef(false)
90
97
  const labelRef = useRef<HTMLDivElement>(null)
91
98
  const shown = dragDisplay ?? value
92
99
  const valueRef = useRef(shown)
@@ -217,45 +224,74 @@ export function SliderControl({
217
224
  [onChange, onCommit, restoreOnCommit],
218
225
  )
219
226
 
227
+ const cancelEdit = useCallback(() => {
228
+ const drag = dragRef.current
229
+ dragRef.current = null
230
+ editingRef.current = false
231
+ if (drag) {
232
+ if (!onCancel) onChange(drag.originValue)
233
+ useScene.temporal.getState().resume()
234
+ }
235
+ onCancel?.()
236
+ setIsDragging(false)
237
+ setIsEditing(false)
238
+ setDragDisplay(null)
239
+ }, [onCancel, onChange])
240
+
241
+ useEffect(
242
+ () => () => {
243
+ if (dragRef.current) useScene.temporal.getState().resume()
244
+ if (dragRef.current || editingRef.current) cancelRef.current?.()
245
+ },
246
+ [],
247
+ )
248
+
249
+ useEffect(() => {
250
+ if (!isDragging) return
251
+ const escape = (event: KeyboardEvent) => {
252
+ if (event.key === 'Escape') cancelEdit()
253
+ }
254
+ window.addEventListener('keydown', escape)
255
+ return () => window.removeEventListener('keydown', escape)
256
+ }, [isDragging, cancelEdit])
257
+
220
258
  const handleValueClick = useCallback(() => {
259
+ editingRef.current = true
221
260
  setIsEditing(true)
222
261
  setInputValue(toDisplay(value).toFixed(precision))
223
262
  }, [value, precision, toDisplay])
224
263
 
264
+ const parseInputValue = useCallback(
265
+ (text: string) => {
266
+ const spec = lingoUnitSpec(unit)
267
+ const parsed = spec
268
+ ? parseMeasurement(text, spec, {
269
+ bareUnit: parseUnit ?? spec.unitId,
270
+ system: isImperial ? 'us' : 'metric',
271
+ })
272
+ : null
273
+ const bare = Number(text)
274
+ const stored = parsed ?? (text.trim() && Number.isFinite(bare) ? toStored(bare) : null)
275
+ return stored === null
276
+ ? null
277
+ : clamp(toStored(Number.parseFloat(toDisplay(stored).toFixed(precision))))
278
+ },
279
+ [unit, parseUnit, isImperial, toStored, clamp, toDisplay, precision],
280
+ )
281
+
225
282
  const submitValue = useCallback(() => {
226
- const spec = lingoUnitSpec(unit)
227
- let stored = spec
228
- ? parseMeasurement(inputValue, spec, {
229
- bareUnit: parseUnit ?? spec.unitId,
230
- system: isImperial ? 'us' : 'metric',
231
- })
232
- : null
233
- if (stored === null) {
234
- // Fallback: a bare number typed in the DISPLAY unit → convert to stored.
235
- const numValue = Number.parseFloat(inputValue)
236
- stored = Number.isFinite(numValue) ? toStored(numValue) : null
237
- }
238
- if (stored === null) {
283
+ if (!editingRef.current) return
284
+ editingRef.current = false
285
+ const nextValue = parseInputValue(inputValue)
286
+ if (nextValue === null) {
287
+ onCancel?.()
239
288
  setInputValue(toDisplay(value).toFixed(precision))
240
289
  } else {
241
- const nextValue = clamp(toStored(Number.parseFloat(toDisplay(stored).toFixed(precision))))
242
290
  onChange(nextValue)
243
291
  onCommit?.(nextValue)
244
292
  }
245
293
  setIsEditing(false)
246
- }, [
247
- inputValue,
248
- unit,
249
- isImperial,
250
- parseUnit,
251
- onChange,
252
- onCommit,
253
- clamp,
254
- precision,
255
- value,
256
- toDisplay,
257
- toStored,
258
- ])
294
+ }, [inputValue, parseInputValue, onCancel, onChange, onCommit, toDisplay, value, precision])
259
295
 
260
296
  const spec = lingoUnitSpec(unit)
261
297
  const hint =
@@ -274,8 +310,7 @@ export function SliderControl({
274
310
  if (e.key === 'Enter') {
275
311
  submitValue()
276
312
  } else if (e.key === 'Escape') {
277
- setInputValue(toDisplay(value).toFixed(precision))
278
- setIsEditing(false)
313
+ cancelEdit()
279
314
  } else if (e.key === 'ArrowUp' || e.key === 'ArrowDown') {
280
315
  e.preventDefault()
281
316
  const direction = e.key === 'ArrowUp' ? 1 : -1
@@ -285,7 +320,7 @@ export function SliderControl({
285
320
  setInputValue(toDisplay(newV).toFixed(precision))
286
321
  }
287
322
  },
288
- [submitValue, value, precision, step, applyDisplayDelta, onChange, toDisplay],
323
+ [submitValue, cancelEdit, value, precision, step, applyDisplayDelta, onChange, toDisplay],
289
324
  )
290
325
 
291
326
  const displayValue = toDisplay(shown)
@@ -309,6 +344,10 @@ export function SliderControl({
309
344
  onPointerDown={handleLabelPointerDown}
310
345
  onPointerMove={handleLabelPointerMove}
311
346
  onPointerUp={handleLabelPointerUp}
347
+ onPointerCancel={cancelEdit}
348
+ onLostPointerCapture={() => {
349
+ if (dragRef.current) cancelEdit()
350
+ }}
312
351
  ref={labelRef}
313
352
  >
314
353
  {/* Grip dots — 2×3 grid */}
@@ -340,7 +379,13 @@ export function SliderControl({
340
379
  autoFocus
341
380
  className="w-14 bg-transparent p-0 text-right font-mono text-foreground outline-none selection:bg-primary/30"
342
381
  onBlur={submitValue}
343
- onChange={(e) => setInputValue(e.target.value)}
382
+ onChange={(e) => {
383
+ setInputValue(e.target.value)
384
+ if (previewWhileTyping) {
385
+ const next = parseInputValue(e.target.value)
386
+ if (next !== null) onChange(next)
387
+ }
388
+ }}
344
389
  onKeyDown={handleInputKeyDown}
345
390
  type="text"
346
391
  value={inputValue}