@pascal-app/editor 0.4.0 → 0.5.1
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 +5 -5
- package/src/components/editor/floating-action-menu.tsx +101 -29
- package/src/components/editor/floating-building-action-menu.tsx +69 -0
- package/src/components/editor/floorplan-panel.tsx +31 -13
- package/src/components/editor/index.tsx +219 -167
- package/src/components/editor/node-action-menu.tsx +26 -10
- package/src/components/editor/selection-manager.tsx +38 -2
- package/src/components/editor/thumbnail-generator.tsx +245 -64
- package/src/components/systems/stair/stair-edit-system.tsx +27 -5
- package/src/components/tools/building/move-building-tool.tsx +157 -0
- package/src/components/tools/door/door-math.ts +1 -1
- package/src/components/tools/door/door-tool.tsx +19 -7
- package/src/components/tools/door/move-door-tool.tsx +17 -8
- package/src/components/tools/fence/fence-drafting.ts +125 -0
- package/src/components/tools/fence/fence-tool.tsx +190 -0
- package/src/components/tools/fence/move-fence-tool.tsx +223 -0
- package/src/components/tools/item/item-tool.tsx +3 -3
- package/src/components/tools/item/move-tool.tsx +7 -0
- package/src/components/tools/item/placement-strategies.ts +15 -7
- package/src/components/tools/item/use-placement-coordinator.tsx +89 -14
- package/src/components/tools/roof/move-roof-tool.tsx +5 -2
- package/src/components/tools/roof/roof-tool.tsx +6 -6
- package/src/components/tools/select/box-select-tool.tsx +2 -2
- package/src/components/tools/shared/polygon-editor.tsx +2 -2
- package/src/components/tools/slab/slab-tool.tsx +4 -4
- package/src/components/tools/stair/stair-defaults.ts +10 -0
- package/src/components/tools/stair/stair-tool.tsx +29 -6
- package/src/components/tools/tool-manager.tsx +42 -14
- package/src/components/tools/wall/wall-tool.tsx +19 -29
- package/src/components/tools/window/move-window-tool.tsx +17 -8
- package/src/components/tools/window/window-math.ts +1 -1
- package/src/components/tools/window/window-tool.tsx +19 -7
- package/src/components/tools/zone/zone-tool.tsx +7 -7
- package/src/components/ui/action-menu/structure-tools.tsx +1 -0
- package/src/components/ui/helpers/building-helper.tsx +32 -0
- package/src/components/ui/helpers/helper-manager.tsx +2 -0
- package/src/components/ui/panels/fence-panel.tsx +184 -0
- package/src/components/ui/panels/panel-manager.tsx +3 -0
- package/src/components/ui/panels/stair-panel.tsx +206 -33
- package/src/components/ui/sidebar/panels/site-panel/building-tree-node.tsx +22 -15
- package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +60 -52
- package/src/components/ui/sidebar/panels/site-panel/door-tree-node.tsx +35 -24
- package/src/components/ui/sidebar/panels/site-panel/fence-tree-node.tsx +65 -0
- package/src/components/ui/sidebar/panels/site-panel/index.tsx +59 -40
- package/src/components/ui/sidebar/panels/site-panel/inline-rename-input.tsx +14 -13
- package/src/components/ui/sidebar/panels/site-panel/item-tree-node.tsx +59 -52
- package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +27 -22
- package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +66 -49
- package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +35 -36
- package/src/components/ui/sidebar/panels/site-panel/stair-tree-node.tsx +66 -49
- package/src/components/ui/sidebar/panels/site-panel/tree-node-actions.tsx +11 -11
- package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +17 -14
- package/src/components/ui/sidebar/panels/site-panel/wall-tree-node.tsx +57 -53
- package/src/components/ui/sidebar/panels/site-panel/window-tree-node.tsx +35 -24
- package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +22 -27
- package/src/components/viewer-overlay.tsx +1 -0
- package/src/hooks/use-auto-save.ts +3 -6
- package/src/hooks/use-contextual-tools.ts +10 -2
- package/src/hooks/use-grid-events.ts +13 -1
- package/src/hooks/use-keyboard.ts +4 -0
- package/src/store/use-editor.tsx +7 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pascal-app/editor",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.1",
|
|
4
4
|
"description": "Pascal building editor component",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -11,8 +11,8 @@
|
|
|
11
11
|
"check-types": "tsc --noEmit"
|
|
12
12
|
},
|
|
13
13
|
"peerDependencies": {
|
|
14
|
-
"@pascal-app/core": "
|
|
15
|
-
"@pascal-app/viewer": "
|
|
14
|
+
"@pascal-app/core": "^0.5.1",
|
|
15
|
+
"@pascal-app/viewer": "^0.5.1",
|
|
16
16
|
"@react-three/drei": "^10",
|
|
17
17
|
"@react-three/fiber": "^9",
|
|
18
18
|
"next": ">=15",
|
|
@@ -50,8 +50,8 @@
|
|
|
50
50
|
"zustand": "^5.0.11"
|
|
51
51
|
},
|
|
52
52
|
"devDependencies": {
|
|
53
|
-
"@pascal-app/core": "
|
|
54
|
-
"@pascal-app/viewer": "
|
|
53
|
+
"@pascal-app/core": "^0.5.1",
|
|
54
|
+
"@pascal-app/viewer": "^0.5.1",
|
|
55
55
|
"@pascal/typescript-config": "*",
|
|
56
56
|
"@types/howler": "^2.2.12",
|
|
57
57
|
"@types/react": "19.2.2",
|
|
@@ -3,10 +3,13 @@
|
|
|
3
3
|
import {
|
|
4
4
|
type AnyNode,
|
|
5
5
|
type AnyNodeId,
|
|
6
|
+
type CeilingNode,
|
|
6
7
|
DoorNode,
|
|
8
|
+
FenceNode,
|
|
7
9
|
ItemNode,
|
|
8
10
|
RoofNode,
|
|
9
11
|
RoofSegmentNode,
|
|
12
|
+
type SlabNode,
|
|
10
13
|
StairNode,
|
|
11
14
|
StairSegmentNode,
|
|
12
15
|
sceneRegistry,
|
|
@@ -31,18 +34,22 @@ const ALLOWED_TYPES = [
|
|
|
31
34
|
'stair',
|
|
32
35
|
'stair-segment',
|
|
33
36
|
'wall',
|
|
37
|
+
'fence',
|
|
34
38
|
'slab',
|
|
39
|
+
'ceiling',
|
|
35
40
|
]
|
|
36
|
-
const DELETE_ONLY_TYPES = ['wall'
|
|
41
|
+
const DELETE_ONLY_TYPES = ['wall']
|
|
42
|
+
const HOLE_TYPES = ['slab', 'ceiling']
|
|
37
43
|
|
|
38
44
|
export function FloatingActionMenu() {
|
|
39
45
|
const selectedIds = useViewer((s) => s.selection.selectedIds)
|
|
40
46
|
const nodes = useScene((s) => s.nodes)
|
|
47
|
+
const updateNode = useScene((s) => s.updateNode)
|
|
41
48
|
const mode = useEditor((s) => s.mode)
|
|
42
|
-
const setMode = useEditor((s) => s.setMode)
|
|
43
49
|
const isFloorplanHovered = useEditor((s) => s.isFloorplanHovered)
|
|
44
50
|
const setMovingNode = useEditor((s) => s.setMovingNode)
|
|
45
51
|
const setSelection = useViewer((s) => s.setSelection)
|
|
52
|
+
const setEditingHole = useEditor((s) => s.setEditingHole)
|
|
46
53
|
|
|
47
54
|
const groupRef = useRef<THREE.Group>(null)
|
|
48
55
|
|
|
@@ -61,8 +68,8 @@ export function FloatingActionMenu() {
|
|
|
61
68
|
if (!box.isEmpty()) {
|
|
62
69
|
const center = box.getCenter(new THREE.Vector3())
|
|
63
70
|
// Position above the object, with extra offset for walls/slabs to avoid covering measurement labels
|
|
64
|
-
const
|
|
65
|
-
const yOffset =
|
|
71
|
+
const isStructural = node && [...DELETE_ONLY_TYPES, ...HOLE_TYPES].includes(node.type)
|
|
72
|
+
const yOffset = isStructural ? 0.8 : 0.3
|
|
66
73
|
groupRef.current.position.set(center.x, box.max.y + yOffset, center.z)
|
|
67
74
|
}
|
|
68
75
|
}
|
|
@@ -77,6 +84,7 @@ export function FloatingActionMenu() {
|
|
|
77
84
|
node.type === 'item' ||
|
|
78
85
|
node.type === 'window' ||
|
|
79
86
|
node.type === 'door' ||
|
|
87
|
+
node.type === 'fence' ||
|
|
80
88
|
node.type === 'roof' ||
|
|
81
89
|
node.type === 'roof-segment' ||
|
|
82
90
|
node.type === 'stair' ||
|
|
@@ -108,11 +116,18 @@ export function FloatingActionMenu() {
|
|
|
108
116
|
duplicate = WindowNode.parse(duplicateInfo)
|
|
109
117
|
} else if (node.type === 'item') {
|
|
110
118
|
duplicate = ItemNode.parse(duplicateInfo)
|
|
119
|
+
} else if (node.type === 'fence') {
|
|
120
|
+
duplicate = FenceNode.parse(duplicateInfo)
|
|
121
|
+
duplicate.start = [duplicate.start[0] + 1, duplicate.start[1] + 1]
|
|
122
|
+
duplicate.end = [duplicate.end[0] + 1, duplicate.end[1] + 1]
|
|
111
123
|
} else if (node.type === 'roof') {
|
|
112
124
|
duplicate = RoofNode.parse(duplicateInfo)
|
|
113
125
|
} else if (node.type === 'roof-segment') {
|
|
114
126
|
duplicate = RoofSegmentNode.parse(duplicateInfo)
|
|
115
127
|
} else if (node.type === 'stair') {
|
|
128
|
+
duplicateInfo.children = []
|
|
129
|
+
duplicateInfo.metadata = { ...duplicateInfo.metadata }
|
|
130
|
+
delete duplicateInfo.metadata?.isNew
|
|
116
131
|
duplicate = StairNode.parse(duplicateInfo)
|
|
117
132
|
} else if (node.type === 'stair-segment') {
|
|
118
133
|
duplicate = StairSegmentNode.parse(duplicateInfo)
|
|
@@ -125,6 +140,8 @@ export function FloatingActionMenu() {
|
|
|
125
140
|
if (duplicate) {
|
|
126
141
|
if (duplicate.type === 'door' || duplicate.type === 'window') {
|
|
127
142
|
useScene.getState().createNode(duplicate, duplicate.parentId as AnyNodeId)
|
|
143
|
+
} else if (duplicate.type === 'fence') {
|
|
144
|
+
useScene.getState().createNode(duplicate, duplicate.parentId as AnyNodeId)
|
|
128
145
|
} else if (
|
|
129
146
|
duplicate.type === 'roof' ||
|
|
130
147
|
duplicate.type === 'roof-segment' ||
|
|
@@ -139,7 +156,35 @@ export function FloatingActionMenu() {
|
|
|
139
156
|
duplicate.position[2] + 1,
|
|
140
157
|
]
|
|
141
158
|
}
|
|
142
|
-
|
|
159
|
+
if (node.type === 'stair' && duplicate.type === 'stair') {
|
|
160
|
+
const nodesState = useScene.getState().nodes
|
|
161
|
+
const createOps: { node: AnyNode; parentId?: AnyNodeId }[] = [
|
|
162
|
+
{ node: duplicate, parentId: duplicate.parentId as AnyNodeId },
|
|
163
|
+
]
|
|
164
|
+
|
|
165
|
+
for (const childId of node.children ?? []) {
|
|
166
|
+
const childNode = nodesState[childId]
|
|
167
|
+
if (childNode?.type !== 'stair-segment') {
|
|
168
|
+
continue
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
let childDuplicateInfo = structuredClone(childNode) as any
|
|
172
|
+
delete childDuplicateInfo.id
|
|
173
|
+
childDuplicateInfo.metadata = { ...childDuplicateInfo.metadata }
|
|
174
|
+
delete childDuplicateInfo.metadata?.isNew
|
|
175
|
+
|
|
176
|
+
try {
|
|
177
|
+
const childDuplicate = StairSegmentNode.parse(childDuplicateInfo)
|
|
178
|
+
createOps.push({ node: childDuplicate, parentId: duplicate.id as AnyNodeId })
|
|
179
|
+
} catch (e) {
|
|
180
|
+
console.error('Failed to duplicate stair segment', e)
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
useScene.getState().createNodes(createOps)
|
|
185
|
+
} else {
|
|
186
|
+
useScene.getState().createNode(duplicate, duplicate.parentId as AnyNodeId)
|
|
187
|
+
}
|
|
143
188
|
|
|
144
189
|
// Duplicate children for roof nodes
|
|
145
190
|
if (node.type === 'roof' && node.children) {
|
|
@@ -161,49 +206,67 @@ export function FloatingActionMenu() {
|
|
|
161
206
|
}
|
|
162
207
|
|
|
163
208
|
// Duplicate children for stair nodes
|
|
164
|
-
if (node.type === 'stair' && node.children) {
|
|
165
|
-
const nodesState = useScene.getState().nodes
|
|
166
|
-
for (const childId of node.children) {
|
|
167
|
-
const childNode = nodesState[childId]
|
|
168
|
-
if (childNode && childNode.type === 'stair-segment') {
|
|
169
|
-
let childDuplicateInfo = structuredClone(childNode) as any
|
|
170
|
-
delete childDuplicateInfo.id
|
|
171
|
-
childDuplicateInfo.metadata = { ...childDuplicateInfo.metadata, isNew: true }
|
|
172
|
-
try {
|
|
173
|
-
const childDuplicate = StairSegmentNode.parse(childDuplicateInfo)
|
|
174
|
-
useScene.getState().createNode(childDuplicate, duplicate.id as AnyNodeId)
|
|
175
|
-
} catch (e) {
|
|
176
|
-
console.error('Failed to duplicate stair segment', e)
|
|
177
|
-
}
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
209
|
}
|
|
182
210
|
if (
|
|
183
211
|
duplicate.type === 'item' ||
|
|
212
|
+
duplicate.type === 'fence' ||
|
|
184
213
|
duplicate.type === 'window' ||
|
|
185
214
|
duplicate.type === 'door' ||
|
|
186
215
|
duplicate.type === 'roof' ||
|
|
187
216
|
duplicate.type === 'roof-segment' ||
|
|
188
|
-
duplicate.type === 'stair' ||
|
|
189
217
|
duplicate.type === 'stair-segment'
|
|
190
218
|
) {
|
|
191
219
|
setMovingNode(duplicate as any)
|
|
220
|
+
} else if (duplicate.type === 'stair') {
|
|
221
|
+
setSelection({ selectedIds: [duplicate.id as AnyNodeId] })
|
|
222
|
+
}
|
|
223
|
+
if (duplicate.type !== 'stair') {
|
|
224
|
+
setSelection({ selectedIds: [] })
|
|
192
225
|
}
|
|
193
|
-
setSelection({ selectedIds: [] })
|
|
194
226
|
}
|
|
195
227
|
},
|
|
196
228
|
[node, setMovingNode, setSelection],
|
|
197
229
|
)
|
|
198
230
|
|
|
231
|
+
const handleAddHole = useCallback(
|
|
232
|
+
(e: React.MouseEvent) => {
|
|
233
|
+
e.stopPropagation()
|
|
234
|
+
if (!(node && selectedId && (node.type === 'slab' || node.type === 'ceiling'))) return
|
|
235
|
+
|
|
236
|
+
const polygon = (node as SlabNode | CeilingNode).polygon
|
|
237
|
+
let cx = 0
|
|
238
|
+
let cz = 0
|
|
239
|
+
for (const [x, z] of polygon) {
|
|
240
|
+
cx += x
|
|
241
|
+
cz += z
|
|
242
|
+
}
|
|
243
|
+
cx /= polygon.length
|
|
244
|
+
cz /= polygon.length
|
|
245
|
+
|
|
246
|
+
const holeSize = 0.5
|
|
247
|
+
const newHole: Array<[number, number]> = [
|
|
248
|
+
[cx - holeSize, cz - holeSize],
|
|
249
|
+
[cx + holeSize, cz - holeSize],
|
|
250
|
+
[cx + holeSize, cz + holeSize],
|
|
251
|
+
[cx - holeSize, cz + holeSize],
|
|
252
|
+
]
|
|
253
|
+
const currentHoles = (node as SlabNode | CeilingNode).holes || []
|
|
254
|
+
updateNode(selectedId as AnyNodeId, { holes: [...currentHoles, newHole] })
|
|
255
|
+
setEditingHole({ nodeId: selectedId, holeIndex: currentHoles.length })
|
|
256
|
+
// Re-assert selection so the node stays selected
|
|
257
|
+
setSelection({ selectedIds: [selectedId] })
|
|
258
|
+
},
|
|
259
|
+
[node, selectedId, updateNode, setEditingHole, setSelection],
|
|
260
|
+
)
|
|
261
|
+
|
|
199
262
|
const handleDelete = useCallback(
|
|
200
263
|
(e: React.MouseEvent) => {
|
|
201
264
|
e.stopPropagation()
|
|
202
|
-
|
|
265
|
+
if (!selectedId) return
|
|
203
266
|
setSelection({ selectedIds: [] })
|
|
204
|
-
|
|
267
|
+
useScene.getState().deleteNode(selectedId as AnyNodeId)
|
|
205
268
|
},
|
|
206
|
-
[
|
|
269
|
+
[selectedId, setSelection],
|
|
207
270
|
)
|
|
208
271
|
|
|
209
272
|
if (!(selectedId && node && isValidType && !isFloorplanHovered && mode !== 'delete')) return null
|
|
@@ -219,9 +282,18 @@ export function FloatingActionMenu() {
|
|
|
219
282
|
zIndexRange={[100, 0]}
|
|
220
283
|
>
|
|
221
284
|
<NodeActionMenu
|
|
285
|
+
onAddHole={node && HOLE_TYPES.includes(node.type) ? handleAddHole : undefined}
|
|
222
286
|
onDelete={handleDelete}
|
|
223
|
-
onDuplicate={
|
|
224
|
-
|
|
287
|
+
onDuplicate={
|
|
288
|
+
node && !DELETE_ONLY_TYPES.includes(node.type) && !HOLE_TYPES.includes(node.type)
|
|
289
|
+
? handleDuplicate
|
|
290
|
+
: undefined
|
|
291
|
+
}
|
|
292
|
+
onMove={
|
|
293
|
+
node && !DELETE_ONLY_TYPES.includes(node.type) && !HOLE_TYPES.includes(node.type)
|
|
294
|
+
? handleMove
|
|
295
|
+
: undefined
|
|
296
|
+
}
|
|
225
297
|
onPointerDown={(e) => e.stopPropagation()}
|
|
226
298
|
onPointerUp={(e) => e.stopPropagation()}
|
|
227
299
|
/>
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { type BuildingNode, sceneRegistry, useScene } from '@pascal-app/core'
|
|
4
|
+
import { useViewer } from '@pascal-app/viewer'
|
|
5
|
+
import { Html } from '@react-three/drei'
|
|
6
|
+
import { useFrame } from '@react-three/fiber'
|
|
7
|
+
import { useCallback, useRef } from 'react'
|
|
8
|
+
import * as THREE from 'three'
|
|
9
|
+
import { sfxEmitter } from '../../lib/sfx-bus'
|
|
10
|
+
import useEditor from '../../store/use-editor'
|
|
11
|
+
import { NodeActionMenu } from './node-action-menu'
|
|
12
|
+
|
|
13
|
+
export function FloatingBuildingActionMenu() {
|
|
14
|
+
const buildingId = useViewer((s) => s.selection.buildingId)
|
|
15
|
+
const levelId = useViewer((s) => s.selection.levelId)
|
|
16
|
+
const setMovingNode = useEditor((s) => s.setMovingNode)
|
|
17
|
+
const setSelection = useViewer((s) => s.setSelection)
|
|
18
|
+
const nodes = useScene((s) => s.nodes)
|
|
19
|
+
|
|
20
|
+
const groupRef = useRef<THREE.Group>(null)
|
|
21
|
+
|
|
22
|
+
useFrame(() => {
|
|
23
|
+
if (!(buildingId && !levelId && groupRef.current)) return
|
|
24
|
+
|
|
25
|
+
const obj = sceneRegistry.nodes.get(buildingId)
|
|
26
|
+
if (obj) {
|
|
27
|
+
const box = new THREE.Box3().setFromObject(obj)
|
|
28
|
+
if (!box.isEmpty()) {
|
|
29
|
+
const center = box.getCenter(new THREE.Vector3())
|
|
30
|
+
groupRef.current.position.set(center.x, 1.5, center.z)
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
const handleMove = useCallback(
|
|
36
|
+
(e: React.MouseEvent) => {
|
|
37
|
+
e.stopPropagation()
|
|
38
|
+
if (!buildingId) return
|
|
39
|
+
const node = nodes[buildingId]
|
|
40
|
+
if (!node || node.type !== 'building') return
|
|
41
|
+
sfxEmitter.emit('sfx:item-pick')
|
|
42
|
+
setMovingNode(node as BuildingNode)
|
|
43
|
+
setSelection({ buildingId: null })
|
|
44
|
+
},
|
|
45
|
+
[buildingId, nodes, setMovingNode, setSelection],
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
// Only show when a building is selected without a level
|
|
49
|
+
if (!buildingId || levelId) return null
|
|
50
|
+
|
|
51
|
+
return (
|
|
52
|
+
<group ref={groupRef}>
|
|
53
|
+
<Html
|
|
54
|
+
center
|
|
55
|
+
style={{
|
|
56
|
+
pointerEvents: 'auto',
|
|
57
|
+
touchAction: 'none',
|
|
58
|
+
}}
|
|
59
|
+
zIndexRange={[100, 0]}
|
|
60
|
+
>
|
|
61
|
+
<NodeActionMenu
|
|
62
|
+
onMove={handleMove}
|
|
63
|
+
onPointerDown={(e) => e.stopPropagation()}
|
|
64
|
+
onPointerUp={(e) => e.stopPropagation()}
|
|
65
|
+
/>
|
|
66
|
+
</Html>
|
|
67
|
+
</group>
|
|
68
|
+
)
|
|
69
|
+
}
|
|
@@ -4593,6 +4593,12 @@ export function FloorplanPanel() {
|
|
|
4593
4593
|
levelNode?.type === 'level' && levelNode.parentId
|
|
4594
4594
|
? (levelNode.parentId as BuildingNode['id'])
|
|
4595
4595
|
: (buildingId as BuildingNode['id'] | null)
|
|
4596
|
+
const buildingRotationY = useScene((state) => {
|
|
4597
|
+
if (!currentBuildingId) return 0
|
|
4598
|
+
const node = state.nodes[currentBuildingId]
|
|
4599
|
+
return node?.type === 'building' ? (node.rotation[1] ?? 0) : 0
|
|
4600
|
+
})
|
|
4601
|
+
const buildingRotationDeg = (buildingRotationY * 180) / Math.PI
|
|
4596
4602
|
const site = useScene((state) => {
|
|
4597
4603
|
for (const rootNodeId of state.rootNodeIds) {
|
|
4598
4604
|
const node = state.nodes[rootNodeId]
|
|
@@ -5664,7 +5670,7 @@ export function FloorplanPanel() {
|
|
|
5664
5670
|
}, [fittedViewport, levelId])
|
|
5665
5671
|
|
|
5666
5672
|
useEffect(() => {
|
|
5667
|
-
if (!(phase === 'site' && levelNode?.type === 'level'
|
|
5673
|
+
if (!(phase === 'site' && levelNode?.type === 'level')) {
|
|
5668
5674
|
return
|
|
5669
5675
|
}
|
|
5670
5676
|
|
|
@@ -5963,9 +5969,14 @@ export function FloorplanPanel() {
|
|
|
5963
5969
|
return null
|
|
5964
5970
|
}
|
|
5965
5971
|
|
|
5972
|
+
if (buildingRotationY !== 0) {
|
|
5973
|
+
const [unrotX, unrotY] = rotatePlanVector(svgPoint.x, svgPoint.y, buildingRotationY)
|
|
5974
|
+
return toPlanPointFromSvgPoint({ x: unrotX, y: unrotY })
|
|
5975
|
+
}
|
|
5976
|
+
|
|
5966
5977
|
return toPlanPointFromSvgPoint(svgPoint)
|
|
5967
5978
|
},
|
|
5968
|
-
[getSvgPointFromClientPoint],
|
|
5979
|
+
[getSvgPointFromClientPoint, buildingRotationY],
|
|
5969
5980
|
)
|
|
5970
5981
|
useEffect(() => {
|
|
5971
5982
|
siteBoundaryDraftRef.current = siteBoundaryDraft
|
|
@@ -6973,6 +6984,7 @@ export function FloorplanPanel() {
|
|
|
6973
6984
|
emitter.emit(`grid:${eventType}` as any, {
|
|
6974
6985
|
nativeEvent: nativeEvent.nativeEvent as any,
|
|
6975
6986
|
position: [snappedPoint[0], worldY, snappedPoint[1]],
|
|
6987
|
+
localPosition: [snappedPoint[0], worldY, snappedPoint[1]],
|
|
6976
6988
|
})
|
|
6977
6989
|
|
|
6978
6990
|
return snappedPoint
|
|
@@ -8051,6 +8063,7 @@ export function FloorplanPanel() {
|
|
|
8051
8063
|
...(typeof cloned.metadata === 'object' && cloned.metadata !== null ? cloned.metadata : {}),
|
|
8052
8064
|
isNew: true,
|
|
8053
8065
|
}
|
|
8066
|
+
cloned.children = []
|
|
8054
8067
|
|
|
8055
8068
|
try {
|
|
8056
8069
|
const duplicate = ItemNodeSchema.parse(cloned)
|
|
@@ -8179,8 +8192,8 @@ export function FloorplanPanel() {
|
|
|
8179
8192
|
delete cloned.id
|
|
8180
8193
|
cloned.metadata = {
|
|
8181
8194
|
...(typeof cloned.metadata === 'object' && cloned.metadata !== null ? cloned.metadata : {}),
|
|
8182
|
-
isNew: true,
|
|
8183
8195
|
}
|
|
8196
|
+
delete (cloned.metadata as Record<string, unknown>).isNew
|
|
8184
8197
|
|
|
8185
8198
|
const nextPosition =
|
|
8186
8199
|
Array.isArray(cloned.position) && cloned.position.length >= 3
|
|
@@ -8195,9 +8208,11 @@ export function FloorplanPanel() {
|
|
|
8195
8208
|
|
|
8196
8209
|
try {
|
|
8197
8210
|
const duplicate = StairNodeSchema.parse(cloned)
|
|
8198
|
-
useScene.getState().createNode(duplicate, stair.parentId as AnyNodeId)
|
|
8199
|
-
|
|
8200
8211
|
const nodesState = useScene.getState().nodes
|
|
8212
|
+
const createOps: { node: AnyNode; parentId?: AnyNodeId }[] = [
|
|
8213
|
+
{ node: duplicate, parentId: stair.parentId as AnyNodeId },
|
|
8214
|
+
]
|
|
8215
|
+
|
|
8201
8216
|
for (const childId of stair.children ?? []) {
|
|
8202
8217
|
const childNode = nodesState[childId]
|
|
8203
8218
|
if (childNode?.type !== 'stair-segment') {
|
|
@@ -8210,19 +8225,20 @@ export function FloorplanPanel() {
|
|
|
8210
8225
|
...(typeof childClone.metadata === 'object' && childClone.metadata !== null
|
|
8211
8226
|
? childClone.metadata
|
|
8212
8227
|
: {}),
|
|
8213
|
-
isNew: true,
|
|
8214
8228
|
}
|
|
8229
|
+
delete (childClone.metadata as Record<string, unknown>).isNew
|
|
8215
8230
|
|
|
8216
8231
|
const childDuplicate = StairSegmentNodeSchema.parse(childClone)
|
|
8217
|
-
|
|
8232
|
+
createOps.push({ node: childDuplicate, parentId: duplicate.id as AnyNodeId })
|
|
8218
8233
|
}
|
|
8219
8234
|
|
|
8220
|
-
|
|
8221
|
-
|
|
8235
|
+
useScene.getState().createNodes(createOps)
|
|
8236
|
+
|
|
8237
|
+
setSelection({ selectedIds: [duplicate.id as AnyNodeId] })
|
|
8222
8238
|
} catch (error) {
|
|
8223
8239
|
console.error('Failed to duplicate stair', error)
|
|
8224
8240
|
}
|
|
8225
|
-
}, [selectedStairEntry,
|
|
8241
|
+
}, [selectedStairEntry, setSelection])
|
|
8226
8242
|
const handleSelectedStairDuplicate = useCallback(
|
|
8227
8243
|
(event: ReactMouseEvent<HTMLButtonElement>) => {
|
|
8228
8244
|
event.stopPropagation()
|
|
@@ -9296,9 +9312,10 @@ export function FloorplanPanel() {
|
|
|
9296
9312
|
y={viewBox.minY}
|
|
9297
9313
|
/>
|
|
9298
9314
|
|
|
9299
|
-
<
|
|
9300
|
-
|
|
9301
|
-
|
|
9315
|
+
<g transform={buildingRotationDeg !== 0 ? `rotate(${buildingRotationDeg})` : undefined}>
|
|
9316
|
+
<FloorplanGridLayer
|
|
9317
|
+
majorGridPath={majorGridPath}
|
|
9318
|
+
minorGridPath={minorGridPath}
|
|
9302
9319
|
palette={palette}
|
|
9303
9320
|
showGrid={showGrid}
|
|
9304
9321
|
/>
|
|
@@ -9601,6 +9618,7 @@ export function FloorplanPanel() {
|
|
|
9601
9618
|
vectorEffect="non-scaling-stroke"
|
|
9602
9619
|
/>
|
|
9603
9620
|
)}
|
|
9621
|
+
</g>
|
|
9604
9622
|
</svg>
|
|
9605
9623
|
)}
|
|
9606
9624
|
</div>
|