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.
@@ -44,7 +44,7 @@ const MyComponent: Component = {
44
44
 
45
45
  ## Usage Modes
46
46
 
47
- **GameCanvas + PrefabRoot**: Pure renderer for embedding prefab data in standard R3F applications. Minimal wrapper - just renders the prefab as Three.js objects. Requires manual `<Physics>` setup. Physics always active. Use this to integrate prefabs into larger R3F scenes.
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
- **GameCanvas + PrefabRoot**: Pure renderer for embedding prefab data in standard R3F applications. Minimal wrapper - just renders the prefab as Three.js objects. Requires manual `<Physics>` setup. Physics always active. Use this to integrate prefabs into larger R3F scenes.
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 '@react-three/rapier';
27
- import { GameCanvas, PrefabRoot } from 'react-three-game';
28
-
29
- <GameCanvas>
30
- <Physics>
31
- <PrefabRoot data={{
32
- root: {
33
- id: "scene",
34
- children: [
35
- {
36
- id: "ground",
37
- components: {
38
- transform: { type: "Transform", properties: { position: [0, 0, 0], rotation: [-1.57, 0, 0] } },
39
- geometry: { type: "Geometry", properties: { geometryType: "plane", args: [50, 50] } },
40
- material: { type: "Material", properties: { color: "#3a3" } },
41
- physics: { type: "Physics", properties: { type: "fixed" } }
42
- }
43
- },
44
- {
45
- id: "ball",
46
- components: {
47
- transform: { type: "Transform", properties: { position: [0, 5, 0] } },
48
- geometry: { type: "Geometry", properties: { geometryType: "sphere" } },
49
- material: { type: "Material", properties: { color: "#f66" } },
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
- </Physics>
57
- </GameCanvas>
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 { DragDropLoader } from './tools/dragdrop/DragDropLoader';
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 { DragDropLoader } from './tools/dragdrop/DragDropLoader';
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/modelLoader";
6
+ import { loadModel } from "../dragdrop";
7
7
  class ErrorBoundary extends ReactComponent {
8
8
  constructor(props) {
9
9
  super(props);
@@ -1,9 +1,22 @@
1
- interface DragDropLoaderProps {
2
- onModelLoaded: (model: any, filename: string) => void;
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
- export declare function DragDropLoader({ onModelLoaded }: DragDropLoaderProps): null;
5
- interface FilePickerProps {
6
- onModelLoaded: (model: any, filename: string) => void;
10
+ type DivProps = Omit<HTMLAttributes<HTMLDivElement>, "children" | "onDrop" | "onDragOver">;
11
+ export interface DragDropLoaderProps extends AssetLoadOptions, DivProps {
12
+ children?: ReactNode;
7
13
  }
8
- export declare function FilePicker({ onModelLoaded }: FilePickerProps): import("react/jsx-runtime").JSX.Element;
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
- import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
11
- // DragDropLoader.tsx
12
- import { useEffect } from "react";
13
- import { parseModelFromFile } from "./modelLoader";
14
- function handleFiles(files, onModelLoaded) {
15
- files.forEach((file) => __awaiter(this, void 0, void 0, function* () {
16
- const result = yield parseModelFromFile(file);
17
- if (result.success && result.model) {
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
- else {
21
- console.error("Model parse error:", result.error);
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 DragDropLoader({ onModelLoaded }) {
26
- useEffect(() => {
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;
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
- export function FilePicker({ onModelLoaded }) {
48
- function onChange(e) {
49
- const files = e.target.files ? Array.from(e.target.files) : [];
50
- handleFiles(files, onModelLoaded);
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
- // Ref for the hidden input
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";
@@ -0,0 +1,2 @@
1
+ export { DragDropLoader, FilePicker, loadFiles } from "./DragDropLoader";
2
+ export { loadModel, loadTexture, parseModelFromFile, parseTextureFromFile } 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?: any;
4
- error?: any;
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
- * Parse a model from a File object (e.g. from drag-drop or file picker).
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 { GLTFLoader, FBXLoader, DRACOLoader } from "three/examples/jsm/Addons.js";
11
- // Singleton loader instances
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
- * Parse a model from a File object (e.g. from drag-drop or file picker).
19
- * Returns the parsed Three.js Object3D scene.
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((resolve) => {
64
+ return new Promise(resolve => {
23
65
  const reader = new FileReader();
24
- reader.onload = (event) => {
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('Failed to read file') });
70
+ resolve({ success: false, error: new Error("Failed to read file") });
29
71
  return;
30
72
  }
31
- const name = file.name.toLowerCase();
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
- if (filename.endsWith('.glb') || filename.endsWith('.gltf')) {
62
- return new Promise((resolve) => {
63
- gltfLoader.load(fullPath, (gltf) => resolve({ success: true, model: gltf.scene }), (progressEvent) => {
64
- if (onProgress) {
65
- // Use loaded as total if total is not available
66
- const total = progressEvent.total || progressEvent.loaded;
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
- }, (error) => resolve({ success: false, error }));
103
+ const total = progressEvent.total || progressEvent.loaded;
104
+ onProgress(filename, progressEvent.loaded, total);
105
+ }, error => resolve({ success: false, error }));
70
106
  });
71
107
  }
72
- else if (filename.endsWith('.fbx')) {
73
- return new Promise((resolve) => {
74
- fbxLoader.load(fullPath, (model) => resolve({ success: true, model }), (progressEvent) => {
75
- if (onProgress) {
76
- // Use loaded as total if total is not available
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
- }, (error) => resolve({ success: false, error }));
114
+ const total = progressEvent.total || progressEvent.loaded;
115
+ onProgress(filename, progressEvent.loaded, total);
116
+ }, error => resolve({ success: false, error }));
81
117
  });
82
118
  }
83
- else {
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, Fragment as _Fragment } from "react/jsx-runtime";
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 "./DragDropLoader";
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 (_jsxs(_Fragment, { children: [_jsx(DragDropLoader, { onModelLoaded: model => setModels(prev => [...prev, model]) }), _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, {})] }) }) })] }));
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 { parseModelFromFile } from "../dragdrop/modelLoader";
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<{