react-three-game 0.0.30 → 0.0.32
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 +18 -5
- package/dist/tools/assetviewer/page.js +22 -8
- package/dist/tools/prefabeditor/EditorUI.js +6 -2
- package/dist/tools/prefabeditor/components/GeometryComponent.js +33 -3
- package/package.json +1 -1
- package/src/tools/assetviewer/page.tsx +37 -22
- package/src/tools/prefabeditor/EditorUI.tsx +7 -1
- package/src/tools/prefabeditor/components/GeometryComponent.tsx +70 -14
package/README.md
CHANGED
|
@@ -138,15 +138,28 @@ React 19 · Three.js WebGPU · TypeScript 5 · Rapier WASM · MIT License
|
|
|
138
138
|
|
|
139
139
|
A small helper script is included to auto-generate asset manifests from the `public` folder. See `docs/generate-manifests.sh`.
|
|
140
140
|
|
|
141
|
-
- What it does
|
|
142
|
-
|
|
141
|
+
- **What it does:**
|
|
142
|
+
Searches `public/models` for `.glb`/`.fbx`, `public/textures` for `.jpg`/`.png`, and `public/sound` for `.mp3`/`.wav`, then writes JSON arrays to:
|
|
143
|
+
- `public/models/manifest.json`
|
|
144
|
+
- `public/textures/manifest.json`
|
|
145
|
+
- `public/sound/manifest.json`
|
|
146
|
+
|
|
147
|
+
These manifest files are used to populate the Asset Viewer in the Editor.
|
|
148
|
+
|
|
149
|
+
- **How to run:**
|
|
143
150
|
|
|
144
151
|
1. Make it executable (once):
|
|
145
152
|
|
|
146
|
-
|
|
153
|
+
```sh
|
|
154
|
+
chmod +x docs/generate-manifests.sh
|
|
155
|
+
```
|
|
147
156
|
|
|
148
157
|
2. Run the script from the repo root (zsh/bash):
|
|
149
158
|
|
|
150
|
-
|
|
159
|
+
```sh
|
|
160
|
+
./docs/generate-manifests.sh
|
|
161
|
+
```
|
|
162
|
+
|
|
151
163
|
|
|
152
|
-
The script is intentionally simple and portable (uses `find`/`sed`).
|
|
164
|
+
The script is intentionally simple and portable (uses `find`/`sed`).
|
|
165
|
+
If you need different file types or output formatting, edit `docs/generate-manifests.sh`.
|
|
@@ -6,6 +6,12 @@ import { Suspense, useEffect, useState, useRef } from "react";
|
|
|
6
6
|
import { TextureLoader } from "three";
|
|
7
7
|
import { loadModel } from "../dragdrop/modelLoader";
|
|
8
8
|
// view models and textures in manifest, onselect callback
|
|
9
|
+
const styles = {
|
|
10
|
+
errorIcon: { color: '#fca5a5', fontSize: 12 }, // text-red-400 text-xs
|
|
11
|
+
flexFillRelative: { flex: 1, position: 'relative' },
|
|
12
|
+
bottomLabel: { backgroundColor: 'rgba(0,0,0,0.6)', fontSize: 10, padding: '0 4px', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', textAlign: 'center' },
|
|
13
|
+
iconLarge: { fontSize: 20 }
|
|
14
|
+
};
|
|
9
15
|
function getItemsInPath(files, currentPath) {
|
|
10
16
|
// Remove the leading category folder (e.g., /textures/, /models/, /sounds/)
|
|
11
17
|
const filesWithoutCategory = files.map(file => {
|
|
@@ -30,7 +36,15 @@ function getItemsInPath(files, currentPath) {
|
|
|
30
36
|
return { folders: Array.from(folders), filesInCurrentPath };
|
|
31
37
|
}
|
|
32
38
|
function FolderTile({ name, onClick }) {
|
|
33
|
-
return (_jsxs("div", { onClick: onClick,
|
|
39
|
+
return (_jsxs("div", { onClick: onClick, style: {
|
|
40
|
+
aspectRatio: '1 / 1',
|
|
41
|
+
backgroundColor: '#1f2937', /* gray-800 */
|
|
42
|
+
cursor: 'pointer',
|
|
43
|
+
display: 'flex',
|
|
44
|
+
flexDirection: 'column',
|
|
45
|
+
alignItems: 'center',
|
|
46
|
+
justifyContent: 'center'
|
|
47
|
+
}, children: [_jsx("div", { style: { fontSize: 24 }, children: "\uD83D\uDCC1" }), _jsx("div", { style: { fontSize: 10, textAlign: 'center', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', width: '100%', padding: '0 4px', marginTop: 4 }, children: name })] }));
|
|
34
48
|
}
|
|
35
49
|
function useInView() {
|
|
36
50
|
const [isInView, setIsInView] = useState(false);
|
|
@@ -56,13 +70,13 @@ function AssetListViewer({ files, selected, onSelect, renderCard }) {
|
|
|
56
70
|
const { folders, filesInCurrentPath } = getItemsInPath(files, currentPath);
|
|
57
71
|
const showCompactView = selected && !showPicker;
|
|
58
72
|
if (showCompactView) {
|
|
59
|
-
return (_jsxs("div", {
|
|
73
|
+
return (_jsxs("div", { style: { display: 'flex', gap: 4, alignItems: 'center' }, children: [renderCard(selected, onSelect), _jsx("button", { onClick: () => setShowPicker(true), style: { padding: '4px 8px', backgroundColor: '#1f2937', color: 'inherit', fontSize: 12, cursor: 'pointer', border: 'none' }, children: "Change" })] }));
|
|
60
74
|
}
|
|
61
75
|
return (_jsxs("div", { children: [currentPath && (_jsx("button", { onClick: () => {
|
|
62
76
|
const pathParts = currentPath.split('/').filter(Boolean);
|
|
63
77
|
pathParts.pop();
|
|
64
78
|
setCurrentPath(pathParts.join('/'));
|
|
65
|
-
},
|
|
79
|
+
}, style: { marginBottom: 4, padding: '4px 8px', backgroundColor: '#1f2937', color: 'inherit', fontSize: 12, cursor: 'pointer', border: 'none' }, children: "\u2190 Back" })), _jsxs("div", { style: { display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: 4 }, children: [folders.map((folder) => (_jsx(FolderTile, { name: folder, onClick: () => setCurrentPath(currentPath ? `${currentPath}/${folder}` : folder) }, folder))), filesInCurrentPath.map((file) => (_jsx("div", { children: renderCard(file, (f) => {
|
|
66
80
|
onSelect(f);
|
|
67
81
|
if (selected)
|
|
68
82
|
setShowPicker(false);
|
|
@@ -77,9 +91,9 @@ function TextureCard({ file, onSelect, basePath = "" }) {
|
|
|
77
91
|
const { ref, isInView } = useInView();
|
|
78
92
|
const fullPath = basePath ? `/${basePath}${file}` : file;
|
|
79
93
|
if (error) {
|
|
80
|
-
return (_jsx("div", { ref: ref,
|
|
94
|
+
return (_jsx("div", { ref: ref, style: { aspectRatio: '1 / 1', backgroundColor: '#374151', cursor: 'pointer', display: 'flex', alignItems: 'center', justifyContent: 'center' }, onClick: () => onSelect(file), children: _jsx("div", { style: styles.errorIcon, children: "\u2717" }) }));
|
|
81
95
|
}
|
|
82
|
-
return (_jsxs("div", { ref: ref,
|
|
96
|
+
return (_jsxs("div", { ref: ref, style: { aspectRatio: '1 / 1', backgroundColor: '#1f2937', cursor: 'pointer', display: 'flex', flexDirection: 'column' }, onClick: () => onSelect(file), onMouseEnter: () => setIsHovered(true), onMouseLeave: () => setIsHovered(false), children: [_jsx("div", { style: { flex: 1, position: 'relative' }, children: isInView ? (_jsxs(View, { style: { width: '100%', height: '100%' }, children: [_jsx(PerspectiveCamera, { makeDefault: true, position: [0, 0, 2.5], fov: 50 }), _jsxs(Suspense, { fallback: null, children: [_jsx("ambientLight", { intensity: 0.8 }), _jsx("pointLight", { position: [5, 5, 5], intensity: 0.5 }), _jsx(TextureSphere, { url: fullPath, onError: () => setError(true) }), _jsx(OrbitControls, { enableZoom: false, enablePan: false, autoRotate: isHovered, autoRotateSpeed: 2 })] })] })) : null }), _jsx("div", { style: { backgroundColor: 'rgba(0,0,0,0.6)', fontSize: 10, padding: '0 4px', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', textAlign: 'center' }, children: file.split('/').pop() })] }));
|
|
83
97
|
}
|
|
84
98
|
function TextureSphere({ url, onError }) {
|
|
85
99
|
const texture = useLoader(TextureLoader, url, undefined, (error) => {
|
|
@@ -96,9 +110,9 @@ function ModelCard({ file, onSelect, basePath = "" }) {
|
|
|
96
110
|
const { ref, isInView } = useInView();
|
|
97
111
|
const fullPath = basePath ? `/${basePath}${file}` : file;
|
|
98
112
|
if (error) {
|
|
99
|
-
return (_jsx("div", { ref: ref,
|
|
113
|
+
return (_jsx("div", { ref: ref, style: { aspectRatio: '1 / 1', backgroundColor: '#374151', cursor: 'pointer', display: 'flex', alignItems: 'center', justifyContent: 'center' }, onClick: () => onSelect(file), children: _jsx("div", { style: styles.errorIcon, children: "\u2717" }) }));
|
|
100
114
|
}
|
|
101
|
-
return (_jsxs("div", { ref: ref,
|
|
115
|
+
return (_jsxs("div", { ref: ref, style: { aspectRatio: '1 / 1', backgroundColor: '#111827', cursor: 'pointer', display: 'flex', flexDirection: 'column' }, onClick: () => onSelect(file), children: [_jsx("div", { style: styles.flexFillRelative, children: isInView ? (_jsxs(View, { style: { width: '100%', height: '100%' }, children: [_jsx(PerspectiveCamera, { makeDefault: true, position: [0, 1, 3], fov: 50 }), _jsxs(Suspense, { fallback: null, children: [_jsx(Stage, { intensity: 0.5, environment: "city", children: _jsx(ModelPreview, { url: fullPath, onError: () => setError(true) }) }), _jsx(OrbitControls, { enableZoom: false })] })] })) : null }), _jsx("div", { style: { backgroundColor: 'rgba(0,0,0,0.6)', fontSize: 10, padding: '0 4px', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', textAlign: 'center' }, children: file.split('/').pop() })] }));
|
|
102
116
|
}
|
|
103
117
|
function ModelPreview({ url, onError }) {
|
|
104
118
|
const [model, setModel] = useState(null);
|
|
@@ -130,7 +144,7 @@ export function SoundListViewer({ files, selected, onSelect, basePath = "" }) {
|
|
|
130
144
|
function SoundCard({ file, onSelect, basePath = "" }) {
|
|
131
145
|
const fileName = file.split('/').pop() || '';
|
|
132
146
|
const fullPath = basePath ? `/${basePath}${file}` : file;
|
|
133
|
-
return (_jsxs("div", { onClick: () => onSelect(file),
|
|
147
|
+
return (_jsxs("div", { onClick: () => onSelect(file), style: { aspectRatio: '1 / 1', backgroundColor: '#374151', cursor: 'pointer', display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center' }, children: [_jsx("div", { style: styles.iconLarge, children: "\uD83D\uDD0A" }), _jsx("div", { style: { fontSize: 12, padding: '0 4px', marginTop: 4, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', textAlign: 'center', width: '100%' }, children: fileName })] }));
|
|
134
148
|
}
|
|
135
149
|
// Shared Canvas Component - can be used independently in any viewer
|
|
136
150
|
export function SharedCanvas() {
|
|
@@ -29,7 +29,11 @@ function EditorUI({ prefabData, setPrefabData, selectedId, setSelectedId, transf
|
|
|
29
29
|
setSelectedId(null);
|
|
30
30
|
};
|
|
31
31
|
const selectedNode = selectedId && prefabData ? findNode(prefabData.root, selectedId) : null;
|
|
32
|
-
return _jsxs(_Fragment, { children: [
|
|
32
|
+
return _jsxs(_Fragment, { children: [_jsx("style", { children: `.prefab-scroll::-webkit-scrollbar { width: 8px; height: 8px; }
|
|
33
|
+
.prefab-scroll::-webkit-scrollbar-track { background: transparent; }
|
|
34
|
+
.prefab-scroll::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.06); border-radius: 8px; }
|
|
35
|
+
.prefab-scroll { scrollbar-width: thin; scrollbar-color: rgba(255,255,255,0.06) transparent; }
|
|
36
|
+
` }), _jsxs("div", { style: inspector.panel, children: [_jsxs("div", { style: base.header, onClick: () => setCollapsed(!collapsed), children: [_jsx("span", { children: "Inspector" }), _jsx("span", { children: collapsed ? '◀' : '▼' })] }), !collapsed && selectedNode && (_jsx(NodeInspector, { node: selectedNode, updateNode: updateNodeHandler, deleteNode: deleteNodeHandler, transformMode: transformMode, setTransformMode: setTransformMode, basePath: basePath }))] }), _jsx("div", { style: { position: 'absolute', top: 8, left: 8, zIndex: 20 }, children: _jsx(EditorTree, { prefabData: prefabData, setPrefabData: setPrefabData, selectedId: selectedId, setSelectedId: setSelectedId }) })] });
|
|
33
37
|
}
|
|
34
38
|
function NodeInspector({ node, updateNode, deleteNode, transformMode, setTransformMode, basePath }) {
|
|
35
39
|
var _a;
|
|
@@ -42,7 +46,7 @@ function NodeInspector({ node, updateNode, deleteNode, transformMode, setTransfo
|
|
|
42
46
|
if (!newAvailable.includes(addType))
|
|
43
47
|
setAddType(newAvailable[0] || "");
|
|
44
48
|
}, [Object.keys(node.components || {}).join(',')]);
|
|
45
|
-
return _jsxs("div", { style: inspector.content, children: [_jsxs("div", { style: base.section, children: [_jsx("div", { style: base.label, children: "Node ID" }), _jsx("input", { style: base.input, value: (_a = node.name) !== null && _a !== void 0 ? _a : "", onChange: e => updateNode(n => (Object.assign(Object.assign({}, n), { name: e.target.value }))) })] }), _jsxs("div", { style: base.section, children: [_jsxs("div", { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 8 }, children: [_jsx("div", { style: base.label, children: "Components" }), _jsx("button", { style: Object.assign(Object.assign({}, base.btn), base.btnDanger), onClick: deleteNode, children: "Delete Node" })] }), node.components && Object.entries(node.components).map(([key, comp]) => {
|
|
49
|
+
return _jsxs("div", { style: inspector.content, className: "prefab-scroll", children: [_jsxs("div", { style: base.section, children: [_jsx("div", { style: base.label, children: "Node ID" }), _jsx("input", { style: base.input, value: (_a = node.name) !== null && _a !== void 0 ? _a : "", onChange: e => updateNode(n => (Object.assign(Object.assign({}, n), { name: e.target.value }))) })] }), _jsxs("div", { style: base.section, children: [_jsxs("div", { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 8 }, children: [_jsx("div", { style: base.label, children: "Components" }), _jsx("button", { style: Object.assign(Object.assign({}, base.btn), base.btnDanger), onClick: deleteNode, children: "Delete Node" })] }), node.components && Object.entries(node.components).map(([key, comp]) => {
|
|
46
50
|
if (!comp)
|
|
47
51
|
return null;
|
|
48
52
|
const def = ALL_COMPONENTS[comp.type];
|
|
@@ -1,6 +1,35 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
|
|
3
|
-
|
|
2
|
+
const GEOMETRY_ARGS = {
|
|
3
|
+
box: {
|
|
4
|
+
labels: ["Width", "Height", "Depth"],
|
|
5
|
+
defaults: [1, 1, 1],
|
|
6
|
+
},
|
|
7
|
+
sphere: {
|
|
8
|
+
labels: ["Radius", "Width Segments", "Height Segments"],
|
|
9
|
+
defaults: [1, 32, 16],
|
|
10
|
+
},
|
|
11
|
+
plane: {
|
|
12
|
+
labels: ["Width", "Height"],
|
|
13
|
+
defaults: [1, 1],
|
|
14
|
+
},
|
|
15
|
+
};
|
|
16
|
+
function GeometryComponentEditor({ component, onUpdate, }) {
|
|
17
|
+
const { geometryType, args = [] } = component.properties;
|
|
18
|
+
const schema = GEOMETRY_ARGS[geometryType];
|
|
19
|
+
return (_jsxs("div", { className: "flex flex-col gap-1", children: [_jsx("label", { className: "label", children: "Type" }), _jsxs("select", { className: "select", value: geometryType, onChange: e => {
|
|
20
|
+
const type = e.target.value;
|
|
21
|
+
onUpdate({
|
|
22
|
+
geometryType: type,
|
|
23
|
+
args: GEOMETRY_ARGS[type].defaults,
|
|
24
|
+
});
|
|
25
|
+
}, children: [_jsx("option", { value: "box", children: "Box" }), _jsx("option", { value: "sphere", children: "Sphere" }), _jsx("option", { value: "plane", children: "Plane" })] }), schema.labels.map((label, i) => {
|
|
26
|
+
var _a;
|
|
27
|
+
return (_jsxs("div", { children: [_jsx("label", { className: "label", children: label }), _jsx("input", { type: "number", className: "input", value: (_a = args[i]) !== null && _a !== void 0 ? _a : schema.defaults[i], step: "0.1", onChange: e => {
|
|
28
|
+
const next = [...args];
|
|
29
|
+
next[i] = parseFloat(e.target.value);
|
|
30
|
+
onUpdate({ args: next });
|
|
31
|
+
} })] }, label));
|
|
32
|
+
})] }));
|
|
4
33
|
}
|
|
5
34
|
// View for Geometry component
|
|
6
35
|
function GeometryComponentView({ properties, children }) {
|
|
@@ -22,7 +51,8 @@ const GeometryComponent = {
|
|
|
22
51
|
Editor: GeometryComponentEditor,
|
|
23
52
|
View: GeometryComponentView,
|
|
24
53
|
defaultProperties: {
|
|
25
|
-
geometryType: 'box'
|
|
54
|
+
geometryType: 'box',
|
|
55
|
+
args: GEOMETRY_ARGS.box.defaults,
|
|
26
56
|
}
|
|
27
57
|
};
|
|
28
58
|
export default GeometryComponent;
|
package/package.json
CHANGED
|
@@ -8,6 +8,13 @@ import { loadModel } from "../dragdrop/modelLoader";
|
|
|
8
8
|
|
|
9
9
|
// view models and textures in manifest, onselect callback
|
|
10
10
|
|
|
11
|
+
const styles: Record<string, any> = {
|
|
12
|
+
errorIcon: { color: '#fca5a5', fontSize: 12 }, // text-red-400 text-xs
|
|
13
|
+
flexFillRelative: { flex: 1, position: 'relative' },
|
|
14
|
+
bottomLabel: { backgroundColor: 'rgba(0,0,0,0.6)', fontSize: 10, padding: '0 4px', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', textAlign: 'center' },
|
|
15
|
+
iconLarge: { fontSize: 20 }
|
|
16
|
+
};
|
|
17
|
+
|
|
11
18
|
function getItemsInPath(files: string[], currentPath: string) {
|
|
12
19
|
// Remove the leading category folder (e.g., /textures/, /models/, /sounds/)
|
|
13
20
|
const filesWithoutCategory = files.map(file => {
|
|
@@ -40,10 +47,18 @@ function FolderTile({ name, onClick }: { name: string; onClick: () => void }) {
|
|
|
40
47
|
return (
|
|
41
48
|
<div
|
|
42
49
|
onClick={onClick}
|
|
43
|
-
|
|
50
|
+
style={{
|
|
51
|
+
aspectRatio: '1 / 1',
|
|
52
|
+
backgroundColor: '#1f2937', /* gray-800 */
|
|
53
|
+
cursor: 'pointer',
|
|
54
|
+
display: 'flex',
|
|
55
|
+
flexDirection: 'column',
|
|
56
|
+
alignItems: 'center',
|
|
57
|
+
justifyContent: 'center'
|
|
58
|
+
}}
|
|
44
59
|
>
|
|
45
|
-
<div
|
|
46
|
-
<div
|
|
60
|
+
<div style={{ fontSize: 24 }}>📁</div>
|
|
61
|
+
<div style={{ fontSize: 10, textAlign: 'center', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', width: '100%', padding: '0 4px', marginTop: 4 }}>{name}</div>
|
|
47
62
|
</div>
|
|
48
63
|
);
|
|
49
64
|
}
|
|
@@ -90,11 +105,11 @@ function AssetListViewer({ files, selected, onSelect, renderCard }: AssetListVie
|
|
|
90
105
|
|
|
91
106
|
if (showCompactView) {
|
|
92
107
|
return (
|
|
93
|
-
<div
|
|
108
|
+
<div style={{ display: 'flex', gap: 4, alignItems: 'center' }}>
|
|
94
109
|
{renderCard(selected, onSelect)}
|
|
95
110
|
<button
|
|
96
111
|
onClick={() => setShowPicker(true)}
|
|
97
|
-
|
|
112
|
+
style={{ padding: '4px 8px', backgroundColor: '#1f2937', color: 'inherit', fontSize: 12, cursor: 'pointer', border: 'none' }}
|
|
98
113
|
>
|
|
99
114
|
Change
|
|
100
115
|
</button>
|
|
@@ -111,12 +126,12 @@ function AssetListViewer({ files, selected, onSelect, renderCard }: AssetListVie
|
|
|
111
126
|
pathParts.pop();
|
|
112
127
|
setCurrentPath(pathParts.join('/'));
|
|
113
128
|
}}
|
|
114
|
-
|
|
129
|
+
style={{ marginBottom: 4, padding: '4px 8px', backgroundColor: '#1f2937', color: 'inherit', fontSize: 12, cursor: 'pointer', border: 'none' }}
|
|
115
130
|
>
|
|
116
131
|
← Back
|
|
117
132
|
</button>
|
|
118
133
|
)}
|
|
119
|
-
<div
|
|
134
|
+
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: 4 }}>
|
|
120
135
|
{folders.map((folder) => (
|
|
121
136
|
<FolderTile
|
|
122
137
|
key={folder}
|
|
@@ -170,10 +185,10 @@ function TextureCard({ file, onSelect, basePath = "" }: { file: string; onSelect
|
|
|
170
185
|
return (
|
|
171
186
|
<div
|
|
172
187
|
ref={ref}
|
|
173
|
-
|
|
188
|
+
style={{ aspectRatio: '1 / 1', backgroundColor: '#374151', cursor: 'pointer', display: 'flex', alignItems: 'center', justifyContent: 'center' }}
|
|
174
189
|
onClick={() => onSelect(file)}
|
|
175
190
|
>
|
|
176
|
-
<div
|
|
191
|
+
<div style={styles.errorIcon}>✗</div>
|
|
177
192
|
</div>
|
|
178
193
|
);
|
|
179
194
|
}
|
|
@@ -181,14 +196,14 @@ function TextureCard({ file, onSelect, basePath = "" }: { file: string; onSelect
|
|
|
181
196
|
return (
|
|
182
197
|
<div
|
|
183
198
|
ref={ref}
|
|
184
|
-
|
|
199
|
+
style={{ aspectRatio: '1 / 1', backgroundColor: '#1f2937', cursor: 'pointer', display: 'flex', flexDirection: 'column' }}
|
|
185
200
|
onClick={() => onSelect(file)}
|
|
186
201
|
onMouseEnter={() => setIsHovered(true)}
|
|
187
202
|
onMouseLeave={() => setIsHovered(false)}
|
|
188
203
|
>
|
|
189
|
-
<div
|
|
204
|
+
<div style={{ flex: 1, position: 'relative' }}>
|
|
190
205
|
{isInView ? (
|
|
191
|
-
<View
|
|
206
|
+
<View style={{ width: '100%', height: '100%' }}>
|
|
192
207
|
<PerspectiveCamera makeDefault position={[0, 0, 2.5]} fov={50} />
|
|
193
208
|
<Suspense fallback={null}>
|
|
194
209
|
<ambientLight intensity={0.8} />
|
|
@@ -204,7 +219,7 @@ function TextureCard({ file, onSelect, basePath = "" }: { file: string; onSelect
|
|
|
204
219
|
</View>
|
|
205
220
|
) : null}
|
|
206
221
|
</div>
|
|
207
|
-
<div
|
|
222
|
+
<div style={{ backgroundColor: 'rgba(0,0,0,0.6)', fontSize: 10, padding: '0 4px', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', textAlign: 'center' }}>
|
|
208
223
|
{file.split('/').pop()}
|
|
209
224
|
</div>
|
|
210
225
|
</div>
|
|
@@ -256,10 +271,10 @@ function ModelCard({ file, onSelect, basePath = "" }: { file: string; onSelect:
|
|
|
256
271
|
return (
|
|
257
272
|
<div
|
|
258
273
|
ref={ref}
|
|
259
|
-
|
|
274
|
+
style={{ aspectRatio: '1 / 1', backgroundColor: '#374151', cursor: 'pointer', display: 'flex', alignItems: 'center', justifyContent: 'center' }}
|
|
260
275
|
onClick={() => onSelect(file)}
|
|
261
276
|
>
|
|
262
|
-
<div
|
|
277
|
+
<div style={styles.errorIcon}>✗</div>
|
|
263
278
|
</div>
|
|
264
279
|
);
|
|
265
280
|
}
|
|
@@ -267,12 +282,12 @@ function ModelCard({ file, onSelect, basePath = "" }: { file: string; onSelect:
|
|
|
267
282
|
return (
|
|
268
283
|
<div
|
|
269
284
|
ref={ref}
|
|
270
|
-
|
|
285
|
+
style={{ aspectRatio: '1 / 1', backgroundColor: '#111827', cursor: 'pointer', display: 'flex', flexDirection: 'column' }}
|
|
271
286
|
onClick={() => onSelect(file)}
|
|
272
287
|
>
|
|
273
|
-
<div
|
|
288
|
+
<div style={styles.flexFillRelative}>
|
|
274
289
|
{isInView ? (
|
|
275
|
-
<View
|
|
290
|
+
<View style={{ width: '100%', height: '100%' }}>
|
|
276
291
|
<PerspectiveCamera makeDefault position={[0, 1, 3]} fov={50} />
|
|
277
292
|
<Suspense fallback={null}>
|
|
278
293
|
<Stage intensity={0.5} environment="city">
|
|
@@ -283,7 +298,7 @@ function ModelCard({ file, onSelect, basePath = "" }: { file: string; onSelect:
|
|
|
283
298
|
</View>
|
|
284
299
|
) : null}
|
|
285
300
|
</div>
|
|
286
|
-
<div
|
|
301
|
+
<div style={{ backgroundColor: 'rgba(0,0,0,0.6)', fontSize: 10, padding: '0 4px', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', textAlign: 'center' }}>
|
|
287
302
|
{file.split('/').pop()}
|
|
288
303
|
</div>
|
|
289
304
|
</div>
|
|
@@ -341,10 +356,10 @@ function SoundCard({ file, onSelect, basePath = "" }: { file: string; onSelect:
|
|
|
341
356
|
return (
|
|
342
357
|
<div
|
|
343
358
|
onClick={() => onSelect(file)}
|
|
344
|
-
|
|
359
|
+
style={{ aspectRatio: '1 / 1', backgroundColor: '#374151', cursor: 'pointer', display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center' }}
|
|
345
360
|
>
|
|
346
|
-
<div
|
|
347
|
-
<div
|
|
361
|
+
<div style={styles.iconLarge}>🔊</div>
|
|
362
|
+
<div style={{ fontSize: 12, padding: '0 4px', marginTop: 4, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', textAlign: 'center', width: '100%' }}>{fileName}</div>
|
|
348
363
|
</div>
|
|
349
364
|
);
|
|
350
365
|
}
|
|
@@ -33,6 +33,11 @@ function EditorUI({ prefabData, setPrefabData, selectedId, setSelectedId, transf
|
|
|
33
33
|
const selectedNode = selectedId && prefabData ? findNode(prefabData.root, selectedId) : null;
|
|
34
34
|
|
|
35
35
|
return <>
|
|
36
|
+
<style>{`.prefab-scroll::-webkit-scrollbar { width: 8px; height: 8px; }
|
|
37
|
+
.prefab-scroll::-webkit-scrollbar-track { background: transparent; }
|
|
38
|
+
.prefab-scroll::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.06); border-radius: 8px; }
|
|
39
|
+
.prefab-scroll { scrollbar-width: thin; scrollbar-color: rgba(255,255,255,0.06) transparent; }
|
|
40
|
+
`}</style>
|
|
36
41
|
<div style={inspector.panel}>
|
|
37
42
|
<div style={base.header} onClick={() => setCollapsed(!collapsed)}>
|
|
38
43
|
<span>Inspector</span>
|
|
@@ -46,6 +51,7 @@ function EditorUI({ prefabData, setPrefabData, selectedId, setSelectedId, transf
|
|
|
46
51
|
transformMode={transformMode}
|
|
47
52
|
setTransformMode={setTransformMode}
|
|
48
53
|
basePath={basePath}
|
|
54
|
+
// add class to make scrollbar gutter transparent via CSS above
|
|
49
55
|
/>
|
|
50
56
|
)}
|
|
51
57
|
</div>
|
|
@@ -79,7 +85,7 @@ function NodeInspector({ node, updateNode, deleteNode, transformMode, setTransfo
|
|
|
79
85
|
if (!newAvailable.includes(addType)) setAddType(newAvailable[0] || "");
|
|
80
86
|
}, [Object.keys(node.components || {}).join(',')]);
|
|
81
87
|
|
|
82
|
-
return <div style={inspector.content}>
|
|
88
|
+
return <div style={inspector.content} className="prefab-scroll">
|
|
83
89
|
{/* Node ID */}
|
|
84
90
|
<div style={base.section}>
|
|
85
91
|
<div style={base.label}>Node ID</div>
|
|
@@ -1,20 +1,75 @@
|
|
|
1
1
|
import { Component } from "./ComponentRegistry";
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
3
|
+
const GEOMETRY_ARGS: Record<string, {
|
|
4
|
+
labels: string[];
|
|
5
|
+
defaults: number[];
|
|
6
|
+
}> = {
|
|
7
|
+
box: {
|
|
8
|
+
labels: ["Width", "Height", "Depth"],
|
|
9
|
+
defaults: [1, 1, 1],
|
|
10
|
+
},
|
|
11
|
+
sphere: {
|
|
12
|
+
labels: ["Radius", "Width Segments", "Height Segments"],
|
|
13
|
+
defaults: [1, 32, 16],
|
|
14
|
+
},
|
|
15
|
+
plane: {
|
|
16
|
+
labels: ["Width", "Height"],
|
|
17
|
+
defaults: [1, 1],
|
|
18
|
+
},
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
function GeometryComponentEditor({
|
|
22
|
+
component,
|
|
23
|
+
onUpdate,
|
|
24
|
+
}: {
|
|
25
|
+
component: any;
|
|
26
|
+
onUpdate: (newProps: any) => void;
|
|
27
|
+
}) {
|
|
28
|
+
const { geometryType, args = [] } = component.properties;
|
|
29
|
+
const schema = GEOMETRY_ARGS[geometryType];
|
|
30
|
+
|
|
31
|
+
return (
|
|
32
|
+
<div className="flex flex-col gap-1">
|
|
33
|
+
{/* Geometry Type */}
|
|
34
|
+
<label className="label">Type</label>
|
|
35
|
+
<select
|
|
36
|
+
className="select"
|
|
37
|
+
value={geometryType}
|
|
38
|
+
onChange={e => {
|
|
39
|
+
const type = e.target.value;
|
|
40
|
+
onUpdate({
|
|
41
|
+
geometryType: type,
|
|
42
|
+
args: GEOMETRY_ARGS[type].defaults,
|
|
43
|
+
});
|
|
44
|
+
}}
|
|
45
|
+
>
|
|
46
|
+
<option value="box">Box</option>
|
|
47
|
+
<option value="sphere">Sphere</option>
|
|
48
|
+
<option value="plane">Plane</option>
|
|
49
|
+
</select>
|
|
50
|
+
|
|
51
|
+
{/* Args */}
|
|
52
|
+
{schema.labels.map((label, i) => (
|
|
53
|
+
<div key={label}>
|
|
54
|
+
<label className="label">{label}</label>
|
|
55
|
+
<input
|
|
56
|
+
type="number"
|
|
57
|
+
className="input"
|
|
58
|
+
value={args[i] ?? schema.defaults[i]}
|
|
59
|
+
step="0.1"
|
|
60
|
+
onChange={e => {
|
|
61
|
+
const next = [...args];
|
|
62
|
+
next[i] = parseFloat(e.target.value);
|
|
63
|
+
onUpdate({ args: next });
|
|
64
|
+
}}
|
|
65
|
+
/>
|
|
66
|
+
</div>
|
|
67
|
+
))}
|
|
68
|
+
</div>
|
|
69
|
+
);
|
|
16
70
|
}
|
|
17
71
|
|
|
72
|
+
|
|
18
73
|
// View for Geometry component
|
|
19
74
|
function GeometryComponentView({ properties, children }: { properties: any, children?: React.ReactNode }) {
|
|
20
75
|
const { geometryType, args = [] } = properties;
|
|
@@ -36,7 +91,8 @@ const GeometryComponent: Component = {
|
|
|
36
91
|
Editor: GeometryComponentEditor,
|
|
37
92
|
View: GeometryComponentView,
|
|
38
93
|
defaultProperties: {
|
|
39
|
-
geometryType: 'box'
|
|
94
|
+
geometryType: 'box',
|
|
95
|
+
args: GEOMETRY_ARGS.box.defaults,
|
|
40
96
|
}
|
|
41
97
|
};
|
|
42
98
|
|