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.
- package/dist/tools/dragdrop/DragDropLoader.d.ts +8 -8
- package/dist/tools/dragdrop/DragDropLoader.js +33 -15
- package/dist/tools/dragdrop/index.d.ts +3 -3
- package/dist/tools/dragdrop/index.js +1 -1
- package/dist/tools/dragdrop/modelLoader.d.ts +10 -1
- package/dist/tools/dragdrop/modelLoader.js +39 -0
- package/dist/tools/prefabeditor/PrefabEditor.js +17 -26
- package/dist/tools/prefabeditor/PrefabRoot.d.ts +1 -1
- package/dist/tools/prefabeditor/PrefabRoot.js +2 -8
- package/package.json +1 -1
- package/src/tools/dragdrop/DragDropLoader.tsx +48 -25
- package/src/tools/dragdrop/index.ts +3 -3
- package/src/tools/dragdrop/modelLoader.ts +60 -1
- package/src/tools/prefabeditor/PrefabEditor.tsx +17 -23
- package/src/tools/prefabeditor/PrefabRoot.tsx +3 -9
|
@@ -1,22 +1,22 @@
|
|
|
1
1
|
import type { HTMLAttributes, ReactNode } from "react";
|
|
2
|
-
import type { LoadedModel } from "./modelLoader";
|
|
3
|
-
export interface
|
|
2
|
+
import type { LoadedModel, LoadedTexture } from "./modelLoader";
|
|
3
|
+
export interface AssetLoadOptions {
|
|
4
4
|
onModelLoaded?: (model: LoadedModel, filename: string, file: File) => void | Promise<void>;
|
|
5
|
-
|
|
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
|
-
|
|
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
|
|
11
|
+
export interface DragDropLoaderProps extends AssetLoadOptions, DivProps {
|
|
12
12
|
children?: ReactNode;
|
|
13
13
|
}
|
|
14
|
-
export interface FilePickerProps extends
|
|
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,
|
|
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,
|
|
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
|
-
|
|
31
|
-
|
|
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
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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 (
|
|
40
|
-
yield
|
|
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
|
-
|
|
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 =
|
|
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 {
|
|
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 {
|
|
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
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
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
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
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
|
-
|
|
165
|
-
|
|
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,
|
|
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,
|
|
43
|
+
const injectTexture = useCallback((filename, texture) => {
|
|
44
44
|
loading.current.add(filename);
|
|
45
|
-
|
|
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,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
|
|
6
|
+
export interface AssetLoadOptions {
|
|
7
7
|
onModelLoaded?: (model: LoadedModel, filename: string, file: File) => void | Promise<void>;
|
|
8
|
-
|
|
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
|
-
|
|
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
|
|
16
|
+
export interface DragDropLoaderProps extends AssetLoadOptions, DivProps {
|
|
17
17
|
children?: ReactNode;
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
-
export interface FilePickerProps extends
|
|
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,
|
|
34
|
+
{ onModelLoaded, onTextureLoaded, onUnhandledFile, onFilesLoaded, onLoadError }: AssetLoadOptions,
|
|
33
35
|
) {
|
|
34
36
|
await Promise.all(
|
|
35
37
|
files.map(async (file) => {
|
|
36
|
-
|
|
38
|
+
const shouldParseModel = canParseModelFile(file);
|
|
39
|
+
const shouldParseTexture = canParseTextureFile(file);
|
|
37
40
|
|
|
38
|
-
if (
|
|
39
|
-
|
|
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
|
-
|
|
49
|
+
if (onLoadError) {
|
|
50
|
+
await onLoadError(result.error, file.name, file);
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
43
53
|
|
|
44
|
-
|
|
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 (
|
|
50
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
72
|
-
|
|
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 =
|
|
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 {
|
|
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 {
|
|
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
|
|
|
@@ -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, () => ({
|