react-three-game 0.0.58 → 0.0.60
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 +43 -32
- 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 +96 -41
- package/dist/tools/dragdrop/index.d.ts +4 -0
- package/dist/tools/dragdrop/index.js +2 -0
- package/dist/tools/dragdrop/modelLoader.d.ts +14 -6
- package/dist/tools/dragdrop/modelLoader.js +99 -47
- package/dist/tools/dragdrop/page.js +3 -3
- package/dist/tools/prefabeditor/PrefabEditor.js +17 -26
- package/dist/tools/prefabeditor/PrefabRoot.d.ts +1 -1
- package/dist/tools/prefabeditor/PrefabRoot.js +7 -11
- package/package.json +2 -2
- package/react-three-game-skill/react-three-game/SKILL.md +59 -4
- 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 +142 -56
- package/src/tools/dragdrop/index.ts +4 -0
- package/src/tools/dragdrop/modelLoader.ts +153 -49
- package/src/tools/dragdrop/page.tsx +7 -4
- package/src/tools/prefabeditor/PrefabEditor.tsx +17 -23
- package/src/tools/prefabeditor/PrefabRoot.tsx +9 -12
|
@@ -1,14 +1,24 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { Object3D, Texture } from "three";
|
|
2
|
+
import { SRGBColorSpace, TextureLoader } from "three";
|
|
3
|
+
import { DRACOLoader, FBXLoader, GLTFLoader } from "three/examples/jsm/Addons.js";
|
|
4
|
+
|
|
5
|
+
export type LoadedModel = Object3D;
|
|
6
|
+
export type LoadedTexture = Texture;
|
|
2
7
|
|
|
3
8
|
export type ModelLoadResult = {
|
|
4
9
|
success: boolean;
|
|
5
|
-
model?:
|
|
6
|
-
error?:
|
|
10
|
+
model?: LoadedModel;
|
|
11
|
+
error?: unknown;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export type TextureLoadResult = {
|
|
15
|
+
success: boolean;
|
|
16
|
+
texture?: LoadedTexture;
|
|
17
|
+
error?: unknown;
|
|
7
18
|
};
|
|
8
19
|
|
|
9
20
|
export type ProgressCallback = (filename: string, loaded: number, total: number) => void;
|
|
10
21
|
|
|
11
|
-
// Singleton loader instances
|
|
12
22
|
const dracoLoader = new DRACOLoader();
|
|
13
23
|
dracoLoader.setDecoderPath("https://www.gstatic.com/draco/v1/decoders/");
|
|
14
24
|
|
|
@@ -16,84 +26,178 @@ const gltfLoader = new GLTFLoader();
|
|
|
16
26
|
gltfLoader.setDRACOLoader(dracoLoader);
|
|
17
27
|
|
|
18
28
|
const fbxLoader = new FBXLoader();
|
|
29
|
+
const textureLoader = new TextureLoader();
|
|
30
|
+
|
|
31
|
+
type ModelFileKind = "gltf" | "fbx";
|
|
32
|
+
const TEXTURE_FILE_EXTENSIONS = [".png", ".jpg", ".jpeg", ".webp", ".gif", ".bmp", ".svg"] as const;
|
|
33
|
+
|
|
34
|
+
function normalizeModelPath(name: string) {
|
|
35
|
+
return name.split(/[?#]/, 1)[0].toLowerCase();
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function getModelFileKind(name: string): ModelFileKind | null {
|
|
39
|
+
const normalizedName = normalizeModelPath(name);
|
|
40
|
+
|
|
41
|
+
if (normalizedName.endsWith(".glb") || normalizedName.endsWith(".gltf")) {
|
|
42
|
+
return "gltf";
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (normalizedName.endsWith(".fbx")) {
|
|
46
|
+
return "fbx";
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function canParseModelFile(file: File | string) {
|
|
53
|
+
const filename = typeof file === "string" ? file : file.name;
|
|
54
|
+
return getModelFileKind(filename) !== null;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function canParseTextureFile(file: File | string) {
|
|
58
|
+
const filename = typeof file === "string" ? file : file.name;
|
|
59
|
+
const normalizedName = normalizeModelPath(filename);
|
|
60
|
+
|
|
61
|
+
return TEXTURE_FILE_EXTENSIONS.some(extension => normalizedName.endsWith(extension));
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function parseModelBuffer(arrayBuffer: ArrayBuffer, sourceName: string): Promise<ModelLoadResult> {
|
|
65
|
+
const modelFileKind = getModelFileKind(sourceName);
|
|
66
|
+
|
|
67
|
+
if (modelFileKind === "gltf") {
|
|
68
|
+
return new Promise(resolve => {
|
|
69
|
+
gltfLoader.parse(
|
|
70
|
+
arrayBuffer,
|
|
71
|
+
"",
|
|
72
|
+
gltf => {
|
|
73
|
+
resolve({ success: true, model: gltf.scene });
|
|
74
|
+
},
|
|
75
|
+
error => {
|
|
76
|
+
resolve({ success: false, error });
|
|
77
|
+
},
|
|
78
|
+
);
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (modelFileKind === "fbx") {
|
|
83
|
+
try {
|
|
84
|
+
const model = fbxLoader.parse(arrayBuffer, "");
|
|
85
|
+
return Promise.resolve({ success: true, model });
|
|
86
|
+
} catch (error) {
|
|
87
|
+
return Promise.resolve({ success: false, error });
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return Promise.resolve({ success: false, error: new Error(`Unsupported file format: ${sourceName}`) });
|
|
92
|
+
}
|
|
19
93
|
|
|
20
|
-
/**
|
|
21
|
-
* Parse a model from a File object (e.g. from drag-drop or file picker).
|
|
22
|
-
* Returns the parsed Three.js Object3D scene.
|
|
23
|
-
*/
|
|
24
94
|
export function parseModelFromFile(file: File): Promise<ModelLoadResult> {
|
|
25
|
-
return new Promise(
|
|
95
|
+
return new Promise(resolve => {
|
|
26
96
|
const reader = new FileReader();
|
|
27
|
-
|
|
97
|
+
|
|
98
|
+
reader.onload = event => {
|
|
28
99
|
const arrayBuffer = event.target?.result as ArrayBuffer;
|
|
100
|
+
|
|
29
101
|
if (!arrayBuffer) {
|
|
30
|
-
resolve({ success: false, error: new Error(
|
|
102
|
+
resolve({ success: false, error: new Error("Failed to read file") });
|
|
31
103
|
return;
|
|
32
104
|
}
|
|
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
|
-
}
|
|
105
|
+
|
|
106
|
+
void parseModelBuffer(arrayBuffer, file.name).then(resolve);
|
|
50
107
|
};
|
|
108
|
+
|
|
51
109
|
reader.onerror = () => resolve({ success: false, error: reader.error });
|
|
52
110
|
reader.readAsArrayBuffer(file);
|
|
53
111
|
});
|
|
54
112
|
}
|
|
55
113
|
|
|
114
|
+
export function parseTextureFromFile(file: File): Promise<TextureLoadResult> {
|
|
115
|
+
return new Promise(resolve => {
|
|
116
|
+
const url = URL.createObjectURL(file);
|
|
117
|
+
|
|
118
|
+
textureLoader.load(
|
|
119
|
+
url,
|
|
120
|
+
texture => {
|
|
121
|
+
texture.colorSpace = SRGBColorSpace;
|
|
122
|
+
resolve({ success: true, texture });
|
|
123
|
+
URL.revokeObjectURL(url);
|
|
124
|
+
},
|
|
125
|
+
undefined,
|
|
126
|
+
error => {
|
|
127
|
+
resolve({ success: false, error });
|
|
128
|
+
URL.revokeObjectURL(url);
|
|
129
|
+
},
|
|
130
|
+
);
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
|
|
56
134
|
export async function loadModel(
|
|
57
135
|
filename: string,
|
|
58
|
-
onProgress?: ProgressCallback
|
|
136
|
+
onProgress?: ProgressCallback,
|
|
59
137
|
): Promise<ModelLoadResult> {
|
|
60
138
|
try {
|
|
61
|
-
// Use filename directly (should already include leading /)
|
|
62
139
|
const fullPath = filename;
|
|
140
|
+
const modelFileKind = getModelFileKind(filename);
|
|
63
141
|
|
|
64
|
-
if (
|
|
65
|
-
return new Promise(
|
|
142
|
+
if (modelFileKind === "gltf") {
|
|
143
|
+
return new Promise(resolve => {
|
|
66
144
|
gltfLoader.load(
|
|
67
145
|
fullPath,
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
if (onProgress) {
|
|
71
|
-
|
|
72
|
-
const total = progressEvent.total || progressEvent.loaded;
|
|
73
|
-
onProgress(filename, progressEvent.loaded, total);
|
|
146
|
+
gltf => resolve({ success: true, model: gltf.scene }),
|
|
147
|
+
progressEvent => {
|
|
148
|
+
if (!onProgress) {
|
|
149
|
+
return;
|
|
74
150
|
}
|
|
151
|
+
|
|
152
|
+
const total = progressEvent.total || progressEvent.loaded;
|
|
153
|
+
onProgress(filename, progressEvent.loaded, total);
|
|
75
154
|
},
|
|
76
|
-
|
|
155
|
+
error => resolve({ success: false, error }),
|
|
77
156
|
);
|
|
78
157
|
});
|
|
79
|
-
}
|
|
80
|
-
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (modelFileKind === "fbx") {
|
|
161
|
+
return new Promise(resolve => {
|
|
81
162
|
fbxLoader.load(
|
|
82
163
|
fullPath,
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
if (onProgress) {
|
|
86
|
-
|
|
87
|
-
const total = progressEvent.total || progressEvent.loaded;
|
|
88
|
-
onProgress(filename, progressEvent.loaded, total);
|
|
164
|
+
model => resolve({ success: true, model }),
|
|
165
|
+
progressEvent => {
|
|
166
|
+
if (!onProgress) {
|
|
167
|
+
return;
|
|
89
168
|
}
|
|
169
|
+
|
|
170
|
+
const total = progressEvent.total || progressEvent.loaded;
|
|
171
|
+
onProgress(filename, progressEvent.loaded, total);
|
|
90
172
|
},
|
|
91
|
-
|
|
173
|
+
error => resolve({ success: false, error }),
|
|
92
174
|
);
|
|
93
175
|
});
|
|
94
|
-
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return { success: false, error: new Error(`Unsupported file format: ${filename}`) };
|
|
179
|
+
} catch (error) {
|
|
180
|
+
return { success: false, error };
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
export async function loadTexture(filename: string): Promise<TextureLoadResult> {
|
|
185
|
+
try {
|
|
186
|
+
if (!canParseTextureFile(filename)) {
|
|
95
187
|
return { success: false, error: new Error(`Unsupported file format: ${filename}`) };
|
|
96
188
|
}
|
|
189
|
+
|
|
190
|
+
return await new Promise(resolve => {
|
|
191
|
+
textureLoader.load(
|
|
192
|
+
filename,
|
|
193
|
+
texture => {
|
|
194
|
+
texture.colorSpace = SRGBColorSpace;
|
|
195
|
+
resolve({ success: true, texture });
|
|
196
|
+
},
|
|
197
|
+
undefined,
|
|
198
|
+
error => resolve({ success: false, error }),
|
|
199
|
+
);
|
|
200
|
+
});
|
|
97
201
|
} catch (error) {
|
|
98
202
|
return { success: false, error };
|
|
99
203
|
}
|
|
@@ -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
|
}
|
|
@@ -7,7 +7,7 @@ import EditorUI from "./EditorUI";
|
|
|
7
7
|
import { base, toolbar } from "./styles";
|
|
8
8
|
import { EditorContext } from "./EditorContext";
|
|
9
9
|
import { exportGLB, createModelNode, createImageNode } from "./utils";
|
|
10
|
-
import {
|
|
10
|
+
import { loadFiles } from "../dragdrop";
|
|
11
11
|
|
|
12
12
|
export interface PrefabEditorRef {
|
|
13
13
|
screenshot: () => void;
|
|
@@ -135,25 +135,21 @@ const PrefabEditor = forwardRef<PrefabEditorRef, {
|
|
|
135
135
|
|
|
136
136
|
// --- Drag & drop files to add nodes ---
|
|
137
137
|
useEffect(() => {
|
|
138
|
-
const IMAGE_EXTS = ['png', 'jpg', 'jpeg', 'webp', 'gif', 'bmp', 'svg'];
|
|
139
|
-
const MODEL_EXTS = ['glb', 'gltf', 'fbx'];
|
|
140
|
-
|
|
141
138
|
function handleDragOver(e: DragEvent) {
|
|
142
139
|
e.preventDefault();
|
|
143
140
|
e.stopPropagation();
|
|
144
141
|
}
|
|
142
|
+
|
|
145
143
|
function handleDrop(e: DragEvent) {
|
|
146
144
|
e.preventDefault();
|
|
147
145
|
e.stopPropagation();
|
|
148
|
-
const files = e.dataTransfer?.files ? Array.from(e.dataTransfer.files) : [];
|
|
149
|
-
files.forEach(file => {
|
|
150
|
-
const ext = file.name.split('.').pop()?.toLowerCase();
|
|
151
|
-
if (!ext) return;
|
|
152
146
|
|
|
153
|
-
|
|
147
|
+
const files = e.dataTransfer?.files ? Array.from(e.dataTransfer.files) : [];
|
|
154
148
|
|
|
155
|
-
|
|
156
|
-
|
|
149
|
+
void loadFiles(files, {
|
|
150
|
+
onModelLoaded: (model, filename) => {
|
|
151
|
+
const modelPath = `models/${filename}`;
|
|
152
|
+
const baseName = filename.replace(/\.[^.]+$/, '');
|
|
157
153
|
const newNode = createModelNode(modelPath, baseName);
|
|
158
154
|
|
|
159
155
|
updatePrefab(prev => ({
|
|
@@ -161,15 +157,11 @@ const PrefabEditor = forwardRef<PrefabEditorRef, {
|
|
|
161
157
|
root: { ...prev.root, children: [...(prev.root.children ?? []), newNode] }
|
|
162
158
|
}));
|
|
163
159
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
}
|
|
170
|
-
});
|
|
171
|
-
} else if (IMAGE_EXTS.includes(ext)) {
|
|
172
|
-
const texturePath = `textures/${file.name}`;
|
|
160
|
+
prefabRootRef.current?.injectModel(modelPath, model);
|
|
161
|
+
},
|
|
162
|
+
onTextureLoaded: (texture, filename) => {
|
|
163
|
+
const texturePath = `textures/${filename}`;
|
|
164
|
+
const baseName = filename.replace(/\.[^.]+$/, '');
|
|
173
165
|
const newNode = createImageNode(texturePath, baseName);
|
|
174
166
|
|
|
175
167
|
updatePrefab(prev => ({
|
|
@@ -177,9 +169,11 @@ const PrefabEditor = forwardRef<PrefabEditorRef, {
|
|
|
177
169
|
root: { ...prev.root, children: [...(prev.root.children ?? []), newNode] }
|
|
178
170
|
}));
|
|
179
171
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
172
|
+
prefabRootRef.current?.injectTexture(texturePath, texture);
|
|
173
|
+
},
|
|
174
|
+
onLoadError: error => {
|
|
175
|
+
console.error('Drop asset error:', error);
|
|
176
|
+
},
|
|
183
177
|
});
|
|
184
178
|
}
|
|
185
179
|
|
|
@@ -6,7 +6,7 @@ import { ThreeEvent } from "@react-three/fiber";
|
|
|
6
6
|
import { Prefab, GameObject as GameObjectType } from "./types";
|
|
7
7
|
import { getComponent, registerComponent, getNonComposableKeys } from "./components/ComponentRegistry";
|
|
8
8
|
import components from "./components";
|
|
9
|
-
import { loadModel } from "../dragdrop
|
|
9
|
+
import { loadModel } from "../dragdrop";
|
|
10
10
|
import { GameInstance, GameInstanceProvider, useInstanceCheck } from "./InstanceProvider";
|
|
11
11
|
import { focusCameraOnObject, updateNode } from "./utils";
|
|
12
12
|
import { PhysicsProps } from "./components/PhysicsComponent";
|
|
@@ -23,7 +23,7 @@ export interface PrefabRootRef {
|
|
|
23
23
|
root: Group | null;
|
|
24
24
|
rigidBodyRefs: Map<string, any>; // RigidBody refs only populated when using physics
|
|
25
25
|
injectModel: (filename: string, model: Object3D) => void;
|
|
26
|
-
injectTexture: (filename: string,
|
|
26
|
+
injectTexture: (filename: string, texture: Texture) => void;
|
|
27
27
|
focusNode: (nodeId: string) => void;
|
|
28
28
|
}
|
|
29
29
|
|
|
@@ -59,15 +59,9 @@ export const PrefabRoot = forwardRef<PrefabRootRef, {
|
|
|
59
59
|
setModels(m => ({ ...m, [filename]: model }));
|
|
60
60
|
}, []);
|
|
61
61
|
|
|
62
|
-
const injectTexture = useCallback((filename: string,
|
|
62
|
+
const injectTexture = useCallback((filename: string, texture: Texture) => {
|
|
63
63
|
loading.current.add(filename);
|
|
64
|
-
|
|
65
|
-
const loader = new TextureLoader();
|
|
66
|
-
loader.load(url, tex => {
|
|
67
|
-
tex.colorSpace = SRGBColorSpace;
|
|
68
|
-
setTextures(t => ({ ...t, [filename]: tex }));
|
|
69
|
-
URL.revokeObjectURL(url);
|
|
70
|
-
}, undefined, () => URL.revokeObjectURL(url));
|
|
64
|
+
setTextures(t => ({ ...t, [filename]: texture }));
|
|
71
65
|
}, []);
|
|
72
66
|
|
|
73
67
|
useImperativeHandle(ref, () => ({
|
|
@@ -155,8 +149,11 @@ export const PrefabRoot = forwardRef<PrefabRootRef, {
|
|
|
155
149
|
: `${basePath}/${file}`;
|
|
156
150
|
|
|
157
151
|
const res = await loadModel(path);
|
|
158
|
-
|
|
159
|
-
|
|
152
|
+
const model = res.model;
|
|
153
|
+
|
|
154
|
+
if (res.success && model) {
|
|
155
|
+
setModels(m => ({ ...m, [file]: model }));
|
|
156
|
+
}
|
|
160
157
|
});
|
|
161
158
|
|
|
162
159
|
const loader = new TextureLoader();
|