react-three-game 0.0.1 → 0.0.2

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 (54) hide show
  1. package/.gitattributes +2 -0
  2. package/.github/copilot-instructions.md +207 -0
  3. package/LICENSE +661 -0
  4. package/README.md +664 -0
  5. package/dist/index.d.ts +4 -1
  6. package/dist/index.js +4 -1
  7. package/dist/shared/GameCanvas.d.ts +6 -0
  8. package/dist/shared/GameCanvas.js +48 -0
  9. package/dist/shared/extend-three.d.ts +1 -0
  10. package/dist/shared/extend-three.js +13 -0
  11. package/dist/tools/assetviewer/page.d.ts +21 -0
  12. package/dist/tools/assetviewer/page.js +153 -0
  13. package/dist/tools/dragdrop/DragDropLoader.d.ts +9 -0
  14. package/dist/tools/dragdrop/DragDropLoader.js +78 -0
  15. package/dist/tools/dragdrop/modelLoader.d.ts +7 -0
  16. package/dist/tools/dragdrop/modelLoader.js +53 -0
  17. package/dist/tools/dragdrop/page.d.ts +1 -0
  18. package/dist/tools/dragdrop/page.js +11 -0
  19. package/dist/tools/prefabeditor/EditorTree.d.ts +10 -0
  20. package/dist/tools/prefabeditor/EditorTree.js +182 -0
  21. package/dist/tools/prefabeditor/EditorUI.d.ts +11 -0
  22. package/dist/tools/prefabeditor/EditorUI.js +96 -0
  23. package/dist/tools/prefabeditor/EventSystem.d.ts +7 -0
  24. package/dist/tools/prefabeditor/EventSystem.js +23 -0
  25. package/dist/tools/prefabeditor/InstanceProvider.d.ts +30 -0
  26. package/dist/tools/prefabeditor/InstanceProvider.js +172 -0
  27. package/dist/tools/prefabeditor/PrefabEditor.d.ts +4 -0
  28. package/dist/tools/prefabeditor/PrefabEditor.js +89 -0
  29. package/dist/tools/prefabeditor/PrefabRoot.d.ts +12 -0
  30. package/dist/tools/prefabeditor/PrefabRoot.js +273 -0
  31. package/dist/tools/prefabeditor/components/ComponentRegistry.d.ts +13 -0
  32. package/dist/tools/prefabeditor/components/ComponentRegistry.js +13 -0
  33. package/dist/tools/prefabeditor/components/GeometryComponent.d.ts +3 -0
  34. package/dist/tools/prefabeditor/components/GeometryComponent.js +28 -0
  35. package/dist/tools/prefabeditor/components/MaterialComponent.d.ts +3 -0
  36. package/dist/tools/prefabeditor/components/MaterialComponent.js +66 -0
  37. package/dist/tools/prefabeditor/components/ModelComponent.d.ts +3 -0
  38. package/dist/tools/prefabeditor/components/ModelComponent.js +39 -0
  39. package/dist/tools/prefabeditor/components/PhysicsComponent.d.ts +3 -0
  40. package/dist/tools/prefabeditor/components/PhysicsComponent.js +19 -0
  41. package/dist/tools/prefabeditor/components/SpotLightComponent.d.ts +3 -0
  42. package/dist/tools/prefabeditor/components/SpotLightComponent.js +19 -0
  43. package/dist/tools/prefabeditor/components/TransformComponent.d.ts +8 -0
  44. package/dist/tools/prefabeditor/components/TransformComponent.js +22 -0
  45. package/dist/tools/prefabeditor/components/index.d.ts +2 -0
  46. package/dist/tools/prefabeditor/components/index.js +14 -0
  47. package/dist/tools/prefabeditor/page.d.ts +1 -0
  48. package/dist/tools/prefabeditor/page.js +5 -0
  49. package/dist/tools/prefabeditor/types.d.ts +29 -0
  50. package/dist/tools/prefabeditor/types.js +1 -0
  51. package/package.json +16 -4
  52. package/tsconfig.json +2 -1
  53. package/dist/GameCanvas.d.ts +0 -6
  54. package/dist/GameCanvas.js +0 -5
