@pascal-app/editor 0.9.1 → 1.0.0-beta.1

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 (366) hide show
  1. package/package.json +14 -9
  2. package/src/components/editor/alignment-3d-guide-layer.tsx +10 -2
  3. package/src/components/editor/bake-exporter.tsx +49 -0
  4. package/src/components/editor/bake-thumbnail.tsx +95 -0
  5. package/src/components/editor/camera-dragging-lifecycle.test.ts +49 -0
  6. package/src/components/editor/camera-dragging-lifecycle.ts +41 -0
  7. package/src/components/editor/custom-camera-controls.tsx +332 -199
  8. package/src/components/editor/delete-confirmation-dialog.tsx +54 -0
  9. package/src/components/editor/editor-layout-mobile.tsx +5 -0
  10. package/src/components/editor/editor-layout-v2.tsx +18 -0
  11. package/src/components/editor/elevation-3d-guide-layer.tsx +120 -0
  12. package/src/components/editor/export-manager.tsx +71 -63
  13. package/src/components/editor/fence-tangent-lines-3d.tsx +87 -0
  14. package/src/components/editor/first-person/build-collider-world.test.ts +102 -5
  15. package/src/components/editor/first-person/build-collider-world.ts +52 -5
  16. package/src/components/editor/first-person/terrain-collider.test.ts +132 -0
  17. package/src/components/editor/first-person/terrain-collider.ts +212 -0
  18. package/src/components/editor/first-person-controls.tsx +410 -136
  19. package/src/components/editor/floating-action-menu.tsx +435 -64
  20. package/src/components/editor/floating-building-action-menu.tsx +1 -1
  21. package/src/components/editor/floorplan-background-selection.test.ts +72 -0
  22. package/src/components/editor/floorplan-background-selection.ts +7 -20
  23. package/src/components/editor/floorplan-camera-sync.test.ts +249 -0
  24. package/src/components/editor/floorplan-camera-sync.ts +213 -0
  25. package/src/components/editor/floorplan-mode-coordinator.tsx +82 -0
  26. package/src/components/editor/floorplan-navigation-presentation.test.ts +142 -0
  27. package/src/components/editor/floorplan-navigation-presentation.ts +156 -0
  28. package/src/components/editor/floorplan-panel.tsx +2517 -1492
  29. package/src/components/editor/grid.tsx +166 -39
  30. package/src/components/editor/group-actions.ts +597 -0
  31. package/src/components/editor/group-floating-action-menu.tsx +130 -0
  32. package/src/components/editor/group-move-3d.ts +397 -0
  33. package/src/components/editor/group-rotate-handle.tsx +102 -64
  34. package/src/components/editor/group-selection-box-3d.tsx +173 -0
  35. package/src/components/editor/group-transform-shared.test.ts +192 -1
  36. package/src/components/editor/group-transform-shared.ts +240 -7
  37. package/src/components/editor/handles/handle-arrow.tsx +81 -23
  38. package/src/components/editor/handles/handle-drag-history.test.ts +96 -0
  39. package/src/components/editor/handles/handle-drag-history.ts +14 -0
  40. package/src/components/editor/handles/preview-overrides.test.ts +26 -0
  41. package/src/components/editor/handles/preview-overrides.ts +13 -0
  42. package/src/components/editor/handles/resize-snap.test.ts +52 -0
  43. package/src/components/editor/handles/resize-snap.ts +23 -0
  44. package/src/components/editor/handles/use-handle-drag.ts +49 -6
  45. package/src/components/editor/index.tsx +289 -35
  46. package/src/components/editor/measurement-pill.tsx +68 -23
  47. package/src/components/editor/node-action-menu.tsx +14 -1
  48. package/src/components/editor/node-arrow-handles.tsx +409 -180
  49. package/src/components/editor/opening-guides-3d-layer.tsx +144 -0
  50. package/src/components/editor/quick-measurement-card.tsx +76 -0
  51. package/src/components/editor/quick-measurement-hud.tsx +34 -0
  52. package/src/components/editor/riser-diagram-panel.tsx +137 -0
  53. package/src/components/editor/selection-manager.tsx +814 -466
  54. package/src/components/editor/site-edge-labels.tsx +28 -6
  55. package/src/components/editor/slab-hole-highlights.tsx +13 -5
  56. package/src/components/editor/snapshot-capture-overlay.tsx +258 -115
  57. package/src/components/editor/three-context-bridge.ts +22 -0
  58. package/src/components/editor/thumbnail-generator.tsx +118 -253
  59. package/src/components/editor/use-floorplan-background-placement.ts +113 -45
  60. package/src/components/editor/use-floorplan-hit-testing.test.ts +82 -0
  61. package/src/components/editor/use-floorplan-hit-testing.ts +144 -161
  62. package/src/components/editor/use-floorplan-scene-data.ts +5 -5
  63. package/src/components/editor/use-mesh-settle-epoch.ts +27 -0
  64. package/src/components/editor/wall-measurement-label.tsx +10 -28
  65. package/src/components/editor/wall-move-side-handles.tsx +229 -39
  66. package/src/components/editor/wall-opening-highlights.tsx +63 -32
  67. package/src/components/editor/wall-snap-beacon-layer.tsx +162 -6
  68. package/src/components/editor-2d/floorplan-action-menu-layer.tsx +9 -5
  69. package/src/components/editor-2d/floorplan-alignment-guide-layer.tsx +2 -1
  70. package/src/components/editor-2d/floorplan-cursor-indicator-overlay.tsx +40 -9
  71. package/src/components/editor-2d/floorplan-cursor-indicator-position.test.ts +31 -0
  72. package/src/components/editor-2d/floorplan-cursor-indicator-position.ts +39 -0
  73. package/src/components/editor-2d/floorplan-group-action-menu.tsx +87 -0
  74. package/src/components/editor-2d/floorplan-group-move.tsx +744 -0
  75. package/src/components/editor-2d/floorplan-measurement-tool-layer.test.ts +113 -0
  76. package/src/components/editor-2d/floorplan-measurement-tool-layer.tsx +1672 -0
  77. package/src/components/editor-2d/floorplan-quick-measure-layer.tsx +212 -0
  78. package/src/components/editor-2d/floorplan-registered-tool-layer.tsx +72 -0
  79. package/src/components/editor-2d/floorplan-registry-action-menu.tsx +219 -20
  80. package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +327 -59
  81. package/src/components/editor-2d/floorplan-render-context.test.ts +27 -0
  82. package/src/components/editor-2d/floorplan-render-context.tsx +113 -8
  83. package/src/components/editor-2d/floorplan-snap-beacon-layer.tsx +5 -3
  84. package/src/components/editor-2d/renderers/floorplan-annotation-layout.test.ts +285 -0
  85. package/src/components/editor-2d/renderers/floorplan-annotation-layout.ts +516 -0
  86. package/src/components/editor-2d/renderers/floorplan-dimension-renderer.test.tsx +236 -0
  87. package/src/components/editor-2d/renderers/floorplan-dimension-renderer.tsx +618 -0
  88. package/src/components/editor-2d/renderers/floorplan-geometry-renderer.test.tsx +369 -0
  89. package/src/components/editor-2d/renderers/floorplan-geometry-renderer.tsx +431 -32
  90. package/src/components/editor-2d/renderers/floorplan-label-angle.test.ts +89 -0
  91. package/src/components/editor-2d/renderers/floorplan-label-angle.ts +106 -0
  92. package/src/components/editor-2d/renderers/floorplan-placement-preview-layer.tsx +94 -26
  93. package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +587 -0
  94. package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +2280 -578
  95. package/src/components/editor-2d/renderers/floorplan-stair-layer.tsx +20 -10
  96. package/src/components/editor-2d/renderers/floorplan-voronoi-layer.tsx +128 -0
  97. package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +425 -61
  98. package/src/components/systems/ceiling/ceiling-system.tsx +97 -4
  99. package/src/components/systems/roof/roof-edit-system.tsx +1768 -13
  100. package/src/components/systems/selection-affordance-manager.tsx +38 -0
  101. package/src/components/systems/zone/zone-label-editor-system.tsx +14 -1
  102. package/src/components/systems/zone/zone-system.tsx +42 -13
  103. package/src/components/tools/elevator/elevator-tool.tsx +34 -8
  104. package/src/components/tools/elevator/move-elevator-tool.tsx +13 -2
  105. package/src/components/tools/fence/fence-drafting.ts +107 -8
  106. package/src/components/tools/item/move-tool.tsx +17 -13
  107. package/src/components/tools/item/placement-math.test.ts +13 -1
  108. package/src/components/tools/item/placement-math.ts +28 -2
  109. package/src/components/tools/item/placement-strategies.ts +246 -5
  110. package/src/components/tools/item/placement-types.ts +13 -1
  111. package/src/components/tools/item/use-draft-node.ts +146 -57
  112. package/src/components/tools/item/use-placement-coordinator.tsx +813 -125
  113. package/src/components/tools/registry/move-registry-node-tool.tsx +636 -103
  114. package/src/components/tools/roof/roof-tool.tsx +336 -94
  115. package/src/components/tools/select/box-select-state.ts +18 -7
  116. package/src/components/tools/select/box-select-tool.tsx +176 -40
  117. package/src/components/tools/select/marquee-geometry.test.ts +161 -0
  118. package/src/components/tools/select/marquee-geometry.ts +155 -0
  119. package/src/components/tools/select/plane-box-select-tool.tsx +1 -8
  120. package/src/components/tools/select/select-candidates.ts +1 -1
  121. package/src/components/tools/shared/cursor-sphere.tsx +62 -26
  122. package/src/components/tools/shared/drag-bounding-box.tsx +28 -4
  123. package/src/components/tools/shared/facing-indicator.tsx +87 -0
  124. package/src/components/tools/shared/facing-pose-indicator.tsx +54 -0
  125. package/src/components/tools/shared/floor-stack-preview.ts +4 -0
  126. package/src/components/tools/shared/horizontal-construction-plane.ts +123 -0
  127. package/src/components/tools/shared/placement-box.tsx +183 -1
  128. package/src/components/tools/shared/pointer-support-cap.test.ts +98 -0
  129. package/src/components/tools/shared/pointer-support-cap.ts +273 -0
  130. package/src/components/tools/shared/polygon-editor.tsx +276 -35
  131. package/src/components/tools/site/site-boundary-editor.tsx +101 -42
  132. package/src/components/tools/site/stroke-pointer-ownership.test.ts +91 -0
  133. package/src/components/tools/site/stroke-pointer-ownership.ts +57 -0
  134. package/src/components/tools/site/terrain-brush-cursor.tsx +247 -0
  135. package/src/components/tools/site/terrain-sculpt-grid.tsx +192 -0
  136. package/src/components/tools/site/terrain-sculpt-tool.tsx +356 -0
  137. package/src/components/tools/stair/stair-click-guard.test.ts +58 -0
  138. package/src/components/tools/stair/stair-click-guard.ts +72 -0
  139. package/src/components/tools/stair/stair-defaults.ts +1 -0
  140. package/src/components/tools/stair/stair-tool.tsx +164 -42
  141. package/src/components/tools/tool-manager.tsx +149 -30
  142. package/src/components/tools/wall/wall-drafting.test.ts +447 -0
  143. package/src/components/tools/wall/wall-drafting.ts +304 -108
  144. package/src/components/tools/wall/wall-snap-geometry.test.ts +52 -0
  145. package/src/components/tools/wall/wall-snap-geometry.ts +137 -9
  146. package/src/components/tools/zone/zone-boundary-editor.tsx +5 -1
  147. package/src/components/tools/zone/zone-tool.tsx +98 -88
  148. package/src/components/ui/action-menu/camera-actions.tsx +24 -17
  149. package/src/components/ui/action-menu/control-modes.tsx +44 -94
  150. package/src/components/ui/action-menu/furnish-tools.tsx +5 -5
  151. package/src/components/ui/action-menu/index.tsx +2 -4
  152. package/src/components/ui/action-menu/measurement-control.tsx +267 -0
  153. package/src/components/ui/action-menu/structure-tools.tsx +22 -13
  154. package/src/components/ui/action-menu/view-toggles.tsx +42 -90
  155. package/src/components/ui/command-palette/editor-commands.tsx +16 -3
  156. package/src/components/ui/command-palette/index.tsx +4 -3
  157. package/src/components/ui/controls/material-paint-panel.tsx +93 -18
  158. package/src/components/ui/controls/material-picker.tsx +162 -152
  159. package/src/components/ui/controls/material-properties-editor.tsx +108 -0
  160. package/src/components/ui/controls/metric-control.tsx +118 -44
  161. package/src/components/ui/controls/scene-material-list.tsx +247 -0
  162. package/src/components/ui/controls/slider-control.tsx +88 -38
  163. package/src/components/ui/controls/terrain-sculpt-panel.tsx +193 -0
  164. package/src/components/ui/floating-level-selector.tsx +74 -20
  165. package/src/components/ui/helpers/building-helper.tsx +10 -23
  166. package/src/components/ui/helpers/contextual-helper-panel.tsx +428 -0
  167. package/src/components/ui/helpers/helper-manager.tsx +273 -14
  168. package/src/components/ui/helpers/item-helper.tsx +28 -32
  169. package/src/components/ui/helpers/registered-tool-helper.tsx +45 -11
  170. package/src/components/ui/helpers/roof-helper.tsx +10 -12
  171. package/src/components/ui/icon-ref.tsx +47 -0
  172. package/src/components/ui/item-catalog/catalog-items.tsx +39 -1
  173. package/src/components/ui/item-catalog/item-catalog.tsx +13 -36
  174. package/src/components/ui/level-duplicate-dialog.tsx +1 -1
  175. package/src/components/ui/panels/multi-selection-panel.tsx +57 -0
  176. package/src/components/ui/panels/node-display.ts +17 -17
  177. package/src/components/ui/panels/panel-manager.tsx +36 -14
  178. package/src/components/ui/panels/panel-wrapper.tsx +24 -3
  179. package/src/components/ui/panels/parametric-inspector.tsx +19 -4
  180. package/src/components/ui/panels/reference-panel.tsx +54 -19
  181. package/src/components/ui/panels/selection-breakdown.test.ts +24 -0
  182. package/src/components/ui/panels/selection-breakdown.ts +20 -0
  183. package/src/components/ui/primitives/shortcut-token.tsx +39 -3
  184. package/src/components/ui/primitives/sidebar.tsx +1 -0
  185. package/src/components/ui/sidebar/app-sidebar.tsx +5 -1
  186. package/src/components/ui/sidebar/icon-rail.tsx +50 -31
  187. package/src/components/ui/sidebar/panels/plugins-panel.tsx +218 -0
  188. package/src/components/ui/sidebar/panels/settings-panel/index.tsx +82 -41
  189. package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +76 -6
  190. package/src/components/ui/sidebar/panels/settings-panel/load-build-dialog.tsx +12 -4
  191. package/src/components/ui/sidebar/panels/site-panel/building-tree-node.tsx +2 -1
  192. package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +4 -3
  193. package/src/components/ui/sidebar/panels/site-panel/chimney-tree-node.tsx +10 -7
  194. package/src/components/ui/sidebar/panels/site-panel/column-tree-node.tsx +1 -1
  195. package/src/components/ui/sidebar/panels/site-panel/door-tree-node.tsx +7 -2
  196. package/src/components/ui/sidebar/panels/site-panel/dormer-tree-node.tsx +10 -7
  197. package/src/components/ui/sidebar/panels/site-panel/elevator-tree-node.tsx +1 -1
  198. package/src/components/ui/sidebar/panels/site-panel/fence-tree-node.tsx +1 -1
  199. package/src/components/ui/sidebar/panels/site-panel/gutter-tree-node.tsx +10 -7
  200. package/src/components/ui/sidebar/panels/site-panel/index.tsx +42 -17
  201. package/src/components/ui/sidebar/panels/site-panel/item-tree-node.tsx +31 -17
  202. package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +1 -1
  203. package/src/components/ui/sidebar/panels/site-panel/registry-tree-node.tsx +72 -25
  204. package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +7 -3
  205. package/src/components/ui/sidebar/panels/site-panel/shelf-tree-node.tsx +12 -8
  206. package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +4 -3
  207. package/src/components/ui/sidebar/panels/site-panel/solar-panel-tree-node.tsx +10 -7
  208. package/src/components/ui/sidebar/panels/site-panel/spawn-tree-node.tsx +1 -1
  209. package/src/components/ui/sidebar/panels/site-panel/stair-tree-node.tsx +2 -2
  210. package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +44 -3
  211. package/src/components/ui/sidebar/panels/site-panel/tree-structure.ts +36 -0
  212. package/src/components/ui/sidebar/panels/site-panel/wall-tree-node.tsx +1 -1
  213. package/src/components/ui/sidebar/panels/site-panel/window-tree-node.tsx +7 -2
  214. package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +3 -3
  215. package/src/components/ui/sidebar/tab-bar.tsx +42 -28
  216. package/src/components/ui/sidebar/use-plugin-panels.tsx +132 -0
  217. package/src/components/ui/snap-target-badge.test.tsx +40 -0
  218. package/src/components/ui/snap-target-badge.tsx +88 -0
  219. package/src/components/viewer/viewer-controls-bar.tsx +466 -0
  220. package/src/components/viewer/viewer-scene-header.tsx +235 -0
  221. package/src/components/viewer-overlay.tsx +36 -596
  222. package/src/components/viewer-zone-system.tsx +6 -1
  223. package/src/components/walkthrough-hud.tsx +111 -0
  224. package/src/hooks/use-auto-save.test.ts +14 -0
  225. package/src/hooks/use-auto-save.ts +54 -7
  226. package/src/hooks/use-ceiling-events.ts +3 -2
  227. package/src/hooks/use-drag-action.ts +4 -1
  228. package/src/hooks/use-grid-events.ts +24 -0
  229. package/src/hooks/use-keyboard.ts +438 -70
  230. package/src/index.tsx +355 -9
  231. package/src/lib/active-placement-surface.test.ts +24 -0
  232. package/src/lib/active-placement-surface.ts +42 -0
  233. package/src/lib/camera-pose.test.ts +237 -0
  234. package/src/lib/camera-pose.ts +172 -0
  235. package/src/lib/ceiling-plan-snap.ts +24 -0
  236. package/src/lib/contextual-help.test.ts +112 -0
  237. package/src/lib/contextual-help.ts +144 -0
  238. package/src/lib/continuation.ts +64 -0
  239. package/src/lib/direct-manipulation.test.ts +275 -0
  240. package/src/lib/direct-manipulation.ts +118 -0
  241. package/src/lib/door-interaction.ts +1 -1
  242. package/src/lib/editor-api.ts +23 -35
  243. package/src/lib/elevation-guides.test.ts +107 -0
  244. package/src/lib/elevation-guides.ts +236 -0
  245. package/src/lib/floorplan/annotation-visibility.test.ts +327 -0
  246. package/src/lib/floorplan/annotation-visibility.ts +133 -0
  247. package/src/lib/floorplan/apply-alignment.test.ts +25 -0
  248. package/src/lib/floorplan/apply-alignment.ts +18 -12
  249. package/src/lib/floorplan/drawing-coordination.test.ts +46 -0
  250. package/src/lib/floorplan/drawing-coordination.ts +13 -0
  251. package/src/lib/floorplan/floorplan-export.test.ts +319 -0
  252. package/src/lib/floorplan/floorplan-export.tsx +978 -0
  253. package/src/lib/floorplan/floorplan-extension.test.ts +43 -0
  254. package/src/lib/floorplan/floorplan-extension.ts +181 -0
  255. package/src/lib/floorplan/floorplan-mode.test.ts +120 -0
  256. package/src/lib/floorplan/floorplan-mode.ts +73 -0
  257. package/src/lib/floorplan/floorplan-pdfkit-document.ts +194 -0
  258. package/src/lib/floorplan/floorplan-pdfkit-renderer.test.ts +107 -0
  259. package/src/lib/floorplan/floorplan-pdfkit-renderer.ts +680 -0
  260. package/src/lib/floorplan/geometry.ts +52 -0
  261. package/src/lib/floorplan/index.ts +3 -0
  262. package/src/lib/floorplan/items.ts +5 -2
  263. package/src/lib/floorplan/plan-coords.ts +21 -0
  264. package/src/lib/fresh-planar-placement.test.ts +283 -3
  265. package/src/lib/fresh-planar-placement.ts +85 -1
  266. package/src/lib/glb-export.test.ts +556 -0
  267. package/src/lib/glb-export.ts +1071 -0
  268. package/src/lib/ground-surface.ts +95 -0
  269. package/src/lib/history.test.ts +96 -0
  270. package/src/lib/history.ts +31 -0
  271. package/src/lib/interaction/hot-set.test.ts +133 -0
  272. package/src/lib/interaction/hot-set.ts +67 -0
  273. package/src/lib/interaction/overlay-policy.test.ts +52 -0
  274. package/src/lib/interaction/overlay-policy.ts +59 -0
  275. package/src/lib/interaction/scope.ts +243 -0
  276. package/src/lib/level-duplication.test.ts +2 -1
  277. package/src/lib/level-selection.ts +2 -2
  278. package/src/lib/material-paint.ts +36 -49
  279. package/src/lib/measurement-kind.test.ts +30 -0
  280. package/src/lib/measurement-kind.ts +19 -0
  281. package/src/lib/measurement-label.test.ts +47 -0
  282. package/src/lib/measurement-label.ts +68 -0
  283. package/src/lib/measurement-parser.ts +116 -0
  284. package/src/lib/measurements.test.ts +188 -0
  285. package/src/lib/measurements.ts +210 -0
  286. package/src/lib/paint-scope.test.ts +454 -0
  287. package/src/lib/paint-scope.ts +461 -0
  288. package/src/lib/placement-drag-release.ts +23 -0
  289. package/src/lib/planar-cursor-placement.test.ts +57 -0
  290. package/src/lib/plugin-panels.test.ts +19 -0
  291. package/src/lib/plugin-panels.ts +91 -0
  292. package/src/lib/quick-action-feedback.ts +33 -0
  293. package/src/lib/quick-action-nodes.test.ts +94 -0
  294. package/src/lib/quick-action-nodes.ts +47 -0
  295. package/src/lib/quick-measurement.test.ts +77 -0
  296. package/src/lib/quick-measurement.ts +109 -0
  297. package/src/lib/roof-duplication.ts +6 -6
  298. package/src/lib/roof-hover-outline-proxy.ts +22 -0
  299. package/src/lib/roof-wall-hit.ts +164 -0
  300. package/src/lib/scene-clipboard.test.ts +325 -0
  301. package/src/lib/scene-clipboard.ts +310 -21
  302. package/src/lib/scene.ts +28 -5
  303. package/src/lib/selection-routing.test.ts +313 -0
  304. package/src/lib/selection-routing.ts +180 -0
  305. package/src/lib/sfx/index.ts +1 -0
  306. package/src/lib/sfx/movement-tick.test.ts +65 -0
  307. package/src/lib/sfx/movement-tick.ts +18 -0
  308. package/src/lib/sfx-bus.ts +82 -14
  309. package/src/lib/sfx-player.test.ts +266 -0
  310. package/src/lib/sfx-player.ts +262 -69
  311. package/src/lib/site-boundary.test.ts +53 -0
  312. package/src/lib/site-boundary.ts +27 -0
  313. package/src/lib/slab-plan-snap.test.ts +93 -0
  314. package/src/lib/slab-plan-snap.ts +108 -0
  315. package/src/lib/snapping-mode.test.ts +165 -0
  316. package/src/lib/snapping-mode.ts +191 -0
  317. package/src/lib/stair-duplication.test.ts +103 -0
  318. package/src/lib/stair-duplication.ts +58 -86
  319. package/src/lib/stair-levels.ts +2 -0
  320. package/src/lib/surface-plan-snap.test.ts +71 -0
  321. package/src/lib/surface-plan-snap.ts +256 -0
  322. package/src/lib/terrain-sculpt.test.ts +429 -0
  323. package/src/lib/terrain-sculpt.ts +301 -0
  324. package/src/lib/terrain-verb-color.test.ts +51 -0
  325. package/src/lib/terrain-verb-color.ts +58 -0
  326. package/src/lib/touch-gesture-priority.test.ts +45 -0
  327. package/src/lib/touch-gesture-priority.ts +30 -0
  328. package/src/lib/use-linear-display.ts +40 -0
  329. package/src/lib/window-interaction.ts +1 -1
  330. package/src/lib/world-grid-snap.ts +37 -5
  331. package/src/store/camera-pose-store.test.ts +29 -0
  332. package/src/store/camera-pose-store.ts +27 -0
  333. package/src/store/live-draft-preview-stores.test.ts +136 -0
  334. package/src/store/navigation-sync-pose-store.test.ts +34 -0
  335. package/src/store/navigation-sync-pose-store.ts +27 -0
  336. package/src/store/terrain-sculpt-mode.test.ts +223 -0
  337. package/src/store/use-delete-confirmation.ts +27 -0
  338. package/src/store/use-direct-manipulation-feedback.ts +20 -0
  339. package/src/store/use-drawing-view.test.ts +23 -0
  340. package/src/store/use-drawing-view.ts +84 -0
  341. package/src/store/use-editor.tsx +665 -126
  342. package/src/store/use-elevation-guides.ts +40 -0
  343. package/src/store/use-facing-pose.ts +44 -0
  344. package/src/store/use-fence-curve-draft.ts +51 -0
  345. package/src/store/use-first-person-hud.ts +40 -0
  346. package/src/store/use-floorplan-annotation-visibility.ts +60 -0
  347. package/src/store/use-floorplan-draft-preview.ts +151 -0
  348. package/src/store/use-floorplan-marquee.ts +50 -0
  349. package/src/store/use-floorplan-mode.ts +110 -0
  350. package/src/store/use-interaction-scope.test.ts +206 -0
  351. package/src/store/use-interaction-scope.ts +140 -0
  352. package/src/store/use-measurement-draft.test.ts +559 -0
  353. package/src/store/use-measurement-draft.ts +570 -0
  354. package/src/store/use-opening-guides.ts +41 -0
  355. package/src/store/use-path-draft-preview.ts +90 -0
  356. package/src/store/use-placement-preview.ts +11 -3
  357. package/src/store/use-quick-measurement-hud.test.ts +53 -0
  358. package/src/store/use-quick-measurement-hud.ts +77 -0
  359. package/src/store/use-segment-draft-chain.ts +28 -0
  360. package/src/store/use-stair-build-preview.ts +53 -0
  361. package/src/store/use-wall-snap-indicator.ts +2 -0
  362. package/src/components/editor/first-person/bvh-ecctrl.tsx +0 -860
  363. package/src/components/editor/group-move-handle.tsx +0 -316
  364. package/src/components/ui/panels/paint-panel.tsx +0 -163
  365. package/src/lib/floorplan/selection-tool.ts +0 -271
  366. package/src/lib/level-name.ts +0 -11
