react-three-game 0.0.59 → 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,22 +1,22 @@
1
1
  import type { HTMLAttributes, ReactNode } from "react";
2
- import type { LoadedModel } from "./modelLoader";
3
- export interface FileLoadOptions {
2
+ import type { LoadedModel, LoadedTexture } from "./modelLoader";
3
+ export interface AssetLoadOptions {
4
4
  onModelLoaded?: (model: LoadedModel, filename: string, file: File) => void | Promise<void>;
5
- onFileLoaded?: (file: File) => void | Promise<void>;
5
+ onTextureLoaded?: (texture: LoadedTexture, filename: string, file: File) => void | Promise<void>;
6
+ onUnhandledFile?: (file: File) => void | Promise<void>;
6
7
  onFilesLoaded?: (files: File[]) => void | Promise<void>;
7
- onModelError?: (error: unknown, filename: string, file: File) => void | Promise<void>;
8
- parseModels?: boolean;
8
+ onLoadError?: (error: unknown, filename: string, file: File) => void | Promise<void>;
9
9
  }
10
10
  type DivProps = Omit<HTMLAttributes<HTMLDivElement>, "children" | "onDrop" | "onDragOver">;
11
- export interface DragDropLoaderProps extends FileLoadOptions, DivProps {
11
+ export interface DragDropLoaderProps extends AssetLoadOptions, DivProps {
12
12
  children?: ReactNode;
13
13
  }
14
- export interface FilePickerProps extends FileLoadOptions, DivProps {
14
+ export interface FilePickerProps extends AssetLoadOptions, DivProps {
15
15
  accept?: string;
16
16
  children?: ReactNode;
17
17
  multiple?: boolean;
18
18
  }
19
- export declare function loadFiles(files: File[], { onModelLoaded, onFileLoaded, onFilesLoaded, onModelError, parseModels }: FileLoadOptions): Promise<void>;
19
+ export declare function loadFiles(files: File[], { onModelLoaded, onTextureLoaded, onUnhandledFile, onFilesLoaded, onLoadError }: AssetLoadOptions): Promise<void>;
20
20
  export declare function DragDropLoader({ children, ...divProps }: DragDropLoaderProps): import("react/jsx-runtime").JSX.Element;
21
21
  export declare function FilePicker({ accept, children, multiple, ...divProps }: FilePickerProps): import("react/jsx-runtime").JSX.Element;
22
22
  export {};
@@ -20,27 +20,45 @@ var __rest = (this && this.__rest) || function (s, e) {
20
20
  };
21
21
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
22
22
  import { useRef } from "react";
23
- import { canParseModelFile, parseModelFromFile } from "./modelLoader";
23
+ import { canParseModelFile, canParseTextureFile, parseModelFromFile, parseTextureFromFile } from "./modelLoader";
24
+ const DEFAULT_ACCEPT = ".glb,.gltf,.fbx,.png,.jpg,.jpeg,.webp,.gif,.bmp,.svg";
24
25
  function getFiles(fileList) {
25
26
  return fileList ? Array.from(fileList) : [];
26
27
  }
27
28
  export function loadFiles(files_1, _a) {
28
- return __awaiter(this, arguments, void 0, function* (files, { onModelLoaded, onFileLoaded, onFilesLoaded, onModelError, parseModels = true }) {
29
+ return __awaiter(this, arguments, void 0, function* (files, { onModelLoaded, onTextureLoaded, onUnhandledFile, onFilesLoaded, onLoadError }) {
29
30
  yield Promise.all(files.map((file) => __awaiter(this, void 0, void 0, function* () {
30
- yield (onFileLoaded === null || onFileLoaded === void 0 ? void 0 : onFileLoaded(file));
31
- if (!parseModels || !canParseModelFile(file) || (!onModelLoaded && !onModelError)) {
31
+ const shouldParseModel = canParseModelFile(file);
32
+ const shouldParseTexture = canParseTextureFile(file);
33
+ if (shouldParseModel) {
34
+ const result = yield parseModelFromFile(file);
35
+ if (result.success && result.model) {
36
+ yield (onModelLoaded === null || onModelLoaded === void 0 ? void 0 : onModelLoaded(result.model, file.name, file));
37
+ return;
38
+ }
39
+ if (onLoadError) {
40
+ yield onLoadError(result.error, file.name, file);
41
+ return;
42
+ }
43
+ console.error("Model parse error:", result.error);
32
44
  return;
33
45
  }
34
- const result = yield parseModelFromFile(file);
35
- if (result.success && result.model) {
36
- yield (onModelLoaded === null || onModelLoaded === void 0 ? void 0 : onModelLoaded(result.model, file.name, file));
46
+ if (shouldParseTexture) {
47
+ const result = yield parseTextureFromFile(file);
48
+ if (result.success && result.texture) {
49
+ yield (onTextureLoaded === null || onTextureLoaded === void 0 ? void 0 : onTextureLoaded(result.texture, file.name, file));
50
+ return;
51
+ }
52
+ if (onLoadError) {
53
+ yield onLoadError(result.error, file.name, file);
54
+ return;
55
+ }
56
+ console.error("Texture parse error:", result.error);
37
57
  return;
38
58
  }
39
- if (onModelError) {
40
- yield onModelError(result.error, file.name, file);
41
- return;
59
+ if (onUnhandledFile) {
60
+ yield onUnhandledFile(file);
42
61
  }
43
- console.error("Model parse error:", result.error);
44
62
  })));
45
63
  yield (onFilesLoaded === null || onFilesLoaded === void 0 ? void 0 : onFilesLoaded(files));
46
64
  });
@@ -50,11 +68,11 @@ function reportFileLoadError(error) {
50
68
  }
51
69
  function createLoadHandlers(options) {
52
70
  return {
53
- onFileLoaded: options.onFileLoaded,
54
71
  onFilesLoaded: options.onFilesLoaded,
55
- onModelError: options.onModelError,
56
72
  onModelLoaded: options.onModelLoaded,
57
- parseModels: options.parseModels,
73
+ onTextureLoaded: options.onTextureLoaded,
74
+ onUnhandledFile: options.onUnhandledFile,
75
+ onLoadError: options.onLoadError,
58
76
  };
59
77
  }
60
78
  export function DragDropLoader(_a) {
@@ -73,7 +91,7 @@ export function DragDropLoader(_a) {
73
91
  return (_jsx("div", Object.assign({}, divProps, { onDrop: handleDrop, onDragOver: handleDragOver, children: children })));
74
92
  }
75
93
  export function FilePicker(_a) {
76
- var { accept = ".glb,.gltf,.fbx", children, multiple = true } = _a, divProps = __rest(_a, ["accept", "children", "multiple"]);
94
+ var { accept = DEFAULT_ACCEPT, children, multiple = true } = _a, divProps = __rest(_a, ["accept", "children", "multiple"]);
77
95
  const inputRef = useRef(null);
78
96
  const { onClick } = divProps, wrapperProps = __rest(divProps, ["onClick"]);
79
97
  const loadOptions = createLoadHandlers(divProps);
@@ -1,4 +1,4 @@
1
1
  export { DragDropLoader, FilePicker, loadFiles } from "./DragDropLoader";
2
- export type { DragDropLoaderProps, FileLoadOptions, FilePickerProps } from "./DragDropLoader";
3
- export { loadModel, parseModelFromFile } from "./modelLoader";
4
- export type { LoadedModel, ModelLoadResult, ProgressCallback } from "./modelLoader";
2
+ export type { AssetLoadOptions, DragDropLoaderProps, FilePickerProps } from "./DragDropLoader";
3
+ export { loadModel, loadTexture, parseModelFromFile, parseTextureFromFile } from "./modelLoader";
4
+ export type { LoadedModel, LoadedTexture, ModelLoadResult, ProgressCallback, TextureLoadResult } from "./modelLoader";
@@ -1,2 +1,2 @@
1
1
  export { DragDropLoader, FilePicker, loadFiles } from "./DragDropLoader";
2
- export { loadModel, parseModelFromFile } from "./modelLoader";
2
+ export { loadModel, loadTexture, parseModelFromFile, parseTextureFromFile } from "./modelLoader";
@@ -1,11 +1,20 @@
1
- import type { Object3D } from "three";
1
+ import type { Object3D, Texture } from "three";
2
2
  export type LoadedModel = Object3D;
3
+ export type LoadedTexture = Texture;
3
4
  export type ModelLoadResult = {
4
5
  success: boolean;
5
6
  model?: LoadedModel;
6
7
  error?: unknown;
7
8
  };
9
+ export type TextureLoadResult = {
10
+ success: boolean;
11
+ texture?: LoadedTexture;
12
+ error?: unknown;
13
+ };
8
14
  export type ProgressCallback = (filename: string, loaded: number, total: number) => void;
9
15
  export declare function canParseModelFile(file: File | string): boolean;
16
+ export declare function canParseTextureFile(file: File | string): boolean;
10
17
  export declare function parseModelFromFile(file: File): Promise<ModelLoadResult>;
18
+ export declare function parseTextureFromFile(file: File): Promise<TextureLoadResult>;
11
19
  export declare function loadModel(filename: string, onProgress?: ProgressCallback): Promise<ModelLoadResult>;
20
+ export declare function loadTexture(filename: string): Promise<TextureLoadResult>;
@@ -7,12 +7,15 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
7
7
  step((generator = generator.apply(thisArg, _arguments || [])).next());
8
8
  });
9
9
  };
10
+ import { SRGBColorSpace, TextureLoader } from "three";
10
11
  import { DRACOLoader, FBXLoader, GLTFLoader } from "three/examples/jsm/Addons.js";
11
12
  const dracoLoader = new DRACOLoader();
12
13
  dracoLoader.setDecoderPath("https://www.gstatic.com/draco/v1/decoders/");
13
14
  const gltfLoader = new GLTFLoader();
14
15
  gltfLoader.setDRACOLoader(dracoLoader);
15
16
  const fbxLoader = new FBXLoader();
17
+ const textureLoader = new TextureLoader();
18
+ const TEXTURE_FILE_EXTENSIONS = [".png", ".jpg", ".jpeg", ".webp", ".gif", ".bmp", ".svg"];
16
19
  function normalizeModelPath(name) {
17
20
  return name.split(/[?#]/, 1)[0].toLowerCase();
18
21
  }
@@ -30,6 +33,11 @@ export function canParseModelFile(file) {
30
33
  const filename = typeof file === "string" ? file : file.name;
31
34
  return getModelFileKind(filename) !== null;
32
35
  }
36
+ export function canParseTextureFile(file) {
37
+ const filename = typeof file === "string" ? file : file.name;
38
+ const normalizedName = normalizeModelPath(filename);
39
+ return TEXTURE_FILE_EXTENSIONS.some(extension => normalizedName.endsWith(extension));
40
+ }
33
41
  function parseModelBuffer(arrayBuffer, sourceName) {
34
42
  const modelFileKind = getModelFileKind(sourceName);
35
43
  if (modelFileKind === "gltf") {
@@ -68,6 +76,19 @@ export function parseModelFromFile(file) {
68
76
  reader.readAsArrayBuffer(file);
69
77
  });
70
78
  }
79
+ export function parseTextureFromFile(file) {
80
+ return new Promise(resolve => {
81
+ const url = URL.createObjectURL(file);
82
+ textureLoader.load(url, texture => {
83
+ texture.colorSpace = SRGBColorSpace;
84
+ resolve({ success: true, texture });
85
+ URL.revokeObjectURL(url);
86
+ }, undefined, error => {
87
+ resolve({ success: false, error });
88
+ URL.revokeObjectURL(url);
89
+ });
90
+ });
91
+ }
71
92
  export function loadModel(filename, onProgress) {
72
93
  return __awaiter(this, void 0, void 0, function* () {
73
94
  try {
@@ -102,3 +123,21 @@ export function loadModel(filename, onProgress) {
102
123
  }
103
124
  });
104
125
  }
126
+ export function loadTexture(filename) {
127
+ return __awaiter(this, void 0, void 0, function* () {
128
+ try {
129
+ if (!canParseTextureFile(filename)) {
130
+ return { success: false, error: new Error(`Unsupported file format: ${filename}`) };
131
+ }
132
+ return yield new Promise(resolve => {
133
+ textureLoader.load(filename, texture => {
134
+ texture.colorSpace = SRGBColorSpace;
135
+ resolve({ success: true, texture });
136
+ }, undefined, error => resolve({ success: false, error }));
137
+ });
138
+ }
139
+ catch (error) {
140
+ return { success: false, error };
141
+ }
142
+ });
143
+ }
@@ -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";
10
+ import { loadFiles } from "../dragdrop";
11
11
  const DEFAULT_PREFAB = {
12
12
  id: "prefab-default",
13
13
  name: "New Prefab",
@@ -120,8 +120,6 @@ const PrefabEditor = forwardRef(({ basePath, initialPrefab, physics = true, onPr
120
120
  }, []);
121
121
  // --- Drag & drop files to add nodes ---
122
122
  useEffect(() => {
123
- const IMAGE_EXTS = ['png', 'jpg', 'jpeg', 'webp', 'gif', 'bmp', 'svg'];
124
- const MODEL_EXTS = ['glb', 'gltf', 'fbx'];
125
123
  function handleDragOver(e) {
126
124
  e.preventDefault();
127
125
  e.stopPropagation();
@@ -131,39 +129,32 @@ const PrefabEditor = forwardRef(({ basePath, initialPrefab, physics = true, onPr
131
129
  e.preventDefault();
132
130
  e.stopPropagation();
133
131
  const files = ((_a = e.dataTransfer) === null || _a === void 0 ? void 0 : _a.files) ? Array.from(e.dataTransfer.files) : [];
134
- files.forEach(file => {
135
- var _a, _b;
136
- const ext = (_a = file.name.split('.').pop()) === null || _a === void 0 ? void 0 : _a.toLowerCase();
137
- if (!ext)
138
- return;
139
- const baseName = file.name.replace(/\.[^.]+$/, '');
140
- if (MODEL_EXTS.includes(ext)) {
141
- const modelPath = `models/${file.name}`;
132
+ void loadFiles(files, {
133
+ onModelLoaded: (model, filename) => {
134
+ var _a;
135
+ const modelPath = `models/${filename}`;
136
+ const baseName = filename.replace(/\.[^.]+$/, '');
142
137
  const newNode = createModelNode(modelPath, baseName);
143
138
  updatePrefab(prev => {
144
139
  var _a;
145
140
  return (Object.assign(Object.assign({}, prev), { root: Object.assign(Object.assign({}, prev.root), { children: [...((_a = prev.root.children) !== null && _a !== void 0 ? _a : []), newNode] }) }));
146
141
  });
147
- parseModelFromFile(file).then(result => {
148
- var _a;
149
- if (result.success && result.model) {
150
- (_a = prefabRootRef.current) === null || _a === void 0 ? void 0 : _a.injectModel(modelPath, result.model);
151
- }
152
- else {
153
- console.error('Drop parse error:', result.error);
154
- }
155
- });
156
- }
157
- else if (IMAGE_EXTS.includes(ext)) {
158
- const texturePath = `textures/${file.name}`;
142
+ (_a = prefabRootRef.current) === null || _a === void 0 ? void 0 : _a.injectModel(modelPath, model);
143
+ },
144
+ onTextureLoaded: (texture, filename) => {
145
+ var _a;
146
+ const texturePath = `textures/${filename}`;
147
+ const baseName = filename.replace(/\.[^.]+$/, '');
159
148
  const newNode = createImageNode(texturePath, baseName);
160
149
  updatePrefab(prev => {
161
150
  var _a;
162
151
  return (Object.assign(Object.assign({}, prev), { root: Object.assign(Object.assign({}, prev.root), { children: [...((_a = prev.root.children) !== null && _a !== void 0 ? _a : []), newNode] }) }));
163
152
  });
164
- // Inject a blob URL texture so it renders immediately
165
- (_b = prefabRootRef.current) === null || _b === void 0 ? void 0 : _b.injectTexture(texturePath, file);
166
- }
153
+ (_a = prefabRootRef.current) === null || _a === void 0 ? void 0 : _a.injectTexture(texturePath, texture);
154
+ },
155
+ onLoadError: error => {
156
+ console.error('Drop asset error:', error);
157
+ },
167
158
  });
168
159
  }
169
160
  window.addEventListener('dragover', handleDragOver);
@@ -5,7 +5,7 @@ export interface PrefabRootRef {
5
5
  root: Group | null;
6
6
  rigidBodyRefs: Map<string, any>;
7
7
  injectModel: (filename: string, model: Object3D) => void;
8
- injectTexture: (filename: string, file: File) => void;
8
+ injectTexture: (filename: string, texture: Texture) => void;
9
9
  focusNode: (nodeId: string) => void;
10
10
  }
11
11
  export declare const PrefabRoot: import("react").ForwardRefExoticComponent<{
@@ -40,15 +40,9 @@ export const PrefabRoot = forwardRef(({ editMode, data, onPrefabChange, selected
40
40
  const injectModel = useCallback((filename, model) => {
41
41
  setModels(m => (Object.assign(Object.assign({}, m), { [filename]: model })));
42
42
  }, []);
43
- const injectTexture = useCallback((filename, file) => {
43
+ const injectTexture = useCallback((filename, texture) => {
44
44
  loading.current.add(filename);
45
- const url = URL.createObjectURL(file);
46
- const loader = new TextureLoader();
47
- loader.load(url, tex => {
48
- tex.colorSpace = SRGBColorSpace;
49
- setTextures(t => (Object.assign(Object.assign({}, t), { [filename]: tex })));
50
- URL.revokeObjectURL(url);
51
- }, undefined, () => URL.revokeObjectURL(url));
45
+ setTextures(t => (Object.assign(Object.assign({}, t), { [filename]: texture })));
52
46
  }, []);
53
47
  useImperativeHandle(ref, () => ({
54
48
  root: rootRef.current,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-three-game",
3
- "version": "0.0.59",
3
+ "version": "0.0.60",
4
4
  "description": "high performance 3D game engine for React",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.js",
@@ -1,57 +1,80 @@
1
1
  import { ChangeEvent, useRef } from "react";
2
2
  import type { DragEvent, HTMLAttributes, MouseEvent, ReactNode } from "react";
3
- import type { LoadedModel } from "./modelLoader";
4
- import { canParseModelFile, parseModelFromFile } from "./modelLoader";
3
+ import type { LoadedModel, LoadedTexture } from "./modelLoader";
4
+ import { canParseModelFile, canParseTextureFile, parseModelFromFile, parseTextureFromFile } from "./modelLoader";
5
5
 
6
- export interface FileLoadOptions {
6
+ export interface AssetLoadOptions {
7
7
  onModelLoaded?: (model: LoadedModel, filename: string, file: File) => void | Promise<void>;
8
- onFileLoaded?: (file: File) => void | Promise<void>;
8
+ onTextureLoaded?: (texture: LoadedTexture, filename: string, file: File) => void | Promise<void>;
9
+ onUnhandledFile?: (file: File) => void | Promise<void>;
9
10
  onFilesLoaded?: (files: File[]) => void | Promise<void>;
10
- onModelError?: (error: unknown, filename: string, file: File) => void | Promise<void>;
11
- parseModels?: boolean;
11
+ onLoadError?: (error: unknown, filename: string, file: File) => void | Promise<void>;
12
12
  }
13
13
 
14
14
  type DivProps = Omit<HTMLAttributes<HTMLDivElement>, "children" | "onDrop" | "onDragOver">;
15
15
 
16
- export interface DragDropLoaderProps extends FileLoadOptions, DivProps {
16
+ export interface DragDropLoaderProps extends AssetLoadOptions, DivProps {
17
17
  children?: ReactNode;
18
18
  }
19
19
 
20
- export interface FilePickerProps extends FileLoadOptions, DivProps {
20
+ export interface FilePickerProps extends AssetLoadOptions, DivProps {
21
21
  accept?: string;
22
22
  children?: ReactNode;
23
23
  multiple?: boolean;
24
24
  }
25
25
 
26
+ const DEFAULT_ACCEPT = ".glb,.gltf,.fbx,.png,.jpg,.jpeg,.webp,.gif,.bmp,.svg";
27
+
26
28
  function getFiles(fileList?: FileList | null) {
27
29
  return fileList ? Array.from(fileList) : [];
28
30
  }
29
31
 
30
32
  export async function loadFiles(
31
33
  files: File[],
32
- { onModelLoaded, onFileLoaded, onFilesLoaded, onModelError, parseModels = true }: FileLoadOptions,
34
+ { onModelLoaded, onTextureLoaded, onUnhandledFile, onFilesLoaded, onLoadError }: AssetLoadOptions,
33
35
  ) {
34
36
  await Promise.all(
35
37
  files.map(async (file) => {
36
- await onFileLoaded?.(file);
38
+ const shouldParseModel = canParseModelFile(file);
39
+ const shouldParseTexture = canParseTextureFile(file);
37
40
 
38
- if (!parseModels || !canParseModelFile(file) || (!onModelLoaded && !onModelError)) {
39
- return;
40
- }
41
+ if (shouldParseModel) {
42
+ const result = await parseModelFromFile(file);
43
+
44
+ if (result.success && result.model) {
45
+ await onModelLoaded?.(result.model, file.name, file);
46
+ return;
47
+ }
41
48
 
42
- const result = await parseModelFromFile(file);
49
+ if (onLoadError) {
50
+ await onLoadError(result.error, file.name, file);
51
+ return;
52
+ }
43
53
 
44
- if (result.success && result.model) {
45
- await onModelLoaded?.(result.model, file.name, file);
54
+ console.error("Model parse error:", result.error);
46
55
  return;
47
56
  }
48
57
 
49
- if (onModelError) {
50
- await onModelError(result.error, file.name, file);
58
+ if (shouldParseTexture) {
59
+ const result = await parseTextureFromFile(file);
60
+
61
+ if (result.success && result.texture) {
62
+ await onTextureLoaded?.(result.texture, file.name, file);
63
+ return;
64
+ }
65
+
66
+ if (onLoadError) {
67
+ await onLoadError(result.error, file.name, file);
68
+ return;
69
+ }
70
+
71
+ console.error("Texture parse error:", result.error);
51
72
  return;
52
73
  }
53
74
 
54
- console.error("Model parse error:", result.error);
75
+ if (onUnhandledFile) {
76
+ await onUnhandledFile(file);
77
+ }
55
78
  }),
56
79
  );
57
80
 
@@ -62,14 +85,14 @@ function reportFileLoadError(error: unknown) {
62
85
  console.error("File load error:", error);
63
86
  }
64
87
 
65
- function createLoadHandlers(options: FileLoadOptions) {
88
+ function createLoadHandlers(options: AssetLoadOptions) {
66
89
  return {
67
- onFileLoaded: options.onFileLoaded,
68
90
  onFilesLoaded: options.onFilesLoaded,
69
- onModelError: options.onModelError,
70
91
  onModelLoaded: options.onModelLoaded,
71
- parseModels: options.parseModels,
72
- } satisfies FileLoadOptions;
92
+ onTextureLoaded: options.onTextureLoaded,
93
+ onUnhandledFile: options.onUnhandledFile,
94
+ onLoadError: options.onLoadError,
95
+ } satisfies AssetLoadOptions;
73
96
  }
74
97
 
75
98
  export function DragDropLoader({
@@ -98,7 +121,7 @@ export function DragDropLoader({
98
121
  }
99
122
 
100
123
  export function FilePicker({
101
- accept = ".glb,.gltf,.fbx",
124
+ accept = DEFAULT_ACCEPT,
102
125
  children,
103
126
  multiple = true,
104
127
  ...divProps
@@ -1,4 +1,4 @@
1
1
  export { DragDropLoader, FilePicker, loadFiles } from "./DragDropLoader";
2
- export type { DragDropLoaderProps, FileLoadOptions, FilePickerProps } from "./DragDropLoader";
3
- export { loadModel, parseModelFromFile } from "./modelLoader";
4
- export type { LoadedModel, ModelLoadResult, ProgressCallback } from "./modelLoader";
2
+ export type { AssetLoadOptions, DragDropLoaderProps, FilePickerProps } from "./DragDropLoader";
3
+ export { loadModel, loadTexture, parseModelFromFile, parseTextureFromFile } from "./modelLoader";
4
+ export type { LoadedModel, LoadedTexture, ModelLoadResult, ProgressCallback, TextureLoadResult } from "./modelLoader";
@@ -1,7 +1,9 @@
1
- import type { Object3D } from "three";
1
+ import type { Object3D, Texture } from "three";
2
+ import { SRGBColorSpace, TextureLoader } from "three";
2
3
  import { DRACOLoader, FBXLoader, GLTFLoader } from "three/examples/jsm/Addons.js";
3
4
 
4
5
  export type LoadedModel = Object3D;
6
+ export type LoadedTexture = Texture;
5
7
 
6
8
  export type ModelLoadResult = {
7
9
  success: boolean;
@@ -9,6 +11,12 @@ export type ModelLoadResult = {
9
11
  error?: unknown;
10
12
  };
11
13
 
14
+ export type TextureLoadResult = {
15
+ success: boolean;
16
+ texture?: LoadedTexture;
17
+ error?: unknown;
18
+ };
19
+
12
20
  export type ProgressCallback = (filename: string, loaded: number, total: number) => void;
13
21
 
14
22
  const dracoLoader = new DRACOLoader();
@@ -18,8 +26,10 @@ const gltfLoader = new GLTFLoader();
18
26
  gltfLoader.setDRACOLoader(dracoLoader);
19
27
 
20
28
  const fbxLoader = new FBXLoader();
29
+ const textureLoader = new TextureLoader();
21
30
 
22
31
  type ModelFileKind = "gltf" | "fbx";
32
+ const TEXTURE_FILE_EXTENSIONS = [".png", ".jpg", ".jpeg", ".webp", ".gif", ".bmp", ".svg"] as const;
23
33
 
24
34
  function normalizeModelPath(name: string) {
25
35
  return name.split(/[?#]/, 1)[0].toLowerCase();
@@ -44,6 +54,13 @@ export function canParseModelFile(file: File | string) {
44
54
  return getModelFileKind(filename) !== null;
45
55
  }
46
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
+
47
64
  function parseModelBuffer(arrayBuffer: ArrayBuffer, sourceName: string): Promise<ModelLoadResult> {
48
65
  const modelFileKind = getModelFileKind(sourceName);
49
66
 
@@ -94,6 +111,26 @@ export function parseModelFromFile(file: File): Promise<ModelLoadResult> {
94
111
  });
95
112
  }
96
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
+
97
134
  export async function loadModel(
98
135
  filename: string,
99
136
  onProgress?: ProgressCallback,
@@ -143,3 +180,25 @@ export async function loadModel(
143
180
  return { success: false, error };
144
181
  }
145
182
  }
183
+
184
+ export async function loadTexture(filename: string): Promise<TextureLoadResult> {
185
+ try {
186
+ if (!canParseTextureFile(filename)) {
187
+ return { success: false, error: new Error(`Unsupported file format: ${filename}`) };
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
+ });
201
+ } catch (error) {
202
+ return { success: false, error };
203
+ }
204
+ }
@@ -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";
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
 
@@ -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, () => ({