@pascal-app/editor 0.7.0 → 0.8.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 +6 -6
- package/src/components/editor/custom-camera-controls.tsx +2 -1
- package/src/components/editor/editor-layout-v2.tsx +4 -3
- package/src/components/editor/first-person/build-collider-world.ts +5 -7
- package/src/components/editor/first-person/bvh-ecctrl.tsx +119 -54
- package/src/components/editor/first-person-controls.tsx +11 -11
- package/src/components/editor/floating-action-menu.tsx +0 -0
- package/src/components/editor/floorplan-panel.tsx +44 -37
- package/src/components/editor/index.tsx +68 -53
- package/src/components/editor/selection-manager.tsx +2 -2
- package/src/components/editor/snapshot-capture-overlay.tsx +465 -0
- package/src/components/editor/thumbnail-generator.tsx +18 -61
- package/src/components/editor/use-floorplan-background-placement.ts +3 -3
- package/src/components/editor/wall-measurement-label.tsx +0 -0
- package/src/components/editor-2d/renderers/floorplan-draft-layer.tsx +6 -1
- package/src/components/editor-2d/renderers/floorplan-measurements-layer.tsx +6 -1
- package/src/components/editor-2d/renderers/floorplan-stair-layer.tsx +5 -5
- package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +10 -12
- package/src/components/systems/roof/roof-edit-system.tsx +1 -1
- package/src/components/systems/stair/stair-edit-system.tsx +1 -1
- package/src/components/systems/zone/zone-label-editor-system.tsx +0 -0
- package/src/components/systems/zone/zone-system.tsx +0 -0
- package/src/components/tools/ceiling/move-ceiling-tool.tsx +9 -2
- package/src/components/tools/fence/curve-fence-tool.tsx +4 -5
- package/src/components/tools/fence/fence-tool.tsx +2 -2
- package/src/components/tools/fence/move-fence-endpoint-tool.tsx +11 -8
- package/src/components/tools/fence/move-fence-tool.tsx +13 -9
- package/src/components/tools/item/move-tool.tsx +3 -6
- package/src/components/tools/item/placement-math.ts +2 -4
- package/src/components/tools/item/placement-strategies.ts +11 -10
- package/src/components/tools/item/use-draft-node.ts +0 -1
- package/src/components/tools/item/use-placement-coordinator.tsx +9 -111
- package/src/components/tools/roof/move-roof-tool.tsx +7 -2
- package/src/components/tools/select/box-select-tool.tsx +12 -17
- package/src/components/tools/shared/segment-angle.ts +1 -1
- package/src/components/tools/tool-manager.tsx +12 -12
- package/src/components/tools/wall/curve-wall-tool.tsx +8 -6
- package/src/components/tools/wall/move-wall-endpoint-tool.tsx +11 -8
- package/src/components/tools/wall/move-wall-tool.tsx +6 -4
- package/src/components/tools/wall/wall-drafting.ts +0 -0
- package/src/components/tools/wall/wall-tool.tsx +3 -3
- package/src/components/tools/zone/zone-tool.tsx +20 -5
- package/src/components/ui/action-menu/camera-actions.tsx +0 -0
- package/src/components/ui/action-menu/control-modes.tsx +7 -1
- package/src/components/ui/action-menu/furnish-tools.tsx +6 -92
- package/src/components/ui/action-menu/index.tsx +35 -86
- package/src/components/ui/action-menu/view-toggles.tsx +19 -31
- package/src/components/ui/command-palette/editor-commands.tsx +6 -4
- package/src/components/ui/command-palette/index.tsx +4 -255
- package/src/components/ui/controls/material-picker.tsx +8 -5
- package/src/components/ui/floating-level-selector.tsx +1 -1
- package/src/components/ui/helpers/helper-manager.tsx +5 -0
- package/src/components/ui/item-catalog/catalog-items.tsx +1742 -315
- package/src/components/ui/item-catalog/item-catalog.tsx +88 -46
- package/src/components/ui/level-duplicate-dialog.tsx +3 -5
- package/src/components/ui/panels/ceiling-panel.tsx +2 -3
- package/src/components/ui/panels/column-panel.tsx +62 -18
- package/src/components/ui/panels/door-panel.tsx +272 -265
- package/src/components/ui/panels/fence-panel.tsx +0 -5
- package/src/components/ui/panels/paint-panel.tsx +66 -41
- package/src/components/ui/panels/panel-manager.tsx +3 -32
- package/src/components/ui/panels/reference-panel.tsx +28 -13
- package/src/components/ui/panels/roof-panel.tsx +52 -2
- package/src/components/ui/panels/roof-segment-panel.tsx +0 -0
- package/src/components/ui/panels/slab-panel.tsx +0 -0
- package/src/components/ui/panels/spawn-panel.tsx +10 -4
- package/src/components/ui/panels/stair-panel.tsx +66 -14
- package/src/components/ui/panels/wall-panel.tsx +97 -1
- package/src/components/ui/panels/window-panel.tsx +13 -5
- package/src/components/ui/primitives/number-input.tsx +1 -1
- package/src/components/ui/primitives/sidebar.tsx +0 -0
- package/src/components/ui/sidebar/app-sidebar.tsx +0 -0
- package/src/components/ui/sidebar/icon-rail.tsx +0 -0
- package/src/components/ui/sidebar/panels/items-panel/index.tsx +330 -0
- package/src/components/ui/sidebar/panels/settings-panel/index.tsx +0 -0
- package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +0 -0
- package/src/components/ui/sidebar/panels/site-panel/index.tsx +4 -6
- package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +0 -0
- package/src/components/ui/sidebar/panels/site-panel/spawn-tree-node.tsx +1 -7
- package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +3 -1
- package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +3 -1
- package/src/components/ui/sidebar/panels/zone-panel/index.tsx +1 -1
- package/src/components/ui/slider.tsx +1 -1
- package/src/components/viewer-overlay.tsx +0 -0
- package/src/components/viewer-zone-system.tsx +0 -0
- package/src/hooks/use-auto-save.ts +14 -0
- package/src/hooks/use-keyboard.ts +10 -0
- package/src/index.tsx +8 -1
- package/src/lib/level-duplication.test.ts +0 -2
- package/src/lib/level-duplication.ts +1 -1
- package/src/lib/material-paint.ts +1 -1
- package/src/lib/roof-duplication.ts +1 -1
- package/src/lib/scene-bounds.ts +1 -1
- package/src/lib/scene.ts +0 -0
- package/src/lib/sfx-bus.ts +2 -0
- package/src/lib/sfx-player.ts +5 -5
- package/src/lib/stair-duplication.ts +2 -2
- package/src/store/use-editor.tsx +27 -59
- package/tsconfig.json +2 -1
- package/src/components/feedback-dialog.tsx +0 -265
- package/src/components/pascal-radio.tsx +0 -280
- package/src/components/preview-button.tsx +0 -16
- package/src/components/ui/viewer-toolbar.tsx +0 -436
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
3
|
import useEditor from '../../../store/use-editor'
|
|
4
|
-
import { SliderControl } from '../controls/slider-control'
|
|
5
|
-
import { Input } from '../primitives/input'
|
|
6
4
|
import { PanelSection } from '../controls/panel-section'
|
|
5
|
+
import { Input } from '../primitives/input'
|
|
7
6
|
import { PanelWrapper } from './panel-wrapper'
|
|
8
7
|
|
|
9
8
|
function buildDefaultCustomMaterial() {
|
|
@@ -53,11 +52,7 @@ export function PaintPanel() {
|
|
|
53
52
|
}
|
|
54
53
|
|
|
55
54
|
return (
|
|
56
|
-
<PanelWrapper
|
|
57
|
-
onClose={() => setPaintPanelOpen(false)}
|
|
58
|
-
title="Material"
|
|
59
|
-
width={320}
|
|
60
|
-
>
|
|
55
|
+
<PanelWrapper onClose={() => setPaintPanelOpen(false)} title="Material" width={320}>
|
|
61
56
|
<PanelSection title="Custom Material">
|
|
62
57
|
<div className="space-y-3">
|
|
63
58
|
<div className="space-y-2">
|
|
@@ -78,41 +73,71 @@ export function PaintPanel() {
|
|
|
78
73
|
</div>
|
|
79
74
|
</div>
|
|
80
75
|
|
|
81
|
-
<div className="space-y-
|
|
82
|
-
<
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
<
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
76
|
+
<div className="space-y-2">
|
|
77
|
+
<div className="flex items-center justify-between">
|
|
78
|
+
<label className="font-medium text-muted-foreground text-xs uppercase tracking-[0.12em]">
|
|
79
|
+
Roughness
|
|
80
|
+
</label>
|
|
81
|
+
<span className="font-mono text-muted-foreground text-xs">
|
|
82
|
+
{currentProps.roughness.toFixed(2)}
|
|
83
|
+
</span>
|
|
84
|
+
</div>
|
|
85
|
+
<input
|
|
86
|
+
className="h-2 w-full cursor-pointer appearance-none rounded-full bg-accent"
|
|
87
|
+
max={1}
|
|
88
|
+
min={0}
|
|
89
|
+
onChange={(e) =>
|
|
90
|
+
updateCustomMaterial({ roughness: Number.parseFloat(e.target.value) })
|
|
91
|
+
}
|
|
92
|
+
step={0.01}
|
|
93
|
+
type="range"
|
|
94
|
+
value={currentProps.roughness}
|
|
95
|
+
/>
|
|
96
|
+
</div>
|
|
97
|
+
|
|
98
|
+
<div className="space-y-2">
|
|
99
|
+
<div className="flex items-center justify-between">
|
|
100
|
+
<label className="font-medium text-muted-foreground text-xs uppercase tracking-[0.12em]">
|
|
101
|
+
Metalness
|
|
102
|
+
</label>
|
|
103
|
+
<span className="font-mono text-muted-foreground text-xs">
|
|
104
|
+
{currentProps.metalness.toFixed(2)}
|
|
105
|
+
</span>
|
|
106
|
+
</div>
|
|
107
|
+
<input
|
|
108
|
+
className="h-2 w-full cursor-pointer appearance-none rounded-full bg-accent"
|
|
109
|
+
max={1}
|
|
110
|
+
min={0}
|
|
111
|
+
onChange={(e) =>
|
|
112
|
+
updateCustomMaterial({ metalness: Number.parseFloat(e.target.value) })
|
|
113
|
+
}
|
|
114
|
+
step={0.01}
|
|
115
|
+
type="range"
|
|
116
|
+
value={currentProps.metalness}
|
|
117
|
+
/>
|
|
118
|
+
</div>
|
|
119
|
+
|
|
120
|
+
<div className="space-y-2">
|
|
121
|
+
<div className="flex items-center justify-between">
|
|
122
|
+
<label className="font-medium text-muted-foreground text-xs uppercase tracking-[0.12em]">
|
|
123
|
+
Opacity
|
|
124
|
+
</label>
|
|
125
|
+
<span className="font-mono text-muted-foreground text-xs">
|
|
126
|
+
{currentProps.opacity.toFixed(2)}
|
|
127
|
+
</span>
|
|
115
128
|
</div>
|
|
129
|
+
<input
|
|
130
|
+
className="h-2 w-full cursor-pointer appearance-none rounded-full bg-accent"
|
|
131
|
+
max={1}
|
|
132
|
+
min={0}
|
|
133
|
+
onChange={(e) => {
|
|
134
|
+
const opacity = Number.parseFloat(e.target.value)
|
|
135
|
+
updateCustomMaterial({ opacity }, opacity < 1 || currentProps.transparent)
|
|
136
|
+
}}
|
|
137
|
+
step={0.01}
|
|
138
|
+
type="range"
|
|
139
|
+
value={currentProps.opacity}
|
|
140
|
+
/>
|
|
116
141
|
</div>
|
|
117
142
|
|
|
118
143
|
<div className="space-y-2">
|
|
@@ -92,6 +92,8 @@ function panelForType(type: string | null) {
|
|
|
92
92
|
return <StairSegmentPanel />
|
|
93
93
|
case 'slab':
|
|
94
94
|
return <SlabPanel />
|
|
95
|
+
case 'spawn':
|
|
96
|
+
return <SpawnPanel />
|
|
95
97
|
case 'ceiling':
|
|
96
98
|
return <CeilingPanel />
|
|
97
99
|
case 'column':
|
|
@@ -238,36 +240,5 @@ export function PanelManager() {
|
|
|
238
240
|
}
|
|
239
241
|
|
|
240
242
|
// Show appropriate panel based on selected node type
|
|
241
|
-
|
|
242
|
-
switch (selectedNodeType) {
|
|
243
|
-
case 'item':
|
|
244
|
-
return <ItemPanel />
|
|
245
|
-
case 'roof':
|
|
246
|
-
return <RoofPanel />
|
|
247
|
-
case 'roof-segment':
|
|
248
|
-
return <RoofSegmentPanel />
|
|
249
|
-
case 'stair':
|
|
250
|
-
return <StairPanel />
|
|
251
|
-
case 'stair-segment':
|
|
252
|
-
return <StairSegmentPanel />
|
|
253
|
-
case 'slab':
|
|
254
|
-
return <SlabPanel />
|
|
255
|
-
case 'spawn':
|
|
256
|
-
return <SpawnPanel />
|
|
257
|
-
case 'ceiling':
|
|
258
|
-
return <CeilingPanel />
|
|
259
|
-
case 'column':
|
|
260
|
-
return <ColumnPanel />
|
|
261
|
-
case 'wall':
|
|
262
|
-
return <WallPanel />
|
|
263
|
-
case 'fence':
|
|
264
|
-
return <FencePanel />
|
|
265
|
-
case 'door':
|
|
266
|
-
return <DoorPanel />
|
|
267
|
-
case 'window':
|
|
268
|
-
return <WindowPanel />
|
|
269
|
-
}
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
return null
|
|
243
|
+
return panelForType(selectedNodeType)
|
|
273
244
|
}
|
|
@@ -4,11 +4,21 @@ import {
|
|
|
4
4
|
type AnyNode,
|
|
5
5
|
type GuideNode,
|
|
6
6
|
loadAssetUrl,
|
|
7
|
-
saveAsset,
|
|
8
7
|
type ScanNode,
|
|
8
|
+
saveAsset,
|
|
9
9
|
useScene,
|
|
10
10
|
} from '@pascal-app/core'
|
|
11
|
-
import {
|
|
11
|
+
import {
|
|
12
|
+
Eye,
|
|
13
|
+
EyeOff,
|
|
14
|
+
LocateFixed,
|
|
15
|
+
Lock,
|
|
16
|
+
RotateCcw,
|
|
17
|
+
Ruler,
|
|
18
|
+
Trash2,
|
|
19
|
+
Unlock,
|
|
20
|
+
Upload,
|
|
21
|
+
} from 'lucide-react'
|
|
12
22
|
import { useCallback, useEffect, useRef, useState } from 'react'
|
|
13
23
|
import { guideEmitter } from '../../../lib/guide-events'
|
|
14
24
|
import { getGuideImageName } from '../../../lib/local-guide-image'
|
|
@@ -32,7 +42,9 @@ function getScaleStatus(guide: GuideNode, scaleReferenceVisible: boolean) {
|
|
|
32
42
|
export function ReferencePanel() {
|
|
33
43
|
const selectedReferenceId = useEditor((s) => s.selectedReferenceId)
|
|
34
44
|
const setSelectedReferenceId = useEditor((s) => s.setSelectedReferenceId)
|
|
35
|
-
const guideUi = useEditor((s) =>
|
|
45
|
+
const guideUi = useEditor((s) =>
|
|
46
|
+
selectedReferenceId ? s.guideUi[selectedReferenceId] : undefined,
|
|
47
|
+
)
|
|
36
48
|
const setGuideLocked = useEditor((s) => s.setGuideLocked)
|
|
37
49
|
const setGuideScaleReferenceVisible = useEditor((s) => s.setGuideScaleReferenceVisible)
|
|
38
50
|
const clearGuideUi = useEditor((s) => s.clearGuideUi)
|
|
@@ -77,11 +89,14 @@ export function ReferencePanel() {
|
|
|
77
89
|
|
|
78
90
|
try {
|
|
79
91
|
const assetUrl = await saveAsset(file)
|
|
80
|
-
updateNode(
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
92
|
+
updateNode(
|
|
93
|
+
selectedReferenceId as AnyNode['id'],
|
|
94
|
+
{
|
|
95
|
+
name: getGuideImageName(file.name),
|
|
96
|
+
url: assetUrl,
|
|
97
|
+
scaleReference: null,
|
|
98
|
+
} as Partial<GuideNode>,
|
|
99
|
+
)
|
|
85
100
|
setGuideScaleReferenceVisible(selectedReferenceId, true)
|
|
86
101
|
} catch {
|
|
87
102
|
setReplaceError('Could not replace that image.')
|
|
@@ -138,7 +153,7 @@ export function ReferencePanel() {
|
|
|
138
153
|
const isScan = node.type === 'scan'
|
|
139
154
|
const guideLocked = !isScan && guideUi?.locked === true
|
|
140
155
|
const scaleReferenceVisible = !isScan && guideUi?.scaleReferenceVisible !== false
|
|
141
|
-
const scaleStatus =
|
|
156
|
+
const scaleStatus = isScan ? null : getScaleStatus(node, scaleReferenceVisible)
|
|
142
157
|
|
|
143
158
|
return (
|
|
144
159
|
<PanelWrapper
|
|
@@ -165,16 +180,16 @@ export function ReferencePanel() {
|
|
|
165
180
|
|
|
166
181
|
<ActionGroup>
|
|
167
182
|
<ActionButton
|
|
183
|
+
disabled={isReplacing}
|
|
168
184
|
icon={<Upload className="h-3.5 w-3.5" />}
|
|
169
185
|
label={isReplacing ? 'Replacing...' : 'Replace'}
|
|
170
186
|
onClick={() => replaceInputRef.current?.click()}
|
|
171
|
-
disabled={isReplacing}
|
|
172
187
|
/>
|
|
173
188
|
<ActionButton
|
|
189
|
+
className="text-destructive hover:bg-destructive/10"
|
|
174
190
|
icon={<Trash2 className="h-3.5 w-3.5" />}
|
|
175
191
|
label="Delete"
|
|
176
192
|
onClick={handleDeleteGuide}
|
|
177
|
-
className="text-destructive hover:bg-destructive/10"
|
|
178
193
|
/>
|
|
179
194
|
</ActionGroup>
|
|
180
195
|
|
|
@@ -232,16 +247,16 @@ export function ReferencePanel() {
|
|
|
232
247
|
|
|
233
248
|
<ActionGroup>
|
|
234
249
|
<ActionButton
|
|
235
|
-
label={scaleReferenceVisible ? 'Hide Scale' : 'Show Scale'}
|
|
236
250
|
disabled={!node.scaleReference}
|
|
251
|
+
label={scaleReferenceVisible ? 'Hide Scale' : 'Show Scale'}
|
|
237
252
|
onClick={() => {
|
|
238
253
|
if (!node.scaleReference) return
|
|
239
254
|
setGuideScaleReferenceVisible(node.id, !scaleReferenceVisible)
|
|
240
255
|
}}
|
|
241
256
|
/>
|
|
242
257
|
<ActionButton
|
|
243
|
-
label="Clear Scale"
|
|
244
258
|
disabled={!node.scaleReference}
|
|
259
|
+
label="Clear Scale"
|
|
245
260
|
onClick={() => handleUpdate({ scaleReference: null } as Partial<GuideNode>)}
|
|
246
261
|
/>
|
|
247
262
|
</ActionGroup>
|
|
@@ -3,21 +3,25 @@
|
|
|
3
3
|
import {
|
|
4
4
|
type AnyNode,
|
|
5
5
|
type AnyNodeId,
|
|
6
|
+
getEffectiveRoofSurfaceMaterial,
|
|
7
|
+
type MaterialSchema,
|
|
6
8
|
type RoofNode,
|
|
7
|
-
type RoofSurfaceMaterialRole,
|
|
8
9
|
RoofNode as RoofNodeSchema,
|
|
9
10
|
type RoofSegmentNode,
|
|
10
11
|
RoofSegmentNode as RoofSegmentNodeSchema,
|
|
12
|
+
type RoofSurfaceMaterialRole,
|
|
11
13
|
useScene,
|
|
12
14
|
} from '@pascal-app/core'
|
|
13
15
|
import { useViewer } from '@pascal-app/viewer'
|
|
14
16
|
import { Copy, Move, Plus, Trash2 } from 'lucide-react'
|
|
15
17
|
import { useCallback } from 'react'
|
|
16
18
|
import { useShallow } from 'zustand/react/shallow'
|
|
17
|
-
import {
|
|
19
|
+
import { buildRoofSurfaceMaterialPatch } from '../../../lib/material-paint'
|
|
18
20
|
import { duplicateRoofSubtree } from '../../../lib/roof-duplication'
|
|
21
|
+
import { sfxEmitter } from '../../../lib/sfx-bus'
|
|
19
22
|
import useEditor from '../../../store/use-editor'
|
|
20
23
|
import { ActionButton, ActionGroup } from '../controls/action-button'
|
|
24
|
+
import { MaterialPicker } from '../controls/material-picker'
|
|
21
25
|
import { PanelSection } from '../controls/panel-section'
|
|
22
26
|
import { SliderControl } from '../controls/slider-control'
|
|
23
27
|
import { PanelWrapper } from './panel-wrapper'
|
|
@@ -28,6 +32,7 @@ export function RoofPanel() {
|
|
|
28
32
|
const updateNode = useScene((s) => s.updateNode)
|
|
29
33
|
const createNode = useScene((s) => s.createNode)
|
|
30
34
|
const setMovingNode = useEditor((s) => s.setMovingNode)
|
|
35
|
+
const selectedMaterialTarget = useEditor((s) => s.selectedMaterialTarget)
|
|
31
36
|
|
|
32
37
|
const node = useScene((s) =>
|
|
33
38
|
selectedId ? (s.nodes[selectedId as AnyNode['id']] as RoofNode | undefined) : undefined,
|
|
@@ -50,6 +55,35 @@ export function RoofPanel() {
|
|
|
50
55
|
[selectedId, updateNode],
|
|
51
56
|
)
|
|
52
57
|
|
|
58
|
+
const materialTargetRole =
|
|
59
|
+
selectedMaterialTarget &&
|
|
60
|
+
selectedMaterialTarget.nodeId === node?.id &&
|
|
61
|
+
(selectedMaterialTarget.role === 'top' ||
|
|
62
|
+
selectedMaterialTarget.role === 'edge' ||
|
|
63
|
+
selectedMaterialTarget.role === 'wall')
|
|
64
|
+
? selectedMaterialTarget.role
|
|
65
|
+
: null
|
|
66
|
+
const materialPickerValue =
|
|
67
|
+
node && materialTargetRole ? getEffectiveRoofSurfaceMaterial(node, materialTargetRole) : {}
|
|
68
|
+
|
|
69
|
+
const handleTargetedMaterialChange = useCallback(
|
|
70
|
+
(material: MaterialSchema) => {
|
|
71
|
+
if (!(node && materialTargetRole)) return
|
|
72
|
+
handleUpdate(buildRoofSurfaceMaterialPatch(node, materialTargetRole, material, undefined))
|
|
73
|
+
},
|
|
74
|
+
[handleUpdate, materialTargetRole, node],
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
const handleTargetedMaterialPresetChange = useCallback(
|
|
78
|
+
(materialPreset: string) => {
|
|
79
|
+
if (!(node && materialTargetRole)) return
|
|
80
|
+
handleUpdate(
|
|
81
|
+
buildRoofSurfaceMaterialPatch(node, materialTargetRole, undefined, materialPreset),
|
|
82
|
+
)
|
|
83
|
+
},
|
|
84
|
+
[handleUpdate, materialTargetRole, node],
|
|
85
|
+
)
|
|
86
|
+
|
|
53
87
|
const handleClose = useCallback(() => {
|
|
54
88
|
setSelection({ selectedIds: [] })
|
|
55
89
|
}, [setSelection])
|
|
@@ -225,6 +259,22 @@ export function RoofPanel() {
|
|
|
225
259
|
/>
|
|
226
260
|
</ActionGroup>
|
|
227
261
|
</PanelSection>
|
|
262
|
+
<PanelSection title="Material">
|
|
263
|
+
{materialTargetRole ? null : (
|
|
264
|
+
<div className="mb-3 rounded-lg border border-border/50 bg-[#2C2C2E] px-3 py-2 text-[11px] text-muted-foreground">
|
|
265
|
+
Click the roof surface you want to edit. Materials apply to one target at a time.
|
|
266
|
+
</div>
|
|
267
|
+
)}
|
|
268
|
+
<MaterialPicker
|
|
269
|
+
disabled={!materialTargetRole}
|
|
270
|
+
hideSideControl
|
|
271
|
+
nodeType="roof"
|
|
272
|
+
onChange={handleTargetedMaterialChange}
|
|
273
|
+
onSelectMaterialPreset={handleTargetedMaterialPresetChange}
|
|
274
|
+
selectedMaterialPreset={materialPickerValue.materialPreset}
|
|
275
|
+
value={materialPickerValue.material}
|
|
276
|
+
/>
|
|
277
|
+
</PanelSection>
|
|
228
278
|
</PanelWrapper>
|
|
229
279
|
)
|
|
230
280
|
}
|
|
File without changes
|
|
File without changes
|
|
@@ -87,7 +87,7 @@ export function SpawnPanel() {
|
|
|
87
87
|
|
|
88
88
|
if (!(node && node.type === 'spawn' && selectedId)) return null
|
|
89
89
|
|
|
90
|
-
const rotationDegrees = Math.round(((
|
|
90
|
+
const rotationDegrees = Math.round(((draftRotation ?? node.rotation) * 180) / Math.PI)
|
|
91
91
|
const storedRotationDegrees = Math.round((node.rotation * 180) / Math.PI)
|
|
92
92
|
|
|
93
93
|
return (
|
|
@@ -97,7 +97,9 @@ export function SpawnPanel() {
|
|
|
97
97
|
label="X"
|
|
98
98
|
max={node.position[0] + 2}
|
|
99
99
|
min={node.position[0] - 2}
|
|
100
|
-
onChange={(value) =>
|
|
100
|
+
onChange={(value) =>
|
|
101
|
+
handleUpdate({ position: [value, node.position[1], node.position[2]] })
|
|
102
|
+
}
|
|
101
103
|
precision={2}
|
|
102
104
|
step={0.01}
|
|
103
105
|
unit="m"
|
|
@@ -107,7 +109,9 @@ export function SpawnPanel() {
|
|
|
107
109
|
label="Y"
|
|
108
110
|
max={node.position[1] + 2}
|
|
109
111
|
min={node.position[1] - 2}
|
|
110
|
-
onChange={(value) =>
|
|
112
|
+
onChange={(value) =>
|
|
113
|
+
handleUpdate({ position: [node.position[0], value, node.position[2]] })
|
|
114
|
+
}
|
|
111
115
|
precision={2}
|
|
112
116
|
step={0.01}
|
|
113
117
|
unit="m"
|
|
@@ -117,7 +121,9 @@ export function SpawnPanel() {
|
|
|
117
121
|
label="Z"
|
|
118
122
|
max={node.position[2] + 2}
|
|
119
123
|
min={node.position[2] - 2}
|
|
120
|
-
onChange={(value) =>
|
|
124
|
+
onChange={(value) =>
|
|
125
|
+
handleUpdate({ position: [node.position[0], node.position[1], value] })
|
|
126
|
+
}
|
|
121
127
|
precision={2}
|
|
122
128
|
step={0.01}
|
|
123
129
|
unit="m"
|
|
@@ -3,25 +3,31 @@
|
|
|
3
3
|
import {
|
|
4
4
|
type AnyNode,
|
|
5
5
|
type AnyNodeId,
|
|
6
|
+
getEffectiveStairSurfaceMaterial,
|
|
6
7
|
type LevelNode,
|
|
8
|
+
type MaterialSchema,
|
|
7
9
|
type StairNode,
|
|
10
|
+
StairNode as StairNodeSchema,
|
|
8
11
|
type StairRailingMode,
|
|
12
|
+
type StairSegmentNode,
|
|
13
|
+
StairSegmentNode as StairSegmentNodeSchema,
|
|
9
14
|
type StairSlabOpeningMode,
|
|
15
|
+
type StairSurfaceMaterialRole,
|
|
10
16
|
type StairTopLandingMode,
|
|
11
17
|
type StairType,
|
|
12
|
-
type StairSegmentNode,
|
|
13
|
-
StairSegmentNode as StairSegmentNodeSchema,
|
|
14
18
|
useScene,
|
|
15
19
|
} from '@pascal-app/core'
|
|
16
20
|
import { useViewer } from '@pascal-app/viewer'
|
|
17
21
|
import { Copy, Move, Plus, Trash2 } from 'lucide-react'
|
|
18
22
|
import { useCallback } from 'react'
|
|
19
|
-
import { duplicateStairSubtree } from '../../../lib/stair-duplication'
|
|
20
23
|
import { useShallow } from 'zustand/react/shallow'
|
|
24
|
+
import { buildStairSurfaceMaterialPatch } from '../../../lib/material-paint'
|
|
21
25
|
import { sfxEmitter } from '../../../lib/sfx-bus'
|
|
26
|
+
import { duplicateStairSubtree } from '../../../lib/stair-duplication'
|
|
22
27
|
import useEditor from '../../../store/use-editor'
|
|
23
28
|
import { DEFAULT_SPIRAL_STAIR_SWEEP_ANGLE } from '../../tools/stair/stair-defaults'
|
|
24
29
|
import { ActionButton, ActionGroup } from '../controls/action-button'
|
|
30
|
+
import { MaterialPicker } from '../controls/material-picker'
|
|
25
31
|
import { MetricControl } from '../controls/metric-control'
|
|
26
32
|
import { PanelSection } from '../controls/panel-section'
|
|
27
33
|
import { SegmentedControl } from '../controls/segmented-control'
|
|
@@ -59,6 +65,7 @@ export function StairPanel() {
|
|
|
59
65
|
const updateNode = useScene((s) => s.updateNode)
|
|
60
66
|
const createNode = useScene((s) => s.createNode)
|
|
61
67
|
const setMovingNode = useEditor((s) => s.setMovingNode)
|
|
68
|
+
const selectedMaterialTarget = useEditor((s) => s.selectedMaterialTarget)
|
|
62
69
|
|
|
63
70
|
const node = useScene((s) =>
|
|
64
71
|
selectedId ? (s.nodes[selectedId as AnyNode['id']] as StairNode | undefined) : undefined,
|
|
@@ -89,6 +96,35 @@ export function StairPanel() {
|
|
|
89
96
|
[selectedId, updateNode],
|
|
90
97
|
)
|
|
91
98
|
|
|
99
|
+
const materialTargetRole =
|
|
100
|
+
selectedMaterialTarget &&
|
|
101
|
+
selectedMaterialTarget.nodeId === node?.id &&
|
|
102
|
+
(selectedMaterialTarget.role === 'railing' ||
|
|
103
|
+
selectedMaterialTarget.role === 'tread' ||
|
|
104
|
+
selectedMaterialTarget.role === 'side')
|
|
105
|
+
? selectedMaterialTarget.role
|
|
106
|
+
: null
|
|
107
|
+
const materialPickerValue =
|
|
108
|
+
node && materialTargetRole ? getEffectiveStairSurfaceMaterial(node, materialTargetRole) : {}
|
|
109
|
+
|
|
110
|
+
const handleTargetedMaterialChange = useCallback(
|
|
111
|
+
(material: MaterialSchema) => {
|
|
112
|
+
if (!(node && materialTargetRole)) return
|
|
113
|
+
handleUpdate(buildStairSurfaceMaterialPatch(node, materialTargetRole, material, undefined))
|
|
114
|
+
},
|
|
115
|
+
[handleUpdate, materialTargetRole, node],
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
const handleTargetedMaterialPresetChange = useCallback(
|
|
119
|
+
(materialPreset: string) => {
|
|
120
|
+
if (!(node && materialTargetRole)) return
|
|
121
|
+
handleUpdate(
|
|
122
|
+
buildStairSurfaceMaterialPatch(node, materialTargetRole, undefined, materialPreset),
|
|
123
|
+
)
|
|
124
|
+
},
|
|
125
|
+
[handleUpdate, materialTargetRole, node],
|
|
126
|
+
)
|
|
127
|
+
|
|
92
128
|
const handleClose = useCallback(() => {
|
|
93
129
|
setSelection({ selectedIds: [] })
|
|
94
130
|
}, [setSelection])
|
|
@@ -222,11 +258,11 @@ export function StairPanel() {
|
|
|
222
258
|
/>
|
|
223
259
|
|
|
224
260
|
<div className="space-y-1.5">
|
|
225
|
-
<div className="px-1 text-[11px] uppercase tracking-[0.14em]
|
|
261
|
+
<div className="px-1 text-[11px] text-muted-foreground uppercase tracking-[0.14em]">
|
|
226
262
|
From Level
|
|
227
263
|
</div>
|
|
228
264
|
<select
|
|
229
|
-
className="h-9 w-full rounded-lg border border-border/50 bg-[#2C2C2E] px-3 text-
|
|
265
|
+
className="h-9 w-full rounded-lg border border-border/50 bg-[#2C2C2E] px-3 text-foreground text-sm"
|
|
230
266
|
onChange={(event) => handleUpdate({ fromLevelId: event.target.value })}
|
|
231
267
|
value={resolvedFromLevelId ?? ''}
|
|
232
268
|
>
|
|
@@ -239,11 +275,11 @@ export function StairPanel() {
|
|
|
239
275
|
</div>
|
|
240
276
|
|
|
241
277
|
<div className="space-y-1.5">
|
|
242
|
-
<div className="px-1 text-[11px] uppercase tracking-[0.14em]
|
|
278
|
+
<div className="px-1 text-[11px] text-muted-foreground uppercase tracking-[0.14em]">
|
|
243
279
|
To Level
|
|
244
280
|
</div>
|
|
245
281
|
<select
|
|
246
|
-
className="h-9 w-full rounded-lg border border-border/50 bg-[#2C2C2E] px-3 text-
|
|
282
|
+
className="h-9 w-full rounded-lg border border-border/50 bg-[#2C2C2E] px-3 text-foreground text-sm"
|
|
247
283
|
onChange={(event) => handleUpdate({ toLevelId: event.target.value })}
|
|
248
284
|
value={resolvedToLevelId ?? ''}
|
|
249
285
|
>
|
|
@@ -262,7 +298,7 @@ export function StairPanel() {
|
|
|
262
298
|
/>
|
|
263
299
|
|
|
264
300
|
{(node.slabOpeningMode ?? 'none') === 'destination' ? (
|
|
265
|
-
<
|
|
301
|
+
<MetricControl
|
|
266
302
|
label="Opening Offset"
|
|
267
303
|
max={0.5}
|
|
268
304
|
min={0}
|
|
@@ -308,7 +344,7 @@ export function StairPanel() {
|
|
|
308
344
|
|
|
309
345
|
{(node.stairType === 'curved' || node.stairType === 'spiral') && (
|
|
310
346
|
<PanelSection title="Geometry">
|
|
311
|
-
<
|
|
347
|
+
<MetricControl
|
|
312
348
|
label="Width"
|
|
313
349
|
max={10}
|
|
314
350
|
min={0.4}
|
|
@@ -318,7 +354,7 @@ export function StairPanel() {
|
|
|
318
354
|
unit="m"
|
|
319
355
|
value={Math.round((node.width ?? 1) * 100) / 100}
|
|
320
356
|
/>
|
|
321
|
-
<
|
|
357
|
+
<MetricControl
|
|
322
358
|
label="Rise"
|
|
323
359
|
max={10}
|
|
324
360
|
min={0.2}
|
|
@@ -328,7 +364,7 @@ export function StairPanel() {
|
|
|
328
364
|
unit="m"
|
|
329
365
|
value={Math.round((node.totalRise ?? 2.5) * 100) / 100}
|
|
330
366
|
/>
|
|
331
|
-
<
|
|
367
|
+
<MetricControl
|
|
332
368
|
label="Steps"
|
|
333
369
|
max={32}
|
|
334
370
|
min={2}
|
|
@@ -346,7 +382,7 @@ export function StairPanel() {
|
|
|
346
382
|
/>
|
|
347
383
|
)}
|
|
348
384
|
{(node.stairType === 'spiral' || !(node.fillToFloor ?? true)) && (
|
|
349
|
-
<
|
|
385
|
+
<MetricControl
|
|
350
386
|
label="Thickness"
|
|
351
387
|
max={1}
|
|
352
388
|
min={0.02}
|
|
@@ -357,7 +393,7 @@ export function StairPanel() {
|
|
|
357
393
|
value={Math.round((node.thickness ?? 0.25) * 100) / 100}
|
|
358
394
|
/>
|
|
359
395
|
)}
|
|
360
|
-
<
|
|
396
|
+
<MetricControl
|
|
361
397
|
label="Inner Radius"
|
|
362
398
|
max={10}
|
|
363
399
|
min={node.stairType === 'spiral' ? 0.05 : 0.2}
|
|
@@ -385,7 +421,7 @@ export function StairPanel() {
|
|
|
385
421
|
value={node.topLandingMode ?? 'none'}
|
|
386
422
|
/>
|
|
387
423
|
{(node.topLandingMode ?? 'none') === 'integrated' && (
|
|
388
|
-
<
|
|
424
|
+
<MetricControl
|
|
389
425
|
label="Top Landing"
|
|
390
426
|
max={5}
|
|
391
427
|
min={0.3}
|
|
@@ -520,6 +556,22 @@ export function StairPanel() {
|
|
|
520
556
|
/>
|
|
521
557
|
</ActionGroup>
|
|
522
558
|
</PanelSection>
|
|
559
|
+
<PanelSection title="Material">
|
|
560
|
+
{materialTargetRole ? null : (
|
|
561
|
+
<div className="mb-3 rounded-lg border border-border/50 bg-[#2C2C2E] px-3 py-2 text-[11px] text-muted-foreground">
|
|
562
|
+
Click the stair surface you want to edit. Materials apply to one target at a time.
|
|
563
|
+
</div>
|
|
564
|
+
)}
|
|
565
|
+
<MaterialPicker
|
|
566
|
+
disabled={!materialTargetRole}
|
|
567
|
+
hideSideControl
|
|
568
|
+
nodeType="stair"
|
|
569
|
+
onChange={handleTargetedMaterialChange}
|
|
570
|
+
onSelectMaterialPreset={handleTargetedMaterialPresetChange}
|
|
571
|
+
selectedMaterialPreset={materialPickerValue.materialPreset}
|
|
572
|
+
value={materialPickerValue.material}
|
|
573
|
+
/>
|
|
574
|
+
</PanelSection>
|
|
523
575
|
</PanelWrapper>
|
|
524
576
|
)
|
|
525
577
|
}
|