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.
Files changed (39) hide show
  1. package/.github/copilot-instructions.md +1 -1
  2. package/README.md +59 -35
  3. package/dist/index.d.ts +1 -1
  4. package/dist/index.js +1 -1
  5. package/dist/tools/assetviewer/page.js +1 -1
  6. package/dist/tools/dragdrop/DragDropLoader.d.ts +19 -6
  7. package/dist/tools/dragdrop/DragDropLoader.js +77 -40
  8. package/dist/tools/dragdrop/index.d.ts +4 -0
  9. package/dist/tools/dragdrop/index.js +2 -0
  10. package/dist/tools/dragdrop/modelLoader.d.ts +5 -6
  11. package/dist/tools/dragdrop/modelLoader.js +62 -49
  12. package/dist/tools/dragdrop/page.js +3 -3
  13. package/dist/tools/prefabeditor/EditorTree.js +24 -48
  14. package/dist/tools/prefabeditor/EditorTreeMenus.d.ts +33 -0
  15. package/dist/tools/prefabeditor/EditorTreeMenus.js +136 -0
  16. package/dist/tools/prefabeditor/PrefabEditor.js +1 -1
  17. package/dist/tools/prefabeditor/PrefabRoot.js +5 -3
  18. package/dist/tools/prefabeditor/components/CameraComponent.js +32 -12
  19. package/dist/tools/prefabeditor/components/DirectionalLightComponent.js +49 -23
  20. package/dist/tools/prefabeditor/components/MaterialComponent.d.ts +8 -0
  21. package/dist/tools/prefabeditor/components/MaterialComponent.js +11 -5
  22. package/dist/tools/prefabeditor/components/SpotLightComponent.js +34 -13
  23. package/package.json +2 -2
  24. package/react-three-game-skill/react-three-game/SKILL.md +63 -5
  25. package/react-three-game-skill/react-three-game/rules/ADVANCED_PHYSICS.md +7 -5
  26. package/src/index.ts +1 -1
  27. package/src/tools/assetviewer/page.tsx +1 -1
  28. package/src/tools/dragdrop/DragDropLoader.tsx +118 -55
  29. package/src/tools/dragdrop/index.ts +4 -0
  30. package/src/tools/dragdrop/modelLoader.ts +95 -50
  31. package/src/tools/dragdrop/page.tsx +7 -4
  32. package/src/tools/prefabeditor/EditorTree.tsx +56 -125
  33. package/src/tools/prefabeditor/EditorTreeMenus.tsx +307 -0
  34. package/src/tools/prefabeditor/PrefabEditor.tsx +1 -1
  35. package/src/tools/prefabeditor/PrefabRoot.tsx +6 -3
  36. package/src/tools/prefabeditor/components/CameraComponent.tsx +51 -14
  37. package/src/tools/prefabeditor/components/DirectionalLightComponent.tsx +59 -28
  38. package/src/tools/prefabeditor/components/MaterialComponent.tsx +18 -9
  39. package/src/tools/prefabeditor/components/SpotLightComponent.tsx +49 -18
@@ -1,33 +1,54 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
- import { useRef, useEffect } from "react";
2
+ import { useRef, useEffect, useMemo, useState } from "react";
3
3
  import { BooleanField, ColorField, FieldGroup, NumberField } from "./Input";
4
- import { useHelper } from "@react-three/drei";
5
4
  import { SpotLightHelper } from "three";
