@pascal-app/editor 0.9.1 → 0.9.2
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.
- package/package.json +12 -9
- package/src/components/editor/alignment-3d-guide-layer.tsx +10 -2
- package/src/components/editor/bake-exporter.tsx +47 -0
- package/src/components/editor/custom-camera-controls.tsx +63 -25
- package/src/components/editor/editor-layout-mobile.tsx +5 -0
- package/src/components/editor/editor-layout-v2.tsx +18 -0
- package/src/components/editor/export-manager.tsx +71 -63
- package/src/components/editor/fence-tangent-lines-3d.tsx +87 -0
- package/src/components/editor/first-person/build-collider-world.test.ts +36 -3
- package/src/components/editor/first-person/build-collider-world.ts +32 -5
- package/src/components/editor/first-person-controls.tsx +118 -7
- package/src/components/editor/floating-action-menu.tsx +410 -33
- package/src/components/editor/floating-building-action-menu.tsx +1 -1
- package/src/components/editor/floorplan-background-selection.test.ts +58 -0
- package/src/components/editor/floorplan-background-selection.ts +3 -2
- package/src/components/editor/floorplan-panel.tsx +1804 -849
- package/src/components/editor/grid.tsx +166 -39
- package/src/components/editor/group-actions.ts +457 -0
- package/src/components/editor/group-floating-action-menu.tsx +130 -0
- package/src/components/editor/group-move-3d.ts +397 -0
- package/src/components/editor/group-rotate-handle.tsx +102 -64
- package/src/components/editor/group-selection-box-3d.tsx +173 -0
- package/src/components/editor/group-transform-shared.test.ts +192 -1
- package/src/components/editor/group-transform-shared.ts +240 -7
- package/src/components/editor/handles/handle-arrow.tsx +81 -23
- package/src/components/editor/handles/use-handle-drag.ts +37 -4
- package/src/components/editor/index.tsx +248 -27
- package/src/components/editor/measurement-pill.tsx +62 -22
- package/src/components/editor/node-action-menu.tsx +14 -1
- package/src/components/editor/node-arrow-handles.tsx +366 -173
- package/src/components/editor/opening-guides-3d-layer.tsx +144 -0
- package/src/components/editor/quick-measurement-card.tsx +70 -0
- package/src/components/editor/quick-measurement-hud.tsx +28 -0
- package/src/components/editor/riser-diagram-panel.tsx +137 -0
- package/src/components/editor/selection-manager.tsx +812 -466
- package/src/components/editor/site-edge-labels.tsx +4 -4
- package/src/components/editor/slab-hole-highlights.tsx +13 -5
- package/src/components/editor/snapshot-capture-overlay.tsx +255 -115
- package/src/components/editor/three-context-bridge.ts +22 -0
- package/src/components/editor/thumbnail-generator.tsx +136 -43
- package/src/components/editor/use-floorplan-background-placement.ts +113 -45
- package/src/components/editor/use-floorplan-scene-data.ts +5 -5
- package/src/components/editor/use-mesh-settle-epoch.ts +26 -0
- package/src/components/editor/wall-measurement-label.tsx +3 -13
- package/src/components/editor/wall-move-side-handles.tsx +36 -21
- package/src/components/editor/wall-snap-beacon-layer.tsx +160 -6
- package/src/components/editor-2d/floorplan-action-menu-layer.tsx +9 -5
- package/src/components/editor-2d/floorplan-cursor-indicator-overlay.tsx +1 -1
- package/src/components/editor-2d/floorplan-group-action-menu.tsx +87 -0
- package/src/components/editor-2d/floorplan-group-move.tsx +701 -0
- package/src/components/editor-2d/floorplan-measurement-tool-layer.test.ts +113 -0
- package/src/components/editor-2d/floorplan-measurement-tool-layer.tsx +1656 -0
- package/src/components/editor-2d/floorplan-quick-measure-layer.tsx +212 -0
- package/src/components/editor-2d/floorplan-registry-action-menu.tsx +169 -4
- package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +300 -59
- package/src/components/editor-2d/floorplan-snap-beacon-layer.tsx +5 -3
- package/src/components/editor-2d/renderers/floorplan-label-angle.test.ts +15 -0
- package/src/components/editor-2d/renderers/floorplan-label-angle.ts +14 -0
- package/src/components/editor-2d/renderers/floorplan-placement-preview-layer.tsx +4 -1
- package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +308 -0
- package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +1704 -342
- package/src/components/editor-2d/renderers/floorplan-voronoi-layer.tsx +128 -0
- package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +419 -60
- package/src/components/systems/ceiling/ceiling-system.tsx +97 -4
- package/src/components/systems/roof/roof-edit-system.tsx +1768 -13
- package/src/components/systems/selection-affordance-manager.tsx +38 -0
- package/src/components/systems/zone/zone-label-editor-system.tsx +14 -1
- package/src/components/systems/zone/zone-system.tsx +42 -13
- package/src/components/tools/elevator/elevator-tool.tsx +34 -8
- package/src/components/tools/elevator/move-elevator-tool.tsx +13 -2
- package/src/components/tools/fence/fence-drafting.ts +80 -8
- package/src/components/tools/item/move-tool.tsx +17 -13
- package/src/components/tools/item/placement-math.test.ts +13 -1
- package/src/components/tools/item/placement-math.ts +28 -2
- package/src/components/tools/item/placement-strategies.ts +246 -5
- package/src/components/tools/item/placement-types.ts +13 -1
- package/src/components/tools/item/use-draft-node.ts +31 -1
- package/src/components/tools/item/use-placement-coordinator.tsx +761 -121
- package/src/components/tools/registry/move-registry-node-tool.tsx +561 -99
- package/src/components/tools/roof/roof-tool.tsx +320 -94
- package/src/components/tools/select/box-select-state.ts +18 -7
- package/src/components/tools/select/box-select-tool.tsx +176 -40
- package/src/components/tools/select/marquee-geometry.test.ts +161 -0
- package/src/components/tools/select/marquee-geometry.ts +155 -0
- package/src/components/tools/select/plane-box-select-tool.tsx +1 -8
- package/src/components/tools/select/select-candidates.ts +1 -1
- package/src/components/tools/shared/cursor-sphere.tsx +62 -26
- package/src/components/tools/shared/drag-bounding-box.tsx +28 -4
- package/src/components/tools/shared/facing-indicator.tsx +87 -0
- package/src/components/tools/shared/facing-pose-indicator.tsx +54 -0
- package/src/components/tools/shared/placement-box.tsx +183 -1
- package/src/components/tools/shared/polygon-editor.tsx +276 -35
- package/src/components/tools/site/site-boundary-editor.tsx +88 -36
- package/src/components/tools/stair/stair-defaults.ts +1 -0
- package/src/components/tools/stair/stair-tool.tsx +82 -11
- package/src/components/tools/tool-manager.tsx +86 -25
- package/src/components/tools/wall/wall-drafting.test.ts +330 -0
- package/src/components/tools/wall/wall-drafting.ts +180 -103
- package/src/components/tools/wall/wall-snap-geometry.test.ts +44 -0
- package/src/components/tools/wall/wall-snap-geometry.ts +67 -6
- package/src/components/tools/zone/zone-boundary-editor.tsx +5 -1
- package/src/components/tools/zone/zone-tool.tsx +82 -88
- package/src/components/ui/action-menu/camera-actions.tsx +24 -17
- package/src/components/ui/action-menu/control-modes.tsx +40 -93
- package/src/components/ui/action-menu/furnish-tools.tsx +5 -5
- package/src/components/ui/action-menu/index.tsx +2 -4
- package/src/components/ui/action-menu/measurement-control.tsx +188 -0
- package/src/components/ui/action-menu/structure-tools.tsx +22 -13
- package/src/components/ui/action-menu/view-toggles.tsx +42 -90
- package/src/components/ui/command-palette/editor-commands.tsx +2 -2
- package/src/components/ui/command-palette/index.tsx +4 -3
- package/src/components/ui/controls/material-paint-panel.tsx +86 -17
- package/src/components/ui/controls/material-picker.tsx +83 -152
- package/src/components/ui/controls/material-properties-editor.tsx +108 -0
- package/src/components/ui/controls/metric-control.tsx +118 -44
- package/src/components/ui/controls/scene-material-list.tsx +247 -0
- package/src/components/ui/controls/slider-control.tsx +88 -38
- package/src/components/ui/floating-level-selector.tsx +1 -1
- package/src/components/ui/helpers/building-helper.tsx +10 -23
- package/src/components/ui/helpers/contextual-helper-panel.tsx +428 -0
- package/src/components/ui/helpers/helper-manager.tsx +227 -14
- package/src/components/ui/helpers/item-helper.tsx +28 -32
- package/src/components/ui/helpers/registered-tool-helper.tsx +45 -11
- package/src/components/ui/helpers/roof-helper.tsx +10 -12
- package/src/components/ui/icon-ref.tsx +47 -0
- package/src/components/ui/item-catalog/catalog-items.tsx +39 -1
- package/src/components/ui/item-catalog/item-catalog.tsx +13 -36
- package/src/components/ui/level-duplicate-dialog.tsx +1 -1
- package/src/components/ui/panels/node-display.ts +17 -17
- package/src/components/ui/panels/panel-manager.tsx +22 -13
- package/src/components/ui/panels/panel-wrapper.tsx +24 -3
- package/src/components/ui/panels/parametric-inspector.tsx +15 -2
- package/src/components/ui/panels/reference-panel.tsx +54 -19
- package/src/components/ui/primitives/shortcut-token.tsx +39 -3
- package/src/components/ui/primitives/sidebar.tsx +1 -0
- package/src/components/ui/sidebar/app-sidebar.tsx +5 -1
- package/src/components/ui/sidebar/icon-rail.tsx +50 -31
- package/src/components/ui/sidebar/panels/plugins-panel.tsx +218 -0
- package/src/components/ui/sidebar/panels/settings-panel/index.tsx +71 -40
- package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +60 -6
- package/src/components/ui/sidebar/panels/settings-panel/load-build-dialog.tsx +12 -4
- package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +4 -3
- package/src/components/ui/sidebar/panels/site-panel/chimney-tree-node.tsx +10 -7
- package/src/components/ui/sidebar/panels/site-panel/column-tree-node.tsx +1 -1
- package/src/components/ui/sidebar/panels/site-panel/door-tree-node.tsx +7 -2
- package/src/components/ui/sidebar/panels/site-panel/dormer-tree-node.tsx +10 -7
- package/src/components/ui/sidebar/panels/site-panel/elevator-tree-node.tsx +1 -1
- package/src/components/ui/sidebar/panels/site-panel/fence-tree-node.tsx +1 -1
- package/src/components/ui/sidebar/panels/site-panel/gutter-tree-node.tsx +10 -7
- package/src/components/ui/sidebar/panels/site-panel/index.tsx +40 -17
- package/src/components/ui/sidebar/panels/site-panel/item-tree-node.tsx +31 -17
- package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +1 -1
- package/src/components/ui/sidebar/panels/site-panel/registry-tree-node.tsx +72 -25
- package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +7 -3
- package/src/components/ui/sidebar/panels/site-panel/shelf-tree-node.tsx +12 -8
- package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +4 -3
- package/src/components/ui/sidebar/panels/site-panel/solar-panel-tree-node.tsx +10 -7
- package/src/components/ui/sidebar/panels/site-panel/spawn-tree-node.tsx +1 -1
- package/src/components/ui/sidebar/panels/site-panel/stair-tree-node.tsx +2 -2
- package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +44 -3
- package/src/components/ui/sidebar/panels/site-panel/tree-structure.ts +36 -0
- package/src/components/ui/sidebar/panels/site-panel/wall-tree-node.tsx +1 -1
- package/src/components/ui/sidebar/panels/site-panel/window-tree-node.tsx +7 -2
- package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +3 -3
- package/src/components/ui/sidebar/tab-bar.tsx +42 -28
- package/src/components/ui/sidebar/use-plugin-panels.tsx +132 -0
- package/src/components/ui/snap-target-badge.test.tsx +40 -0
- package/src/components/ui/snap-target-badge.tsx +88 -0
- package/src/components/viewer-overlay.tsx +85 -10
- package/src/components/viewer-zone-system.tsx +6 -1
- package/src/hooks/use-auto-save.test.ts +14 -0
- package/src/hooks/use-auto-save.ts +54 -7
- package/src/hooks/use-ceiling-events.ts +3 -2
- package/src/hooks/use-drag-action.ts +4 -1
- package/src/hooks/use-keyboard.ts +385 -40
- package/src/index.tsx +219 -6
- package/src/lib/active-placement-surface.ts +35 -0
- package/src/lib/ceiling-plan-snap.ts +24 -0
- package/src/lib/contextual-help.test.ts +112 -0
- package/src/lib/contextual-help.ts +144 -0
- package/src/lib/continuation.ts +64 -0
- package/src/lib/direct-manipulation.test.ts +192 -0
- package/src/lib/direct-manipulation.ts +109 -0
- package/src/lib/editor-api.ts +23 -35
- package/src/lib/floorplan/apply-alignment.test.ts +25 -0
- package/src/lib/floorplan/apply-alignment.ts +18 -12
- package/src/lib/floorplan/floorplan-export.tsx +364 -0
- package/src/lib/floorplan/geometry.ts +53 -0
- package/src/lib/floorplan/index.ts +3 -0
- package/src/lib/floorplan/items.ts +5 -2
- package/src/lib/floorplan/plan-coords.ts +21 -0
- package/src/lib/fresh-planar-placement.test.ts +240 -3
- package/src/lib/fresh-planar-placement.ts +68 -1
- package/src/lib/glb-export.test.ts +441 -0
- package/src/lib/glb-export.ts +874 -0
- package/src/lib/history.ts +9 -0
- package/src/lib/interaction/hot-set.test.ts +133 -0
- package/src/lib/interaction/hot-set.ts +67 -0
- package/src/lib/interaction/overlay-policy.test.ts +51 -0
- package/src/lib/interaction/overlay-policy.ts +59 -0
- package/src/lib/interaction/scope.ts +211 -0
- package/src/lib/level-selection.ts +2 -2
- package/src/lib/material-paint.ts +36 -49
- package/src/lib/measurement-kind.test.ts +30 -0
- package/src/lib/measurement-kind.ts +19 -0
- package/src/lib/measurement-label.test.ts +47 -0
- package/src/lib/measurement-label.ts +68 -0
- package/src/lib/measurement-parser.ts +116 -0
- package/src/lib/measurements.test.ts +183 -0
- package/src/lib/measurements.ts +199 -0
- package/src/lib/paint-scope.test.ts +454 -0
- package/src/lib/paint-scope.ts +461 -0
- package/src/lib/placement-drag-release.ts +23 -0
- package/src/lib/planar-cursor-placement.test.ts +57 -0
- package/src/lib/plugin-panels.test.ts +19 -0
- package/src/lib/plugin-panels.ts +91 -0
- package/src/lib/quick-measurement.test.ts +77 -0
- package/src/lib/quick-measurement.ts +109 -0
- package/src/lib/roof-duplication.ts +6 -6
- package/src/lib/roof-hover-outline-proxy.ts +22 -0
- package/src/lib/roof-wall-hit.ts +164 -0
- package/src/lib/scene-clipboard.test.ts +196 -0
- package/src/lib/scene-clipboard.ts +73 -8
- package/src/lib/scene.ts +28 -5
- package/src/lib/selection-routing.test.ts +298 -0
- package/src/lib/selection-routing.ts +175 -0
- package/src/lib/sfx/index.ts +1 -0
- package/src/lib/sfx/movement-tick.test.ts +65 -0
- package/src/lib/sfx/movement-tick.ts +18 -0
- package/src/lib/sfx-bus.ts +50 -13
- package/src/lib/sfx-player.test.ts +124 -0
- package/src/lib/sfx-player.ts +111 -66
- package/src/lib/slab-plan-snap.test.ts +93 -0
- package/src/lib/slab-plan-snap.ts +108 -0
- package/src/lib/snapping-mode.test.ts +138 -0
- package/src/lib/snapping-mode.ts +179 -0
- package/src/lib/stair-duplication.ts +4 -4
- package/src/lib/surface-plan-snap.test.ts +71 -0
- package/src/lib/surface-plan-snap.ts +256 -0
- package/src/lib/use-linear-display.ts +40 -0
- package/src/lib/world-grid-snap.ts +37 -5
- package/src/store/use-direct-manipulation-feedback.ts +20 -0
- package/src/store/use-editor.tsx +484 -114
- package/src/store/use-facing-pose.ts +44 -0
- package/src/store/use-fence-curve-draft.ts +21 -0
- package/src/store/use-floorplan-draft-preview.ts +101 -0
- package/src/store/use-floorplan-marquee.ts +50 -0
- package/src/store/use-interaction-scope.test.ts +186 -0
- package/src/store/use-interaction-scope.ts +132 -0
- package/src/store/use-measurement-draft.test.ts +530 -0
- package/src/store/use-measurement-draft.ts +543 -0
- package/src/store/use-opening-guides.ts +41 -0
- package/src/store/use-placement-preview.ts +11 -3
- package/src/store/use-quick-measurement-hud.test.ts +53 -0
- package/src/store/use-quick-measurement-hud.ts +77 -0
- package/src/store/use-segment-draft-chain.ts +28 -0
- package/src/store/use-stair-build-preview.ts +43 -0
- package/src/store/use-wall-snap-indicator.ts +2 -0
- package/src/components/editor/first-person/bvh-ecctrl.tsx +0 -860
- package/src/components/editor/group-move-handle.tsx +0 -316
- package/src/components/ui/panels/paint-panel.tsx +0 -163
- package/src/lib/level-name.ts +0 -11
|
@@ -1,22 +1,31 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
|
-
import {
|
|
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
|
|
19
|
-
*
|
|
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
30
|
export function MaterialPaintPanel() {
|
|
22
31
|
const activePaintMaterial = useEditor((state) => state.activePaintMaterial)
|
|
@@ -25,8 +34,11 @@ export function MaterialPaintPanel() {
|
|
|
25
34
|
const setActivePaintTarget = useEditor((state) => state.setActivePaintTarget)
|
|
26
35
|
const paintEraser = useEditor((state) => state.paintEraser)
|
|
27
36
|
const setPaintEraser = useEditor((state) => state.setPaintEraser)
|
|
37
|
+
// Id of a just-created scene material whose inline editor should open on mount.
|
|
38
|
+
const [autoEditMaterialId, setAutoEditMaterialId] = useState<SceneMaterialId | null>(null)
|
|
28
39
|
const selectedIds = useViewer((state) => state.selection.selectedIds)
|
|
29
40
|
const nodes = useScene((state) => state.nodes)
|
|
41
|
+
const materialCount = useScene((state) => Object.keys(state.materials).length)
|
|
30
42
|
const selectedId = selectedIds.length === 1 ? (selectedIds[0] ?? null) : null
|
|
31
43
|
const selectedNode = selectedId ? nodes[selectedId as AnyNodeId] : null
|
|
32
44
|
const canResetSelection =
|
|
@@ -44,9 +56,34 @@ export function MaterialPaintPanel() {
|
|
|
44
56
|
useScene.getState().updateNodes(buildResetSurfaceMaterialUpdates(nodes, selectedNode))
|
|
45
57
|
}
|
|
46
58
|
|
|
59
|
+
// Create a blank custom scene material, select it as the brush (`scene:` ref so
|
|
60
|
+
// edits propagate), and open its inline editor. Available from any category.
|
|
61
|
+
const createCustomMaterial = () => {
|
|
62
|
+
const id = generateSceneMaterialId()
|
|
63
|
+
const count = Object.keys(useScene.getState().materials).length
|
|
64
|
+
useScene.getState().addSceneMaterial({
|
|
65
|
+
id,
|
|
66
|
+
name: `Material ${count + 1}`,
|
|
67
|
+
material: {
|
|
68
|
+
preset: 'custom',
|
|
69
|
+
properties: {
|
|
70
|
+
color: '#ffffff',
|
|
71
|
+
roughness: 0.5,
|
|
72
|
+
metalness: 0,
|
|
73
|
+
opacity: 1,
|
|
74
|
+
transparent: false,
|
|
75
|
+
side: 'front',
|
|
76
|
+
},
|
|
77
|
+
},
|
|
78
|
+
})
|
|
79
|
+
setActivePaintMaterial({ materialPreset: toSceneMaterialRef(id), sourceTarget: activePaintTarget })
|
|
80
|
+
setAutoEditMaterialId(id)
|
|
81
|
+
}
|
|
82
|
+
|
|
47
83
|
return (
|
|
48
|
-
<div className="w-full
|
|
49
|
-
|
|
84
|
+
<div className="flex h-full min-h-0 w-full flex-col">
|
|
85
|
+
{/* Fixed: eraser / reset. */}
|
|
86
|
+
<div className="flex shrink-0 items-center gap-2 pb-2">
|
|
50
87
|
<Button
|
|
51
88
|
aria-pressed={paintEraser}
|
|
52
89
|
className="flex-1"
|
|
@@ -68,16 +105,48 @@ export function MaterialPaintPanel() {
|
|
|
68
105
|
Reset all
|
|
69
106
|
</Button>
|
|
70
107
|
</div>
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
108
|
+
|
|
109
|
+
{/* Scrolls: category tabs (fixed inside) + catalog grid (the scroll). */}
|
|
110
|
+
<div className="min-h-0 flex-1">
|
|
111
|
+
<MaterialPicker
|
|
112
|
+
onSelectMaterialPreset={(materialPreset) => {
|
|
113
|
+
setActivePaintMaterial({ materialPreset, sourceTarget: activePaintTarget })
|
|
114
|
+
}}
|
|
115
|
+
selectedMaterialPreset={activePaintMaterial?.materialPreset}
|
|
116
|
+
/>
|
|
117
|
+
</div>
|
|
118
|
+
|
|
119
|
+
{/* Fixed footer: scene materials, always visible, with a `+` to add one. */}
|
|
120
|
+
<div className="mt-2 shrink-0 space-y-1.5 border-border/60 border-t pt-2">
|
|
121
|
+
<div className="flex items-center justify-between">
|
|
122
|
+
<span className="font-medium text-muted-foreground text-xs uppercase tracking-[0.12em]">
|
|
123
|
+
Scene materials
|
|
124
|
+
</span>
|
|
125
|
+
<Tooltip>
|
|
126
|
+
<TooltipTrigger asChild>
|
|
127
|
+
<Button
|
|
128
|
+
aria-label="Add material"
|
|
129
|
+
onClick={createCustomMaterial}
|
|
130
|
+
size="icon-sm"
|
|
131
|
+
type="button"
|
|
132
|
+
variant="outline"
|
|
133
|
+
>
|
|
134
|
+
<Plus />
|
|
135
|
+
</Button>
|
|
136
|
+
</TooltipTrigger>
|
|
137
|
+
<TooltipContent>Add material</TooltipContent>
|
|
138
|
+
</Tooltip>
|
|
139
|
+
</div>
|
|
140
|
+
<div className="subtle-scrollbar max-h-56 overflow-y-auto">
|
|
141
|
+
{materialCount > 0 ? (
|
|
142
|
+
<SceneMaterialList autoEditId={autoEditMaterialId} />
|
|
143
|
+
) : (
|
|
144
|
+
<p className="px-0.5 py-1 text-muted-foreground text-xs">
|
|
145
|
+
No custom materials yet — add one with +.
|
|
146
|
+
</p>
|
|
147
|
+
)}
|
|
148
|
+
</div>
|
|
149
|
+
</div>
|
|
81
150
|
</div>
|
|
82
151
|
)
|
|
83
152
|
}
|
|
@@ -5,17 +5,14 @@ import {
|
|
|
5
5
|
getLibraryMaterialIdFromRef,
|
|
6
6
|
getMaterialsForCategory,
|
|
7
7
|
MATERIAL_CATEGORIES,
|
|
8
|
-
type MaterialSchema,
|
|
9
8
|
type MaterialTarget,
|
|
10
9
|
toLibraryMaterialRef,
|
|
11
10
|
} from '@pascal-app/core'
|
|
12
|
-
import { useEffect,
|
|
13
|
-
import
|
|
11
|
+
import { useEffect, useState } from 'react'
|
|
12
|
+
import { triggerSFX } from '../../../lib/sfx-bus'
|
|
14
13
|
|
|
15
14
|
type MaterialPickerProps = {
|
|
16
|
-
value?: MaterialSchema
|
|
17
15
|
selectedMaterialPreset?: string
|
|
18
|
-
onChange?: (material: MaterialSchema) => void
|
|
19
16
|
onSelectMaterialPreset?: (materialPreset: string) => void
|
|
20
17
|
disabled?: boolean
|
|
21
18
|
nodeType?: MaterialTarget
|
|
@@ -23,174 +20,108 @@ type MaterialPickerProps = {
|
|
|
23
20
|
}
|
|
24
21
|
|
|
25
22
|
function getCategoryLabel(category: (typeof MATERIAL_CATEGORIES)[number]) {
|
|
26
|
-
if (category === 'roof') return 'Roofing'
|
|
27
23
|
return category.charAt(0).toUpperCase() + category.slice(1)
|
|
28
24
|
}
|
|
29
25
|
|
|
26
|
+
/**
|
|
27
|
+
* Catalog material picker: a fixed row of category tabs over a scrollable grid
|
|
28
|
+
* of swatches. Custom-material creation lives in the scene-material section
|
|
29
|
+
* (the host's `+` action), not here, so it's available from any category.
|
|
30
|
+
*/
|
|
30
31
|
export function MaterialPicker({
|
|
31
|
-
value,
|
|
32
32
|
selectedMaterialPreset,
|
|
33
|
-
onChange,
|
|
34
33
|
onSelectMaterialPreset,
|
|
35
34
|
disabled = false,
|
|
36
35
|
}: MaterialPickerProps) {
|
|
37
|
-
const setPaintPanelOpen = useEditor((state) => state.setPaintPanelOpen)
|
|
38
|
-
const [showCustom, setShowCustom] = useState<boolean>(!!value?.properties)
|
|
39
36
|
const [selectedCategory, setSelectedCategory] = useState<(typeof MATERIAL_CATEGORIES)[number]>(
|
|
40
37
|
MATERIAL_CATEGORIES[0],
|
|
41
38
|
)
|
|
42
|
-
const
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
: getMaterialsForCategory(selectedCategory)
|
|
47
|
-
|
|
48
|
-
useEffect(() => {
|
|
49
|
-
setShowCustom(!!value?.properties && !selectedMaterialPreset)
|
|
50
|
-
}, [selectedMaterialPreset, value?.properties])
|
|
39
|
+
const availableCategories = MATERIAL_CATEGORIES.filter(
|
|
40
|
+
(category) => getMaterialsForCategory(category).length > 0,
|
|
41
|
+
)
|
|
42
|
+
const catalogItems = getMaterialsForCategory(selectedCategory)
|
|
51
43
|
|
|
44
|
+
// Keep the visible category in sync with the externally-selected catalog
|
|
45
|
+
// material (a `scene:` ref matches no catalog entry, so the tab stays put).
|
|
52
46
|
useEffect(() => {
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
const catalogId = getLibraryMaterialIdFromRef(selectedMaterialPreset) ?? value?.id ?? undefined
|
|
59
|
-
const selectedCatalogEntry = getCatalogMaterialById(catalogId)
|
|
60
|
-
if (selectedCatalogEntry?.category) {
|
|
61
|
-
setSelectedCategory(selectedCatalogEntry.category)
|
|
62
|
-
}
|
|
63
|
-
}, [selectedMaterialPreset, value?.id])
|
|
64
|
-
|
|
65
|
-
const selectedCatalogId =
|
|
66
|
-
selectedMaterialPreset ?? (value?.id ? toLibraryMaterialRef(value.id) : undefined)
|
|
47
|
+
const catalogId = getLibraryMaterialIdFromRef(selectedMaterialPreset) ?? undefined
|
|
48
|
+
const entry = getCatalogMaterialById(catalogId)
|
|
49
|
+
if (entry?.category) setSelectedCategory(entry.category)
|
|
50
|
+
}, [selectedMaterialPreset])
|
|
67
51
|
|
|
68
52
|
const handleCatalogSelect = (materialId: string) => {
|
|
69
53
|
if (disabled) return
|
|
70
|
-
setShowCustom(false)
|
|
71
|
-
setPaintPanelOpen(false)
|
|
72
54
|
onSelectMaterialPreset?.(toLibraryMaterialRef(materialId))
|
|
73
55
|
}
|
|
74
56
|
|
|
75
|
-
useEffect(() => {
|
|
76
|
-
const container = categoryScrollRef.current
|
|
77
|
-
if (!container) return
|
|
78
|
-
|
|
79
|
-
const handleWheel = (event: WheelEvent) => {
|
|
80
|
-
const deltaX = event.deltaX
|
|
81
|
-
const deltaY = event.deltaY
|
|
82
|
-
const nextScrollLeft = container.scrollLeft + deltaX + deltaY
|
|
83
|
-
|
|
84
|
-
if (nextScrollLeft === container.scrollLeft) return
|
|
85
|
-
|
|
86
|
-
event.preventDefault()
|
|
87
|
-
container.scrollLeft = nextScrollLeft
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
container.addEventListener('wheel', handleWheel, { passive: false })
|
|
91
|
-
return () => {
|
|
92
|
-
container.removeEventListener('wheel', handleWheel)
|
|
93
|
-
}
|
|
94
|
-
}, [])
|
|
95
|
-
|
|
96
|
-
const handleCustomOpen = () => {
|
|
97
|
-
if (disabled) return
|
|
98
|
-
setShowCustom(true)
|
|
99
|
-
setPaintPanelOpen(true)
|
|
100
|
-
onChange?.({
|
|
101
|
-
preset: 'custom',
|
|
102
|
-
properties: {
|
|
103
|
-
color: value?.properties?.color || '#ffffff',
|
|
104
|
-
roughness: value?.properties?.roughness ?? 0.5,
|
|
105
|
-
metalness: value?.properties?.metalness ?? 0,
|
|
106
|
-
opacity: value?.properties?.opacity ?? 1,
|
|
107
|
-
transparent: value?.properties?.transparent ?? false,
|
|
108
|
-
side: value?.properties?.side ?? 'front',
|
|
109
|
-
},
|
|
110
|
-
})
|
|
111
|
-
}
|
|
112
|
-
|
|
113
57
|
return (
|
|
114
|
-
<div
|
|
115
|
-
{
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
}
|
|
136
|
-
if (category !== 'other') {
|
|
137
|
-
setPaintPanelOpen(false)
|
|
138
|
-
}
|
|
139
|
-
}}
|
|
140
|
-
type="button"
|
|
141
|
-
>
|
|
142
|
-
{getCategoryLabel(category)}
|
|
143
|
-
</button>
|
|
144
|
-
))}
|
|
145
|
-
</div>
|
|
146
|
-
</div>
|
|
147
|
-
<div
|
|
148
|
-
className="grid gap-1.5 pb-1"
|
|
149
|
-
style={{ gridTemplateColumns: 'repeat(auto-fill, minmax(72px, 1fr))' }}
|
|
58
|
+
<div
|
|
59
|
+
className={`flex h-full min-h-0 flex-col gap-2 ${disabled ? 'pointer-events-none opacity-50' : ''}`}
|
|
60
|
+
>
|
|
61
|
+
{/* Fixed category tabs — outside the scroll region. */}
|
|
62
|
+
<div className="flex shrink-0 flex-wrap gap-1">
|
|
63
|
+
{availableCategories.map((category) => (
|
|
64
|
+
<button
|
|
65
|
+
className={`rounded-full px-3 py-1 font-medium text-xs transition-colors ${
|
|
66
|
+
selectedCategory === category
|
|
67
|
+
? 'bg-foreground text-background'
|
|
68
|
+
: 'text-muted-foreground hover:text-foreground'
|
|
69
|
+
}`}
|
|
70
|
+
key={category}
|
|
71
|
+
onClick={() => {
|
|
72
|
+
setSelectedCategory(category)
|
|
73
|
+
// Auto-select the first material in the category so the brush is
|
|
74
|
+
// immediately ready (and the swatch shows as selected).
|
|
75
|
+
const first = getMaterialsForCategory(category)[0]
|
|
76
|
+
if (first) handleCatalogSelect(first.id)
|
|
77
|
+
}}
|
|
78
|
+
type="button"
|
|
150
79
|
>
|
|
151
|
-
{
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
80
|
+
{getCategoryLabel(category)}
|
|
81
|
+
</button>
|
|
82
|
+
))}
|
|
83
|
+
</div>
|
|
84
|
+
{/* The only scrolling region. */}
|
|
85
|
+
<div
|
|
86
|
+
className="subtle-scrollbar grid min-h-0 flex-1 auto-rows-min gap-2 overflow-y-auto pb-1"
|
|
87
|
+
style={{ gridTemplateColumns: 'repeat(auto-fill, minmax(72px, 1fr))' }}
|
|
88
|
+
>
|
|
89
|
+
{catalogItems.map((item) => {
|
|
90
|
+
const isSelected = selectedMaterialPreset === toLibraryMaterialRef(item.id)
|
|
91
|
+
return (
|
|
92
|
+
<button
|
|
93
|
+
className={`group relative flex flex-col gap-1.5 rounded-xl p-1.5 transition-colors hover:cursor-pointer hover:bg-sidebar-accent ${
|
|
94
|
+
isSelected ? 'bg-sidebar-accent ring-1 ring-primary ring-inset' : ''
|
|
95
|
+
}`}
|
|
96
|
+
key={item.id}
|
|
97
|
+
onClick={() => {
|
|
98
|
+
triggerSFX('sfx:menu-click')
|
|
99
|
+
handleCatalogSelect(item.id)
|
|
100
|
+
}}
|
|
101
|
+
onMouseEnter={() => triggerSFX('sfx:menu-hover')}
|
|
102
|
+
type="button"
|
|
103
|
+
>
|
|
104
|
+
<div className="relative aspect-square w-full overflow-hidden rounded-lg">
|
|
105
|
+
{item.previewThumbnailUrl ? (
|
|
106
|
+
<img
|
|
107
|
+
alt={item.label}
|
|
108
|
+
className="h-full w-full object-cover"
|
|
109
|
+
src={item.previewThumbnailUrl}
|
|
110
|
+
/>
|
|
111
|
+
) : (
|
|
112
|
+
<div
|
|
113
|
+
className="h-full w-full"
|
|
114
|
+
style={{ backgroundColor: item.previewColor ?? '#f3f4f6' }}
|
|
115
|
+
/>
|
|
116
|
+
)}
|
|
117
|
+
</div>
|
|
118
|
+
<span className="truncate px-0.5 text-left font-medium text-[11px] text-muted-foreground group-hover:text-foreground">
|
|
119
|
+
{item.label}
|
|
120
|
+
</span>
|
|
121
|
+
</button>
|
|
122
|
+
)
|
|
123
|
+
})}
|
|
124
|
+
</div>
|
|
194
125
|
</div>
|
|
195
126
|
)
|
|
196
127
|
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import type { MaterialProperties, MaterialSchema } from '@pascal-app/core'
|
|
4
|
+
import { Input } from '../primitives/input'
|
|
5
|
+
import { SliderControl } from './slider-control'
|
|
6
|
+
|
|
7
|
+
const DEFAULT_MATERIAL_PROPERTIES: MaterialProperties = {
|
|
8
|
+
color: '#ffffff',
|
|
9
|
+
roughness: 0.5,
|
|
10
|
+
metalness: 0,
|
|
11
|
+
opacity: 1,
|
|
12
|
+
transparent: false,
|
|
13
|
+
side: 'front',
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function MaterialPropertiesEditor({
|
|
17
|
+
value,
|
|
18
|
+
onChange,
|
|
19
|
+
}: {
|
|
20
|
+
value: MaterialSchema
|
|
21
|
+
onChange: (next: MaterialSchema) => void
|
|
22
|
+
}) {
|
|
23
|
+
const currentProps = value.properties ?? DEFAULT_MATERIAL_PROPERTIES
|
|
24
|
+
|
|
25
|
+
const updateMaterial = (
|
|
26
|
+
updates: Partial<MaterialProperties>,
|
|
27
|
+
nextTransparent = currentProps.transparent,
|
|
28
|
+
) => {
|
|
29
|
+
onChange({
|
|
30
|
+
...value,
|
|
31
|
+
preset: value.preset ?? 'custom',
|
|
32
|
+
properties: {
|
|
33
|
+
...currentProps,
|
|
34
|
+
...updates,
|
|
35
|
+
transparent: nextTransparent,
|
|
36
|
+
},
|
|
37
|
+
})
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return (
|
|
41
|
+
<div className="space-y-3">
|
|
42
|
+
<div className="space-y-2">
|
|
43
|
+
<label className="block font-medium text-muted-foreground text-xs uppercase tracking-[0.12em]">
|
|
44
|
+
Color
|
|
45
|
+
</label>
|
|
46
|
+
<div className="flex items-center gap-2">
|
|
47
|
+
<input
|
|
48
|
+
className="h-9 w-9 shrink-0 cursor-pointer rounded-md border border-input bg-transparent p-0 [&::-moz-color-swatch]:rounded-[5px] [&::-moz-color-swatch]:border-none [&::-webkit-color-swatch-wrapper]:p-0 [&::-webkit-color-swatch]:rounded-[5px] [&::-webkit-color-swatch]:border-none"
|
|
49
|
+
onChange={(e) => updateMaterial({ color: e.target.value })}
|
|
50
|
+
type="color"
|
|
51
|
+
value={currentProps.color}
|
|
52
|
+
/>
|
|
53
|
+
<Input
|
|
54
|
+
onChange={(e) => updateMaterial({ color: e.target.value })}
|
|
55
|
+
value={currentProps.color}
|
|
56
|
+
/>
|
|
57
|
+
</div>
|
|
58
|
+
</div>
|
|
59
|
+
|
|
60
|
+
<SliderControl
|
|
61
|
+
label="Roughness"
|
|
62
|
+
max={1}
|
|
63
|
+
min={0}
|
|
64
|
+
onChange={(value) => updateMaterial({ roughness: value })}
|
|
65
|
+
precision={2}
|
|
66
|
+
step={0.01}
|
|
67
|
+
value={currentProps.roughness}
|
|
68
|
+
/>
|
|
69
|
+
|
|
70
|
+
<SliderControl
|
|
71
|
+
label="Metalness"
|
|
72
|
+
max={1}
|
|
73
|
+
min={0}
|
|
74
|
+
onChange={(value) => updateMaterial({ metalness: value })}
|
|
75
|
+
precision={2}
|
|
76
|
+
step={0.01}
|
|
77
|
+
value={currentProps.metalness}
|
|
78
|
+
/>
|
|
79
|
+
|
|
80
|
+
<SliderControl
|
|
81
|
+
label="Opacity"
|
|
82
|
+
max={1}
|
|
83
|
+
min={0}
|
|
84
|
+
onChange={(value) => updateMaterial({ opacity: value }, value < 1 || currentProps.transparent)}
|
|
85
|
+
precision={2}
|
|
86
|
+
step={0.01}
|
|
87
|
+
value={currentProps.opacity}
|
|
88
|
+
/>
|
|
89
|
+
|
|
90
|
+
<div className="space-y-2">
|
|
91
|
+
<label className="block font-medium text-muted-foreground text-xs uppercase tracking-[0.12em]">
|
|
92
|
+
Side
|
|
93
|
+
</label>
|
|
94
|
+
<select
|
|
95
|
+
className="h-9 w-full rounded-md border border-input bg-transparent px-3 text-sm shadow-xs outline-none transition-[color,box-shadow] focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 dark:bg-input/30"
|
|
96
|
+
onChange={(e) =>
|
|
97
|
+
updateMaterial({ side: e.target.value as 'front' | 'back' | 'double' })
|
|
98
|
+
}
|
|
99
|
+
value={currentProps.side}
|
|
100
|
+
>
|
|
101
|
+
<option value="front">Front</option>
|
|
102
|
+
<option value="back">Back</option>
|
|
103
|
+
<option value="double">Double</option>
|
|
104
|
+
</select>
|
|
105
|
+
</div>
|
|
106
|
+
</div>
|
|
107
|
+
)
|
|
108
|
+
}
|