react-three-game 0.0.4 → 0.0.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -2,3 +2,5 @@ export { default as GameCanvas } from './shared/GameCanvas';
2
2
  export { default as PrefabEditor } from './tools/prefabeditor/PrefabEditor';
3
3
  export { default as PrefabRoot } from './tools/prefabeditor/PrefabRoot';
4
4
  export { DragDropLoader } from './tools/dragdrop/DragDropLoader';
5
+ export { default as AssetViewerPage, TextureListViewer, ModelListViewer, SoundListViewer, SharedCanvas, } from './tools/assetviewer/page';
6
+ export type { Prefab, GameObject } from './tools/prefabeditor/types';
package/dist/index.js CHANGED
@@ -1,4 +1,6 @@
1
+ // Components
1
2
  export { default as GameCanvas } from './shared/GameCanvas';
2
3
  export { default as PrefabEditor } from './tools/prefabeditor/PrefabEditor';
3
4
  export { default as PrefabRoot } from './tools/prefabeditor/PrefabRoot';
4
5
  export { DragDropLoader } from './tools/dragdrop/DragDropLoader';
6
+ export { default as AssetViewerPage, TextureListViewer, ModelListViewer, SoundListViewer, SharedCanvas, } from './tools/assetviewer/page';
@@ -1,21 +1,26 @@
1
- export default function AssetViewerPage(): import("react/jsx-runtime").JSX.Element;
1
+ export default function AssetViewerPage({ basePath }?: {
2
+ basePath?: string;
3
+ }): import("react/jsx-runtime").JSX.Element;
2
4
  interface TextureListViewerProps {
3
5
  files: string[];
4
6
  selected?: string;
5
7
  onSelect: (file: string) => void;
8
+ basePath?: string;
6
9
  }
7
- export declare function TextureListViewer({ files, selected, onSelect }: TextureListViewerProps): import("react/jsx-runtime").JSX.Element;
10
+ export declare function TextureListViewer({ files, selected, onSelect, basePath }: TextureListViewerProps): import("react/jsx-runtime").JSX.Element;
8
11
  interface ModelListViewerProps {
9
12
  files: string[];
10
13
  selected?: string;
11
14
  onSelect: (file: string) => void;
15
+ basePath?: string;
12
16
  }
13
- export declare function ModelListViewer({ files, selected, onSelect }: ModelListViewerProps): import("react/jsx-runtime").JSX.Element;
17
+ export declare function ModelListViewer({ files, selected, onSelect, basePath }: ModelListViewerProps): import("react/jsx-runtime").JSX.Element;
14
18
  interface SoundListViewerProps {
15
19
  files: string[];
16
20
  selected?: string;
17
21
  onSelect: (file: string) => void;
22
+ basePath?: string;
18
23
  }
19
- export declare function SoundListViewer({ files, selected, onSelect }: SoundListViewerProps): import("react/jsx-runtime").JSX.Element;
24
+ export declare function SoundListViewer({ files, selected, onSelect, basePath }: SoundListViewerProps): import("react/jsx-runtime").JSX.Element;
20
25
  export declare function SharedCanvas(): import("react/jsx-runtime").JSX.Element;
21
26
  export {};
@@ -49,27 +49,28 @@ function useInView() {
49
49
  }, []);
50
50
  return { ref, isInView };
51
51
  }
52
- export default function AssetViewerPage() {
52
+ export default function AssetViewerPage({ basePath = "" } = {}) {
53
53
  const [textures, setTextures] = useState([]);
54
54
  const [models, setModels] = useState([]);
55
55
  const [sounds, setSounds] = useState([]);
56
56
  const [loading, setLoading] = useState(true);
57
57
  useEffect(() => {
58
+ const base = basePath ? `${basePath}/` : '';
58
59
  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(() => [])
60
+ fetch(`/${base}textures/manifest.json`).then(r => r.json()),
61
+ fetch(`/${base}models/manifest.json`).then(r => r.json()),
62
+ fetch(`/${base}sound/manifest.json`).then(r => r.json()).catch(() => [])
62
63
  ]).then(([textureData, modelData, soundData]) => {
63
64
  setTextures(textureData);
64
65
  setModels(modelData);
65
66
  setSounds(soundData);
66
67
  setLoading(false);
67
68
  });
68
- }, []);
69
+ }, [basePath]);
69
70
  if (loading) {
70
71
  return _jsx("div", { className: "p-4 text-gray-300", children: "Loading manifests..." });
71
72
  }
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
+ 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, basePath: basePath, 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, basePath: basePath, 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, basePath: basePath, onSelect: (file) => console.log('Selected sound:', file) })] }))] }), _jsx(SharedCanvas, {})] }));
73
74
  }
