@pascal-app/editor 0.6.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 +13 -9
- package/src/components/editor/bottom-sheet.tsx +149 -0
- package/src/components/editor/custom-camera-controls.tsx +74 -5
- package/src/components/editor/editor-layout-mobile.tsx +264 -0
- package/src/components/editor/editor-layout-v2.tsx +24 -3
- package/src/components/editor/first-person/build-collider-world.ts +363 -0
- package/src/components/editor/first-person/bvh-ecctrl.tsx +860 -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 +9861 -3297
- package/src/components/editor/index.tsx +295 -32
- package/src/components/editor/selection-manager.tsx +575 -13
- package/src/components/editor/snapshot-capture-overlay.tsx +465 -0
- package/src/components/editor/thumbnail-generator.tsx +56 -68
- 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 +124 -0
- package/src/components/editor-2d/renderers/floorplan-marquee-layer.tsx +58 -0
- package/src/components/editor-2d/renderers/floorplan-measurements-layer.tsx +202 -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/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/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/ceiling/move-ceiling-tool.tsx +9 -2
- 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/curve-fence-tool.tsx +4 -5
- package/src/components/tools/fence/fence-drafting.ts +10 -3
- package/src/components/tools/fence/fence-tool.tsx +160 -4
- package/src/components/tools/fence/move-fence-endpoint-tool.tsx +139 -25
- package/src/components/tools/fence/move-fence-tool.tsx +111 -40
- package/src/components/tools/item/move-tool.tsx +7 -1
- package/src/components/tools/item/placement-math.ts +32 -5
- package/src/components/tools/item/placement-strategies.ts +110 -31
- package/src/components/tools/item/placement-types.ts +7 -0
- package/src/components/tools/item/use-draft-node.ts +1 -0
- package/src/components/tools/item/use-placement-coordinator.tsx +558 -52
- package/src/components/tools/roof/move-roof-tool.tsx +29 -17
- package/src/components/tools/select/box-select-tool.tsx +12 -17
- 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 +20 -5
- package/src/components/tools/wall/curve-wall-tool.tsx +8 -6
- package/src/components/tools/wall/move-wall-endpoint-tool.tsx +131 -27
- package/src/components/tools/wall/move-wall-tool.tsx +6 -4
- package/src/components/tools/wall/wall-drafting.ts +18 -9
- package/src/components/tools/wall/wall-tool.tsx +136 -4
- package/src/components/tools/window/move-window-tool.tsx +18 -0
- package/src/components/tools/window/window-tool.tsx +5 -0
- package/src/components/tools/zone/zone-tool.tsx +20 -5
- package/src/components/ui/action-menu/camera-actions.tsx +37 -33
- package/src/components/ui/action-menu/control-modes.tsx +34 -1
- package/src/components/ui/action-menu/furnish-tools.tsx +6 -92
- package/src/components/ui/action-menu/index.tsx +98 -59
- package/src/components/ui/action-menu/structure-tools.tsx +2 -0
- package/src/components/ui/action-menu/view-toggles.tsx +418 -41
- package/src/components/ui/command-palette/editor-commands.tsx +24 -5
- package/src/components/ui/command-palette/index.tsx +4 -255
- package/src/components/ui/controls/material-picker.tsx +154 -164
- 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 +10 -0
- package/src/components/ui/item-catalog/catalog-items.tsx +2563 -1239
- package/src/components/ui/item-catalog/item-catalog.tsx +96 -187
- package/src/components/ui/level-duplicate-dialog.tsx +113 -0
- package/src/components/ui/panels/ceiling-panel.tsx +3 -28
- package/src/components/ui/panels/column-panel.tsx +759 -0
- package/src/components/ui/panels/door-panel.tsx +989 -290
- package/src/components/ui/panels/fence-panel.tsx +2 -49
- 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 +163 -0
- package/src/components/ui/panels/panel-manager.tsx +208 -28
- package/src/components/ui/panels/panel-wrapper.tsx +48 -39
- package/src/components/ui/panels/reference-panel.tsx +253 -5
- package/src/components/ui/panels/roof-panel.tsx +13 -64
- 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 +161 -0
- package/src/components/ui/panels/stair-panel.tsx +20 -74
- package/src/components/ui/panels/stair-segment-panel.tsx +0 -25
- package/src/components/ui/panels/wall-panel.tsx +10 -8
- package/src/components/ui/panels/window-panel.tsx +668 -139
- 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/mobile-tab-bar.tsx +46 -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/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/ceiling-tree-node.tsx +0 -0
- package/src/components/ui/sidebar/panels/site-panel/column-tree-node.tsx +77 -0
- package/src/components/ui/sidebar/panels/site-panel/index.tsx +105 -22
- package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +2 -2
- 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 +76 -0
- package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +11 -3
- package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +10 -5
- package/src/components/ui/sidebar/panels/zone-panel/index.tsx +1 -1
- package/src/components/ui/sidebar/tab-bar.tsx +3 -0
- 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-frame.ts +45 -0
- package/src/hooks/use-auto-save.ts +14 -0
- package/src/hooks/use-keyboard.ts +74 -7
- package/src/hooks/use-mobile.ts +12 -12
- package/src/index.tsx +8 -1
- 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 +70 -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/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 +126 -0
- package/src/lib/window-interaction.ts +86 -0
- package/src/store/use-editor.tsx +186 -62
- 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 -395
|
@@ -6,16 +6,18 @@ import {
|
|
|
6
6
|
getEffectiveRoofSurfaceMaterial,
|
|
7
7
|
type MaterialSchema,
|
|
8
8
|
type RoofNode,
|
|
9
|
-
type RoofSurfaceMaterialRole,
|
|
10
9
|
RoofNode as RoofNodeSchema,
|
|
11
10
|
type RoofSegmentNode,
|
|
12
11
|
RoofSegmentNode as RoofSegmentNodeSchema,
|
|
12
|
+
type RoofSurfaceMaterialRole,
|
|
13
13
|
useScene,
|
|
14
14
|
} from '@pascal-app/core'
|
|
15
15
|
import { useViewer } from '@pascal-app/viewer'
|
|
16
16
|
import { Copy, Move, Plus, Trash2 } from 'lucide-react'
|
|
17
17
|
import { useCallback } from 'react'
|
|
18
18
|
import { useShallow } from 'zustand/react/shallow'
|
|
19
|
+
import { buildRoofSurfaceMaterialPatch } from '../../../lib/material-paint'
|
|
20
|
+
import { duplicateRoofSubtree } from '../../../lib/roof-duplication'
|
|
19
21
|
import { sfxEmitter } from '../../../lib/sfx-bus'
|
|
20
22
|
import useEditor from '../../../store/use-editor'
|
|
21
23
|
import { ActionButton, ActionGroup } from '../controls/action-button'
|
|
@@ -24,32 +26,6 @@ import { PanelSection } from '../controls/panel-section'
|
|
|
24
26
|
import { SliderControl } from '../controls/slider-control'
|
|
25
27
|
import { PanelWrapper } from './panel-wrapper'
|
|
26
28
|
|
|
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
29
|
export function RoofPanel() {
|
|
54
30
|
const selectedId = useViewer((s) => s.selection.selectedIds[0])
|
|
55
31
|
const setSelection = useViewer((s) => s.setSelection)
|
|
@@ -92,7 +68,7 @@ export function RoofPanel() {
|
|
|
92
68
|
|
|
93
69
|
const handleTargetedMaterialChange = useCallback(
|
|
94
70
|
(material: MaterialSchema) => {
|
|
95
|
-
if (!node
|
|
71
|
+
if (!(node && materialTargetRole)) return
|
|
96
72
|
handleUpdate(buildRoofSurfaceMaterialPatch(node, materialTargetRole, material, undefined))
|
|
97
73
|
},
|
|
98
74
|
[handleUpdate, materialTargetRole, node],
|
|
@@ -100,8 +76,10 @@ export function RoofPanel() {
|
|
|
100
76
|
|
|
101
77
|
const handleTargetedMaterialPresetChange = useCallback(
|
|
102
78
|
(materialPreset: string) => {
|
|
103
|
-
if (!node
|
|
104
|
-
handleUpdate(
|
|
79
|
+
if (!(node && materialTargetRole)) return
|
|
80
|
+
handleUpdate(
|
|
81
|
+
buildRoofSurfaceMaterialPatch(node, materialTargetRole, undefined, materialPreset),
|
|
82
|
+
)
|
|
105
83
|
},
|
|
106
84
|
[handleUpdate, materialTargetRole, node],
|
|
107
85
|
)
|
|
@@ -131,44 +109,15 @@ export function RoofPanel() {
|
|
|
131
109
|
)
|
|
132
110
|
|
|
133
111
|
const handleDuplicate = useCallback(() => {
|
|
134
|
-
if (!node
|
|
112
|
+
if (!node) return
|
|
135
113
|
sfxEmitter.emit('sfx:item-pick')
|
|
136
114
|
|
|
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
115
|
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)
|
|
116
|
+
duplicateRoofSubtree(node.id as AnyNodeId, { mode: 'move' })
|
|
168
117
|
} catch (e) {
|
|
169
118
|
console.error('Failed to duplicate roof', e)
|
|
170
119
|
}
|
|
171
|
-
}, [node
|
|
120
|
+
}, [node])
|
|
172
121
|
|
|
173
122
|
const handleMove = useCallback(() => {
|
|
174
123
|
if (node) {
|
|
@@ -311,11 +260,11 @@ export function RoofPanel() {
|
|
|
311
260
|
</ActionGroup>
|
|
312
261
|
</PanelSection>
|
|
313
262
|
<PanelSection title="Material">
|
|
314
|
-
{
|
|
263
|
+
{materialTargetRole ? null : (
|
|
315
264
|
<div className="mb-3 rounded-lg border border-border/50 bg-[#2C2C2E] px-3 py-2 text-[11px] text-muted-foreground">
|
|
316
265
|
Click the roof surface you want to edit. Materials apply to one target at a time.
|
|
317
266
|
</div>
|
|
318
|
-
)
|
|
267
|
+
)}
|
|
319
268
|
<MaterialPicker
|
|
320
269
|
disabled={!materialTargetRole}
|
|
321
270
|
hideSideControl
|
|
@@ -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,161 @@
|
|
|
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) =>
|
|
101
|
+
handleUpdate({ position: [value, node.position[1], node.position[2]] })
|
|
102
|
+
}
|
|
103
|
+
precision={2}
|
|
104
|
+
step={0.01}
|
|
105
|
+
unit="m"
|
|
106
|
+
value={Math.round(node.position[0] * 100) / 100}
|
|
107
|
+
/>
|
|
108
|
+
<SliderControl
|
|
109
|
+
label="Y"
|
|
110
|
+
max={node.position[1] + 2}
|
|
111
|
+
min={node.position[1] - 2}
|
|
112
|
+
onChange={(value) =>
|
|
113
|
+
handleUpdate({ position: [node.position[0], value, node.position[2]] })
|
|
114
|
+
}
|
|
115
|
+
precision={2}
|
|
116
|
+
step={0.01}
|
|
117
|
+
unit="m"
|
|
118
|
+
value={Math.round(node.position[1] * 100) / 100}
|
|
119
|
+
/>
|
|
120
|
+
<SliderControl
|
|
121
|
+
label="Z"
|
|
122
|
+
max={node.position[2] + 2}
|
|
123
|
+
min={node.position[2] - 2}
|
|
124
|
+
onChange={(value) =>
|
|
125
|
+
handleUpdate({ position: [node.position[0], node.position[1], value] })
|
|
126
|
+
}
|
|
127
|
+
precision={2}
|
|
128
|
+
step={0.01}
|
|
129
|
+
unit="m"
|
|
130
|
+
value={Math.round(node.position[2] * 100) / 100}
|
|
131
|
+
/>
|
|
132
|
+
</PanelSection>
|
|
133
|
+
|
|
134
|
+
<PanelSection title="Facing">
|
|
135
|
+
<SliderControl
|
|
136
|
+
label="Yaw"
|
|
137
|
+
max={storedRotationDegrees + 90}
|
|
138
|
+
min={storedRotationDegrees - 90}
|
|
139
|
+
onChange={handleRotationChange}
|
|
140
|
+
onCommit={commitRotation}
|
|
141
|
+
precision={0}
|
|
142
|
+
step={1}
|
|
143
|
+
unit="°"
|
|
144
|
+
value={rotationDegrees}
|
|
145
|
+
/>
|
|
146
|
+
</PanelSection>
|
|
147
|
+
|
|
148
|
+
<PanelSection title="Actions">
|
|
149
|
+
<ActionGroup>
|
|
150
|
+
<ActionButton icon={<Move className="h-4 w-4" />} label="Move" onClick={handleMove} />
|
|
151
|
+
<ActionButton
|
|
152
|
+
className="border-red-500/40 text-red-200 hover:bg-red-500/15"
|
|
153
|
+
icon={<Trash2 className="h-4 w-4" />}
|
|
154
|
+
label="Delete"
|
|
155
|
+
onClick={handleDelete}
|
|
156
|
+
/>
|
|
157
|
+
</ActionGroup>
|
|
158
|
+
</PanelSection>
|
|
159
|
+
</PanelWrapper>
|
|
160
|
+
)
|
|
161
|
+
}
|
|
@@ -7,21 +7,23 @@ import {
|
|
|
7
7
|
type LevelNode,
|
|
8
8
|
type MaterialSchema,
|
|
9
9
|
type StairNode,
|
|
10
|
+
StairNode as StairNodeSchema,
|
|
10
11
|
type StairRailingMode,
|
|
11
|
-
type
|
|
12
|
+
type StairSegmentNode,
|
|
13
|
+
StairSegmentNode as StairSegmentNodeSchema,
|
|
12
14
|
type StairSlabOpeningMode,
|
|
15
|
+
type StairSurfaceMaterialRole,
|
|
13
16
|
type StairTopLandingMode,
|
|
14
17
|
type StairType,
|
|
15
|
-
StairNode as StairNodeSchema,
|
|
16
|
-
type StairSegmentNode,
|
|
17
|
-
StairSegmentNode as StairSegmentNodeSchema,
|
|
18
18
|
useScene,
|
|
19
19
|
} from '@pascal-app/core'
|
|
20
20
|
import { useViewer } from '@pascal-app/viewer'
|
|
21
21
|
import { Copy, Move, Plus, Trash2 } from 'lucide-react'
|
|
22
22
|
import { useCallback } from 'react'
|
|
23
23
|
import { useShallow } from 'zustand/react/shallow'
|
|
24
|
+
import { buildStairSurfaceMaterialPatch } from '../../../lib/material-paint'
|
|
24
25
|
import { sfxEmitter } from '../../../lib/sfx-bus'
|
|
26
|
+
import { duplicateStairSubtree } from '../../../lib/stair-duplication'
|
|
25
27
|
import useEditor from '../../../store/use-editor'
|
|
26
28
|
import { DEFAULT_SPIRAL_STAIR_SWEEP_ANGLE } from '../../tools/stair/stair-defaults'
|
|
27
29
|
import { ActionButton, ActionGroup } from '../controls/action-button'
|
|
@@ -33,32 +35,6 @@ import { SliderControl } from '../controls/slider-control'
|
|
|
33
35
|
import { ToggleControl } from '../controls/toggle-control'
|
|
34
36
|
import { PanelWrapper } from './panel-wrapper'
|
|
35
37
|
|
|
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
38
|
const RAILING_MODE_OPTIONS: { label: string; value: StairRailingMode }[] = [
|
|
63
39
|
{ label: 'None', value: 'none' },
|
|
64
40
|
{ label: 'Left', value: 'left' },
|
|
@@ -88,7 +64,6 @@ export function StairPanel() {
|
|
|
88
64
|
const setSelection = useViewer((s) => s.setSelection)
|
|
89
65
|
const updateNode = useScene((s) => s.updateNode)
|
|
90
66
|
const createNode = useScene((s) => s.createNode)
|
|
91
|
-
const createNodes = useScene((s) => s.createNodes)
|
|
92
67
|
const setMovingNode = useEditor((s) => s.setMovingNode)
|
|
93
68
|
const selectedMaterialTarget = useEditor((s) => s.selectedMaterialTarget)
|
|
94
69
|
|
|
@@ -134,7 +109,7 @@ export function StairPanel() {
|
|
|
134
109
|
|
|
135
110
|
const handleTargetedMaterialChange = useCallback(
|
|
136
111
|
(material: MaterialSchema) => {
|
|
137
|
-
if (!node
|
|
112
|
+
if (!(node && materialTargetRole)) return
|
|
138
113
|
handleUpdate(buildStairSurfaceMaterialPatch(node, materialTargetRole, material, undefined))
|
|
139
114
|
},
|
|
140
115
|
[handleUpdate, materialTargetRole, node],
|
|
@@ -142,8 +117,10 @@ export function StairPanel() {
|
|
|
142
117
|
|
|
143
118
|
const handleTargetedMaterialPresetChange = useCallback(
|
|
144
119
|
(materialPreset: string) => {
|
|
145
|
-
if (!node
|
|
146
|
-
handleUpdate(
|
|
120
|
+
if (!(node && materialTargetRole)) return
|
|
121
|
+
handleUpdate(
|
|
122
|
+
buildStairSurfaceMaterialPatch(node, materialTargetRole, undefined, materialPreset),
|
|
123
|
+
)
|
|
147
124
|
},
|
|
148
125
|
[handleUpdate, materialTargetRole, node],
|
|
149
126
|
)
|
|
@@ -209,46 +186,15 @@ export function StairPanel() {
|
|
|
209
186
|
)
|
|
210
187
|
|
|
211
188
|
const handleDuplicate = useCallback(() => {
|
|
212
|
-
if (!node
|
|
189
|
+
if (!node) return
|
|
213
190
|
sfxEmitter.emit('sfx:item-pick')
|
|
214
191
|
|
|
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
192
|
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']] })
|
|
193
|
+
duplicateStairSubtree(node.id as AnyNodeId, { mode: 'move' })
|
|
248
194
|
} catch (e) {
|
|
249
195
|
console.error('Failed to duplicate stair', e)
|
|
250
196
|
}
|
|
251
|
-
}, [
|
|
197
|
+
}, [node])
|
|
252
198
|
|
|
253
199
|
const handleMove = useCallback(() => {
|
|
254
200
|
if (node) {
|
|
@@ -312,11 +258,11 @@ export function StairPanel() {
|
|
|
312
258
|
/>
|
|
313
259
|
|
|
314
260
|
<div className="space-y-1.5">
|
|
315
|
-
<div className="px-1 text-[11px] uppercase tracking-[0.14em]
|
|
261
|
+
<div className="px-1 text-[11px] text-muted-foreground uppercase tracking-[0.14em]">
|
|
316
262
|
From Level
|
|
317
263
|
</div>
|
|
318
264
|
<select
|
|
319
|
-
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"
|
|
320
266
|
onChange={(event) => handleUpdate({ fromLevelId: event.target.value })}
|
|
321
267
|
value={resolvedFromLevelId ?? ''}
|
|
322
268
|
>
|
|
@@ -329,11 +275,11 @@ export function StairPanel() {
|
|
|
329
275
|
</div>
|
|
330
276
|
|
|
331
277
|
<div className="space-y-1.5">
|
|
332
|
-
<div className="px-1 text-[11px] uppercase tracking-[0.14em]
|
|
278
|
+
<div className="px-1 text-[11px] text-muted-foreground uppercase tracking-[0.14em]">
|
|
333
279
|
To Level
|
|
334
280
|
</div>
|
|
335
281
|
<select
|
|
336
|
-
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"
|
|
337
283
|
onChange={(event) => handleUpdate({ toLevelId: event.target.value })}
|
|
338
284
|
value={resolvedToLevelId ?? ''}
|
|
339
285
|
>
|
|
@@ -611,11 +557,11 @@ export function StairPanel() {
|
|
|
611
557
|
</ActionGroup>
|
|
612
558
|
</PanelSection>
|
|
613
559
|
<PanelSection title="Material">
|
|
614
|
-
{
|
|
560
|
+
{materialTargetRole ? null : (
|
|
615
561
|
<div className="mb-3 rounded-lg border border-border/50 bg-[#2C2C2E] px-3 py-2 text-[11px] text-muted-foreground">
|
|
616
562
|
Click the stair surface you want to edit. Materials apply to one target at a time.
|
|
617
563
|
</div>
|
|
618
|
-
)
|
|
564
|
+
)}
|
|
619
565
|
<MaterialPicker
|
|
620
566
|
disabled={!materialTargetRole}
|
|
621
567
|
hideSideControl
|
|
@@ -4,7 +4,6 @@ import {
|
|
|
4
4
|
type AnyNode,
|
|
5
5
|
type AnyNodeId,
|
|
6
6
|
type AttachmentSide,
|
|
7
|
-
type MaterialSchema,
|
|
8
7
|
type StairSegmentNode,
|
|
9
8
|
StairSegmentNode as StairSegmentNodeSchema,
|
|
10
9
|
type StairSegmentType,
|
|
@@ -16,7 +15,6 @@ import { useCallback } from 'react'
|
|
|
16
15
|
import { sfxEmitter } from '../../../lib/sfx-bus'
|
|
17
16
|
import useEditor from '../../../store/use-editor'
|
|
18
17
|
import { ActionButton, ActionGroup } from '../controls/action-button'
|
|
19
|
-
import { MaterialPicker } from '../controls/material-picker'
|
|
20
18
|
import { PanelSection } from '../controls/panel-section'
|
|
21
19
|
import { SegmentedControl } from '../controls/segmented-control'
|
|
22
20
|
import { SliderControl } from '../controls/slider-control'
|
|
@@ -61,20 +59,6 @@ export function StairSegmentPanel() {
|
|
|
61
59
|
[selectedId, updateNode],
|
|
62
60
|
)
|
|
63
61
|
|
|
64
|
-
const handleMaterialChange = useCallback(
|
|
65
|
-
(material: MaterialSchema) => {
|
|
66
|
-
handleUpdate({ material, materialPreset: undefined })
|
|
67
|
-
},
|
|
68
|
-
[handleUpdate],
|
|
69
|
-
)
|
|
70
|
-
|
|
71
|
-
const handleMaterialPresetChange = useCallback(
|
|
72
|
-
(materialPreset: string) => {
|
|
73
|
-
handleUpdate({ materialPreset, material: undefined })
|
|
74
|
-
},
|
|
75
|
-
[handleUpdate],
|
|
76
|
-
)
|
|
77
|
-
|
|
78
62
|
const handleClose = useCallback(() => {
|
|
79
63
|
setSelection({ selectedIds: [] })
|
|
80
64
|
}, [setSelection])
|
|
@@ -336,15 +320,6 @@ export function StairSegmentPanel() {
|
|
|
336
320
|
/>
|
|
337
321
|
</ActionGroup>
|
|
338
322
|
</PanelSection>
|
|
339
|
-
<PanelSection title="Material">
|
|
340
|
-
<MaterialPicker
|
|
341
|
-
nodeType="stair-segment"
|
|
342
|
-
onChange={handleMaterialChange}
|
|
343
|
-
onSelectMaterialPreset={handleMaterialPresetChange}
|
|
344
|
-
selectedMaterialPreset={node.materialPreset}
|
|
345
|
-
value={node.material}
|
|
346
|
-
/>
|
|
347
|
-
</PanelSection>
|
|
348
323
|
</PanelWrapper>
|
|
349
324
|
)
|
|
350
325
|
}
|