react-three-game 0.0.41 → 0.0.44

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 (65) hide show
  1. package/README.md +0 -17
  2. package/dist/helpers/SoundManager.d.ts +35 -0
  3. package/dist/helpers/SoundManager.js +93 -0
  4. package/dist/helpers/index.d.ts +35 -0
  5. package/dist/helpers/index.js +44 -0
  6. package/dist/index.d.ts +14 -0
  7. package/dist/index.js +14 -0
  8. package/dist/shared/GameCanvas.d.ts +9 -0
  9. package/dist/shared/GameCanvas.js +47 -0
  10. package/dist/tools/assetviewer/page.d.ts +35 -0
  11. package/dist/tools/assetviewer/page.js +166 -0
  12. package/dist/tools/dragdrop/DragDropLoader.d.ts +9 -0
  13. package/dist/tools/dragdrop/DragDropLoader.js +78 -0
  14. package/dist/tools/dragdrop/modelLoader.d.ts +7 -0
  15. package/dist/tools/dragdrop/modelLoader.js +52 -0
  16. package/dist/tools/dragdrop/page.d.ts +1 -0
  17. package/dist/tools/dragdrop/page.js +11 -0
  18. package/dist/tools/prefabeditor/EditorContext.d.ts +11 -0
  19. package/dist/tools/prefabeditor/EditorContext.js +9 -0
  20. package/dist/tools/prefabeditor/EditorTree.d.ts +12 -0
  21. package/dist/tools/prefabeditor/EditorTree.js +150 -0
  22. package/dist/tools/prefabeditor/EditorUI.d.ts +14 -0
  23. package/dist/tools/prefabeditor/EditorUI.js +71 -0
  24. package/dist/tools/prefabeditor/EventSystem.d.ts +7 -0
  25. package/dist/tools/prefabeditor/EventSystem.js +23 -0
  26. package/dist/tools/prefabeditor/InstanceProvider.d.ts +30 -0
  27. package/dist/tools/prefabeditor/InstanceProvider.js +254 -0
  28. package/dist/tools/prefabeditor/PrefabEditor.d.ts +16 -0
  29. package/dist/tools/prefabeditor/PrefabEditor.js +140 -0
  30. package/dist/tools/prefabeditor/PrefabRoot.d.ts +28 -0
  31. package/dist/tools/prefabeditor/PrefabRoot.js +293 -0
  32. package/dist/tools/prefabeditor/components/ComponentRegistry.d.ts +18 -0
  33. package/dist/tools/prefabeditor/components/ComponentRegistry.js +13 -0
  34. package/dist/tools/prefabeditor/components/DirectionalLightComponent.d.ts +3 -0
  35. package/dist/tools/prefabeditor/components/DirectionalLightComponent.js +78 -0
  36. package/dist/tools/prefabeditor/components/GeometryComponent.d.ts +3 -0
  37. package/dist/tools/prefabeditor/components/GeometryComponent.js +66 -0
  38. package/dist/tools/prefabeditor/components/Input.d.ts +20 -0
  39. package/dist/tools/prefabeditor/components/Input.js +129 -0
  40. package/dist/tools/prefabeditor/components/MaterialComponent.d.ts +3 -0
  41. package/dist/tools/prefabeditor/components/MaterialComponent.js +100 -0
  42. package/dist/tools/prefabeditor/components/ModelComponent.d.ts +3 -0
  43. package/dist/tools/prefabeditor/components/ModelComponent.js +57 -0
  44. package/dist/tools/prefabeditor/components/PhysicsComponent.d.ts +10 -0
  45. package/dist/tools/prefabeditor/components/PhysicsComponent.js +33 -0
  46. package/dist/tools/prefabeditor/components/SpotLightComponent.d.ts +3 -0
  47. package/dist/tools/prefabeditor/components/SpotLightComponent.js +49 -0
  48. package/dist/tools/prefabeditor/components/TransformComponent.d.ts +3 -0
  49. package/dist/tools/prefabeditor/components/TransformComponent.js +42 -0
  50. package/dist/tools/prefabeditor/components/index.d.ts +2 -0
  51. package/dist/tools/prefabeditor/components/index.js +16 -0
  52. package/dist/tools/prefabeditor/page.d.ts +1 -0
  53. package/dist/tools/prefabeditor/page.js +5 -0
  54. package/dist/tools/prefabeditor/styles.d.ts +1809 -0
  55. package/dist/tools/prefabeditor/styles.js +167 -0
  56. package/dist/tools/prefabeditor/types.d.ts +19 -0
  57. package/dist/tools/prefabeditor/types.js +1 -0
  58. package/dist/tools/prefabeditor/utils.d.ts +26 -0
  59. package/dist/tools/prefabeditor/utils.js +131 -0
  60. package/package.json +2 -4
  61. package/src/shared/GameCanvas.tsx +1 -1
  62. package/src/tools/assetviewer/page.tsx +1 -3
  63. package/.claude/settings.local.json +0 -9
  64. package/dist/index.umd.js +0 -438
  65. package/vite.config.ts +0 -34