74
75
  function AssetListViewer({ files, selected, onSelect, renderCard }) {
75
76
  const [currentPath, setCurrentPath] = useState('');
@@ -89,17 +90,18 @@ function AssetListViewer({ files, selected, onSelect, renderCard }) {
89
90
  setShowPicker(false);
90
91
  }) }, file)))] })] }));
91
92
  }
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, {})] }));
93
+ export function TextureListViewer({ files, selected, onSelect, basePath = "" }) {
94
+ 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, {})] }));
94
95
  }
95
- function TextureCard({ file, onSelect }) {
96
+ function TextureCard({ file, onSelect, basePath = "" }) {
96
97
  const [error, setError] = useState(false);
97
98
  const [isHovered, setIsHovered] = useState(false);
98
99
  const { ref, isInView } = useInView();
100
+ const fullPath = basePath ? `/${basePath}${file}` : file;
99
101
  if (error) {
100
102
  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
103
  }
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() })] }));
104
+ 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: fullPath, 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
105
  }
104
106
  function TextureSphere({ url, onError }) {
105
107
  const texture = useLoader(TextureLoader, url, undefined, (error) => {
@@ -108,16 +110,17 @@ function TextureSphere({ url, onError }) {
108
110
  });
109
111
  return (_jsxs("mesh", { position: [0, 0, 0], children: [_jsx("sphereGeometry", { args: [1, 32, 32] }), _jsx("meshStandardMaterial", { map: texture })] }));
110
112
  }
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
+ export function ModelListViewer({ files, selected, onSelect, basePath = "" }) {
114
+ 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, {})] }));
113
115
  }
114
- function ModelCard({ file, onSelect }) {
116
+ function ModelCard({ file, onSelect, basePath = "" }) {
115
117
  const [error, setError] = useState(false);
116
118
  const { ref, isInView } = useInView();
119
+ const fullPath = basePath ? `/${basePath}${file}` : file;
117
120
  if (error) {
118
121
  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
122
  }
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() })] }));
123
+ 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: fullPath, 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
124
  }
122
125
  function ModelPreview({ url, onError }) {
123
126
  const isFbx = url.toLowerCase().endsWith('.fbx');
@@ -133,11 +136,12 @@ function FBXModel({ url, onError }) {
133
136
  const fbx = useFBX(url);
134
137
  return _jsx("primitive", { object: fbx, scale: 0.01 });
135
138
  }
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 })) }));
139
+ export function SoundListViewer({ files, selected, onSelect, basePath = "" }) {
140
+ return (_jsx(AssetListViewer, { files: files, selected: selected, onSelect: onSelect, renderCard: (file, onSelectHandler) => (_jsx(SoundCard, { file: file, basePath: basePath, onSelect: onSelectHandler })) }));
138
141
  }
