react-three-game 0.0.57 → 0.0.59
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 +1 -1
- package/README.md +59 -35
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/tools/assetviewer/page.js +1 -1
- package/dist/tools/dragdrop/DragDropLoader.d.ts +19 -6
- package/dist/tools/dragdrop/DragDropLoader.js +77 -40
- package/dist/tools/dragdrop/index.d.ts +4 -0
- package/dist/tools/dragdrop/index.js +2 -0
- package/dist/tools/dragdrop/modelLoader.d.ts +5 -6
- package/dist/tools/dragdrop/modelLoader.js +62 -49
- package/dist/tools/dragdrop/page.js +3 -3
- package/dist/tools/prefabeditor/EditorTree.js +24 -48
- package/dist/tools/prefabeditor/EditorTreeMenus.d.ts +33 -0
- package/dist/tools/prefabeditor/EditorTreeMenus.js +136 -0
- package/dist/tools/prefabeditor/PrefabEditor.js +1 -1
- package/dist/tools/prefabeditor/PrefabRoot.js +5 -3
- package/dist/tools/prefabeditor/components/CameraComponent.js +32 -12
- package/dist/tools/prefabeditor/components/DirectionalLightComponent.js +49 -23
- package/dist/tools/prefabeditor/components/MaterialComponent.d.ts +8 -0
- package/dist/tools/prefabeditor/components/MaterialComponent.js +11 -5
- package/dist/tools/prefabeditor/components/SpotLightComponent.js +34 -13
- package/package.json +2 -2
- package/react-three-game-skill/react-three-game/SKILL.md +63 -5
- package/react-three-game-skill/react-three-game/rules/ADVANCED_PHYSICS.md +7 -5
- package/src/index.ts +1 -1
- package/src/tools/assetviewer/page.tsx +1 -1
- package/src/tools/dragdrop/DragDropLoader.tsx +118 -55
- package/src/tools/dragdrop/index.ts +4 -0
- package/src/tools/dragdrop/modelLoader.ts +95 -50
- package/src/tools/dragdrop/page.tsx +7 -4
- package/src/tools/prefabeditor/EditorTree.tsx +56 -125
- package/src/tools/prefabeditor/EditorTreeMenus.tsx +307 -0
- package/src/tools/prefabeditor/PrefabEditor.tsx +1 -1
- package/src/tools/prefabeditor/PrefabRoot.tsx +6 -3
- package/src/tools/prefabeditor/components/CameraComponent.tsx +51 -14
- package/src/tools/prefabeditor/components/DirectionalLightComponent.tsx +59 -28
- package/src/tools/prefabeditor/components/MaterialComponent.tsx +18 -9
- package/src/tools/prefabeditor/components/SpotLightComponent.tsx +49 -18
|
@@ -1,14 +1,16 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { Object3D } from "three";
|
|
2
|
+
import { DRACOLoader, FBXLoader, GLTFLoader } from "three/examples/jsm/Addons.js";
|
|
3
|
+
|
|
4
|
+
export type LoadedModel = Object3D;
|
|
2
5
|
|
|
3
6
|
export type ModelLoadResult = {
|
|
4
7
|
success: boolean;
|
|
5
|
-
model?:
|
|
6
|
-
error?:
|
|
8
|
+
model?: LoadedModel;
|
|
9
|
+
error?: unknown;
|
|
7
10
|
};
|
|
8
11
|
|
|
9
12
|
export type ProgressCallback = (filename: string, loaded: number, total: number) => void;
|
|
10
13
|
|
|
11
|
-
// Singleton loader instances
|
|
12
14
|
const dracoLoader = new DRACOLoader();
|
|
13
15
|
dracoLoader.setDecoderPath("https://www.gstatic.com/draco/v1/decoders/");
|
|
14
16
|
|
|
@@ -17,37 +19,76 @@ gltfLoader.setDRACOLoader(dracoLoader);
|
|
|
17
19
|
|
|
18
20
|
const fbxLoader = new FBXLoader();
|
|
19
21
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
22
|
+
type ModelFileKind = "gltf" | "fbx";
|
|
23
|
+
|
|
24
|
+
function normalizeModelPath(name: string) {
|
|
25
|
+
return name.split(/[?#]/, 1)[0].toLowerCase();
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function getModelFileKind(name: string): ModelFileKind | null {
|
|
29
|
+
const normalizedName = normalizeModelPath(name);
|
|
30
|
+
|
|
31
|
+
if (normalizedName.endsWith(".glb") || normalizedName.endsWith(".gltf")) {
|
|
32
|
+
return "gltf";
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (normalizedName.endsWith(".fbx")) {
|
|
36
|
+
return "fbx";
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function canParseModelFile(file: File | string) {
|
|
43
|
+
const filename = typeof file === "string" ? file : file.name;
|
|
44
|
+
return getModelFileKind(filename) !== null;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function parseModelBuffer(arrayBuffer: ArrayBuffer, sourceName: string): Promise<ModelLoadResult> {
|
|
48
|
+
const modelFileKind = getModelFileKind(sourceName);
|
|
49
|
+
|
|
50
|
+
if (modelFileKind === "gltf") {
|
|
51
|
+
return new Promise(resolve => {
|
|
52
|
+
gltfLoader.parse(
|
|
53
|
+
arrayBuffer,
|
|
54
|
+
"",
|
|
55
|
+
gltf => {
|
|
56
|
+
resolve({ success: true, model: gltf.scene });
|
|
57
|
+
},
|
|
58
|
+
error => {
|
|
59
|
+
resolve({ success: false, error });
|
|
60
|
+
},
|
|
61
|
+
);
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (modelFileKind === "fbx") {
|
|
66
|
+
try {
|
|
67
|
+
const model = fbxLoader.parse(arrayBuffer, "");
|
|
68
|
+
return Promise.resolve({ success: true, model });
|
|
69
|
+
} catch (error) {
|
|
70
|
+
return Promise.resolve({ success: false, error });
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return Promise.resolve({ success: false, error: new Error(`Unsupported file format: ${sourceName}`) });
|
|
75
|
+
}
|
|
76
|
+
|
|
24
77
|
export function parseModelFromFile(file: File): Promise<ModelLoadResult> {
|
|
25
|
-
return new Promise(
|
|
78
|
+
return new Promise(resolve => {
|
|
26
79
|
const reader = new FileReader();
|
|
27
|
-
|
|
80
|
+
|
|
81
|
+
reader.onload = event => {
|
|
28
82
|
const arrayBuffer = event.target?.result as ArrayBuffer;
|
|
83
|
+
|
|
29
84
|
if (!arrayBuffer) {
|
|
30
|
-
resolve({ success: false, error: new Error(
|
|
85
|
+
resolve({ success: false, error: new Error("Failed to read file") });
|
|
31
86
|
return;
|
|
32
87
|
}
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
gltfLoader.parse(arrayBuffer, '', (gltf) => {
|
|
36
|
-
resolve({ success: true, model: gltf.scene });
|
|
37
|
-
}, (error) => {
|
|
38
|
-
resolve({ success: false, error });
|
|
39
|
-
});
|
|
40
|
-
} else if (name.endsWith('.fbx')) {
|
|
41
|
-
try {
|
|
42
|
-
const model = fbxLoader.parse(arrayBuffer, '');
|
|
43
|
-
resolve({ success: true, model });
|
|
44
|
-
} catch (error) {
|
|
45
|
-
resolve({ success: false, error });
|
|
46
|
-
}
|
|
47
|
-
} else {
|
|
48
|
-
resolve({ success: false, error: new Error(`Unsupported file format: ${file.name}`) });
|
|
49
|
-
}
|
|
88
|
+
|
|
89
|
+
void parseModelBuffer(arrayBuffer, file.name).then(resolve);
|
|
50
90
|
};
|
|
91
|
+
|
|
51
92
|
reader.onerror = () => resolve({ success: false, error: reader.error });
|
|
52
93
|
reader.readAsArrayBuffer(file);
|
|
53
94
|
});
|
|
@@ -55,45 +96,49 @@ export function parseModelFromFile(file: File): Promise<ModelLoadResult> {
|
|
|
55
96
|
|
|
56
97
|
export async function loadModel(
|
|
57
98
|
filename: string,
|
|
58
|
-
onProgress?: ProgressCallback
|
|
99
|
+
onProgress?: ProgressCallback,
|
|
59
100
|
): Promise<ModelLoadResult> {
|
|
60
101
|
try {
|
|
61
|
-
// Use filename directly (should already include leading /)
|
|
62
102
|
const fullPath = filename;
|
|
103
|
+
const modelFileKind = getModelFileKind(filename);
|
|
63
104
|
|
|
64
|
-
if (
|
|
65
|
-
return new Promise(
|
|
105
|
+
if (modelFileKind === "gltf") {
|
|
106
|
+
return new Promise(resolve => {
|
|
66
107
|
gltfLoader.load(
|
|
67
108
|
fullPath,
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
if (onProgress) {
|
|
71
|
-
|
|
72
|
-
const total = progressEvent.total || progressEvent.loaded;
|
|
73
|
-
onProgress(filename, progressEvent.loaded, total);
|
|
109
|
+
gltf => resolve({ success: true, model: gltf.scene }),
|
|
110
|
+
progressEvent => {
|
|
111
|
+
if (!onProgress) {
|
|
112
|
+
return;
|
|
74
113
|
}
|
|
114
|
+
|
|
115
|
+
const total = progressEvent.total || progressEvent.loaded;
|
|
116
|
+
onProgress(filename, progressEvent.loaded, total);
|
|
75
117
|
},
|
|
76
|
-
|
|
118
|
+
error => resolve({ success: false, error }),
|
|
77
119
|
);
|
|
78
120
|
});
|
|
79
|
-
}
|
|
80
|
-
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (modelFileKind === "fbx") {
|
|
124
|
+
return new Promise(resolve => {
|
|
81
125
|
fbxLoader.load(
|
|
82
126
|
fullPath,
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
if (onProgress) {
|
|
86
|
-
|
|
87
|
-
const total = progressEvent.total || progressEvent.loaded;
|
|
88
|
-
onProgress(filename, progressEvent.loaded, total);
|
|
127
|
+
model => resolve({ success: true, model }),
|
|
128
|
+
progressEvent => {
|
|
129
|
+
if (!onProgress) {
|
|
130
|
+
return;
|
|
89
131
|
}
|
|
132
|
+
|
|
133
|
+
const total = progressEvent.total || progressEvent.loaded;
|
|
134
|
+
onProgress(filename, progressEvent.loaded, total);
|
|
90
135
|
},
|
|
91
|
-
|
|
136
|
+
error => resolve({ success: false, error }),
|
|
92
137
|
);
|
|
93
138
|
});
|
|
94
|
-
} else {
|
|
95
|
-
return { success: false, error: new Error(`Unsupported file format: ${filename}`) };
|
|
96
139
|
}
|
|
140
|
+
|
|
141
|
+
return { success: false, error: new Error(`Unsupported file format: ${filename}`) };
|
|
97
142
|
} catch (error) {
|
|
98
143
|
return { success: false, error };
|
|
99
144
|
}
|
|
@@ -3,15 +3,18 @@
|
|
|
3
3
|
import { Physics, RigidBody } from "@react-three/rapier";
|
|
4
4
|
import { OrbitControls } from "@react-three/drei";
|
|
5
5
|
import { useState } from "react";
|
|
6
|
-
import { DragDropLoader } from "./
|
|
6
|
+
import { DragDropLoader } from "./index";
|
|
7
7
|
import GameCanvas from "../../shared/GameCanvas";
|
|
8
8
|
|
|
9
9
|
export default function Home() {
|
|
10
10
|
const [models, setModels] = useState<any[]>([]);
|
|
11
11
|
|
|
12
12
|
return (
|
|
13
|
-
|
|
14
|
-
|
|
13
|
+
<DragDropLoader
|
|
14
|
+
onModelLoaded={model => setModels(prev => [...prev, model])}
|
|
15
|
+
className="w-full items-center justify-items-center min-h-screen"
|
|
16
|
+
style={{ height: "100vh" }}
|
|
17
|
+
>
|
|
15
18
|
<div className="w-full items-center justify-items-center min-h-screen" style={{ height: "100vh" }}>
|
|
16
19
|
<GameCanvas>
|
|
17
20
|
<Physics>
|
|
@@ -37,6 +40,6 @@ export default function Home() {
|
|
|
37
40
|
</Physics>
|
|
38
41
|
</GameCanvas>
|
|
39
42
|
</div>
|
|
40
|
-
|
|
43
|
+
</DragDropLoader>
|
|
41
44
|
);
|
|
42
45
|
}
|
|
@@ -1,10 +1,11 @@
|
|
|
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
7
|
import { Dropdown } from './Dropdown';
|
|
8
|
+
import { FileMenu, MenuTriggerButton, TreeContextMenu, TreeContextMenuState, TreeNodeMenu } from './EditorTreeMenus';
|
|
8
9
|
|
|
9
10
|
type DropPosition = 'before' | 'inside';
|
|
10
11
|
|
|
@@ -100,6 +101,7 @@ export default function EditorTree({
|
|
|
100
101
|
const [collapsedIds, setCollapsedIds] = useState<Set<string>>(new Set());
|
|
101
102
|
const [collapsed, setCollapsed] = useState(false);
|
|
102
103
|
const [searchQuery, setSearchQuery] = useState('');
|
|
104
|
+
const [contextMenu, setContextMenu] = useState<TreeContextMenuState>(null);
|
|
103
105
|
|
|
104
106
|
if (!prefabData || !setPrefabData) return null;
|
|
105
107
|
|
|
@@ -165,6 +167,30 @@ export default function EditorTree({
|
|
|
165
167
|
}));
|
|
166
168
|
};
|
|
167
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
|
+
|
|
168
194
|
const handleDragStart = (e: React.DragEvent, id: string) => {
|
|
169
195
|
if (id === prefabData.root.id) return e.preventDefault();
|
|
170
196
|
e.dataTransfer.effectAllowed = "move";
|
|
@@ -240,6 +266,11 @@ export default function EditorTree({
|
|
|
240
266
|
}}
|
|
241
267
|
draggable={!isRoot}
|
|
242
268
|
onClick={(e) => { e.stopPropagation(); setSelectedId(node.id); }}
|
|
269
|
+
onContextMenu={(e) => {
|
|
270
|
+
e.preventDefault();
|
|
271
|
+
e.stopPropagation();
|
|
272
|
+
openContextMenu(node.id, e.clientX, e.clientY);
|
|
273
|
+
}}
|
|
243
274
|
onDragStart={(e) => handleDragStart(e, node.id)}
|
|
244
275
|
onDragEnd={() => { setDraggedId(null); setDropTarget(null); }}
|
|
245
276
|
onDragOver={(e) => handleDragOver(e, node.id, isRoot)}
|
|
@@ -269,8 +300,10 @@ export default function EditorTree({
|
|
|
269
300
|
<Dropdown
|
|
270
301
|
placement="bottom-end"
|
|
271
302
|
trigger={({ ref, toggle }) => (
|
|
272
|
-
<
|
|
273
|
-
|
|
303
|
+
<MenuTriggerButton
|
|
304
|
+
buttonRef={ref}
|
|
305
|
+
onToggle={toggle}
|
|
306
|
+
title="Node Actions"
|
|
274
307
|
style={{
|
|
275
308
|
background: 'none',
|
|
276
309
|
border: 'none',
|
|
@@ -280,32 +313,12 @@ export default function EditorTree({
|
|
|
280
313
|
opacity: 0.7,
|
|
281
314
|
color: 'inherit',
|
|
282
315
|
}}
|
|
283
|
-
onClick={(e) => {
|
|
284
|
-
e.stopPropagation();
|
|
285
|
-
toggle();
|
|
286
|
-
}}
|
|
287
|
-
title="Node Actions"
|
|
288
316
|
>
|
|
289
317
|
⋯
|
|
290
|
-
</
|
|
318
|
+
</MenuTriggerButton>
|
|
291
319
|
)}
|
|
292
320
|
>
|
|
293
|
-
{(close) => (
|
|
294
|
-
<div style={{ ...menu.container, position: 'static' }} onClick={(e) => e.stopPropagation()}>
|
|
295
|
-
<button style={menu.item} onClick={() => { handleAddChild(node.id); close(); }}>
|
|
296
|
-
Add Child
|
|
297
|
-
</button>
|
|
298
|
-
<button style={menu.item} onClick={() => { setSelectedId(node.id); onFocusNode?.(node.id); close(); }}>
|
|
299
|
-
Focus Camera
|
|
300
|
-
</button>
|
|
301
|
-
<button style={menu.item} onClick={() => { handleDuplicate(node.id); close(); }}>
|
|
302
|
-
Duplicate
|
|
303
|
-
</button>
|
|
304
|
-
<button style={{ ...menu.item, ...menu.danger }} onClick={() => { handleDelete(node.id); close(); }}>
|
|
305
|
-
Delete
|
|
306
|
-
</button>
|
|
307
|
-
</div>
|
|
308
|
-
)}
|
|
321
|
+
{(close) => renderTreeNodeMenu(node.id, false, close)}
|
|
309
322
|
</Dropdown>
|
|
310
323
|
<button
|
|
311
324
|
style={{
|
|
@@ -331,8 +344,10 @@ export default function EditorTree({
|
|
|
331
344
|
<Dropdown
|
|
332
345
|
placement="bottom-end"
|
|
333
346
|
trigger={({ ref, toggle }) => (
|
|
334
|
-
<
|
|
335
|
-
|
|
347
|
+
<MenuTriggerButton
|
|
348
|
+
buttonRef={ref}
|
|
349
|
+
onToggle={toggle}
|
|
350
|
+
title="Scene Actions"
|
|
336
351
|
style={{
|
|
337
352
|
background: 'none',
|
|
338
353
|
border: 'none',
|
|
@@ -342,26 +357,12 @@ export default function EditorTree({
|
|
|
342
357
|
opacity: 0.7,
|
|
343
358
|
color: 'inherit',
|
|
344
359
|
}}
|
|
345
|
-
onClick={(e) => {
|
|
346
|
-
e.stopPropagation();
|
|
347
|
-
toggle();
|
|
348
|
-
}}
|
|
349
|
-
title="Scene Actions"
|
|
350
360
|
>
|
|
351
361
|
⋯
|
|
352
|
-
</
|
|
362
|
+
</MenuTriggerButton>
|
|
353
363
|
)}
|
|
354
364
|
>
|
|
355
|
-
{(close) => (
|
|
356
|
-
<div style={{ ...menu.container, position: 'static' }} onClick={(e) => e.stopPropagation()}>
|
|
357
|
-
<button style={menu.item} onClick={() => { handleAddChild(node.id); close(); }}>
|
|
358
|
-
Add Child
|
|
359
|
-
</button>
|
|
360
|
-
<button style={menu.item} onClick={() => { setSelectedId(node.id); onFocusNode?.(node.id); close(); }}>
|
|
361
|
-
Focus Camera
|
|
362
|
-
</button>
|
|
363
|
-
</div>
|
|
364
|
-
)}
|
|
365
|
+
{(close) => renderTreeNodeMenu(node.id, true, close)}
|
|
365
366
|
</Dropdown>
|
|
366
367
|
)}
|
|
367
368
|
</div>
|
|
@@ -399,14 +400,14 @@ export default function EditorTree({
|
|
|
399
400
|
<Dropdown
|
|
400
401
|
placement="bottom-end"
|
|
401
402
|
trigger={({ ref, toggle }) => (
|
|
402
|
-
<
|
|
403
|
-
|
|
403
|
+
<MenuTriggerButton
|
|
404
|
+
buttonRef={ref}
|
|
405
|
+
onToggle={toggle}
|
|
406
|
+
title="Menu"
|
|
404
407
|
style={{ ...base.btn, padding: '2px 6px', fontSize: 10 }}
|
|
405
|
-
onClick={(e) => { e.stopPropagation(); toggle(); }}
|
|
406
|
-
title="File"
|
|
407
408
|
>
|
|
408
409
|
⋮
|
|
409
|
-
</
|
|
410
|
+
</MenuTriggerButton>
|
|
410
411
|
)}
|
|
411
412
|
>
|
|
412
413
|
{(close) => (
|
|
@@ -439,83 +440,13 @@ export default function EditorTree({
|
|
|
439
440
|
</>
|
|
440
441
|
)}
|
|
441
442
|
</div>
|
|
443
|
+
<TreeContextMenu
|
|
444
|
+
contextMenu={contextMenu}
|
|
445
|
+
onClose={closeContextMenu}
|
|
446
|
+
>
|
|
447
|
+
{(nodeId, close) => renderTreeNodeMenu(nodeId, nodeId === prefabData.root.id, close)}
|
|
448
|
+
</TreeContextMenu>
|
|
442
449
|
|
|
443
450
|
</>
|
|
444
451
|
);
|
|
445
452
|
}
|
|
446
|
-
|
|
447
|
-
function FileMenu({
|
|
448
|
-
prefabData,
|
|
449
|
-
setPrefabData,
|
|
450
|
-
onClose
|
|
451
|
-
}: {
|
|
452
|
-
prefabData: Prefab;
|
|
453
|
-
setPrefabData: Dispatch<SetStateAction<Prefab>>;
|
|
454
|
-
onClose: () => void;
|
|
455
|
-
}) {
|
|
456
|
-
const { onScreenshot, onExportGLB } = useEditorContext();
|
|
457
|
-
|
|
458
|
-
const handleLoad = async () => {
|
|
459
|
-
const loadedPrefab = await loadJson();
|
|
460
|
-
if (!loadedPrefab) return;
|
|
461
|
-
setPrefabData(loadedPrefab);
|
|
462
|
-
onClose();
|
|
463
|
-
};
|
|
464
|
-
|
|
465
|
-
const handleSave = () => {
|
|
466
|
-
saveJson(prefabData, "prefab");
|
|
467
|
-
onClose();
|
|
468
|
-
};
|
|
469
|
-
|
|
470
|
-
const handleLoadIntoScene = async () => {
|
|
471
|
-
const loadedPrefab = await loadJson();
|
|
472
|
-
if (!loadedPrefab) return;
|
|
473
|
-
|
|
474
|
-
setPrefabData(prev => ({
|
|
475
|
-
...prev,
|
|
476
|
-
root: updateNodeById(prev.root, prev.root.id, root => ({
|
|
477
|
-
...root,
|
|
478
|
-
children: [...(root.children ?? []), regenerateIds(loadedPrefab.root)]
|
|
479
|
-
}))
|
|
480
|
-
}));
|
|
481
|
-
onClose();
|
|
482
|
-
};
|
|
483
|
-
|
|
484
|
-
return (
|
|
485
|
-
<div
|
|
486
|
-
style={{ ...menu.container, position: 'static' }}
|
|
487
|
-
onClick={(e) => e.stopPropagation()}
|
|
488
|
-
>
|
|
489
|
-
<button
|
|
490
|
-
style={menu.item}
|
|
491
|
-
onClick={handleLoad}
|
|
492
|
-
>
|
|
493
|
-
📥 Load Prefab JSON
|
|
494
|
-
</button>
|
|
495
|
-
<button
|
|
496
|
-
style={menu.item}
|
|
497
|
-
onClick={handleSave}
|
|
498
|
-
>
|
|
499
|
-
💾 Save Prefab JSON
|
|
500
|
-
</button>
|
|
501
|
-
<button
|
|
502
|
-
style={menu.item}
|
|
503
|
-
onClick={handleLoadIntoScene}
|
|
504
|
-
>
|
|
505
|
-
📂 Load into Scene
|
|
506
|
-
</button>
|
|
507
|
-
<button
|
|
508
|
-
style={menu.item}
|
|
509
|
-
onClick={() => { onScreenshot?.(); onClose(); }}
|
|
510
|
-
>
|
|
511
|
-
📸 Screenshot
|
|
512
|
-
</button>
|
|
513
|
-
<button
|
|
514
|
-
style={menu.item}
|
|
515
|
-
onClick={() => { onExportGLB?.(); onClose(); }}
|
|
516
|
-
>
|
|
517
|
-
📦 Export GLB
|
|
518
|
-
</button>
|
|
519
|
-
</div>
|
|
520
|
-
);
|
|
521
|
-
}
|