5
+ import { useFrame } from "@react-three/fiber";
6
+ const spotLightDefaults = {
7
+ color: '#ffffff',
8
+ intensity: 1,
9
+ angle: Math.PI / 6,
10
+ penumbra: 0.5,
11
+ distance: 100,
12
+ castShadow: true,
13
+ };
6
14
  function SpotLightComponentEditor({ component, onUpdate }) {
7
- return (_jsxs(FieldGroup, { children: [_jsx(ColorField, { name: "color", label: "Color", values: component.properties, onChange: onUpdate }), _jsx(NumberField, { name: "intensity", label: "Intensity", values: component.properties, onChange: onUpdate, min: 0, step: 0.1, fallback: 1 }), _jsx(NumberField, { name: "angle", label: "Angle", values: component.properties, onChange: onUpdate, min: 0, max: Math.PI, step: 0.05, fallback: Math.PI / 6 }), _jsx(NumberField, { name: "penumbra", label: "Penumbra", values: component.properties, onChange: onUpdate, min: 0, max: 1, step: 0.05, fallback: 0.5 }), _jsx(NumberField, { name: "distance", label: "Distance", values: component.properties, onChange: onUpdate, min: 0, step: 1, fallback: 100 }), _jsx(BooleanField, { name: "castShadow", label: "Cast Shadow", values: component.properties, onChange: onUpdate, fallback: true })] }));
15
+ const values = Object.assign(Object.assign({}, spotLightDefaults), component.properties);
16
+ return (_jsxs(FieldGroup, { children: [_jsx(ColorField, { name: "color", label: "Color", values: values, onChange: onUpdate }), _jsx(NumberField, { name: "intensity", label: "Intensity", values: values, onChange: onUpdate, min: 0, step: 0.1, fallback: 1 }), _jsx(NumberField, { name: "angle", label: "Angle", values: values, onChange: onUpdate, min: 0, max: Math.PI, step: 0.05, fallback: Math.PI / 6 }), _jsx(NumberField, { name: "penumbra", label: "Penumbra", values: values, onChange: onUpdate, min: 0, max: 1, step: 0.05, fallback: 0.5 }), _jsx(NumberField, { name: "distance", label: "Distance", values: values, onChange: onUpdate, min: 0, step: 1, fallback: 100 }), _jsx(BooleanField, { name: "castShadow", label: "Cast Shadow", values: values, onChange: onUpdate, fallback: true })] }));
8
17
  }
9
18
  function SpotLightView({ properties, editMode, isSelected }) {
10
- var _a, _b, _c, _d, _e, _f;
11
- const color = (_a = properties.color) !== null && _a !== void 0 ? _a : '#ffffff';
12
- const intensity = (_b = properties.intensity) !== null && _b !== void 0 ? _b : 1.0;
13
- const angle = (_c = properties.angle) !== null && _c !== void 0 ? _c : Math.PI / 6;
14
- const penumbra = (_d = properties.penumbra) !== null && _d !== void 0 ? _d : 0.5;
15
- const distance = (_e = properties.distance) !== null && _e !== void 0 ? _e : 100;
16
- const castShadow = (_f = properties.castShadow) !== null && _f !== void 0 ? _f : true;
19
+ const merged = Object.assign(Object.assign({}, spotLightDefaults), properties);
20
+ const color = merged.color;
21
+ const intensity = merged.intensity;
22
+ const angle = merged.angle;
23
+ const penumbra = merged.penumbra;
24
+ const distance = merged.distance;
25
+ const castShadow = merged.castShadow;
17
26
  const spotLightRef = useRef(null);
18
27
  const targetRef = useRef(null);
19
- useHelper(editMode && isSelected ? spotLightRef : null, SpotLightHelper, color);
28
+ const [spotLight, setSpotLight] = useState(null);
29
+ const spotLightHelper = useMemo(() => spotLight ? new SpotLightHelper(spotLight, color) : null, [spotLight, color]);
30
+ useEffect(() => {
31
+ return () => {
32
+ spotLightHelper === null || spotLightHelper === void 0 ? void 0 : spotLightHelper.dispose();
33
+ };
34
+ }, [spotLightHelper]);
20
35
  useEffect(() => {
21
36
  if (spotLightRef.current && targetRef.current) {
22
37
  spotLightRef.current.target = targetRef.current;
38
+ setSpotLight(spotLightRef.current);
23
39
  }
24
40
  }, []);
25
- return (_jsxs(_Fragment, { children: [_jsx("spotLight", { ref: spotLightRef, color: color, intensity: intensity, angle: angle, penumbra: penumbra, distance: distance, castShadow: castShadow, "shadow-mapSize-width": 1024, "shadow-mapSize-height": 1024, "shadow-bias": -0.0001, "shadow-normalBias": 0.02 }), _jsx("object3D", { ref: targetRef, position: [0, -5, 0] }), editMode && (_jsxs(_Fragment, { children: [_jsxs("mesh", { children: [_jsx("sphereGeometry", { args: [0.2, 8, 6] }), _jsx("meshBasicMaterial", { color: color, wireframe: true })] }), _jsxs("mesh", { position: [0, -5, 0], children: [_jsx("sphereGeometry", { args: [0.15, 8, 6] }), _jsx("meshBasicMaterial", { color: color, wireframe: true, opacity: 0.5, transparent: true })] })] }))] }));
41
+ useFrame(() => {
42
+ if (spotLightHelper && editMode && isSelected) {
43
+ spotLightHelper.update();
44
+ }
45
+ });
46
+ return (_jsxs(_Fragment, { children: [_jsx("spotLight", { ref: spotLightRef, color: color, intensity: intensity, angle: angle, penumbra: penumbra, distance: distance, castShadow: castShadow, "shadow-mapSize-width": 1024, "shadow-mapSize-height": 1024, "shadow-bias": -0.0001, "shadow-normalBias": 0.02 }), editMode && isSelected && spotLightHelper && (_jsx("primitive", { object: spotLightHelper })), _jsx("object3D", { ref: targetRef, position: [0, -5, 0] }), editMode && (_jsxs(_Fragment, { children: [_jsxs("mesh", { children: [_jsx("sphereGeometry", { args: [0.2, 8, 6] }), _jsx("meshBasicMaterial", { color: color, wireframe: true })] }), _jsxs("mesh", { position: [0, -5, 0], children: [_jsx("sphereGeometry", { args: [0.15, 8, 6] }), _jsx("meshBasicMaterial", { color: color, wireframe: true, opacity: 0.5, transparent: true })] })] }))] }));
26
47
  }
