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
|
@@ -44,7 +44,7 @@ const MyComponent: Component = {
|
|
|
44
44
|
|
|
45
45
|
## Usage Modes
|
|
46
46
|
|
|
47
|
-
**
|
|
47
|
+
**PrefabRoot**: Pure renderer for embedding prefab data in standard R3F applications. Render it inside a regular `@react-three/fiber` `Canvas`. `GameCanvas` provides the WebGPU canvas setup. Add a `Physics` wrapper to enable physics. Use this to integrate prefabs into larger R3F scenes.
|
|
48
48
|
|
|
49
49
|
**PrefabEditor**: Managed scene with editor UI and play/pause controls for physics. Full authoring tool for level design and prototyping. Includes canvas, physics, transform gizmos, and inspector. Physics only runs in play mode. Can pass R3F components as children.
|
|
50
50
|
|
package/README.md
CHANGED
|
@@ -16,47 +16,58 @@ npx skills add https://github.com/prnthh/react-three-game-skill
|
|
|
16
16
|
|
|
17
17
|
## Usage Modes
|
|
18
18
|
|
|
19
|
-
**
|
|
19
|
+
**PrefabRoot**: Pure renderer for embedding prefab data in standard R3F applications. Render it inside a regular `@react-three/fiber` `Canvas`. `GameCanvas` provides the WebGPU canvas setup. Add a `Physics` wrapper to enable physics. Use this to integrate prefabs into larger R3F scenes.
|
|
20
20
|
|
|
21
21
|
**PrefabEditor**: Managed scene with editor UI and play/pause controls for physics. Full authoring tool for level design and prototyping. Includes canvas, physics, transform gizmos, and inspector. Physics only runs in play mode. Can pass R3F components as children.
|
|
22
22
|
|
|
23
23
|
## Basic Usage
|
|
24
24
|
|
|
25
25
|
```jsx
|
|
26
|
-
import { Physics } from
|
|
27
|
-
import { GameCanvas, PrefabRoot } from
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
physics: { type: "Physics", properties: { type: "dynamic" } }
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
]
|
|
26
|
+
import { Physics } from "@react-three/rapier";
|
|
27
|
+
import { GameCanvas, PrefabRoot } from "react-three-game";
|
|
28
|
+
|
|
29
|
+
const sceneData = {
|
|
30
|
+
root: {
|
|
31
|
+
id: "scene",
|
|
32
|
+
children: [
|
|
33
|
+
{
|
|
34
|
+
id: "ground",
|
|
35
|
+
components: {
|
|
36
|
+
transform: { type: "Transform", properties: { position: [0, 0, 0], rotation: [-1.57, 0, 0] } },
|
|
37
|
+
geometry: { type: "Geometry", properties: { geometryType: "plane", args: [50, 50] } },
|
|
38
|
+
material: { type: "Material", properties: { color: "#3a3" } },
|
|
39
|
+
physics: { type: "Physics", properties: { type: "fixed" } }
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
id: "ball",
|
|
44
|
+
components: {
|
|
45
|
+
transform: { type: "Transform", properties: { position: [0, 5, 0] } },
|
|
46
|
+
geometry: { type: "Geometry", properties: { geometryType: "sphere" } },
|
|
47
|
+
material: { type: "Material", properties: { color: "#f66" } },
|
|
48
|
+
physics: { type: "Physics", properties: { type: "dynamic" } }
|
|
49
|
+
}
|
|
54
50
|
}
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
51
|
+
]
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
export default function Home() {
|
|
56
|
+
return (
|
|
57
|
+
<main className="flex h-screen w-screen">
|
|
58
|
+
<GameCanvas>
|
|
59
|
+
<Physics>
|
|
60
|
+
<ambientLight intensity={0.8} />
|
|
61
|
+
<PrefabRoot data={sceneData} />
|
|
62
|
+
</Physics>
|
|
63
|
+
</GameCanvas>
|
|
64
|
+
</main>
|
|
65
|
+
);
|
|
66
|
+
}
|
|
58
67
|
```
|
|
59
68
|
|
|
69
|
+
`GameCanvas` provides the library's WebGPU canvas setup.
|
|
70
|
+
|
|
60
71
|
## GameObject Schema
|
|
61
72
|
|
|
62
73
|
```typescript
|
package/dist/index.d.ts
CHANGED
|
@@ -16,5 +16,5 @@ export { gameEvents, useGameEvent, getEntityIdFromRigidBody } from './tools/pref
|
|
|
16
16
|
export type { GameEventType, GameEventMap, GameEventPayload, PhysicsEventType, PhysicsEventPayload } from './tools/prefabeditor/GameEvents';
|
|
17
17
|
export { entityEvents, useEntityEvent } from './tools/prefabeditor/GameEvents';
|
|
18
18
|
export type { EntityEventType, EntityEventPayload } from './tools/prefabeditor/GameEvents';
|
|
19
|
-
export
|
|
19
|
+
export * from './tools/dragdrop';
|
|
20
20
|
export { TextureListViewer, ModelListViewer, SoundListViewer, SharedCanvas, } from './tools/assetviewer/page';
|
package/dist/index.js
CHANGED
|
@@ -17,5 +17,5 @@ export { gameEvents, useGameEvent, getEntityIdFromRigidBody } from './tools/pref
|
|
|
17
17
|
// Backward compatibility aliases
|
|
18
18
|
export { entityEvents, useEntityEvent } from './tools/prefabeditor/GameEvents';
|
|
19
19
|
// Asset Tools
|
|
20
|
-
export
|
|
20
|
+
export * from './tools/dragdrop';
|
|
21
21
|
export { TextureListViewer, ModelListViewer, SoundListViewer, SharedCanvas, } from './tools/assetviewer/page';
|
|
@@ -3,7 +3,7 @@ import { Canvas } from "@react-three/fiber";
|
|
|
3
3
|
import { OrbitControls, View, PerspectiveCamera } from "@react-three/drei";
|
|
4
4
|
import { Component as ReactComponent, Suspense, useEffect, useState, useRef } from "react";
|
|
5
5
|
import { TextureLoader } from "three";
|
|
6
|
-
import { loadModel } from "../dragdrop
|
|
6
|
+
import { loadModel } from "../dragdrop";
|
|
7
7
|
class ErrorBoundary extends ReactComponent {
|
|
8
8
|
constructor(props) {
|
|
9
9
|
super(props);
|
|
@@ -1,9 +1,22 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
import type { HTMLAttributes, ReactNode } from "react";
|
|
2
|
+
import type { LoadedModel, LoadedTexture } from "./modelLoader";
|
|
3
|
+
export interface AssetLoadOptions {
|
|
4
|
+
onModelLoaded?: (model: LoadedModel, filename: string, file: File) => void | Promise<void>;
|
|
5
|
+
onTextureLoaded?: (texture: LoadedTexture, filename: string, file: File) => void | Promise<void>;
|
|
6
|
+
onUnhandledFile?: (file: File) => void | Promise<void>;
|
|
7
|
+
onFilesLoaded?: (files: File[]) => void | Promise<void>;
|
|
8
|
+
onLoadError?: (error: unknown, filename: string, file: File) => void | Promise<void>;
|
|
3
9
|
}
|
|
4
|
-
|
|
5
|
-
interface
|
|
6
|
-
|
|
10
|
+
type DivProps = Omit<HTMLAttributes<HTMLDivElement>, "children" | "onDrop" | "onDragOver">;
|
|
11
|
+
export interface DragDropLoaderProps extends AssetLoadOptions, DivProps {
|
|
12
|
+
children?: ReactNode;
|
|
7
13
|
}
|
|
8
|
-
export
|
|
14
|
+
export interface FilePickerProps extends AssetLoadOptions, DivProps {
|
|
15
|
+
accept?: string;
|
|
16
|
+
children?: ReactNode;
|
|
17
|
+
multiple?: boolean;
|
|
18
|
+
}
|
|
19
|
+
export declare function loadFiles(files: File[], { onModelLoaded, onTextureLoaded, onUnhandledFile, onFilesLoaded, onLoadError }: AssetLoadOptions): Promise<void>;
|
|
20
|
+
export declare function DragDropLoader({ children, ...divProps }: DragDropLoaderProps): import("react/jsx-runtime").JSX.Element;
|
|
21
|
+
export declare function FilePicker({ accept, children, multiple, ...divProps }: FilePickerProps): import("react/jsx-runtime").JSX.Element;
|
|
9
22
|
export {};
|
|
@@ -7,49 +7,104 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
|
|
|
7
7
|
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
8
8
|
});
|
|
9
9
|
};
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
onModelLoaded(result.model, file.name);
|
|
10
|
+
var __rest = (this && this.__rest) || function (s, e) {
|
|
11
|
+
var t = {};
|
|
12
|
+
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
|
|
13
|
+
t[p] = s[p];
|
|
14
|
+
if (s != null && typeof Object.getOwnPropertySymbols === "function")
|
|
15
|
+
for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
|
|
16
|
+
if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
|
|
17
|
+
t[p[i]] = s[p[i]];
|
|
19
18
|
}
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
19
|
+
return t;
|
|
20
|
+
};
|
|
21
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
22
|
+
import { useRef } from "react";
|
|
23
|
+
import { canParseModelFile, canParseTextureFile, parseModelFromFile, parseTextureFromFile } from "./modelLoader";
|
|
24
|
+
const DEFAULT_ACCEPT = ".glb,.gltf,.fbx,.png,.jpg,.jpeg,.webp,.gif,.bmp,.svg";
|
|
25
|
+
function getFiles(fileList) {
|
|
26
|
+
return fileList ? Array.from(fileList) : [];
|
|
24
27
|
}
|
|
25
|
-
export function
|
|
26
|
-
|
|
27
|
-
function
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
28
|
+
export function loadFiles(files_1, _a) {
|
|
29
|
+
return __awaiter(this, arguments, void 0, function* (files, { onModelLoaded, onTextureLoaded, onUnhandledFile, onFilesLoaded, onLoadError }) {
|
|
30
|
+
yield Promise.all(files.map((file) => __awaiter(this, void 0, void 0, function* () {
|
|
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);
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
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);
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
if (onUnhandledFile) {
|
|
60
|
+
yield onUnhandledFile(file);
|
|
61
|
+
}
|
|
62
|
+
})));
|
|
63
|
+
yield (onFilesLoaded === null || onFilesLoaded === void 0 ? void 0 : onFilesLoaded(files));
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
function reportFileLoadError(error) {
|
|
67
|
+
console.error("File load error:", error);
|
|
46
68
|
}
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
69
|
+
function createLoadHandlers(options) {
|
|
70
|
+
return {
|
|
71
|
+
onFilesLoaded: options.onFilesLoaded,
|
|
72
|
+
onModelLoaded: options.onModelLoaded,
|
|
73
|
+
onTextureLoaded: options.onTextureLoaded,
|
|
74
|
+
onUnhandledFile: options.onUnhandledFile,
|
|
75
|
+
onLoadError: options.onLoadError,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
export function DragDropLoader(_a) {
|
|
79
|
+
var { children } = _a, divProps = __rest(_a, ["children"]);
|
|
80
|
+
const loadOptions = createLoadHandlers(divProps);
|
|
81
|
+
function handleDrop(event) {
|
|
82
|
+
var _a;
|
|
83
|
+
event.preventDefault();
|
|
84
|
+
event.stopPropagation();
|
|
85
|
+
void loadFiles(getFiles((_a = event.dataTransfer) === null || _a === void 0 ? void 0 : _a.files), loadOptions).catch(reportFileLoadError);
|
|
86
|
+
}
|
|
87
|
+
function handleDragOver(event) {
|
|
88
|
+
event.preventDefault();
|
|
89
|
+
event.stopPropagation();
|
|
90
|
+
}
|
|
91
|
+
return (_jsx("div", Object.assign({}, divProps, { onDrop: handleDrop, onDragOver: handleDragOver, children: children })));
|
|
92
|
+
}
|
|
93
|
+
export function FilePicker(_a) {
|
|
94
|
+
var { accept = DEFAULT_ACCEPT, children, multiple = true } = _a, divProps = __rest(_a, ["accept", "children", "multiple"]);
|
|
95
|
+
const inputRef = useRef(null);
|
|
96
|
+
const { onClick } = divProps, wrapperProps = __rest(divProps, ["onClick"]);
|
|
97
|
+
const loadOptions = createLoadHandlers(divProps);
|
|
98
|
+
function onChange(event) {
|
|
99
|
+
void loadFiles(getFiles(event.target.files), loadOptions).catch(reportFileLoadError);
|
|
100
|
+
event.target.value = "";
|
|
101
|
+
}
|
|
102
|
+
function handleClick(event) {
|
|
103
|
+
var _a;
|
|
104
|
+
onClick === null || onClick === void 0 ? void 0 : onClick(event);
|
|
105
|
+
if (!event.defaultPrevented) {
|
|
106
|
+
(_a = inputRef.current) === null || _a === void 0 ? void 0 : _a.click();
|
|
107
|
+
}
|
|
51
108
|
}
|
|
52
|
-
|
|
53
|
-
const inputId = "file-picker-input";
|
|
54
|
-
return (_jsxs(_Fragment, { children: [_jsx("input", { id: inputId, type: "file", accept: ".glb,.gltf,.fbx", multiple: true, onChange: onChange, className: "hidden" }), _jsx("button", { className: "px-3 py-1 bg-blue-500/20 hover:bg-blue-500/30 border border-blue-400/40 hover:border-blue-400/60 text-blue-200 hover:text-blue-100 text-xs font-medium transition-all", type: "button", onClick: () => { var _a; return (_a = document.getElementById(inputId)) === null || _a === void 0 ? void 0 : _a.click(); }, children: "Select Files" })] }));
|
|
109
|
+
return (_jsxs("div", Object.assign({}, wrapperProps, { onClick: handleClick, children: [_jsx("input", { ref: inputRef, type: "file", accept: accept, multiple: multiple, onChange: onChange, hidden: true }), children !== null && children !== void 0 ? children : "Select Files"] })));
|
|
55
110
|
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export { DragDropLoader, FilePicker, loadFiles } from "./DragDropLoader";
|
|
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,12 +1,20 @@
|
|
|
1
|
+
import type { Object3D, Texture } from "three";
|
|
2
|
+
export type LoadedModel = Object3D;
|
|
3
|
+
export type LoadedTexture = Texture;
|
|
1
4
|
export type ModelLoadResult = {
|
|
2
5
|
success: boolean;
|
|
3
|
-
model?:
|
|
4
|
-
error?:
|
|
6
|
+
model?: LoadedModel;
|
|
7
|
+
error?: unknown;
|
|
8
|
+
};
|
|
9
|
+
export type TextureLoadResult = {
|
|
10
|
+
success: boolean;
|
|
11
|
+
texture?: LoadedTexture;
|
|
12
|
+
error?: unknown;
|
|
5
13
|
};
|
|
6
14
|
export type ProgressCallback = (filename: string, loaded: number, total: number) => void;
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
* Returns the parsed Three.js Object3D scene.
|
|
10
|
-
*/
|
|
15
|
+
export declare function canParseModelFile(file: File | string): boolean;
|
|
16
|
+
export declare function canParseTextureFile(file: File | string): boolean;
|
|
11
17
|
export declare function parseModelFromFile(file: File): Promise<ModelLoadResult>;
|
|
18
|
+
export declare function parseTextureFromFile(file: File): Promise<TextureLoadResult>;
|
|
12
19
|
export declare function loadModel(filename: string, onProgress?: ProgressCallback): Promise<ModelLoadResult>;
|
|
20
|
+
export declare function loadTexture(filename: string): Promise<TextureLoadResult>;
|
|
@@ -7,82 +7,134 @@ 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 {
|
|
11
|
-
|
|
10
|
+
import { SRGBColorSpace, TextureLoader } from "three";
|
|
11
|
+
import { DRACOLoader, FBXLoader, GLTFLoader } from "three/examples/jsm/Addons.js";
|
|
12
12
|
const dracoLoader = new DRACOLoader();
|
|
13
13
|
dracoLoader.setDecoderPath("https://www.gstatic.com/draco/v1/decoders/");
|
|
14
14
|
const gltfLoader = new GLTFLoader();
|
|
15
15
|
gltfLoader.setDRACOLoader(dracoLoader);
|
|
16
16
|
const fbxLoader = new FBXLoader();
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
17
|
+
const textureLoader = new TextureLoader();
|
|
18
|
+
const TEXTURE_FILE_EXTENSIONS = [".png", ".jpg", ".jpeg", ".webp", ".gif", ".bmp", ".svg"];
|
|
19
|
+
function normalizeModelPath(name) {
|
|
20
|
+
return name.split(/[?#]/, 1)[0].toLowerCase();
|
|
21
|
+
}
|
|
22
|
+
function getModelFileKind(name) {
|
|
23
|
+
const normalizedName = normalizeModelPath(name);
|
|
24
|
+
if (normalizedName.endsWith(".glb") || normalizedName.endsWith(".gltf")) {
|
|
25
|
+
return "gltf";
|
|
26
|
+
}
|
|
27
|
+
if (normalizedName.endsWith(".fbx")) {
|
|
28
|
+
return "fbx";
|
|
29
|
+
}
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
export function canParseModelFile(file) {
|
|
33
|
+
const filename = typeof file === "string" ? file : file.name;
|
|
34
|
+
return getModelFileKind(filename) !== null;
|
|
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
|
+
}
|
|
41
|
+
function parseModelBuffer(arrayBuffer, sourceName) {
|
|
42
|
+
const modelFileKind = getModelFileKind(sourceName);
|
|
43
|
+
if (modelFileKind === "gltf") {
|
|
44
|
+
return new Promise(resolve => {
|
|
45
|
+
gltfLoader.parse(arrayBuffer, "", gltf => {
|
|
46
|
+
resolve({ success: true, model: gltf.scene });
|
|
47
|
+
}, error => {
|
|
48
|
+
resolve({ success: false, error });
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
if (modelFileKind === "fbx") {
|
|
53
|
+
try {
|
|
54
|
+
const model = fbxLoader.parse(arrayBuffer, "");
|
|
55
|
+
return Promise.resolve({ success: true, model });
|
|
56
|
+
}
|
|
57
|
+
catch (error) {
|
|
58
|
+
return Promise.resolve({ success: false, error });
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return Promise.resolve({ success: false, error: new Error(`Unsupported file format: ${sourceName}`) });
|
|
62
|
+
}
|
|
21
63
|
export function parseModelFromFile(file) {
|
|
22
|
-
return new Promise(
|
|
64
|
+
return new Promise(resolve => {
|
|
23
65
|
const reader = new FileReader();
|
|
24
|
-
reader.onload =
|
|
66
|
+
reader.onload = event => {
|
|
25
67
|
var _a;
|
|
26
68
|
const arrayBuffer = (_a = event.target) === null || _a === void 0 ? void 0 : _a.result;
|
|
27
69
|
if (!arrayBuffer) {
|
|
28
|
-
resolve({ success: false, error: new Error(
|
|
70
|
+
resolve({ success: false, error: new Error("Failed to read file") });
|
|
29
71
|
return;
|
|
30
72
|
}
|
|
31
|
-
|
|
32
|
-
if (name.endsWith('.glb') || name.endsWith('.gltf')) {
|
|
33
|
-
gltfLoader.parse(arrayBuffer, '', (gltf) => {
|
|
34
|
-
resolve({ success: true, model: gltf.scene });
|
|
35
|
-
}, (error) => {
|
|
36
|
-
resolve({ success: false, error });
|
|
37
|
-
});
|
|
38
|
-
}
|
|
39
|
-
else if (name.endsWith('.fbx')) {
|
|
40
|
-
try {
|
|
41
|
-
const model = fbxLoader.parse(arrayBuffer, '');
|
|
42
|
-
resolve({ success: true, model });
|
|
43
|
-
}
|
|
44
|
-
catch (error) {
|
|
45
|
-
resolve({ success: false, error });
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
else {
|
|
49
|
-
resolve({ success: false, error: new Error(`Unsupported file format: ${file.name}`) });
|
|
50
|
-
}
|
|
73
|
+
void parseModelBuffer(arrayBuffer, file.name).then(resolve);
|
|
51
74
|
};
|
|
52
75
|
reader.onerror = () => resolve({ success: false, error: reader.error });
|
|
53
76
|
reader.readAsArrayBuffer(file);
|
|
54
77
|
});
|
|
55
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
|
+
}
|
|
56
92
|
export function loadModel(filename, onProgress) {
|
|
57
93
|
return __awaiter(this, void 0, void 0, function* () {
|
|
58
94
|
try {
|
|
59
|
-
// Use filename directly (should already include leading /)
|
|
60
95
|
const fullPath = filename;
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
onProgress(filename, progressEvent.loaded, total);
|
|
96
|
+
const modelFileKind = getModelFileKind(filename);
|
|
97
|
+
if (modelFileKind === "gltf") {
|
|
98
|
+
return new Promise(resolve => {
|
|
99
|
+
gltfLoader.load(fullPath, gltf => resolve({ success: true, model: gltf.scene }), progressEvent => {
|
|
100
|
+
if (!onProgress) {
|
|
101
|
+
return;
|
|
68
102
|
}
|
|
69
|
-
|
|
103
|
+
const total = progressEvent.total || progressEvent.loaded;
|
|
104
|
+
onProgress(filename, progressEvent.loaded, total);
|
|
105
|
+
}, error => resolve({ success: false, error }));
|
|
70
106
|
});
|
|
71
107
|
}
|
|
72
|
-
|
|
73
|
-
return new Promise(
|
|
74
|
-
fbxLoader.load(fullPath,
|
|
75
|
-
if (onProgress) {
|
|
76
|
-
|
|
77
|
-
const total = progressEvent.total || progressEvent.loaded;
|
|
78
|
-
onProgress(filename, progressEvent.loaded, total);
|
|
108
|
+
if (modelFileKind === "fbx") {
|
|
109
|
+
return new Promise(resolve => {
|
|
110
|
+
fbxLoader.load(fullPath, model => resolve({ success: true, model }), progressEvent => {
|
|
111
|
+
if (!onProgress) {
|
|
112
|
+
return;
|
|
79
113
|
}
|
|
80
|
-
|
|
114
|
+
const total = progressEvent.total || progressEvent.loaded;
|
|
115
|
+
onProgress(filename, progressEvent.loaded, total);
|
|
116
|
+
}, error => resolve({ success: false, error }));
|
|
81
117
|
});
|
|
82
118
|
}
|
|
83
|
-
|
|
119
|
+
return { success: false, error: new Error(`Unsupported file format: ${filename}`) };
|
|
120
|
+
}
|
|
121
|
+
catch (error) {
|
|
122
|
+
return { success: false, error };
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
export function loadTexture(filename) {
|
|
127
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
128
|
+
try {
|
|
129
|
+
if (!canParseTextureFile(filename)) {
|
|
84
130
|
return { success: false, error: new Error(`Unsupported file format: ${filename}`) };
|
|
85
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
|
+
});
|
|
86
138
|
}
|
|
87
139
|
catch (error) {
|
|
88
140
|
return { success: false, error };
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import { jsx as _jsx, jsxs as _jsxs
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
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
|
export default function Home() {
|
|
9
9
|
const [models, setModels] = useState([]);
|
|
10
|
-
return (
|
|
10
|
+
return (_jsx(DragDropLoader, { onModelLoaded: model => setModels(prev => [...prev, model]), className: "w-full items-center justify-items-center min-h-screen", style: { height: "100vh" }, children: _jsx("div", { className: "w-full items-center justify-items-center min-h-screen", style: { height: "100vh" }, children: _jsx(GameCanvas, { children: _jsxs(Physics, { children: [_jsx(RigidBody, { children: _jsxs("mesh", { castShadow: true, children: [_jsx("boxGeometry", { args: [1, 1, 1] }), _jsx("meshStandardMaterial", { color: "orange" })] }) }), _jsx(RigidBody, { type: "fixed", children: _jsxs("mesh", { position: [0, -2, 0], scale: [10, 0.1, 10], receiveShadow: true, children: [_jsx("boxGeometry", {}), _jsx("meshStandardMaterial", { color: "gray" })] }) }), models.map((model, idx) => (_jsx("primitive", { object: model, position: [0, 0, 0] }, idx))), _jsx("ambientLight", { intensity: 0.5 }), _jsx("pointLight", { position: [10, 10, 10], castShadow: true, intensity: 1000 }), _jsx(OrbitControls, {})] }) }) }) }));
|
|
11
11
|
}
|
|
@@ -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<{
|