@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
|
@@ -2,8 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
import { type AnyNode, type MaterialSchema, type SlabNode, useScene } from '@pascal-app/core'
|
|
4
4
|
import { useViewer } from '@pascal-app/viewer'
|
|
5
|
-
import { Edit, Plus, Trash2 } from 'lucide-react'
|
|
5
|
+
import { Edit, Move, Plus, Trash2 } from 'lucide-react'
|
|
6
6
|
import { useCallback, useEffect } from 'react'
|
|
7
|
+
import { sfxEmitter } from '../../../lib/sfx-bus'
|
|
7
8
|
import useEditor from '../../../store/use-editor'
|
|
8
9
|
import { ActionButton, ActionGroup } from '../controls/action-button'
|
|
9
10
|
import { MaterialPicker } from '../controls/material-picker'
|
|
@@ -12,15 +13,16 @@ import { SliderControl } from '../controls/slider-control'
|
|
|
12
13
|
import { PanelWrapper } from './panel-wrapper'
|
|
13
14
|
|
|
14
15
|
export function SlabPanel() {
|
|
15
|
-
const
|
|
16
|
+
const selectedId = useViewer((s) => s.selection.selectedIds[0])
|
|
16
17
|
const setSelection = useViewer((s) => s.setSelection)
|
|
17
|
-
const nodes = useScene((s) => s.nodes)
|
|
18
18
|
const updateNode = useScene((s) => s.updateNode)
|
|
19
19
|
const editingHole = useEditor((s) => s.editingHole)
|
|
20
20
|
const setEditingHole = useEditor((s) => s.setEditingHole)
|
|
21
|
+
const setMovingNode = useEditor((s) => s.setMovingNode)
|
|
21
22
|
|
|
22
|
-
const
|
|
23
|
-
|
|
23
|
+
const node = useScene((s) =>
|
|
24
|
+
selectedId ? (s.nodes[selectedId as AnyNode['id']] as SlabNode | undefined) : undefined,
|
|
25
|
+
)
|
|
24
26
|
|
|
25
27
|
const handleUpdate = useCallback(
|
|
26
28
|
(updates: Partial<SlabNode>) => {
|
|
@@ -30,9 +32,16 @@ export function SlabPanel() {
|
|
|
30
32
|
[selectedId, updateNode],
|
|
31
33
|
)
|
|
32
34
|
|
|
33
|
-
const
|
|
35
|
+
const handleMaterialPresetChange = useCallback(
|
|
36
|
+
(materialPreset: string) => {
|
|
37
|
+
handleUpdate({ materialPreset, material: undefined })
|
|
38
|
+
},
|
|
39
|
+
[handleUpdate],
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
const handleCustomMaterialChange = useCallback(
|
|
34
43
|
(material: MaterialSchema) => {
|
|
35
|
-
handleUpdate({ material })
|
|
44
|
+
handleUpdate({ material, materialPreset: undefined })
|
|
36
45
|
},
|
|
37
46
|
[handleUpdate],
|
|
38
47
|
)
|
|
@@ -75,7 +84,13 @@ export function SlabPanel() {
|
|
|
75
84
|
[cx - holeSize, cz + holeSize],
|
|
76
85
|
]
|
|
77
86
|
const currentHoles = node?.holes || []
|
|
78
|
-
|
|
87
|
+
const currentMetadata = currentHoles.map(
|
|
88
|
+
(_, index) => node?.holeMetadata?.[index] ?? { source: 'manual' as const },
|
|
89
|
+
)
|
|
90
|
+
handleUpdate({
|
|
91
|
+
holes: [...currentHoles, newHole],
|
|
92
|
+
holeMetadata: [...currentMetadata, { source: 'manual' }],
|
|
93
|
+
})
|
|
79
94
|
setEditingHole({ nodeId: selectedId, holeIndex: currentHoles.length })
|
|
80
95
|
}, [node, selectedId, handleUpdate, setEditingHole])
|
|
81
96
|
|
|
@@ -91,16 +106,28 @@ export function SlabPanel() {
|
|
|
91
106
|
(index: number) => {
|
|
92
107
|
if (!selectedId) return
|
|
93
108
|
const currentHoles = node?.holes || []
|
|
109
|
+
if (node?.holeMetadata?.[index]?.source === 'stair') return
|
|
94
110
|
const newHoles = currentHoles.filter((_, i) => i !== index)
|
|
95
|
-
|
|
111
|
+
const currentMetadata = currentHoles.map(
|
|
112
|
+
(_, metadataIndex) => node?.holeMetadata?.[metadataIndex] ?? { source: 'manual' as const },
|
|
113
|
+
)
|
|
114
|
+
const newMetadata = currentMetadata.filter((_, i) => i !== index)
|
|
115
|
+
handleUpdate({ holes: newHoles, holeMetadata: newMetadata })
|
|
96
116
|
if (editingHole?.nodeId === selectedId && editingHole?.holeIndex === index) {
|
|
97
117
|
setEditingHole(null)
|
|
98
118
|
}
|
|
99
119
|
},
|
|
100
|
-
[selectedId, node?.holes, handleUpdate, editingHole, setEditingHole],
|
|
120
|
+
[selectedId, node?.holes, node?.holeMetadata, handleUpdate, editingHole, setEditingHole],
|
|
101
121
|
)
|
|
102
122
|
|
|
103
|
-
|
|
123
|
+
const handleMove = useCallback(() => {
|
|
124
|
+
if (!node) return
|
|
125
|
+
sfxEmitter.emit('sfx:item-pick')
|
|
126
|
+
setMovingNode(node)
|
|
127
|
+
setSelection({ selectedIds: [] })
|
|
128
|
+
}, [node, setMovingNode, setSelection])
|
|
129
|
+
|
|
130
|
+
if (!(node && node.type === 'slab' && selectedId)) return null
|
|
104
131
|
|
|
105
132
|
const calculateArea = (polygon: Array<[number, number]>): number => {
|
|
106
133
|
if (polygon.length < 3) return 0
|
|
@@ -108,8 +135,11 @@ export function SlabPanel() {
|
|
|
108
135
|
const n = polygon.length
|
|
109
136
|
for (let i = 0; i < n; i++) {
|
|
110
137
|
const j = (i + 1) % n
|
|
111
|
-
|
|
112
|
-
|
|
138
|
+
const current = polygon[i]
|
|
139
|
+
const next = polygon[j]
|
|
140
|
+
if (!(current && next)) continue
|
|
141
|
+
area += current[0] * next[1]
|
|
142
|
+
area -= next[0] * current[1]
|
|
113
143
|
}
|
|
114
144
|
return Math.abs(area) / 2
|
|
115
145
|
}
|
|
@@ -157,6 +187,8 @@ export function SlabPanel() {
|
|
|
157
187
|
const holeArea = calculateArea(hole)
|
|
158
188
|
const isEditing =
|
|
159
189
|
editingHole?.nodeId === selectedId && editingHole?.holeIndex === index
|
|
190
|
+
const source = node.holeMetadata?.[index]?.source ?? 'manual'
|
|
191
|
+
const isAutoHole = source === 'stair'
|
|
160
192
|
return (
|
|
161
193
|
<div
|
|
162
194
|
className={`flex items-center justify-between rounded-lg border p-2 transition-colors ${
|
|
@@ -173,7 +205,8 @@ export function SlabPanel() {
|
|
|
173
205
|
Hole {index + 1} {isEditing && '(Editing)'}
|
|
174
206
|
</p>
|
|
175
207
|
<p className="text-[10px] text-muted-foreground">
|
|
176
|
-
{holeArea.toFixed(2)} m² · {hole.length} pts
|
|
208
|
+
{holeArea.toFixed(2)} m² · {hole.length} pts ·{' '}
|
|
209
|
+
{isAutoHole ? 'Auto stair cutout' : 'Manual'}
|
|
177
210
|
</p>
|
|
178
211
|
</div>
|
|
179
212
|
<div className="flex items-center gap-1">
|
|
@@ -183,6 +216,10 @@ export function SlabPanel() {
|
|
|
183
216
|
label="Done"
|
|
184
217
|
onClick={() => setEditingHole(null)}
|
|
185
218
|
/>
|
|
219
|
+
) : isAutoHole ? (
|
|
220
|
+
<div className="rounded-md bg-[#2C2C2E] px-2 py-1 text-[10px] text-muted-foreground">
|
|
221
|
+
Auto
|
|
222
|
+
</div>
|
|
186
223
|
) : (
|
|
187
224
|
<>
|
|
188
225
|
<button
|
|
@@ -221,7 +258,18 @@ export function SlabPanel() {
|
|
|
221
258
|
</div>
|
|
222
259
|
</PanelSection>
|
|
223
260
|
<PanelSection title="Material">
|
|
224
|
-
<MaterialPicker
|
|
261
|
+
<MaterialPicker
|
|
262
|
+
nodeType="slab"
|
|
263
|
+
onChange={handleCustomMaterialChange}
|
|
264
|
+
onSelectMaterialPreset={handleMaterialPresetChange}
|
|
265
|
+
selectedMaterialPreset={node.materialPreset}
|
|
266
|
+
value={node.material}
|
|
267
|
+
/>
|
|
268
|
+
</PanelSection>
|
|
269
|
+
<PanelSection title="Actions">
|
|
270
|
+
<ActionGroup>
|
|
271
|
+
<ActionButton icon={<Move className="h-3.5 w-3.5" />} label="Move" onClick={handleMove} />
|
|
272
|
+
</ActionGroup>
|
|
225
273
|
</PanelSection>
|
|
226
274
|
</PanelWrapper>
|
|
227
275
|
)
|
|
@@ -3,9 +3,13 @@
|
|
|
3
3
|
import {
|
|
4
4
|
type AnyNode,
|
|
5
5
|
type AnyNodeId,
|
|
6
|
+
getEffectiveStairSurfaceMaterial,
|
|
7
|
+
type LevelNode,
|
|
6
8
|
type MaterialSchema,
|
|
7
9
|
type StairNode,
|
|
8
10
|
type StairRailingMode,
|
|
11
|
+
type StairSurfaceMaterialRole,
|
|
12
|
+
type StairSlabOpeningMode,
|
|
9
13
|
type StairTopLandingMode,
|
|
10
14
|
type StairType,
|
|
11
15
|
StairNode as StairNodeSchema,
|
|
@@ -16,6 +20,7 @@ import {
|
|
|
16
20
|
import { useViewer } from '@pascal-app/viewer'
|
|
17
21
|
import { Copy, Move, Plus, Trash2 } from 'lucide-react'
|
|
18
22
|
import { useCallback } from 'react'
|
|
23
|
+
import { useShallow } from 'zustand/react/shallow'
|
|
19
24
|
import { sfxEmitter } from '../../../lib/sfx-bus'
|
|
20
25
|
import useEditor from '../../../store/use-editor'
|
|
21
26
|
import { DEFAULT_SPIRAL_STAIR_SWEEP_ANGLE } from '../../tools/stair/stair-defaults'
|
|
@@ -28,6 +33,32 @@ import { SliderControl } from '../controls/slider-control'
|
|
|
28
33
|
import { ToggleControl } from '../controls/toggle-control'
|
|
29
34
|
import { PanelWrapper } from './panel-wrapper'
|
|
30
35
|
|
|
36
|
+
function buildStairSurfaceMaterialPatch(
|
|
37
|
+
node: StairNode,
|
|
38
|
+
targetRole: StairSurfaceMaterialRole,
|
|
39
|
+
material: MaterialSchema | undefined,
|
|
40
|
+
materialPreset: string | undefined,
|
|
41
|
+
): Partial<StairNode> {
|
|
42
|
+
const nextSurfaceMaterial = { material, materialPreset }
|
|
43
|
+
const nextRailing =
|
|
44
|
+
targetRole === 'railing' ? nextSurfaceMaterial : getEffectiveStairSurfaceMaterial(node, 'railing')
|
|
45
|
+
const nextTread =
|
|
46
|
+
targetRole === 'tread' ? nextSurfaceMaterial : getEffectiveStairSurfaceMaterial(node, 'tread')
|
|
47
|
+
const nextSide =
|
|
48
|
+
targetRole === 'side' ? nextSurfaceMaterial : getEffectiveStairSurfaceMaterial(node, 'side')
|
|
49
|
+
|
|
50
|
+
return {
|
|
51
|
+
railingMaterial: nextRailing.material,
|
|
52
|
+
railingMaterialPreset: nextRailing.materialPreset,
|
|
53
|
+
treadMaterial: nextTread.material,
|
|
54
|
+
treadMaterialPreset: nextTread.materialPreset,
|
|
55
|
+
sideMaterial: nextSide.material,
|
|
56
|
+
sideMaterialPreset: nextSide.materialPreset,
|
|
57
|
+
material: undefined,
|
|
58
|
+
materialPreset: undefined,
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
31
62
|
const RAILING_MODE_OPTIONS: { label: string; value: StairRailingMode }[] = [
|
|
32
63
|
{ label: 'None', value: 'none' },
|
|
33
64
|
{ label: 'Left', value: 'left' },
|
|
@@ -46,19 +77,41 @@ const TOP_LANDING_MODE_OPTIONS: { label: string; value: StairTopLandingMode }[]
|
|
|
46
77
|
{ label: 'Integrated', value: 'integrated' },
|
|
47
78
|
]
|
|
48
79
|
|
|
80
|
+
const STAIR_SLAB_OPENING_OPTIONS: { label: string; value: StairSlabOpeningMode }[] = [
|
|
81
|
+
{ label: 'None', value: 'none' },
|
|
82
|
+
{ label: 'Destination', value: 'destination' },
|
|
83
|
+
]
|
|
84
|
+
|
|
49
85
|
export function StairPanel() {
|
|
50
|
-
const
|
|
86
|
+
const selectedId = useViewer((s) => s.selection.selectedIds[0])
|
|
87
|
+
const selectedCount = useViewer((s) => s.selection.selectedIds.length)
|
|
51
88
|
const setSelection = useViewer((s) => s.setSelection)
|
|
52
|
-
const nodes = useScene((s) => s.nodes)
|
|
53
89
|
const updateNode = useScene((s) => s.updateNode)
|
|
54
90
|
const createNode = useScene((s) => s.createNode)
|
|
55
91
|
const createNodes = useScene((s) => s.createNodes)
|
|
56
92
|
const setMovingNode = useEditor((s) => s.setMovingNode)
|
|
93
|
+
const selectedMaterialTarget = useEditor((s) => s.selectedMaterialTarget)
|
|
57
94
|
|
|
58
|
-
const
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
95
|
+
const node = useScene((s) =>
|
|
96
|
+
selectedId ? (s.nodes[selectedId as AnyNode['id']] as StairNode | undefined) : undefined,
|
|
97
|
+
)
|
|
98
|
+
const levels = useScene(
|
|
99
|
+
useShallow((s) =>
|
|
100
|
+
Object.values(s.nodes)
|
|
101
|
+
.filter((entry): entry is LevelNode => entry.type === 'level')
|
|
102
|
+
.sort((left, right) => left.level - right.level),
|
|
103
|
+
),
|
|
104
|
+
)
|
|
105
|
+
const segments = useScene(
|
|
106
|
+
useShallow((s) => {
|
|
107
|
+
if (!selectedId) return []
|
|
108
|
+
const stairNode = s.nodes[selectedId as AnyNode['id']] as StairNode | undefined
|
|
109
|
+
if (stairNode?.type !== 'stair') return []
|
|
110
|
+
return (stairNode.children ?? [])
|
|
111
|
+
.map((childId) => s.nodes[childId as AnyNodeId] as StairSegmentNode | undefined)
|
|
112
|
+
.filter((entry): entry is StairSegmentNode => entry?.type === 'stair-segment')
|
|
113
|
+
}),
|
|
114
|
+
)
|
|
62
115
|
|
|
63
116
|
const handleUpdate = useCallback(
|
|
64
117
|
(updates: Partial<StairNode>) => {
|
|
@@ -68,11 +121,31 @@ export function StairPanel() {
|
|
|
68
121
|
[selectedId, updateNode],
|
|
69
122
|
)
|
|
70
123
|
|
|
71
|
-
const
|
|
124
|
+
const materialTargetRole =
|
|
125
|
+
selectedMaterialTarget &&
|
|
126
|
+
selectedMaterialTarget.nodeId === node?.id &&
|
|
127
|
+
(selectedMaterialTarget.role === 'railing' ||
|
|
128
|
+
selectedMaterialTarget.role === 'tread' ||
|
|
129
|
+
selectedMaterialTarget.role === 'side')
|
|
130
|
+
? selectedMaterialTarget.role
|
|
131
|
+
: null
|
|
132
|
+
const materialPickerValue =
|
|
133
|
+
node && materialTargetRole ? getEffectiveStairSurfaceMaterial(node, materialTargetRole) : {}
|
|
134
|
+
|
|
135
|
+
const handleTargetedMaterialChange = useCallback(
|
|
72
136
|
(material: MaterialSchema) => {
|
|
73
|
-
|
|
137
|
+
if (!node || !materialTargetRole) return
|
|
138
|
+
handleUpdate(buildStairSurfaceMaterialPatch(node, materialTargetRole, material, undefined))
|
|
139
|
+
},
|
|
140
|
+
[handleUpdate, materialTargetRole, node],
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
const handleTargetedMaterialPresetChange = useCallback(
|
|
144
|
+
(materialPreset: string) => {
|
|
145
|
+
if (!node || !materialTargetRole) return
|
|
146
|
+
handleUpdate(buildStairSurfaceMaterialPatch(node, materialTargetRole, undefined, materialPreset))
|
|
74
147
|
},
|
|
75
|
-
[handleUpdate],
|
|
148
|
+
[handleUpdate, materialTargetRole, node],
|
|
76
149
|
)
|
|
77
150
|
|
|
78
151
|
const handleClose = useCallback(() => {
|
|
@@ -84,13 +157,15 @@ export function StairPanel() {
|
|
|
84
157
|
const children = node.children ?? []
|
|
85
158
|
const lastChildId = children[children.length - 1]
|
|
86
159
|
if (lastChildId) {
|
|
87
|
-
const lastChild = nodes[lastChildId as AnyNodeId] as
|
|
160
|
+
const lastChild = useScene.getState().nodes[lastChildId as AnyNodeId] as
|
|
161
|
+
| StairSegmentNode
|
|
162
|
+
| undefined
|
|
88
163
|
if (lastChild?.type === 'stair-segment') {
|
|
89
164
|
return { fillToFloor: lastChild.fillToFloor }
|
|
90
165
|
}
|
|
91
166
|
}
|
|
92
167
|
return { fillToFloor: true }
|
|
93
|
-
}, [node
|
|
168
|
+
}, [node])
|
|
94
169
|
|
|
95
170
|
const handleAddFlight = useCallback(() => {
|
|
96
171
|
if (!node) return
|
|
@@ -194,11 +269,10 @@ export function StairPanel() {
|
|
|
194
269
|
setSelection({ selectedIds: [] })
|
|
195
270
|
}, [selectedId, node, setSelection])
|
|
196
271
|
|
|
197
|
-
if (!node
|
|
272
|
+
if (!(node && node.type === 'stair' && selectedId && selectedCount === 1)) return null
|
|
198
273
|
|
|
199
|
-
const
|
|
200
|
-
|
|
201
|
-
.filter((n): n is StairSegmentNode => n?.type === 'stair-segment')
|
|
274
|
+
const resolvedFromLevelId = node.fromLevelId ?? node.parentId ?? levels[0]?.id ?? null
|
|
275
|
+
const resolvedToLevelId = node.toLevelId ?? resolvedFromLevelId
|
|
202
276
|
|
|
203
277
|
return (
|
|
204
278
|
<PanelWrapper
|
|
@@ -225,6 +299,73 @@ export function StairPanel() {
|
|
|
225
299
|
/>
|
|
226
300
|
</PanelSection>
|
|
227
301
|
|
|
302
|
+
<PanelSection title="Opening">
|
|
303
|
+
<div className="space-y-3">
|
|
304
|
+
<ToggleControl
|
|
305
|
+
checked={(node.slabOpeningMode ?? 'none') === 'destination'}
|
|
306
|
+
label="Auto Cutout"
|
|
307
|
+
onChange={(checked) =>
|
|
308
|
+
handleUpdate({
|
|
309
|
+
slabOpeningMode: checked ? 'destination' : 'none',
|
|
310
|
+
})
|
|
311
|
+
}
|
|
312
|
+
/>
|
|
313
|
+
|
|
314
|
+
<div className="space-y-1.5">
|
|
315
|
+
<div className="px-1 text-[11px] uppercase tracking-[0.14em] text-muted-foreground">
|
|
316
|
+
From Level
|
|
317
|
+
</div>
|
|
318
|
+
<select
|
|
319
|
+
className="h-9 w-full rounded-lg border border-border/50 bg-[#2C2C2E] px-3 text-sm text-foreground"
|
|
320
|
+
onChange={(event) => handleUpdate({ fromLevelId: event.target.value })}
|
|
321
|
+
value={resolvedFromLevelId ?? ''}
|
|
322
|
+
>
|
|
323
|
+
{levels.map((level) => (
|
|
324
|
+
<option key={level.id} value={level.id}>
|
|
325
|
+
{level.name || `Level ${level.level + 1}`}
|
|
326
|
+
</option>
|
|
327
|
+
))}
|
|
328
|
+
</select>
|
|
329
|
+
</div>
|
|
330
|
+
|
|
331
|
+
<div className="space-y-1.5">
|
|
332
|
+
<div className="px-1 text-[11px] uppercase tracking-[0.14em] text-muted-foreground">
|
|
333
|
+
To Level
|
|
334
|
+
</div>
|
|
335
|
+
<select
|
|
336
|
+
className="h-9 w-full rounded-lg border border-border/50 bg-[#2C2C2E] px-3 text-sm text-foreground"
|
|
337
|
+
onChange={(event) => handleUpdate({ toLevelId: event.target.value })}
|
|
338
|
+
value={resolvedToLevelId ?? ''}
|
|
339
|
+
>
|
|
340
|
+
{levels.map((level) => (
|
|
341
|
+
<option key={level.id} value={level.id}>
|
|
342
|
+
{level.name || `Level ${level.level + 1}`}
|
|
343
|
+
</option>
|
|
344
|
+
))}
|
|
345
|
+
</select>
|
|
346
|
+
</div>
|
|
347
|
+
|
|
348
|
+
<SegmentedControl
|
|
349
|
+
onChange={(value) => handleUpdate({ slabOpeningMode: value as StairSlabOpeningMode })}
|
|
350
|
+
options={STAIR_SLAB_OPENING_OPTIONS}
|
|
351
|
+
value={node.slabOpeningMode ?? 'none'}
|
|
352
|
+
/>
|
|
353
|
+
|
|
354
|
+
{(node.slabOpeningMode ?? 'none') === 'destination' ? (
|
|
355
|
+
<MetricControl
|
|
356
|
+
label="Opening Offset"
|
|
357
|
+
max={0.5}
|
|
358
|
+
min={0}
|
|
359
|
+
onChange={(value) => handleUpdate({ openingOffset: value })}
|
|
360
|
+
precision={2}
|
|
361
|
+
step={0.01}
|
|
362
|
+
unit="m"
|
|
363
|
+
value={Math.round((node.openingOffset ?? 0) * 100) / 100}
|
|
364
|
+
/>
|
|
365
|
+
) : null}
|
|
366
|
+
</div>
|
|
367
|
+
</PanelSection>
|
|
368
|
+
|
|
228
369
|
{node.stairType === 'straight' && (
|
|
229
370
|
<PanelSection title="Segments">
|
|
230
371
|
<div className="flex flex-col gap-1">
|
|
@@ -361,7 +502,7 @@ export function StairPanel() {
|
|
|
361
502
|
)}
|
|
362
503
|
|
|
363
504
|
<PanelSection title="Position">
|
|
364
|
-
<
|
|
505
|
+
<SliderControl
|
|
365
506
|
label="X"
|
|
366
507
|
max={50}
|
|
367
508
|
min={-50}
|
|
@@ -375,7 +516,7 @@ export function StairPanel() {
|
|
|
375
516
|
unit="m"
|
|
376
517
|
value={Math.round(node.position[0] * 100) / 100}
|
|
377
518
|
/>
|
|
378
|
-
<
|
|
519
|
+
<SliderControl
|
|
379
520
|
label="Y"
|
|
380
521
|
max={50}
|
|
381
522
|
min={-50}
|
|
@@ -389,7 +530,7 @@ export function StairPanel() {
|
|
|
389
530
|
unit="m"
|
|
390
531
|
value={Math.round(node.position[1] * 100) / 100}
|
|
391
532
|
/>
|
|
392
|
-
<
|
|
533
|
+
<SliderControl
|
|
393
534
|
label="Z"
|
|
394
535
|
max={50}
|
|
395
536
|
min={-50}
|
|
@@ -470,7 +611,20 @@ export function StairPanel() {
|
|
|
470
611
|
</ActionGroup>
|
|
471
612
|
</PanelSection>
|
|
472
613
|
<PanelSection title="Material">
|
|
473
|
-
|
|
614
|
+
{!materialTargetRole ? (
|
|
615
|
+
<div className="mb-3 rounded-lg border border-border/50 bg-[#2C2C2E] px-3 py-2 text-[11px] text-muted-foreground">
|
|
616
|
+
Click the stair surface you want to edit. Materials apply to one target at a time.
|
|
617
|
+
</div>
|
|
618
|
+
) : null}
|
|
619
|
+
<MaterialPicker
|
|
620
|
+
disabled={!materialTargetRole}
|
|
621
|
+
hideSideControl
|
|
622
|
+
nodeType="stair"
|
|
623
|
+
onChange={handleTargetedMaterialChange}
|
|
624
|
+
onSelectMaterialPreset={handleTargetedMaterialPresetChange}
|
|
625
|
+
selectedMaterialPreset={materialPickerValue.materialPreset}
|
|
626
|
+
value={materialPickerValue.material}
|
|
627
|
+
/>
|
|
474
628
|
</PanelSection>
|
|
475
629
|
</PanelWrapper>
|
|
476
630
|
)
|
|
@@ -17,7 +17,6 @@ import { sfxEmitter } from '../../../lib/sfx-bus'
|
|
|
17
17
|
import useEditor from '../../../store/use-editor'
|
|
18
18
|
import { ActionButton, ActionGroup } from '../controls/action-button'
|
|
19
19
|
import { MaterialPicker } from '../controls/material-picker'
|
|
20
|
-
import { MetricControl } from '../controls/metric-control'
|
|
21
20
|
import { PanelSection } from '../controls/panel-section'
|
|
22
21
|
import { SegmentedControl } from '../controls/segmented-control'
|
|
23
22
|
import { SliderControl } from '../controls/slider-control'
|
|
@@ -35,25 +34,24 @@ const ATTACHMENT_SIDE_OPTIONS: { label: string; value: AttachmentSide }[] = [
|
|
|
35
34
|
]
|
|
36
35
|
|
|
37
36
|
export function StairSegmentPanel() {
|
|
38
|
-
const
|
|
37
|
+
const selectedId = useViewer((s) => s.selection.selectedIds[0])
|
|
39
38
|
const setSelection = useViewer((s) => s.setSelection)
|
|
40
|
-
const nodes = useScene((s) => s.nodes)
|
|
41
39
|
const updateNode = useScene((s) => s.updateNode)
|
|
42
40
|
const setMovingNode = useEditor((s) => s.setMovingNode)
|
|
43
41
|
|
|
44
|
-
const
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
: undefined
|
|
42
|
+
const node = useScene((s) =>
|
|
43
|
+
selectedId ? (s.nodes[selectedId as AnyNode['id']] as StairSegmentNode | undefined) : undefined,
|
|
44
|
+
)
|
|
48
45
|
|
|
49
|
-
//
|
|
50
|
-
|
|
46
|
+
// Boolean selector — re-renders only when this segment's position among the
|
|
47
|
+
// parent stair's children flips to/from "first".
|
|
48
|
+
const isFirstSegment = useScene((s) => {
|
|
51
49
|
if (!node?.parentId) return true
|
|
52
|
-
const parent = nodes[node.parentId as AnyNodeId]
|
|
50
|
+
const parent = s.nodes[node.parentId as AnyNodeId]
|
|
53
51
|
if (!parent || parent.type !== 'stair') return true
|
|
54
52
|
const children = (parent as any).children ?? []
|
|
55
53
|
return children[0] === node.id
|
|
56
|
-
})
|
|
54
|
+
})
|
|
57
55
|
|
|
58
56
|
const handleUpdate = useCallback(
|
|
59
57
|
(updates: Partial<StairSegmentNode>) => {
|
|
@@ -65,7 +63,14 @@ export function StairSegmentPanel() {
|
|
|
65
63
|
|
|
66
64
|
const handleMaterialChange = useCallback(
|
|
67
65
|
(material: MaterialSchema) => {
|
|
68
|
-
handleUpdate({ material })
|
|
66
|
+
handleUpdate({ material, materialPreset: undefined })
|
|
67
|
+
},
|
|
68
|
+
[handleUpdate],
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
const handleMaterialPresetChange = useCallback(
|
|
72
|
+
(materialPreset: string) => {
|
|
73
|
+
handleUpdate({ materialPreset, material: undefined })
|
|
69
74
|
},
|
|
70
75
|
[handleUpdate],
|
|
71
76
|
)
|
|
@@ -124,7 +129,7 @@ export function StairSegmentPanel() {
|
|
|
124
129
|
}
|
|
125
130
|
}, [selectedId, node, setSelection])
|
|
126
131
|
|
|
127
|
-
if (!node
|
|
132
|
+
if (!(node && node.type === 'stair-segment' && selectedId)) return null
|
|
128
133
|
|
|
129
134
|
return (
|
|
130
135
|
<PanelWrapper
|
|
@@ -243,7 +248,7 @@ export function StairSegmentPanel() {
|
|
|
243
248
|
</PanelSection>
|
|
244
249
|
|
|
245
250
|
<PanelSection title="Position">
|
|
246
|
-
<
|
|
251
|
+
<SliderControl
|
|
247
252
|
label="X"
|
|
248
253
|
max={50}
|
|
249
254
|
min={-50}
|
|
@@ -257,7 +262,7 @@ export function StairSegmentPanel() {
|
|
|
257
262
|
unit="m"
|
|
258
263
|
value={Math.round(node.position[0] * 100) / 100}
|
|
259
264
|
/>
|
|
260
|
-
<
|
|
265
|
+
<SliderControl
|
|
261
266
|
label="Y"
|
|
262
267
|
max={50}
|
|
263
268
|
min={-50}
|
|
@@ -271,7 +276,7 @@ export function StairSegmentPanel() {
|
|
|
271
276
|
unit="m"
|
|
272
277
|
value={Math.round(node.position[1] * 100) / 100}
|
|
273
278
|
/>
|
|
274
|
-
<
|
|
279
|
+
<SliderControl
|
|
275
280
|
label="Z"
|
|
276
281
|
max={50}
|
|
277
282
|
min={-50}
|
|
@@ -332,7 +337,13 @@ export function StairSegmentPanel() {
|
|
|
332
337
|
</ActionGroup>
|
|
333
338
|
</PanelSection>
|
|
334
339
|
<PanelSection title="Material">
|
|
335
|
-
<MaterialPicker
|
|
340
|
+
<MaterialPicker
|
|
341
|
+
nodeType="stair-segment"
|
|
342
|
+
onChange={handleMaterialChange}
|
|
343
|
+
onSelectMaterialPreset={handleMaterialPresetChange}
|
|
344
|
+
selectedMaterialPreset={node.materialPreset}
|
|
345
|
+
value={node.material}
|
|
346
|
+
/>
|
|
336
347
|
</PanelSection>
|
|
337
348
|
</PanelWrapper>
|
|
338
349
|
)
|