react-three-game 0.0.9 → 0.0.11
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/README.md +2 -2
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/tools/assetviewer/page.d.ts +0 -3
- package/dist/tools/assetviewer/page.js +0 -23
- package/dist/tools/dragdrop/modelLoader.d.ts +1 -1
- package/dist/tools/dragdrop/modelLoader.js +4 -4
- package/dist/tools/prefabeditor/EditorTree.js +1 -1
- package/dist/tools/prefabeditor/PrefabEditor.d.ts +2 -1
- package/dist/tools/prefabeditor/PrefabEditor.js +16 -4
- package/dist/tools/prefabeditor/PrefabRoot.js +5 -2
- package/package.json +1 -1
- package/src/index.ts +0 -1
- package/src/tools/assetviewer/page.tsx +0 -48
- package/src/tools/dragdrop/modelLoader.ts +2 -3
- package/src/tools/prefabeditor/EditorTree.tsx +1 -0
- package/src/tools/prefabeditor/PrefabEditor.tsx +18 -4
- package/src/tools/prefabeditor/PrefabRoot.tsx +5 -2
package/README.md
CHANGED
|
@@ -40,7 +40,7 @@ Add the library path to your CSS entry point using the `@source` directive:
|
|
|
40
40
|
|
|
41
41
|
```css
|
|
42
42
|
@import "tailwindcss";
|
|
43
|
-
@source "
|
|
43
|
+
@source "../../node_modules/react-three-game/dist/**/*.{js,ts,jsx,tsx}";
|
|
44
44
|
```
|
|
45
45
|
|
|
46
46
|
### Tailwind v3
|
|
@@ -51,7 +51,7 @@ Add the library path to your `tailwind.config.js`:
|
|
|
51
51
|
module.exports = {
|
|
52
52
|
content: [
|
|
53
53
|
// ...
|
|
54
|
-
"./node_modules/react-three-game/
|
|
54
|
+
"./node_modules/react-three-game/dist/**/*.{js,ts,jsx,tsx}",
|
|
55
55
|
],
|
|
56
56
|
// ...
|
|
57
57
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -2,5 +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 {
|
|
5
|
+
export { TextureListViewer, ModelListViewer, SoundListViewer, SharedCanvas, } from './tools/assetviewer/page';
|
|
6
6
|
export type { Prefab, GameObject } from './tools/prefabeditor/types';
|
package/dist/index.js
CHANGED
|
@@ -3,4 +3,4 @@ export { default as GameCanvas } from './shared/GameCanvas';
|
|
|
3
3
|
export { default as PrefabEditor } from './tools/prefabeditor/PrefabEditor';
|
|
4
4
|
export { default as PrefabRoot } from './tools/prefabeditor/PrefabRoot';
|
|
5
5
|
export { DragDropLoader } from './tools/dragdrop/DragDropLoader';
|
|
6
|
-
export {
|
|
6
|
+
export { TextureListViewer, ModelListViewer, SoundListViewer, SharedCanvas, } from './tools/assetviewer/page';
|
|
@@ -49,29 +49,6 @@ function useInView() {
|
|
|
49
49
|
}, []);
|
|
50
50
|
return { ref, isInView };
|
|
51
51
|
}
|
|
52
|
-
export default function AssetViewerPage({ basePath = "" } = {}) {
|
|
53
|
-
const [textures, setTextures] = useState([]);
|
|
54
|
-
const [models, setModels] = useState([]);
|
|
55
|
-
const [sounds, setSounds] = useState([]);
|
|
56
|
-
const [loading, setLoading] = useState(true);
|
|
57
|
-
useEffect(() => {
|
|
58
|
-
const base = basePath ? `${basePath}/` : '';
|
|
59
|
-
Promise.all([
|
|
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(() => [])
|
|
63
|
-
]).then(([textureData, modelData, soundData]) => {
|
|
64
|
-
setTextures(textureData);
|
|
65
|
-
setModels(modelData);
|
|
66
|
-
setSounds(soundData);
|
|
67
|
-
setLoading(false);
|
|
68
|
-
});
|
|
69
|
-
}, [basePath]);
|
|
70
|
-
if (loading) {
|
|
71
|
-
return _jsx("div", { className: "p-4 text-gray-300", children: "Loading manifests..." });
|
|
72
|
-
}
|
|
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, {})] }));
|
|
74
|
-
}
|
|
75
52
|
function AssetListViewer({ files, selected, onSelect, renderCard }) {
|
|
76
53
|
const [currentPath, setCurrentPath] = useState('');
|
|
77
54
|
const [showPicker, setShowPicker] = useState(false);
|
|
@@ -4,4 +4,4 @@ export type ModelLoadResult = {
|
|
|
4
4
|
error?: any;
|
|
5
5
|
};
|
|
6
6
|
export type ProgressCallback = (filename: string, loaded: number, total: number) => void;
|
|
7
|
-
export declare function loadModel(filename: string,
|
|
7
|
+
export declare function loadModel(filename: string, onProgress?: ProgressCallback): Promise<ModelLoadResult>;
|
|
@@ -14,11 +14,11 @@ dracoLoader.setDecoderPath("https://www.gstatic.com/draco/v1/decoders/");
|
|
|
14
14
|
const gltfLoader = new GLTFLoader();
|
|
15
15
|
gltfLoader.setDRACOLoader(dracoLoader);
|
|
16
16
|
const fbxLoader = new FBXLoader();
|
|
17
|
-
export function loadModel(
|
|
18
|
-
return __awaiter(this,
|
|
17
|
+
export function loadModel(filename, onProgress) {
|
|
18
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
19
19
|
try {
|
|
20
|
-
//
|
|
21
|
-
const fullPath =
|
|
20
|
+
// Use filename directly (should already include leading /)
|
|
21
|
+
const fullPath = filename;
|
|
22
22
|
if (filename.endsWith('.glb') || filename.endsWith('.gltf')) {
|
|
23
23
|
return new Promise((resolve) => {
|
|
24
24
|
gltfLoader.load(fullPath, (gltf) => resolve({ success: true, model: gltf.scene }), (progressEvent) => {
|
|
@@ -136,7 +136,7 @@ export default function EditorTree({ prefabData, setPrefabData, selectedId, setS
|
|
|
136
136
|
const hasChildren = node.children && node.children.length > 0;
|
|
137
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
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" })] }))] }))] }));
|
|
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), onPointerLeave: closeContextMenu, 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
140
|
}
|
|
141
141
|
// --- Helpers ---
|
|
142
142
|
function findNode(root, id) {
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { Prefab } from "./types";
|
|
2
|
-
declare const PrefabEditor: ({ basePath, initialPrefab, children }: {
|
|
2
|
+
declare const PrefabEditor: ({ basePath, initialPrefab, onPrefabChange, children }: {
|
|
3
3
|
basePath?: string;
|
|
4
4
|
initialPrefab?: Prefab;
|
|
5
|
+
onPrefabChange?: (prefab: Prefab) => void;
|
|
5
6
|
children?: React.ReactNode;
|
|
6
7
|
}) => import("react/jsx-runtime").JSX.Element;
|
|
7
8
|
export default PrefabEditor;
|
|
@@ -10,11 +10,11 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
|
|
|
10
10
|
};
|
|
11
11
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
12
12
|
import GameCanvas from "../../shared/GameCanvas";
|
|
13
|
-
import { useState, useRef, } from "react";
|
|
13
|
+
import { useState, useRef, useEffect } from "react";
|
|
14
14
|
import PrefabRoot from "./PrefabRoot";
|
|
15
15
|
import { Physics } from "@react-three/rapier";
|
|
16
16
|
import EditorUI from "./EditorUI";
|
|
17
|
-
const PrefabEditor = ({ basePath, initialPrefab, children }) => {
|
|
17
|
+
const PrefabEditor = ({ basePath, initialPrefab, onPrefabChange, children }) => {
|
|
18
18
|
const [editMode, setEditMode] = useState(true);
|
|
19
19
|
const [loadedPrefab, setLoadedPrefab] = useState(initialPrefab !== null && initialPrefab !== void 0 ? initialPrefab : {
|
|
20
20
|
"id": "prefab-default",
|
|
@@ -38,13 +38,25 @@ const PrefabEditor = ({ basePath, initialPrefab, children }) => {
|
|
|
38
38
|
const [selectedId, setSelectedId] = useState(null);
|
|
39
39
|
const [transformMode, setTransformMode] = useState("translate");
|
|
40
40
|
const prefabRef = useRef(null);
|
|
41
|
+
// Sync internal state with external initialPrefab prop
|
|
42
|
+
useEffect(() => {
|
|
43
|
+
if (initialPrefab) {
|
|
44
|
+
setLoadedPrefab(initialPrefab);
|
|
45
|
+
}
|
|
46
|
+
}, [initialPrefab]);
|
|
47
|
+
// Wrapper to update prefab and notify parent
|
|
48
|
+
const updatePrefab = (newPrefab) => {
|
|
49
|
+
setLoadedPrefab(newPrefab);
|
|
50
|
+
const resolved = typeof newPrefab === 'function' ? newPrefab(loadedPrefab) : newPrefab;
|
|
51
|
+
onPrefabChange === null || onPrefabChange === void 0 ? void 0 : onPrefabChange(resolved);
|
|
52
|
+
};
|
|
41
53
|
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,
|
|
42
54
|
// props for edit mode
|
|
43
|
-
editMode: editMode, onPrefabChange:
|
|
55
|
+
editMode: editMode, onPrefabChange: updatePrefab, 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* () {
|
|
44
56
|
const prefab = yield loadJson();
|
|
45
57
|
if (prefab)
|
|
46
58
|
setLoadedPrefab(prefab);
|
|
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:
|
|
59
|
+
}), 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: updatePrefab, selectedId: selectedId, setSelectedId: setSelectedId, transformMode: transformMode, setTransformMode: setTransformMode, basePath: basePath })] });
|
|
48
60
|
};
|
|
49
61
|
const saveJson = (data, filename) => {
|
|
50
62
|
const dataStr = "data:text/json;charset=utf-8," + encodeURIComponent(JSON.stringify(data, null, 2));
|
|
@@ -108,7 +108,9 @@ 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
|
-
|
|
111
|
+
// Load model directly from public root, prepend "/" if not present
|
|
112
|
+
const modelPath = filename.startsWith('/') ? filename : `/${filename}`;
|
|
113
|
+
const result = yield loadModel(modelPath);
|
|
112
114
|
if (result.success && result.model) {
|
|
113
115
|
setLoadedModels(prev => (Object.assign(Object.assign({}, prev), { [filename]: result.model })));
|
|
114
116
|
}
|
|
@@ -118,7 +120,8 @@ export const PrefabRoot = forwardRef(({ editMode, data, onPrefabChange, selected
|
|
|
118
120
|
for (const filename of texturesToLoad) {
|
|
119
121
|
if (!loadedTextures[filename] && !loadingRefs.current.has(filename)) {
|
|
120
122
|
loadingRefs.current.add(filename);
|
|
121
|
-
|
|
123
|
+
// Load texture directly from public root, prepend "/" if not present
|
|
124
|
+
const texturePath = filename.startsWith('/') ? filename : `/${filename}`;
|
|
122
125
|
textureLoader.load(texturePath, (texture) => {
|
|
123
126
|
texture.colorSpace = SRGBColorSpace;
|
|
124
127
|
setLoadedTextures(prev => (Object.assign(Object.assign({}, prev), { [filename]: texture })));
|
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -4,7 +4,6 @@ export { default as PrefabEditor } from './tools/prefabeditor/PrefabEditor';
|
|
|
4
4
|
export { default as PrefabRoot } from './tools/prefabeditor/PrefabRoot';
|
|
5
5
|
export { DragDropLoader } from './tools/dragdrop/DragDropLoader';
|
|
6
6
|
export {
|
|
7
|
-
default as AssetViewerPage,
|
|
8
7
|
TextureListViewer,
|
|
9
8
|
ModelListViewer,
|
|
10
9
|
SoundListViewer,
|
|
@@ -3,7 +3,6 @@
|
|
|
3
3
|
import { Canvas, useLoader } from "@react-three/fiber";
|
|
4
4
|
import { OrbitControls, useGLTF, useFBX, Stage, View, PerspectiveCamera } from "@react-three/drei";
|
|
5
5
|
import { Suspense, useEffect, useState, useRef } from "react";
|
|
6
|
-
import * as React from "react";
|
|
7
6
|
import { TextureLoader } from "three";
|
|
8
7
|
|
|
9
8
|
// view models and textures in manifest, onselect callback
|
|
@@ -74,53 +73,6 @@ function useInView() {
|
|
|
74
73
|
return { ref, isInView };
|
|
75
74
|
}
|
|
76
75
|
|
|
77
|
-
export default function AssetViewerPage({ basePath = "" }: { basePath?: string } = {}) {
|
|
78
|
-
const [textures, setTextures] = useState<string[]>([]);
|
|
79
|
-
const [models, setModels] = useState<string[]>([]);
|
|
80
|
-
const [sounds, setSounds] = useState<string[]>([]);
|
|
81
|
-
const [loading, setLoading] = useState(true);
|
|
82
|
-
|
|
83
|
-
useEffect(() => {
|
|
84
|
-
const base = basePath ? `${basePath}/` : '';
|
|
85
|
-
Promise.all([
|
|
86
|
-
fetch(`/${base}textures/manifest.json`).then(r => r.json()),
|
|
87
|
-
fetch(`/${base}models/manifest.json`).then(r => r.json()),
|
|
88
|
-
fetch(`/${base}sound/manifest.json`).then(r => r.json()).catch(() => [])
|
|
89
|
-
]).then(([textureData, modelData, soundData]) => {
|
|
90
|
-
setTextures(textureData);
|
|
91
|
-
setModels(modelData);
|
|
92
|
-
setSounds(soundData);
|
|
93
|
-
setLoading(false);
|
|
94
|
-
});
|
|
95
|
-
}, [basePath]);
|
|
96
|
-
|
|
97
|
-
if (loading) {
|
|
98
|
-
return <div className="p-4 text-gray-300">Loading manifests...</div>;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
return (
|
|
102
|
-
<>
|
|
103
|
-
<div className="p-2 text-gray-300 overflow-y-auto h-screen text-sm">
|
|
104
|
-
<h1 className="text-lg mb-2 font-bold">Asset Viewer</h1>
|
|
105
|
-
|
|
106
|
-
<h2 className="text-sm mt-4 mb-1 font-semibold">Textures ({textures.length})</h2>
|
|
107
|
-
<TextureListViewer files={textures} basePath={basePath} onSelect={(file) => console.log('Selected texture:', file)} />
|
|
108
|
-
|
|
109
|
-
<h2 className="text-sm mt-4 mb-1 font-semibold">Models ({models.length})</h2>
|
|
110
|
-
<ModelListViewer files={models} basePath={basePath} onSelect={(file) => console.log('Selected model:', file)} />
|
|
111
|
-
|
|
112
|
-
{sounds.length > 0 && (
|
|
113
|
-
<>
|
|
114
|
-
<h2 className="text-sm mt-4 mb-1 font-semibold">Sounds ({sounds.length})</h2>
|
|
115
|
-
<SoundListViewer files={sounds} basePath={basePath} onSelect={(file) => console.log('Selected sound:', file)} />
|
|
116
|
-
</>
|
|
117
|
-
)}
|
|
118
|
-
</div>
|
|
119
|
-
<SharedCanvas />
|
|
120
|
-
</>
|
|
121
|
-
);
|
|
122
|
-
}
|
|
123
|
-
|
|
124
76
|
interface AssetListViewerProps {
|
|
125
77
|
files: string[];
|
|
126
78
|
selected?: string;
|
|
@@ -19,12 +19,11 @@ const fbxLoader = new FBXLoader();
|
|
|
19
19
|
|
|
20
20
|
export async function loadModel(
|
|
21
21
|
filename: string,
|
|
22
|
-
resourcePath: string = "",
|
|
23
22
|
onProgress?: ProgressCallback
|
|
24
23
|
): Promise<ModelLoadResult> {
|
|
25
24
|
try {
|
|
26
|
-
//
|
|
27
|
-
const fullPath =
|
|
25
|
+
// Use filename directly (should already include leading /)
|
|
26
|
+
const fullPath = filename;
|
|
28
27
|
|
|
29
28
|
if (filename.endsWith('.glb') || filename.endsWith('.gltf')) {
|
|
30
29
|
return new Promise((resolve) => {
|
|
@@ -210,6 +210,7 @@ export default function EditorTree({ prefabData, setPrefabData, selectedId, setS
|
|
|
210
210
|
<button
|
|
211
211
|
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"
|
|
212
212
|
onClick={() => handleAddChild(contextMenu.nodeId)}
|
|
213
|
+
onPointerLeave={closeContextMenu}
|
|
213
214
|
>
|
|
214
215
|
Add Child
|
|
215
216
|
</button>
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
3
|
import GameCanvas from "../../shared/GameCanvas";
|
|
4
|
-
import { useState, useRef, } from "react";
|
|
4
|
+
import { useState, useRef, useEffect } from "react";
|
|
5
5
|
import { Group, } from "three";
|
|
6
6
|
import { Prefab, } from "./types";
|
|
7
7
|
import PrefabRoot from "./PrefabRoot";
|
|
8
8
|
import { Physics } from "@react-three/rapier";
|
|
9
9
|
import EditorUI from "./EditorUI";
|
|
10
10
|
|
|
11
|
-
const PrefabEditor = ({ basePath, initialPrefab, children }: { basePath?: string, initialPrefab?: Prefab, children?: React.ReactNode }) => {
|
|
11
|
+
const PrefabEditor = ({ basePath, initialPrefab, onPrefabChange, children }: { basePath?: string, initialPrefab?: Prefab, onPrefabChange?: (prefab: Prefab) => void, children?: React.ReactNode }) => {
|
|
12
12
|
const [editMode, setEditMode] = useState(true);
|
|
13
13
|
const [loadedPrefab, setLoadedPrefab] = useState<Prefab>(initialPrefab ?? {
|
|
14
14
|
"id": "prefab-default",
|
|
@@ -33,6 +33,20 @@ const PrefabEditor = ({ basePath, initialPrefab, children }: { basePath?: string
|
|
|
33
33
|
const [transformMode, setTransformMode] = useState<"translate" | "rotate" | "scale">("translate");
|
|
34
34
|
const prefabRef = useRef<Group>(null);
|
|
35
35
|
|
|
36
|
+
// Sync internal state with external initialPrefab prop
|
|
37
|
+
useEffect(() => {
|
|
38
|
+
if (initialPrefab) {
|
|
39
|
+
setLoadedPrefab(initialPrefab);
|
|
40
|
+
}
|
|
41
|
+
}, [initialPrefab]);
|
|
42
|
+
|
|
43
|
+
// Wrapper to update prefab and notify parent
|
|
44
|
+
const updatePrefab = (newPrefab: Prefab | ((prev: Prefab) => Prefab)) => {
|
|
45
|
+
setLoadedPrefab(newPrefab);
|
|
46
|
+
const resolved = typeof newPrefab === 'function' ? newPrefab(loadedPrefab) : newPrefab;
|
|
47
|
+
onPrefabChange?.(resolved);
|
|
48
|
+
};
|
|
49
|
+
|
|
36
50
|
return <>
|
|
37
51
|
<GameCanvas>
|
|
38
52
|
<Physics paused={editMode}>
|
|
@@ -44,7 +58,7 @@ const PrefabEditor = ({ basePath, initialPrefab, children }: { basePath?: string
|
|
|
44
58
|
|
|
45
59
|
// props for edit mode
|
|
46
60
|
editMode={editMode}
|
|
47
|
-
onPrefabChange={
|
|
61
|
+
onPrefabChange={updatePrefab}
|
|
48
62
|
selectedId={selectedId}
|
|
49
63
|
onSelect={setSelectedId}
|
|
50
64
|
transformMode={transformMode}
|
|
@@ -81,7 +95,7 @@ const PrefabEditor = ({ basePath, initialPrefab, children }: { basePath?: string
|
|
|
81
95
|
</div>
|
|
82
96
|
{editMode && <EditorUI
|
|
83
97
|
prefabData={loadedPrefab}
|
|
84
|
-
setPrefabData={
|
|
98
|
+
setPrefabData={updatePrefab}
|
|
85
99
|
selectedId={selectedId}
|
|
86
100
|
setSelectedId={setSelectedId}
|
|
87
101
|
transformMode={transformMode}
|
|
@@ -133,7 +133,9 @@ export const PrefabRoot = forwardRef<Group, {
|
|
|
133
133
|
for (const filename of modelsToLoad) {
|
|
134
134
|
if (!loadedModels[filename] && !loadingRefs.current.has(filename)) {
|
|
135
135
|
loadingRefs.current.add(filename);
|
|
136
|
-
|
|
136
|
+
// Load model directly from public root, prepend "/" if not present
|
|
137
|
+
const modelPath = filename.startsWith('/') ? filename : `/${filename}`;
|
|
138
|
+
const result = await loadModel(modelPath);
|
|
137
139
|
if (result.success && result.model) {
|
|
138
140
|
setLoadedModels(prev => ({ ...prev, [filename]: result.model }));
|
|
139
141
|
}
|
|
@@ -144,7 +146,8 @@ export const PrefabRoot = forwardRef<Group, {
|
|
|
144
146
|
for (const filename of texturesToLoad) {
|
|
145
147
|
if (!loadedTextures[filename] && !loadingRefs.current.has(filename)) {
|
|
146
148
|
loadingRefs.current.add(filename);
|
|
147
|
-
|
|
149
|
+
// Load texture directly from public root, prepend "/" if not present
|
|
150
|
+
const texturePath = filename.startsWith('/') ? filename : `/${filename}`;
|
|
148
151
|
textureLoader.load(texturePath, (texture) => {
|
|
149
152
|
texture.colorSpace = SRGBColorSpace;
|
|
150
153
|
setLoadedTextures(prev => ({ ...prev, [filename]: texture }));
|