@pascal-app/editor 0.5.1 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +8 -7
- package/src/components/editor/editor-layout-v2.tsx +9 -0
- package/src/components/editor/floating-action-menu.tsx +255 -34
- package/src/components/editor/floating-building-action-menu.tsx +4 -3
- package/src/components/editor/floorplan-panel.tsx +1323 -713
- package/src/components/editor/index.tsx +2 -0
- package/src/components/editor/node-action-menu.tsx +14 -1
- package/src/components/editor/selection-manager.tsx +200 -8
- package/src/components/editor/site-edge-labels.tsx +9 -3
- package/src/components/editor/thumbnail-generator.tsx +319 -157
- package/src/components/editor/wall-measurement-label.tsx +120 -32
- package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +272 -0
- package/src/components/systems/roof/roof-edit-system.tsx +5 -5
- package/src/components/tools/ceiling/ceiling-hole-editor.tsx +1 -0
- package/src/components/tools/ceiling/move-ceiling-tool.tsx +257 -0
- package/src/components/tools/door/door-tool.tsx +12 -0
- package/src/components/tools/door/move-door-tool.tsx +10 -0
- package/src/components/tools/fence/curve-fence-tool.tsx +179 -0
- package/src/components/tools/fence/fence-drafting.ts +19 -7
- package/src/components/tools/fence/move-fence-endpoint-tool.tsx +327 -0
- package/src/components/tools/fence/move-fence-tool.tsx +8 -0
- package/src/components/tools/item/move-tool.tsx +9 -0
- package/src/components/tools/item/placement-math.ts +14 -6
- package/src/components/tools/item/placement-strategies.ts +2 -2
- package/src/components/tools/item/use-placement-coordinator.tsx +42 -10
- package/src/components/tools/roof/move-roof-tool.tsx +89 -28
- package/src/components/tools/shared/polygon-editor.tsx +98 -8
- package/src/components/tools/slab/move-slab-tool.tsx +182 -0
- package/src/components/tools/slab/slab-hole-editor.tsx +1 -0
- package/src/components/tools/stair/stair-tool.tsx +11 -3
- package/src/components/tools/tool-manager.tsx +12 -0
- package/src/components/tools/wall/curve-wall-tool.tsx +176 -0
- package/src/components/tools/wall/move-wall-endpoint-tool.tsx +322 -0
- package/src/components/tools/wall/move-wall-tool.tsx +356 -0
- package/src/components/tools/wall/wall-drafting.ts +331 -9
- package/src/components/tools/window/move-window-tool.tsx +10 -0
- package/src/components/tools/window/window-tool.tsx +12 -0
- package/src/components/ui/action-menu/control-modes.tsx +9 -4
- package/src/components/ui/command-palette/editor-commands.tsx +9 -4
- package/src/components/ui/command-palette/index.tsx +0 -1
- package/src/components/ui/controls/material-picker.tsx +127 -94
- package/src/components/ui/controls/slider-control.tsx +28 -14
- package/src/components/ui/item-catalog/catalog-items.tsx +5 -0
- package/src/components/ui/panels/ceiling-panel.tsx +61 -17
- package/src/components/ui/panels/door-panel.tsx +5 -5
- package/src/components/ui/panels/fence-panel.tsx +97 -12
- package/src/components/ui/panels/item-panel.tsx +5 -5
- package/src/components/ui/panels/panel-manager.tsx +31 -29
- package/src/components/ui/panels/reference-panel.tsx +5 -4
- package/src/components/ui/panels/roof-panel.tsx +91 -22
- package/src/components/ui/panels/roof-segment-panel.tsx +23 -13
- package/src/components/ui/panels/slab-panel.tsx +63 -15
- package/src/components/ui/panels/stair-panel.tsx +173 -19
- package/src/components/ui/panels/stair-segment-panel.tsx +28 -17
- package/src/components/ui/panels/wall-panel.tsx +159 -11
- package/src/components/ui/panels/window-panel.tsx +5 -7
- package/src/components/ui/sidebar/panels/site-panel/building-tree-node.tsx +7 -3
- package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +7 -3
- package/src/components/ui/sidebar/panels/site-panel/door-tree-node.tsx +7 -3
- package/src/components/ui/sidebar/panels/site-panel/fence-tree-node.tsx +7 -3
- package/src/components/ui/sidebar/panels/site-panel/index.tsx +29 -32
- package/src/components/ui/sidebar/panels/site-panel/item-tree-node.tsx +7 -3
- package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +7 -3
- package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +7 -3
- package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +7 -3
- package/src/components/ui/sidebar/panels/site-panel/stair-tree-node.tsx +7 -3
- package/src/components/ui/sidebar/panels/site-panel/tree-node-actions.tsx +3 -3
- package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +3 -3
- package/src/components/ui/sidebar/panels/site-panel/wall-tree-node.tsx +7 -3
- package/src/components/ui/sidebar/panels/site-panel/window-tree-node.tsx +7 -3
- package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +7 -3
- package/src/components/ui/viewer-toolbar.tsx +55 -2
- package/src/components/viewer-overlay.tsx +25 -19
- package/src/hooks/use-contextual-tools.ts +14 -13
- package/src/hooks/use-keyboard.ts +3 -2
- package/src/index.tsx +2 -1
- package/src/lib/history.ts +20 -0
- package/src/lib/sfx-player.ts +96 -13
- package/src/store/use-editor.tsx +118 -10
|
@@ -1,32 +1,53 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
type AnyNode,
|
|
5
|
+
type AnyNodeId,
|
|
6
|
+
type FenceNode,
|
|
7
|
+
getClampedWallCurveOffset,
|
|
8
|
+
getMaxWallCurveOffset,
|
|
9
|
+
getWallCurveLength,
|
|
10
|
+
type MaterialSchema,
|
|
11
|
+
normalizeWallCurveOffset,
|
|
12
|
+
useScene,
|
|
13
|
+
} from '@pascal-app/core'
|
|
4
14
|
import { useViewer } from '@pascal-app/viewer'
|
|
15
|
+
import { Move, Spline } from 'lucide-react'
|
|
5
16
|
import { useCallback } from 'react'
|
|
17
|
+
import { sfxEmitter } from '../../../lib/sfx-bus'
|
|
18
|
+
import useEditor from '../../../store/use-editor'
|
|
19
|
+
import { ActionButton, ActionGroup } from '../controls/action-button'
|
|
20
|
+
import { MaterialPicker } from '../controls/material-picker'
|
|
6
21
|
import { PanelSection } from '../controls/panel-section'
|
|
7
22
|
import { SegmentedControl } from '../controls/segmented-control'
|
|
8
23
|
import { SliderControl } from '../controls/slider-control'
|
|
9
24
|
import { PanelWrapper } from './panel-wrapper'
|
|
10
25
|
|
|
11
|
-
|
|
26
|
+
type FenceStyleValue = 'slat' | 'rail' | 'privacy'
|
|
27
|
+
type FenceBaseStyleValue = 'grounded' | 'floating'
|
|
28
|
+
|
|
29
|
+
const FENCE_STYLE_OPTIONS: { label: string; value: FenceStyleValue }[] = [
|
|
12
30
|
{ label: 'Slat', value: 'slat' },
|
|
13
31
|
{ label: 'Rail', value: 'rail' },
|
|
14
32
|
{ label: 'Privacy', value: 'privacy' },
|
|
15
33
|
]
|
|
16
34
|
|
|
17
|
-
const FENCE_BASE_STYLE_OPTIONS: { label: string; value:
|
|
35
|
+
const FENCE_BASE_STYLE_OPTIONS: { label: string; value: FenceBaseStyleValue }[] = [
|
|
18
36
|
{ label: 'Grounded', value: 'grounded' },
|
|
19
37
|
{ label: 'Floating', value: 'floating' },
|
|
20
38
|
]
|
|
21
39
|
|
|
22
40
|
export function FencePanel() {
|
|
23
|
-
const
|
|
41
|
+
const selectedId = useViewer((s) => s.selection.selectedIds[0])
|
|
42
|
+
const selectedCount = useViewer((s) => s.selection.selectedIds.length)
|
|
24
43
|
const setSelection = useViewer((s) => s.setSelection)
|
|
25
|
-
const nodes = useScene((s) => s.nodes)
|
|
26
44
|
const updateNode = useScene((s) => s.updateNode)
|
|
45
|
+
const setMovingNode = useEditor((s) => s.setMovingNode)
|
|
46
|
+
const setCurvingFence = useEditor((s) => s.setCurvingFence)
|
|
27
47
|
|
|
28
|
-
const
|
|
29
|
-
|
|
48
|
+
const node = useScene((s) =>
|
|
49
|
+
selectedId ? (s.nodes[selectedId as AnyNode['id']] as FenceNode | undefined) : undefined,
|
|
50
|
+
)
|
|
30
51
|
|
|
31
52
|
const handleUpdate = useCallback(
|
|
32
53
|
(updates: Partial<FenceNode>) => {
|
|
@@ -62,14 +83,47 @@ export function FencePanel() {
|
|
|
62
83
|
setSelection({ selectedIds: [] })
|
|
63
84
|
}, [setSelection])
|
|
64
85
|
|
|
65
|
-
|
|
86
|
+
const handleMove = useCallback(() => {
|
|
87
|
+
if (!node) return
|
|
88
|
+
sfxEmitter.emit('sfx:item-pick')
|
|
89
|
+
setMovingNode(node)
|
|
90
|
+
setSelection({ selectedIds: [] })
|
|
91
|
+
}, [node, setMovingNode, setSelection])
|
|
92
|
+
|
|
93
|
+
const handleCurve = useCallback(() => {
|
|
94
|
+
if (!node) return
|
|
95
|
+
sfxEmitter.emit('sfx:item-pick')
|
|
96
|
+
setCurvingFence(node)
|
|
97
|
+
setSelection({ selectedIds: [] })
|
|
98
|
+
}, [node, setCurvingFence, setSelection])
|
|
99
|
+
|
|
100
|
+
const handleMaterialPresetChange = useCallback(
|
|
101
|
+
(materialPreset: string) => {
|
|
102
|
+
handleUpdate({ materialPreset, material: undefined })
|
|
103
|
+
},
|
|
104
|
+
[handleUpdate],
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
const handleCustomMaterialChange = useCallback(
|
|
108
|
+
(material: MaterialSchema) => {
|
|
109
|
+
handleUpdate({ material, materialPreset: undefined })
|
|
110
|
+
},
|
|
111
|
+
[handleUpdate],
|
|
112
|
+
)
|
|
66
113
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
const length =
|
|
114
|
+
if (!(node && node.type === 'fence' && selectedId && selectedCount === 1)) return null
|
|
115
|
+
|
|
116
|
+
const length = getWallCurveLength(node)
|
|
117
|
+
const curveOffset = getClampedWallCurveOffset(node)
|
|
118
|
+
const maxCurveOffset = getMaxWallCurveOffset(node)
|
|
70
119
|
|
|
71
120
|
return (
|
|
72
|
-
<PanelWrapper
|
|
121
|
+
<PanelWrapper
|
|
122
|
+
icon="/icons/build.png"
|
|
123
|
+
onClose={handleClose}
|
|
124
|
+
title={node.name || 'Fence'}
|
|
125
|
+
width={300}
|
|
126
|
+
>
|
|
73
127
|
<PanelSection title="Style">
|
|
74
128
|
<SegmentedControl
|
|
75
129
|
onChange={(value) => handleUpdate({ style: value })}
|
|
@@ -95,6 +149,16 @@ export function FencePanel() {
|
|
|
95
149
|
unit="m"
|
|
96
150
|
value={length}
|
|
97
151
|
/>
|
|
152
|
+
<SliderControl
|
|
153
|
+
label="Curve"
|
|
154
|
+
max={Math.max(0.01, maxCurveOffset)}
|
|
155
|
+
min={-Math.max(0.01, maxCurveOffset)}
|
|
156
|
+
onChange={(value) => handleUpdate({ curveOffset: normalizeWallCurveOffset(node, value) })}
|
|
157
|
+
precision={2}
|
|
158
|
+
step={0.1}
|
|
159
|
+
unit="m"
|
|
160
|
+
value={Math.round(curveOffset * 100) / 100}
|
|
161
|
+
/>
|
|
98
162
|
<SliderControl
|
|
99
163
|
label="Height"
|
|
100
164
|
max={4}
|
|
@@ -179,6 +243,27 @@ export function FencePanel() {
|
|
|
179
243
|
value={node.edgeInset}
|
|
180
244
|
/>
|
|
181
245
|
</PanelSection>
|
|
246
|
+
|
|
247
|
+
<PanelSection title="Material">
|
|
248
|
+
<MaterialPicker
|
|
249
|
+
nodeType="fence"
|
|
250
|
+
onChange={handleCustomMaterialChange}
|
|
251
|
+
onSelectMaterialPreset={handleMaterialPresetChange}
|
|
252
|
+
selectedMaterialPreset={node.materialPreset}
|
|
253
|
+
value={node.material}
|
|
254
|
+
/>
|
|
255
|
+
</PanelSection>
|
|
256
|
+
|
|
257
|
+
<PanelSection title="Actions">
|
|
258
|
+
<ActionGroup>
|
|
259
|
+
<ActionButton icon={<Move className="h-3.5 w-3.5" />} label="Move" onClick={handleMove} />
|
|
260
|
+
<ActionButton
|
|
261
|
+
icon={<Spline className="h-3.5 w-3.5" />}
|
|
262
|
+
label="Curve"
|
|
263
|
+
onClick={handleCurve}
|
|
264
|
+
/>
|
|
265
|
+
</ActionGroup>
|
|
266
|
+
</PanelSection>
|
|
182
267
|
</PanelWrapper>
|
|
183
268
|
)
|
|
184
269
|
}
|
|
@@ -14,15 +14,15 @@ import { CollectionsPopover } from './collections/collections-popover'
|
|
|
14
14
|
import { PanelWrapper } from './panel-wrapper'
|
|
15
15
|
|
|
16
16
|
export function ItemPanel() {
|
|
17
|
-
const
|
|
17
|
+
const selectedId = useViewer((s) => s.selection.selectedIds[0])
|
|
18
18
|
const setSelection = useViewer((s) => s.setSelection)
|
|
19
|
-
const nodes = useScene((s) => s.nodes)
|
|
20
19
|
const updateNode = useScene((s) => s.updateNode)
|
|
21
20
|
const deleteNode = useScene((s) => s.deleteNode)
|
|
22
21
|
const setMovingNode = useEditor((s) => s.setMovingNode)
|
|
23
22
|
|
|
24
|
-
const
|
|
25
|
-
|
|
23
|
+
const node = useScene((s) =>
|
|
24
|
+
selectedId ? (s.nodes[selectedId as AnyNode['id']] as ItemNode | undefined) : undefined,
|
|
25
|
+
)
|
|
26
26
|
|
|
27
27
|
const [uniformScale, setUniformScale] = useState(true)
|
|
28
28
|
|
|
@@ -75,7 +75,7 @@ export function ItemPanel() {
|
|
|
75
75
|
setSelection({ selectedIds: [] })
|
|
76
76
|
}, [selectedId, deleteNode, setSelection])
|
|
77
77
|
|
|
78
|
-
if (!node
|
|
78
|
+
if (!(node && node.type === 'item' && selectedId)) return null
|
|
79
79
|
|
|
80
80
|
return (
|
|
81
81
|
<PanelWrapper
|
|
@@ -19,7 +19,13 @@ import { WindowPanel } from './window-panel'
|
|
|
19
19
|
export function PanelManager() {
|
|
20
20
|
const selectedIds = useViewer((s) => s.selection.selectedIds)
|
|
21
21
|
const selectedReferenceId = useEditor((s) => s.selectedReferenceId)
|
|
22
|
-
|
|
22
|
+
// Only subscribe to the *type* of the single-selected node — string primitive
|
|
23
|
+
// so we don't re-render on unrelated scene mutations.
|
|
24
|
+
const selectedNodeType = useScene((s) => {
|
|
25
|
+
if (selectedIds.length !== 1) return null
|
|
26
|
+
const id = selectedIds[0]
|
|
27
|
+
return id ? (s.nodes[id as AnyNodeId]?.type ?? null) : null
|
|
28
|
+
})
|
|
23
29
|
|
|
24
30
|
// Show reference panel if a reference is selected
|
|
25
31
|
if (selectedReferenceId) {
|
|
@@ -27,34 +33,30 @@ export function PanelManager() {
|
|
|
27
33
|
}
|
|
28
34
|
|
|
29
35
|
// Show appropriate panel based on selected node type
|
|
30
|
-
if (
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
return <DoorPanel />
|
|
55
|
-
case 'window':
|
|
56
|
-
return <WindowPanel />
|
|
57
|
-
}
|
|
36
|
+
if (selectedNodeType) {
|
|
37
|
+
switch (selectedNodeType) {
|
|
38
|
+
case 'item':
|
|
39
|
+
return <ItemPanel />
|
|
40
|
+
case 'roof':
|
|
41
|
+
return <RoofPanel />
|
|
42
|
+
case 'roof-segment':
|
|
43
|
+
return <RoofSegmentPanel />
|
|
44
|
+
case 'stair':
|
|
45
|
+
return <StairPanel />
|
|
46
|
+
case 'stair-segment':
|
|
47
|
+
return <StairSegmentPanel />
|
|
48
|
+
case 'slab':
|
|
49
|
+
return <SlabPanel />
|
|
50
|
+
case 'ceiling':
|
|
51
|
+
return <CeilingPanel />
|
|
52
|
+
case 'wall':
|
|
53
|
+
return <WallPanel />
|
|
54
|
+
case 'fence':
|
|
55
|
+
return <FencePanel />
|
|
56
|
+
case 'door':
|
|
57
|
+
return <DoorPanel />
|
|
58
|
+
case 'window':
|
|
59
|
+
return <WindowPanel />
|
|
58
60
|
}
|
|
59
61
|
}
|
|
60
62
|
|
|
@@ -15,12 +15,13 @@ type ReferenceNode = ScanNode | GuideNode
|
|
|
15
15
|
export function ReferencePanel() {
|
|
16
16
|
const selectedReferenceId = useEditor((s) => s.selectedReferenceId)
|
|
17
17
|
const setSelectedReferenceId = useEditor((s) => s.setSelectedReferenceId)
|
|
18
|
-
const nodes = useScene((s) => s.nodes)
|
|
19
18
|
const updateNode = useScene((s) => s.updateNode)
|
|
20
19
|
|
|
21
|
-
const node =
|
|
22
|
-
|
|
23
|
-
|
|
20
|
+
const node = useScene((s) =>
|
|
21
|
+
selectedReferenceId
|
|
22
|
+
? (s.nodes[selectedReferenceId as AnyNode['id']] as ReferenceNode | undefined)
|
|
23
|
+
: undefined,
|
|
24
|
+
)
|
|
24
25
|
|
|
25
26
|
const handleUpdate = useCallback(
|
|
26
27
|
(updates: Partial<ReferenceNode>) => {
|
|
@@ -3,8 +3,10 @@
|
|
|
3
3
|
import {
|
|
4
4
|
type AnyNode,
|
|
5
5
|
type AnyNodeId,
|
|
6
|
+
getEffectiveRoofSurfaceMaterial,
|
|
6
7
|
type MaterialSchema,
|
|
7
8
|
type RoofNode,
|
|
9
|
+
type RoofSurfaceMaterialRole,
|
|
8
10
|
RoofNode as RoofNodeSchema,
|
|
9
11
|
type RoofSegmentNode,
|
|
10
12
|
RoofSegmentNode as RoofSegmentNodeSchema,
|
|
@@ -13,25 +15,61 @@ import {
|
|
|
13
15
|
import { useViewer } from '@pascal-app/viewer'
|
|
14
16
|
import { Copy, Move, Plus, Trash2 } from 'lucide-react'
|
|
15
17
|
import { useCallback } from 'react'
|
|
18
|
+
import { useShallow } from 'zustand/react/shallow'
|
|
16
19
|
import { sfxEmitter } from '../../../lib/sfx-bus'
|
|
17
20
|
import useEditor from '../../../store/use-editor'
|
|
18
21
|
import { ActionButton, ActionGroup } from '../controls/action-button'
|
|
19
22
|
import { MaterialPicker } from '../controls/material-picker'
|
|
20
|
-
import { MetricControl } from '../controls/metric-control'
|
|
21
23
|
import { PanelSection } from '../controls/panel-section'
|
|
22
24
|
import { SliderControl } from '../controls/slider-control'
|
|
23
25
|
import { PanelWrapper } from './panel-wrapper'
|
|
24
26
|
|
|
27
|
+
function buildRoofSurfaceMaterialPatch(
|
|
28
|
+
node: RoofNode,
|
|
29
|
+
targetRole: RoofSurfaceMaterialRole,
|
|
30
|
+
material: MaterialSchema | undefined,
|
|
31
|
+
materialPreset: string | undefined,
|
|
32
|
+
): Partial<RoofNode> {
|
|
33
|
+
const nextSurfaceMaterial = { material, materialPreset }
|
|
34
|
+
const nextTop =
|
|
35
|
+
targetRole === 'top' ? nextSurfaceMaterial : getEffectiveRoofSurfaceMaterial(node, 'top')
|
|
36
|
+
const nextEdge =
|
|
37
|
+
targetRole === 'edge' ? nextSurfaceMaterial : getEffectiveRoofSurfaceMaterial(node, 'edge')
|
|
38
|
+
const nextWall =
|
|
39
|
+
targetRole === 'wall' ? nextSurfaceMaterial : getEffectiveRoofSurfaceMaterial(node, 'wall')
|
|
40
|
+
|
|
41
|
+
return {
|
|
42
|
+
topMaterial: nextTop.material,
|
|
43
|
+
topMaterialPreset: nextTop.materialPreset,
|
|
44
|
+
edgeMaterial: nextEdge.material,
|
|
45
|
+
edgeMaterialPreset: nextEdge.materialPreset,
|
|
46
|
+
wallMaterial: nextWall.material,
|
|
47
|
+
wallMaterialPreset: nextWall.materialPreset,
|
|
48
|
+
material: undefined,
|
|
49
|
+
materialPreset: undefined,
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
25
53
|
export function RoofPanel() {
|
|
26
|
-
const
|
|
54
|
+
const selectedId = useViewer((s) => s.selection.selectedIds[0])
|
|
27
55
|
const setSelection = useViewer((s) => s.setSelection)
|
|
28
|
-
const nodes = useScene((s) => s.nodes)
|
|
29
56
|
const updateNode = useScene((s) => s.updateNode)
|
|
30
57
|
const createNode = useScene((s) => s.createNode)
|
|
31
58
|
const setMovingNode = useEditor((s) => s.setMovingNode)
|
|
59
|
+
const selectedMaterialTarget = useEditor((s) => s.selectedMaterialTarget)
|
|
32
60
|
|
|
33
|
-
const
|
|
34
|
-
|
|
61
|
+
const node = useScene((s) =>
|
|
62
|
+
selectedId ? (s.nodes[selectedId as AnyNode['id']] as RoofNode | undefined) : undefined,
|
|
63
|
+
)
|
|
64
|
+
// Shallow selector — only re-renders when the segment list content changes.
|
|
65
|
+
const segments = useScene(
|
|
66
|
+
useShallow((s) => {
|
|
67
|
+
if (!node) return []
|
|
68
|
+
return (node.children ?? [])
|
|
69
|
+
.map((childId) => s.nodes[childId as AnyNodeId] as RoofSegmentNode | undefined)
|
|
70
|
+
.filter((n): n is RoofSegmentNode => n?.type === 'roof-segment')
|
|
71
|
+
}),
|
|
72
|
+
)
|
|
35
73
|
|
|
36
74
|
const handleUpdate = useCallback(
|
|
37
75
|
(updates: Partial<RoofNode>) => {
|
|
@@ -41,11 +79,31 @@ export function RoofPanel() {
|
|
|
41
79
|
[selectedId, updateNode],
|
|
42
80
|
)
|
|
43
81
|
|
|
44
|
-
const
|
|
82
|
+
const materialTargetRole =
|
|
83
|
+
selectedMaterialTarget &&
|
|
84
|
+
selectedMaterialTarget.nodeId === node?.id &&
|
|
85
|
+
(selectedMaterialTarget.role === 'top' ||
|
|
86
|
+
selectedMaterialTarget.role === 'edge' ||
|
|
87
|
+
selectedMaterialTarget.role === 'wall')
|
|
88
|
+
? selectedMaterialTarget.role
|
|
89
|
+
: null
|
|
90
|
+
const materialPickerValue =
|
|
91
|
+
node && materialTargetRole ? getEffectiveRoofSurfaceMaterial(node, materialTargetRole) : {}
|
|
92
|
+
|
|
93
|
+
const handleTargetedMaterialChange = useCallback(
|
|
45
94
|
(material: MaterialSchema) => {
|
|
46
|
-
|
|
95
|
+
if (!node || !materialTargetRole) return
|
|
96
|
+
handleUpdate(buildRoofSurfaceMaterialPatch(node, materialTargetRole, material, undefined))
|
|
47
97
|
},
|
|
48
|
-
[handleUpdate],
|
|
98
|
+
[handleUpdate, materialTargetRole, node],
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
const handleTargetedMaterialPresetChange = useCallback(
|
|
102
|
+
(materialPreset: string) => {
|
|
103
|
+
if (!node || !materialTargetRole) return
|
|
104
|
+
handleUpdate(buildRoofSurfaceMaterialPatch(node, materialTargetRole, undefined, materialPreset))
|
|
105
|
+
},
|
|
106
|
+
[handleUpdate, materialTargetRole, node],
|
|
49
107
|
)
|
|
50
108
|
|
|
51
109
|
const handleClose = useCallback(() => {
|
|
@@ -131,11 +189,7 @@ export function RoofPanel() {
|
|
|
131
189
|
setSelection({ selectedIds: [] })
|
|
132
190
|
}, [selectedId, node, setSelection])
|
|
133
191
|
|
|
134
|
-
if (!node
|
|
135
|
-
|
|
136
|
-
const segments = (node.children ?? [])
|
|
137
|
-
.map((childId) => nodes[childId as AnyNodeId] as RoofSegmentNode | undefined)
|
|
138
|
-
.filter((n): n is RoofSegmentNode => n?.type === 'roof-segment')
|
|
192
|
+
if (!(node && node.type === 'roof' && selectedId)) return null
|
|
139
193
|
|
|
140
194
|
return (
|
|
141
195
|
<PanelWrapper
|
|
@@ -158,15 +212,17 @@ export function RoofPanel() {
|
|
|
158
212
|
</button>
|
|
159
213
|
))}
|
|
160
214
|
</div>
|
|
161
|
-
<
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
215
|
+
<ActionGroup>
|
|
216
|
+
<ActionButton
|
|
217
|
+
icon={<Plus className="h-3.5 w-3.5" />}
|
|
218
|
+
label="Add Segment"
|
|
219
|
+
onClick={handleAddSegment}
|
|
220
|
+
/>
|
|
221
|
+
</ActionGroup>
|
|
166
222
|
</PanelSection>
|
|
167
223
|
|
|
168
224
|
<PanelSection title="Position">
|
|
169
|
-
<
|
|
225
|
+
<SliderControl
|
|
170
226
|
label="X"
|
|
171
227
|
max={50}
|
|
172
228
|
min={-50}
|
|
@@ -180,7 +236,7 @@ export function RoofPanel() {
|
|
|
180
236
|
unit="m"
|
|
181
237
|
value={Math.round(node.position[0] * 100) / 100}
|
|
182
238
|
/>
|
|
183
|
-
<
|
|
239
|
+
<SliderControl
|
|
184
240
|
label="Y"
|
|
185
241
|
max={50}
|
|
186
242
|
min={-50}
|
|
@@ -194,7 +250,7 @@ export function RoofPanel() {
|
|
|
194
250
|
unit="m"
|
|
195
251
|
value={Math.round(node.position[1] * 100) / 100}
|
|
196
252
|
/>
|
|
197
|
-
<
|
|
253
|
+
<SliderControl
|
|
198
254
|
label="Z"
|
|
199
255
|
max={50}
|
|
200
256
|
min={-50}
|
|
@@ -255,7 +311,20 @@ export function RoofPanel() {
|
|
|
255
311
|
</ActionGroup>
|
|
256
312
|
</PanelSection>
|
|
257
313
|
<PanelSection title="Material">
|
|
258
|
-
|
|
314
|
+
{!materialTargetRole ? (
|
|
315
|
+
<div className="mb-3 rounded-lg border border-border/50 bg-[#2C2C2E] px-3 py-2 text-[11px] text-muted-foreground">
|
|
316
|
+
Click the roof surface you want to edit. Materials apply to one target at a time.
|
|
317
|
+
</div>
|
|
318
|
+
) : null}
|
|
319
|
+
<MaterialPicker
|
|
320
|
+
disabled={!materialTargetRole}
|
|
321
|
+
hideSideControl
|
|
322
|
+
nodeType="roof"
|
|
323
|
+
onChange={handleTargetedMaterialChange}
|
|
324
|
+
onSelectMaterialPreset={handleTargetedMaterialPresetChange}
|
|
325
|
+
selectedMaterialPreset={materialPickerValue.materialPreset}
|
|
326
|
+
value={materialPickerValue.material}
|
|
327
|
+
/>
|
|
259
328
|
</PanelSection>
|
|
260
329
|
</PanelWrapper>
|
|
261
330
|
)
|
|
@@ -16,7 +16,6 @@ import { sfxEmitter } from '../../../lib/sfx-bus'
|
|
|
16
16
|
import useEditor from '../../../store/use-editor'
|
|
17
17
|
import { ActionButton, ActionGroup } from '../controls/action-button'
|
|
18
18
|
import { MaterialPicker } from '../controls/material-picker'
|
|
19
|
-
import { MetricControl } from '../controls/metric-control'
|
|
20
19
|
import { PanelSection } from '../controls/panel-section'
|
|
21
20
|
import { SegmentedControl } from '../controls/segmented-control'
|
|
22
21
|
import { SliderControl } from '../controls/slider-control'
|
|
@@ -36,16 +35,14 @@ const ROOF_TYPE_OPTIONS_2: { label: string; value: RoofType }[] = [
|
|
|
36
35
|
]
|
|
37
36
|
|
|
38
37
|
export function RoofSegmentPanel() {
|
|
39
|
-
const
|
|
38
|
+
const selectedId = useViewer((s) => s.selection.selectedIds[0])
|
|
40
39
|
const setSelection = useViewer((s) => s.setSelection)
|
|
41
|
-
const nodes = useScene((s) => s.nodes)
|
|
42
40
|
const updateNode = useScene((s) => s.updateNode)
|
|
43
41
|
const setMovingNode = useEditor((s) => s.setMovingNode)
|
|
44
42
|
|
|
45
|
-
const
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
: undefined
|
|
43
|
+
const node = useScene((s) =>
|
|
44
|
+
selectedId ? (s.nodes[selectedId as AnyNode['id']] as RoofSegmentNode | undefined) : undefined,
|
|
45
|
+
)
|
|
49
46
|
|
|
50
47
|
const handleUpdate = useCallback(
|
|
51
48
|
(updates: Partial<RoofSegmentNode>) => {
|
|
@@ -57,7 +54,14 @@ export function RoofSegmentPanel() {
|
|
|
57
54
|
|
|
58
55
|
const handleMaterialChange = useCallback(
|
|
59
56
|
(material: MaterialSchema) => {
|
|
60
|
-
handleUpdate({ material })
|
|
57
|
+
handleUpdate({ material, materialPreset: undefined })
|
|
58
|
+
},
|
|
59
|
+
[handleUpdate],
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
const handleMaterialPresetChange = useCallback(
|
|
63
|
+
(materialPreset: string) => {
|
|
64
|
+
handleUpdate({ materialPreset, material: undefined })
|
|
61
65
|
},
|
|
62
66
|
[handleUpdate],
|
|
63
67
|
)
|
|
@@ -117,7 +121,7 @@ export function RoofSegmentPanel() {
|
|
|
117
121
|
}
|
|
118
122
|
}, [selectedId, node, setSelection])
|
|
119
123
|
|
|
120
|
-
if (!node
|
|
124
|
+
if (!(node && node.type === 'roof-segment' && selectedId)) return null
|
|
121
125
|
|
|
122
126
|
return (
|
|
123
127
|
<PanelWrapper
|
|
@@ -230,7 +234,7 @@ export function RoofSegmentPanel() {
|
|
|
230
234
|
</PanelSection>
|
|
231
235
|
|
|
232
236
|
<PanelSection title="Position">
|
|
233
|
-
<
|
|
237
|
+
<SliderControl
|
|
234
238
|
label="X"
|
|
235
239
|
max={50}
|
|
236
240
|
min={-50}
|
|
@@ -244,7 +248,7 @@ export function RoofSegmentPanel() {
|
|
|
244
248
|
unit="m"
|
|
245
249
|
value={Math.round(node.position[0] * 100) / 100}
|
|
246
250
|
/>
|
|
247
|
-
<
|
|
251
|
+
<SliderControl
|
|
248
252
|
label="Y"
|
|
249
253
|
max={50}
|
|
250
254
|
min={-50}
|
|
@@ -258,7 +262,7 @@ export function RoofSegmentPanel() {
|
|
|
258
262
|
unit="m"
|
|
259
263
|
value={Math.round(node.position[1] * 100) / 100}
|
|
260
264
|
/>
|
|
261
|
-
<
|
|
265
|
+
<SliderControl
|
|
262
266
|
label="Z"
|
|
263
267
|
max={50}
|
|
264
268
|
min={-50}
|
|
@@ -319,7 +323,13 @@ export function RoofSegmentPanel() {
|
|
|
319
323
|
</ActionGroup>
|
|
320
324
|
</PanelSection>
|
|
321
325
|
<PanelSection title="Material">
|
|
322
|
-
<MaterialPicker
|
|
326
|
+
<MaterialPicker
|
|
327
|
+
nodeType="roof-segment"
|
|
328
|
+
onChange={handleMaterialChange}
|
|
329
|
+
onSelectMaterialPreset={handleMaterialPresetChange}
|
|
330
|
+
selectedMaterialPreset={node.materialPreset}
|
|
331
|
+
value={node.material}
|
|
332
|
+
/>
|
|
323
333
|
</PanelSection>
|
|
324
334
|
</PanelWrapper>
|
|
325
335
|
)
|