react-three-game 0.0.56 → 0.0.58
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/README.md +16 -3
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/shared/GameCanvas.js +1 -3
- package/dist/tools/assetviewer/page.js +35 -14
- package/dist/tools/prefabeditor/Dropdown.d.ts +15 -0
- package/dist/tools/prefabeditor/Dropdown.js +82 -0
- package/dist/tools/prefabeditor/EditorContext.d.ts +5 -0
- package/dist/tools/prefabeditor/EditorTree.js +149 -91
- package/dist/tools/prefabeditor/EditorTreeMenus.d.ts +33 -0
- package/dist/tools/prefabeditor/EditorTreeMenus.js +136 -0
- package/dist/tools/prefabeditor/EditorUI.js +1 -1
- package/dist/tools/prefabeditor/PrefabEditor.d.ts +1 -0
- package/dist/tools/prefabeditor/PrefabEditor.js +13 -2
- package/dist/tools/prefabeditor/PrefabRoot.d.ts +1 -0
- package/dist/tools/prefabeditor/PrefabRoot.js +120 -34
- package/dist/tools/prefabeditor/components/AmbientLightComponent.js +3 -7
- package/dist/tools/prefabeditor/components/CameraComponent.d.ts +3 -0
- package/dist/tools/prefabeditor/components/CameraComponent.js +45 -0
- package/dist/tools/prefabeditor/components/DirectionalLightComponent.js +50 -24
- package/dist/tools/prefabeditor/components/EnvironmentComponent.d.ts +3 -0
- package/dist/tools/prefabeditor/components/EnvironmentComponent.js +15 -0
- package/dist/tools/prefabeditor/components/GeometryComponent.js +46 -46
- package/dist/tools/prefabeditor/components/Input.d.ts +51 -1
- package/dist/tools/prefabeditor/components/Input.js +73 -21
- package/dist/tools/prefabeditor/components/MaterialComponent.d.ts +16 -2
- package/dist/tools/prefabeditor/components/MaterialComponent.js +129 -15
- package/dist/tools/prefabeditor/components/ModelComponent.js +44 -3
- package/dist/tools/prefabeditor/components/PhysicsComponent.js +16 -81
- package/dist/tools/prefabeditor/components/SpotLightComponent.js +36 -23
- package/dist/tools/prefabeditor/components/TextComponent.js +7 -53
- package/dist/tools/prefabeditor/components/TransformComponent.js +18 -8
- package/dist/tools/prefabeditor/components/index.js +5 -1
- package/dist/tools/prefabeditor/styles.d.ts +5 -2
- package/dist/tools/prefabeditor/styles.js +7 -3
- package/dist/tools/prefabeditor/utils.d.ts +4 -3
- package/dist/tools/prefabeditor/utils.js +53 -5
- package/package.json +1 -1
- package/react-three-game-skill/react-three-game/SKILL.md +4 -1
- package/src/index.ts +7 -0
- package/src/shared/GameCanvas.tsx +0 -3
- package/src/tools/assetviewer/page.tsx +77 -45
- package/src/tools/prefabeditor/Dropdown.tsx +112 -0
- package/src/tools/prefabeditor/EditorContext.tsx +5 -0
- package/src/tools/prefabeditor/EditorTree.tsx +242 -178
- package/src/tools/prefabeditor/EditorTreeMenus.tsx +307 -0
- package/src/tools/prefabeditor/EditorUI.tsx +1 -1
- package/src/tools/prefabeditor/PrefabEditor.tsx +17 -4
- package/src/tools/prefabeditor/PrefabRoot.tsx +208 -58
- package/src/tools/prefabeditor/components/AmbientLightComponent.tsx +5 -11
- package/src/tools/prefabeditor/components/CameraComponent.tsx +117 -0
- package/src/tools/prefabeditor/components/DirectionalLightComponent.tsx +61 -30
- package/src/tools/prefabeditor/components/EnvironmentComponent.tsx +47 -0
- package/src/tools/prefabeditor/components/GeometryComponent.tsx +69 -63
- package/src/tools/prefabeditor/components/Input.tsx +220 -27
- package/src/tools/prefabeditor/components/MaterialComponent.tsx +189 -18
- package/src/tools/prefabeditor/components/ModelComponent.tsx +51 -4
- package/src/tools/prefabeditor/components/PhysicsComponent.tsx +44 -85
- package/src/tools/prefabeditor/components/SpotLightComponent.tsx +52 -27
- package/src/tools/prefabeditor/components/TextComponent.tsx +58 -57
- package/src/tools/prefabeditor/components/TransformComponent.tsx +61 -9
- package/src/tools/prefabeditor/components/index.ts +5 -1
- package/src/tools/prefabeditor/styles.ts +7 -3
- package/src/tools/prefabeditor/utils.ts +55 -4
|
@@ -1,9 +1,80 @@
|
|
|
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, colors, tree
|
|
5
|
-
import { findNode, findParent, deleteNode, cloneNode, updateNodeById
|
|
4
|
+
import { base, colors, tree } from './styles';
|
|
5
|
+
import { findNode, findParent, deleteNode, cloneNode, updateNodeById } from './utils';
|
|
6
6
|
import { useEditorContext } from './EditorContext';
|
|
7
|
+
import { Dropdown } from './Dropdown';
|
|
8
|
+
import { FileMenu, MenuTriggerButton, TreeContextMenu, TreeContextMenuState, TreeNodeMenu } from './EditorTreeMenus';
|
|
9
|
+
|
|
10
|
+
type DropPosition = 'before' | 'inside';
|
|
11
|
+
|
|
12
|
+
function moveNode(root: GameObject, draggedId: string, targetId: string, position: DropPosition): GameObject {
|
|
13
|
+
const draggedNode = findNode(root, draggedId);
|
|
14
|
+
const oldParent = findParent(root, draggedId);
|
|
15
|
+
|
|
16
|
+
if (!draggedNode || !oldParent || findNode(draggedNode, targetId)) {
|
|
17
|
+
return root;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
if (position === 'before') {
|
|
21
|
+
const targetParent = findParent(root, targetId);
|
|
22
|
+
if (!targetParent?.children) return root;
|
|
23
|
+
|
|
24
|
+
if (targetParent.id === oldParent.id) {
|
|
25
|
+
const siblings = targetParent.children.filter(child => child.id !== draggedId);
|
|
26
|
+
const targetIndex = siblings.findIndex(child => child.id === targetId);
|
|
27
|
+
if (targetIndex === -1) return root;
|
|
28
|
+
|
|
29
|
+
siblings.splice(targetIndex, 0, draggedNode);
|
|
30
|
+
return updateNodeById(root, targetParent.id, parent => ({ ...parent, children: siblings }));
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const rootWithoutDragged = updateNodeById(root, oldParent.id, parent => ({
|
|
34
|
+
...parent,
|
|
35
|
+
children: (parent.children ?? []).filter(child => child.id !== draggedId)
|
|
36
|
+
}));
|
|
37
|
+
|
|
38
|
+
return updateNodeById(rootWithoutDragged, targetParent.id, parent => {
|
|
39
|
+
const children = [...(parent.children ?? [])];
|
|
40
|
+
const targetIndex = children.findIndex(child => child.id === targetId);
|
|
41
|
+
if (targetIndex === -1) return parent;
|
|
42
|
+
|
|
43
|
+
children.splice(targetIndex, 0, draggedNode);
|
|
44
|
+
return { ...parent, children };
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const rootWithoutDragged = updateNodeById(root, oldParent.id, parent => ({
|
|
49
|
+
...parent,
|
|
50
|
+
children: (parent.children ?? []).filter(child => child.id !== draggedId)
|
|
51
|
+
}));
|
|
52
|
+
|
|
53
|
+
return updateNodeById(rootWithoutDragged, targetId, target => ({
|
|
54
|
+
...target,
|
|
55
|
+
children: [...(target.children ?? []), draggedNode]
|
|
56
|
+
}));
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function duplicateNodeBelow(root: GameObject, nodeId: string): { root: GameObject; duplicatedId: string } | null {
|
|
60
|
+
const node = findNode(root, nodeId);
|
|
61
|
+
const parent = findParent(root, nodeId);
|
|
62
|
+
if (!node || !parent) return null;
|
|
63
|
+
|
|
64
|
+
const duplicate = cloneNode(node);
|
|
65
|
+
const nextRoot = updateNodeById(root, parent.id, currentParent => ({
|
|
66
|
+
...currentParent,
|
|
67
|
+
children: (() => {
|
|
68
|
+
const children = [...(currentParent.children ?? [])];
|
|
69
|
+
const index = children.findIndex(child => child.id === nodeId);
|
|
70
|
+
if (index === -1) return [...children, duplicate];
|
|
71
|
+
children.splice(index + 1, 0, duplicate);
|
|
72
|
+
return children;
|
|
73
|
+
})()
|
|
74
|
+
}));
|
|
75
|
+
|
|
76
|
+
return { root: nextRoot, duplicatedId: duplicate.id };
|
|
77
|
+
}
|
|
7
78
|
|
|
8
79
|
export default function EditorTree({
|
|
9
80
|
prefabData,
|
|
@@ -24,21 +95,16 @@ export default function EditorTree({
|
|
|
24
95
|
canUndo?: boolean;
|
|
25
96
|
canRedo?: boolean;
|
|
26
97
|
}) {
|
|
27
|
-
const
|
|
98
|
+
const { onFocusNode } = useEditorContext();
|
|
28
99
|
const [draggedId, setDraggedId] = useState<string | null>(null);
|
|
100
|
+
const [dropTarget, setDropTarget] = useState<{ id: string; position: DropPosition } | null>(null);
|
|
29
101
|
const [collapsedIds, setCollapsedIds] = useState<Set<string>>(new Set());
|
|
30
102
|
const [collapsed, setCollapsed] = useState(false);
|
|
31
|
-
const [fileMenuOpen, setFileMenuOpen] = useState(false);
|
|
32
103
|
const [searchQuery, setSearchQuery] = useState('');
|
|
104
|
+
const [contextMenu, setContextMenu] = useState<TreeContextMenuState>(null);
|
|
33
105
|
|
|
34
106
|
if (!prefabData || !setPrefabData) return null;
|
|
35
107
|
|
|
36
|
-
const handleContextMenu = (e: MouseEvent, nodeId: string) => {
|
|
37
|
-
e.preventDefault();
|
|
38
|
-
e.stopPropagation();
|
|
39
|
-
setContextMenu({ x: e.clientX, y: e.clientY, nodeId });
|
|
40
|
-
};
|
|
41
|
-
|
|
42
108
|
const toggleCollapse = (e: MouseEvent, id: string) => {
|
|
43
109
|
e.stopPropagation();
|
|
44
110
|
setCollapsedIds(prev => {
|
|
@@ -49,47 +115,46 @@ export default function EditorTree({
|
|
|
49
115
|
};
|
|
50
116
|
|
|
51
117
|
const handleAddChild = (parentId: string) => {
|
|
118
|
+
const newNode = {
|
|
119
|
+
id: crypto.randomUUID(),
|
|
120
|
+
name: "New Node",
|
|
121
|
+
components: {
|
|
122
|
+
transform: {
|
|
123
|
+
type: "Transform",
|
|
124
|
+
properties: { ...getComponent('Transform')?.defaultProperties }
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
};
|
|
128
|
+
|
|
52
129
|
setPrefabData(prev => ({
|
|
53
130
|
...prev,
|
|
54
131
|
root: updateNodeById(prev.root, parentId, parent => ({
|
|
55
132
|
...parent,
|
|
56
|
-
children: [...(parent.children ?? []),
|
|
57
|
-
id: crypto.randomUUID(),
|
|
58
|
-
name: "New Node",
|
|
59
|
-
components: {
|
|
60
|
-
transform: {
|
|
61
|
-
type: "Transform",
|
|
62
|
-
properties: { ...getComponent('Transform')?.defaultProperties }
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
}]
|
|
133
|
+
children: [...(parent.children ?? []), newNode]
|
|
66
134
|
}))
|
|
67
135
|
}));
|
|
68
|
-
|
|
136
|
+
setSelectedId(newNode.id);
|
|
69
137
|
};
|
|
70
138
|
|
|
71
139
|
const handleDuplicate = (nodeId: string) => {
|
|
72
140
|
if (nodeId === prefabData.root.id) return;
|
|
73
141
|
setPrefabData(prev => {
|
|
74
|
-
const
|
|
75
|
-
|
|
76
|
-
|
|
142
|
+
const result = duplicateNodeBelow(prev.root, nodeId);
|
|
143
|
+
if (!result) return prev;
|
|
144
|
+
|
|
145
|
+
setSelectedId(result.duplicatedId);
|
|
146
|
+
|
|
77
147
|
return {
|
|
78
148
|
...prev,
|
|
79
|
-
root:
|
|
80
|
-
...p,
|
|
81
|
-
children: [...(p.children ?? []), cloneNode(node)]
|
|
82
|
-
}))
|
|
149
|
+
root: result.root
|
|
83
150
|
};
|
|
84
151
|
});
|
|
85
|
-
setContextMenu(null);
|
|
86
152
|
};
|
|
87
153
|
|
|
88
154
|
const handleDelete = (nodeId: string) => {
|
|
89
155
|
if (nodeId === prefabData.root.id) return;
|
|
90
156
|
setPrefabData(prev => ({ ...prev, root: deleteNode(prev.root, nodeId)! }));
|
|
91
157
|
if (selectedId === nodeId) setSelectedId(null);
|
|
92
|
-
setContextMenu(null);
|
|
93
158
|
};
|
|
94
159
|
|
|
95
160
|
const handleToggleDisabled = (nodeId: string) => {
|
|
@@ -100,43 +165,68 @@ export default function EditorTree({
|
|
|
100
165
|
disabled: !node.disabled
|
|
101
166
|
}))
|
|
102
167
|
}));
|
|
103
|
-
setContextMenu(null);
|
|
104
168
|
};
|
|
105
169
|
|
|
170
|
+
const closeContextMenu = () => setContextMenu(null);
|
|
171
|
+
|
|
172
|
+
const openContextMenu = (nodeId: string, x: number, y: number) => {
|
|
173
|
+
setSelectedId(nodeId);
|
|
174
|
+
setContextMenu({ nodeId, x, y });
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
const handleFocus = (nodeId: string) => {
|
|
178
|
+
setSelectedId(nodeId);
|
|
179
|
+
onFocusNode?.(nodeId);
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
const renderTreeNodeMenu = (nodeId: string, isRoot: boolean, onClose: () => void) => (
|
|
183
|
+
<TreeNodeMenu
|
|
184
|
+
isRoot={isRoot}
|
|
185
|
+
nodeId={nodeId}
|
|
186
|
+
onAddChild={handleAddChild}
|
|
187
|
+
onFocus={handleFocus}
|
|
188
|
+
onDuplicate={isRoot ? undefined : handleDuplicate}
|
|
189
|
+
onDelete={isRoot ? undefined : handleDelete}
|
|
190
|
+
onClose={onClose}
|
|
191
|
+
/>
|
|
192
|
+
);
|
|
193
|
+
|
|
106
194
|
const handleDragStart = (e: React.DragEvent, id: string) => {
|
|
107
195
|
if (id === prefabData.root.id) return e.preventDefault();
|
|
108
196
|
e.dataTransfer.effectAllowed = "move";
|
|
109
197
|
setDraggedId(id);
|
|
110
198
|
};
|
|
111
199
|
|
|
112
|
-
const
|
|
200
|
+
const getDropPosition = (e: React.DragEvent<HTMLDivElement>, isRoot: boolean): DropPosition => {
|
|
201
|
+
if (isRoot) return 'inside';
|
|
202
|
+
const rect = e.currentTarget.getBoundingClientRect();
|
|
203
|
+
return e.clientY <= rect.top + rect.height * 0.35 ? 'before' : 'inside';
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
const handleDragOver = (e: React.DragEvent<HTMLDivElement>, targetId: string, isRoot: boolean) => {
|
|
113
207
|
if (!draggedId || draggedId === targetId) return;
|
|
114
208
|
const draggedNode = findNode(prefabData.root, draggedId);
|
|
115
209
|
if (draggedNode && findNode(draggedNode, targetId)) return;
|
|
116
210
|
e.preventDefault();
|
|
211
|
+
setDropTarget({ id: targetId, position: getDropPosition(e, isRoot) });
|
|
117
212
|
};
|
|
118
213
|
|
|
119
|
-
const
|
|
214
|
+
const handleDragLeave = (e: React.DragEvent<HTMLDivElement>, targetId: string) => {
|
|
215
|
+
const relatedTarget = e.relatedTarget;
|
|
216
|
+
if (relatedTarget instanceof Node && e.currentTarget.contains(relatedTarget)) return;
|
|
217
|
+
setDropTarget(current => current?.id === targetId ? null : current);
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
const handleDrop = (e: React.DragEvent<HTMLDivElement>, targetId: string, isRoot: boolean) => {
|
|
120
221
|
if (!draggedId || draggedId === targetId) return;
|
|
121
222
|
e.preventDefault();
|
|
223
|
+
const dropPosition = getDropPosition(e, isRoot);
|
|
122
224
|
setPrefabData(prev => {
|
|
123
|
-
const
|
|
124
|
-
|
|
125
|
-
if (!draggedNode || !oldParent || findNode(draggedNode, targetId)) return prev;
|
|
126
|
-
|
|
127
|
-
let root = updateNodeById(prev.root, oldParent.id, p => ({
|
|
128
|
-
...p,
|
|
129
|
-
children: p.children!.filter(c => c.id !== draggedId)
|
|
130
|
-
}));
|
|
131
|
-
|
|
132
|
-
root = updateNodeById(root, targetId, t => ({
|
|
133
|
-
...t,
|
|
134
|
-
children: [...(t.children ?? []), draggedNode]
|
|
135
|
-
}));
|
|
136
|
-
|
|
137
|
-
return { ...prev, root };
|
|
225
|
+
const root = moveNode(prev.root, draggedId, targetId, dropPosition);
|
|
226
|
+
return root === prev.root ? prev : { ...prev, root };
|
|
138
227
|
});
|
|
139
228
|
setDraggedId(null);
|
|
229
|
+
setDropTarget(null);
|
|
140
230
|
};
|
|
141
231
|
|
|
142
232
|
|
|
@@ -156,6 +246,9 @@ export default function EditorTree({
|
|
|
156
246
|
const isCollapsed = collapsedIds.has(node.id);
|
|
157
247
|
const hasChildren = node.children && node.children.length > 0;
|
|
158
248
|
const isRoot = node.id === prefabData.root.id;
|
|
249
|
+
const isDropTarget = dropTarget?.id === node.id;
|
|
250
|
+
const showDropBefore = isDropTarget && dropTarget?.position === 'before';
|
|
251
|
+
const showDropInside = isDropTarget && dropTarget?.position === 'inside';
|
|
159
252
|
|
|
160
253
|
return (
|
|
161
254
|
<div key={node.id}>
|
|
@@ -168,14 +261,21 @@ export default function EditorTree({
|
|
|
168
261
|
display: 'flex',
|
|
169
262
|
alignItems: 'center',
|
|
170
263
|
justifyContent: 'space-between',
|
|
264
|
+
borderTop: showDropBefore ? `2px solid ${colors.accent}` : undefined,
|
|
265
|
+
boxShadow: showDropInside ? `inset 0 0 0 1px ${colors.accentBorder}` : undefined,
|
|
171
266
|
}}
|
|
172
267
|
draggable={!isRoot}
|
|
173
268
|
onClick={(e) => { e.stopPropagation(); setSelectedId(node.id); }}
|
|
174
|
-
onContextMenu={(e) =>
|
|
269
|
+
onContextMenu={(e) => {
|
|
270
|
+
e.preventDefault();
|
|
271
|
+
e.stopPropagation();
|
|
272
|
+
openContextMenu(node.id, e.clientX, e.clientY);
|
|
273
|
+
}}
|
|
175
274
|
onDragStart={(e) => handleDragStart(e, node.id)}
|
|
176
|
-
onDragEnd={() => setDraggedId(null)}
|
|
177
|
-
onDragOver={(e) => handleDragOver(e, node.id)}
|
|
178
|
-
|
|
275
|
+
onDragEnd={() => { setDraggedId(null); setDropTarget(null); }}
|
|
276
|
+
onDragOver={(e) => handleDragOver(e, node.id, isRoot)}
|
|
277
|
+
onDragLeave={(e) => handleDragLeave(e, node.id)}
|
|
278
|
+
onDrop={(e) => handleDrop(e, node.id, isRoot)}
|
|
179
279
|
>
|
|
180
280
|
<div style={{ display: 'flex', alignItems: 'center', flex: 1, minWidth: 0 }}>
|
|
181
281
|
<span
|
|
@@ -196,24 +296,74 @@ export default function EditorTree({
|
|
|
196
296
|
</span>
|
|
197
297
|
</div>
|
|
198
298
|
{!isRoot && (
|
|
199
|
-
<
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
299
|
+
<div style={{ display: 'flex', alignItems: 'center', gap: 2 }}>
|
|
300
|
+
<Dropdown
|
|
301
|
+
placement="bottom-end"
|
|
302
|
+
trigger={({ ref, toggle }) => (
|
|
303
|
+
<MenuTriggerButton
|
|
304
|
+
buttonRef={ref}
|
|
305
|
+
onToggle={toggle}
|
|
306
|
+
title="Node Actions"
|
|
307
|
+
style={{
|
|
308
|
+
background: 'none',
|
|
309
|
+
border: 'none',
|
|
310
|
+
cursor: 'pointer',
|
|
311
|
+
padding: '0 4px',
|
|
312
|
+
fontSize: 14,
|
|
313
|
+
opacity: 0.7,
|
|
314
|
+
color: 'inherit',
|
|
315
|
+
}}
|
|
316
|
+
>
|
|
317
|
+
⋯
|
|
318
|
+
</MenuTriggerButton>
|
|
319
|
+
)}
|
|
320
|
+
>
|
|
321
|
+
{(close) => renderTreeNodeMenu(node.id, false, close)}
|
|
322
|
+
</Dropdown>
|
|
323
|
+
<button
|
|
324
|
+
style={{
|
|
325
|
+
background: 'none',
|
|
326
|
+
border: 'none',
|
|
327
|
+
cursor: 'pointer',
|
|
328
|
+
padding: '0 4px',
|
|
329
|
+
fontSize: 14,
|
|
330
|
+
opacity: node.disabled ? 0.5 : 0.7,
|
|
331
|
+
color: 'inherit',
|
|
332
|
+
}}
|
|
333
|
+
onClick={(e) => {
|
|
334
|
+
e.stopPropagation();
|
|
335
|
+
handleToggleDisabled(node.id);
|
|
336
|
+
}}
|
|
337
|
+
title={node.disabled ? 'Enable' : 'Disable'}
|
|
338
|
+
>
|
|
339
|
+
{node.disabled ? '◎' : '◉'}
|
|
340
|
+
</button>
|
|
341
|
+
</div>
|
|
342
|
+
)}
|
|
343
|
+
{isRoot && (
|
|
344
|
+
<Dropdown
|
|
345
|
+
placement="bottom-end"
|
|
346
|
+
trigger={({ ref, toggle }) => (
|
|
347
|
+
<MenuTriggerButton
|
|
348
|
+
buttonRef={ref}
|
|
349
|
+
onToggle={toggle}
|
|
350
|
+
title="Scene Actions"
|
|
351
|
+
style={{
|
|
352
|
+
background: 'none',
|
|
353
|
+
border: 'none',
|
|
354
|
+
cursor: 'pointer',
|
|
355
|
+
padding: '0 4px',
|
|
356
|
+
fontSize: 14,
|
|
357
|
+
opacity: 0.7,
|
|
358
|
+
color: 'inherit',
|
|
359
|
+
}}
|
|
360
|
+
>
|
|
361
|
+
⋯
|
|
362
|
+
</MenuTriggerButton>
|
|
363
|
+
)}
|
|
214
364
|
>
|
|
215
|
-
{node.
|
|
216
|
-
</
|
|
365
|
+
{(close) => renderTreeNodeMenu(node.id, true, close)}
|
|
366
|
+
</Dropdown>
|
|
217
367
|
)}
|
|
218
368
|
</div>
|
|
219
369
|
{!isCollapsed && node.children && node.children.map(child => renderNode(child, depth + 1))}
|
|
@@ -223,7 +373,7 @@ export default function EditorTree({
|
|
|
223
373
|
|
|
224
374
|
return (
|
|
225
375
|
<>
|
|
226
|
-
<div style={{ ...tree.panel, width: collapsed ? 'auto' : 224 }}
|
|
376
|
+
<div style={{ ...tree.panel, width: collapsed ? 'auto' : 224 }}>
|
|
227
377
|
<div style={base.header}>
|
|
228
378
|
<div style={{ display: 'flex', alignItems: 'center', gap: 6, cursor: 'pointer' }} onClick={() => setCollapsed(!collapsed)}>
|
|
229
379
|
<span>{collapsed ? '▶' : '▼'}</span>
|
|
@@ -247,22 +397,27 @@ export default function EditorTree({
|
|
|
247
397
|
>
|
|
248
398
|
↷
|
|
249
399
|
</button>
|
|
250
|
-
<
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
400
|
+
<Dropdown
|
|
401
|
+
placement="bottom-end"
|
|
402
|
+
trigger={({ ref, toggle }) => (
|
|
403
|
+
<MenuTriggerButton
|
|
404
|
+
buttonRef={ref}
|
|
405
|
+
onToggle={toggle}
|
|
406
|
+
title="Menu"
|
|
407
|
+
style={{ ...base.btn, padding: '2px 6px', fontSize: 10 }}
|
|
408
|
+
>
|
|
409
|
+
⋮
|
|
410
|
+
</MenuTriggerButton>
|
|
411
|
+
)}
|
|
412
|
+
>
|
|
413
|
+
{(close) => (
|
|
259
414
|
<FileMenu
|
|
260
415
|
prefabData={prefabData}
|
|
261
416
|
setPrefabData={setPrefabData}
|
|
262
|
-
onClose={
|
|
417
|
+
onClose={close}
|
|
263
418
|
/>
|
|
264
419
|
)}
|
|
265
|
-
</
|
|
420
|
+
</Dropdown>
|
|
266
421
|
</div>
|
|
267
422
|
)}
|
|
268
423
|
</div>
|
|
@@ -285,104 +440,13 @@ export default function EditorTree({
|
|
|
285
440
|
</>
|
|
286
441
|
)}
|
|
287
442
|
</div>
|
|
443
|
+
<TreeContextMenu
|
|
444
|
+
contextMenu={contextMenu}
|
|
445
|
+
onClose={closeContextMenu}
|
|
446
|
+
>
|
|
447
|
+
{(nodeId, close) => renderTreeNodeMenu(nodeId, nodeId === prefabData.root.id, close)}
|
|
448
|
+
</TreeContextMenu>
|
|
288
449
|
|
|
289
|
-
{contextMenu && (
|
|
290
|
-
<div
|
|
291
|
-
style={{ ...menu.container, top: contextMenu.y, left: contextMenu.x }}
|
|
292
|
-
onClick={(e) => e.stopPropagation()}
|
|
293
|
-
onPointerLeave={() => setContextMenu(null)}
|
|
294
|
-
>
|
|
295
|
-
<button style={menu.item} onClick={() => handleAddChild(contextMenu.nodeId)}>
|
|
296
|
-
Add Child
|
|
297
|
-
</button>
|
|
298
|
-
{contextMenu.nodeId !== prefabData.root.id && (
|
|
299
|
-
<>
|
|
300
|
-
<button style={menu.item} onClick={() => handleDuplicate(contextMenu.nodeId)}>
|
|
301
|
-
Duplicate
|
|
302
|
-
</button>
|
|
303
|
-
<button style={{ ...menu.item, ...menu.danger }} onClick={() => handleDelete(contextMenu.nodeId)}>
|
|
304
|
-
Delete
|
|
305
|
-
</button>
|
|
306
|
-
</>
|
|
307
|
-
)}
|
|
308
|
-
</div>
|
|
309
|
-
)}
|
|
310
450
|
</>
|
|
311
451
|
);
|
|
312
452
|
}
|
|
313
|
-
|
|
314
|
-
function FileMenu({
|
|
315
|
-
prefabData,
|
|
316
|
-
setPrefabData,
|
|
317
|
-
onClose
|
|
318
|
-
}: {
|
|
319
|
-
prefabData: Prefab;
|
|
320
|
-
setPrefabData: Dispatch<SetStateAction<Prefab>>;
|
|
321
|
-
onClose: () => void;
|
|
322
|
-
}) {
|
|
323
|
-
const { onScreenshot, onExportGLB } = useEditorContext();
|
|
324
|
-
|
|
325
|
-
const handleLoad = async () => {
|
|
326
|
-
const loadedPrefab = await loadJson();
|
|
327
|
-
if (!loadedPrefab) return;
|
|
328
|
-
setPrefabData(loadedPrefab);
|
|
329
|
-
onClose();
|
|
330
|
-
};
|
|
331
|
-
|
|
332
|
-
const handleSave = () => {
|
|
333
|
-
saveJson(prefabData, "prefab");
|
|
334
|
-
onClose();
|
|
335
|
-
};
|
|
336
|
-
|
|
337
|
-
const handleLoadIntoScene = async () => {
|
|
338
|
-
const loadedPrefab = await loadJson();
|
|
339
|
-
if (!loadedPrefab) return;
|
|
340
|
-
|
|
341
|
-
setPrefabData(prev => ({
|
|
342
|
-
...prev,
|
|
343
|
-
root: updateNodeById(prev.root, prev.root.id, root => ({
|
|
344
|
-
...root,
|
|
345
|
-
children: [...(root.children ?? []), regenerateIds(loadedPrefab.root)]
|
|
346
|
-
}))
|
|
347
|
-
}));
|
|
348
|
-
onClose();
|
|
349
|
-
};
|
|
350
|
-
|
|
351
|
-
return (
|
|
352
|
-
<div
|
|
353
|
-
style={{ ...menu.container, position: 'absolute', top: 28, right: 0 }}
|
|
354
|
-
onClick={(e) => e.stopPropagation()}
|
|
355
|
-
>
|
|
356
|
-
<button
|
|
357
|
-
style={menu.item}
|
|
358
|
-
onClick={handleLoad}
|
|
359
|
-
>
|
|
360
|
-
📥 Load Prefab JSON
|
|
361
|
-
</button>
|
|
362
|
-
<button
|
|
363
|
-
style={menu.item}
|
|
364
|
-
onClick={handleSave}
|
|
365
|
-
>
|
|
366
|
-
💾 Save Prefab JSON
|
|
367
|
-
</button>
|
|
368
|
-
<button
|
|
369
|
-
style={menu.item}
|
|
370
|
-
onClick={handleLoadIntoScene}
|
|
371
|
-
>
|
|
372
|
-
📂 Load into Scene
|
|
373
|
-
</button>
|
|
374
|
-
<button
|
|
375
|
-
style={menu.item}
|
|
376
|
-
onClick={() => { onScreenshot?.(); onClose(); }}
|
|
377
|
-
>
|
|
378
|
-
📸 Screenshot
|
|
379
|
-
</button>
|
|
380
|
-
<button
|
|
381
|
-
style={menu.item}
|
|
382
|
-
onClick={() => { onExportGLB?.(); onClose(); }}
|
|
383
|
-
>
|
|
384
|
-
📦 Export GLB
|
|
385
|
-
</button>
|
|
386
|
-
</div>
|
|
387
|
-
);
|
|
388
|
-
}
|