@pascal-app/editor 0.6.0 → 0.7.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 +9 -5
- package/src/components/editor/bottom-sheet.tsx +149 -0
- package/src/components/editor/custom-camera-controls.tsx +75 -7
- package/src/components/editor/editor-layout-mobile.tsx +264 -0
- package/src/components/editor/editor-layout-v2.tsx +20 -0
- package/src/components/editor/first-person/build-collider-world.ts +365 -0
- package/src/components/editor/first-person/bvh-ecctrl.tsx +795 -0
- package/src/components/editor/first-person-controls.tsx +496 -143
- package/src/components/editor/floating-action-menu.tsx +32 -55
- package/src/components/editor/floorplan-background-selection.ts +113 -0
- package/src/components/editor/floorplan-panel.tsx +9855 -3298
- package/src/components/editor/index.tsx +269 -21
- package/src/components/editor/selection-manager.tsx +575 -13
- package/src/components/editor/thumbnail-generator.tsx +38 -7
- package/src/components/editor/use-floorplan-background-placement.ts +257 -0
- package/src/components/editor/use-floorplan-hit-testing.ts +171 -0
- package/src/components/editor/use-floorplan-scene-data.ts +189 -0
- package/src/components/editor/wall-measurement-label.tsx +267 -36
- package/src/components/editor-2d/floorplan-action-menu-layer.tsx +95 -0
- package/src/components/editor-2d/floorplan-cursor-indicator-overlay.tsx +160 -0
- package/src/components/editor-2d/floorplan-hotkey-handlers.tsx +92 -0
- package/src/components/editor-2d/renderers/floorplan-draft-layer.tsx +119 -0
- package/src/components/editor-2d/renderers/floorplan-marquee-layer.tsx +58 -0
- package/src/components/editor-2d/renderers/floorplan-measurements-layer.tsx +197 -0
- package/src/components/editor-2d/renderers/floorplan-roof-layer.tsx +113 -0
- package/src/components/editor-2d/renderers/floorplan-stair-layer.tsx +474 -0
- package/src/components/editor-2d/svg-paths.ts +119 -0
- package/src/components/tools/ceiling/ceiling-boundary-editor.tsx +1 -0
- package/src/components/tools/ceiling/ceiling-hole-editor.tsx +1 -0
- package/src/components/tools/ceiling/ceiling-tool.tsx +5 -5
- package/src/components/tools/column/column-tool.tsx +97 -0
- package/src/components/tools/column/move-column-tool.tsx +105 -0
- package/src/components/tools/door/door-tool.tsx +7 -0
- package/src/components/tools/door/move-door-tool.tsx +28 -8
- package/src/components/tools/fence/fence-drafting.ts +10 -3
- package/src/components/tools/fence/fence-tool.tsx +159 -3
- package/src/components/tools/fence/move-fence-endpoint-tool.tsx +129 -18
- package/src/components/tools/fence/move-fence-tool.tsx +101 -34
- package/src/components/tools/item/move-tool.tsx +10 -1
- package/src/components/tools/item/placement-math.ts +30 -1
- package/src/components/tools/item/placement-strategies.ts +109 -31
- package/src/components/tools/item/placement-types.ts +7 -0
- package/src/components/tools/item/use-draft-node.ts +2 -0
- package/src/components/tools/item/use-placement-coordinator.tsx +660 -52
- package/src/components/tools/roof/move-roof-tool.tsx +22 -15
- package/src/components/tools/shared/polygon-editor.tsx +153 -28
- package/src/components/tools/shared/segment-angle.ts +156 -0
- package/src/components/tools/slab/slab-boundary-editor.tsx +1 -0
- package/src/components/tools/slab/slab-hole-editor.tsx +1 -0
- package/src/components/tools/spawn/move-spawn-tool.tsx +101 -0
- package/src/components/tools/spawn/spawn-tool.tsx +130 -0
- package/src/components/tools/tool-manager.tsx +18 -3
- package/src/components/tools/wall/move-wall-endpoint-tool.tsx +121 -20
- package/src/components/tools/wall/wall-drafting.ts +18 -9
- package/src/components/tools/wall/wall-tool.tsx +134 -2
- package/src/components/tools/window/move-window-tool.tsx +18 -0
- package/src/components/tools/window/window-tool.tsx +5 -0
- package/src/components/ui/action-menu/camera-actions.tsx +37 -33
- package/src/components/ui/action-menu/control-modes.tsx +28 -1
- package/src/components/ui/action-menu/index.tsx +91 -1
- package/src/components/ui/action-menu/structure-tools.tsx +2 -0
- package/src/components/ui/action-menu/view-toggles.tsx +424 -35
- package/src/components/ui/command-palette/editor-commands.tsx +18 -1
- package/src/components/ui/controls/material-picker.tsx +152 -165
- package/src/components/ui/controls/slider-control.tsx +66 -18
- package/src/components/ui/floating-level-selector.tsx +286 -55
- package/src/components/ui/helpers/helper-manager.tsx +5 -0
- package/src/components/ui/item-catalog/catalog-items.tsx +1116 -1219
- package/src/components/ui/item-catalog/item-catalog.tsx +42 -175
- package/src/components/ui/level-duplicate-dialog.tsx +115 -0
- package/src/components/ui/panels/ceiling-panel.tsx +1 -25
- package/src/components/ui/panels/column-panel.tsx +715 -0
- package/src/components/ui/panels/door-panel.tsx +981 -289
- package/src/components/ui/panels/fence-panel.tsx +3 -45
- package/src/components/ui/panels/mobile-panel-sheet.tsx +108 -0
- package/src/components/ui/panels/mobile-selection-bar.tsx +100 -0
- package/src/components/ui/panels/node-display.ts +39 -0
- package/src/components/ui/panels/paint-panel.tsx +138 -0
- package/src/components/ui/panels/panel-manager.tsx +210 -1
- package/src/components/ui/panels/panel-wrapper.tsx +48 -39
- package/src/components/ui/panels/reference-panel.tsx +238 -5
- package/src/components/ui/panels/roof-panel.tsx +4 -105
- package/src/components/ui/panels/roof-segment-panel.tsx +0 -25
- package/src/components/ui/panels/slab-panel.tsx +4 -30
- package/src/components/ui/panels/spawn-panel.tsx +155 -0
- package/src/components/ui/panels/stair-panel.tsx +11 -117
- package/src/components/ui/panels/stair-segment-panel.tsx +0 -25
- package/src/components/ui/panels/wall-panel.tsx +1 -95
- package/src/components/ui/panels/window-panel.tsx +660 -139
- package/src/components/ui/sidebar/mobile-tab-bar.tsx +46 -0
- package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +2 -2
- package/src/components/ui/sidebar/panels/site-panel/building-tree-node.tsx +2 -2
- package/src/components/ui/sidebar/panels/site-panel/column-tree-node.tsx +77 -0
- package/src/components/ui/sidebar/panels/site-panel/index.tsx +109 -24
- package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +2 -2
- package/src/components/ui/sidebar/panels/site-panel/spawn-tree-node.tsx +82 -0
- package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +9 -3
- package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +8 -5
- package/src/components/ui/sidebar/tab-bar.tsx +3 -0
- package/src/components/ui/viewer-toolbar.tsx +42 -1
- package/src/hooks/use-auto-frame.ts +45 -0
- package/src/hooks/use-keyboard.ts +64 -7
- package/src/hooks/use-mobile.ts +12 -12
- package/src/lib/door-interaction.ts +88 -0
- package/src/lib/floorplan/geometry.ts +263 -0
- package/src/lib/floorplan/index.ts +38 -0
- package/src/lib/floorplan/items.ts +179 -0
- package/src/lib/floorplan/selection-tool.ts +231 -0
- package/src/lib/floorplan/stairs.ts +478 -0
- package/src/lib/floorplan/types.ts +57 -0
- package/src/lib/floorplan/walls.ts +23 -0
- package/src/lib/guide-events.ts +10 -0
- package/src/lib/level-duplication.test.ts +72 -0
- package/src/lib/level-duplication.ts +153 -0
- package/src/lib/local-guide-image.ts +42 -0
- package/src/lib/material-paint.ts +284 -0
- package/src/lib/roof-duplication.ts +214 -0
- package/src/lib/scene-bounds.test.ts +183 -0
- package/src/lib/scene-bounds.ts +169 -0
- package/src/lib/stair-duplication.ts +126 -0
- package/src/lib/window-interaction.ts +86 -0
- package/src/store/use-editor.tsx +164 -8
|
@@ -3,8 +3,6 @@
|
|
|
3
3
|
import {
|
|
4
4
|
type AnyNode,
|
|
5
5
|
type AnyNodeId,
|
|
6
|
-
getEffectiveRoofSurfaceMaterial,
|
|
7
|
-
type MaterialSchema,
|
|
8
6
|
type RoofNode,
|
|
9
7
|
type RoofSurfaceMaterialRole,
|
|
10
8
|
RoofNode as RoofNodeSchema,
|
|
@@ -17,46 +15,19 @@ import { Copy, Move, Plus, Trash2 } from 'lucide-react'
|
|
|
17
15
|
import { useCallback } from 'react'
|
|
18
16
|
import { useShallow } from 'zustand/react/shallow'
|
|
19
17
|
import { sfxEmitter } from '../../../lib/sfx-bus'
|
|
18
|
+
import { duplicateRoofSubtree } from '../../../lib/roof-duplication'
|
|
20
19
|
import useEditor from '../../../store/use-editor'
|
|
21
20
|
import { ActionButton, ActionGroup } from '../controls/action-button'
|
|
22
|
-
import { MaterialPicker } from '../controls/material-picker'
|
|
23
21
|
import { PanelSection } from '../controls/panel-section'
|
|
24
22
|
import { SliderControl } from '../controls/slider-control'
|
|
25
23
|
import { PanelWrapper } from './panel-wrapper'
|
|
26
24
|
|
|
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
|
-
|
|
53
25
|
export function RoofPanel() {
|
|
54
26
|
const selectedId = useViewer((s) => s.selection.selectedIds[0])
|
|
55
27
|
const setSelection = useViewer((s) => s.setSelection)
|
|
56
28
|
const updateNode = useScene((s) => s.updateNode)
|
|
57
29
|
const createNode = useScene((s) => s.createNode)
|
|
58
30
|
const setMovingNode = useEditor((s) => s.setMovingNode)
|
|
59
|
-
const selectedMaterialTarget = useEditor((s) => s.selectedMaterialTarget)
|
|
60
31
|
|
|
61
32
|
const node = useScene((s) =>
|
|
62
33
|
selectedId ? (s.nodes[selectedId as AnyNode['id']] as RoofNode | undefined) : undefined,
|
|
@@ -79,33 +50,6 @@ export function RoofPanel() {
|
|
|
79
50
|
[selectedId, updateNode],
|
|
80
51
|
)
|
|
81
52
|
|
|
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(
|
|
94
|
-
(material: MaterialSchema) => {
|
|
95
|
-
if (!node || !materialTargetRole) return
|
|
96
|
-
handleUpdate(buildRoofSurfaceMaterialPatch(node, materialTargetRole, material, undefined))
|
|
97
|
-
},
|
|
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],
|
|
107
|
-
)
|
|
108
|
-
|
|
109
53
|
const handleClose = useCallback(() => {
|
|
110
54
|
setSelection({ selectedIds: [] })
|
|
111
55
|
}, [setSelection])
|
|
@@ -131,44 +75,15 @@ export function RoofPanel() {
|
|
|
131
75
|
)
|
|
132
76
|
|
|
133
77
|
const handleDuplicate = useCallback(() => {
|
|
134
|
-
if (!node
|
|
78
|
+
if (!node) return
|
|
135
79
|
sfxEmitter.emit('sfx:item-pick')
|
|
136
80
|
|
|
137
|
-
let duplicateInfo = structuredClone(node) as any
|
|
138
|
-
delete duplicateInfo.id
|
|
139
|
-
duplicateInfo.metadata = { ...duplicateInfo.metadata, isNew: true }
|
|
140
|
-
// Offset slightly so it's visible
|
|
141
|
-
duplicateInfo.position = [
|
|
142
|
-
duplicateInfo.position[0] + 1,
|
|
143
|
-
duplicateInfo.position[1],
|
|
144
|
-
duplicateInfo.position[2] + 1,
|
|
145
|
-
]
|
|
146
|
-
|
|
147
81
|
try {
|
|
148
|
-
|
|
149
|
-
useScene.getState().createNode(duplicate, duplicate.parentId as AnyNodeId)
|
|
150
|
-
|
|
151
|
-
// Also duplicate all child segments
|
|
152
|
-
const nodesState = useScene.getState().nodes
|
|
153
|
-
const children = node.children || []
|
|
154
|
-
|
|
155
|
-
for (const childId of children) {
|
|
156
|
-
const childNode = nodesState[childId]
|
|
157
|
-
if (childNode && childNode.type === 'roof-segment') {
|
|
158
|
-
let childDuplicateInfo = structuredClone(childNode) as any
|
|
159
|
-
delete childDuplicateInfo.id
|
|
160
|
-
childDuplicateInfo.metadata = { ...childDuplicateInfo.metadata, isNew: true }
|
|
161
|
-
const childDuplicate = RoofSegmentNodeSchema.parse(childDuplicateInfo)
|
|
162
|
-
useScene.getState().createNode(childDuplicate, duplicate.id as AnyNodeId)
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
setSelection({ selectedIds: [] })
|
|
167
|
-
setMovingNode(duplicate)
|
|
82
|
+
duplicateRoofSubtree(node.id as AnyNodeId, { mode: 'move' })
|
|
168
83
|
} catch (e) {
|
|
169
84
|
console.error('Failed to duplicate roof', e)
|
|
170
85
|
}
|
|
171
|
-
}, [node
|
|
86
|
+
}, [node])
|
|
172
87
|
|
|
173
88
|
const handleMove = useCallback(() => {
|
|
174
89
|
if (node) {
|
|
@@ -310,22 +225,6 @@ export function RoofPanel() {
|
|
|
310
225
|
/>
|
|
311
226
|
</ActionGroup>
|
|
312
227
|
</PanelSection>
|
|
313
|
-
<PanelSection title="Material">
|
|
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
|
-
/>
|
|
328
|
-
</PanelSection>
|
|
329
228
|
</PanelWrapper>
|
|
330
229
|
)
|
|
331
230
|
}
|
|
@@ -3,7 +3,6 @@
|
|
|
3
3
|
import {
|
|
4
4
|
type AnyNode,
|
|
5
5
|
type AnyNodeId,
|
|
6
|
-
type MaterialSchema,
|
|
7
6
|
type RoofSegmentNode,
|
|
8
7
|
RoofSegmentNode as RoofSegmentNodeSchema,
|
|
9
8
|
type RoofType,
|
|
@@ -15,7 +14,6 @@ import { useCallback } from 'react'
|
|
|
15
14
|
import { sfxEmitter } from '../../../lib/sfx-bus'
|
|
16
15
|
import useEditor from '../../../store/use-editor'
|
|
17
16
|
import { ActionButton, ActionGroup } from '../controls/action-button'
|
|
18
|
-
import { MaterialPicker } from '../controls/material-picker'
|
|
19
17
|
import { PanelSection } from '../controls/panel-section'
|
|
20
18
|
import { SegmentedControl } from '../controls/segmented-control'
|
|
21
19
|
import { SliderControl } from '../controls/slider-control'
|
|
@@ -52,20 +50,6 @@ export function RoofSegmentPanel() {
|
|
|
52
50
|
[selectedId, updateNode],
|
|
53
51
|
)
|
|
54
52
|
|
|
55
|
-
const handleMaterialChange = useCallback(
|
|
56
|
-
(material: MaterialSchema) => {
|
|
57
|
-
handleUpdate({ material, materialPreset: undefined })
|
|
58
|
-
},
|
|
59
|
-
[handleUpdate],
|
|
60
|
-
)
|
|
61
|
-
|
|
62
|
-
const handleMaterialPresetChange = useCallback(
|
|
63
|
-
(materialPreset: string) => {
|
|
64
|
-
handleUpdate({ materialPreset, material: undefined })
|
|
65
|
-
},
|
|
66
|
-
[handleUpdate],
|
|
67
|
-
)
|
|
68
|
-
|
|
69
53
|
const handleClose = useCallback(() => {
|
|
70
54
|
setSelection({ selectedIds: [] })
|
|
71
55
|
}, [setSelection])
|
|
@@ -322,15 +306,6 @@ export function RoofSegmentPanel() {
|
|
|
322
306
|
/>
|
|
323
307
|
</ActionGroup>
|
|
324
308
|
</PanelSection>
|
|
325
|
-
<PanelSection title="Material">
|
|
326
|
-
<MaterialPicker
|
|
327
|
-
nodeType="roof-segment"
|
|
328
|
-
onChange={handleMaterialChange}
|
|
329
|
-
onSelectMaterialPreset={handleMaterialPresetChange}
|
|
330
|
-
selectedMaterialPreset={node.materialPreset}
|
|
331
|
-
value={node.material}
|
|
332
|
-
/>
|
|
333
|
-
</PanelSection>
|
|
334
309
|
</PanelWrapper>
|
|
335
310
|
)
|
|
336
311
|
}
|
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
|
-
import { type AnyNode, type
|
|
3
|
+
import { type AnyNode, type SlabNode, useScene } from '@pascal-app/core'
|
|
4
4
|
import { useViewer } from '@pascal-app/viewer'
|
|
5
5
|
import { Edit, Move, Plus, Trash2 } from 'lucide-react'
|
|
6
6
|
import { useCallback, useEffect } from 'react'
|
|
7
7
|
import { sfxEmitter } from '../../../lib/sfx-bus'
|
|
8
8
|
import useEditor from '../../../store/use-editor'
|
|
9
9
|
import { ActionButton, ActionGroup } from '../controls/action-button'
|
|
10
|
-
import { MaterialPicker } from '../controls/material-picker'
|
|
11
10
|
import { PanelSection } from '../controls/panel-section'
|
|
12
11
|
import { SliderControl } from '../controls/slider-control'
|
|
13
12
|
import { PanelWrapper } from './panel-wrapper'
|
|
@@ -32,20 +31,6 @@ export function SlabPanel() {
|
|
|
32
31
|
[selectedId, updateNode],
|
|
33
32
|
)
|
|
34
33
|
|
|
35
|
-
const handleMaterialPresetChange = useCallback(
|
|
36
|
-
(materialPreset: string) => {
|
|
37
|
-
handleUpdate({ materialPreset, material: undefined })
|
|
38
|
-
},
|
|
39
|
-
[handleUpdate],
|
|
40
|
-
)
|
|
41
|
-
|
|
42
|
-
const handleCustomMaterialChange = useCallback(
|
|
43
|
-
(material: MaterialSchema) => {
|
|
44
|
-
handleUpdate({ material, materialPreset: undefined })
|
|
45
|
-
},
|
|
46
|
-
[handleUpdate],
|
|
47
|
-
)
|
|
48
|
-
|
|
49
34
|
const handleClose = useCallback(() => {
|
|
50
35
|
setSelection({ selectedIds: [] })
|
|
51
36
|
setEditingHole(null)
|
|
@@ -257,20 +242,9 @@ export function SlabPanel() {
|
|
|
257
242
|
/>
|
|
258
243
|
</div>
|
|
259
244
|
</PanelSection>
|
|
260
|
-
<
|
|
261
|
-
<
|
|
262
|
-
|
|
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>
|
|
273
|
-
</PanelSection>
|
|
245
|
+
<ActionGroup>
|
|
246
|
+
<ActionButton icon={<Move className="h-3.5 w-3.5" />} label="Move" onClick={handleMove} />
|
|
247
|
+
</ActionGroup>
|
|
274
248
|
</PanelWrapper>
|
|
275
249
|
)
|
|
276
250
|
}
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { type AnyNode, type SpawnNode, useLiveTransforms, useScene } from '@pascal-app/core'
|
|
4
|
+
import { useViewer } from '@pascal-app/viewer'
|
|
5
|
+
import { Move, Trash2 } from 'lucide-react'
|
|
6
|
+
import { useCallback, useEffect, useState } from 'react'
|
|
7
|
+
import { sfxEmitter } from '../../../lib/sfx-bus'
|
|
8
|
+
import useEditor from '../../../store/use-editor'
|
|
9
|
+
import { ActionButton, ActionGroup } from '../controls/action-button'
|
|
10
|
+
import { PanelSection } from '../controls/panel-section'
|
|
11
|
+
import { SliderControl } from '../controls/slider-control'
|
|
12
|
+
import { PanelWrapper } from './panel-wrapper'
|
|
13
|
+
|
|
14
|
+
export function SpawnPanel() {
|
|
15
|
+
const selectedId = useViewer((s) => s.selection.selectedIds[0])
|
|
16
|
+
const setSelection = useViewer((s) => s.setSelection)
|
|
17
|
+
const updateNode = useScene((s) => s.updateNode)
|
|
18
|
+
const deleteNode = useScene((s) => s.deleteNode)
|
|
19
|
+
const setMovingNode = useEditor((s) => s.setMovingNode)
|
|
20
|
+
|
|
21
|
+
const node = useScene((s) =>
|
|
22
|
+
selectedId ? (s.nodes[selectedId as AnyNode['id']] as SpawnNode | undefined) : undefined,
|
|
23
|
+
)
|
|
24
|
+
const [draftRotation, setDraftRotation] = useState<number | null>(null)
|
|
25
|
+
|
|
26
|
+
useEffect(() => {
|
|
27
|
+
if (!(node && node.type === 'spawn')) {
|
|
28
|
+
setDraftRotation(null)
|
|
29
|
+
return
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
setDraftRotation(node.rotation)
|
|
33
|
+
useLiveTransforms.getState().clear(node.id)
|
|
34
|
+
}, [node?.id, node?.rotation, node?.type])
|
|
35
|
+
|
|
36
|
+
const handleUpdate = useCallback(
|
|
37
|
+
(updates: Partial<SpawnNode>) => {
|
|
38
|
+
if (!(selectedId && node)) return
|
|
39
|
+
updateNode(selectedId as AnyNode['id'], updates)
|
|
40
|
+
},
|
|
41
|
+
[node, selectedId, updateNode],
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
const handleRotationChange = useCallback(
|
|
45
|
+
(degrees: number) => {
|
|
46
|
+
if (!(node && selectedId)) return
|
|
47
|
+
const nextRotation = (degrees * Math.PI) / 180
|
|
48
|
+
setDraftRotation(nextRotation)
|
|
49
|
+
useLiveTransforms.getState().set(selectedId as AnyNode['id'], {
|
|
50
|
+
position: [...node.position],
|
|
51
|
+
rotation: nextRotation,
|
|
52
|
+
})
|
|
53
|
+
},
|
|
54
|
+
[node, selectedId],
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
const commitRotation = useCallback(
|
|
58
|
+
(degrees: number) => {
|
|
59
|
+
if (!(node && selectedId)) return
|
|
60
|
+
const nextRotation = (degrees * Math.PI) / 180
|
|
61
|
+
useLiveTransforms.getState().clear(selectedId as AnyNode['id'])
|
|
62
|
+
setDraftRotation(nextRotation)
|
|
63
|
+
if (Math.abs(nextRotation - node.rotation) > 1e-6) {
|
|
64
|
+
updateNode(selectedId as AnyNode['id'], { rotation: nextRotation })
|
|
65
|
+
}
|
|
66
|
+
},
|
|
67
|
+
[node, selectedId, updateNode],
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
const handleClose = useCallback(() => {
|
|
71
|
+
setSelection({ selectedIds: [] })
|
|
72
|
+
}, [setSelection])
|
|
73
|
+
|
|
74
|
+
const handleMove = useCallback(() => {
|
|
75
|
+
if (!node) return
|
|
76
|
+
sfxEmitter.emit('sfx:item-pick')
|
|
77
|
+
setMovingNode(node)
|
|
78
|
+
setSelection({ selectedIds: [] })
|
|
79
|
+
}, [node, setMovingNode, setSelection])
|
|
80
|
+
|
|
81
|
+
const handleDelete = useCallback(() => {
|
|
82
|
+
if (!selectedId) return
|
|
83
|
+
sfxEmitter.emit('sfx:structure-delete')
|
|
84
|
+
deleteNode(selectedId as AnyNode['id'])
|
|
85
|
+
setSelection({ selectedIds: [] })
|
|
86
|
+
}, [deleteNode, selectedId, setSelection])
|
|
87
|
+
|
|
88
|
+
if (!(node && node.type === 'spawn' && selectedId)) return null
|
|
89
|
+
|
|
90
|
+
const rotationDegrees = Math.round((((draftRotation ?? node.rotation) * 180) / Math.PI))
|
|
91
|
+
const storedRotationDegrees = Math.round((node.rotation * 180) / Math.PI)
|
|
92
|
+
|
|
93
|
+
return (
|
|
94
|
+
<PanelWrapper icon="/icons/site.png" onClose={handleClose} title="Spawn Point" width={300}>
|
|
95
|
+
<PanelSection title="Position">
|
|
96
|
+
<SliderControl
|
|
97
|
+
label="X"
|
|
98
|
+
max={node.position[0] + 2}
|
|
99
|
+
min={node.position[0] - 2}
|
|
100
|
+
onChange={(value) => handleUpdate({ position: [value, node.position[1], node.position[2]] })}
|
|
101
|
+
precision={2}
|
|
102
|
+
step={0.01}
|
|
103
|
+
unit="m"
|
|
104
|
+
value={Math.round(node.position[0] * 100) / 100}
|
|
105
|
+
/>
|
|
106
|
+
<SliderControl
|
|
107
|
+
label="Y"
|
|
108
|
+
max={node.position[1] + 2}
|
|
109
|
+
min={node.position[1] - 2}
|
|
110
|
+
onChange={(value) => handleUpdate({ position: [node.position[0], value, node.position[2]] })}
|
|
111
|
+
precision={2}
|
|
112
|
+
step={0.01}
|
|
113
|
+
unit="m"
|
|
114
|
+
value={Math.round(node.position[1] * 100) / 100}
|
|
115
|
+
/>
|
|
116
|
+
<SliderControl
|
|
117
|
+
label="Z"
|
|
118
|
+
max={node.position[2] + 2}
|
|
119
|
+
min={node.position[2] - 2}
|
|
120
|
+
onChange={(value) => handleUpdate({ position: [node.position[0], node.position[1], value] })}
|
|
121
|
+
precision={2}
|
|
122
|
+
step={0.01}
|
|
123
|
+
unit="m"
|
|
124
|
+
value={Math.round(node.position[2] * 100) / 100}
|
|
125
|
+
/>
|
|
126
|
+
</PanelSection>
|
|
127
|
+
|
|
128
|
+
<PanelSection title="Facing">
|
|
129
|
+
<SliderControl
|
|
130
|
+
label="Yaw"
|
|
131
|
+
max={storedRotationDegrees + 90}
|
|
132
|
+
min={storedRotationDegrees - 90}
|
|
133
|
+
onChange={handleRotationChange}
|
|
134
|
+
onCommit={commitRotation}
|
|
135
|
+
precision={0}
|
|
136
|
+
step={1}
|
|
137
|
+
unit="°"
|
|
138
|
+
value={rotationDegrees}
|
|
139
|
+
/>
|
|
140
|
+
</PanelSection>
|
|
141
|
+
|
|
142
|
+
<PanelSection title="Actions">
|
|
143
|
+
<ActionGroup>
|
|
144
|
+
<ActionButton icon={<Move className="h-4 w-4" />} label="Move" onClick={handleMove} />
|
|
145
|
+
<ActionButton
|
|
146
|
+
className="border-red-500/40 text-red-200 hover:bg-red-500/15"
|
|
147
|
+
icon={<Trash2 className="h-4 w-4" />}
|
|
148
|
+
label="Delete"
|
|
149
|
+
onClick={handleDelete}
|
|
150
|
+
/>
|
|
151
|
+
</ActionGroup>
|
|
152
|
+
</PanelSection>
|
|
153
|
+
</PanelWrapper>
|
|
154
|
+
)
|
|
155
|
+
}
|
|
@@ -3,16 +3,12 @@
|
|
|
3
3
|
import {
|
|
4
4
|
type AnyNode,
|
|
5
5
|
type AnyNodeId,
|
|
6
|
-
getEffectiveStairSurfaceMaterial,
|
|
7
6
|
type LevelNode,
|
|
8
|
-
type MaterialSchema,
|
|
9
7
|
type StairNode,
|
|
10
8
|
type StairRailingMode,
|
|
11
|
-
type StairSurfaceMaterialRole,
|
|
12
9
|
type StairSlabOpeningMode,
|
|
13
10
|
type StairTopLandingMode,
|
|
14
11
|
type StairType,
|
|
15
|
-
StairNode as StairNodeSchema,
|
|
16
12
|
type StairSegmentNode,
|
|
17
13
|
StairSegmentNode as StairSegmentNodeSchema,
|
|
18
14
|
useScene,
|
|
@@ -20,12 +16,12 @@ import {
|
|
|
20
16
|
import { useViewer } from '@pascal-app/viewer'
|
|
21
17
|
import { Copy, Move, Plus, Trash2 } from 'lucide-react'
|
|
22
18
|
import { useCallback } from 'react'
|
|
19
|
+
import { duplicateStairSubtree } from '../../../lib/stair-duplication'
|
|
23
20
|
import { useShallow } from 'zustand/react/shallow'
|
|
24
21
|
import { sfxEmitter } from '../../../lib/sfx-bus'
|
|
25
22
|
import useEditor from '../../../store/use-editor'
|
|
26
23
|
import { DEFAULT_SPIRAL_STAIR_SWEEP_ANGLE } from '../../tools/stair/stair-defaults'
|
|
27
24
|
import { ActionButton, ActionGroup } from '../controls/action-button'
|
|
28
|
-
import { MaterialPicker } from '../controls/material-picker'
|
|
29
25
|
import { MetricControl } from '../controls/metric-control'
|
|
30
26
|
import { PanelSection } from '../controls/panel-section'
|
|
31
27
|
import { SegmentedControl } from '../controls/segmented-control'
|
|
@@ -33,32 +29,6 @@ import { SliderControl } from '../controls/slider-control'
|
|
|
33
29
|
import { ToggleControl } from '../controls/toggle-control'
|
|
34
30
|
import { PanelWrapper } from './panel-wrapper'
|
|
35
31
|
|
|
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
|
-
|
|
62
32
|
const RAILING_MODE_OPTIONS: { label: string; value: StairRailingMode }[] = [
|
|
63
33
|
{ label: 'None', value: 'none' },
|
|
64
34
|
{ label: 'Left', value: 'left' },
|
|
@@ -88,9 +58,7 @@ export function StairPanel() {
|
|
|
88
58
|
const setSelection = useViewer((s) => s.setSelection)
|
|
89
59
|
const updateNode = useScene((s) => s.updateNode)
|
|
90
60
|
const createNode = useScene((s) => s.createNode)
|
|
91
|
-
const createNodes = useScene((s) => s.createNodes)
|
|
92
61
|
const setMovingNode = useEditor((s) => s.setMovingNode)
|
|
93
|
-
const selectedMaterialTarget = useEditor((s) => s.selectedMaterialTarget)
|
|
94
62
|
|
|
95
63
|
const node = useScene((s) =>
|
|
96
64
|
selectedId ? (s.nodes[selectedId as AnyNode['id']] as StairNode | undefined) : undefined,
|
|
@@ -121,33 +89,6 @@ export function StairPanel() {
|
|
|
121
89
|
[selectedId, updateNode],
|
|
122
90
|
)
|
|
123
91
|
|
|
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(
|
|
136
|
-
(material: MaterialSchema) => {
|
|
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))
|
|
147
|
-
},
|
|
148
|
-
[handleUpdate, materialTargetRole, node],
|
|
149
|
-
)
|
|
150
|
-
|
|
151
92
|
const handleClose = useCallback(() => {
|
|
152
93
|
setSelection({ selectedIds: [] })
|
|
153
94
|
}, [setSelection])
|
|
@@ -209,46 +150,15 @@ export function StairPanel() {
|
|
|
209
150
|
)
|
|
210
151
|
|
|
211
152
|
const handleDuplicate = useCallback(() => {
|
|
212
|
-
if (!node
|
|
153
|
+
if (!node) return
|
|
213
154
|
sfxEmitter.emit('sfx:item-pick')
|
|
214
155
|
|
|
215
|
-
let duplicateInfo = structuredClone(node) as any
|
|
216
|
-
delete duplicateInfo.id
|
|
217
|
-
duplicateInfo.metadata = { ...duplicateInfo.metadata }
|
|
218
|
-
duplicateInfo.children = []
|
|
219
|
-
duplicateInfo.position = [
|
|
220
|
-
duplicateInfo.position[0] + 1,
|
|
221
|
-
duplicateInfo.position[1],
|
|
222
|
-
duplicateInfo.position[2] + 1,
|
|
223
|
-
]
|
|
224
|
-
|
|
225
156
|
try {
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
const nodesState = useScene.getState().nodes
|
|
229
|
-
const children = node.children || []
|
|
230
|
-
const createOps: { node: AnyNode; parentId?: AnyNodeId }[] = [
|
|
231
|
-
{ node: duplicate, parentId: duplicate.parentId as AnyNodeId },
|
|
232
|
-
]
|
|
233
|
-
|
|
234
|
-
for (const childId of children) {
|
|
235
|
-
const childNode = nodesState[childId]
|
|
236
|
-
if (childNode && childNode.type === 'stair-segment') {
|
|
237
|
-
let childDuplicateInfo = structuredClone(childNode) as any
|
|
238
|
-
delete childDuplicateInfo.id
|
|
239
|
-
childDuplicateInfo.metadata = { ...childDuplicateInfo.metadata }
|
|
240
|
-
const childDuplicate = StairSegmentNodeSchema.parse(childDuplicateInfo)
|
|
241
|
-
createOps.push({ node: childDuplicate, parentId: duplicate.id as AnyNodeId })
|
|
242
|
-
}
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
createNodes(createOps)
|
|
246
|
-
|
|
247
|
-
setSelection({ selectedIds: [duplicate.id as AnyNode['id']] })
|
|
157
|
+
duplicateStairSubtree(node.id as AnyNodeId, { mode: 'move' })
|
|
248
158
|
} catch (e) {
|
|
249
159
|
console.error('Failed to duplicate stair', e)
|
|
250
160
|
}
|
|
251
|
-
}, [
|
|
161
|
+
}, [node])
|
|
252
162
|
|
|
253
163
|
const handleMove = useCallback(() => {
|
|
254
164
|
if (node) {
|
|
@@ -352,7 +262,7 @@ export function StairPanel() {
|
|
|
352
262
|
/>
|
|
353
263
|
|
|
354
264
|
{(node.slabOpeningMode ?? 'none') === 'destination' ? (
|
|
355
|
-
<
|
|
265
|
+
<SliderControl
|
|
356
266
|
label="Opening Offset"
|
|
357
267
|
max={0.5}
|
|
358
268
|
min={0}
|
|
@@ -398,7 +308,7 @@ export function StairPanel() {
|
|
|
398
308
|
|
|
399
309
|
{(node.stairType === 'curved' || node.stairType === 'spiral') && (
|
|
400
310
|
<PanelSection title="Geometry">
|
|
401
|
-
<
|
|
311
|
+
<SliderControl
|
|
402
312
|
label="Width"
|
|
403
313
|
max={10}
|
|
404
314
|
min={0.4}
|
|
@@ -408,7 +318,7 @@ export function StairPanel() {
|
|
|
408
318
|
unit="m"
|
|
409
319
|
value={Math.round((node.width ?? 1) * 100) / 100}
|
|
410
320
|
/>
|
|
411
|
-
<
|
|
321
|
+
<SliderControl
|
|
412
322
|
label="Rise"
|
|
413
323
|
max={10}
|
|
414
324
|
min={0.2}
|
|
@@ -418,7 +328,7 @@ export function StairPanel() {
|
|
|
418
328
|
unit="m"
|
|
419
329
|
value={Math.round((node.totalRise ?? 2.5) * 100) / 100}
|
|
420
330
|
/>
|
|
421
|
-
<
|
|
331
|
+
<SliderControl
|
|
422
332
|
label="Steps"
|
|
423
333
|
max={32}
|
|
424
334
|
min={2}
|
|
@@ -436,7 +346,7 @@ export function StairPanel() {
|
|
|
436
346
|
/>
|
|
437
347
|
)}
|
|
438
348
|
{(node.stairType === 'spiral' || !(node.fillToFloor ?? true)) && (
|
|
439
|
-
<
|
|
349
|
+
<SliderControl
|
|
440
350
|
label="Thickness"
|
|
441
351
|
max={1}
|
|
442
352
|
min={0.02}
|
|
@@ -447,7 +357,7 @@ export function StairPanel() {
|
|
|
447
357
|
value={Math.round((node.thickness ?? 0.25) * 100) / 100}
|
|
448
358
|
/>
|
|
449
359
|
)}
|
|
450
|
-
<
|
|
360
|
+
<SliderControl
|
|
451
361
|
label="Inner Radius"
|
|
452
362
|
max={10}
|
|
453
363
|
min={node.stairType === 'spiral' ? 0.05 : 0.2}
|
|
@@ -475,7 +385,7 @@ export function StairPanel() {
|
|
|
475
385
|
value={node.topLandingMode ?? 'none'}
|
|
476
386
|
/>
|
|
477
387
|
{(node.topLandingMode ?? 'none') === 'integrated' && (
|
|
478
|
-
<
|
|
388
|
+
<SliderControl
|
|
479
389
|
label="Top Landing"
|
|
480
390
|
max={5}
|
|
481
391
|
min={0.3}
|
|
@@ -610,22 +520,6 @@ export function StairPanel() {
|
|
|
610
520
|
/>
|
|
611
521
|
</ActionGroup>
|
|
612
522
|
</PanelSection>
|
|
613
|
-
<PanelSection title="Material">
|
|
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
|
-
/>
|
|
628
|
-
</PanelSection>
|
|
629
523
|
</PanelWrapper>
|
|
630
524
|
)
|
|
631
525
|
}
|