@@ -0,0 +1,48 @@
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, Fragment as _Fragment, jsxs as _jsxs } 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 } = _a, props = __rest(_a, ["loader", "children"]);
35
+ const [frameloop, setFrameloop] = useState("never");
36
+ const [loading, setLoading] = useState(true);
37
+ return _jsxs(_Fragment, { children: [_jsx(Canvas, { shadows: { type: PCFShadowMap, }, frameloop: frameloop, gl: (_a) => __awaiter(this, [_a], void 0, function* ({ canvas }) {
38
+ const renderer = new WebGPURenderer(Object.assign({ canvas: canvas,
39
+ // @ts-expect-error futuristic
40
+ shadowMap: true, antialias: true }, props));
41
+ yield renderer.init().then(() => {
42
+ setFrameloop("always");
43
+ });
44
+ return renderer;
45
+ }), camera: {
46
+ position: [0, 1, 5],
47
+ }, children: _jsx(Suspense, { children: children }) }), loader ? _jsx(Loader, {}) : null] });
48
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,13 @@
1
+ import { extend } from "@react-three/fiber";
2
+ import * as THREE from "three";
3
+ import * as WEBGPU from "three/webgpu";
4
+ console.log('EXTENDING THREE NAMESPACE');
5
+ console.log('THREE keys:', Object.keys(THREE).length);
6
+ console.log('Has Canvas?', 'Canvas' in THREE);
7
+ console.log('Has AmbientLight?', 'AmbientLight' in THREE);
8
+ console.log('Has GridHelper?', 'GridHelper' in THREE);
9
+ // Extend entire THREE namespace to support all THREE objects in JSX
10
+ // This must happen before any Canvas components are created
11
+ extend(THREE);
12
+ extend(WEBGPU);
13
+ console.log('THREE NAMESPACE EXTENDED');
@@ -0,0 +1,21 @@
1
+ export default function AssetViewerPage(): import("react/jsx-runtime").JSX.Element;
2
+ interface TextureListViewerProps {
3
+ files: string[];
4
+ selected?: string;
5
+ onSelect: (file: string) => void;
6
+ }
7
+ export declare function TextureListViewer({ files, selected, onSelect }: TextureListViewerProps): import("react/jsx-runtime").JSX.Element;
8
+ interface ModelListViewerProps {
9
+ files: string[];
10
+ selected?: string;
11
+ onSelect: (file: string) => void;
12
+ }
13
+ export declare function ModelListViewer({ files, selected, onSelect }: ModelListViewerProps): import("react/jsx-runtime").JSX.Element;
14
+ interface SoundListViewerProps {
15
+ files: string[];
16
+ selected?: string;
17
+ onSelect: (file: string) => void;
18
+ }
19
+ export declare function SoundListViewer({ files, selected, onSelect }: SoundListViewerProps): import("react/jsx-runtime").JSX.Element;
20
+ export declare function SharedCanvas(): import("react/jsx-runtime").JSX.Element;
21
+ export {};
@@ -0,0 +1,153 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
+ import { Canvas, useLoader } from "@react-three/fiber";
4
+ import { OrbitControls, useGLTF, useFBX, Stage, View, PerspectiveCamera } from "@react-three/drei";
5
+ import { Suspense, useEffect, useState, useRef } from "react";
6
+ import { TextureLoader } from "three";
7
+ // view models and textures in manifest, onselect callback
8
+ function getItemsInPath(files, currentPath) {
9
+ // Remove the leading category folder (e.g., /textures/, /models/, /sounds/)
10
+ const filesWithoutCategory = files.map(file => {
11
+ const parts = file.split('/').filter(Boolean);
12
+ return parts.length > 1 ? '/' + parts.slice(1).join('/') : '';
13
+ }).filter(Boolean);
14
+ const prefix = currentPath ? `/${currentPath}/` : '/';
15
+ const relevantFiles = filesWithoutCategory.filter(file => file.startsWith(prefix));
16
+ const folders = new Set();
17
+ const filesInCurrentPath = [];
18
+ relevantFiles.forEach((file, index) => {
19
+ const relativePath = file.slice(prefix.length);
20
+ const parts = relativePath.split('/').filter(Boolean);
21
+ if (parts.length > 1) {
22
+ folders.add(parts[0]);
23
+ }
24
+ else if (parts[0]) {
25
+ // Return the original file path
26
+ filesInCurrentPath.push(files[filesWithoutCategory.indexOf(file)]);
27
+ }
28
+ });
29
+ return { folders: Array.from(folders), filesInCurrentPath };
30
+ }
31
+ function FolderTile({ name, onClick }) {
32
+ return (_jsxs("div", { onClick: onClick, className: "aspect-square bg-gray-800 cursor-pointer hover:bg-gray-700 flex flex-col items-center justify-center", children: [_jsx("div", { className: "text-3xl", children: "\uD83D\uDCC1" }), _jsx("div", { className: "text-xs text-center truncate w-full px-1 mt-1", children: name })] }));
33
+ }
34
+ function useInView() {
35
+ const [isInView, setIsInView] = useState(false);
36
+ const ref = useRef(null);
37
+ useEffect(() => {
38
+ const observer = new IntersectionObserver(([entry]) => {
39
+ setIsInView(entry.isIntersecting);
40
+ }, { rootMargin: '100px' });
41
+ if (ref.current) {
42
+ observer.observe(ref.current);
43
+ }
44
+ return () => {
45
+ if (ref.current) {
46
+ observer.unobserve(ref.current);
47
+ }
48
+ };
49
+ }, []);
50
+ return { ref, isInView };
51
+ }
52
+ export default function AssetViewerPage() {
53
+ const [textures, setTextures] = useState([]);
54
+ const [models, setModels] = useState([]);
55
+ const [sounds, setSounds] = useState([]);
56
+ const [loading, setLoading] = useState(true);
57
+ useEffect(() => {
58
+ Promise.all([
59
+ fetch('/textures/manifest.json').then(r => r.json()),
60
+ fetch('/models/manifest.json').then(r => r.json()),
61
+ fetch('/sound/manifest.json').then(r => r.json()).catch(() => [])
62
+ ]).then(([textureData, modelData, soundData]) => {
63
+ setTextures(textureData);
64
+ setModels(modelData);
65
+ setSounds(soundData);
66
+ setLoading(false);
67
+ });
68
+ }, []);
69
+ if (loading) {
70
+ return _jsx("div", { className: "p-4 text-gray-300", children: "Loading manifests..." });
71
+ }
72
+ return (_jsxs(_Fragment, { children: [_jsxs("div", { className: "p-2 text-gray-300 overflow-y-auto h-screen text-sm", children: [_jsx("h1", { className: "text-lg mb-2 font-bold", children: "Asset Viewer" }), _jsxs("h2", { className: "text-sm mt-4 mb-1 font-semibold", children: ["Textures (", textures.length, ")"] }), _jsx(TextureListViewer, { files: textures, onSelect: (file) => console.log('Selected texture:', file) }), _jsxs("h2", { className: "text-sm mt-4 mb-1 font-semibold", children: ["Models (", models.length, ")"] }), _jsx(ModelListViewer, { files: models, onSelect: (file) => console.log('Selected model:', file) }), sounds.length > 0 && (_jsxs(_Fragment, { children: [_jsxs("h2", { className: "text-sm mt-4 mb-1 font-semibold", children: ["Sounds (", sounds.length, ")"] }), _jsx(SoundListViewer, { files: sounds, onSelect: (file) => console.log('Selected sound:', file) })] }))] }), _jsx(SharedCanvas, {})] }));
73
+ }
74
+ function AssetListViewer({ files, selected, onSelect, renderCard }) {
75
+ const [currentPath, setCurrentPath] = useState('');
76
+ const [showPicker, setShowPicker] = useState(false);
77
+ const { folders, filesInCurrentPath } = getItemsInPath(files, currentPath);
78
+ const showCompactView = selected && !showPicker;
79
+ if (showCompactView) {
80
+ return (_jsxs("div", { className: "flex gap-1 items-center", children: [renderCard(selected, onSelect), _jsx("button", { onClick: () => setShowPicker(true), className: "px-2 py-1 bg-gray-800 hover:bg-gray-700 text-xs", children: "Change" })] }));
81
+ }
82
+ return (_jsxs("div", { children: [currentPath && (_jsx("button", { onClick: () => {
83
+ const pathParts = currentPath.split('/').filter(Boolean);
84
+ pathParts.pop();
85
+ setCurrentPath(pathParts.join('/'));
86
+ }, className: "mb-1 px-2 py-1 bg-gray-800 hover:bg-gray-700 text-xs", children: "\u2190 Back" })), _jsxs("div", { className: "grid grid-cols-3 gap-1", children: [folders.map((folder) => (_jsx(FolderTile, { name: folder, onClick: () => setCurrentPath(currentPath ? `${currentPath}/${folder}` : folder) }, folder))), filesInCurrentPath.map((file) => (_jsx("div", { children: renderCard(file, (f) => {
87
+ onSelect(f);
88
+ if (selected)
89
+ setShowPicker(false);
90
+ }) }, file)))] })] }));
91
+ }
92
+ export function TextureListViewer({ files, selected, onSelect }) {
93
+ return (_jsxs(_Fragment, { children: [_jsx(AssetListViewer, { files: files, selected: selected, onSelect: onSelect, renderCard: (file, onSelectHandler) => (_jsx(TextureCard, { file: file, onSelect: onSelectHandler })) }), _jsx(SharedCanvas, {})] }));
94
+ }
95
+ function TextureCard({ file, onSelect }) {
96
+ const [error, setError] = useState(false);
97
+ const [isHovered, setIsHovered] = useState(false);
98
+ const { ref, isInView } = useInView();
99
+ if (error) {
100
+ return (_jsx("div", { ref: ref, className: "aspect-square bg-gray-700 cursor-pointer hover:bg-gray-600 flex items-center justify-center", onClick: () => onSelect(file), children: _jsx("div", { className: "text-red-400 text-xs", children: "\u2717" }) }));
101
+ }
102
+ return (_jsxs("div", { ref: ref, className: "aspect-square bg-gray-800 cursor-pointer hover:bg-gray-700 flex flex-col", onClick: () => onSelect(file), onMouseEnter: () => setIsHovered(true), onMouseLeave: () => setIsHovered(false), children: [_jsx("div", { className: "flex-1 relative", children: isInView ? (_jsxs(View, { className: "w-full h-full", 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: file, onError: () => setError(true) }), _jsx(OrbitControls, { enableZoom: false, enablePan: false, autoRotate: isHovered, autoRotateSpeed: 2 })] })] })) : null }), _jsx("div", { className: "bg-black/60 text-[10px] px-1 truncate text-center", children: file.split('/').pop() })] }));
103
+ }
104
+ function TextureSphere({ url, onError }) {
105
+ const texture = useLoader(TextureLoader, url, undefined, (error) => {
106
+ console.error('Failed to load texture:', url, error);
107
+ onError === null || onError === void 0 ? void 0 : onError();
108
+ });
109
+ return (_jsxs("mesh", { position: [0, 0, 0], children: [_jsx("sphereGeometry", { args: [1, 32, 32] }), _jsx("meshStandardMaterial", { map: texture })] }));
110
+ }
111
+ export function ModelListViewer({ files, selected, onSelect }) {
112
+ return (_jsxs(_Fragment, { children: [_jsx(AssetListViewer, { files: files, selected: selected, onSelect: onSelect, renderCard: (file, onSelectHandler) => (_jsx(ModelCard, { file: file, onSelect: onSelectHandler })) }), _jsx(SharedCanvas, {})] }));
113
+ }
114
+ function ModelCard({ file, onSelect }) {
115
+ const [error, setError] = useState(false);
116
+ const { ref, isInView } = useInView();
117
+ if (error) {
118
+ return (_jsx("div", { ref: ref, className: "aspect-square bg-gray-700 cursor-pointer hover:bg-gray-600 flex items-center justify-center", onClick: () => onSelect(file), children: _jsx("div", { className: "text-red-400 text-xs", children: "\u2717" }) }));
119
+ }
120
+ return (_jsxs("div", { ref: ref, className: "aspect-square bg-gray-900 cursor-pointer hover:bg-gray-800 flex flex-col", onClick: () => onSelect(file), children: [_jsx("div", { className: "flex-1 relative", children: isInView ? (_jsxs(View, { className: "w-full h-full", children: [_jsx(PerspectiveCamera, { makeDefault: true, position: [0, 1, 3], fov: 50 }), _jsxs(Suspense, { fallback: null, children: [_jsx(Stage, { intensity: 0.5, environment: "city", children: _jsx(ModelPreview, { url: file, onError: () => setError(true) }) }), _jsx(OrbitControls, { enableZoom: false })] })] })) : null }), _jsx("div", { className: "bg-black/60 text-[10px] px-1 truncate text-center", children: file.split('/').pop() })] }));
121
+ }
122
+ function ModelPreview({ url, onError }) {
123
+ const isFbx = url.toLowerCase().endsWith('.fbx');
124
+ if (isFbx)
125
+ return _jsx(FBXModel, { url: url, onError: onError });
126
+ return _jsx(GLTFModel, { url: url, onError: onError });
127
+ }
128
+ function GLTFModel({ url, onError }) {
129
+ const { scene } = useGLTF(url);
130
+ return _jsx("primitive", { object: scene });
131
+ }
132
+ function FBXModel({ url, onError }) {
133
+ const fbx = useFBX(url);
134
+ return _jsx("primitive", { object: fbx, scale: 0.01 });
135
+ }
136
+ export function SoundListViewer({ files, selected, onSelect }) {
137
+ return (_jsx(AssetListViewer, { files: files, selected: selected, onSelect: onSelect, renderCard: (file, onSelectHandler) => (_jsx(SoundCard, { file: file, onSelect: onSelectHandler })) }));
138
+ }
139
+ function SoundCard({ file, onSelect }) {
140
+ const fileName = file.split('/').pop() || '';
141
+ return (_jsxs("div", { onClick: () => onSelect(file), className: "aspect-square bg-gray-700 cursor-pointer hover:bg-gray-600 flex flex-col items-center justify-center", children: [_jsx("div", { className: "text-2xl", children: "\uD83D\uDD0A" }), _jsx("div", { className: "text-[10px] px-1 mt-1 truncate text-center w-full", children: fileName })] }));
142
+ }
143
+ // Shared Canvas Component - can be used independently in any viewer
144
+ export function SharedCanvas() {
145
+ return (_jsx(Canvas, { shadows: true, dpr: [1, 1.5], camera: { position: [0, 0, 3], fov: 45, near: 0.1, far: 1000 }, style: {
146
+ position: 'fixed',
147
+ top: 0,
148
+ left: 0,
149
+ width: '100vw',
150
+ height: '100vh',
151
+ pointerEvents: 'none',
152
+ }, eventSource: typeof document !== 'undefined' ? document.getElementById('root') || undefined : undefined, eventPrefix: "client", children: _jsx(View.Port, {}) }));
153
+ }
@@ -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, resourcePath?: string, onProgress?: ProgressCallback): Promise<ModelLoadResult>;
@@ -0,0 +1,53 @@
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
+ import { GLTFLoader, FBXLoader, DRACOLoader } from "three/examples/jsm/Addons.js";
11
+ // Singleton loader instances
12
+ const dracoLoader = new DRACOLoader();
13
+ dracoLoader.setDecoderPath("https://www.gstatic.com/draco/v1/decoders/");
14
+ const gltfLoader = new GLTFLoader();
15
+ gltfLoader.setDRACOLoader(dracoLoader);
16
+ const fbxLoader = new FBXLoader();
17
+ export function loadModel(filename_1) {
18
+ return __awaiter(this, arguments, void 0, function* (filename, resourcePath = "", onProgress) {
19
+ try {
20
+ // Construct full path - always prepend resourcePath if provided (even if empty string)
21
+ // This allows loading from root with resourcePath=""
22
+ const fullPath = `${resourcePath}/${filename}`;
23
+ if (filename.endsWith('.glb') || filename.endsWith('.gltf')) {
24
+ return new Promise((resolve) => {
25
+ gltfLoader.load(fullPath, (gltf) => resolve({ success: true, model: gltf.scene }), (progressEvent) => {
26
+ if (onProgress) {
27
+ // Use loaded as total if total is not available
28
+ const total = progressEvent.total || progressEvent.loaded;
29
+ onProgress(filename, progressEvent.loaded, total);
30
+ }
31
+ }, (error) => resolve({ success: false, error }));
32
+ });
33
+ }
34
+ else if (filename.endsWith('.fbx')) {
35
+ return new Promise((resolve) => {
36
+ fbxLoader.load(fullPath, (model) => resolve({ success: true, model }), (progressEvent) => {
37
+ if (onProgress) {
38
+ // Use loaded as total if total is not available
39
+ const total = progressEvent.total || progressEvent.loaded;
40
+ onProgress(filename, progressEvent.loaded, total);
41
+ }
42
+ }, (error) => resolve({ success: false, error }));
43
+ });
44
+ }
45
+ else {
46
+ return { success: false, error: new Error(`Unsupported file format: ${filename}`) };
47
+ }
48
+ }
49
+ catch (error) {
50
+ return { success: false, error };
51
+ }
52
+ });
53
+ }
@@ -0,0 +1 @@
1
+ export default function Home(): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,11 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
+ import { Physics, RigidBody } from "@react-three/rapier";
4
+ import { OrbitControls } from "@react-three/drei";
5
+ import { useState } from "react";
6
+ import { DragDropLoader } from "./DragDropLoader";
7
+ import GameCanvas from "../../shared/GameCanvas";
8
+ export default function Home() {
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, {})] }) }) })] }));
11
+ }
@@ -0,0 +1,10 @@
1
+ import { Dispatch, SetStateAction } from 'react';
2
+ import { Prefab } from "./types";
3
+ interface EditorTreeProps {
4
+ prefabData?: Prefab;
5
+ setPrefabData?: Dispatch<SetStateAction<Prefab>>;
6
+ selectedId: string | null;
7
+ setSelectedId: Dispatch<SetStateAction<string | null>>;
8
+ }
9
+ export default function EditorTree({ prefabData, setPrefabData, selectedId, setSelectedId }: EditorTreeProps): import("react/jsx-runtime").JSX.Element | null;
10
+ export {};
@@ -0,0 +1,182 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { useState } from 'react';
3
+ import { getComponent } from './components/ComponentRegistry';
4
+ export default function EditorTree({ prefabData, setPrefabData, selectedId, setSelectedId }) {
5
+ const [contextMenu, setContextMenu] = useState(null);
6
+ const [draggedId, setDraggedId] = useState(null);
7
+ const [collapsedIds, setCollapsedIds] = useState(new Set());
8
+ const [isTreeCollapsed, setIsTreeCollapsed] = useState(false);
9
+ if (!prefabData || !setPrefabData)
10
+ return null;
11
+ const handleContextMenu = (e, nodeId) => {
12
+ e.preventDefault();
13
+ e.stopPropagation();
14
+ setContextMenu({ x: e.clientX, y: e.clientY, nodeId });
15
+ };
16
+ const closeContextMenu = () => setContextMenu(null);
17
+ const toggleCollapse = (e, id) => {
18
+ e.stopPropagation();
19
+ setCollapsedIds(prev => {
20
+ const next = new Set(prev);
21
+ if (next.has(id))
22
+ next.delete(id);
23
+ else
24
+ next.add(id);
25
+ return next;
26
+ });
27
+ };
28
+ // Actions
29
+ const handleAddChild = (parentId) => {
30
+ var _a;
31
+ const newNode = {
32
+ id: crypto.randomUUID(),
33
+ enabled: true,
34
+ visible: true,
35
+ components: {
36
+ transform: {
37
+ type: "Transform",
38
+ properties: Object.assign({}, (_a = getComponent('Transform')) === null || _a === void 0 ? void 0 : _a.defaultProperties)
39
+ }
40
+ }
41
+ };
42
+ setPrefabData(prev => {
43
+ const newRoot = JSON.parse(JSON.stringify(prev.root)); // Deep clone for safety
44
+ const parent = findNode(newRoot, parentId);
45
+ if (parent) {
46
+ parent.children = parent.children || [];
47
+ parent.children.push(newNode);
48
+ }
49
+ return Object.assign(Object.assign({}, prev), { root: newRoot });
50
+ });
51
+ closeContextMenu();
52
+ };
53
+ const handleDuplicate = (nodeId) => {
54
+ if (nodeId === prefabData.root.id)
55
+ return; // Cannot duplicate root
56
+ setPrefabData(prev => {
57
+ const newRoot = JSON.parse(JSON.stringify(prev.root));
58
+ const parent = findParent(newRoot, nodeId);
59
+ const node = findNode(newRoot, nodeId);
60
+ if (parent && node) {
61
+ const clone = cloneNode(node);
62
+ parent.children = parent.children || [];
63
+ parent.children.push(clone);
64
+ }
65
+ return Object.assign(Object.assign({}, prev), { root: newRoot });
66
+ });
67
+ closeContextMenu();
68
+ };
69
+ const handleDelete = (nodeId) => {
70
+ if (nodeId === prefabData.root.id)
71
+ return; // Cannot delete root
72
+ setPrefabData(prev => {
73
+ const newRoot = deleteNodeFromTree(JSON.parse(JSON.stringify(prev.root)), nodeId);
74
+ return Object.assign(Object.assign({}, prev), { root: newRoot });
75
+ });
76
+ if (selectedId === nodeId)
77
+ setSelectedId(null);
78
+ closeContextMenu();
79
+ };
80
+ // Drag and Drop
81
+ const handleDragStart = (e, id) => {
82
+ e.stopPropagation();
83
+ if (id === prefabData.root.id) {
84
+ e.preventDefault(); // Cannot drag root
85
+ return;
86
+ }
87
+ setDraggedId(id);
88
+ e.dataTransfer.effectAllowed = "move";
89
+ };
90
+ const handleDragOver = (e, targetId) => {
91
+ e.preventDefault();
92
+ e.stopPropagation();
93
+ if (!draggedId || draggedId === targetId)
94
+ return;
95
+ // Check for cycles: target cannot be a descendant of dragged node
96
+ const draggedNode = findNode(prefabData.root, draggedId);
97
+ if (draggedNode && findNode(draggedNode, targetId))
98
+ return;
99
+ e.dataTransfer.dropEffect = "move";
100
+ };
101
+ const handleDrop = (e, targetId) => {
102
+ e.preventDefault();
103
+ e.stopPropagation();
104
+ if (!draggedId || draggedId === targetId)
105
+ return;
106
+ setPrefabData(prev => {
107
+ var _a;
108
+ const newRoot = JSON.parse(JSON.stringify(prev.root));
109
+ // Check cycle again on the fresh tree
110
+ const draggedNodeRef = findNode(newRoot, draggedId);
111
+ if (draggedNodeRef && findNode(draggedNodeRef, targetId))
112
+ return prev;
113
+ // Remove from old parent
114
+ const parent = findParent(newRoot, draggedId);
115
+ if (!parent)
116
+ return prev;
117
+ const nodeToMove = (_a = parent.children) === null || _a === void 0 ? void 0 : _a.find(c => c.id === draggedId);
118
+ if (!nodeToMove)
119
+ return prev;
120
+ parent.children = parent.children.filter(c => c.id !== draggedId);
121
+ // Add to new parent
122
+ const target = findNode(newRoot, targetId);
123
+ if (target) {
124
+ target.children = target.children || [];
125
+ target.children.push(nodeToMove);
126
+ }
127
+ return Object.assign(Object.assign({}, prev), { root: newRoot });
128
+ });
129
+ setDraggedId(null);
130
+ };
131
+ const renderNode = (node, depth = 0) => {
132
+ if (!node)
133
+ return null;
134
+ const isSelected = node.id === selectedId;
135
+ const isCollapsed = collapsedIds.has(node.id);
136
+ const hasChildren = node.children && node.children.length > 0;
137
+ return (_jsxs("div", { className: "select-none", children: [_jsxs("div", { className: `flex items-center py-0.5 px-1 cursor-pointer border-b border-cyan-500/10 ${isSelected ? 'bg-cyan-500/30 hover:bg-cyan-500/40 border-cyan-400/30' : 'hover:bg-cyan-500/10'}`, style: { paddingLeft: `${depth * 8 + 4}px` }, onClick: (e) => { e.stopPropagation(); setSelectedId(node.id); }, onContextMenu: (e) => handleContextMenu(e, node.id), draggable: node.id !== prefabData.root.id, onDragStart: (e) => handleDragStart(e, node.id), onDragOver: (e) => handleDragOver(e, node.id), onDrop: (e) => handleDrop(e, node.id), children: [_jsx("span", { className: `mr-0.5 w-3 text-center text-cyan-400/50 hover:text-cyan-400 cursor-pointer text-[8px] ${hasChildren ? '' : 'invisible'}`, onClick: (e) => hasChildren && toggleCollapse(e, node.id), children: isCollapsed ? '▶' : '▼' }), _jsx("span", { className: "text-[10px] truncate font-mono text-cyan-300", children: node.id })] }), !isCollapsed && node.children && (_jsx("div", { children: node.children.map(child => renderNode(child, depth + 1)) }))] }, node.id));
138
+ };
139
+ return (_jsxs(_Fragment, { children: [_jsxs("div", { className: "bg-black/70 backdrop-blur-sm text-white border border-cyan-500/30 max-h-[85vh] overflow-y-auto flex flex-col", style: { width: isTreeCollapsed ? 'auto' : '14rem' }, onClick: closeContextMenu, children: [_jsxs("div", { className: "px-1.5 py-1 font-mono text-[10px] bg-cyan-500/10 border-b border-cyan-500/30 sticky top-0 uppercase tracking-wider text-cyan-400/80 cursor-pointer hover:bg-cyan-500/20 flex items-center justify-between", onClick: (e) => { e.stopPropagation(); setIsTreeCollapsed(!isTreeCollapsed); }, children: [_jsx("span", { children: "Prefab Graph" }), _jsx("span", { className: "text-[8px]", children: isTreeCollapsed ? '▶' : '◀' })] }), !isTreeCollapsed && (_jsx("div", { className: "flex-1 py-0.5", children: renderNode(prefabData.root) }))] }), contextMenu && (_jsxs("div", { className: "fixed bg-black/90 backdrop-blur-sm border border-cyan-500/40 z-50 min-w-[100px]", style: { top: contextMenu.y, left: contextMenu.x }, onClick: (e) => e.stopPropagation(), children: [_jsx("button", { className: "w-full text-left px-2 py-1 hover:bg-cyan-500/20 text-[10px] text-cyan-300 font-mono border-b border-cyan-500/20", onClick: () => handleAddChild(contextMenu.nodeId), children: "Add Child" }), contextMenu.nodeId !== prefabData.root.id && (_jsxs(_Fragment, { children: [_jsx("button", { className: "w-full text-left px-2 py-1 hover:bg-cyan-500/20 text-[10px] text-cyan-300 font-mono border-b border-cyan-500/20", onClick: () => handleDuplicate(contextMenu.nodeId), children: "Duplicate" }), _jsx("button", { className: "w-full text-left px-2 py-1 hover:bg-red-500/20 text-[10px] text-red-400 font-mono", onClick: () => handleDelete(contextMenu.nodeId), children: "Delete" })] }))] }))] }));
140
+ }
141
+ // --- Helpers ---
142
+ function findNode(root, id) {
143
+ if (root.id === id)
144
+ return root;
145
+ if (root.children) {
146
+ for (const child of root.children) {
147
+ const found = findNode(child, id);
148
+ if (found)
149
+ return found;
150
+ }
151
+ }
152
+ return null;
153
+ }
154
+ function findParent(root, id) {
155
+ if (!root.children)
156
+ return null;
157
+ for (const child of root.children) {
158
+ if (child.id === id)
159
+ return root;
160
+ const found = findParent(child, id);
161
+ if (found)
162
+ return found;
163
+ }
164
+ return null;
165
+ }
166
+ function deleteNodeFromTree(root, id) {
167
+ if (root.id === id)
168
+ return null;
169
+ if (root.children) {
170
+ root.children = root.children
171
+ .map(child => deleteNodeFromTree(child, id))
172
+ .filter((child) => child !== null);
173
+ }
174
+ return root;
175
+ }
176
+ function cloneNode(node) {
177
+ const newNode = Object.assign(Object.assign({}, node), { id: crypto.randomUUID() });
178
+ if (newNode.children) {
179
+ newNode.children = newNode.children.map(child => cloneNode(child));
180
+ }
181
+ return newNode;
182
+ }
@@ -0,0 +1,11 @@
1
+ import { Dispatch, SetStateAction } from 'react';
2
+ import { Prefab } from "./types";
3
+ declare function EditorUI({ prefabData, setPrefabData, selectedId, setSelectedId, transformMode, setTransformMode }: {
4
+ prefabData?: Prefab;
5
+ setPrefabData?: Dispatch<SetStateAction<Prefab>>;
6
+ selectedId: string | null;
7
+ setSelectedId: Dispatch<SetStateAction<string | null>>;
8
+ transformMode: "translate" | "rotate" | "scale";
9
+ setTransformMode: (m: "translate" | "rotate" | "scale") => void;
10
+ }): import("react/jsx-runtime").JSX.Element;
11
+ export default EditorUI;