react-three-game 0.0.18 → 0.0.20
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/.github/copilot-instructions.md +54 -183
- package/README.md +69 -214
- package/dist/index.d.ts +3 -1
- package/dist/index.js +3 -0
- package/dist/tools/assetviewer/page.js +24 -13
- package/dist/tools/prefabeditor/EditorTree.d.ts +2 -4
- package/dist/tools/prefabeditor/EditorTree.js +20 -194
- package/dist/tools/prefabeditor/EditorUI.js +43 -224
- package/dist/tools/prefabeditor/PrefabEditor.js +33 -99
- package/dist/tools/prefabeditor/PrefabRoot.d.ts +0 -1
- package/dist/tools/prefabeditor/PrefabRoot.js +7 -23
- package/dist/tools/prefabeditor/components/DirectionalLightComponent.js +31 -43
- package/dist/tools/prefabeditor/styles.d.ts +1809 -0
- package/dist/tools/prefabeditor/styles.js +168 -0
- package/dist/tools/prefabeditor/types.d.ts +3 -14
- package/dist/tools/prefabeditor/types.js +0 -1
- package/dist/tools/prefabeditor/utils.d.ts +19 -0
- package/dist/tools/prefabeditor/utils.js +72 -0
- package/package.json +8 -3
- package/src/index.ts +5 -1
- package/src/tools/assetviewer/page.tsx +22 -12
- package/src/tools/prefabeditor/EditorTree.tsx +38 -270
- package/src/tools/prefabeditor/EditorUI.tsx +105 -322
- package/src/tools/prefabeditor/PrefabEditor.tsx +40 -151
- package/src/tools/prefabeditor/PrefabRoot.tsx +11 -32
- package/src/tools/prefabeditor/components/DirectionalLightComponent.tsx +38 -53
- package/src/tools/prefabeditor/styles.ts +195 -0
- package/src/tools/prefabeditor/types.ts +4 -12
- package/src/tools/prefabeditor/utils.ts +80 -0
|
@@ -1,119 +1,19 @@
|
|
|
1
1
|
import { Dispatch, SetStateAction, useState, MouseEvent } from 'react';
|
|
2
2
|
import { Prefab, GameObject } from "./types";
|
|
3
3
|
import { getComponent } from './components/ComponentRegistry';
|
|
4
|
+
import { base, tree, menu } from './styles';
|
|
5
|
+
import { findNode, findParent, deleteNode, cloneNode } from './utils';
|
|
4
6
|
|
|
5
|
-
|
|
7
|
+
export default function EditorTree({ prefabData, setPrefabData, selectedId, setSelectedId }: {
|
|
6
8
|
prefabData?: Prefab;
|
|
7
9
|
setPrefabData?: Dispatch<SetStateAction<Prefab>>;
|
|
8
10
|
selectedId: string | null;
|
|
9
11
|
setSelectedId: Dispatch<SetStateAction<string | null>>;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
export default function EditorTree({ prefabData, setPrefabData, selectedId, setSelectedId }: EditorTreeProps) {
|
|
12
|
+
}) {
|
|
13
13
|
const [contextMenu, setContextMenu] = useState<{ x: number, y: number, nodeId: string } | null>(null);
|
|
14
14
|
const [draggedId, setDraggedId] = useState<string | null>(null);
|
|
15
15
|
const [collapsedIds, setCollapsedIds] = useState<Set<string>>(new Set());
|
|
16
|
-
const [
|
|
17
|
-
|
|
18
|
-
const styles: Record<string, React.CSSProperties> = {
|
|
19
|
-
panel: {
|
|
20
|
-
background: "rgba(0,0,0,0.55)",
|
|
21
|
-
color: "rgba(255,255,255,0.9)",
|
|
22
|
-
border: "1px solid rgba(255,255,255,0.12)",
|
|
23
|
-
borderRadius: 6,
|
|
24
|
-
overflow: "hidden",
|
|
25
|
-
maxHeight: "85vh",
|
|
26
|
-
display: "flex",
|
|
27
|
-
flexDirection: "column",
|
|
28
|
-
backdropFilter: "blur(6px)",
|
|
29
|
-
WebkitBackdropFilter: "blur(6px)",
|
|
30
|
-
fontFamily: "ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace",
|
|
31
|
-
fontSize: 11,
|
|
32
|
-
lineHeight: 1.2,
|
|
33
|
-
userSelect: "none",
|
|
34
|
-
WebkitUserSelect: "none",
|
|
35
|
-
},
|
|
36
|
-
panelHeader: {
|
|
37
|
-
padding: "4px 6px",
|
|
38
|
-
borderBottom: "1px solid rgba(255,255,255,0.10)",
|
|
39
|
-
display: "flex",
|
|
40
|
-
gap: 8,
|
|
41
|
-
alignItems: "center",
|
|
42
|
-
justifyContent: "space-between",
|
|
43
|
-
cursor: "pointer",
|
|
44
|
-
background: "rgba(255,255,255,0.05)",
|
|
45
|
-
textTransform: "uppercase",
|
|
46
|
-
letterSpacing: "0.08em",
|
|
47
|
-
fontSize: 10,
|
|
48
|
-
color: "rgba(255,255,255,0.7)",
|
|
49
|
-
},
|
|
50
|
-
scroll: {
|
|
51
|
-
overflowY: "auto",
|
|
52
|
-
},
|
|
53
|
-
row: {
|
|
54
|
-
display: "flex",
|
|
55
|
-
alignItems: "center",
|
|
56
|
-
padding: "2px 6px",
|
|
57
|
-
borderBottom: "1px solid rgba(255,255,255,0.07)",
|
|
58
|
-
cursor: "pointer",
|
|
59
|
-
whiteSpace: "nowrap",
|
|
60
|
-
},
|
|
61
|
-
rowSelected: {
|
|
62
|
-
background: "rgba(255,255,255,0.10)",
|
|
63
|
-
},
|
|
64
|
-
chevron: {
|
|
65
|
-
width: 12,
|
|
66
|
-
textAlign: "center",
|
|
67
|
-
opacity: 0.55,
|
|
68
|
-
fontSize: 10,
|
|
69
|
-
marginRight: 4,
|
|
70
|
-
cursor: "pointer",
|
|
71
|
-
},
|
|
72
|
-
idText: {
|
|
73
|
-
fontSize: 11,
|
|
74
|
-
overflow: "hidden",
|
|
75
|
-
textOverflow: "ellipsis",
|
|
76
|
-
},
|
|
77
|
-
dragHandle: {
|
|
78
|
-
width: 14,
|
|
79
|
-
height: 14,
|
|
80
|
-
display: "flex",
|
|
81
|
-
alignItems: "center",
|
|
82
|
-
justifyContent: "center",
|
|
83
|
-
marginRight: 4,
|
|
84
|
-
opacity: 0.4,
|
|
85
|
-
cursor: "grab",
|
|
86
|
-
fontSize: 10,
|
|
87
|
-
},
|
|
88
|
-
contextMenu: {
|
|
89
|
-
position: "fixed",
|
|
90
|
-
zIndex: 50,
|
|
91
|
-
minWidth: 120,
|
|
92
|
-
background: "rgba(0,0,0,0.82)",
|
|
93
|
-
border: "1px solid rgba(255,255,255,0.16)",
|
|
94
|
-
borderRadius: 6,
|
|
95
|
-
overflow: "hidden",
|
|
96
|
-
boxShadow: "0 12px 32px rgba(0,0,0,0.45)",
|
|
97
|
-
backdropFilter: "blur(6px)",
|
|
98
|
-
WebkitBackdropFilter: "blur(6px)",
|
|
99
|
-
},
|
|
100
|
-
menuItem: {
|
|
101
|
-
width: "100%",
|
|
102
|
-
textAlign: "left",
|
|
103
|
-
padding: "6px 8px",
|
|
104
|
-
background: "transparent",
|
|
105
|
-
border: "none",
|
|
106
|
-
color: "rgba(255,255,255,0.9)",
|
|
107
|
-
font: "inherit",
|
|
108
|
-
cursor: "pointer",
|
|
109
|
-
},
|
|
110
|
-
menuItemDanger: {
|
|
111
|
-
color: "rgba(255,120,120,0.95)",
|
|
112
|
-
},
|
|
113
|
-
menuDivider: {
|
|
114
|
-
borderTop: "1px solid rgba(255,255,255,0.10)",
|
|
115
|
-
}
|
|
116
|
-
};
|
|
16
|
+
const [collapsed, setCollapsed] = useState(false);
|
|
117
17
|
|
|
118
18
|
if (!prefabData || !setPrefabData) return null;
|
|
119
19
|
|
|
@@ -123,14 +23,11 @@ export default function EditorTree({ prefabData, setPrefabData, selectedId, setS
|
|
|
123
23
|
setContextMenu({ x: e.clientX, y: e.clientY, nodeId });
|
|
124
24
|
};
|
|
125
25
|
|
|
126
|
-
const closeContextMenu = () => setContextMenu(null);
|
|
127
|
-
|
|
128
26
|
const toggleCollapse = (e: MouseEvent, id: string) => {
|
|
129
27
|
e.stopPropagation();
|
|
130
28
|
setCollapsedIds(prev => {
|
|
131
29
|
const next = new Set(prev);
|
|
132
|
-
|
|
133
|
-
else next.add(id);
|
|
30
|
+
next.has(id) ? next.delete(id) : next.add(id);
|
|
134
31
|
return next;
|
|
135
32
|
});
|
|
136
33
|
};
|
|
@@ -148,7 +45,7 @@ export default function EditorTree({ prefabData, setPrefabData, selectedId, setS
|
|
|
148
45
|
};
|
|
149
46
|
|
|
150
47
|
setPrefabData(prev => {
|
|
151
|
-
const newRoot = JSON.parse(JSON.stringify(prev.root));
|
|
48
|
+
const newRoot = JSON.parse(JSON.stringify(prev.root));
|
|
152
49
|
const parent = findNode(newRoot, parentId);
|
|
153
50
|
if (parent) {
|
|
154
51
|
parent.children = parent.children || [];
|
|
@@ -156,11 +53,11 @@ export default function EditorTree({ prefabData, setPrefabData, selectedId, setS
|
|
|
156
53
|
}
|
|
157
54
|
return { ...prev, root: newRoot };
|
|
158
55
|
});
|
|
159
|
-
|
|
56
|
+
setContextMenu(null);
|
|
160
57
|
};
|
|
161
58
|
|
|
162
59
|
const handleDuplicate = (nodeId: string) => {
|
|
163
|
-
if (nodeId === prefabData.root.id) return;
|
|
60
|
+
if (nodeId === prefabData.root.id) return;
|
|
164
61
|
|
|
165
62
|
setPrefabData(prev => {
|
|
166
63
|
const newRoot = JSON.parse(JSON.stringify(prev.root));
|
|
@@ -174,18 +71,15 @@ export default function EditorTree({ prefabData, setPrefabData, selectedId, setS
|
|
|
174
71
|
}
|
|
175
72
|
return { ...prev, root: newRoot };
|
|
176
73
|
});
|
|
177
|
-
|
|
74
|
+
setContextMenu(null);
|
|
178
75
|
};
|
|
179
76
|
|
|
180
77
|
const handleDelete = (nodeId: string) => {
|
|
181
|
-
if (nodeId === prefabData.root.id) return;
|
|
78
|
+
if (nodeId === prefabData.root.id) return;
|
|
182
79
|
|
|
183
|
-
setPrefabData(prev => {
|
|
184
|
-
const newRoot = deleteNodeFromTree(JSON.parse(JSON.stringify(prev.root)), nodeId);
|
|
185
|
-
return { ...prev, root: newRoot! };
|
|
186
|
-
});
|
|
80
|
+
setPrefabData(prev => ({ ...prev, root: deleteNode(prev.root, nodeId)! }));
|
|
187
81
|
if (selectedId === nodeId) setSelectedId(null);
|
|
188
|
-
|
|
82
|
+
setContextMenu(null);
|
|
189
83
|
};
|
|
190
84
|
|
|
191
85
|
// Drag and Drop
|
|
@@ -195,26 +89,18 @@ export default function EditorTree({ prefabData, setPrefabData, selectedId, setS
|
|
|
195
89
|
return;
|
|
196
90
|
}
|
|
197
91
|
e.dataTransfer.effectAllowed = "move";
|
|
198
|
-
e.dataTransfer.setData("text/plain", id);
|
|
199
92
|
setDraggedId(id);
|
|
200
93
|
};
|
|
201
94
|
|
|
202
|
-
const handleDragEnd = () => {
|
|
203
|
-
setDraggedId(null);
|
|
204
|
-
};
|
|
205
|
-
|
|
206
95
|
const handleDragOver = (e: React.DragEvent, targetId: string) => {
|
|
207
96
|
if (!draggedId || draggedId === targetId) return;
|
|
208
97
|
const draggedNode = findNode(prefabData.root, draggedId);
|
|
209
98
|
if (draggedNode && findNode(draggedNode, targetId)) return;
|
|
210
|
-
|
|
211
99
|
e.preventDefault();
|
|
212
|
-
e.dataTransfer.dropEffect = "move";
|
|
213
100
|
};
|
|
214
101
|
|
|
215
102
|
const handleDrop = (e: React.DragEvent, targetId: string) => {
|
|
216
103
|
if (!draggedId || draggedId === targetId) return;
|
|
217
|
-
|
|
218
104
|
e.preventDefault();
|
|
219
105
|
|
|
220
106
|
setPrefabData(prev => {
|
|
@@ -241,152 +127,75 @@ export default function EditorTree({ prefabData, setPrefabData, selectedId, setS
|
|
|
241
127
|
setDraggedId(null);
|
|
242
128
|
};
|
|
243
129
|
|
|
244
|
-
const renderNode = (node: GameObject, depth
|
|
130
|
+
const renderNode = (node: GameObject, depth = 0): React.ReactNode => {
|
|
245
131
|
if (!node) return null;
|
|
246
132
|
|
|
247
133
|
const isSelected = node.id === selectedId;
|
|
248
134
|
const isCollapsed = collapsedIds.has(node.id);
|
|
249
135
|
const hasChildren = node.children && node.children.length > 0;
|
|
136
|
+
const isRoot = node.id === prefabData.root.id;
|
|
250
137
|
|
|
251
138
|
return (
|
|
252
139
|
<div key={node.id}>
|
|
253
140
|
<div
|
|
254
141
|
style={{
|
|
255
|
-
...
|
|
256
|
-
...(isSelected ?
|
|
257
|
-
paddingLeft: `${depth *
|
|
258
|
-
cursor: node.id !== prefabData.root.id ? "grab" : "pointer",
|
|
142
|
+
...tree.row,
|
|
143
|
+
...(isSelected ? tree.selected : {}),
|
|
144
|
+
paddingLeft: `${depth * 12 + 6}px`,
|
|
259
145
|
}}
|
|
260
|
-
draggable={
|
|
146
|
+
draggable={!isRoot}
|
|
261
147
|
onClick={(e) => { e.stopPropagation(); setSelectedId(node.id); }}
|
|
262
148
|
onContextMenu={(e) => handleContextMenu(e, node.id)}
|
|
263
149
|
onDragStart={(e) => handleDragStart(e, node.id)}
|
|
264
|
-
onDragEnd={
|
|
150
|
+
onDragEnd={() => setDraggedId(null)}
|
|
265
151
|
onDragOver={(e) => handleDragOver(e, node.id)}
|
|
266
152
|
onDrop={(e) => handleDrop(e, node.id)}
|
|
267
|
-
onPointerEnter={(e) => {
|
|
268
|
-
if (!isSelected) (e.currentTarget as HTMLDivElement).style.background = "rgba(255,255,255,0.06)";
|
|
269
|
-
}}
|
|
270
|
-
onPointerLeave={(e) => {
|
|
271
|
-
if (!isSelected) (e.currentTarget as HTMLDivElement).style.background = "transparent";
|
|
272
|
-
}}
|
|
273
153
|
>
|
|
274
154
|
<span
|
|
275
155
|
style={{
|
|
276
|
-
|
|
277
|
-
|
|
156
|
+
width: 12,
|
|
157
|
+
opacity: 0.6,
|
|
158
|
+
marginRight: 4,
|
|
159
|
+
cursor: 'pointer',
|
|
160
|
+
visibility: hasChildren ? 'visible' : 'hidden'
|
|
278
161
|
}}
|
|
279
162
|
onClick={(e) => hasChildren && toggleCollapse(e, node.id)}
|
|
280
|
-
onPointerEnter={(e) => {
|
|
281
|
-
(e.currentTarget as HTMLSpanElement).style.opacity = "0.9";
|
|
282
|
-
}}
|
|
283
|
-
onPointerLeave={(e) => {
|
|
284
|
-
(e.currentTarget as HTMLSpanElement).style.opacity = "0.55";
|
|
285
|
-
}}
|
|
286
163
|
>
|
|
287
164
|
{isCollapsed ? '▶' : '▼'}
|
|
288
165
|
</span>
|
|
289
|
-
{
|
|
290
|
-
|
|
291
|
-
style={styles.dragHandle}
|
|
292
|
-
onPointerEnter={(e) => {
|
|
293
|
-
(e.currentTarget as HTMLSpanElement).style.opacity = "0.9";
|
|
294
|
-
}}
|
|
295
|
-
onPointerLeave={(e) => {
|
|
296
|
-
(e.currentTarget as HTMLSpanElement).style.opacity = "0.4";
|
|
297
|
-
}}
|
|
298
|
-
>
|
|
299
|
-
⋮⋮
|
|
300
|
-
</span>
|
|
301
|
-
)}
|
|
302
|
-
<span style={styles.idText}>
|
|
303
|
-
{node.id}
|
|
304
|
-
</span>
|
|
166
|
+
{!isRoot && <span style={{ marginRight: 4, opacity: 0.4 }}>⋮⋮</span>}
|
|
167
|
+
<span style={{ overflow: 'hidden', textOverflow: 'ellipsis' }}>{node.id}</span>
|
|
305
168
|
</div>
|
|
306
|
-
{!isCollapsed && node.children && (
|
|
307
|
-
<div>
|
|
308
|
-
{node.children.map(child => renderNode(child, depth + 1))}
|
|
309
|
-
</div>
|
|
310
|
-
)}
|
|
169
|
+
{!isCollapsed && node.children && node.children.map(child => renderNode(child, depth + 1))}
|
|
311
170
|
</div>
|
|
312
171
|
);
|
|
313
172
|
};
|
|
314
173
|
|
|
315
174
|
return (
|
|
316
175
|
<>
|
|
317
|
-
<div
|
|
318
|
-
style={{
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
}}
|
|
322
|
-
onClick={closeContextMenu}
|
|
323
|
-
>
|
|
324
|
-
<div
|
|
325
|
-
style={styles.panelHeader}
|
|
326
|
-
onClick={(e) => { e.stopPropagation(); setIsTreeCollapsed(!isTreeCollapsed); }}
|
|
327
|
-
onPointerEnter={(e) => {
|
|
328
|
-
(e.currentTarget as HTMLDivElement).style.background = "rgba(255,255,255,0.08)";
|
|
329
|
-
}}
|
|
330
|
-
onPointerLeave={(e) => {
|
|
331
|
-
(e.currentTarget as HTMLDivElement).style.background = "rgba(255,255,255,0.05)";
|
|
332
|
-
}}
|
|
333
|
-
>
|
|
334
|
-
<span>Prefab Graph</span>
|
|
335
|
-
<span style={{ fontSize: 10, opacity: 0.8 }}>{isTreeCollapsed ? '▶' : '◀'}</span>
|
|
176
|
+
<div style={{ ...tree.panel, width: collapsed ? 'auto' : 224 }} onClick={() => setContextMenu(null)}>
|
|
177
|
+
<div style={base.header} onClick={() => setCollapsed(!collapsed)}>
|
|
178
|
+
<span>Scene</span>
|
|
179
|
+
<span>{collapsed ? '▶' : '◀'}</span>
|
|
336
180
|
</div>
|
|
337
|
-
{!
|
|
338
|
-
<div style={{ ...styles.scroll, padding: 2 }}>
|
|
339
|
-
{renderNode(prefabData.root)}
|
|
340
|
-
</div>
|
|
341
|
-
)}
|
|
181
|
+
{!collapsed && <div style={tree.scroll}>{renderNode(prefabData.root)}</div>}
|
|
342
182
|
</div>
|
|
343
183
|
|
|
344
184
|
{contextMenu && (
|
|
345
185
|
<div
|
|
346
|
-
style={{
|
|
347
|
-
...styles.contextMenu,
|
|
348
|
-
top: contextMenu.y,
|
|
349
|
-
left: contextMenu.x,
|
|
350
|
-
}}
|
|
186
|
+
style={{ ...menu.container, top: contextMenu.y, left: contextMenu.x }}
|
|
351
187
|
onClick={(e) => e.stopPropagation()}
|
|
352
|
-
onPointerLeave={
|
|
188
|
+
onPointerLeave={() => setContextMenu(null)}
|
|
353
189
|
>
|
|
354
|
-
<button
|
|
355
|
-
style={{ ...styles.menuItem, ...styles.menuDivider }}
|
|
356
|
-
onClick={() => handleAddChild(contextMenu.nodeId)}
|
|
357
|
-
onPointerEnter={(e) => {
|
|
358
|
-
(e.currentTarget as HTMLButtonElement).style.background = "rgba(255,255,255,0.08)";
|
|
359
|
-
}}
|
|
360
|
-
onPointerLeave={(e) => {
|
|
361
|
-
(e.currentTarget as HTMLButtonElement).style.background = "transparent";
|
|
362
|
-
}}
|
|
363
|
-
>
|
|
190
|
+
<button style={menu.item} onClick={() => handleAddChild(contextMenu.nodeId)}>
|
|
364
191
|
Add Child
|
|
365
192
|
</button>
|
|
366
193
|
{contextMenu.nodeId !== prefabData.root.id && (
|
|
367
194
|
<>
|
|
368
|
-
<button
|
|
369
|
-
style={{ ...styles.menuItem, ...styles.menuDivider }}
|
|
370
|
-
onClick={() => handleDuplicate(contextMenu.nodeId)}
|
|
371
|
-
onPointerEnter={(e) => {
|
|
372
|
-
(e.currentTarget as HTMLButtonElement).style.background = "rgba(255,255,255,0.08)";
|
|
373
|
-
}}
|
|
374
|
-
onPointerLeave={(e) => {
|
|
375
|
-
(e.currentTarget as HTMLButtonElement).style.background = "transparent";
|
|
376
|
-
}}
|
|
377
|
-
>
|
|
195
|
+
<button style={menu.item} onClick={() => handleDuplicate(contextMenu.nodeId)}>
|
|
378
196
|
Duplicate
|
|
379
197
|
</button>
|
|
380
|
-
<button
|
|
381
|
-
style={{ ...styles.menuItem, ...styles.menuItemDanger }}
|
|
382
|
-
onClick={() => handleDelete(contextMenu.nodeId)}
|
|
383
|
-
onPointerEnter={(e) => {
|
|
384
|
-
(e.currentTarget as HTMLButtonElement).style.background = "rgba(255,255,255,0.08)";
|
|
385
|
-
}}
|
|
386
|
-
onPointerLeave={(e) => {
|
|
387
|
-
(e.currentTarget as HTMLButtonElement).style.background = "transparent";
|
|
388
|
-
}}
|
|
389
|
-
>
|
|
198
|
+
<button style={{ ...menu.item, ...menu.danger }} onClick={() => handleDelete(contextMenu.nodeId)}>
|
|
390
199
|
Delete
|
|
391
200
|
</button>
|
|
392
201
|
</>
|
|
@@ -396,44 +205,3 @@ export default function EditorTree({ prefabData, setPrefabData, selectedId, setS
|
|
|
396
205
|
</>
|
|
397
206
|
);
|
|
398
207
|
}
|
|
399
|
-
|
|
400
|
-
// --- Helpers ---
|
|
401
|
-
|
|
402
|
-
function findNode(root: GameObject, id: string): GameObject | null {
|
|
403
|
-
if (root.id === id) return root;
|
|
404
|
-
if (root.children) {
|
|
405
|
-
for (const child of root.children) {
|
|
406
|
-
const found = findNode(child, id);
|
|
407
|
-
if (found) return found;
|
|
408
|
-
}
|
|
409
|
-
}
|
|
410
|
-
return null;
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
function findParent(root: GameObject, id: string): GameObject | null {
|
|
414
|
-
if (!root.children) return null;
|
|
415
|
-
for (const child of root.children) {
|
|
416
|
-
if (child.id === id) return root;
|
|
417
|
-
const found = findParent(child, id);
|
|
418
|
-
if (found) return found;
|
|
419
|
-
}
|
|
420
|
-
return null;
|
|
421
|
-
}
|
|
422
|
-
|
|
423
|
-
function deleteNodeFromTree(root: GameObject, id: string): GameObject | null {
|
|
424
|
-
if (root.id === id) return null;
|
|
425
|
-
if (root.children) {
|
|
426
|
-
root.children = root.children
|
|
427
|
-
.map(child => deleteNodeFromTree(child, id))
|
|
428
|
-
.filter((child): child is GameObject => child !== null);
|
|
429
|
-
}
|
|
430
|
-
return root;
|
|
431
|
-
}
|
|
432
|
-
|
|
433
|
-
function cloneNode(node: GameObject): GameObject {
|
|
434
|
-
const newNode = { ...node, id: crypto.randomUUID() };
|
|
435
|
-
if (newNode.children) {
|
|
436
|
-
newNode.children = newNode.children.map(child => cloneNode(child));
|
|
437
|
-
}
|
|
438
|
-
return newNode;
|
|
439
|
-
}
|