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.
@@ -1,14 +1,24 @@
1
- import { GLTFLoader, FBXLoader, DRACOLoader } from "three/examples/jsm/Addons.js";
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?: any;
6
- error?: any;
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((resolve) => {
95
+ return new Promise(resolve => {
26
96
  const reader = new FileReader();
27
- reader.onload = (event) => {
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('Failed to read file') });
102
+ resolve({ success: false, error: new Error("Failed to read file") });
31
103
  return;
32
104
  }
33
- const name = file.name.toLowerCase();
34
- if (name.endsWith('.glb') || name.endsWith('.gltf')) {
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 (filename.endsWith('.glb') || filename.endsWith('.gltf')) {
65
- return new Promise((resolve) => {
142
+ if (modelFileKind === "gltf") {
143
+ return new Promise(resolve => {
66
144
  gltfLoader.load(
67
145
  fullPath,
68
- (gltf) => resolve({ success: true, model: gltf.scene }),
69
- (progressEvent) => {
70
- if (onProgress) {
71
- // Use loaded as total if total is not available
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
- (error) => resolve({ success: false, error })
155
+ error => resolve({ success: false, error }),
77
156
  );
78
157
  });
79
- } else if (filename.endsWith('.fbx')) {
80
- return new Promise((resolve) => {
158
+ }
159
+
160
+ if (modelFileKind === "fbx") {
161
+ return new Promise(resolve => {
81
162
  fbxLoader.load(
82
163
  fullPath,
83
- (model) => resolve({ success: true, model }),
84
- (progressEvent) => {
85
- if (onProgress) {
86
- // Use loaded as total if total is not available
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
- (error) => resolve({ success: false, error })
173
+ error => resolve({ success: false, error }),
92
174
  );
93
175
  });
94
- } else {
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 "./DragDropLoader";
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
- <DragDropLoader onModelLoaded={model => setModels(prev => [...prev, model])} />
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 { parseModelFromFile } from "../dragdrop/modelLoader";
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
- const baseName = file.name.replace(/\.[^.]+$/, '');
147
+ const files = e.dataTransfer?.files ? Array.from(e.dataTransfer.files) : [];
154
148
 
155
- if (MODEL_EXTS.includes(ext)) {
156
- const modelPath = `models/${file.name}`;
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
- parseModelFromFile(file).then(result => {
165
- if (result.success && result.model) {
166
- prefabRootRef.current?.injectModel(modelPath, result.model);
167
- } else {
168
- console.error('Drop parse error:', result.error);
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
- // Inject a blob URL texture so it renders immediately
181
- prefabRootRef.current?.injectTexture(texturePath, file);
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/modelLoader";
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, file: File) => void;
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, file: File) => {
62
+ const injectTexture = useCallback((filename: string, texture: Texture) => {
63
63
  loading.current.add(filename);
64
- const url = URL.createObjectURL(file);
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
- res.success && res.model &&
159
- setModels(m => ({ ...m, [file]: res.model }));
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();