139
- function SoundCard({ file, onSelect }) {
142
+ function SoundCard({ file, onSelect, basePath = "" }) {
140
143
  const fileName = file.split('/').pop() || '';
144
+ const fullPath = basePath ? `/${basePath}${file}` : file;
141
145
  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
146
  }
143
147
  // Shared Canvas Component - can be used independently in any viewer
@@ -17,9 +17,8 @@ const fbxLoader = new FBXLoader();
17
17
  export function loadModel(filename_1) {
18
18
  return __awaiter(this, arguments, void 0, function* (filename, resourcePath = "", onProgress) {
19
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}`;
20
+ // Construct full path - handle empty resourcePath to avoid double slashes
21
+ const fullPath = resourcePath ? `${resourcePath}/${filename}` : filename;
23
22
  if (filename.endsWith('.glb') || filename.endsWith('.gltf')) {
24
23
  return new Promise((resolve) => {
25
24
  gltfLoader.load(fullPath, (gltf) => resolve({ success: true, model: gltf.scene }), (progressEvent) => {
@@ -1,11 +1,12 @@
1
1
  import { Dispatch, SetStateAction } from 'react';
2
2
  import { Prefab } from "./types";
3
- declare function EditorUI({ prefabData, setPrefabData, selectedId, setSelectedId, transformMode, setTransformMode }: {
3
+ declare function EditorUI({ prefabData, setPrefabData, selectedId, setSelectedId, transformMode, setTransformMode, basePath }: {
4
4
  prefabData?: Prefab;
5
5
  setPrefabData?: Dispatch<SetStateAction<Prefab>>;
6
6
  selectedId: string | null;
7
7
  setSelectedId: Dispatch<SetStateAction<string | null>>;
8
8
  transformMode: "translate" | "rotate" | "scale";
9
9
  setTransformMode: (m: "translate" | "rotate" | "scale") => void;
10
+ basePath?: string;
10
11
  }): import("react/jsx-runtime").JSX.Element;
11
12
  export default EditorUI;
@@ -2,7 +2,7 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
2
2
  import { useState, useEffect } from 'react';
3
3
  import EditorTree from './EditorTree';
4
4
  import { getAllComponents } from './components/ComponentRegistry';
5
- function EditorUI({ prefabData, setPrefabData, selectedId, setSelectedId, transformMode, setTransformMode }) {
5
+ function EditorUI({ prefabData, setPrefabData, selectedId, setSelectedId, transformMode, setTransformMode, basePath }) {
6
6
  const [isInspectorCollapsed, setIsInspectorCollapsed] = useState(false);
7
7
  const updateNode = (updater) => {
8
8
  if (!prefabData || !setPrefabData || !selectedId)
@@ -24,9 +24,9 @@ function EditorUI({ prefabData, setPrefabData, selectedId, setSelectedId, transf
24
24
  };
25
25
  const selectedNode = selectedId && prefabData ? findNode(prefabData.root, selectedId) : null;
26
26
  // if (!selectedNode) return null;
27
- return _jsxs(_Fragment, { children: [_jsxs("div", { style: { position: 'absolute', top: "0.5rem", right: "0.5rem", zIndex: 20, backgroundColor: "rgba(0,0,0,0.7)", backdropFilter: "blur(4px)", color: "white", border: "1px solid rgba(0,255,255,0.3)" }, 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: () => setIsInspectorCollapsed(!isInspectorCollapsed), children: [_jsx("span", { children: "Inspector" }), _jsx("span", { className: "text-[8px]", children: isInspectorCollapsed ? '◀' : '▶' })] }), !isInspectorCollapsed && selectedNode && (_jsx(NodeInspector, { node: selectedNode, updateNode: updateNode, deleteNode: deleteNode, transformMode: transformMode, setTransformMode: setTransformMode }))] }), _jsx("div", { style: { position: 'absolute', top: "0.5rem", left: "0.5rem", zIndex: 20 }, children: _jsx(EditorTree, { prefabData: prefabData, setPrefabData: setPrefabData, selectedId: selectedId, setSelectedId: setSelectedId }) })] });
27
+ return _jsxs(_Fragment, { children: [_jsxs("div", { style: { position: 'absolute', top: "0.5rem", right: "0.5rem", zIndex: 20, backgroundColor: "rgba(0,0,0,0.7)", backdropFilter: "blur(4px)", color: "white", border: "1px solid rgba(0,255,255,0.3)" }, 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: () => setIsInspectorCollapsed(!isInspectorCollapsed), children: [_jsx("span", { children: "Inspector" }), _jsx("span", { className: "text-[8px]", children: isInspectorCollapsed ? '◀' : '▶' })] }), !isInspectorCollapsed && selectedNode && (_jsx(NodeInspector, { node: selectedNode, updateNode: updateNode, deleteNode: deleteNode, transformMode: transformMode, setTransformMode: setTransformMode, basePath: basePath }))] }), _jsx("div", { style: { position: 'absolute', top: "0.5rem", left: "0.5rem", zIndex: 20 }, children: _jsx(EditorTree, { prefabData: prefabData, setPrefabData: setPrefabData, selectedId: selectedId, setSelectedId: setSelectedId }) })] });
28
28
  }
29
- function NodeInspector({ node, updateNode, deleteNode, transformMode, setTransformMode }) {
29
+ function NodeInspector({ node, updateNode, deleteNode, transformMode, setTransformMode, basePath }) {
30
30
  const ALL_COMPONENTS = getAllComponents();
31
31
  const allComponentKeys = Object.keys(ALL_COMPONENTS);
32
32
  const [addComponentType, setAddComponentType] = useState(allComponentKeys[0]);
@@ -50,7 +50,7 @@ function NodeInspector({ node, updateNode, deleteNode, transformMode, setTransfo
50
50
  const components = Object.assign({}, n.components);
51
51
  delete components[key];
52
52
  return Object.assign(Object.assign({}, n), { components });
53
- }), className: "text-[9px] text-red-400/60 hover:text-red-400", children: "\u2715" })] }), EditorComp ? (_jsx(EditorComp, { component: comp, onUpdate: (newProps) => updateNode(n => (Object.assign(Object.assign({}, n), { components: Object.assign(Object.assign({}, n.components), { [key]: Object.assign(Object.assign({}, comp), { properties: Object.assign(Object.assign({}, comp.properties), newProps) }) }) }))) })) : null] }, key));
53
+ }), className: "text-[9px] text-red-400/60 hover:text-red-400", children: "\u2715" })] }), EditorComp ? (_jsx(EditorComp, { component: comp, onUpdate: (newProps) => updateNode(n => (Object.assign(Object.assign({}, n), { components: Object.assign(Object.assign({}, n.components), { [key]: Object.assign(Object.assign({}, comp), { properties: Object.assign(Object.assign({}, comp.properties), newProps) }) }) }))), basePath: basePath })) : null] }, key));
54
54
  }), _jsxs("div", { className: "px-1.5 py-1 border-t border-cyan-500/20", children: [_jsx("label", { className: "block text-[9px] text-cyan-400/60 uppercase tracking-wider mb-0.5", children: "Add Component" }), _jsxs("div", { className: "flex gap-0.5", children: [_jsx("select", { className: "bg-black/40 border border-cyan-500/30 px-1 py-0.5 flex-1 text-[10px] text-cyan-300 font-mono focus:outline-none focus:border-cyan-400/50", value: addComponentType, onChange: e => setAddComponentType(e.target.value), children: allComponentKeys.filter(k => { var _a; return !((_a = node.components) === null || _a === void 0 ? void 0 : _a[k.toLowerCase()]); }).map(k => (_jsx("option", { value: k, children: k }, k))) }), _jsx("button", { className: "bg-cyan-500/20 hover:bg-cyan-500/30 border border-cyan-500/30 px-2 py-0.5 text-[10px] text-cyan-300 font-mono disabled:opacity-30", disabled: !addComponentType, onClick: () => {
55
55
  var _a;
56
56
  if (!addComponentType)
@@ -1,4 +1,7 @@
1
- declare const PrefabEditor: ({ children }: {
1
+ import { Prefab } from "./types";
2
+ declare const PrefabEditor: ({ basePath, initialPrefab, children }: {
3
+ basePath?: string;
4
+ initialPrefab?: Prefab;
2
5
  children?: React.ReactNode;
3
6
  }) => import("react/jsx-runtime").JSX.Element;
4
7
  export default PrefabEditor;
@@ -13,11 +13,10 @@ import GameCanvas from "../../shared/GameCanvas";
13
13
  import { useState, useRef, } from "react";
14
14
  import PrefabRoot from "./PrefabRoot";
15
15
  import { Physics } from "@react-three/rapier";
16
- // import testPrefab from "./samples/test.json";
17
16
  import EditorUI from "./EditorUI";
18
- const PrefabEditor = ({ children }) => {
17
+ const PrefabEditor = ({ basePath, initialPrefab, children }) => {
19
18
  const [editMode, setEditMode] = useState(true);
20
- const [loadedPrefab, setLoadedPrefab] = useState({
19
+ const [loadedPrefab, setLoadedPrefab] = useState(initialPrefab !== null && initialPrefab !== void 0 ? initialPrefab : {
21
20
  "id": "prefab-default",
22
21
  "name": "New Prefab",
23
22
  "root": {
@@ -41,11 +40,11 @@ const PrefabEditor = ({ children }) => {
41
40
  const prefabRef = useRef(null);
42
41
  return _jsxs(_Fragment, { children: [_jsx(GameCanvas, { children: _jsxs(Physics, { paused: editMode, children: [_jsx("ambientLight", { intensity: 1.5 }), _jsx("gridHelper", { args: [10, 10], position: [0, -1, 0] }), _jsx(PrefabRoot, { data: loadedPrefab, ref: prefabRef,
43
42
  // props for edit mode
44
- editMode: editMode, onPrefabChange: setLoadedPrefab, selectedId: selectedId, onSelect: setSelectedId, transformMode: transformMode, setTransformMode: setTransformMode }), children] }) }), _jsxs("div", { style: { position: "absolute", top: "0.5rem", left: "50%", transform: "translateX(-50%)" }, className: "bg-black/70 backdrop-blur-sm border border-cyan-500/30 px-2 py-1 flex items-center gap-1", children: [_jsx("button", { className: "px-1 py-0.5 text-[10px] font-mono text-cyan-300 hover:bg-cyan-500/20 border border-cyan-500/30", onClick: () => setEditMode(!editMode), children: editMode ? "▶" : "⏸" }), _jsx("span", { className: "text-cyan-500/30 text-[10px]", children: "|" }), _jsx("button", { className: "px-1 py-0.5 text-[10px] font-mono text-cyan-300 hover:bg-cyan-500/20 border border-cyan-500/30", onClick: () => __awaiter(void 0, void 0, void 0, function* () {
43
+ editMode: editMode, onPrefabChange: setLoadedPrefab, selectedId: selectedId, onSelect: setSelectedId, transformMode: transformMode, setTransformMode: setTransformMode, basePath: basePath }), children] }) }), _jsxs("div", { style: { position: "absolute", top: "0.5rem", left: "50%", transform: "translateX(-50%)" }, className: "bg-black/70 backdrop-blur-sm border border-cyan-500/30 px-2 py-1 flex items-center gap-1", children: [_jsx("button", { className: "px-1 py-0.5 text-[10px] font-mono text-cyan-300 hover:bg-cyan-500/20 border border-cyan-500/30", onClick: () => setEditMode(!editMode), children: editMode ? "▶" : "⏸" }), _jsx("span", { className: "text-cyan-500/30 text-[10px]", children: "|" }), _jsx("button", { className: "px-1 py-0.5 text-[10px] font-mono text-cyan-300 hover:bg-cyan-500/20 border border-cyan-500/30", onClick: () => __awaiter(void 0, void 0, void 0, function* () {
45
44
  const prefab = yield loadJson();
46
45
  if (prefab)
47
46
  setLoadedPrefab(prefab);
48
- }), children: "\uD83D\uDCE5" }), _jsx("button", { className: "px-1 py-0.5 text-[10px] font-mono text-cyan-300 hover:bg-cyan-500/20 border border-cyan-500/30", onClick: () => saveJson(loadedPrefab, "prefab"), children: "\uD83D\uDCBE" })] }), editMode && _jsx(EditorUI, { prefabData: loadedPrefab, setPrefabData: setLoadedPrefab, selectedId: selectedId, setSelectedId: setSelectedId, transformMode: transformMode, setTransformMode: setTransformMode })] });
47
+ }), children: "\uD83D\uDCE5" }), _jsx("button", { className: "px-1 py-0.5 text-[10px] font-mono text-cyan-300 hover:bg-cyan-500/20 border border-cyan-500/30", onClick: () => saveJson(loadedPrefab, "prefab"), children: "\uD83D\uDCBE" })] }), editMode && _jsx(EditorUI, { prefabData: loadedPrefab, setPrefabData: setLoadedPrefab, selectedId: selectedId, setSelectedId: setSelectedId, transformMode: transformMode, setTransformMode: setTransformMode, basePath: basePath })] });
49
48
  };
50
49
  const saveJson = (data, filename) => {
51
50
  const dataStr = "data:text/json;charset=utf-8," + encodeURIComponent(JSON.stringify(data, null, 2));
@@ -8,5 +8,6 @@ export declare const PrefabRoot: import("react").ForwardRefExoticComponent<{
8
8
  onSelect?: (id: string | null) => void;
9
9
  transformMode?: "translate" | "rotate" | "scale";
10
10
  setTransformMode?: (mode: "translate" | "rotate" | "scale") => void;
11
+ basePath?: string;
11
12
  } & import("react").RefAttributes<Group<import("three").Object3DEventMap>>>;
12
13
  export default PrefabRoot;
@@ -28,7 +28,7 @@ function updatePrefabNode(root, id, update) {
28
28
  }
29
29
  return root;
30
30
  }
31
- export const PrefabRoot = forwardRef(({ editMode, data, onPrefabChange, selectedId, onSelect, transformMode, setTransformMode }, ref) => {
31
+ export const PrefabRoot = forwardRef(({ editMode, data, onPrefabChange, selectedId, onSelect, transformMode, setTransformMode, basePath = "" }, ref) => {
32
32
  const [loadedModels, setLoadedModels] = useState({});
33
33
  const [loadedTextures, setLoadedTextures] = useState({});
34
34
  // const [prefabRoot, setPrefabRoot] = useState<Prefab>(data); // Removed local state
@@ -108,7 +108,7 @@ export const PrefabRoot = forwardRef(({ editMode, data, onPrefabChange, selected
108
108
  for (const filename of modelsToLoad) {
109
109
  if (!loadedModels[filename] && !loadingRefs.current.has(filename)) {
110
110
  loadingRefs.current.add(filename);
111
- const result = yield loadModel(filename, "");
111
+ const result = yield loadModel(filename, basePath);
112
112
  if (result.success && result.model) {
113
113
  setLoadedModels(prev => (Object.assign(Object.assign({}, prev), { [filename]: result.model })));
114
114
  }
@@ -118,7 +118,8 @@ export const PrefabRoot = forwardRef(({ editMode, data, onPrefabChange, selected
118
118
  for (const filename of texturesToLoad) {
119
119
  if (!loadedTextures[filename] && !loadingRefs.current.has(filename)) {
120
120
  loadingRefs.current.add(filename);
121
- textureLoader.load(filename, (texture) => {
121
+ const texturePath = basePath ? `${basePath}/${filename}` : filename;
122
+ textureLoader.load(texturePath, (texture) => {
122
123
  texture.colorSpace = SRGBColorSpace;
123
124
  setLoadedTextures(prev => (Object.assign(Object.assign({}, prev), { [filename]: texture })));
124
125
  });
@@ -4,6 +4,7 @@ export interface Component {
4
4
  Editor: FC<{
5
5
  component: any;
6
6
  onUpdate: (newComp: any) => void;
7
+ basePath?: string;
7
8
  }>;
8
9
  defaultProperties: any;
9
10
  View?: FC<any>;
@@ -1,16 +1,17 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { TextureListViewer } from '../../assetviewer/page';
3
3
  import { useEffect, useState } from 'react';
4
- function MaterialComponentEditor({ component, onUpdate }) {
4
+ function MaterialComponentEditor({ component, onUpdate, basePath = "" }) {
5
5
  var _a, _b, _c, _d;
6
6
  const [textureFiles, setTextureFiles] = useState([]);
7
7
  useEffect(() => {
8
- fetch('/textures/manifest.json')
8
+ const base = basePath ? `${basePath}/` : '';
9
+ fetch(`/${base}textures/manifest.json`)
9
10
  .then(r => r.json())
10
11
  .then(data => setTextureFiles(Array.isArray(data) ? data : data.files || []))
11
12
  .catch(console.error);
12
- }, []);
13
- return (_jsxs("div", { className: "flex flex-col", children: [_jsxs("div", { className: "mb-1", children: [_jsx("label", { className: "block text-[9px] text-cyan-400/60 uppercase tracking-wider mb-0.5", children: "Color" }), _jsxs("div", { className: "flex gap-0.5", children: [_jsx("input", { type: "color", className: "h-5 w-5 bg-transparent border-none cursor-pointer", value: component.properties.color, onChange: e => onUpdate({ 'color': e.target.value }) }), _jsx("input", { type: "text", className: "flex-1 bg-black/40 border border-cyan-500/30 px-1 py-0.5 text-[10px] text-cyan-300 font-mono focus:outline-none focus:border-cyan-400/50", value: component.properties.color, onChange: e => onUpdate({ 'color': e.target.value }) })] })] }), _jsxs("div", { className: "flex items-center gap-1 mb-1", children: [_jsx("input", { type: "checkbox", className: "w-3 h-3", checked: component.properties.wireframe || false, onChange: e => onUpdate({ 'wireframe': e.target.checked }) }), _jsx("label", { className: "text-[9px] text-cyan-400/60", children: "Wireframe" })] }), _jsxs("div", { children: [_jsx("label", { className: "block text-[9px] text-cyan-400/60 uppercase tracking-wider mb-0.5", children: "Texture" }), _jsx("div", { className: "max-h-32 overflow-y-auto", children: _jsx(TextureListViewer, { files: textureFiles, selected: component.properties.texture || undefined, onSelect: (file) => onUpdate({ 'texture': file }) }) })] }), component.properties.texture && (_jsxs("div", { className: "border-t border-cyan-500/20 pt-1 mt-1", children: [_jsxs("div", { className: "flex items-center gap-1 mb-1", children: [_jsx("input", { type: "checkbox", className: "w-3 h-3", checked: component.properties.repeat || false, onChange: e => onUpdate({ 'repeat': e.target.checked }) }), _jsx("label", { className: "text-[9px] text-cyan-400/60", children: "Repeat Texture" })] }), component.properties.repeat && (_jsxs("div", { children: [_jsx("label", { className: "block text-[9px] text-cyan-400/60 uppercase tracking-wider mb-0.5", children: "Repeat (X, Y)" }), _jsxs("div", { className: "flex gap-0.5", children: [_jsx("input", { type: "number", className: "w-full bg-black/40 border border-cyan-500/30 px-1 py-0.5 text-[10px] text-cyan-300 font-mono focus:outline-none focus:border-cyan-400/50", value: (_b = (_a = component.properties.repeatCount) === null || _a === void 0 ? void 0 : _a[0]) !== null && _b !== void 0 ? _b : 1, onChange: e => {
13
+ }, [basePath]);
14
+ return (_jsxs("div", { className: "flex flex-col", children: [_jsxs("div", { className: "mb-1", children: [_jsx("label", { className: "block text-[9px] text-cyan-400/60 uppercase tracking-wider mb-0.5", children: "Color" }), _jsxs("div", { className: "flex gap-0.5", children: [_jsx("input", { type: "color", className: "h-5 w-5 bg-transparent border-none cursor-pointer", value: component.properties.color, onChange: e => onUpdate({ 'color': e.target.value }) }), _jsx("input", { type: "text", className: "flex-1 bg-black/40 border border-cyan-500/30 px-1 py-0.5 text-[10px] text-cyan-300 font-mono focus:outline-none focus:border-cyan-400/50", value: component.properties.color, onChange: e => onUpdate({ 'color': e.target.value }) })] })] }), _jsxs("div", { className: "flex items-center gap-1 mb-1", children: [_jsx("input", { type: "checkbox", className: "w-3 h-3", checked: component.properties.wireframe || false, onChange: e => onUpdate({ 'wireframe': e.target.checked }) }), _jsx("label", { className: "text-[9px] text-cyan-400/60", children: "Wireframe" })] }), _jsxs("div", { children: [_jsx("label", { className: "block text-[9px] text-cyan-400/60 uppercase tracking-wider mb-0.5", children: "Texture" }), _jsx("div", { className: "max-h-32 overflow-y-auto", children: _jsx(TextureListViewer, { files: textureFiles, selected: component.properties.texture || undefined, onSelect: (file) => onUpdate({ 'texture': file }), basePath: basePath }) })] }), component.properties.texture && (_jsxs("div", { className: "border-t border-cyan-500/20 pt-1 mt-1", children: [_jsxs("div", { className: "flex items-center gap-1 mb-1", children: [_jsx("input", { type: "checkbox", className: "w-3 h-3", checked: component.properties.repeat || false, onChange: e => onUpdate({ 'repeat': e.target.checked }) }), _jsx("label", { className: "text-[9px] text-cyan-400/60", children: "Repeat Texture" })] }), component.properties.repeat && (_jsxs("div", { children: [_jsx("label", { className: "block text-[9px] text-cyan-400/60 uppercase tracking-wider mb-0.5", children: "Repeat (X, Y)" }), _jsxs("div", { className: "flex gap-0.5", children: [_jsx("input", { type: "number", className: "w-full bg-black/40 border border-cyan-500/30 px-1 py-0.5 text-[10px] text-cyan-300 font-mono focus:outline-none focus:border-cyan-400/50", value: (_b = (_a = component.properties.repeatCount) === null || _a === void 0 ? void 0 : _a[0]) !== null && _b !== void 0 ? _b : 1, onChange: e => {
14
15
  var _a, _b;
15
16
  const y = (_b = (_a = component.properties.repeatCount) === null || _a === void 0 ? void 0 : _a[1]) !== null && _b !== void 0 ? _b : 1;
16
17
  onUpdate({ 'repeatCount': [parseFloat(e.target.value), y] });
@@ -1,20 +1,21 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  import { ModelListViewer } from '../../assetviewer/page';
3
3
  import { useEffect, useState } from 'react';
4
- function ModelComponentEditor({ component, onUpdate }) {
4
+ function ModelComponentEditor({ component, onUpdate, basePath = "" }) {
5
5
  const [modelFiles, setModelFiles] = useState([]);
6
6
  useEffect(() => {
7
- fetch('/models/manifest.json')
7
+ const base = basePath ? `${basePath}/` : '';
8
+ fetch(`/${base}models/manifest.json`)
8
9
  .then(r => r.json())
9
10
  .then(data => setModelFiles(Array.isArray(data) ? data : data.files || []))
10
11
  .catch(console.error);
11
- }, []);
12
+ }, [basePath]);
12
13
  const handleModelSelect = (file) => {
13
14
  // Remove leading slash for prefab compatibility
14
15
  const filename = file.startsWith('/') ? file.slice(1) : file;
15
16
  onUpdate({ 'filename': filename });
16
17
  };
17
- return _jsxs("div", { children: [_jsxs("div", { className: "mb-1", children: [_jsx("label", { className: "block text-[9px] text-cyan-400/60 uppercase tracking-wider mb-0.5", children: "Model" }), _jsx("div", { className: "max-h-32 overflow-y-auto", children: _jsx(ModelListViewer, { files: modelFiles, selected: component.properties.filename ? `/${component.properties.filename}` : undefined, onSelect: handleModelSelect }) })] }), _jsxs("div", { className: "flex items-center gap-1", children: [_jsx("input", { type: "checkbox", id: "instanced-checkbox", checked: component.properties.instanced || false, onChange: e => onUpdate({ 'instanced': e.target.checked }), className: "w-3 h-3" }), _jsx("label", { htmlFor: "instanced-checkbox", className: "text-[9px] text-cyan-400/60", children: "Instanced" })] })] });
18
+ return _jsxs("div", { children: [_jsxs("div", { className: "mb-1", children: [_jsx("label", { className: "block text-[9px] text-cyan-400/60 uppercase tracking-wider mb-0.5", children: "Model" }), _jsx("div", { className: "max-h-32 overflow-y-auto", children: _jsx(ModelListViewer, { files: modelFiles, selected: component.properties.filename ? `/${component.properties.filename}` : undefined, onSelect: handleModelSelect, basePath: basePath }) })] }), _jsxs("div", { className: "flex items-center gap-1", children: [_jsx("input", { type: "checkbox", id: "instanced-checkbox", checked: component.properties.instanced || false, onChange: e => onUpdate({ 'instanced': e.target.checked }), className: "w-3 h-3" }), _jsx("label", { htmlFor: "instanced-checkbox", className: "text-[9px] text-cyan-400/60", children: "Instanced" })] })] });
18
19
  }
19
20
  // View for Model component
20
21
  function ModelComponentView({ properties, loadedModels, children }) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-three-game",
3
- "version": "0.0.4",
3
+ "version": "0.0.6",
4
4
  "description": "Batteries included React Three Fiber game engine",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.js",