package/README.md CHANGED
@@ -6,23 +6,6 @@ JSON-first 3D game engine. React Three Fiber + WebGPU + Rapier Physics.
6
6
  npm i react-three-game @react-three/fiber @react-three/rapier three
7
7
  ```
8
8
 
9
- ### CDN / UMD Build
10
-
11
- For use with script tags (no build tools):
12
-
13
- ```html
14
- <script src="https://unpkg.com/react@18/umd/react.production.min.js"></script>
15
- <script src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js"></script>
16
- <script src="https://unpkg.com/react-three-game@0.0.41/dist/index.umd.js"></script>
17
-
18
- <script>
19
- const { ModelListViewer, SharedCanvas, TextureListViewer } = window.ReactThreeGame;
20
- // Use components...
21
- </script>
22
- ```
23
-
24
- See [docs/public/test-umd.html](docs/public/test-umd.html) for a complete example.
25
-
26
9
  ![Prefab Editor](assets/editor.gif)
27
10
  ![Architecture](assets/architecture.png)
28
11
 
@@ -0,0 +1,35 @@
1
+ declare class SoundManager {
2
+ private static _instance;
3
+ context: AudioContext;
4
+ private buffers;
5
+ private masterGain;
6
+ private sfxGain;
7
+ private musicGain;
8
+ private constructor();
9
+ /** Singleton accessor */
10
+ static get instance(): SoundManager;
11
+ /** Required once after user gesture (browser) */
12
+ resume(): void;
13
+ /** Preload a sound from URL */
14
+ load(path: string, url: string): Promise<void>;
15
+ /** Play from already-loaded buffer (fails silently if not loaded) */
16
+ playSync(path: string, { volume, playbackRate, detune, pitch, }?: {
17
+ volume?: number;
18
+ playbackRate?: number;
19
+ detune?: number;
20
+ pitch?: number;
21
+ }): void;
22
+ /** Load and play SFX - accepts file path directly */
23
+ play(path: string, options?: {
24
+ volume?: number;
25
+ playbackRate?: number;
26
+ detune?: number;
27
+ pitch?: number;
28
+ }): Promise<void>;
29
+ /** Volume controls */
30
+ setMasterVolume(v: number): void;
31
+ setSfxVolume(v: number): void;
32
+ setMusicVolume(v: number): void;
33
+ }
34
+ export declare const sound: SoundManager;
35
+ export {};
@@ -0,0 +1,93 @@
1
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
2
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
3
+ return new (P || (P = Promise))(function (resolve, reject) {
4
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
5
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
6
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
7
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
8
+ });
9
+ };
10
+ class SoundManager {
11
+ constructor() {
12
+ this.buffers = new Map();
13
+ const AudioCtx = window.AudioContext || window.webkitAudioContext;
14
+ this.context = new AudioCtx();
15
+ this.masterGain = this.context.createGain();
16
+ this.sfxGain = this.context.createGain();
17
+ this.musicGain = this.context.createGain();
18
+ this.sfxGain.connect(this.masterGain);
19
+ this.musicGain.connect(this.masterGain);
20
+ this.masterGain.connect(this.context.destination);
21
+ this.masterGain.gain.value = 1;
22
+ this.sfxGain.gain.value = 1;
23
+ this.musicGain.gain.value = 1;
24
+ }
25
+ /** Singleton accessor */
26
+ static get instance() {
27
+ if (typeof window === 'undefined') {
28
+ // Return a dummy instance for SSR
29
+ return new Proxy({}, {
30
+ get: () => () => { }
31
+ });
32
+ }
33
+ if (!SoundManager._instance) {
34
+ SoundManager._instance = new SoundManager();
35
+ }
36
+ return SoundManager._instance;
37
+ }
38
+ /** Required once after user gesture (browser) */
39
+ resume() {
40
+ if (this.context.state !== "running") {
41
+ this.context.resume();
42
+ }
43
+ }
44
+ /** Preload a sound from URL */
45
+ load(path, url) {
46
+ return __awaiter(this, void 0, void 0, function* () {
47
+ if (this.buffers.has(path))
48
+ return;
49
+ const res = yield fetch(url);
50
+ const arrayBuffer = yield res.arrayBuffer();
51
+ const buffer = yield this.context.decodeAudioData(arrayBuffer);
52
+ this.buffers.set(path, buffer);
53
+ });
54
+ }
55
+ /** Play from already-loaded buffer (fails silently if not loaded) */
56
+ playSync(path, { volume = 1, playbackRate = 1, detune = 0, pitch = 1, } = {}) {
57
+ this.resume();
58
+ const buffer = this.buffers.get(path);
59
+ if (!buffer)
60
+ return;
61
+ const src = this.context.createBufferSource();
62
+ const gain = this.context.createGain();
63
+ src.buffer = buffer;
64
+ src.playbackRate.value = playbackRate * pitch;
65
+ src.detune.value = detune;
66
+ gain.gain.value = volume;
67
+ src.connect(gain);
68
+ gain.connect(this.sfxGain);
69
+ src.start();
70
+ }
71
+ /** Load and play SFX - accepts file path directly */
72
+ play(path, options) {
73
+ return __awaiter(this, void 0, void 0, function* () {
74
+ // Auto-load from path if not already loaded
75
+ if (!this.buffers.has(path)) {
76
+ yield this.load(path, path);
77
+ }
78
+ this.playSync(path, options);
79
+ });
80
+ }
81
+ /** Volume controls */
82
+ setMasterVolume(v) {
83
+ this.masterGain.gain.value = v;
84
+ }
85
+ setSfxVolume(v) {
86
+ this.sfxGain.gain.value = v;
87
+ }
88
+ setMusicVolume(v) {
89
+ this.musicGain.gain.value = v;
90
+ }
91
+ }
92
+ SoundManager._instance = null;
93
+ export const sound = SoundManager.instance;
@@ -0,0 +1,35 @@
1
+ import type { GameObject } from "../tools/prefabeditor/types";
2
+ export type Vec3 = [number, number, number];
3
+ export interface GroundOptions {
4
+ /** GameObject id. Defaults to "ground". */
5
+ id?: string;
6
+ /** Plane size. Defaults to 50. */
7
+ size?: number;
8
+ /** Transform overrides. */
9
+ position?: Vec3;
10
+ rotation?: Vec3;
11
+ scale?: Vec3;
12
+ /** Material overrides. */
13
+ color?: string;
14
+ texture?: string;
15
+ /** When true, set repeat wrapping. Defaults to true if texture is provided. */
16
+ repeat?: boolean;
17
+ /** Texture repeat counts when repeat=true. Defaults to [25,25]. */
18
+ repeatCount?: [number, number];
19
+ /** Physics body type. Defaults to "fixed". */
20
+ physicsType?: "fixed" | "dynamic" | "kinematic";
21
+ /** Set true to hide the node. */
22
+ hidden?: boolean;
23
+ /** Set true to disable the node. */
24
+ disabled?: boolean;
25
+ }
26
+ /**
27
+ * Create a ready-to-use plane ground GameObject.
28
+ *
29
+ * Designed to reduce prefab boilerplate:
30
+ * - Transform (rotated to lie flat)
31
+ * - Geometry (plane)
32
+ * - Material (optional texture + repeat)
33
+ * - Physics (fixed by default)
34
+ */
35
+ export declare function ground(options?: GroundOptions): GameObject;
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Create a ready-to-use plane ground GameObject.
3
+ *
4
+ * Designed to reduce prefab boilerplate:
5
+ * - Transform (rotated to lie flat)
6
+ * - Geometry (plane)
7
+ * - Material (optional texture + repeat)
8
+ * - Physics (fixed by default)
9
+ */
10
+ export function ground(options = {}) {
11
+ const { id = "ground", size = 50, position = [0, 0, 0], rotation = [-Math.PI / 2, 0, 0], scale = [1, 1, 1], color = "white", texture, repeat = texture ? true : false, repeatCount = [25, 25], physicsType = "fixed", hidden = false, disabled = false, } = options;
12
+ return {
13
+ id,
14
+ disabled,
15
+ hidden,
16
+ components: {
17
+ transform: {
18
+ type: "Transform",
19
+ properties: {
20
+ position,
21
+ rotation,
22
+ scale,
23
+ },
24
+ },
25
+ geometry: {
26
+ type: "Geometry",
27
+ properties: {
28
+ geometryType: "plane",
29
+ args: [size, size],
30
+ },
31
+ },
32
+ material: {
33
+ type: "Material",
34
+ properties: Object.assign(Object.assign({ color }, (texture ? { texture } : {})), (repeat ? { repeat: true, repeatCount } : {})),
35
+ },
36
+ physics: {
37
+ type: "Physics",
38
+ properties: {
39
+ type: physicsType,
40
+ },
41
+ },
42
+ },
43
+ };
44
+ }
@@ -0,0 +1,14 @@
1
+ export { default as GameCanvas } from './shared/GameCanvas';
2
+ export { default as PrefabEditor } from './tools/prefabeditor/PrefabEditor';
3
+ export type { PrefabEditorRef } from './tools/prefabeditor/PrefabEditor';
4
+ export { default as PrefabRoot } from './tools/prefabeditor/PrefabRoot';
5
+ export type { PrefabRootRef } from './tools/prefabeditor/PrefabRoot';
6
+ export { registerComponent } from './tools/prefabeditor/components/ComponentRegistry';
7
+ export type { Component } from './tools/prefabeditor/components/ComponentRegistry';
8
+ export type { Prefab, GameObject, ComponentData } from './tools/prefabeditor/types';
9
+ export * as editorStyles from './tools/prefabeditor/styles';
10
+ export * from './tools/prefabeditor/utils';
11
+ export { DragDropLoader } from './tools/dragdrop/DragDropLoader';
12
+ export { TextureListViewer, ModelListViewer, SoundListViewer, SharedCanvas, } from './tools/assetviewer/page';
13
+ export { sound as soundManager } from './helpers/SoundManager';
14
+ export * from './helpers';
package/dist/index.js ADDED
@@ -0,0 +1,14 @@
1
+ // Core Components
2
+ export { default as GameCanvas } from './shared/GameCanvas';
3
+ // Prefab Editor
4
+ export { default as PrefabEditor } from './tools/prefabeditor/PrefabEditor';
5
+ export { default as PrefabRoot } from './tools/prefabeditor/PrefabRoot';
6
+ export { registerComponent } from './tools/prefabeditor/components/ComponentRegistry';
7
+ export * as editorStyles from './tools/prefabeditor/styles';
8
+ export * from './tools/prefabeditor/utils';
9
+ // Asset Tools
10
+ export { DragDropLoader } from './tools/dragdrop/DragDropLoader';
11
+ export { TextureListViewer, ModelListViewer, SoundListViewer, SharedCanvas, } from './tools/assetviewer/page';
12
+ export { sound as soundManager } from './helpers/SoundManager';
13
+ // Helpers
14
+ export * from './helpers';
@@ -0,0 +1,9 @@
1
+ import { CanvasProps } from "@react-three/fiber";
2
+ import { WebGPURendererParameters } from "three/src/renderers/webgpu/WebGPURenderer.Nodes.js";
3
+ interface GameCanvasProps extends Omit<CanvasProps, 'children'> {
4
+ loader?: boolean;
5
+ children: React.ReactNode;
6
+ glConfig?: WebGPURendererParameters;
7
+ }
8
+ export default function GameCanvas({ loader, children, glConfig, ...props }: GameCanvasProps): import("react/jsx-runtime").JSX.Element;
9
+ export {};
@@ -0,0 +1,47 @@
1
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
2
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
3
+ return new (P || (P = Promise))(function (resolve, reject) {
4
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
5
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
6
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
7
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
8
+ });
9
+ };
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]];
18
+ }
19
+ return t;
20
+ };
21
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
22
+ import { Canvas, extend } from "@react-three/fiber";
23
+ import { WebGPURenderer, MeshBasicNodeMaterial, MeshStandardNodeMaterial, SpriteNodeMaterial, PCFShadowMap } from "three/webgpu";
24
+ import { Suspense, useState } from "react";
25
+ import { Loader } from "@react-three/drei";
26
+ // generic version
27
+ // extend(THREE as any)
28
+ extend({
29
+ MeshBasicNodeMaterial: MeshBasicNodeMaterial,
30
+ MeshStandardNodeMaterial: MeshStandardNodeMaterial,
31
+ SpriteNodeMaterial: SpriteNodeMaterial,
32
+ });
33
+ export default function GameCanvas(_a) {
34
+ var { loader = false, children, glConfig } = _a, props = __rest(_a, ["loader", "children", "glConfig"]);
35
+ const [frameloop, setFrameloop] = useState("never");
36
+ return _jsx(_Fragment, { children: _jsxs(Canvas, Object.assign({ style: { touchAction: 'none', userSelect: 'none' }, shadows: { type: PCFShadowMap, }, frameloop: frameloop, gl: (_a) => __awaiter(this, [_a], void 0, function* ({ canvas }) {
37
+ const renderer = new WebGPURenderer(Object.assign({ canvas: canvas,
38
+ // @ts-expect-error futuristic
39
+ shadowMap: true, antialias: true }, glConfig));
40
+ yield renderer.init().then(() => {
41
+ setFrameloop("always");
42
+ });
43
+ return renderer;
44
+ }), camera: {
45
+ position: [0, 1, 5],
46
+ } }, props, { children: [_jsx(Suspense, { children: children }), loader ? _jsx(Loader, {}) : null] })) });
47
+ }
@@ -0,0 +1,35 @@
1
+ interface TextureListViewerProps {
2
+ files: string[];
3
+ selected?: string;
4
+ onSelect: (file: string) => void;
5
+ basePath?: string;
6
+ }
7
+ export declare function TextureListViewer({ files, selected, onSelect, basePath }: TextureListViewerProps): import("react/jsx-runtime").JSX.Element;
8
+ interface ModelListViewerProps {
9
+ files: string[];
10
+ selected?: string;
11
+ onSelect: (file: string) => void;
12
+ basePath?: string;
13
+ }
14
+ export declare function ModelListViewer({ files, selected, onSelect, basePath }: ModelListViewerProps): import("react/jsx-runtime").JSX.Element;
15
+ interface SoundListViewerProps {
16
+ files: string[];
17
+ selected?: string;
18
+ onSelect: (file: string) => void;
19
+ basePath?: string;
20
+ }
21
+ export declare function SoundListViewer({ files, selected, onSelect, basePath }: SoundListViewerProps): import("react/jsx-runtime").JSX.Element;
22
+ export declare function SingleTextureViewer({ file, basePath }: {
23
+ file?: string;
24
+ basePath?: string;
25
+ }): import("react/jsx-runtime").JSX.Element | null;
26
+ export declare function SingleModelViewer({ file, basePath }: {
27
+ file?: string;
28
+ basePath?: string;
29
+ }): import("react/jsx-runtime").JSX.Element | null;
30
+ export declare function SingleSoundViewer({ file, basePath }: {
31
+ file?: string;
32
+ basePath?: string;
33
+ }): import("react/jsx-runtime").JSX.Element | null;
34
+ export declare function SharedCanvas(): import("react/jsx-runtime").JSX.Element;
35
+ export {};
@@ -0,0 +1,166 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { Canvas, useLoader } from "@react-three/fiber";
3
+ import { OrbitControls, View, PerspectiveCamera } from "@react-three/drei";
4
+ import { Suspense, useEffect, useState, useRef } from "react";
5
+ import { TextureLoader } from "three";
6
+ import { loadModel } from "../dragdrop/modelLoader";
7
+ // view models and textures in manifest, onselect callback
8
+ const styles = {
9
+ errorIcon: { color: '#fca5a5', fontSize: 12 }, // text-red-400 text-xs
10
+ flexFillRelative: { flex: 1, position: 'relative' },
11
+ bottomLabel: { backgroundColor: 'rgba(0,0,0,0.6)', fontSize: 10, padding: '0 4px', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', textAlign: 'center' },
12
+ iconLarge: { fontSize: 20 }
13
+ };
14
+ function getItemsInPath(files, currentPath) {
15
+ // Remove the leading category folder (e.g., /textures/, /models/, /sounds/)
16
+ const filesWithoutCategory = files.map(file => {
17
+ const parts = file.split('/').filter(Boolean);
18
+ return parts.length > 1 ? '/' + parts.slice(1).join('/') : '';
19
+ }).filter(Boolean);
20
+ const prefix = currentPath ? `/${currentPath}/` : '/';
21
+ const relevantFiles = filesWithoutCategory.filter(file => file.startsWith(prefix));
22
+ const folders = new Set();
23
+ const filesInCurrentPath = [];
24
+ relevantFiles.forEach((file, index) => {
25
+ const relativePath = file.slice(prefix.length);
26
+ const parts = relativePath.split('/').filter(Boolean);
27
+ if (parts.length > 1) {
28
+ folders.add(parts[0]);
29
+ }
30
+ else if (parts[0]) {
31
+ // Return the original file path
32
+ filesInCurrentPath.push(files[filesWithoutCategory.indexOf(file)]);
33
+ }
34
+ });
35
+ return { folders: Array.from(folders), filesInCurrentPath };
36
+ }
37
+ function FolderTile({ name, onClick }) {
38
+ return (_jsxs("div", { onClick: onClick, style: {
39
+ maxWidth: 60,
40
+ aspectRatio: '1 / 1',
41
+ backgroundColor: '#1f2937', /* gray-800 */
42
+ cursor: 'pointer',
43
+ display: 'flex',
44
+ flexDirection: 'column',
45
+ alignItems: 'center',
46
+ justifyContent: 'center'
47
+ }, children: [_jsx("div", { style: { fontSize: 24 }, children: "\uD83D\uDCC1" }), _jsx("div", { style: { fontSize: 10, textAlign: 'center', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', width: '100%', padding: '0 4px', marginTop: 4 }, children: name })] }));
48
+ }
49
+ function useInView() {
50
+ const [isInView, setIsInView] = useState(false);
51
+ const ref = useRef(null);
52
+ useEffect(() => {
53
+ const observer = new IntersectionObserver(([entry]) => {
54
+ setIsInView(entry.isIntersecting);
55
+ }, { rootMargin: '100px' });
56
+ if (ref.current) {
57
+ observer.observe(ref.current);
58
+ }
59
+ return () => {
60
+ if (ref.current) {
61
+ observer.unobserve(ref.current);
62
+ }
63
+ };
64
+ }, []);
65
+ return { ref, isInView };
66
+ }
67
+ function AssetListViewer({ files, selected, onSelect, renderCard }) {
68
+ const [currentPath, setCurrentPath] = useState('');
69
+ const { folders, filesInCurrentPath } = getItemsInPath(files, currentPath);
70
+ return (_jsxs("div", { children: [currentPath && (_jsx("button", { onClick: () => {
71
+ const pathParts = currentPath.split('/').filter(Boolean);
72
+ pathParts.pop();
73
+ setCurrentPath(pathParts.join('/'));
74
+ }, style: { marginBottom: 4, padding: '4px 8px', backgroundColor: '#1f2937', color: 'inherit', fontSize: 12, cursor: 'pointer', border: 'none' }, children: "\u2190 Back" })), _jsxs("div", { style: { display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: 4 }, children: [folders.map((folder) => (_jsx(FolderTile, { name: folder, onClick: () => setCurrentPath(currentPath ? `${currentPath}/${folder}` : folder) }, folder))), filesInCurrentPath.map((file) => (_jsx("div", { children: renderCard(file, onSelect) }, file)))] })] }));
75
+ }
76
+ export function TextureListViewer({ files, selected, onSelect, basePath = "" }) {
77
+ return (_jsxs(_Fragment, { children: [_jsx(AssetListViewer, { files: files, selected: selected, onSelect: onSelect, renderCard: (file, onSelectHandler) => (_jsx(TextureCard, { file: file, basePath: basePath, onSelect: onSelectHandler })) }), _jsx(SharedCanvas, {})] }));
78
+ }
79
+ function TextureCard({ file, onSelect, basePath = "" }) {
80
+ const [error, setError] = useState(false);
81
+ const [isHovered, setIsHovered] = useState(false);
82
+ const { ref, isInView } = useInView();
83
+ const fullPath = basePath ? `/${basePath}${file}` : file;
84
+ if (error) {
85
+ return (_jsx("div", { ref: ref, style: { aspectRatio: '1 / 1', backgroundColor: '#374151', cursor: 'pointer', display: 'flex', alignItems: 'center', justifyContent: 'center' }, onClick: () => onSelect(file), children: _jsx("div", { style: styles.errorIcon, children: "\u2717" }) }));
86
+ }
87
+ return (_jsxs("div", { ref: ref, style: { maxWidth: 60, aspectRatio: '1 / 1', backgroundColor: '#1f2937', cursor: 'pointer', display: 'flex', flexDirection: 'column' }, onClick: () => onSelect(file), onMouseEnter: () => setIsHovered(true), onMouseLeave: () => setIsHovered(false), children: [_jsx("div", { style: { flex: 1, position: 'relative' }, children: isInView ? (_jsxs(View, { style: { width: '100%', height: '100%' }, children: [_jsx(PerspectiveCamera, { makeDefault: true, position: [0, 0, 2.5], fov: 50 }), _jsxs(Suspense, { fallback: null, children: [_jsx("ambientLight", { intensity: 0.8 }), _jsx("pointLight", { position: [5, 5, 5], intensity: 0.5 }), _jsx(TextureSphere, { url: fullPath, onError: () => setError(true) }), _jsx(OrbitControls, { enableZoom: false, enablePan: false, autoRotate: isHovered, autoRotateSpeed: 2 })] })] })) : null }), _jsx("div", { style: { backgroundColor: 'rgba(0,0,0,0.6)', fontSize: 10, padding: '0 4px', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', textAlign: 'center' }, children: file.split('/').pop() })] }));
88
+ }
89
+ function TextureSphere({ url, onError }) {
90
+ const texture = useLoader(TextureLoader, url, undefined, (error) => {
91
+ console.error('Failed to load texture:', url, error);
92
+ onError === null || onError === void 0 ? void 0 : onError();
93
+ });
94
+ return (_jsxs("mesh", { position: [0, 0, 0], children: [_jsx("sphereGeometry", { args: [1, 32, 32] }), _jsx("meshStandardMaterial", { map: texture })] }));
95
+ }
96
+ export function ModelListViewer({ files, selected, onSelect, basePath = "" }) {
97
+ return (_jsxs(_Fragment, { children: [_jsx(AssetListViewer, { files: files, selected: selected, onSelect: onSelect, renderCard: (file, onSelectHandler) => (_jsx(ModelCard, { file: file, basePath: basePath, onSelect: onSelectHandler })) }), _jsx(SharedCanvas, {})] }));
98
+ }
99
+ function ModelCard({ file, onSelect, basePath = "" }) {
100
+ const [error, setError] = useState(false);
101
+ const { ref, isInView } = useInView();
102
+ const fullPath = basePath ? `/${basePath}${file}` : file;
103
+ if (error) {
104
+ return (_jsx("div", { ref: ref, style: { aspectRatio: '1 / 1', backgroundColor: '#374151', cursor: 'pointer', display: 'flex', alignItems: 'center', justifyContent: 'center' }, onClick: () => onSelect(file), children: _jsx("div", { style: styles.errorIcon, children: "\u2717" }) }));
105
+ }
106
+ return (_jsxs("div", { ref: ref, style: { maxWidth: 60, aspectRatio: '1 / 1', backgroundColor: '#111827', cursor: 'pointer', display: 'flex', flexDirection: 'column' }, onClick: () => onSelect(file), children: [_jsx("div", { style: styles.flexFillRelative, children: isInView ? (_jsxs(View, { style: { width: '100%', height: '100%' }, children: [_jsx(PerspectiveCamera, { makeDefault: true, position: [0, 1, 3], fov: 50 }), _jsxs(Suspense, { fallback: null, children: [_jsx(ModelPreview, { url: fullPath, onError: () => setError(true) }), _jsx(OrbitControls, { enableZoom: false })] })] })) : null }), _jsx("div", { style: { backgroundColor: 'rgba(0,0,0,0.6)', fontSize: 10, padding: '0 4px', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', textAlign: 'center' }, children: file.split('/').pop() })] }));
107
+ }
108
+ function ModelPreview({ url, onError }) {
109
+ const [model, setModel] = useState(null);
110
+ const onErrorRef = useRef(onError);
111
+ onErrorRef.current = onError;
112
+ useEffect(() => {
113
+ let cancelled = false;
114
+ setModel(null);
115
+ loadModel(url).then((result) => {
116
+ var _a;
117
+ if (cancelled)
118
+ return;
119
+ if (result.success && result.model) {
120
+ setModel(result.model);
121
+ }
122
+ else {
123
+ (_a = onErrorRef.current) === null || _a === void 0 ? void 0 : _a.call(onErrorRef);
124
+ }
125
+ });
126
+ return () => { cancelled = true; };
127
+ }, [url]);
128
+ if (!model)
129
+ return null;
130
+ return _jsx("primitive", { object: model });
131
+ }
132
+ export function SoundListViewer({ files, selected, onSelect, basePath = "" }) {
133
+ return (_jsx(AssetListViewer, { files: files, selected: selected, onSelect: onSelect, renderCard: (file, onSelectHandler) => (_jsx(SoundCard, { file: file, basePath: basePath, onSelect: onSelectHandler })) }));
134
+ }
135
+ function SoundCard({ file, onSelect, basePath = "" }) {
136
+ const fileName = file.split('/').pop() || '';
137
+ const fullPath = basePath ? `/${basePath}${file}` : file;
138
+ return (_jsxs("div", { onClick: () => onSelect(file), style: { aspectRatio: '1 / 1', backgroundColor: '#374151', cursor: 'pointer', display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center' }, children: [_jsx("div", { style: styles.iconLarge, children: "\uD83D\uDD0A" }), _jsx("div", { style: { fontSize: 12, padding: '0 4px', marginTop: 4, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', textAlign: 'center', width: '100%' }, children: fileName })] }));
139
+ }
140
+ // Single Asset Viewer Components - display only one selected asset
141
+ export function SingleTextureViewer({ file, basePath = "" }) {
142
+ if (!file)
143
+ return null;
144
+ return (_jsxs(_Fragment, { children: [_jsx(TextureCard, { file: file, basePath: basePath, onSelect: () => { } }), _jsx(SharedCanvas, {})] }));
145
+ }
146
+ export function SingleModelViewer({ file, basePath = "" }) {
147
+ if (!file)
148
+ return null;
149
+ return (_jsxs(_Fragment, { children: [_jsx(ModelCard, { file: file, basePath: basePath, onSelect: () => { } }), _jsx(SharedCanvas, {})] }));
150
+ }
151
+ export function SingleSoundViewer({ file, basePath = "" }) {
152
+ if (!file)
153
+ return null;
154
+ return _jsx(SoundCard, { file: file, basePath: basePath, onSelect: () => { } });
155
+ }
156
+ // Shared Canvas Component - can be used independently in any viewer
157
+ export function SharedCanvas() {
158
+ return (_jsx(Canvas, { shadows: true, dpr: [1, 1.5], camera: { position: [0, 0, 3], fov: 45, near: 0.1, far: 1000 }, style: {
159
+ position: 'absolute',
160
+ top: 0,
161
+ left: 0,
162
+ width: '100vw',
163
+ height: '100vh',
164
+ pointerEvents: 'none',
165
+ }, eventSource: typeof document !== 'undefined' ? document.getElementById('root') || undefined : undefined, eventPrefix: "client", children: _jsx(View.Port, {}) }));
166
+ }
@@ -0,0 +1,9 @@
1
+ interface DragDropLoaderProps {
2
+ onModelLoaded: (model: any, filename: string) => void;
3
+ }
4
+ export declare function DragDropLoader({ onModelLoaded }: DragDropLoaderProps): null;
5
+ interface FilePickerProps {
6
+ onModelLoaded: (model: any, filename: string) => void;
7
+ }
8
+ export declare function FilePicker({ onModelLoaded }: FilePickerProps): import("react/jsx-runtime").JSX.Element;
9
+ export {};
@@ -0,0 +1,78 @@
1
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
+ // DragDropLoader.tsx
3
+ import { useEffect } from "react";
4
+ import { DRACOLoader, FBXLoader, GLTFLoader } from "three/examples/jsm/Addons.js";
5
+ // Shared file handling logic
6
+ function handleFiles(files, onModelLoaded) {
7
+ files.forEach((file) => {
8
+ if (file.name.endsWith(".glb") || file.name.endsWith(".gltf")) {
9
+ loadGLTFFile(file, onModelLoaded);
10
+ }
11
+ else if (file.name.endsWith(".fbx")) {
12
+ loadFBXFile(file, onModelLoaded);
13
+ }
14
+ });
15
+ }
16
+ function loadGLTFFile(file, onModelLoaded) {
17
+ const reader = new FileReader();
18
+ reader.onload = (event) => {
19
+ var _a;
20
+ const arrayBuffer = (_a = event.target) === null || _a === void 0 ? void 0 : _a.result;
21
+ if (arrayBuffer) {
22
+ const loader = new GLTFLoader();
23
+ const dracoLoader = new DRACOLoader();
24
+ dracoLoader.setDecoderPath("https://www.gstatic.com/draco/v1/decoders/");
25
+ loader.setDRACOLoader(dracoLoader);
26
+ loader.parse(arrayBuffer, "", (gltf) => {
27
+ onModelLoaded(gltf.scene, file.name);
28
+ }, (error) => {
29
+ console.error("GLTFLoader parse error", error);
30
+ });
31
+ }
32
+ };
33
+ reader.readAsArrayBuffer(file);
34
+ }
35
+ function loadFBXFile(file, onModelLoaded) {
36
+ const reader = new FileReader();
37
+ reader.onload = (event) => {
38
+ var _a;
39
+ const arrayBuffer = (_a = event.target) === null || _a === void 0 ? void 0 : _a.result;
40
+ if (arrayBuffer) {
41
+ const loader = new FBXLoader();
42
+ const model = loader.parse(arrayBuffer, "");
43
+ onModelLoaded(model, file.name);
44
+ }
45
+ };
46
+ reader.readAsArrayBuffer(file);
47
+ }
48
+ export function DragDropLoader({ onModelLoaded }) {
49
+ useEffect(() => {
50
+ function handleDrop(e) {
51
+ var _a;
52
+ e.preventDefault();
53
+ e.stopPropagation();
54
+ const files = ((_a = e.dataTransfer) === null || _a === void 0 ? void 0 : _a.files) ? Array.from(e.dataTransfer.files) : [];
55
+ handleFiles(files, onModelLoaded);
56
+ }
57
+ function handleDragOver(e) {
58
+ e.preventDefault();
59
+ e.stopPropagation();
60
+ }
61
+ window.addEventListener("drop", handleDrop);
62
+ window.addEventListener("dragover", handleDragOver);
63
+ return () => {
64
+ window.removeEventListener("drop", handleDrop);
65
+ window.removeEventListener("dragover", handleDragOver);
66
+ };
67
+ }, [onModelLoaded]);
68
+ return null;
69
+ }
70
+ export function FilePicker({ onModelLoaded }) {
71
+ function onChange(e) {
72
+ const files = e.target.files ? Array.from(e.target.files) : [];
73
+ handleFiles(files, onModelLoaded);
74
+ }
75
+ // Ref for the hidden input
76
+ const inputId = "file-picker-input";
77
+ 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" })] }));
78
+ }
@@ -0,0 +1,7 @@
1
+ export type ModelLoadResult = {
2
+ success: boolean;
3
+ model?: any;
4
+ error?: any;
5
+ };
6
+ export type ProgressCallback = (filename: string, loaded: number, total: number) => void;
7
+ export declare function loadModel(filename: string, onProgress?: ProgressCallback): Promise<ModelLoadResult>;