react-three-game 0.0.7 → 0.0.9
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 +27 -0
- package/package.json +1 -1
- package/src/index.ts +15 -0
- package/src/shared/GameCanvas.tsx +48 -0
- package/src/tools/assetviewer/page.tsx +411 -0
- package/src/tools/dragdrop/DragDropLoader.tsx +105 -0
- package/src/tools/dragdrop/modelLoader.ts +65 -0
- package/src/tools/dragdrop/page.tsx +42 -0
- package/src/tools/prefabeditor/EditorTree.tsx +277 -0
- package/src/tools/prefabeditor/EditorUI.tsx +273 -0
- package/src/tools/prefabeditor/EventSystem.tsx +36 -0
- package/src/tools/prefabeditor/InstanceProvider.tsx +326 -0
- package/src/tools/prefabeditor/PrefabEditor.tsx +130 -0
- package/src/tools/prefabeditor/PrefabRoot.tsx +460 -0
- package/src/tools/prefabeditor/components/ComponentRegistry.ts +26 -0
- package/src/tools/prefabeditor/components/GeometryComponent.tsx +43 -0
- package/src/tools/prefabeditor/components/MaterialComponent.tsx +153 -0
- package/src/tools/prefabeditor/components/ModelComponent.tsx +68 -0
- package/src/tools/prefabeditor/components/PhysicsComponent.tsx +47 -0
- package/src/tools/prefabeditor/components/SpotLightComponent.tsx +53 -0
- package/src/tools/prefabeditor/components/TransformComponent.tsx +49 -0
- package/src/tools/prefabeditor/components/index.ts +16 -0
- package/src/tools/prefabeditor/page.tsx +10 -0
- package/src/tools/prefabeditor/types.ts +28 -0
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
import React, { createContext, useContext, useMemo, useRef, useState, useEffect, useCallback } from "react";
|
|
2
|
+
import { Merged } from '@react-three/drei';
|
|
3
|
+
import * as THREE from 'three';
|
|
4
|
+
import { InstancedRigidBodies } from "@react-three/rapier";
|
|
5
|
+
|
|
6
|
+
// --- Types ---
|
|
7
|
+
export type InstanceData = {
|
|
8
|
+
id: string;
|
|
9
|
+
position: [number, number, number];
|
|
10
|
+
rotation: [number, number, number];
|
|
11
|
+
scale: [number, number, number];
|
|
12
|
+
meshPath: string;
|
|
13
|
+
physics?: { type: 'dynamic' | 'fixed' };
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
function arrayEquals(a: number[], b: number[]) {
|
|
17
|
+
if (a === b) return true;
|
|
18
|
+
if (a.length !== b.length) return false;
|
|
19
|
+
for (let i = 0; i < a.length; i++) {
|
|
20
|
+
if (a[i] !== b[i]) return false;
|
|
21
|
+
}
|
|
22
|
+
return true;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function instanceEquals(a: InstanceData, b: InstanceData) {
|
|
26
|
+
return a.id === b.id &&
|
|
27
|
+
a.meshPath === b.meshPath &&
|
|
28
|
+
arrayEquals(a.position, b.position) &&
|
|
29
|
+
arrayEquals(a.rotation, b.rotation) &&
|
|
30
|
+
arrayEquals(a.scale, b.scale) &&
|
|
31
|
+
a.physics?.type === b.physics?.type;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// --- Context ---
|
|
35
|
+
type GameInstanceContextType = {
|
|
36
|
+
addInstance: (instance: InstanceData) => void;
|
|
37
|
+
removeInstance: (id: string) => void;
|
|
38
|
+
instances: InstanceData[];
|
|
39
|
+
meshes: Record<string, THREE.Mesh>;
|
|
40
|
+
instancesMap?: Record<string, React.ComponentType<any>>;
|
|
41
|
+
modelParts?: Record<string, number>;
|
|
42
|
+
};
|
|
43
|
+
const GameInstanceContext = createContext<GameInstanceContextType | null>(null);
|
|
44
|
+
|
|
45
|
+
export function GameInstanceProvider({
|
|
46
|
+
children,
|
|
47
|
+
models
|
|
48
|
+
, onSelect, registerRef
|
|
49
|
+
}: {
|
|
50
|
+
children: React.ReactNode,
|
|
51
|
+
models: { [filename: string]: THREE.Object3D },
|
|
52
|
+
onSelect?: (id: string | null) => void,
|
|
53
|
+
registerRef?: (id: string, obj: THREE.Object3D | null) => void,
|
|
54
|
+
}) {
|
|
55
|
+
const [instances, setInstances] = useState<InstanceData[]>([]);
|
|
56
|
+
|
|
57
|
+
const addInstance = useCallback((instance: InstanceData) => {
|
|
58
|
+
setInstances(prev => {
|
|
59
|
+
const idx = prev.findIndex(i => i.id === instance.id);
|
|
60
|
+
if (idx !== -1) {
|
|
61
|
+
if (instanceEquals(prev[idx], instance)) {
|
|
62
|
+
return prev;
|
|
63
|
+
}
|
|
64
|
+
const copy = [...prev];
|
|
65
|
+
copy[idx] = instance;
|
|
66
|
+
return copy;
|
|
67
|
+
}
|
|
68
|
+
return [...prev, instance];
|
|
69
|
+
});
|
|
70
|
+
}, []);
|
|
71
|
+
|
|
72
|
+
const removeInstance = useCallback((id: string) => {
|
|
73
|
+
setInstances(prev => {
|
|
74
|
+
if (!prev.find(i => i.id === id)) return prev;
|
|
75
|
+
return prev.filter(i => i.id !== id);
|
|
76
|
+
});
|
|
77
|
+
}, []);
|
|
78
|
+
|
|
79
|
+
// Flatten all model meshes once
|
|
80
|
+
const { flatMeshes, modelParts } = useMemo(() => {
|
|
81
|
+
const flatMeshes: Record<string, THREE.Mesh> = {};
|
|
82
|
+
const modelParts: Record<string, number> = {};
|
|
83
|
+
|
|
84
|
+
Object.entries(models).forEach(([modelKey, model]) => {
|
|
85
|
+
const root = model;
|
|
86
|
+
root.updateWorldMatrix(false, true);
|
|
87
|
+
const rootInverse = new THREE.Matrix4().copy(root.matrixWorld).invert();
|
|
88
|
+
|
|
89
|
+
let partIndex = 0;
|
|
90
|
+
|
|
91
|
+
root.traverse((obj: any) => {
|
|
92
|
+
if (obj.isMesh) {
|
|
93
|
+
const geom = obj.geometry.clone();
|
|
94
|
+
|
|
95
|
+
const relativeTransform = obj.matrixWorld.clone().premultiply(rootInverse);
|
|
96
|
+
geom.applyMatrix4(relativeTransform);
|
|
97
|
+
|
|
98
|
+
const partKey = `${modelKey}__${partIndex}`;
|
|
99
|
+
flatMeshes[partKey] = new THREE.Mesh(geom, obj.material);
|
|
100
|
+
partIndex++;
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
modelParts[modelKey] = partIndex;
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
return { flatMeshes, modelParts };
|
|
107
|
+
}, [models]);
|
|
108
|
+
|
|
109
|
+
// Group instances by meshPath + physics type
|
|
110
|
+
const grouped = useMemo(() => {
|
|
111
|
+
const groups: Record<string, { physicsType: string, instances: InstanceData[] }> = {};
|
|
112
|
+
for (const inst of instances) {
|
|
113
|
+
const type = inst.physics?.type || 'none';
|
|
114
|
+
const key = `${inst.meshPath}__${type}`;
|
|
115
|
+
if (!groups[key]) groups[key] = { physicsType: type, instances: [] };
|
|
116
|
+
groups[key].instances.push(inst);
|
|
117
|
+
}
|
|
118
|
+
return groups;
|
|
119
|
+
}, [instances]);
|
|
120
|
+
|
|
121
|
+
return (
|
|
122
|
+
<GameInstanceContext.Provider
|
|
123
|
+
value={{
|
|
124
|
+
addInstance,
|
|
125
|
+
removeInstance,
|
|
126
|
+
instances,
|
|
127
|
+
meshes: flatMeshes,
|
|
128
|
+
modelParts
|
|
129
|
+
}}
|
|
130
|
+
>
|
|
131
|
+
{/* 1) Normal prefab hierarchy: NOT inside any <Merged> */}
|
|
132
|
+
{children}
|
|
133
|
+
|
|
134
|
+
{/* 2) Physics instanced groups: no <Merged>, just InstancedRigidBodies */}
|
|
135
|
+
{Object.entries(grouped).map(([key, group]) => {
|
|
136
|
+
if (group.physicsType === 'none') return null;
|
|
137
|
+
const modelKey = group.instances[0].meshPath;
|
|
138
|
+
const partCount = modelParts[modelKey] || 0;
|
|
139
|
+
if (partCount === 0) return null;
|
|
140
|
+
|
|
141
|
+
return (
|
|
142
|
+
<InstancedRigidGroup
|
|
143
|
+
key={key}
|
|
144
|
+
group={group}
|
|
145
|
+
modelKey={modelKey}
|
|
146
|
+
partCount={partCount}
|
|
147
|
+
flatMeshes={flatMeshes}
|
|
148
|
+
/>
|
|
149
|
+
);
|
|
150
|
+
})}
|
|
151
|
+
|
|
152
|
+
{/* 3) Non-physics instanced visuals: own <Merged> per model */}
|
|
153
|
+
{Object.entries(grouped).map(([key, group]) => {
|
|
154
|
+
if (group.physicsType !== 'none') return null;
|
|
155
|
+
|
|
156
|
+
const modelKey = group.instances[0].meshPath;
|
|
157
|
+
const partCount = modelParts[modelKey] || 0;
|
|
158
|
+
if (partCount === 0) return null;
|
|
159
|
+
|
|
160
|
+
// Restrict meshes to just this model's parts for this Merged
|
|
161
|
+
const meshesForModel: Record<string, THREE.Mesh> = {};
|
|
162
|
+
for (let i = 0; i < partCount; i++) {
|
|
163
|
+
const partKey = `${modelKey}__${i}`;
|
|
164
|
+
meshesForModel[partKey] = flatMeshes[partKey];
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return (
|
|
168
|
+
<Merged
|
|
169
|
+
key={key}
|
|
170
|
+
meshes={meshesForModel}
|
|
171
|
+
castShadow
|
|
172
|
+
receiveShadow
|
|
173
|
+
>
|
|
174
|
+
{(instancesMap: any) => (
|
|
175
|
+
<NonPhysicsInstancedGroup
|
|
176
|
+
modelKey={modelKey}
|
|
177
|
+
group={group}
|
|
178
|
+
partCount={partCount}
|
|
179
|
+
instancesMap={instancesMap}
|
|
180
|
+
onSelect={onSelect}
|
|
181
|
+
registerRef={registerRef}
|
|
182
|
+
/>
|
|
183
|
+
)}
|
|
184
|
+
</Merged>
|
|
185
|
+
);
|
|
186
|
+
})}
|
|
187
|
+
</GameInstanceContext.Provider>
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Physics instancing stays the same
|
|
192
|
+
function InstancedRigidGroup({
|
|
193
|
+
group,
|
|
194
|
+
modelKey,
|
|
195
|
+
partCount,
|
|
196
|
+
flatMeshes
|
|
197
|
+
}: {
|
|
198
|
+
group: { physicsType: string, instances: InstanceData[] },
|
|
199
|
+
modelKey: string,
|
|
200
|
+
partCount: number,
|
|
201
|
+
flatMeshes: Record<string, THREE.Mesh>
|
|
202
|
+
}) {
|
|
203
|
+
const instances = useMemo(
|
|
204
|
+
() => group.instances.map(inst => ({
|
|
205
|
+
key: inst.id,
|
|
206
|
+
position: inst.position,
|
|
207
|
+
rotation: inst.rotation,
|
|
208
|
+
scale: inst.scale,
|
|
209
|
+
})),
|
|
210
|
+
[group.instances]
|
|
211
|
+
);
|
|
212
|
+
|
|
213
|
+
return (
|
|
214
|
+
<InstancedRigidBodies
|
|
215
|
+
instances={instances}
|
|
216
|
+
colliders={group.physicsType === 'fixed' ? 'trimesh' : 'hull'}
|
|
217
|
+
type={group.physicsType as 'dynamic' | 'fixed'}
|
|
218
|
+
>
|
|
219
|
+
{Array.from({ length: partCount }).map((_, i) => {
|
|
220
|
+
const mesh = flatMeshes[`${modelKey}__${i}`];
|
|
221
|
+
return (
|
|
222
|
+
<instancedMesh
|
|
223
|
+
key={i}
|
|
224
|
+
args={[mesh.geometry, mesh.material, group.instances.length]}
|
|
225
|
+
castShadow
|
|
226
|
+
receiveShadow
|
|
227
|
+
frustumCulled={false}
|
|
228
|
+
/>
|
|
229
|
+
);
|
|
230
|
+
})}
|
|
231
|
+
</InstancedRigidBodies>
|
|
232
|
+
);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// Non-physics instanced visuals: per-instance group using Merged's Instance components
|
|
236
|
+
function NonPhysicsInstancedGroup({
|
|
237
|
+
modelKey,
|
|
238
|
+
group,
|
|
239
|
+
partCount,
|
|
240
|
+
instancesMap
|
|
241
|
+
, onSelect, registerRef
|
|
242
|
+
}: {
|
|
243
|
+
modelKey: string;
|
|
244
|
+
group: { physicsType: string, instances: InstanceData[] };
|
|
245
|
+
partCount: number;
|
|
246
|
+
instancesMap: Record<string, React.ComponentType<any>>;
|
|
247
|
+
onSelect?: (id: string | null) => void;
|
|
248
|
+
registerRef?: (id: string, obj: THREE.Object3D | null) => void;
|
|
249
|
+
}) {
|
|
250
|
+
const clickValid = useRef(false);
|
|
251
|
+
const handlePointerDown = (e: any) => { e.stopPropagation(); clickValid.current = true; };
|
|
252
|
+
const handlePointerMove = () => { if (clickValid.current) clickValid.current = false; };
|
|
253
|
+
const handlePointerUp = (e: any, id: string) => {
|
|
254
|
+
if (clickValid.current) {
|
|
255
|
+
e.stopPropagation();
|
|
256
|
+
onSelect?.(id);
|
|
257
|
+
}
|
|
258
|
+
clickValid.current = false;
|
|
259
|
+
};
|
|
260
|
+
|
|
261
|
+
return (
|
|
262
|
+
<>
|
|
263
|
+
{group.instances.map(inst => (
|
|
264
|
+
<group
|
|
265
|
+
key={inst.id}
|
|
266
|
+
ref={(el) => { registerRef?.(inst.id, el as unknown as THREE.Object3D | null); }}
|
|
267
|
+
position={inst.position}
|
|
268
|
+
rotation={inst.rotation}
|
|
269
|
+
scale={inst.scale}
|
|
270
|
+
onPointerDown={handlePointerDown}
|
|
271
|
+
onPointerMove={handlePointerMove}
|
|
272
|
+
onPointerUp={(e) => handlePointerUp(e, inst.id)}
|
|
273
|
+
>
|
|
274
|
+
{Array.from({ length: partCount }).map((_, i) => {
|
|
275
|
+
const Instance = instancesMap[`${modelKey}__${i}`];
|
|
276
|
+
if (!Instance) return null;
|
|
277
|
+
return <Instance key={i} />;
|
|
278
|
+
})}
|
|
279
|
+
</group>
|
|
280
|
+
))}
|
|
281
|
+
</>
|
|
282
|
+
);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
// --- GameInstance: just registers an instance, renders nothing ---
|
|
287
|
+
export const GameInstance = React.forwardRef<THREE.Group, {
|
|
288
|
+
id: string;
|
|
289
|
+
modelUrl: string;
|
|
290
|
+
position: [number, number, number];
|
|
291
|
+
rotation: [number, number, number];
|
|
292
|
+
scale: [number, number, number];
|
|
293
|
+
physics?: { type: 'dynamic' | 'fixed' };
|
|
294
|
+
}>(({
|
|
295
|
+
id,
|
|
296
|
+
modelUrl,
|
|
297
|
+
position,
|
|
298
|
+
rotation,
|
|
299
|
+
scale,
|
|
300
|
+
physics = undefined,
|
|
301
|
+
}, ref) => {
|
|
302
|
+
const ctx = useContext(GameInstanceContext);
|
|
303
|
+
const addInstance = ctx?.addInstance;
|
|
304
|
+
const removeInstance = ctx?.removeInstance;
|
|
305
|
+
|
|
306
|
+
const instance = useMemo<InstanceData>(() => ({
|
|
307
|
+
id,
|
|
308
|
+
meshPath: modelUrl,
|
|
309
|
+
position,
|
|
310
|
+
rotation,
|
|
311
|
+
scale,
|
|
312
|
+
physics,
|
|
313
|
+
}), [id, modelUrl, position, rotation, scale, physics]);
|
|
314
|
+
|
|
315
|
+
useEffect(() => {
|
|
316
|
+
if (!addInstance || !removeInstance) return;
|
|
317
|
+
addInstance(instance);
|
|
318
|
+
return () => {
|
|
319
|
+
removeInstance(instance.id);
|
|
320
|
+
};
|
|
321
|
+
}, [addInstance, removeInstance, instance]);
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
// No visual here – provider will render visuals for all instances
|
|
325
|
+
return null;
|
|
326
|
+
});
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import GameCanvas from "../../shared/GameCanvas";
|
|
4
|
+
import { useState, useRef, } from "react";
|
|
5
|
+
import { Group, } from "three";
|
|
6
|
+
import { Prefab, } from "./types";
|
|
7
|
+
import PrefabRoot from "./PrefabRoot";
|
|
8
|
+
import { Physics } from "@react-three/rapier";
|
|
9
|
+
import EditorUI from "./EditorUI";
|
|
10
|
+
|
|
11
|
+
const PrefabEditor = ({ basePath, initialPrefab, children }: { basePath?: string, initialPrefab?: Prefab, children?: React.ReactNode }) => {
|
|
12
|
+
const [editMode, setEditMode] = useState(true);
|
|
13
|
+
const [loadedPrefab, setLoadedPrefab] = useState<Prefab>(initialPrefab ?? {
|
|
14
|
+
"id": "prefab-default",
|
|
15
|
+
"name": "New Prefab",
|
|
16
|
+
"root": {
|
|
17
|
+
"id": "root",
|
|
18
|
+
"enabled": true,
|
|
19
|
+
"visible": true,
|
|
20
|
+
"components": {
|
|
21
|
+
"transform": {
|
|
22
|
+
"type": "Transform",
|
|
23
|
+
"properties": {
|
|
24
|
+
"position": [0, 0, 0],
|
|
25
|
+
"rotation": [0, 0, 0],
|
|
26
|
+
"scale": [1, 1, 1]
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
const [selectedId, setSelectedId] = useState<string | null>(null);
|
|
33
|
+
const [transformMode, setTransformMode] = useState<"translate" | "rotate" | "scale">("translate");
|
|
34
|
+
const prefabRef = useRef<Group>(null);
|
|
35
|
+
|
|
36
|
+
return <>
|
|
37
|
+
<GameCanvas>
|
|
38
|
+
<Physics paused={editMode}>
|
|
39
|
+
<ambientLight intensity={1.5} />
|
|
40
|
+
<gridHelper args={[10, 10]} position={[0, -1, 0]} />
|
|
41
|
+
<PrefabRoot
|
|
42
|
+
data={loadedPrefab}
|
|
43
|
+
ref={prefabRef}
|
|
44
|
+
|
|
45
|
+
// props for edit mode
|
|
46
|
+
editMode={editMode}
|
|
47
|
+
onPrefabChange={setLoadedPrefab}
|
|
48
|
+
selectedId={selectedId}
|
|
49
|
+
onSelect={setSelectedId}
|
|
50
|
+
transformMode={transformMode}
|
|
51
|
+
setTransformMode={setTransformMode}
|
|
52
|
+
basePath={basePath}
|
|
53
|
+
/>
|
|
54
|
+
{children}
|
|
55
|
+
</Physics>
|
|
56
|
+
</GameCanvas>
|
|
57
|
+
|
|
58
|
+
<div style={{ position: "absolute", top: "0.5rem", left: "50%", transform: "translateX(-50%)" }} className="bg-black/70 backdrop-blur-sm border border-cyan-500/30 px-2 py-1 flex items-center gap-1">
|
|
59
|
+
<button
|
|
60
|
+
className="px-1 py-0.5 text-[10px] font-mono text-cyan-300 hover:bg-cyan-500/20 border border-cyan-500/30"
|
|
61
|
+
onClick={() => setEditMode(!editMode)}
|
|
62
|
+
>
|
|
63
|
+
{editMode ? "▶" : "⏸"}
|
|
64
|
+
</button>
|
|
65
|
+
<span className="text-cyan-500/30 text-[10px]">|</span>
|
|
66
|
+
<button
|
|
67
|
+
className="px-1 py-0.5 text-[10px] font-mono text-cyan-300 hover:bg-cyan-500/20 border border-cyan-500/30"
|
|
68
|
+
onClick={async () => {
|
|
69
|
+
const prefab = await loadJson();
|
|
70
|
+
if (prefab) setLoadedPrefab(prefab);
|
|
71
|
+
}}
|
|
72
|
+
>
|
|
73
|
+
📥
|
|
74
|
+
</button>
|
|
75
|
+
<button
|
|
76
|
+
className="px-1 py-0.5 text-[10px] font-mono text-cyan-300 hover:bg-cyan-500/20 border border-cyan-500/30"
|
|
77
|
+
onClick={() => saveJson(loadedPrefab, "prefab")}
|
|
78
|
+
>
|
|
79
|
+
💾
|
|
80
|
+
</button>
|
|
81
|
+
</div>
|
|
82
|
+
{editMode && <EditorUI
|
|
83
|
+
prefabData={loadedPrefab}
|
|
84
|
+
setPrefabData={setLoadedPrefab}
|
|
85
|
+
selectedId={selectedId}
|
|
86
|
+
setSelectedId={setSelectedId}
|
|
87
|
+
transformMode={transformMode}
|
|
88
|
+
setTransformMode={setTransformMode}
|
|
89
|
+
basePath={basePath}
|
|
90
|
+
/>}
|
|
91
|
+
</>
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const saveJson = (data: any, filename: string) => {
|
|
95
|
+
const dataStr = "data:text/json;charset=utf-8," + encodeURIComponent(JSON.stringify(data, null, 2));
|
|
96
|
+
const downloadAnchorNode = document.createElement('a');
|
|
97
|
+
downloadAnchorNode.setAttribute("href", dataStr);
|
|
98
|
+
downloadAnchorNode.setAttribute("download", (filename || 'prefab') + ".json");
|
|
99
|
+
document.body.appendChild(downloadAnchorNode);
|
|
100
|
+
downloadAnchorNode.click();
|
|
101
|
+
downloadAnchorNode.remove();
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
const loadJson = async () => {
|
|
105
|
+
return new Promise<Prefab | undefined>((resolve) => {
|
|
106
|
+
const input = document.createElement('input');
|
|
107
|
+
input.type = 'file';
|
|
108
|
+
input.accept = '.json,application/json';
|
|
109
|
+
input.onchange = e => {
|
|
110
|
+
const file = (e.target as HTMLInputElement).files?.[0];
|
|
111
|
+
if (!file) return resolve(undefined);
|
|
112
|
+
const reader = new FileReader();
|
|
113
|
+
reader.onload = e => {
|
|
114
|
+
try {
|
|
115
|
+
const text = e.target?.result;
|
|
116
|
+
if (typeof text === 'string') {
|
|
117
|
+
const json = JSON.parse(text);
|
|
118
|
+
resolve(json as Prefab);
|
|
119
|
+
}
|
|
120
|
+
} catch (err) {
|
|
121
|
+
console.error('Error parsing prefab JSON:', err);
|
|
122
|
+
resolve(undefined);
|
|
123
|
+
}
|
|
124
|
+
};
|
|
125
|
+
reader.readAsText(file);
|
|
126
|
+
};
|
|
127
|
+
input.click();
|
|
128
|
+
});
|
|
129
|
+
};
|
|
130
|
+
export default PrefabEditor;
|