@@ -1,6 +1,5 @@
1
1
  'use client'
2
2
 
3
- import { Icon } from '@iconify/react'
4
3
  import {
5
4
  type AnyNodeId,
6
5
  type BuildingNode,
@@ -10,26 +9,22 @@ import {
10
9
  useScene,
11
10
  } from '@pascal-app/core'
12
11
  import { useViewer } from '@pascal-app/viewer'
13
- import { Check, ChevronDown, Eye, EyeOff, Layers2, Plus, Trash2 } from 'lucide-react'
12
+ import { Check, ChevronDown, Eye, EyeOff, Layers2, Plus, Trash2, Waypoints } from 'lucide-react'
14
13
  import { useCallback, useRef, useState } from 'react'
15
14
  import { useShallow } from 'zustand/react/shallow'
16
- import { getLevelDisplayName } from '../../../lib/level-name'
15
+ import { getLevelDisplayName } from '@pascal-app/core'
17
16
  import { createLocalGuideImage } from '../../../lib/local-guide-image'
18
17
  import { cn } from '../../../lib/utils'
19
- import useEditor, { type GridSnapStep } from '../../../store/use-editor'
18
+ import useEditor from '../../../store/use-editor'
20
19
  import { useUploadStore } from '../../../store/use-upload'
21
20
  import { SliderControl } from '../controls/slider-control'
22
21
  import { Popover, PopoverContent, PopoverTrigger } from '../primitives/popover'
23
- import { Tooltip, TooltipContent, TooltipTrigger } from '../primitives/tooltip'
24
22
  import { ActionButton } from './action-button'
25
23
 
26
24
  const MAX_FILE_SIZE = 200 * 1024 * 1024 // 200MB
27
25
  const ACCEPTED_FILE_TYPES = '.glb,.gltf,image/jpeg,image/png,image/webp,image/gif'
28
- const GRID_SNAP_STEPS: GridSnapStep[] = [0.5, 0.25, 0.1, 0.05]
29
-
30
- function formatGridSnapStep(step: GridSnapStep) {
31
- return step.toFixed(2)
32
- }
26
+ const REFERENCES_EMPTY_TEXT =
27
+ 'Upload GLB meshes as scan references or blueprint images as guide references.'
33
28
 
34
29
  // ── Helper: get guide images for the current level ──────────────────────────
35
30
 
@@ -224,7 +219,7 @@ function GuidesControl() {
224
219
  <img
225
220
  alt="Guides"
226
221
  className="h-[28px] w-[28px] object-contain"
227
- src="/icons/floorplan.png"
222
+ src="/icons/floorplan.webp"
228
223
  />
229
224
  <span className="absolute -right-1.5 -bottom-1 min-w-[14px] rounded-full bg-white/20 px-[3px] text-center font-medium text-[9px] text-white/70 leading-[14px]">
230
225
  {guides.length}
@@ -263,7 +258,7 @@ function GuidesControl() {
263
258
  <div className="space-y-3">
264
259
  <div className="flex items-center gap-2">
265
260
  <span className="flex h-7 w-7 shrink-0 items-center justify-center rounded-lg bg-background/80">
266
- <img alt="" className="h-4 w-4 object-contain" src="/icons/floorplan.png" />
261
+ <img alt="" className="h-4 w-4 object-contain" src="/icons/floorplan.webp" />
267
262
  </span>
268
263
  <div className="min-w-0 flex-1">
269
264
  <p className="font-medium text-foreground text-sm">Guide images</p>
@@ -303,7 +298,7 @@ function GuidesControl() {
303
298
  <img
304
299
  alt=""
305
300
  className="h-3.5 w-3.5 shrink-0 object-contain opacity-70"
306
- src="/icons/floorplan.png"
301
+ src="/icons/floorplan.webp"
307
302
  />
308
303
  <p className="truncate font-medium text-foreground text-sm">
309
304
  {guide.name || `Guide image ${index + 1}`}
@@ -342,7 +337,7 @@ function GuidesControl() {
342
337
  </div>
343
338
  ) : (
344
339
  <div className="rounded-xl border border-border/45 border-dashed bg-background/60 px-3 py-4 text-muted-foreground text-sm">
345
- No guide images on this level yet.
340
+ {REFERENCES_EMPTY_TEXT}
346
341
  </div>
347
342
  )}
348
343
  </div>
@@ -351,70 +346,6 @@ function GuidesControl() {
351
346
  )
352
347
  }
353
348
 
354
- // ── Grid snap toggle ────────────────────────────────────────────────────────
355
-
356
- function GridSnapControl() {
357
- const [isOpen, setIsOpen] = useState(false)
358
- const gridSnapStep = useEditor((state) => state.gridSnapStep)
359
- const setGridSnapStep = useEditor((state) => state.setGridSnapStep)
360
-
361
- return (
362
- <Popover onOpenChange={setIsOpen} open={isOpen}>
363
- <Tooltip>
364
- <TooltipTrigger asChild>
365
- <PopoverTrigger asChild>
366
- <button
367
- aria-expanded={isOpen}
368
- aria-label={`Grid snap: ${formatGridSnapStep(gridSnapStep)}`}
369
- className={cn(
370
- 'flex h-11 w-11 flex-col items-center justify-center rounded-lg text-muted-foreground transition-all hover:bg-white/5 hover:text-foreground',
371
- isOpen && 'bg-white/10 text-foreground',
372
- )}
373
- type="button"
374
- >
375
- <Icon height={16} icon="lucide:grid-2x2" width={16} />
376
- <span className="mt-1 font-medium text-[9px] leading-none">
377
- {formatGridSnapStep(gridSnapStep)}
378
- </span>
379
- </button>
380
- </PopoverTrigger>
381
- </TooltipTrigger>
382
- <TooltipContent side="top">Grid snap: {formatGridSnapStep(gridSnapStep)}</TooltipContent>
383
- </Tooltip>
384
-
385
- <PopoverContent
386
- align="center"
387
- className="w-36 rounded-xl border-border/45 bg-background/96 p-2 shadow-elevation-3 backdrop-blur-xl"
388
- side="top"
389
- sideOffset={14}
390
- >
391
- <div className="space-y-1">
392
- {GRID_SNAP_STEPS.map((step) => {
393
- const isActive = step === gridSnapStep
394
- return (
395
- <button
396
- className={cn(
397
- 'flex w-full items-center justify-between rounded-lg px-2.5 py-2 text-left text-sm transition-colors hover:bg-white/8',
398
- isActive && 'bg-white/10 text-foreground',
399
- )}
400
- key={step}
401
- onClick={() => {
402
- setGridSnapStep(step)
403
- setIsOpen(false)
404
- }}
405
- type="button"
406
- >
407
- <span>{formatGridSnapStep(step)}</span>
408
- {isActive ? <Check className="h-3.5 w-3.5" /> : <span className="h-3.5 w-3.5" />}
409
- </button>
410
- )
411
- })}
412
- </div>
413
- </PopoverContent>
414
- </Popover>
415
- )
416
- }
417
-
418
349
  // ── Scans toggle + dropdown ─────────────────────────────────────────────────
419
350
 
420
351
  function ScansControl() {
@@ -464,7 +395,7 @@ function ScansControl() {
464
395
  variant="ghost"
465
396
  >
466
397
  <div className="relative">
467
- <img alt="Scans" className="h-[28px] w-[28px] object-contain" src="/icons/mesh.png" />
398
+ <img alt="Scans" className="h-[28px] w-[28px] object-contain" src="/icons/mesh.webp" />
468
399
  <span className="absolute -right-1.5 -bottom-1 min-w-[14px] rounded-full bg-white/20 px-[3px] text-center font-medium text-[9px] text-white/70 leading-[14px]">
469
400
  {scans.length}
470
401
  </span>
@@ -502,7 +433,7 @@ function ScansControl() {
502
433
  <div className="space-y-3">
503
434
  <div className="flex items-center gap-2">
504
435
  <span className="flex h-7 w-7 shrink-0 items-center justify-center rounded-lg bg-background/80">
505
- <img alt="" className="h-4 w-4 object-contain" src="/icons/mesh.png" />
436
+ <img alt="" className="h-4 w-4 object-contain" src="/icons/mesh.webp" />
506
437
  </span>
507
438
  <div className="min-w-0 flex-1">
508
439
  <p className="font-medium text-foreground text-sm">Scans</p>
@@ -542,7 +473,7 @@ function ScansControl() {
542
473
  <img
543
474
  alt=""
544
475
  className="h-3.5 w-3.5 shrink-0 object-contain opacity-70"
545
- src="/icons/mesh.png"
476
+ src="/icons/mesh.webp"
546
477
  />
547
478
  <p className="truncate font-medium text-foreground text-sm">
548
479
  {scan.name || `Scan ${index + 1}`}
@@ -581,7 +512,7 @@ function ScansControl() {
581
512
  </div>
582
513
  ) : (
583
514
  <div className="rounded-xl border border-border/45 border-dashed bg-background/60 px-3 py-4 text-muted-foreground text-sm">
584
- No scans on this level yet.
515
+ {REFERENCES_EMPTY_TEXT}
585
516
  </div>
586
517
  )}
587
518
  </div>
@@ -763,7 +694,7 @@ function ReferencesControl() {
763
694
  <img
764
695
  alt="References"
765
696
  className="h-[28px] w-[28px] object-contain"
766
- src="/icons/floorplan.png"
697
+ src="/icons/floorplan.webp"
767
698
  />
768
699
  <span className="absolute -right-1.5 -bottom-1 min-w-[14px] rounded-full bg-white/20 px-[3px] text-center font-medium text-[9px] text-white/70 leading-[14px]">
769
700
  {total}
@@ -805,8 +736,8 @@ function ReferencesControl() {
805
736
  </div>
806
737
  )}
807
738
  <ReferenceListSection
808
- emptyText="No scans on this level yet."
809
- iconSrc="/icons/mesh.png"
739
+ emptyText={REFERENCES_EMPTY_TEXT}
740
+ iconSrc="/icons/mesh.webp"
810
741
  nodes={scans}
811
742
  noun="scan"
812
743
  onError={setUploadError}
@@ -816,8 +747,8 @@ function ReferencesControl() {
816
747
  />
817
748
  <div className="h-px bg-border/45" />
818
749
  <ReferenceListSection
819
- emptyText="No guide images on this level yet."
820
- iconSrc="/icons/floorplan.png"
750
+ emptyText={REFERENCES_EMPTY_TEXT}
751
+ iconSrc="/icons/floorplan.webp"
821
752
  nodes={guides}
822
753
  noun="guide image"
823
754
  onError={setUploadError}
@@ -987,9 +918,30 @@ function ReferenceFloorControl() {
987
918
  )
988
919
  }
989
920
 
990
- // ── Exports ─────────────────────────────────────────────────────────────────
921
+ // ── Riser diagram control ────────────────────────────────────────────────────
922
+
923
+ function RiserControl() {
924
+ const isRiserOpen = useEditor((state) => state.isRiserOpen)
925
+ const toggleRiserOpen = useEditor((state) => state.toggleRiserOpen)
926
+
927
+ return (
928
+ <ActionButton
929
+ className={cn(
930
+ isRiserOpen
931
+ ? 'bg-white/15'
932
+ : 'opacity-60 grayscale hover:bg-white/5 hover:opacity-100 hover:grayscale-0',
933
+ )}
934
+ label="Riser diagram"
935
+ onClick={toggleRiserOpen}
936
+ size="icon"
937
+ variant="ghost"
938
+ >
939
+ <Waypoints className="h-4 w-4" />
940
+ </ActionButton>
941
+ )
942
+ }
991
943
 
992
- export { GridSnapControl }
944
+ // ── Exports ─────────────────────────────────────────────────────────────────
993
945
 
994
946
  export function SecondaryToggles() {
995
947
  return (
@@ -1002,10 +954,10 @@ export function SecondaryToggles() {
1002
954
  export function ViewToggles() {
1003
955
  return (
1004
956
  <div className="flex items-center gap-1">
1005
- <GridSnapControl />
1006
957
  <ScansControl />
1007
958
  <GuidesControl />
1008
959
  <ReferenceFloorControl />
960
+ <RiserControl />
1009
961
  </div>
1010
962
  )
1011
963
  }
@@ -1,7 +1,7 @@
1
1
  'use client'
2
2
 
3
3
  import type { AnyNodeId } from '@pascal-app/core'
4
- import { LevelNode, useScene } from '@pascal-app/core'
4
+ import { DEFAULT_LEVEL_HEIGHT, LevelNode, useScene } from '@pascal-app/core'
5
5
  import { useViewer } from '@pascal-app/viewer'
6
6
  import {
7
7
  AppWindow,
@@ -22,6 +22,7 @@ import {
22
22
  Minimize2,
23
23
  Moon,
24
24
  MousePointer2,
25
+ Mountain,
25
26
  Package,
26
27
  PaintBucket,
27
28
  PencilLine,
@@ -168,6 +169,17 @@ export function EditorCommands() {
168
169
  setMode('material-paint')
169
170
  }),
170
171
  },
172
+ {
173
+ id: 'editor.mode.terrain-sculpt',
174
+ label: 'Sculpt Terrain',
175
+ group: 'Scene',
176
+ icon: <Mountain className="h-4 w-4" />,
177
+ keywords: ['terrain', 'ground', 'elevation', 'sculpt', 'hill', 'slope', 'grade', 'dig'],
178
+ shortcut: ['G'],
179
+ // No `setPhase`: `setMode` moves to the site phase itself, and doing it
180
+ // here would set the phase twice with a mode reset in between.
181
+ execute: () => run(() => setMode('terrain-sculpt')),
182
+ },
171
183
 
172
184
  // ── Levels ───────────────────────────────────────────────────────────
173
185
  {
@@ -196,6 +208,7 @@ export function EditorCommands() {
196
208
  ).length
197
209
  const newLevel = LevelNode.parse({
198
210
  level: levelCount,
211
+ height: DEFAULT_LEVEL_HEIGHT,
199
212
  children: [],
200
213
  parentId: building.id,
201
214
  })
@@ -245,10 +258,10 @@ export function EditorCommands() {
245
258
  label: 'Wall Mode',
246
259
  group: 'Viewer Controls',
247
260
  icon: <Layers className="h-4 w-4" />,
248
- keywords: ['wall', 'cutaway', 'up', 'down', 'view'],
261
+ keywords: ['wall', 'cutaway', 'up', 'down', 'translucent', 'view'],
249
262
  badge: () => {
250
263
  const mode = useViewer.getState().wallMode
251
- return { cutaway: 'Cutaway', up: 'Up', down: 'Down' }[mode]
264
+ return { cutaway: 'Cutaway', up: 'Up', down: 'Down', translucent: 'Translucent' }[mode]
252
265
  },
253
266
  navigate: true,
254
267
  execute: () => navigateTo('wall-mode'),
@@ -10,7 +10,7 @@ import { useEffect, useState } from 'react'
10
10
  import { create } from 'zustand'
11
11
  import { useShallow } from 'zustand/shallow'
12
12
  import { Dialog, DialogContent, DialogTitle } from './../../../components/ui/primitives/dialog'
13
- import { getLevelDisplayName } from '../../../lib/level-name'
13
+ import { getLevelDisplayName } from '@pascal-app/core'
14
14
  import { useCommandRegistry } from '../../../store/use-command-registry'
15
15
  import { usePaletteViewRegistry } from '../../../store/use-palette-view-registry'
16
16
 
@@ -244,10 +244,11 @@ export function CommandPalette({ emptyAction }: { emptyAction?: CommandPaletteEm
244
244
  setOpen(false)
245
245
  }
246
246
 
247
- const wallModeLabel: Record<'cutaway' | 'up' | 'down', string> = {
247
+ const wallModeLabel: Record<'cutaway' | 'up' | 'down' | 'translucent', string> = {
248
248
  cutaway: 'Cutaway',
249
249
  up: 'Up',
250
250
  down: 'Down',
251
+ translucent: 'Translucent',
251
252
  }
252
253
  const levelModeLabel: Record<'manual' | 'stacked' | 'exploded' | 'solo', string> = {
253
254
  manual: 'Manual',
@@ -373,7 +374,7 @@ export function CommandPalette({ emptyAction }: { emptyAction?: CommandPaletteEm
373
374
  {/* ── Wall Mode sub-page ────────────────────────────────────── */}
374
375
  {page === 'wall-mode' && (
375
376
  <Command.Group heading="Wall Mode">
376
- {(['cutaway', 'up', 'down'] as const).map((mode) => (
377
+ {(['cutaway', 'up', 'down', 'translucent'] as const).map((mode) => (
377
378
  <OptionItem
378
379
  isActive={wallMode === mode}
379
380
  key={mode}
@@ -1,32 +1,49 @@
1
1
  'use client'
2
2
 
3
- import { type AnyNodeId, useScene } from '@pascal-app/core'
3
+ import {
4
+ type AnyNodeId,
5
+ generateSceneMaterialId,
6
+ type SceneMaterialId,
7
+ toSceneMaterialRef,
8
+ useScene,
9
+ } from '@pascal-app/core'
4
10
  import { useViewer } from '@pascal-app/viewer'
5
- import { Eraser, RotateCcw } from 'lucide-react'
6
- import { useEffect } from 'react'
11
+ import { Eraser, Plus, RotateCcw } from 'lucide-react'
12
+ import { useEffect, useState } from 'react'
7
13
  import {
8
14
  buildResetSurfaceMaterialUpdates,
9
15
  resolvePaintTargetFromSelection,
10
16
  } from './../../../lib/material-paint'
11
17
  import useEditor from './../../../store/use-editor'
12
18
  import { Button } from '../primitives/button'
19
+ import { Tooltip, TooltipContent, TooltipTrigger } from '../primitives/tooltip'
13
20
  import { MaterialPicker } from './material-picker'
21
+ import { SceneMaterialList } from './scene-material-list'
14
22
 
15
23
  /**
16
24
  * Material picker for paint mode. Embedders render this wherever paint controls
17
25
  * belong (the community editor places it in the Build sidebar while paint mode
18
- * is active). It owns the paint-target/material wiring so the host only needs
19
- * to mount it; it fills its container's width.
26
+ * 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
28
+ * scene-material footer (always visible, with a `+` to add a custom material).
20
29
  */
21
- export function MaterialPaintPanel() {
30
+ export type MaterialPaintPanelProps = {
31
+ /** When provided, the catalog grid leads with a "New material" tile that invokes it. */
32
+ onCreateMaterialRequest?: () => void
33
+ }
34
+
35
+ export function MaterialPaintPanel({ onCreateMaterialRequest }: MaterialPaintPanelProps) {
22
36
  const activePaintMaterial = useEditor((state) => state.activePaintMaterial)
23
37
  const activePaintTarget = useEditor((state) => state.activePaintTarget)
24
38
  const setActivePaintMaterial = useEditor((state) => state.setActivePaintMaterial)
25
39
  const setActivePaintTarget = useEditor((state) => state.setActivePaintTarget)
26
40
  const paintEraser = useEditor((state) => state.paintEraser)
27
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)
28
44
  const selectedIds = useViewer((state) => state.selection.selectedIds)
29
45
  const nodes = useScene((state) => state.nodes)
46
+ const materialCount = useScene((state) => Object.keys(state.materials).length)
30
47
  const selectedId = selectedIds.length === 1 ? (selectedIds[0] ?? null) : null
31
48
  const selectedNode = selectedId ? nodes[selectedId as AnyNodeId] : null
32
49
  const canResetSelection =
@@ -44,9 +61,34 @@ export function MaterialPaintPanel() {
44
61
  useScene.getState().updateNodes(buildResetSurfaceMaterialUpdates(nodes, selectedNode))
45
62
  }
46
63
 
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
+ setActivePaintMaterial({ materialPreset: toSceneMaterialRef(id), sourceTarget: activePaintTarget })
85
+ setAutoEditMaterialId(id)
86
+ }
87
+
47
88
  return (
48
- <div className="w-full space-y-2">
49
- <div className="flex items-center gap-2">
89
+ <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">
50
92
  <Button
51
93
  aria-pressed={paintEraser}
52
94
  className="flex-1"
@@ -68,16 +110,49 @@ export function MaterialPaintPanel() {
68
110
  Reset all
69
111
  </Button>
70
112
  </div>
71
- <MaterialPicker
72
- onChange={(material) => {
73
- setActivePaintMaterial({ material, sourceTarget: activePaintTarget })
74
- }}
75
- onSelectMaterialPreset={(materialPreset) => {
76
- setActivePaintMaterial({ materialPreset, sourceTarget: activePaintTarget })
77
- }}
78
- selectedMaterialPreset={activePaintMaterial?.materialPreset}
79
- value={activePaintMaterial?.material}
80
- />
113
+
114
+ {/* Scrolls: category tabs (fixed inside) + catalog grid (the scroll). */}
115
+ <div className="min-h-0 flex-1">
116
+ <MaterialPicker
117
+ onCreateMaterialRequest={onCreateMaterialRequest}
118
+ onSelectMaterialPreset={(materialPreset) => {
119
+ setActivePaintMaterial({ materialPreset, sourceTarget: activePaintTarget })
120
+ }}
121
+ selectedMaterialPreset={activePaintMaterial?.materialPreset}
122
+ />
123
+ </div>
124
+
125
+ {/* Fixed footer: scene materials, always visible, with a `+` to add one. */}
126
+ <div className="mt-2 shrink-0 space-y-1.5 border-border/60 border-t pt-2">
127
+ <div className="flex items-center justify-between">
128
+ <span className="font-medium text-muted-foreground text-xs uppercase tracking-[0.12em]">
129
+ Scene materials
130
+ </span>
131
+ <Tooltip>
132
+ <TooltipTrigger asChild>
133
+ <Button
134
+ aria-label="Add material"
135
+ onClick={createCustomMaterial}
136
+ size="icon-sm"
137
+ type="button"
138
+ variant="outline"
139
+ >
140
+ <Plus />
141
+ </Button>
142
+ </TooltipTrigger>
143
+ <TooltipContent>Add material</TooltipContent>
144
+ </Tooltip>
145
+ </div>
146
+ <div className="subtle-scrollbar max-h-56 overflow-y-auto">
147
+ {materialCount > 0 ? (
148
+ <SceneMaterialList autoEditId={autoEditMaterialId} />
149
+ ) : (
150
+ <p className="px-0.5 py-1 text-muted-foreground text-xs">
151
+ No custom materials yet — add one with +.
152
+ </p>
153
+ )}
154
+ </div>
155
+ </div>
81
156
  </div>
82
157
  )
83
158
  }