27
48
  const SpotLightComponent = {
28
49
  name: 'SpotLight',
29
50
  Editor: SpotLightComponentEditor,
30
51
  View: SpotLightView,
31
- defaultProperties: {}
52
+ defaultProperties: spotLightDefaults
32
53
  };
33
54
  export default SpotLightComponent;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "react-three-game",
3
- "version": "0.0.57",
4
- "description": "Batteries included React Three Fiber game engine",
3
+ "version": "0.0.59",
4
+ "description": "high performance 3D game engine for React",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.js",
7
7
  "types": "dist/index.d.ts",
@@ -81,12 +81,15 @@ Every game object follows this schema:
81
81
  ```typescript
82
82
  interface GameObject {
83
83
  id: string;
84
+ name?: string;
84
85
  disabled?: boolean;
85
86
  components?: Record<string, { type: string; properties: any }>;
86
87
  children?: GameObject[];
87
88
  }
88
89
  ```
89
90
 
91
+ `disabled` is the canonical visibility toggle. Transforms are local to the parent node.
92
+
90
93
  ### Prefab JSON Format
91
94
 
92
95
  Scenes are defined as JSON prefabs with a root node containing children:
@@ -121,6 +124,8 @@ Scenes are defined as JSON prefabs with a root node containing children:
121
124
  | SpotLight | `SpotLight` | `color`, `intensity`, `angle`, `penumbra`, `distance?`, `castShadow?` |
122
125
  | DirectionalLight | `DirectionalLight` | `color`, `intensity`, `castShadow?`, `targetOffset?: [x,y,z]` |
123
126
  | AmbientLight | `AmbientLight` | `color`, `intensity` |
127
+ | Environment | `Environment` | `intensity`, `resolution` |
128
+ | Camera | `Camera` | `fov`, `near`, `far`, `zoom` |
124
129
  | Text | `Text` | `text`, `font`, `size`, `depth`, `width`, `align`, `color` |
125
130
 
126
131
  ### Text Component
@@ -164,21 +169,24 @@ Use radians: `1.57` = 90°, `3.14` = 180°, `-1.57` = -90°
164
169
 
165
170
  ### Usage Modes
