@pascal-app/editor 0.4.0 → 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 +341 -48
- package/src/components/editor/floating-building-action-menu.tsx +70 -0
- package/src/components/editor/floorplan-panel.tsx +1350 -722
- package/src/components/editor/index.tsx +221 -167
- package/src/components/editor/node-action-menu.tsx +40 -11
- package/src/components/editor/selection-manager.tsx +238 -10
- package/src/components/editor/site-edge-labels.tsx +9 -3
- package/src/components/editor/thumbnail-generator.tsx +422 -79
- 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/systems/stair/stair-edit-system.tsx +27 -5
- package/src/components/tools/building/move-building-tool.tsx +157 -0
- 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-math.ts +1 -1
- package/src/components/tools/door/door-tool.tsx +31 -7
- package/src/components/tools/door/move-door-tool.tsx +27 -8
- package/src/components/tools/fence/curve-fence-tool.tsx +179 -0
- package/src/components/tools/fence/fence-drafting.ts +137 -0
- package/src/components/tools/fence/fence-tool.tsx +190 -0
- package/src/components/tools/fence/move-fence-endpoint-tool.tsx +327 -0
- package/src/components/tools/fence/move-fence-tool.tsx +231 -0
- package/src/components/tools/item/item-tool.tsx +3 -3
- package/src/components/tools/item/move-tool.tsx +16 -0
- package/src/components/tools/item/placement-math.ts +14 -6
- package/src/components/tools/item/placement-strategies.ts +17 -9
- package/src/components/tools/item/use-placement-coordinator.tsx +123 -16
- package/src/components/tools/roof/move-roof-tool.tsx +90 -26
- package/src/components/tools/roof/roof-tool.tsx +6 -6
- package/src/components/tools/select/box-select-tool.tsx +2 -2
- 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/slab/slab-tool.tsx +4 -4
- package/src/components/tools/stair/stair-defaults.ts +10 -0
- package/src/components/tools/stair/stair-tool.tsx +39 -8
- package/src/components/tools/tool-manager.tsx +54 -14
- 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/wall/wall-tool.tsx +19 -29
- package/src/components/tools/window/move-window-tool.tsx +27 -8
- package/src/components/tools/window/window-math.ts +1 -1
- package/src/components/tools/window/window-tool.tsx +31 -7
- package/src/components/tools/zone/zone-tool.tsx +7 -7
- package/src/components/ui/action-menu/control-modes.tsx +9 -4
- package/src/components/ui/action-menu/structure-tools.tsx +1 -0
- 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/helpers/building-helper.tsx +32 -0
- package/src/components/ui/helpers/helper-manager.tsx +2 -0
- 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 +269 -0
- package/src/components/ui/panels/item-panel.tsx +5 -5
- package/src/components/ui/panels/panel-manager.tsx +32 -27
- 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 +377 -50
- 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 +28 -17
- package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +65 -53
- package/src/components/ui/sidebar/panels/site-panel/door-tree-node.tsx +40 -25
- package/src/components/ui/sidebar/panels/site-panel/fence-tree-node.tsx +69 -0
- package/src/components/ui/sidebar/panels/site-panel/index.tsx +88 -72
- package/src/components/ui/sidebar/panels/site-panel/inline-rename-input.tsx +14 -13
- package/src/components/ui/sidebar/panels/site-panel/item-tree-node.tsx +64 -53
- package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +32 -23
- package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +72 -51
- package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +40 -37
- package/src/components/ui/sidebar/panels/site-panel/stair-tree-node.tsx +72 -51
- package/src/components/ui/sidebar/panels/site-panel/tree-node-actions.tsx +13 -13
- package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +20 -17
- package/src/components/ui/sidebar/panels/site-panel/wall-tree-node.tsx +62 -54
- package/src/components/ui/sidebar/panels/site-panel/window-tree-node.tsx +40 -25
- package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +27 -28
- package/src/components/ui/viewer-toolbar.tsx +55 -2
- package/src/components/viewer-overlay.tsx +26 -19
- package/src/hooks/use-auto-save.ts +3 -6
- package/src/hooks/use-contextual-tools.ts +25 -16
- package/src/hooks/use-grid-events.ts +13 -1
- package/src/hooks/use-keyboard.ts +7 -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 +125 -10
|
@@ -25,17 +25,17 @@ import { PanelWrapper } from './panel-wrapper'
|
|
|
25
25
|
import { PresetsPopover } from './presets/presets-popover'
|
|
26
26
|
|
|
27
27
|
export function DoorPanel() {
|
|
28
|
-
const
|
|
28
|
+
const selectedId = useViewer((s) => s.selection.selectedIds[0])
|
|
29
29
|
const setSelection = useViewer((s) => s.setSelection)
|
|
30
|
-
const nodes = useScene((s) => s.nodes)
|
|
31
30
|
const updateNode = useScene((s) => s.updateNode)
|
|
32
31
|
const deleteNode = useScene((s) => s.deleteNode)
|
|
33
32
|
const setMovingNode = useEditor((s) => s.setMovingNode)
|
|
34
33
|
|
|
35
34
|
const adapter = usePresetsAdapter()
|
|
36
35
|
|
|
37
|
-
const
|
|
38
|
-
|
|
36
|
+
const node = useScene((s) =>
|
|
37
|
+
selectedId ? (s.nodes[selectedId as AnyNode['id']] as DoorNode | undefined) : undefined,
|
|
38
|
+
)
|
|
39
39
|
|
|
40
40
|
const handleUpdate = useCallback(
|
|
41
41
|
(updates: Partial<DoorNode>) => {
|
|
@@ -182,7 +182,7 @@ export function DoorPanel() {
|
|
|
182
182
|
[handleUpdate],
|
|
183
183
|
)
|
|
184
184
|
|
|
185
|
-
if (!node
|
|
185
|
+
if (!(node && node.type === 'door' && selectedId)) return null
|
|
186
186
|
|
|
187
187
|
const hSum = node.segments.reduce((s, seg) => s + seg.heightRatio, 0)
|
|
188
188
|
const normHeights = node.segments.map((seg) => seg.heightRatio / hSum)
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
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'
|
|
14
|
+
import { useViewer } from '@pascal-app/viewer'
|
|
15
|
+
import { Move, Spline } from 'lucide-react'
|
|
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'
|
|
21
|
+
import { PanelSection } from '../controls/panel-section'
|
|
22
|
+
import { SegmentedControl } from '../controls/segmented-control'
|
|
23
|
+
import { SliderControl } from '../controls/slider-control'
|
|
24
|
+
import { PanelWrapper } from './panel-wrapper'
|
|
25
|
+
|
|
26
|
+
type FenceStyleValue = 'slat' | 'rail' | 'privacy'
|
|
27
|
+
type FenceBaseStyleValue = 'grounded' | 'floating'
|
|
28
|
+
|
|
29
|
+
const FENCE_STYLE_OPTIONS: { label: string; value: FenceStyleValue }[] = [
|
|
30
|
+
{ label: 'Slat', value: 'slat' },
|
|
31
|
+
{ label: 'Rail', value: 'rail' },
|
|
32
|
+
{ label: 'Privacy', value: 'privacy' },
|
|
33
|
+
]
|
|
34
|
+
|
|
35
|
+
const FENCE_BASE_STYLE_OPTIONS: { label: string; value: FenceBaseStyleValue }[] = [
|
|
36
|
+
{ label: 'Grounded', value: 'grounded' },
|
|
37
|
+
{ label: 'Floating', value: 'floating' },
|
|
38
|
+
]
|
|
39
|
+
|
|
40
|
+
export function FencePanel() {
|
|
41
|
+
const selectedId = useViewer((s) => s.selection.selectedIds[0])
|
|
42
|
+
const selectedCount = useViewer((s) => s.selection.selectedIds.length)
|
|
43
|
+
const setSelection = useViewer((s) => s.setSelection)
|
|
44
|
+
const updateNode = useScene((s) => s.updateNode)
|
|
45
|
+
const setMovingNode = useEditor((s) => s.setMovingNode)
|
|
46
|
+
const setCurvingFence = useEditor((s) => s.setCurvingFence)
|
|
47
|
+
|
|
48
|
+
const node = useScene((s) =>
|
|
49
|
+
selectedId ? (s.nodes[selectedId as AnyNode['id']] as FenceNode | undefined) : undefined,
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
const handleUpdate = useCallback(
|
|
53
|
+
(updates: Partial<FenceNode>) => {
|
|
54
|
+
if (!selectedId) return
|
|
55
|
+
updateNode(selectedId as AnyNode['id'], updates)
|
|
56
|
+
useScene.getState().dirtyNodes.add(selectedId as AnyNodeId)
|
|
57
|
+
},
|
|
58
|
+
[selectedId, updateNode],
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
const handleUpdateLength = useCallback(
|
|
62
|
+
(newLength: number) => {
|
|
63
|
+
if (!node || newLength <= 0) return
|
|
64
|
+
|
|
65
|
+
const dx = node.end[0] - node.start[0]
|
|
66
|
+
const dz = node.end[1] - node.start[1]
|
|
67
|
+
const currentLength = Math.sqrt(dx * dx + dz * dz)
|
|
68
|
+
if (currentLength === 0) return
|
|
69
|
+
|
|
70
|
+
const dirX = dx / currentLength
|
|
71
|
+
const dirZ = dz / currentLength
|
|
72
|
+
const newEnd: [number, number] = [
|
|
73
|
+
node.start[0] + dirX * newLength,
|
|
74
|
+
node.start[1] + dirZ * newLength,
|
|
75
|
+
]
|
|
76
|
+
|
|
77
|
+
handleUpdate({ end: newEnd })
|
|
78
|
+
},
|
|
79
|
+
[node, handleUpdate],
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
const handleClose = useCallback(() => {
|
|
83
|
+
setSelection({ selectedIds: [] })
|
|
84
|
+
}, [setSelection])
|
|
85
|
+
|
|
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
|
+
)
|
|
113
|
+
|
|
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)
|
|
119
|
+
|
|
120
|
+
return (
|
|
121
|
+
<PanelWrapper
|
|
122
|
+
icon="/icons/build.png"
|
|
123
|
+
onClose={handleClose}
|
|
124
|
+
title={node.name || 'Fence'}
|
|
125
|
+
width={300}
|
|
126
|
+
>
|
|
127
|
+
<PanelSection title="Style">
|
|
128
|
+
<SegmentedControl
|
|
129
|
+
onChange={(value) => handleUpdate({ style: value })}
|
|
130
|
+
options={FENCE_STYLE_OPTIONS}
|
|
131
|
+
value={node.style}
|
|
132
|
+
/>
|
|
133
|
+
<SegmentedControl
|
|
134
|
+
className="mt-2"
|
|
135
|
+
onChange={(value) => handleUpdate({ baseStyle: value })}
|
|
136
|
+
options={FENCE_BASE_STYLE_OPTIONS}
|
|
137
|
+
value={node.baseStyle}
|
|
138
|
+
/>
|
|
139
|
+
</PanelSection>
|
|
140
|
+
|
|
141
|
+
<PanelSection title="Dimensions">
|
|
142
|
+
<SliderControl
|
|
143
|
+
label="Length"
|
|
144
|
+
max={50}
|
|
145
|
+
min={0.1}
|
|
146
|
+
onChange={handleUpdateLength}
|
|
147
|
+
precision={2}
|
|
148
|
+
step={0.01}
|
|
149
|
+
unit="m"
|
|
150
|
+
value={length}
|
|
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
|
+
/>
|
|
162
|
+
<SliderControl
|
|
163
|
+
label="Height"
|
|
164
|
+
max={4}
|
|
165
|
+
min={0.4}
|
|
166
|
+
onChange={(value) => handleUpdate({ height: Math.max(0.4, value) })}
|
|
167
|
+
precision={2}
|
|
168
|
+
step={0.05}
|
|
169
|
+
unit="m"
|
|
170
|
+
value={node.height}
|
|
171
|
+
/>
|
|
172
|
+
<SliderControl
|
|
173
|
+
label="Thickness"
|
|
174
|
+
max={0.5}
|
|
175
|
+
min={0.03}
|
|
176
|
+
onChange={(value) => handleUpdate({ thickness: Math.max(0.03, value) })}
|
|
177
|
+
precision={3}
|
|
178
|
+
step={0.005}
|
|
179
|
+
unit="m"
|
|
180
|
+
value={node.thickness}
|
|
181
|
+
/>
|
|
182
|
+
</PanelSection>
|
|
183
|
+
|
|
184
|
+
<PanelSection title="Structure">
|
|
185
|
+
<SliderControl
|
|
186
|
+
label="Base Height"
|
|
187
|
+
max={1}
|
|
188
|
+
min={0.04}
|
|
189
|
+
onChange={(value) => handleUpdate({ baseHeight: Math.max(0.04, value) })}
|
|
190
|
+
precision={3}
|
|
191
|
+
step={0.01}
|
|
192
|
+
unit="m"
|
|
193
|
+
value={node.baseHeight}
|
|
194
|
+
/>
|
|
195
|
+
<SliderControl
|
|
196
|
+
label="Top Rail"
|
|
197
|
+
max={0.25}
|
|
198
|
+
min={0.01}
|
|
199
|
+
onChange={(value) => handleUpdate({ topRailHeight: Math.max(0.01, value) })}
|
|
200
|
+
precision={3}
|
|
201
|
+
step={0.005}
|
|
202
|
+
unit="m"
|
|
203
|
+
value={node.topRailHeight}
|
|
204
|
+
/>
|
|
205
|
+
<SliderControl
|
|
206
|
+
label="Post Spacing"
|
|
207
|
+
max={5}
|
|
208
|
+
min={0.2}
|
|
209
|
+
onChange={(value) => handleUpdate({ postSpacing: Math.max(0.2, value) })}
|
|
210
|
+
precision={2}
|
|
211
|
+
step={0.05}
|
|
212
|
+
unit="m"
|
|
213
|
+
value={node.postSpacing}
|
|
214
|
+
/>
|
|
215
|
+
<SliderControl
|
|
216
|
+
label="Post Size"
|
|
217
|
+
max={0.4}
|
|
218
|
+
min={0.01}
|
|
219
|
+
onChange={(value) => handleUpdate({ postSize: Math.max(0.01, value) })}
|
|
220
|
+
precision={3}
|
|
221
|
+
step={0.005}
|
|
222
|
+
unit="m"
|
|
223
|
+
value={node.postSize}
|
|
224
|
+
/>
|
|
225
|
+
<SliderControl
|
|
226
|
+
label="Ground Clear"
|
|
227
|
+
max={0.6}
|
|
228
|
+
min={0}
|
|
229
|
+
onChange={(value) => handleUpdate({ groundClearance: Math.max(0, value) })}
|
|
230
|
+
precision={3}
|
|
231
|
+
step={0.005}
|
|
232
|
+
unit="m"
|
|
233
|
+
value={node.groundClearance}
|
|
234
|
+
/>
|
|
235
|
+
<SliderControl
|
|
236
|
+
label="Edge Inset"
|
|
237
|
+
max={0.25}
|
|
238
|
+
min={0.005}
|
|
239
|
+
onChange={(value) => handleUpdate({ edgeInset: Math.max(0.005, value) })}
|
|
240
|
+
precision={3}
|
|
241
|
+
step={0.005}
|
|
242
|
+
unit="m"
|
|
243
|
+
value={node.edgeInset}
|
|
244
|
+
/>
|
|
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>
|
|
267
|
+
</PanelWrapper>
|
|
268
|
+
)
|
|
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
|
|
@@ -5,6 +5,7 @@ import { useViewer } from '@pascal-app/viewer'
|
|
|
5
5
|
import useEditor from '../../../store/use-editor'
|
|
6
6
|
import { CeilingPanel } from './ceiling-panel'
|
|
7
7
|
import { DoorPanel } from './door-panel'
|
|
8
|
+
import { FencePanel } from './fence-panel'
|
|
8
9
|
import { ItemPanel } from './item-panel'
|
|
9
10
|
import { ReferencePanel } from './reference-panel'
|
|
10
11
|
import { RoofPanel } from './roof-panel'
|
|
@@ -18,7 +19,13 @@ import { WindowPanel } from './window-panel'
|
|
|
18
19
|
export function PanelManager() {
|
|
19
20
|
const selectedIds = useViewer((s) => s.selection.selectedIds)
|
|
20
21
|
const selectedReferenceId = useEditor((s) => s.selectedReferenceId)
|
|
21
|
-
|
|
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
|
+
})
|
|
22
29
|
|
|
23
30
|
// Show reference panel if a reference is selected
|
|
24
31
|
if (selectedReferenceId) {
|
|
@@ -26,32 +33,30 @@ export function PanelManager() {
|
|
|
26
33
|
}
|
|
27
34
|
|
|
28
35
|
// Show appropriate panel based on selected node type
|
|
29
|
-
if (
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
return <WindowPanel />
|
|
54
|
-
}
|
|
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 />
|
|
55
60
|
}
|
|
56
61
|
}
|
|
57
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
|
)
|