react-three-game 0.0.29 → 0.0.31
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 -0
- package/assets/architecture.png +0 -0
- package/dist/tools/assetviewer/page.js +22 -8
- package/dist/tools/prefabeditor/EditorUI.js +6 -2
- package/dist/tools/prefabeditor/components/DirectionalLightComponent.js +1 -1
- package/dist/tools/prefabeditor/components/GeometryComponent.js +1 -1
- package/dist/tools/prefabeditor/components/MaterialComponent.js +2 -2
- package/dist/tools/prefabeditor/components/ModelComponent.js +1 -1
- package/dist/tools/prefabeditor/components/PhysicsComponent.js +3 -3
- package/dist/tools/prefabeditor/components/SpotLightComponent.js +1 -1
- package/dist/tools/prefabeditor/components/TransformComponent.js +2 -2
- 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/DirectionalLightComponent.tsx +35 -35
- package/src/tools/prefabeditor/components/GeometryComponent.tsx +2 -2
- package/src/tools/prefabeditor/components/MaterialComponent.tsx +19 -19
- package/src/tools/prefabeditor/components/ModelComponent.tsx +6 -6
- package/src/tools/prefabeditor/components/PhysicsComponent.tsx +6 -6
- package/src/tools/prefabeditor/components/SpotLightComponent.tsx +15 -15
- package/src/tools/prefabeditor/components/TransformComponent.tsx +9 -9
package/README.md
CHANGED
|
@@ -7,6 +7,7 @@ npm i react-three-game @react-three/fiber @react-three/rapier three
|
|
|
7
7
|
```
|
|
8
8
|
|
|
9
9
|

|
|
10
|
+

|
|
10
11
|
|
|
11
12
|
## Usage
|
|
12
13
|
|
|
@@ -132,3 +133,20 @@ npm run release # build + publish
|
|
|
132
133
|
---
|
|
133
134
|
|
|
134
135
|
React 19 · Three.js WebGPU · TypeScript 5 · Rapier WASM · MIT License
|
|
136
|
+
|
|
137
|
+
## Manifest generation script
|
|
138
|
+
|
|
139
|
+
A small helper script is included to auto-generate asset manifests from the `public` folder. See `docs/generate-manifests.sh`.
|
|
140
|
+
|
|
141
|
+
- What it does: searches `public/models` for `.glb`/`.fbx`, `public/textures` for `.jpg`/`.png`, and `public/sound` for `.mp3`/`.wav`, then writes JSON arrays to `public/models/manifest.json`, `public/textures/manifest.json`, and `public/sound/manifest.json`. These manifest files are used top populate the Asset Viewer in the the Editor.
|
|
142
|
+
- How to run:
|
|
143
|
+
|
|
144
|
+
1. Make it executable (once):
|
|
145
|
+
|
|
146
|
+
chmod +x docs/generate-manifests.sh
|
|
147
|
+
|
|
148
|
+
2. Run the script from the repo root (zsh/bash):
|
|
149
|
+
|
|
150
|
+
./docs/generate-manifests.sh
|
|
151
|
+
|
|
152
|
+
The script is intentionally simple and portable (uses `find`/`sed`). If you need different file types or output formatting, edit `docs/generate-manifests.sh`.
|
|
Binary file
|
|
@@ -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];
|
|
@@ -17,7 +17,7 @@ function DirectionalLightComponentEditor({ component, onUpdate }) {
|
|
|
17
17
|
shadowCameraRight: (_k = component.properties.shadowCameraRight) !== null && _k !== void 0 ? _k : 30,
|
|
18
18
|
targetOffset: (_l = component.properties.targetOffset) !== null && _l !== void 0 ? _l : [0, -5, 0]
|
|
19
19
|
};
|
|
20
|
-
return _jsxs("div", {
|
|
20
|
+
return _jsxs("div", { style: { display: 'flex', flexDirection: 'column', gap: 8 }, children: [_jsxs("div", { children: [_jsx("label", { style: { display: 'block', fontSize: '9px', color: 'rgba(34, 211, 238, 0.6)', textTransform: 'uppercase', letterSpacing: '0.05em', marginBottom: 2 }, children: "Color" }), _jsxs("div", { style: { display: 'flex', gap: 2 }, children: [_jsx("input", { type: "color", style: { height: 20, width: 20, backgroundColor: 'transparent', border: 'none', cursor: 'pointer' }, value: props.color, onChange: e => onUpdate(Object.assign(Object.assign({}, component.properties), { 'color': e.target.value })) }), _jsx("input", { type: "text", style: { flex: 1, backgroundColor: 'rgba(0, 0, 0, 0.4)', border: '1px solid rgba(34, 211, 238, 0.3)', padding: '2px 4px', fontSize: '10px', color: 'rgba(165, 243, 252, 1)', fontFamily: 'monospace', outline: 'none' }, value: props.color, onChange: e => onUpdate(Object.assign(Object.assign({}, component.properties), { 'color': e.target.value })) })] })] }), _jsxs("div", { children: [_jsx("label", { style: { display: 'block', fontSize: '9px', color: 'rgba(34, 211, 238, 0.6)', textTransform: 'uppercase', letterSpacing: '0.05em', marginBottom: 2 }, children: "Intensity" }), _jsx("input", { type: "number", step: "0.1", style: { width: '100%', backgroundColor: 'rgba(0, 0, 0, 0.4)', border: '1px solid rgba(34, 211, 238, 0.3)', padding: '2px 4px', fontSize: '10px', color: 'rgba(165, 243, 252, 1)', fontFamily: 'monospace', outline: 'none' }, value: props.intensity, onChange: e => onUpdate(Object.assign(Object.assign({}, component.properties), { 'intensity': parseFloat(e.target.value) })) })] }), _jsxs("div", { children: [_jsx("label", { style: { display: 'block', fontSize: '9px', color: 'rgba(34, 211, 238, 0.6)', textTransform: 'uppercase', letterSpacing: '0.05em', marginBottom: 2 }, children: "Cast Shadow" }), _jsx("input", { type: "checkbox", style: { height: 16, width: 16, backgroundColor: 'rgba(0, 0, 0, 0.4)', border: '1px solid rgba(34, 211, 238, 0.3)', cursor: 'pointer' }, checked: props.castShadow, onChange: e => onUpdate(Object.assign(Object.assign({}, component.properties), { 'castShadow': e.target.checked })) })] }), _jsxs("div", { children: [_jsx("label", { style: { display: 'block', fontSize: '9px', color: 'rgba(34, 211, 238, 0.6)', textTransform: 'uppercase', letterSpacing: '0.05em', marginBottom: 2 }, children: "Shadow Map Size" }), _jsx("input", { type: "number", step: "256", style: { width: '100%', backgroundColor: 'rgba(0, 0, 0, 0.4)', border: '1px solid rgba(34, 211, 238, 0.3)', padding: '2px 4px', fontSize: '10px', color: 'rgba(165, 243, 252, 1)', fontFamily: 'monospace', outline: 'none' }, value: props.shadowMapSize, onChange: e => onUpdate(Object.assign(Object.assign({}, component.properties), { 'shadowMapSize': parseFloat(e.target.value) })) })] }), _jsxs("div", { style: { borderTop: '1px solid rgba(34, 211, 238, 0.2)', paddingTop: 8, marginTop: 8 }, children: [_jsx("label", { style: { display: 'block', fontSize: '9px', color: 'rgba(34, 211, 238, 0.6)', textTransform: 'uppercase', letterSpacing: '0.05em', marginBottom: 4 }, children: "Shadow Camera" }), _jsxs("div", { style: { display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 4 }, children: [_jsxs("div", { children: [_jsx("label", { style: { display: 'block', fontSize: '8px', color: 'rgba(34, 211, 238, 0.5)', marginBottom: 2 }, children: "Near" }), _jsx("input", { type: "number", step: "0.1", style: { width: '100%', backgroundColor: 'rgba(0, 0, 0, 0.4)', border: '1px solid rgba(34, 211, 238, 0.3)', padding: '2px 4px', fontSize: '10px', color: 'rgba(165, 243, 252, 1)', fontFamily: 'monospace', outline: 'none' }, value: props.shadowCameraNear, onChange: e => onUpdate(Object.assign(Object.assign({}, component.properties), { 'shadowCameraNear': parseFloat(e.target.value) })) })] }), _jsxs("div", { children: [_jsx("label", { style: { display: 'block', fontSize: '8px', color: 'rgba(34, 211, 238, 0.5)', marginBottom: 2 }, children: "Far" }), _jsx("input", { type: "number", step: "1", style: { width: '100%', backgroundColor: 'rgba(0, 0, 0, 0.4)', border: '1px solid rgba(34, 211, 238, 0.3)', padding: '2px 4px', fontSize: '10px', color: 'rgba(165, 243, 252, 1)', fontFamily: 'monospace', outline: 'none' }, value: props.shadowCameraFar, onChange: e => onUpdate(Object.assign(Object.assign({}, component.properties), { 'shadowCameraFar': parseFloat(e.target.value) })) })] }), _jsxs("div", { children: [_jsx("label", { style: { display: 'block', fontSize: '8px', color: 'rgba(34, 211, 238, 0.5)', marginBottom: 2 }, children: "Top" }), _jsx("input", { type: "number", step: "1", style: { width: '100%', backgroundColor: 'rgba(0, 0, 0, 0.4)', border: '1px solid rgba(34, 211, 238, 0.3)', padding: '2px 4px', fontSize: '10px', color: 'rgba(165, 243, 252, 1)', fontFamily: 'monospace', outline: 'none' }, value: props.shadowCameraTop, onChange: e => onUpdate(Object.assign(Object.assign({}, component.properties), { 'shadowCameraTop': parseFloat(e.target.value) })) })] }), _jsxs("div", { children: [_jsx("label", { style: { display: 'block', fontSize: '8px', color: 'rgba(34, 211, 238, 0.5)', marginBottom: 2 }, children: "Bottom" }), _jsx("input", { type: "number", step: "1", style: { width: '100%', backgroundColor: 'rgba(0, 0, 0, 0.4)', border: '1px solid rgba(34, 211, 238, 0.3)', padding: '2px 4px', fontSize: '10px', color: 'rgba(165, 243, 252, 1)', fontFamily: 'monospace', outline: 'none' }, value: props.shadowCameraBottom, onChange: e => onUpdate(Object.assign(Object.assign({}, component.properties), { 'shadowCameraBottom': parseFloat(e.target.value) })) })] }), _jsxs("div", { children: [_jsx("label", { style: { display: 'block', fontSize: '8px', color: 'rgba(34, 211, 238, 0.5)', marginBottom: 2 }, children: "Left" }), _jsx("input", { type: "number", step: "1", style: { width: '100%', backgroundColor: 'rgba(0, 0, 0, 0.4)', border: '1px solid rgba(34, 211, 238, 0.3)', padding: '2px 4px', fontSize: '10px', color: 'rgba(165, 243, 252, 1)', fontFamily: 'monospace', outline: 'none' }, value: props.shadowCameraLeft, onChange: e => onUpdate(Object.assign(Object.assign({}, component.properties), { 'shadowCameraLeft': parseFloat(e.target.value) })) })] }), _jsxs("div", { children: [_jsx("label", { style: { display: 'block', fontSize: '8px', color: 'rgba(34, 211, 238, 0.5)', marginBottom: 2 }, children: "Right" }), _jsx("input", { type: "number", step: "1", style: { width: '100%', backgroundColor: 'rgba(0, 0, 0, 0.4)', border: '1px solid rgba(34, 211, 238, 0.3)', padding: '2px 4px', fontSize: '10px', color: 'rgba(165, 243, 252, 1)', fontFamily: 'monospace', outline: 'none' }, value: props.shadowCameraRight, onChange: e => onUpdate(Object.assign(Object.assign({}, component.properties), { 'shadowCameraRight': parseFloat(e.target.value) })) })] })] })] }), _jsxs("div", { style: { borderTop: '1px solid rgba(34, 211, 238, 0.2)', paddingTop: 8, marginTop: 8 }, children: [_jsx("label", { style: { display: 'block', fontSize: '9px', color: 'rgba(34, 211, 238, 0.6)', textTransform: 'uppercase', letterSpacing: '0.05em', marginBottom: 4 }, children: "Target Offset" }), _jsxs("div", { style: { display: 'grid', gridTemplateColumns: '1fr 1fr 1fr', gap: 4 }, children: [_jsxs("div", { children: [_jsx("label", { style: { display: 'block', fontSize: '8px', color: 'rgba(34, 211, 238, 0.5)', marginBottom: 2 }, children: "X" }), _jsx("input", { type: "number", step: "0.5", style: { width: '100%', backgroundColor: 'rgba(0, 0, 0, 0.4)', border: '1px solid rgba(34, 211, 238, 0.3)', padding: '2px 4px', fontSize: '10px', color: 'rgba(165, 243, 252, 1)', fontFamily: 'monospace', outline: 'none' }, value: props.targetOffset[0], onChange: e => onUpdate(Object.assign(Object.assign({}, component.properties), { 'targetOffset': [parseFloat(e.target.value), props.targetOffset[1], props.targetOffset[2]] })) })] }), _jsxs("div", { children: [_jsx("label", { style: { display: 'block', fontSize: '8px', color: 'rgba(34, 211, 238, 0.5)', marginBottom: 2 }, children: "Y" }), _jsx("input", { type: "number", step: "0.5", style: { width: '100%', backgroundColor: 'rgba(0, 0, 0, 0.4)', border: '1px solid rgba(34, 211, 238, 0.3)', padding: '2px 4px', fontSize: '10px', color: 'rgba(165, 243, 252, 1)', fontFamily: 'monospace', outline: 'none' }, value: props.targetOffset[1], onChange: e => onUpdate(Object.assign(Object.assign({}, component.properties), { 'targetOffset': [props.targetOffset[0], parseFloat(e.target.value), props.targetOffset[2]] })) })] }), _jsxs("div", { children: [_jsx("label", { style: { display: 'block', fontSize: '8px', color: 'rgba(34, 211, 238, 0.5)', marginBottom: 2 }, children: "Z" }), _jsx("input", { type: "number", step: "0.5", style: { width: '100%', backgroundColor: 'rgba(0, 0, 0, 0.4)', border: '1px solid rgba(34, 211, 238, 0.3)', padding: '2px 4px', fontSize: '10px', color: 'rgba(165, 243, 252, 1)', fontFamily: 'monospace', outline: 'none' }, value: props.targetOffset[2], onChange: e => onUpdate(Object.assign(Object.assign({}, component.properties), { 'targetOffset': [props.targetOffset[0], props.targetOffset[1], parseFloat(e.target.value)] })) })] })] })] })] });
|
|
21
21
|
}
|
|
22
22
|
function DirectionalLightView({ properties, editMode }) {
|
|
23
23
|
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
function GeometryComponentEditor({ component, onUpdate }) {
|
|
3
|
-
return _jsxs("div", { children: [_jsx("label", {
|
|
3
|
+
return _jsxs("div", { children: [_jsx("label", { style: { display: 'block', fontSize: '9px', color: 'rgba(34, 211, 238, 0.6)', textTransform: 'uppercase', letterSpacing: '0.05em', marginBottom: 2 }, children: "Type" }), _jsxs("select", { style: { width: '100%', backgroundColor: 'rgba(0, 0, 0, 0.4)', border: '1px solid rgba(34, 211, 238, 0.3)', padding: '2px 4px', fontSize: '10px', color: 'rgba(165, 243, 252, 1)', fontFamily: 'monospace', outline: 'none' }, value: component.properties.geometryType, onChange: e => onUpdate({ geometryType: e.target.value }), children: [_jsx("option", { value: "box", children: "Box" }), _jsx("option", { value: "sphere", children: "Sphere" }), _jsx("option", { value: "plane", children: "Plane" })] })] });
|
|
4
4
|
}
|
|
5
5
|
// View for Geometry component
|
|
6
6
|
function GeometryComponentView({ properties, children }) {
|
|
@@ -11,11 +11,11 @@ function MaterialComponentEditor({ component, onUpdate, basePath = "" }) {
|
|
|
11
11
|
.then(data => setTextureFiles(Array.isArray(data) ? data : data.files || []))
|
|
12
12
|
.catch(console.error);
|
|
13
13
|
}, [basePath]);
|
|
14
|
-
return (_jsxs("div", {
|
|
14
|
+
return (_jsxs("div", { style: { display: 'flex', flexDirection: 'column' }, children: [_jsxs("div", { style: { marginBottom: 4 }, children: [_jsx("label", { style: { display: 'block', fontSize: '9px', color: 'rgba(34, 211, 238, 0.6)', textTransform: 'uppercase', letterSpacing: '0.05em', marginBottom: 2 }, children: "Color" }), _jsxs("div", { style: { display: 'flex', gap: 2 }, children: [_jsx("input", { type: "color", style: { height: 20, width: 20, backgroundColor: 'transparent', border: 'none', cursor: 'pointer' }, value: component.properties.color, onChange: e => onUpdate({ 'color': e.target.value }) }), _jsx("input", { type: "text", style: { flex: 1, backgroundColor: 'rgba(0, 0, 0, 0.4)', border: '1px solid rgba(34, 211, 238, 0.3)', padding: '2px 4px', fontSize: '10px', color: 'rgba(165, 243, 252, 1)', fontFamily: 'monospace', outline: 'none' }, value: component.properties.color, onChange: e => onUpdate({ 'color': e.target.value }) })] })] }), _jsxs("div", { style: { display: 'flex', alignItems: 'center', gap: 4, marginBottom: 4 }, children: [_jsx("input", { type: "checkbox", style: { width: 12, height: 12 }, checked: component.properties.wireframe || false, onChange: e => onUpdate({ 'wireframe': e.target.checked }) }), _jsx("label", { style: { fontSize: '9px', color: 'rgba(34, 211, 238, 0.6)' }, children: "Wireframe" })] }), _jsxs("div", { children: [_jsx("label", { style: { display: 'block', fontSize: '9px', color: 'rgba(34, 211, 238, 0.6)', textTransform: 'uppercase', letterSpacing: '0.05em', marginBottom: 2 }, children: "Texture" }), _jsx("div", { style: { maxHeight: 128, overflowY: 'auto' }, children: _jsx(TextureListViewer, { files: textureFiles, selected: component.properties.texture || undefined, onSelect: (file) => onUpdate({ 'texture': file }), basePath: basePath }) })] }), component.properties.texture && (_jsxs("div", { style: { borderTop: '1px solid rgba(34, 211, 238, 0.2)', paddingTop: 4, marginTop: 4 }, children: [_jsxs("div", { style: { display: 'flex', alignItems: 'center', gap: 4, marginBottom: 4 }, children: [_jsx("input", { type: "checkbox", style: { width: 12, height: 12 }, checked: component.properties.repeat || false, onChange: e => onUpdate({ 'repeat': e.target.checked }) }), _jsx("label", { style: { fontSize: '9px', color: 'rgba(34, 211, 238, 0.6)' }, children: "Repeat Texture" })] }), component.properties.repeat && (_jsxs("div", { children: [_jsx("label", { style: { display: 'block', fontSize: '9px', color: 'rgba(34, 211, 238, 0.6)', textTransform: 'uppercase', letterSpacing: '0.05em', marginBottom: 2 }, children: "Repeat (X, Y)" }), _jsxs("div", { style: { display: 'flex', gap: 2 }, children: [_jsx("input", { type: "number", style: { width: '100%', backgroundColor: 'rgba(0, 0, 0, 0.4)', border: '1px solid rgba(34, 211, 238, 0.3)', padding: '2px 4px', fontSize: '10px', color: 'rgba(165, 243, 252, 1)', fontFamily: 'monospace', outline: 'none' }, value: (_b = (_a = component.properties.repeatCount) === null || _a === void 0 ? void 0 : _a[0]) !== null && _b !== void 0 ? _b : 1, onChange: e => {
|
|
15
15
|
var _a, _b;
|
|
16
16
|
const y = (_b = (_a = component.properties.repeatCount) === null || _a === void 0 ? void 0 : _a[1]) !== null && _b !== void 0 ? _b : 1;
|
|
17
17
|
onUpdate({ 'repeatCount': [parseFloat(e.target.value), y] });
|
|
18
|
-
} }), _jsx("input", { type: "number",
|
|
18
|
+
} }), _jsx("input", { type: "number", style: { width: '100%', backgroundColor: 'rgba(0, 0, 0, 0.4)', border: '1px solid rgba(34, 211, 238, 0.3)', padding: '2px 4px', fontSize: '10px', color: 'rgba(165, 243, 252, 1)', fontFamily: 'monospace', outline: 'none' }, value: (_d = (_c = component.properties.repeatCount) === null || _c === void 0 ? void 0 : _c[1]) !== null && _d !== void 0 ? _d : 1, onChange: e => {
|
|
19
19
|
var _a, _b;
|
|
20
20
|
const x = (_b = (_a = component.properties.repeatCount) === null || _a === void 0 ? void 0 : _a[0]) !== null && _b !== void 0 ? _b : 1;
|
|
21
21
|
onUpdate({ 'repeatCount': [x, parseFloat(e.target.value)] });
|
|
@@ -15,7 +15,7 @@ function ModelComponentEditor({ component, onUpdate, basePath = "" }) {
|
|
|
15
15
|
const filename = file.startsWith('/') ? file.slice(1) : file;
|
|
16
16
|
onUpdate({ 'filename': filename });
|
|
17
17
|
};
|
|
18
|
-
return _jsxs("div", { children: [_jsxs("div", {
|
|
18
|
+
return _jsxs("div", { children: [_jsxs("div", { style: { marginBottom: 4 }, children: [_jsx("label", { style: { display: 'block', fontSize: '9px', color: 'rgba(34, 211, 238, 0.6)', textTransform: 'uppercase', letterSpacing: '0.05em', marginBottom: 2 }, children: "Model" }), _jsx("div", { style: { maxHeight: 128, overflowY: 'auto' }, children: _jsx(ModelListViewer, { files: modelFiles, selected: component.properties.filename ? `/${component.properties.filename}` : undefined, onSelect: handleModelSelect, basePath: basePath }) })] }), _jsxs("div", { style: { display: 'flex', alignItems: 'center', gap: 4 }, children: [_jsx("input", { type: "checkbox", id: "instanced-checkbox", checked: component.properties.instanced || false, onChange: e => onUpdate({ 'instanced': e.target.checked }), style: { width: 12, height: 12 } }), _jsx("label", { htmlFor: "instanced-checkbox", style: { fontSize: '9px', color: 'rgba(34, 211, 238, 0.6)' }, children: "Instanced" })] })] });
|
|
19
19
|
}
|
|
20
20
|
// View for Model component
|
|
21
21
|
function ModelComponentView({ properties, loadedModels, children }) {
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
2
|
import { RigidBody } from "@react-three/rapier";
|
|
3
|
-
const selectClass =
|
|
4
|
-
const labelClass =
|
|
3
|
+
const selectClass = { width: '100%', backgroundColor: 'rgba(0, 0, 0, 0.4)', border: '1px solid rgba(34, 211, 238, 0.3)', padding: '2px 4px', fontSize: '10px', color: 'rgba(165, 243, 252, 1)', fontFamily: 'monospace', outline: 'none' };
|
|
4
|
+
const labelClass = { display: 'block', fontSize: '9px', color: 'rgba(34, 211, 238, 0.6)', textTransform: 'uppercase', letterSpacing: '0.05em', marginBottom: 2 };
|
|
5
5
|
function PhysicsComponentEditor({ component, onUpdate }) {
|
|
6
6
|
const { type = 'dynamic', collider = 'hull' } = component.properties;
|
|
7
|
-
return (_jsxs("div", { children: [_jsx("label", {
|
|
7
|
+
return (_jsxs("div", { children: [_jsx("label", { style: labelClass, children: "Type" }), _jsxs("select", { style: selectClass, value: type, onChange: e => onUpdate({ type: e.target.value }), children: [_jsx("option", { value: "dynamic", children: "Dynamic" }), _jsx("option", { value: "fixed", children: "Fixed" })] }), _jsx("label", { style: Object.assign(Object.assign({}, labelClass), { marginTop: 8 }), children: "Collider" }), _jsxs("select", { style: selectClass, value: collider, onChange: e => onUpdate({ collider: e.target.value }), children: [_jsx("option", { value: "hull", children: "Hull (convex)" }), _jsx("option", { value: "trimesh", children: "Trimesh (exact)" }), _jsx("option", { value: "cuboid", children: "Cuboid (box)" }), _jsx("option", { value: "ball", children: "Ball (sphere)" })] })] }));
|
|
8
8
|
}
|
|
9
9
|
function PhysicsComponentView({ properties, editMode, children }) {
|
|
10
10
|
if (editMode)
|
|
@@ -10,7 +10,7 @@ function SpotLightComponentEditor({ component, onUpdate }) {
|
|
|
10
10
|
distance: (_e = component.properties.distance) !== null && _e !== void 0 ? _e : 100,
|
|
11
11
|
castShadow: (_f = component.properties.castShadow) !== null && _f !== void 0 ? _f : true
|
|
12
12
|
};
|
|
13
|
-
return _jsxs("div", {
|
|
13
|
+
return _jsxs("div", { style: { display: 'flex', flexDirection: 'column', gap: 8 }, children: [_jsxs("div", { children: [_jsx("label", { style: { display: 'block', fontSize: '9px', color: 'rgba(34, 211, 238, 0.6)', textTransform: 'uppercase', letterSpacing: '0.05em', marginBottom: 2 }, children: "Color" }), _jsxs("div", { style: { display: 'flex', gap: 2 }, children: [_jsx("input", { type: "color", style: { height: 20, width: 20, backgroundColor: 'transparent', border: 'none', cursor: 'pointer' }, value: props.color, onChange: e => onUpdate(Object.assign(Object.assign({}, component.properties), { 'color': e.target.value })) }), _jsx("input", { type: "text", style: { flex: 1, backgroundColor: 'rgba(0, 0, 0, 0.4)', border: '1px solid rgba(34, 211, 238, 0.3)', padding: '2px 4px', fontSize: '10px', color: 'rgba(165, 243, 252, 1)', fontFamily: 'monospace', outline: 'none' }, value: props.color, onChange: e => onUpdate(Object.assign(Object.assign({}, component.properties), { 'color': e.target.value })) })] })] }), _jsxs("div", { children: [_jsx("label", { style: { display: 'block', fontSize: '9px', color: 'rgba(34, 211, 238, 0.6)', textTransform: 'uppercase', letterSpacing: '0.05em', marginBottom: 2 }, children: "Intensity" }), _jsx("input", { type: "number", step: "0.1", style: { width: '100%', backgroundColor: 'rgba(0, 0, 0, 0.4)', border: '1px solid rgba(34, 211, 238, 0.3)', padding: '2px 4px', fontSize: '10px', color: 'rgba(165, 243, 252, 1)', fontFamily: 'monospace', outline: 'none' }, value: props.intensity, onChange: e => onUpdate(Object.assign(Object.assign({}, component.properties), { 'intensity': parseFloat(e.target.value) })) })] }), _jsxs("div", { children: [_jsx("label", { style: { display: 'block', fontSize: '9px', color: 'rgba(34, 211, 238, 0.6)', textTransform: 'uppercase', letterSpacing: '0.05em', marginBottom: 2 }, children: "Angle" }), _jsx("input", { type: "number", step: "0.1", min: "0", max: Math.PI, style: { width: '100%', backgroundColor: 'rgba(0, 0, 0, 0.4)', border: '1px solid rgba(34, 211, 238, 0.3)', padding: '2px 4px', fontSize: '10px', color: 'rgba(165, 243, 252, 1)', fontFamily: 'monospace', outline: 'none' }, value: props.angle, onChange: e => onUpdate(Object.assign(Object.assign({}, component.properties), { 'angle': parseFloat(e.target.value) })) })] }), _jsxs("div", { children: [_jsx("label", { style: { display: 'block', fontSize: '9px', color: 'rgba(34, 211, 238, 0.6)', textTransform: 'uppercase', letterSpacing: '0.05em', marginBottom: 2 }, children: "Penumbra" }), _jsx("input", { type: "number", step: "0.1", min: "0", max: "1", style: { width: '100%', backgroundColor: 'rgba(0, 0, 0, 0.4)', border: '1px solid rgba(34, 211, 238, 0.3)', padding: '2px 4px', fontSize: '10px', color: 'rgba(165, 243, 252, 1)', fontFamily: 'monospace', outline: 'none' }, value: props.penumbra, onChange: e => onUpdate(Object.assign(Object.assign({}, component.properties), { 'penumbra': parseFloat(e.target.value) })) })] }), _jsxs("div", { children: [_jsx("label", { style: { display: 'block', fontSize: '9px', color: 'rgba(34, 211, 238, 0.6)', textTransform: 'uppercase', letterSpacing: '0.05em', marginBottom: 2 }, children: "Distance" }), _jsx("input", { type: "number", step: "1", min: "0", style: { width: '100%', backgroundColor: 'rgba(0, 0, 0, 0.4)', border: '1px solid rgba(34, 211, 238, 0.3)', padding: '2px 4px', fontSize: '10px', color: 'rgba(165, 243, 252, 1)', fontFamily: 'monospace', outline: 'none' }, value: props.distance, onChange: e => onUpdate(Object.assign(Object.assign({}, component.properties), { 'distance': parseFloat(e.target.value) })) })] }), _jsxs("div", { children: [_jsx("label", { style: { display: 'block', fontSize: '9px', color: 'rgba(34, 211, 238, 0.6)', textTransform: 'uppercase', letterSpacing: '0.05em', marginBottom: 2 }, children: "Cast Shadow" }), _jsx("input", { type: "checkbox", style: { height: 16, width: 16, backgroundColor: 'rgba(0, 0, 0, 0.4)', border: '1px solid rgba(34, 211, 238, 0.3)', cursor: 'pointer' }, checked: props.castShadow, onChange: e => onUpdate(Object.assign(Object.assign({}, component.properties), { 'castShadow': e.target.checked })) })] })] });
|
|
14
14
|
}
|
|
15
15
|
function SpotLightView({ properties, editMode }) {
|
|
16
16
|
var _a, _b, _c, _d, _e, _f;
|
|
@@ -15,7 +15,7 @@ function TransformComponentEditor({ component, onUpdate, transformMode, setTrans
|
|
|
15
15
|
background: 'rgba(255,255,255,0.10)',
|
|
16
16
|
},
|
|
17
17
|
};
|
|
18
|
-
return _jsxs("div", {
|
|
18
|
+
return _jsxs("div", { style: { display: 'flex', flexDirection: 'column' }, children: [transformMode && setTransformMode && (_jsxs("div", { style: { marginBottom: 8 }, children: [_jsx("label", { style: { display: 'block', fontSize: '9px', color: 'rgba(34, 211, 238, 0.6)', textTransform: 'uppercase', letterSpacing: '0.05em', marginBottom: 4 }, children: "Transform Mode" }), _jsx("div", { style: { display: 'flex', gap: 6 }, children: ["translate", "rotate", "scale"].map(mode => (_jsx("button", { onClick: () => setTransformMode(mode), style: Object.assign(Object.assign(Object.assign({}, s.button), { flex: 1 }), (transformMode === mode ? s.buttonActive : {})), onPointerEnter: (e) => {
|
|
19
19
|
if (transformMode !== mode)
|
|
20
20
|
e.currentTarget.style.background = 'rgba(255,255,255,0.08)';
|
|
21
21
|
}, onPointerLeave: (e) => {
|
|
@@ -90,7 +90,7 @@ export function Vector3Input({ label, value, onChange }) {
|
|
|
90
90
|
{ key: "y", color: "green", index: 1 },
|
|
91
91
|
{ key: "z", color: "blue", index: 2 }
|
|
92
92
|
];
|
|
93
|
-
return (_jsxs("div", {
|
|
93
|
+
return (_jsxs("div", { style: { marginBottom: 8 }, children: [_jsx("label", { style: { display: 'block', fontSize: '9px', color: 'rgba(34, 211, 238, 0.6)', textTransform: 'uppercase', letterSpacing: '0.05em', marginBottom: 4 }, children: label }), _jsx("div", { style: { display: 'flex', gap: 4 }, children: axes.map(({ key, color, index }) => (_jsxs("div", { style: { flex: 1, display: 'flex', alignItems: 'center', gap: 4, backgroundColor: 'rgba(0, 0, 0, 0.3)', border: '1px solid rgba(34, 211, 238, 0.2)', borderRadius: 4, padding: '4px 6px', minHeight: 32 }, children: [_jsx("span", { style: { fontSize: '12px', fontWeight: 'bold', color: color === 'red' ? 'rgba(248, 113, 113, 1)' : color === 'green' ? 'rgba(134, 239, 172, 1)' : 'rgba(96, 165, 250, 1)', width: 12, cursor: 'ew-resize', userSelect: 'none' }, onPointerDown: e => startScrub(e, index), onPointerMove: onScrubMove, onPointerUp: endScrub, children: key.toUpperCase() }), _jsx("input", { style: { flex: 1, backgroundColor: 'transparent', fontSize: '12px', color: 'rgba(165, 243, 252, 1)', fontFamily: 'monospace', outline: 'none', width: '100%', minWidth: 0 }, type: "text", value: draft[index], onChange: e => {
|
|
94
94
|
const next = [...draft];
|
|
95
95
|
next[index] = e.target.value;
|
|
96
96
|
setDraft(next);
|
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>
|
|
@@ -18,127 +18,127 @@ function DirectionalLightComponentEditor({ component, onUpdate }: { component: a
|
|
|
18
18
|
targetOffset: component.properties.targetOffset ?? [0, -5, 0]
|
|
19
19
|
};
|
|
20
20
|
|
|
21
|
-
return <div
|
|
21
|
+
return <div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
|
|
22
22
|
<div>
|
|
23
|
-
<label
|
|
24
|
-
<div
|
|
23
|
+
<label style={{ display: 'block', fontSize: '9px', color: 'rgba(34, 211, 238, 0.6)', textTransform: 'uppercase', letterSpacing: '0.05em', marginBottom: 2 }}>Color</label>
|
|
24
|
+
<div style={{ display: 'flex', gap: 2 }}>
|
|
25
25
|
<input
|
|
26
26
|
type="color"
|
|
27
|
-
|
|
27
|
+
style={{ height: 20, width: 20, backgroundColor: 'transparent', border: 'none', cursor: 'pointer' }}
|
|
28
28
|
value={props.color}
|
|
29
29
|
onChange={e => onUpdate({ ...component.properties, 'color': e.target.value })}
|
|
30
30
|
/>
|
|
31
31
|
<input
|
|
32
32
|
type="text"
|
|
33
|
-
|
|
33
|
+
style={{ flex: 1, backgroundColor: 'rgba(0, 0, 0, 0.4)', border: '1px solid rgba(34, 211, 238, 0.3)', padding: '2px 4px', fontSize: '10px', color: 'rgba(165, 243, 252, 1)', fontFamily: 'monospace', outline: 'none' }}
|
|
34
34
|
value={props.color}
|
|
35
35
|
onChange={e => onUpdate({ ...component.properties, 'color': e.target.value })}
|
|
36
36
|
/>
|
|
37
37
|
</div>
|
|
38
38
|
</div>
|
|
39
39
|
<div>
|
|
40
|
-
<label
|
|
40
|
+
<label style={{ display: 'block', fontSize: '9px', color: 'rgba(34, 211, 238, 0.6)', textTransform: 'uppercase', letterSpacing: '0.05em', marginBottom: 2 }}>Intensity</label>
|
|
41
41
|
<input
|
|
42
42
|
type="number"
|
|
43
43
|
step="0.1"
|
|
44
|
-
|
|
44
|
+
style={{ width: '100%', backgroundColor: 'rgba(0, 0, 0, 0.4)', border: '1px solid rgba(34, 211, 238, 0.3)', padding: '2px 4px', fontSize: '10px', color: 'rgba(165, 243, 252, 1)', fontFamily: 'monospace', outline: 'none' }}
|
|
45
45
|
value={props.intensity}
|
|
46
46
|
onChange={e => onUpdate({ ...component.properties, 'intensity': parseFloat(e.target.value) })}
|
|
47
47
|
/>
|
|
48
48
|
</div>
|
|
49
49
|
<div>
|
|
50
|
-
<label
|
|
50
|
+
<label style={{ display: 'block', fontSize: '9px', color: 'rgba(34, 211, 238, 0.6)', textTransform: 'uppercase', letterSpacing: '0.05em', marginBottom: 2 }}>Cast Shadow</label>
|
|
51
51
|
<input
|
|
52
52
|
type="checkbox"
|
|
53
|
-
|
|
53
|
+
style={{ height: 16, width: 16, backgroundColor: 'rgba(0, 0, 0, 0.4)', border: '1px solid rgba(34, 211, 238, 0.3)', cursor: 'pointer' }}
|
|
54
54
|
checked={props.castShadow}
|
|
55
55
|
onChange={e => onUpdate({ ...component.properties, 'castShadow': e.target.checked })}
|
|
56
56
|
/>
|
|
57
57
|
</div>
|
|
58
58
|
<div>
|
|
59
|
-
<label
|
|
59
|
+
<label style={{ display: 'block', fontSize: '9px', color: 'rgba(34, 211, 238, 0.6)', textTransform: 'uppercase', letterSpacing: '0.05em', marginBottom: 2 }}>Shadow Map Size</label>
|
|
60
60
|
<input
|
|
61
61
|
type="number"
|
|
62
62
|
step="256"
|
|
63
|
-
|
|
63
|
+
style={{ width: '100%', backgroundColor: 'rgba(0, 0, 0, 0.4)', border: '1px solid rgba(34, 211, 238, 0.3)', padding: '2px 4px', fontSize: '10px', color: 'rgba(165, 243, 252, 1)', fontFamily: 'monospace', outline: 'none' }}
|
|
64
64
|
value={props.shadowMapSize}
|
|
65
65
|
onChange={e => onUpdate({ ...component.properties, 'shadowMapSize': parseFloat(e.target.value) })}
|
|
66
66
|
/>
|
|
67
67
|
</div>
|
|
68
|
-
<div
|
|
69
|
-
<label
|
|
70
|
-
<div
|
|
68
|
+
<div style={{ borderTop: '1px solid rgba(34, 211, 238, 0.2)', paddingTop: 8, marginTop: 8 }}>
|
|
69
|
+
<label style={{ display: 'block', fontSize: '9px', color: 'rgba(34, 211, 238, 0.6)', textTransform: 'uppercase', letterSpacing: '0.05em', marginBottom: 4 }}>Shadow Camera</label>
|
|
70
|
+
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 4 }}>
|
|
71
71
|
<div>
|
|
72
|
-
<label
|
|
72
|
+
<label style={{ display: 'block', fontSize: '8px', color: 'rgba(34, 211, 238, 0.5)', marginBottom: 2 }}>Near</label>
|
|
73
73
|
<input
|
|
74
74
|
type="number"
|
|
75
75
|
step="0.1"
|
|
76
|
-
|
|
76
|
+
style={{ width: '100%', backgroundColor: 'rgba(0, 0, 0, 0.4)', border: '1px solid rgba(34, 211, 238, 0.3)', padding: '2px 4px', fontSize: '10px', color: 'rgba(165, 243, 252, 1)', fontFamily: 'monospace', outline: 'none' }}
|
|
77
77
|
value={props.shadowCameraNear}
|
|
78
78
|
onChange={e => onUpdate({ ...component.properties, 'shadowCameraNear': parseFloat(e.target.value) })}
|
|
79
79
|
/>
|
|
80
80
|
</div>
|
|
81
81
|
<div>
|
|
82
|
-
<label
|
|
82
|
+
<label style={{ display: 'block', fontSize: '8px', color: 'rgba(34, 211, 238, 0.5)', marginBottom: 2 }}>Far</label>
|
|
83
83
|
<input
|
|
84
84
|
type="number"
|
|
85
85
|
step="1"
|
|
86
|
-
|
|
86
|
+
style={{ width: '100%', backgroundColor: 'rgba(0, 0, 0, 0.4)', border: '1px solid rgba(34, 211, 238, 0.3)', padding: '2px 4px', fontSize: '10px', color: 'rgba(165, 243, 252, 1)', fontFamily: 'monospace', outline: 'none' }}
|
|
87
87
|
value={props.shadowCameraFar}
|
|
88
88
|
onChange={e => onUpdate({ ...component.properties, 'shadowCameraFar': parseFloat(e.target.value) })}
|
|
89
89
|
/>
|
|
90
90
|
</div>
|
|
91
91
|
<div>
|
|
92
|
-
<label
|
|
92
|
+
<label style={{ display: 'block', fontSize: '8px', color: 'rgba(34, 211, 238, 0.5)', marginBottom: 2 }}>Top</label>
|
|
93
93
|
<input
|
|
94
94
|
type="number"
|
|
95
95
|
step="1"
|
|
96
|
-
|
|
96
|
+
style={{ width: '100%', backgroundColor: 'rgba(0, 0, 0, 0.4)', border: '1px solid rgba(34, 211, 238, 0.3)', padding: '2px 4px', fontSize: '10px', color: 'rgba(165, 243, 252, 1)', fontFamily: 'monospace', outline: 'none' }}
|
|
97
97
|
value={props.shadowCameraTop}
|
|
98
98
|
onChange={e => onUpdate({ ...component.properties, 'shadowCameraTop': parseFloat(e.target.value) })}
|
|
99
99
|
/>
|
|
100
100
|
</div>
|
|
101
101
|
<div>
|
|
102
|
-
<label
|
|
102
|
+
<label style={{ display: 'block', fontSize: '8px', color: 'rgba(34, 211, 238, 0.5)', marginBottom: 2 }}>Bottom</label>
|
|
103
103
|
<input
|
|
104
104
|
type="number"
|
|
105
105
|
step="1"
|
|
106
|
-
|
|
106
|
+
style={{ width: '100%', backgroundColor: 'rgba(0, 0, 0, 0.4)', border: '1px solid rgba(34, 211, 238, 0.3)', padding: '2px 4px', fontSize: '10px', color: 'rgba(165, 243, 252, 1)', fontFamily: 'monospace', outline: 'none' }}
|
|
107
107
|
value={props.shadowCameraBottom}
|
|
108
108
|
onChange={e => onUpdate({ ...component.properties, 'shadowCameraBottom': parseFloat(e.target.value) })}
|
|
109
109
|
/>
|
|
110
110
|
</div>
|
|
111
111
|
<div>
|
|
112
|
-
<label
|
|
112
|
+
<label style={{ display: 'block', fontSize: '8px', color: 'rgba(34, 211, 238, 0.5)', marginBottom: 2 }}>Left</label>
|
|
113
113
|
<input
|
|
114
114
|
type="number"
|
|
115
115
|
step="1"
|
|
116
|
-
|
|
116
|
+
style={{ width: '100%', backgroundColor: 'rgba(0, 0, 0, 0.4)', border: '1px solid rgba(34, 211, 238, 0.3)', padding: '2px 4px', fontSize: '10px', color: 'rgba(165, 243, 252, 1)', fontFamily: 'monospace', outline: 'none' }}
|
|
117
117
|
value={props.shadowCameraLeft}
|
|
118
118
|
onChange={e => onUpdate({ ...component.properties, 'shadowCameraLeft': parseFloat(e.target.value) })}
|
|
119
119
|
/>
|
|
120
120
|
</div>
|
|
121
121
|
<div>
|
|
122
|
-
<label
|
|
122
|
+
<label style={{ display: 'block', fontSize: '8px', color: 'rgba(34, 211, 238, 0.5)', marginBottom: 2 }}>Right</label>
|
|
123
123
|
<input
|
|
124
124
|
type="number"
|
|
125
125
|
step="1"
|
|
126
|
-
|
|
126
|
+
style={{ width: '100%', backgroundColor: 'rgba(0, 0, 0, 0.4)', border: '1px solid rgba(34, 211, 238, 0.3)', padding: '2px 4px', fontSize: '10px', color: 'rgba(165, 243, 252, 1)', fontFamily: 'monospace', outline: 'none' }}
|
|
127
127
|
value={props.shadowCameraRight}
|
|
128
128
|
onChange={e => onUpdate({ ...component.properties, 'shadowCameraRight': parseFloat(e.target.value) })}
|
|
129
129
|
/>
|
|
130
130
|
</div>
|
|
131
131
|
</div>
|
|
132
132
|
</div>
|
|
133
|
-
<div
|
|
134
|
-
<label
|
|
135
|
-
<div
|
|
133
|
+
<div style={{ borderTop: '1px solid rgba(34, 211, 238, 0.2)', paddingTop: 8, marginTop: 8 }}>
|
|
134
|
+
<label style={{ display: 'block', fontSize: '9px', color: 'rgba(34, 211, 238, 0.6)', textTransform: 'uppercase', letterSpacing: '0.05em', marginBottom: 4 }}>Target Offset</label>
|
|
135
|
+
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr 1fr', gap: 4 }}>
|
|
136
136
|
<div>
|
|
137
|
-
<label
|
|
137
|
+
<label style={{ display: 'block', fontSize: '8px', color: 'rgba(34, 211, 238, 0.5)', marginBottom: 2 }}>X</label>
|
|
138
138
|
<input
|
|
139
139
|
type="number"
|
|
140
140
|
step="0.5"
|
|
141
|
-
|
|
141
|
+
style={{ width: '100%', backgroundColor: 'rgba(0, 0, 0, 0.4)', border: '1px solid rgba(34, 211, 238, 0.3)', padding: '2px 4px', fontSize: '10px', color: 'rgba(165, 243, 252, 1)', fontFamily: 'monospace', outline: 'none' }}
|
|
142
142
|
value={props.targetOffset[0]}
|
|
143
143
|
onChange={e => onUpdate({
|
|
144
144
|
...component.properties,
|
|
@@ -147,11 +147,11 @@ function DirectionalLightComponentEditor({ component, onUpdate }: { component: a
|
|
|
147
147
|
/>
|
|
148
148
|
</div>
|
|
149
149
|
<div>
|
|
150
|
-
<label
|
|
150
|
+
<label style={{ display: 'block', fontSize: '8px', color: 'rgba(34, 211, 238, 0.5)', marginBottom: 2 }}>Y</label>
|
|
151
151
|
<input
|
|
152
152
|
type="number"
|
|
153
153
|
step="0.5"
|
|
154
|
-
|
|
154
|
+
style={{ width: '100%', backgroundColor: 'rgba(0, 0, 0, 0.4)', border: '1px solid rgba(34, 211, 238, 0.3)', padding: '2px 4px', fontSize: '10px', color: 'rgba(165, 243, 252, 1)', fontFamily: 'monospace', outline: 'none' }}
|
|
155
155
|
value={props.targetOffset[1]}
|
|
156
156
|
onChange={e => onUpdate({
|
|
157
157
|
...component.properties,
|
|
@@ -160,11 +160,11 @@ function DirectionalLightComponentEditor({ component, onUpdate }: { component: a
|
|
|
160
160
|
/>
|
|
161
161
|
</div>
|
|
162
162
|
<div>
|
|
163
|
-
<label
|
|
163
|
+
<label style={{ display: 'block', fontSize: '8px', color: 'rgba(34, 211, 238, 0.5)', marginBottom: 2 }}>Z</label>
|
|
164
164
|
<input
|
|
165
165
|
type="number"
|
|
166
166
|
step="0.5"
|
|
167
|
-
|
|
167
|
+
style={{ width: '100%', backgroundColor: 'rgba(0, 0, 0, 0.4)', border: '1px solid rgba(34, 211, 238, 0.3)', padding: '2px 4px', fontSize: '10px', color: 'rgba(165, 243, 252, 1)', fontFamily: 'monospace', outline: 'none' }}
|
|
168
168
|
value={props.targetOffset[2]}
|
|
169
169
|
onChange={e => onUpdate({
|
|
170
170
|
...component.properties,
|
|
@@ -2,9 +2,9 @@ import { Component } from "./ComponentRegistry";
|
|
|
2
2
|
|
|
3
3
|
function GeometryComponentEditor({ component, onUpdate }: { component: any; onUpdate: (newComp: any) => void }) {
|
|
4
4
|
return <div>
|
|
5
|
-
<label
|
|
5
|
+
<label style={{ display: 'block', fontSize: '9px', color: 'rgba(34, 211, 238, 0.6)', textTransform: 'uppercase', letterSpacing: '0.05em', marginBottom: 2 }}>Type</label>
|
|
6
6
|
<select
|
|
7
|
-
|
|
7
|
+
style={{ width: '100%', backgroundColor: 'rgba(0, 0, 0, 0.4)', border: '1px solid rgba(34, 211, 238, 0.3)', padding: '2px 4px', fontSize: '10px', color: 'rgba(165, 243, 252, 1)', fontFamily: 'monospace', outline: 'none' }}
|
|
8
8
|
value={component.properties.geometryType}
|
|
9
9
|
onChange={e => onUpdate({ geometryType: e.target.value })}
|
|
10
10
|
>
|
|
@@ -14,37 +14,37 @@ function MaterialComponentEditor({ component, onUpdate, basePath = "" }: { compo
|
|
|
14
14
|
}, [basePath]);
|
|
15
15
|
|
|
16
16
|
return (
|
|
17
|
-
<div
|
|
18
|
-
<div
|
|
19
|
-
<label
|
|
20
|
-
<div
|
|
17
|
+
<div style={{ display: 'flex', flexDirection: 'column' }}>
|
|
18
|
+
<div style={{ marginBottom: 4 }}>
|
|
19
|
+
<label style={{ display: 'block', fontSize: '9px', color: 'rgba(34, 211, 238, 0.6)', textTransform: 'uppercase', letterSpacing: '0.05em', marginBottom: 2 }}>Color</label>
|
|
20
|
+
<div style={{ display: 'flex', gap: 2 }}>
|
|
21
21
|
<input
|
|
22
22
|
type="color"
|
|
23
|
-
|
|
23
|
+
style={{ height: 20, width: 20, backgroundColor: 'transparent', border: 'none', cursor: 'pointer' }}
|
|
24
24
|
value={component.properties.color}
|
|
25
25
|
onChange={e => onUpdate({ 'color': e.target.value })}
|
|
26
26
|
/>
|
|
27
27
|
<input
|
|
28
28
|
type="text"
|
|
29
|
-
|
|
29
|
+
style={{ flex: 1, backgroundColor: 'rgba(0, 0, 0, 0.4)', border: '1px solid rgba(34, 211, 238, 0.3)', padding: '2px 4px', fontSize: '10px', color: 'rgba(165, 243, 252, 1)', fontFamily: 'monospace', outline: 'none' }}
|
|
30
30
|
value={component.properties.color}
|
|
31
31
|
onChange={e => onUpdate({ 'color': e.target.value })}
|
|
32
32
|
/>
|
|
33
33
|
</div>
|
|
34
34
|
</div>
|
|
35
|
-
<div
|
|
35
|
+
<div style={{ display: 'flex', alignItems: 'center', gap: 4, marginBottom: 4 }}>
|
|
36
36
|
<input
|
|
37
37
|
type="checkbox"
|
|
38
|
-
|
|
38
|
+
style={{ width: 12, height: 12 }}
|
|
39
39
|
checked={component.properties.wireframe || false}
|
|
40
40
|
onChange={e => onUpdate({ 'wireframe': e.target.checked })}
|
|
41
41
|
/>
|
|
42
|
-
<label
|
|
42
|
+
<label style={{ fontSize: '9px', color: 'rgba(34, 211, 238, 0.6)' }}>Wireframe</label>
|
|
43
43
|
</div>
|
|
44
44
|
|
|
45
45
|
<div>
|
|
46
|
-
<label
|
|
47
|
-
<div
|
|
46
|
+
<label style={{ display: 'block', fontSize: '9px', color: 'rgba(34, 211, 238, 0.6)', textTransform: 'uppercase', letterSpacing: '0.05em', marginBottom: 2 }}>Texture</label>
|
|
47
|
+
<div style={{ maxHeight: 128, overflowY: 'auto' }}>
|
|
48
48
|
<TextureListViewer
|
|
49
49
|
files={textureFiles}
|
|
50
50
|
selected={component.properties.texture || undefined}
|
|
@@ -55,24 +55,24 @@ function MaterialComponentEditor({ component, onUpdate, basePath = "" }: { compo
|
|
|
55
55
|
</div>
|
|
56
56
|
|
|
57
57
|
{component.properties.texture && (
|
|
58
|
-
<div
|
|
59
|
-
<div
|
|
58
|
+
<div style={{ borderTop: '1px solid rgba(34, 211, 238, 0.2)', paddingTop: 4, marginTop: 4 }}>
|
|
59
|
+
<div style={{ display: 'flex', alignItems: 'center', gap: 4, marginBottom: 4 }}>
|
|
60
60
|
<input
|
|
61
61
|
type="checkbox"
|
|
62
|
-
|
|
62
|
+
style={{ width: 12, height: 12 }}
|
|
63
63
|
checked={component.properties.repeat || false}
|
|
64
64
|
onChange={e => onUpdate({ 'repeat': e.target.checked })}
|
|
65
65
|
/>
|
|
66
|
-
<label
|
|
66
|
+
<label style={{ fontSize: '9px', color: 'rgba(34, 211, 238, 0.6)' }}>Repeat Texture</label>
|
|
67
67
|
</div>
|
|
68
68
|
|
|
69
69
|
{component.properties.repeat && (
|
|
70
70
|
<div>
|
|
71
|
-
<label
|
|
72
|
-
<div
|
|
71
|
+
<label style={{ display: 'block', fontSize: '9px', color: 'rgba(34, 211, 238, 0.6)', textTransform: 'uppercase', letterSpacing: '0.05em', marginBottom: 2 }}>Repeat (X, Y)</label>
|
|
72
|
+
<div style={{ display: 'flex', gap: 2 }}>
|
|
73
73
|
<input
|
|
74
74
|
type="number"
|
|
75
|
-
|
|
75
|
+
style={{ width: '100%', backgroundColor: 'rgba(0, 0, 0, 0.4)', border: '1px solid rgba(34, 211, 238, 0.3)', padding: '2px 4px', fontSize: '10px', color: 'rgba(165, 243, 252, 1)', fontFamily: 'monospace', outline: 'none' }}
|
|
76
76
|
value={component.properties.repeatCount?.[0] ?? 1}
|
|
77
77
|
onChange={e => {
|
|
78
78
|
const y = component.properties.repeatCount?.[1] ?? 1;
|
|
@@ -81,7 +81,7 @@ function MaterialComponentEditor({ component, onUpdate, basePath = "" }: { compo
|
|
|
81
81
|
/>
|
|
82
82
|
<input
|
|
83
83
|
type="number"
|
|
84
|
-
|
|
84
|
+
style={{ width: '100%', backgroundColor: 'rgba(0, 0, 0, 0.4)', border: '1px solid rgba(34, 211, 238, 0.3)', padding: '2px 4px', fontSize: '10px', color: 'rgba(165, 243, 252, 1)', fontFamily: 'monospace', outline: 'none' }}
|
|
85
85
|
value={component.properties.repeatCount?.[1] ?? 1}
|
|
86
86
|
onChange={e => {
|
|
87
87
|
const x = component.properties.repeatCount?.[0] ?? 1;
|
|
@@ -20,9 +20,9 @@ function ModelComponentEditor({ component, onUpdate, basePath = "" }: { componen
|
|
|
20
20
|
};
|
|
21
21
|
|
|
22
22
|
return <div>
|
|
23
|
-
<div
|
|
24
|
-
<label
|
|
25
|
-
<div
|
|
23
|
+
<div style={{ marginBottom: 4 }}>
|
|
24
|
+
<label style={{ display: 'block', fontSize: '9px', color: 'rgba(34, 211, 238, 0.6)', textTransform: 'uppercase', letterSpacing: '0.05em', marginBottom: 2 }}>Model</label>
|
|
25
|
+
<div style={{ maxHeight: 128, overflowY: 'auto' }}>
|
|
26
26
|
<ModelListViewer
|
|
27
27
|
files={modelFiles}
|
|
28
28
|
selected={component.properties.filename ? `/${component.properties.filename}` : undefined}
|
|
@@ -31,15 +31,15 @@ function ModelComponentEditor({ component, onUpdate, basePath = "" }: { componen
|
|
|
31
31
|
/>
|
|
32
32
|
</div>
|
|
33
33
|
</div>
|
|
34
|
-
<div
|
|
34
|
+
<div style={{ display: 'flex', alignItems: 'center', gap: 4 }}>
|
|
35
35
|
<input
|
|
36
36
|
type="checkbox"
|
|
37
37
|
id="instanced-checkbox"
|
|
38
38
|
checked={component.properties.instanced || false}
|
|
39
39
|
onChange={e => onUpdate({ 'instanced': e.target.checked })}
|
|
40
|
-
|
|
40
|
+
style={{ width: 12, height: 12 }}
|
|
41
41
|
/>
|
|
42
|
-
<label htmlFor="instanced-checkbox"
|
|
42
|
+
<label htmlFor="instanced-checkbox" style={{ fontSize: '9px', color: 'rgba(34, 211, 238, 0.6)' }}>Instanced</label>
|
|
43
43
|
</div>
|
|
44
44
|
</div>;
|
|
45
45
|
}
|
|
@@ -2,21 +2,21 @@ import { RigidBody } from "@react-three/rapier";
|
|
|
2
2
|
import type { ReactNode } from 'react';
|
|
3
3
|
import { Component } from "./ComponentRegistry";
|
|
4
4
|
|
|
5
|
-
const selectClass =
|
|
6
|
-
const labelClass =
|
|
5
|
+
const selectClass = { width: '100%', backgroundColor: 'rgba(0, 0, 0, 0.4)', border: '1px solid rgba(34, 211, 238, 0.3)', padding: '2px 4px', fontSize: '10px', color: 'rgba(165, 243, 252, 1)', fontFamily: 'monospace', outline: 'none' };
|
|
6
|
+
const labelClass = { display: 'block', fontSize: '9px', color: 'rgba(34, 211, 238, 0.6)', textTransform: 'uppercase', letterSpacing: '0.05em', marginBottom: 2 };
|
|
7
7
|
|
|
8
8
|
function PhysicsComponentEditor({ component, onUpdate }: { component: { properties: { type?: 'dynamic' | 'fixed'; collider?: string;[k: string]: any } }; onUpdate: (props: Partial<Record<string, any>>) => void }) {
|
|
9
9
|
const { type = 'dynamic', collider = 'hull' } = component.properties;
|
|
10
10
|
return (
|
|
11
11
|
<div>
|
|
12
|
-
<label
|
|
13
|
-
<select
|
|
12
|
+
<label style={labelClass}>Type</label>
|
|
13
|
+
<select style={selectClass as any} value={type} onChange={e => onUpdate({ type: e.target.value })}>
|
|
14
14
|
<option value="dynamic">Dynamic</option>
|
|
15
15
|
<option value="fixed">Fixed</option>
|
|
16
16
|
</select>
|
|
17
17
|
|
|
18
|
-
<label
|
|
19
|
-
<select
|
|
18
|
+
<label style={{ ...labelClass, marginTop: 8 }}>Collider</label>
|
|
19
|
+
<select style={selectClass as any} value={collider} onChange={e => onUpdate({ collider: e.target.value })}>
|
|
20
20
|
<option value="hull">Hull (convex)</option>
|
|
21
21
|
<option value="trimesh">Trimesh (exact)</option>
|
|
22
22
|
<option value="cuboid">Cuboid (box)</option>
|
|
@@ -11,74 +11,74 @@ function SpotLightComponentEditor({ component, onUpdate }: { component: any; onU
|
|
|
11
11
|
castShadow: component.properties.castShadow ?? true
|
|
12
12
|
};
|
|
13
13
|
|
|
14
|
-
return <div
|
|
14
|
+
return <div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
|
|
15
15
|
<div>
|
|
16
|
-
<label
|
|
17
|
-
<div
|
|
16
|
+
<label style={{ display: 'block', fontSize: '9px', color: 'rgba(34, 211, 238, 0.6)', textTransform: 'uppercase', letterSpacing: '0.05em', marginBottom: 2 }}>Color</label>
|
|
17
|
+
<div style={{ display: 'flex', gap: 2 }}>
|
|
18
18
|
<input
|
|
19
19
|
type="color"
|
|
20
|
-
|
|
20
|
+
style={{ height: 20, width: 20, backgroundColor: 'transparent', border: 'none', cursor: 'pointer' }}
|
|
21
21
|
value={props.color}
|
|
22
22
|
onChange={e => onUpdate({ ...component.properties, 'color': e.target.value })}
|
|
23
23
|
/>
|
|
24
24
|
<input
|
|
25
25
|
type="text"
|
|
26
|
-
|
|
26
|
+
style={{ flex: 1, backgroundColor: 'rgba(0, 0, 0, 0.4)', border: '1px solid rgba(34, 211, 238, 0.3)', padding: '2px 4px', fontSize: '10px', color: 'rgba(165, 243, 252, 1)', fontFamily: 'monospace', outline: 'none' }}
|
|
27
27
|
value={props.color}
|
|
28
28
|
onChange={e => onUpdate({ ...component.properties, 'color': e.target.value })}
|
|
29
29
|
/>
|
|
30
30
|
</div>
|
|
31
31
|
</div>
|
|
32
32
|
<div>
|
|
33
|
-
<label
|
|
33
|
+
<label style={{ display: 'block', fontSize: '9px', color: 'rgba(34, 211, 238, 0.6)', textTransform: 'uppercase', letterSpacing: '0.05em', marginBottom: 2 }}>Intensity</label>
|
|
34
34
|
<input
|
|
35
35
|
type="number"
|
|
36
36
|
step="0.1"
|
|
37
|
-
|
|
37
|
+
style={{ width: '100%', backgroundColor: 'rgba(0, 0, 0, 0.4)', border: '1px solid rgba(34, 211, 238, 0.3)', padding: '2px 4px', fontSize: '10px', color: 'rgba(165, 243, 252, 1)', fontFamily: 'monospace', outline: 'none' }}
|
|
38
38
|
value={props.intensity}
|
|
39
39
|
onChange={e => onUpdate({ ...component.properties, 'intensity': parseFloat(e.target.value) })}
|
|
40
40
|
/>
|
|
41
41
|
</div>
|
|
42
42
|
<div>
|
|
43
|
-
<label
|
|
43
|
+
<label style={{ display: 'block', fontSize: '9px', color: 'rgba(34, 211, 238, 0.6)', textTransform: 'uppercase', letterSpacing: '0.05em', marginBottom: 2 }}>Angle</label>
|
|
44
44
|
<input
|
|
45
45
|
type="number"
|
|
46
46
|
step="0.1"
|
|
47
47
|
min="0"
|
|
48
48
|
max={Math.PI}
|
|
49
|
-
|
|
49
|
+
style={{ width: '100%', backgroundColor: 'rgba(0, 0, 0, 0.4)', border: '1px solid rgba(34, 211, 238, 0.3)', padding: '2px 4px', fontSize: '10px', color: 'rgba(165, 243, 252, 1)', fontFamily: 'monospace', outline: 'none' }}
|
|
50
50
|
value={props.angle}
|
|
51
51
|
onChange={e => onUpdate({ ...component.properties, 'angle': parseFloat(e.target.value) })}
|
|
52
52
|
/>
|
|
53
53
|
</div>
|
|
54
54
|
<div>
|
|
55
|
-
<label
|
|
55
|
+
<label style={{ display: 'block', fontSize: '9px', color: 'rgba(34, 211, 238, 0.6)', textTransform: 'uppercase', letterSpacing: '0.05em', marginBottom: 2 }}>Penumbra</label>
|
|
56
56
|
<input
|
|
57
57
|
type="number"
|
|
58
58
|
step="0.1"
|
|
59
59
|
min="0"
|
|
60
60
|
max="1"
|
|
61
|
-
|
|
61
|
+
style={{ width: '100%', backgroundColor: 'rgba(0, 0, 0, 0.4)', border: '1px solid rgba(34, 211, 238, 0.3)', padding: '2px 4px', fontSize: '10px', color: 'rgba(165, 243, 252, 1)', fontFamily: 'monospace', outline: 'none' }}
|
|
62
62
|
value={props.penumbra}
|
|
63
63
|
onChange={e => onUpdate({ ...component.properties, 'penumbra': parseFloat(e.target.value) })}
|
|
64
64
|
/>
|
|
65
65
|
</div>
|
|
66
66
|
<div>
|
|
67
|
-
<label
|
|
67
|
+
<label style={{ display: 'block', fontSize: '9px', color: 'rgba(34, 211, 238, 0.6)', textTransform: 'uppercase', letterSpacing: '0.05em', marginBottom: 2 }}>Distance</label>
|
|
68
68
|
<input
|
|
69
69
|
type="number"
|
|
70
70
|
step="1"
|
|
71
71
|
min="0"
|
|
72
|
-
|
|
72
|
+
style={{ width: '100%', backgroundColor: 'rgba(0, 0, 0, 0.4)', border: '1px solid rgba(34, 211, 238, 0.3)', padding: '2px 4px', fontSize: '10px', color: 'rgba(165, 243, 252, 1)', fontFamily: 'monospace', outline: 'none' }}
|
|
73
73
|
value={props.distance}
|
|
74
74
|
onChange={e => onUpdate({ ...component.properties, 'distance': parseFloat(e.target.value) })}
|
|
75
75
|
/>
|
|
76
76
|
</div>
|
|
77
77
|
<div>
|
|
78
|
-
<label
|
|
78
|
+
<label style={{ display: 'block', fontSize: '9px', color: 'rgba(34, 211, 238, 0.6)', textTransform: 'uppercase', letterSpacing: '0.05em', marginBottom: 2 }}>Cast Shadow</label>
|
|
79
79
|
<input
|
|
80
80
|
type="checkbox"
|
|
81
|
-
|
|
81
|
+
style={{ height: 16, width: 16, backgroundColor: 'rgba(0, 0, 0, 0.4)', border: '1px solid rgba(34, 211, 238, 0.3)', cursor: 'pointer' }}
|
|
82
82
|
checked={props.castShadow}
|
|
83
83
|
onChange={e => onUpdate({ ...component.properties, 'castShadow': e.target.checked })}
|
|
84
84
|
/>
|
|
@@ -22,10 +22,10 @@ function TransformComponentEditor({ component, onUpdate, transformMode, setTrans
|
|
|
22
22
|
},
|
|
23
23
|
};
|
|
24
24
|
|
|
25
|
-
return <div
|
|
25
|
+
return <div style={{ display: 'flex', flexDirection: 'column' }}>
|
|
26
26
|
{transformMode && setTransformMode && (
|
|
27
|
-
<div
|
|
28
|
-
<label
|
|
27
|
+
<div style={{ marginBottom: 8 }}>
|
|
28
|
+
<label style={{ display: 'block', fontSize: '9px', color: 'rgba(34, 211, 238, 0.6)', textTransform: 'uppercase', letterSpacing: '0.05em', marginBottom: 4 }}>Transform Mode</label>
|
|
29
29
|
<div style={{ display: 'flex', gap: 6 }}>
|
|
30
30
|
{["translate", "rotate", "scale"].map(mode => (
|
|
31
31
|
<button
|
|
@@ -151,20 +151,20 @@ export function Vector3Input({
|
|
|
151
151
|
] as const;
|
|
152
152
|
|
|
153
153
|
return (
|
|
154
|
-
<div
|
|
155
|
-
<label
|
|
154
|
+
<div style={{ marginBottom: 8 }}>
|
|
155
|
+
<label style={{ display: 'block', fontSize: '9px', color: 'rgba(34, 211, 238, 0.6)', textTransform: 'uppercase', letterSpacing: '0.05em', marginBottom: 4 }}>
|
|
156
156
|
{label}
|
|
157
157
|
</label>
|
|
158
158
|
|
|
159
|
-
<div
|
|
159
|
+
<div style={{ display: 'flex', gap: 4 }}>
|
|
160
160
|
{axes.map(({ key, color, index }) => (
|
|
161
161
|
<div
|
|
162
162
|
key={key}
|
|
163
|
-
|
|
163
|
+
style={{ flex: 1, display: 'flex', alignItems: 'center', gap: 4, backgroundColor: 'rgba(0, 0, 0, 0.3)', border: '1px solid rgba(34, 211, 238, 0.2)', borderRadius: 4, padding: '4px 6px', minHeight: 32 }}
|
|
164
164
|
>
|
|
165
165
|
{/* SCRUB HANDLE */}
|
|
166
166
|
<span
|
|
167
|
-
|
|
167
|
+
style={{ fontSize: '12px', fontWeight: 'bold', color: color === 'red' ? 'rgba(248, 113, 113, 1)' : color === 'green' ? 'rgba(134, 239, 172, 1)' : 'rgba(96, 165, 250, 1)', width: 12, cursor: 'ew-resize', userSelect: 'none' }}
|
|
168
168
|
onPointerDown={e => startScrub(e, index)}
|
|
169
169
|
onPointerMove={onScrubMove}
|
|
170
170
|
onPointerUp={endScrub}
|
|
@@ -174,7 +174,7 @@ export function Vector3Input({
|
|
|
174
174
|
|
|
175
175
|
{/* TEXT INPUT */}
|
|
176
176
|
<input
|
|
177
|
-
|
|
177
|
+
style={{ flex: 1, backgroundColor: 'transparent', fontSize: '12px', color: 'rgba(165, 243, 252, 1)', fontFamily: 'monospace', outline: 'none', width: '100%', minWidth: 0 }}
|
|
178
178
|
type="text"
|
|
179
179
|
value={draft[index]}
|
|
180
180
|
onChange={e => {
|