166
171
 
167
- **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.
172
+ **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.
168
173
 
169
174
  ```jsx
175
+ import { Canvas } from '@react-three/fiber';
170
176
  import { Physics } from '@react-three/rapier';
171
- import { GameCanvas, PrefabRoot } from 'react-three-game';
177
+ import { PrefabRoot } from 'react-three-game';
172
178
 
173
- <GameCanvas>
179
+ <Canvas>
174
180
  <Physics>
175
181
  <PrefabRoot data={prefabData} />
176
182
  <CustomComponent />
177
183
  </Physics>
178
- </GameCanvas>
184
+ </Canvas>
179
185
  ```
180
186
 
181
- **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.
187
+ `GameCanvas` provides the library's WebGPU canvas setup.
188
+
189
+ **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. Editor actions live under `Menu > File`, and exports under `Menu > Export`.
182
190
 
183
191
  ```jsx
184
192
  import { PrefabEditor } from 'react-three-game';
@@ -228,6 +236,36 @@ function DynamicLight() {
228
236
 
229
237
  **Use cases**: Player controllers, AI behaviors, procedural animation, real-time effects.
230
238
 
239
+ ## World Scene Pattern
240
+
241
+ The current world demo combines prefab-authored level geometry with runtime React behavior:
242
+
243
+ - Static level layout, props, and collision live in prefab JSON.
244
+ - `Environment` can wrap sky geometry or lighting content for a full scene backdrop.
245
+ - `Camera` can live in the prefab so view-only scenes and editor scenes share the same authored viewpoint.
246
+ - Runtime logic can use `useFrame` plus `updateNodeById` to animate prefab entities without abandoning the JSON scene model.
247
+
248
+ ```json
249
+ {
250
+ "id": "environment",
251
+ "components": {
252
+ "environment": {
253
+ "type": "Environment",
254
+ "properties": { "intensity": 1, "resolution": 256 }
255
+ }
256
+ },
257
+ "children": [
258
+ {
259
+ "id": "sky",
260
+ "components": {
261
+ "geometry": { "type": "Geometry", "properties": { "geometryType": "sphere", "args": [100, 32, 16] } },
262
+ "material": { "type": "Material", "properties": { "texture": "/textures/skybox/skybox1.jpg", "side": "BackSide", "materialType": "basic" } }
263
+ }
264
+ }
265
+ ]
266
+ }
267
+ ```
268
+
231
269
  ## Quick Reference Examples
232
270
 
233
271
  ```json
