react-three-game 0.0.57 → 0.0.59
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 +59 -35
- 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 +77 -40
- package/dist/tools/dragdrop/index.d.ts +4 -0
- package/dist/tools/dragdrop/index.js +2 -0
- package/dist/tools/dragdrop/modelLoader.d.ts +5 -6
- package/dist/tools/dragdrop/modelLoader.js +62 -49
- package/dist/tools/dragdrop/page.js +3 -3
- package/dist/tools/prefabeditor/EditorTree.js +24 -48
- package/dist/tools/prefabeditor/EditorTreeMenus.d.ts +33 -0
- package/dist/tools/prefabeditor/EditorTreeMenus.js +136 -0
- package/dist/tools/prefabeditor/PrefabEditor.js +1 -1
- package/dist/tools/prefabeditor/PrefabRoot.js +5 -3
- package/dist/tools/prefabeditor/components/CameraComponent.js +32 -12
- package/dist/tools/prefabeditor/components/DirectionalLightComponent.js +49 -23
- package/dist/tools/prefabeditor/components/MaterialComponent.d.ts +8 -0
- package/dist/tools/prefabeditor/components/MaterialComponent.js +11 -5
- package/dist/tools/prefabeditor/components/SpotLightComponent.js +34 -13
- package/package.json +2 -2
- package/react-three-game-skill/react-three-game/SKILL.md +63 -5
- 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 +118 -55
- package/src/tools/dragdrop/index.ts +4 -0
- package/src/tools/dragdrop/modelLoader.ts +95 -50
- package/src/tools/dragdrop/page.tsx +7 -4
- package/src/tools/prefabeditor/EditorTree.tsx +56 -125
- package/src/tools/prefabeditor/EditorTreeMenus.tsx +307 -0
- package/src/tools/prefabeditor/PrefabEditor.tsx +1 -1
- package/src/tools/prefabeditor/PrefabRoot.tsx +6 -3
- package/src/tools/prefabeditor/components/CameraComponent.tsx +51 -14
- package/src/tools/prefabeditor/components/DirectionalLightComponent.tsx +59 -28
- package/src/tools/prefabeditor/components/MaterialComponent.tsx +18 -9
- package/src/tools/prefabeditor/components/SpotLightComponent.tsx +49 -18
|
@@ -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,58 +16,78 @@ 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
|
|
74
|
+
interface Prefab {
|
|
75
|
+
id?: string;
|
|
76
|
+
name?: string;
|
|
77
|
+
root: GameObject;
|
|
78
|
+
}
|
|
79
|
+
|
|
63
80
|
interface GameObject {
|
|
64
81
|
id: string;
|
|
82
|
+
name?: string;
|
|
65
83
|
disabled?: boolean;
|
|
66
84
|
components?: Record<string, { type: string; properties: any }>;
|
|
67
85
|
children?: GameObject[];
|
|
68
86
|
}
|
|
69
87
|
```
|
|
70
88
|
|
|
89
|
+
`disabled` is the canonical visibility toggle in the current schema. Transforms are local to the parent node.
|
|
90
|
+
|
|
71
91
|
## Built-in Components
|
|
72
92
|
|
|
73
93
|
| Component | Key Properties |
|
|
@@ -110,7 +130,7 @@ const Rotator: Component = {
|
|
|
110
130
|
registerComponent(Rotator); // before rendering PrefabEditor
|
|
111
131
|
```
|
|
112
132
|
|
|
113
|
-
|
|
133
|
+
Components may render visible content, wrap child content, or contribute runtime behavior. Keep those semantics explicit in the component `View` rather than relying on hidden tree rules.
|
|
114
134
|
|
|
115
135
|
### Schema-Driven Field Types
|
|
116
136
|
|
|
@@ -152,12 +172,16 @@ import { PrefabEditor } from 'react-three-game';
|
|
|
152
172
|
</PrefabEditor>
|
|
153
173
|
```
|
|
154
174
|
|
|
155
|
-
Keys: **T**ranslate / **R**otate / **S**cale. Drag tree nodes to reparent.
|
|
175
|
+
Keys: **T**ranslate / **R**otate / **S**cale. Drag tree nodes to reparent. Physics only runs in play mode.
|
|
176
|
+
|
|
177
|
+
Editor menu structure:
|
|
178
|
+
- `Menu > File`: new scene, load/save prefab JSON, load prefab into scene
|
|
179
|
+
- `Menu > Export`: `GLB`, `PNG`
|
|
156
180
|
|
|
157
181
|
## Internals
|
|
158
182
|
|
|
159
183
|
- **Transforms**: Local in JSON, world computed via matrix multiplication
|
|
160
|
-
- **Instancing**: `model.properties.instanced = true`
|
|
184
|
+
- **Instancing**: `model.properties.instanced = true` switches the node to the batched instance path (`<Merged>` / `<InstancedRigidBodies>`) instead of the standard model render path
|
|
161
185
|
- **Models**: GLB/GLTF (Draco) and FBX auto-load from `filename`
|
|
162
186
|
|
|
163
187
|
## Tree Utilities
|
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 } from "./modelLoader";
|
|
3
|
+
export interface FileLoadOptions {
|
|
4
|
+
onModelLoaded?: (model: LoadedModel, filename: string, file: File) => void | Promise<void>;
|
|
5
|
+
onFileLoaded?: (file: File) => void | Promise<void>;
|
|
6
|
+
onFilesLoaded?: (files: File[]) => void | Promise<void>;
|
|
7
|
+
onModelError?: (error: unknown, filename: string, file: File) => void | Promise<void>;
|
|
8
|
+
parseModels?: boolean;
|
|
3
9
|
}
|
|
4
|
-
|
|
5
|
-
interface
|
|
6
|
-
|
|
10
|
+
type DivProps = Omit<HTMLAttributes<HTMLDivElement>, "children" | "onDrop" | "onDragOver">;
|
|
11
|
+
export interface DragDropLoaderProps extends FileLoadOptions, DivProps {
|
|
12
|
+
children?: ReactNode;
|
|
7
13
|
}
|
|
8
|
-
export
|
|
14
|
+
export interface FilePickerProps extends FileLoadOptions, DivProps {
|
|
15
|
+
accept?: string;
|
|
16
|
+
children?: ReactNode;
|
|
17
|
+
multiple?: boolean;
|
|
18
|
+
}
|
|
19
|
+
export declare function loadFiles(files: File[], { onModelLoaded, onFileLoaded, onFilesLoaded, onModelError, parseModels }: FileLoadOptions): 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,86 @@ 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
|
-
|
|
19
|
+
return t;
|
|
20
|
+
};
|
|
21
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
22
|
+
import { useRef } from "react";
|
|
23
|
+
import { canParseModelFile, parseModelFromFile } from "./modelLoader";
|
|
24
|
+
function getFiles(fileList) {
|
|
25
|
+
return fileList ? Array.from(fileList) : [];
|
|
26
|
+
}
|
|
27
|
+
export function loadFiles(files_1, _a) {
|
|
28
|
+
return __awaiter(this, arguments, void 0, function* (files, { onModelLoaded, onFileLoaded, onFilesLoaded, onModelError, parseModels = true }) {
|
|
29
|
+
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)) {
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
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 (onModelError) {
|
|
40
|
+
yield onModelError(result.error, file.name, file);
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
21
43
|
console.error("Model parse error:", result.error);
|
|
22
|
-
}
|
|
23
|
-
|
|
44
|
+
})));
|
|
45
|
+
yield (onFilesLoaded === null || onFilesLoaded === void 0 ? void 0 : onFilesLoaded(files));
|
|
46
|
+
});
|
|
24
47
|
}
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
function handleDrop(e) {
|
|
28
|
-
var _a;
|
|
29
|
-
e.preventDefault();
|
|
30
|
-
e.stopPropagation();
|
|
31
|
-
const files = ((_a = e.dataTransfer) === null || _a === void 0 ? void 0 : _a.files) ? Array.from(e.dataTransfer.files) : [];
|
|
32
|
-
handleFiles(files, onModelLoaded);
|
|
33
|
-
}
|
|
34
|
-
function handleDragOver(e) {
|
|
35
|
-
e.preventDefault();
|
|
36
|
-
e.stopPropagation();
|
|
37
|
-
}
|
|
38
|
-
window.addEventListener("drop", handleDrop);
|
|
39
|
-
window.addEventListener("dragover", handleDragOver);
|
|
40
|
-
return () => {
|
|
41
|
-
window.removeEventListener("drop", handleDrop);
|
|
42
|
-
window.removeEventListener("dragover", handleDragOver);
|
|
43
|
-
};
|
|
44
|
-
}, [onModelLoaded]);
|
|
45
|
-
return null;
|
|
48
|
+
function reportFileLoadError(error) {
|
|
49
|
+
console.error("File load error:", error);
|
|
46
50
|
}
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
+
function createLoadHandlers(options) {
|
|
52
|
+
return {
|
|
53
|
+
onFileLoaded: options.onFileLoaded,
|
|
54
|
+
onFilesLoaded: options.onFilesLoaded,
|
|
55
|
+
onModelError: options.onModelError,
|
|
56
|
+
onModelLoaded: options.onModelLoaded,
|
|
57
|
+
parseModels: options.parseModels,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
export function DragDropLoader(_a) {
|
|
61
|
+
var { children } = _a, divProps = __rest(_a, ["children"]);
|
|
62
|
+
const loadOptions = createLoadHandlers(divProps);
|
|
63
|
+
function handleDrop(event) {
|
|
64
|
+
var _a;
|
|
65
|
+
event.preventDefault();
|
|
66
|
+
event.stopPropagation();
|
|
67
|
+
void loadFiles(getFiles((_a = event.dataTransfer) === null || _a === void 0 ? void 0 : _a.files), loadOptions).catch(reportFileLoadError);
|
|
68
|
+
}
|
|
69
|
+
function handleDragOver(event) {
|
|
70
|
+
event.preventDefault();
|
|
71
|
+
event.stopPropagation();
|
|
72
|
+
}
|
|
73
|
+
return (_jsx("div", Object.assign({}, divProps, { onDrop: handleDrop, onDragOver: handleDragOver, children: children })));
|
|
74
|
+
}
|
|
75
|
+
export function FilePicker(_a) {
|
|
76
|
+
var { accept = ".glb,.gltf,.fbx", children, multiple = true } = _a, divProps = __rest(_a, ["accept", "children", "multiple"]);
|
|
77
|
+
const inputRef = useRef(null);
|
|
78
|
+
const { onClick } = divProps, wrapperProps = __rest(divProps, ["onClick"]);
|
|
79
|
+
const loadOptions = createLoadHandlers(divProps);
|
|
80
|
+
function onChange(event) {
|
|
81
|
+
void loadFiles(getFiles(event.target.files), loadOptions).catch(reportFileLoadError);
|
|
82
|
+
event.target.value = "";
|
|
83
|
+
}
|
|
84
|
+
function handleClick(event) {
|
|
85
|
+
var _a;
|
|
86
|
+
onClick === null || onClick === void 0 ? void 0 : onClick(event);
|
|
87
|
+
if (!event.defaultPrevented) {
|
|
88
|
+
(_a = inputRef.current) === null || _a === void 0 ? void 0 : _a.click();
|
|
89
|
+
}
|
|
51
90
|
}
|
|
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" })] }));
|
|
91
|
+
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
92
|
}
|
|
@@ -0,0 +1,4 @@
|
|
|
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";
|
|
@@ -1,12 +1,11 @@
|
|
|
1
|
+
import type { Object3D } from "three";
|
|
2
|
+
export type LoadedModel = Object3D;
|
|
1
3
|
export type ModelLoadResult = {
|
|
2
4
|
success: boolean;
|
|
3
|
-
model?:
|
|
4
|
-
error?:
|
|
5
|
+
model?: LoadedModel;
|
|
6
|
+
error?: unknown;
|
|
5
7
|
};
|
|
6
8
|
export type ProgressCallback = (filename: string, loaded: number, total: number) => void;
|
|
7
|
-
|
|
8
|
-
* Parse a model from a File object (e.g. from drag-drop or file picker).
|
|
9
|
-
* Returns the parsed Three.js Object3D scene.
|
|
10
|
-
*/
|
|
9
|
+
export declare function canParseModelFile(file: File | string): boolean;
|
|
11
10
|
export declare function parseModelFromFile(file: File): Promise<ModelLoadResult>;
|
|
12
11
|
export declare function loadModel(filename: string, onProgress?: ProgressCallback): Promise<ModelLoadResult>;
|
|
@@ -7,47 +7,62 @@ 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
|
-
// Singleton loader instances
|
|
10
|
+
import { DRACOLoader, FBXLoader, GLTFLoader } from "three/examples/jsm/Addons.js";
|
|
12
11
|
const dracoLoader = new DRACOLoader();
|
|
13
12
|
dracoLoader.setDecoderPath("https://www.gstatic.com/draco/v1/decoders/");
|
|
14
13
|
const gltfLoader = new GLTFLoader();
|
|
15
14
|
gltfLoader.setDRACOLoader(dracoLoader);
|
|
16
15
|
const fbxLoader = new FBXLoader();
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
16
|
+
function normalizeModelPath(name) {
|
|
17
|
+
return name.split(/[?#]/, 1)[0].toLowerCase();
|
|
18
|
+
}
|
|
19
|
+
function getModelFileKind(name) {
|
|
20
|
+
const normalizedName = normalizeModelPath(name);
|
|
21
|
+
if (normalizedName.endsWith(".glb") || normalizedName.endsWith(".gltf")) {
|
|
22
|
+
return "gltf";
|
|
23
|
+
}
|
|
24
|
+
if (normalizedName.endsWith(".fbx")) {
|
|
25
|
+
return "fbx";
|
|
26
|
+
}
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
export function canParseModelFile(file) {
|
|
30
|
+
const filename = typeof file === "string" ? file : file.name;
|
|
31
|
+
return getModelFileKind(filename) !== null;
|
|
32
|
+
}
|
|
33
|
+
function parseModelBuffer(arrayBuffer, sourceName) {
|
|
34
|
+
const modelFileKind = getModelFileKind(sourceName);
|
|
35
|
+
if (modelFileKind === "gltf") {
|
|
36
|
+
return new Promise(resolve => {
|
|
37
|
+
gltfLoader.parse(arrayBuffer, "", gltf => {
|
|
38
|
+
resolve({ success: true, model: gltf.scene });
|
|
39
|
+
}, error => {
|
|
40
|
+
resolve({ success: false, error });
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
if (modelFileKind === "fbx") {
|
|
45
|
+
try {
|
|
46
|
+
const model = fbxLoader.parse(arrayBuffer, "");
|
|
47
|
+
return Promise.resolve({ success: true, model });
|
|
48
|
+
}
|
|
49
|
+
catch (error) {
|
|
50
|
+
return Promise.resolve({ success: false, error });
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return Promise.resolve({ success: false, error: new Error(`Unsupported file format: ${sourceName}`) });
|
|
54
|
+
}
|
|
21
55
|
export function parseModelFromFile(file) {
|
|
22
|
-
return new Promise(
|
|
56
|
+
return new Promise(resolve => {
|
|
23
57
|
const reader = new FileReader();
|
|
24
|
-
reader.onload =
|
|
58
|
+
reader.onload = event => {
|
|
25
59
|
var _a;
|
|
26
60
|
const arrayBuffer = (_a = event.target) === null || _a === void 0 ? void 0 : _a.result;
|
|
27
61
|
if (!arrayBuffer) {
|
|
28
|
-
resolve({ success: false, error: new Error(
|
|
62
|
+
resolve({ success: false, error: new Error("Failed to read file") });
|
|
29
63
|
return;
|
|
30
64
|
}
|
|
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
|
-
}
|
|
65
|
+
void parseModelBuffer(arrayBuffer, file.name).then(resolve);
|
|
51
66
|
};
|
|
52
67
|
reader.onerror = () => resolve({ success: false, error: reader.error });
|
|
53
68
|
reader.readAsArrayBuffer(file);
|
|
@@ -56,33 +71,31 @@ export function parseModelFromFile(file) {
|
|
|
56
71
|
export function loadModel(filename, onProgress) {
|
|
57
72
|
return __awaiter(this, void 0, void 0, function* () {
|
|
58
73
|
try {
|
|
59
|
-
// Use filename directly (should already include leading /)
|
|
60
74
|
const fullPath = filename;
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
onProgress(filename, progressEvent.loaded, total);
|
|
75
|
+
const modelFileKind = getModelFileKind(filename);
|
|
76
|
+
if (modelFileKind === "gltf") {
|
|
77
|
+
return new Promise(resolve => {
|
|
78
|
+
gltfLoader.load(fullPath, gltf => resolve({ success: true, model: gltf.scene }), progressEvent => {
|
|
79
|
+
if (!onProgress) {
|
|
80
|
+
return;
|
|
68
81
|
}
|
|
69
|
-
|
|
82
|
+
const total = progressEvent.total || progressEvent.loaded;
|
|
83
|
+
onProgress(filename, progressEvent.loaded, total);
|
|
84
|
+
}, error => resolve({ success: false, error }));
|
|
70
85
|
});
|
|
71
86
|
}
|
|
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);
|
|
87
|
+
if (modelFileKind === "fbx") {
|
|
88
|
+
return new Promise(resolve => {
|
|
89
|
+
fbxLoader.load(fullPath, model => resolve({ success: true, model }), progressEvent => {
|
|
90
|
+
if (!onProgress) {
|
|
91
|
+
return;
|
|
79
92
|
}
|
|
80
|
-
|
|
93
|
+
const total = progressEvent.total || progressEvent.loaded;
|
|
94
|
+
onProgress(filename, progressEvent.loaded, total);
|
|
95
|
+
}, error => resolve({ success: false, error }));
|
|
81
96
|
});
|
|
82
97
|
}
|
|
83
|
-
|
|
84
|
-
return { success: false, error: new Error(`Unsupported file format: ${filename}`) };
|
|
85
|
-
}
|
|
98
|
+
return { success: false, error: new Error(`Unsupported file format: ${filename}`) };
|
|
86
99
|
}
|
|
87
100
|
catch (error) {
|
|
88
101
|
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
|
}
|