@@ -376,6 +414,26 @@ const MyComponent: Component = {
376
414
  registerComponent(MyComponent);
377
415
  ```
378
416
 
417
+ Use the component in prefab JSON by adding a component entry whose `type` matches the registered component name:
418
+
419
+ ```json
420
+ {
421
+ "components": {
422
+ "mycomponent": {
423
+ "type": "MyComponent",
424
+ "properties": {
425
+ "speed": 1
426
+ }
427
+ }
428
+ }
429
+ }
430
+ ```
431
+
432
+ Rules:
433
+ - Call `registerComponent(MyComponent)` before rendering `<PrefabEditor>` or `<PrefabRoot>` with prefab data that uses it.
434
+ - `type` must match the registered component name exactly (`name: 'MyComponent'` -> `"type": "MyComponent"`).
435
+ - Use `View` to render visible content, wrap `children`, or add runtime behavior with hooks like `useFrame`.
436
+
379
437
  **Field types**: `vector3`, `number`, `string`, `color`, `boolean`, `select`, `custom`
380
438
 
381
439
  ## Game Events
@@ -48,6 +48,7 @@ Complete reference for `Physics` component properties:
48
48
  | `lockRotations` | `boolean` | `false` | Freeze rotation |
49
49
  | `enabledTranslations` | `[bool, bool, bool]` | `[true, true, true]` | Lock per axis (X, Y, Z) |
50
50
  | `enabledRotations` | `[bool, bool, bool]` | `[true, true, true]` | Lock rotation per axis |
51
+ | `colliders` | `'hull'` \| `'trimesh'` \| `'cuboid'` \| `'ball'` | auto | Collider shape override (`fixed` defaults to `trimesh`, others to `hull`) |
51
52
  | `ccd` | `boolean` | `false` | Continuous collision detection (fast objects) |
52
53
  | `sensor` | `boolean` | `false` | Trigger only, no collision response |
53
54
  | `activeCollisionTypes` | `'all'` | - | Enable kinematic/fixed collision detection (default: dynamic only) |
@@ -284,21 +285,21 @@ Objects will **slide off** the tilted surface.
284
285
 
285
286
  ## Instanced Physics
286
287
 
287
- When using `"instanced": true` on models, physics behaves differently than standard objects. **All instances of the same model share a single `InstancedRigidBodies` component** for optimal GPU performance.
288
+ When using `"instanced": true` on models, physics behaves differently than standard objects. Physics instancing is designed for batched `fixed` and `dynamic` bodies, where instances of the same model share an `InstancedRigidBodies` path for better performance.
288
289
 
289
290
  ### Standard vs Instanced Physics
290
291
 
291
292
  | Aspect | Standard Physics | Instanced Physics |
292
293
  |--------|------------------|-------------------|
293
- | RigidBody Component | Individual `<RigidBody>` per object | Single `<InstancedRigidBodies>` for all instances |
294
+ | RigidBody Component | Individual `<RigidBody>` per object | Single `<InstancedRigidBodies>` group per model + supported physics type |
294
295
  | Ref Access | `rigidBodyRefs.get(nodeId)` returns single RigidBody | Not accessible via `rigidBodyRefs` |
295
296
  | Force Application | Direct per-object | Must access via InstancedRigidBodies ref |
296
- | Collider Type | `hull` (dynamic) or `trimesh` (fixed) | Same, auto-selected |
297
+ | Collider Type | `hull` (dynamic) or `trimesh` (fixed) | Auto-selected by instanced physics path |
297
298
  | Performance | One draw call per object | One draw call for all instances |
298
299
 
299
300
  ### Defining Instanced Objects
300
301
 
301
- Set `"instanced": true` in the model component. **All instances of the same model+physics type are automatically batched**:
302
+ Set `"instanced": true` in the model component. **Instances with the same model path and supported physics type are automatically batched**:
302
303
 
303
304
  ```json
304
305
  {
@@ -326,7 +327,7 @@ Add multiple instances - they'll be automatically batched:
326
327
 
327
328
  ### Force Application on Instanced Objects
328
329
 
329
- **Instanced physics bodies are not individually accessible.** For objects requiring force/impulse control, use non-instanced physics (`"instanced": false` or omit the property).
330
+ **Instanced physics bodies are not individually accessible.** For objects requiring force/impulse control, kinematic motion, or per-body refs, use non-instanced physics (`"instanced": false` or omit the property).
330
331
 
331
332
  ### When to Use Instanced Physics
332
333
 
@@ -345,6 +346,7 @@ Add multiple instances - they'll be automatically batched:
345
346
  ### Performance Notes
346
347
 
347
348
  - **Batching**: All instances with the same `filename` and `physics.type` are rendered in a single draw call
349
+ - **Supported body types**: The instanced physics path is intended for `fixed` and `dynamic` bodies; use standard non-instanced physics for kinematic bodies
348
350
  - **Scale handling**: Visual scale is applied per-instance, but collider scale may differ
349
351
  - **Transform updates**: Use `updateNodeById` to move instances (triggers re-sync)
350
352
  - **Memory**: One set of GPU buffers shared across all instances
package/src/index.ts CHANGED
@@ -50,7 +50,7 @@ export { entityEvents, useEntityEvent } from './tools/prefabeditor/GameEvents';
50
50
  export type { EntityEventType, EntityEventPayload } from './tools/prefabeditor/GameEvents';
51
51
 
52
52
  // Asset Tools
53
- export { DragDropLoader } from './tools/dragdrop/DragDropLoader';
53
+ export * from './tools/dragdrop';
54
54
  export {
55
55
  TextureListViewer,
56
56
  ModelListViewer,
@@ -2,7 +2,7 @@ import { Canvas } from "@react-three/fiber";
2
2
  import { OrbitControls, Stage, View, PerspectiveCamera } from "@react-three/drei";
3
3
  import { Component as ReactComponent, Suspense, useEffect, useState, useRef } from "react";
4
4
  import { TextureLoader } from "three";
5
- import { loadModel } from "../dragdrop/modelLoader";
5
+ import { loadModel } from "../dragdrop";
6
6
 
7
7
  class ErrorBoundary extends ReactComponent<{ onError?: () => void; children: React.ReactNode }, { hasError: boolean }> {
8
8
  constructor(props: any) {
@@ -1,73 +1,136 @@
1
- // DragDropLoader.tsx
2
- import { useEffect, ChangeEvent } from "react";
3
- import { parseModelFromFile } from "./modelLoader";
1
+ import { ChangeEvent, useRef } from "react";
2
+ import type { DragEvent, HTMLAttributes, MouseEvent, ReactNode } from "react";
3
+ import type { LoadedModel } from "./modelLoader";
4
+ import { canParseModelFile, parseModelFromFile } from "./modelLoader";
4
5
 
5
- interface DragDropLoaderProps {
6
- onModelLoaded: (model: any, filename: string) => void;
6
+ export interface FileLoadOptions {
7
+ onModelLoaded?: (model: LoadedModel, filename: string, file: File) => void | Promise<void>;
8
+ onFileLoaded?: (file: File) => void | Promise<void>;
9
+ onFilesLoaded?: (files: File[]) => void | Promise<void>;
10
+ onModelError?: (error: unknown, filename: string, file: File) => void | Promise<void>;
11
+ parseModels?: boolean;
7
12
  }
8
13
 
9
- function handleFiles(files: File[], onModelLoaded: (model: any, filename: string) => void) {
10
- files.forEach(async (file) => {
11
- const result = await parseModelFromFile(file);
12
- if (result.success && result.model) {
13
- onModelLoaded(result.model, file.name);
14
- } else {
14
+ type DivProps = Omit<HTMLAttributes<HTMLDivElement>, "children" | "onDrop" | "onDragOver">;
15
+
16
+ export interface DragDropLoaderProps extends FileLoadOptions, DivProps {
17
+ children?: ReactNode;
18
+ }
19
+
20
+ export interface FilePickerProps extends FileLoadOptions, DivProps {
21
+ accept?: string;
22
+ children?: ReactNode;
23
+ multiple?: boolean;
24
+ }
25
+
26
+ function getFiles(fileList?: FileList | null) {
27
+ return fileList ? Array.from(fileList) : [];
28
+ }
29
+
30
+ export async function loadFiles(
31
+ files: File[],
32
+ { onModelLoaded, onFileLoaded, onFilesLoaded, onModelError, parseModels = true }: FileLoadOptions,
33
+ ) {
34
+ await Promise.all(
35
+ files.map(async (file) => {
36
+ await onFileLoaded?.(file);
37
+
38
+ if (!parseModels || !canParseModelFile(file) || (!onModelLoaded && !onModelError)) {
39
+ return;
40
+ }
41
+
42
+ const result = await parseModelFromFile(file);
43
+
44
+ if (result.success && result.model) {
45
+ await onModelLoaded?.(result.model, file.name, file);
46
+ return;
47
+ }
48
+
49
+ if (onModelError) {
50
+ await onModelError(result.error, file.name, file);
51
+ return;
52
+ }
53
+
15
54
  console.error("Model parse error:", result.error);
16
- }
17
- });
55
+ }),
56
+ );
57
+
58
+ await onFilesLoaded?.(files);
18
59
  }
19
60
 
20
- export function DragDropLoader({ onModelLoaded }: DragDropLoaderProps) {
21
- useEffect(() => {
22
- function handleDrop(e: DragEvent) {
23
- e.preventDefault();
24
- e.stopPropagation();
25
- const files = e.dataTransfer?.files ? Array.from(e.dataTransfer.files) : [];
26
- handleFiles(files, onModelLoaded);
27
- }
28
- function handleDragOver(e: DragEvent) {
29
- e.preventDefault();
30
- e.stopPropagation();
31
- }
32
- window.addEventListener("drop", handleDrop);
33
- window.addEventListener("dragover", handleDragOver);
34
- return () => {
35
- window.removeEventListener("drop", handleDrop);
36
- window.removeEventListener("dragover", handleDragOver);
37
- };
38
- }, [onModelLoaded]);
39
- return null;
61
+ function reportFileLoadError(error: unknown) {
62
+ console.error("File load error:", error);
40
63
  }
41
64
 
42
- // FilePicker component
43
- interface FilePickerProps {
44
- onModelLoaded: (model: any, filename: string) => void;
65
+ function createLoadHandlers(options: FileLoadOptions) {
66
+ return {
67
+ onFileLoaded: options.onFileLoaded,
68
+ onFilesLoaded: options.onFilesLoaded,
69
+ onModelError: options.onModelError,
70
+ onModelLoaded: options.onModelLoaded,
71
+ parseModels: options.parseModels,
72
+ } satisfies FileLoadOptions;
45
73
  }
46
74
 
47
- export function FilePicker({ onModelLoaded }: FilePickerProps) {
48
- function onChange(e: ChangeEvent<HTMLInputElement>) {
49
- const files = e.target.files ? Array.from(e.target.files) : [];
50
- handleFiles(files, onModelLoaded);
75
+ export function DragDropLoader({
76
+ children,
77
+ ...divProps
78
+ }: DragDropLoaderProps) {
79
+ const loadOptions = createLoadHandlers(divProps);
80
+
81
+ function handleDrop(event: DragEvent<HTMLDivElement>) {
82
+ event.preventDefault();
83
+ event.stopPropagation();
84
+
85
+ void loadFiles(getFiles(event.dataTransfer?.files), loadOptions).catch(reportFileLoadError);
86
+ }
87
+
88
+ function handleDragOver(event: DragEvent<HTMLDivElement>) {
89
+ event.preventDefault();
90
+ event.stopPropagation();
91
+ }
92
+
93
+ return (
94
+ <div {...divProps} onDrop={handleDrop} onDragOver={handleDragOver}>
95
+ {children}
96
+ </div>
97
+ );
98
+ }
99
+
100
+ export function FilePicker({
101
+ accept = ".glb,.gltf,.fbx",
102
+ children,
103
+ multiple = true,
104
+ ...divProps
105
+ }: FilePickerProps) {
106
+ const inputRef = useRef<HTMLInputElement>(null);
107
+ const { onClick, ...wrapperProps } = divProps;
108
+ const loadOptions = createLoadHandlers(divProps);
109
+
110
+ function onChange(event: ChangeEvent<HTMLInputElement>) {
111
+ void loadFiles(getFiles(event.target.files), loadOptions).catch(reportFileLoadError);
112
+ event.target.value = "";
113
+ }
114
+
115
+ function handleClick(event: MouseEvent<HTMLDivElement>) {
116
+ onClick?.(event);
117
+
118
+ if (!event.defaultPrevented) {
119
+ inputRef.current?.click();
120
+ }
51
121
  }
52
- // Ref for the hidden input
53
- const inputId = "file-picker-input";
122
+
54
123
  return (
55
- <>
124
+ <div {...wrapperProps} onClick={handleClick}>
56
125
  <input
57
- id={inputId}
126
+ ref={inputRef}
58
127
  type="file"
59
- accept=".glb,.gltf,.fbx"
60
- multiple
128
+ accept={accept}
129
+ multiple={multiple}
61
130
  onChange={onChange}
62
- className="hidden"
131
+ hidden
63
132
  />
64
- <button
65
- 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"
66
- type="button"
67
- onClick={() => document.getElementById(inputId)?.click()}
68
- >
69
- Select Files
70
- </button>
71
- </>
133
+ {children ?? "Select Files"}
134
+ </div>
72
135
  );
73
136
  }
@@ -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";