react-three-game 0.0.39 → 0.0.40
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/tools/assetviewer/page.d.ts +12 -0
- package/dist/tools/assetviewer/page.js +21 -13
- package/dist/tools/prefabeditor/components/MaterialComponent.js +6 -2
- package/dist/tools/prefabeditor/components/ModelComponent.js +6 -2
- package/dist/tools/prefabeditor/styles.js +0 -1
- package/package.json +1 -1
- package/src/tools/assetviewer/page.tsx +31 -24
- package/src/tools/prefabeditor/components/MaterialComponent.tsx +25 -9
- package/src/tools/prefabeditor/components/ModelComponent.tsx +25 -10
- package/src/tools/prefabeditor/styles.ts +0 -1
|
@@ -19,5 +19,17 @@ interface SoundListViewerProps {
|
|
|
19
19
|
basePath?: string;
|
|
20
20
|
}
|
|
21
21
|
export declare function SoundListViewer({ files, selected, onSelect, basePath }: SoundListViewerProps): import("react/jsx-runtime").JSX.Element;
|
|
22
|
+
export declare function SingleTextureViewer({ file, basePath }: {
|
|
23
|
+
file?: string;
|
|
24
|
+
basePath?: string;
|
|
25
|
+
}): import("react/jsx-runtime").JSX.Element | null;
|
|
26
|
+
export declare function SingleModelViewer({ file, basePath }: {
|
|
27
|
+
file?: string;
|
|
28
|
+
basePath?: string;
|
|
29
|
+
}): import("react/jsx-runtime").JSX.Element | null;
|
|
30
|
+
export declare function SingleSoundViewer({ file, basePath }: {
|
|
31
|
+
file?: string;
|
|
32
|
+
basePath?: string;
|
|
33
|
+
}): import("react/jsx-runtime").JSX.Element | null;
|
|
22
34
|
export declare function SharedCanvas(): import("react/jsx-runtime").JSX.Element;
|
|
23
35
|
export {};
|
|
@@ -37,6 +37,7 @@ function getItemsInPath(files, currentPath) {
|
|
|
37
37
|
}
|
|
38
38
|
function FolderTile({ name, onClick }) {
|
|
39
39
|
return (_jsxs("div", { onClick: onClick, style: {
|
|
40
|
+
maxWidth: 60,
|
|
40
41
|
aspectRatio: '1 / 1',
|
|
41
42
|
backgroundColor: '#1f2937', /* gray-800 */
|
|
42
43
|
cursor: 'pointer',
|
|
@@ -66,21 +67,12 @@ function useInView() {
|
|
|
66
67
|
}
|
|
67
68
|
function AssetListViewer({ files, selected, onSelect, renderCard }) {
|
|
68
69
|
const [currentPath, setCurrentPath] = useState('');
|
|
69
|
-
const [showPicker, setShowPicker] = useState(false);
|
|
70
70
|
const { folders, filesInCurrentPath } = getItemsInPath(files, currentPath);
|
|
71
|
-
const showCompactView = selected && !showPicker;
|
|
72
|
-
if (showCompactView) {
|
|
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" })] }));
|
|
74
|
-
}
|
|
75
71
|
return (_jsxs("div", { children: [currentPath && (_jsx("button", { onClick: () => {
|
|
76
72
|
const pathParts = currentPath.split('/').filter(Boolean);
|
|
77
73
|
pathParts.pop();
|
|
78
74
|
setCurrentPath(pathParts.join('/'));
|
|
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,
|
|
80
|
-
onSelect(f);
|
|
81
|
-
if (selected)
|
|
82
|
-
setShowPicker(false);
|
|
83
|
-
}) }, file)))] })] }));
|
|
75
|
+
}, 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, onSelect) }, file)))] })] }));
|
|
84
76
|
}
|
|
85
77
|
export function TextureListViewer({ files, selected, onSelect, basePath = "" }) {
|
|
86
78
|
return (_jsxs(_Fragment, { children: [_jsx(AssetListViewer, { files: files, selected: selected, onSelect: onSelect, renderCard: (file, onSelectHandler) => (_jsx(TextureCard, { file: file, basePath: basePath, onSelect: onSelectHandler })) }), _jsx(SharedCanvas, {})] }));
|
|
@@ -93,7 +85,7 @@ function TextureCard({ file, onSelect, basePath = "" }) {
|
|
|
93
85
|
if (error) {
|
|
94
86
|
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" }) }));
|
|
95
87
|
}
|
|
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() })] }));
|
|
88
|
+
return (_jsxs("div", { ref: ref, style: { maxWidth: 60, 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() })] }));
|
|
97
89
|
}
|
|
98
90
|
function TextureSphere({ url, onError }) {
|
|
99
91
|
const texture = useLoader(TextureLoader, url, undefined, (error) => {
|
|
@@ -112,7 +104,7 @@ function ModelCard({ file, onSelect, basePath = "" }) {
|
|
|
112
104
|
if (error) {
|
|
113
105
|
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" }) }));
|
|
114
106
|
}
|
|
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() })] }));
|
|
107
|
+
return (_jsxs("div", { ref: ref, style: { maxWidth: 60, 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() })] }));
|
|
116
108
|
}
|
|
117
109
|
function ModelPreview({ url, onError }) {
|
|
118
110
|
const [model, setModel] = useState(null);
|
|
@@ -146,10 +138,26 @@ function SoundCard({ file, onSelect, basePath = "" }) {
|
|
|
146
138
|
const fullPath = basePath ? `/${basePath}${file}` : file;
|
|
147
139
|
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 })] }));
|
|
148
140
|
}
|
|
141
|
+
// Single Asset Viewer Components - display only one selected asset
|
|
142
|
+
export function SingleTextureViewer({ file, basePath = "" }) {
|
|
143
|
+
if (!file)
|
|
144
|
+
return null;
|
|
145
|
+
return (_jsxs(_Fragment, { children: [_jsx(TextureCard, { file: file, basePath: basePath, onSelect: () => { } }), _jsx(SharedCanvas, {})] }));
|
|
146
|
+
}
|
|
147
|
+
export function SingleModelViewer({ file, basePath = "" }) {
|
|
148
|
+
if (!file)
|
|
149
|
+
return null;
|
|
150
|
+
return (_jsxs(_Fragment, { children: [_jsx(ModelCard, { file: file, basePath: basePath, onSelect: () => { } }), _jsx(SharedCanvas, {})] }));
|
|
151
|
+
}
|
|
152
|
+
export function SingleSoundViewer({ file, basePath = "" }) {
|
|
153
|
+
if (!file)
|
|
154
|
+
return null;
|
|
155
|
+
return _jsx(SoundCard, { file: file, basePath: basePath, onSelect: () => { } });
|
|
156
|
+
}
|
|
149
157
|
// Shared Canvas Component - can be used independently in any viewer
|
|
150
158
|
export function SharedCanvas() {
|
|
151
159
|
return (_jsx(Canvas, { shadows: true, dpr: [1, 1.5], camera: { position: [0, 0, 3], fov: 45, near: 0.1, far: 1000 }, style: {
|
|
152
|
-
position: '
|
|
160
|
+
position: 'absolute',
|
|
153
161
|
top: 0,
|
|
154
162
|
left: 0,
|
|
155
163
|
width: '100vw',
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { TextureListViewer } from '../../assetviewer/page';
|
|
2
|
+
import { SingleTextureViewer, TextureListViewer } from '../../assetviewer/page';
|
|
3
3
|
import { useEffect, useState } from 'react';
|
|
4
4
|
import { Input, Label } from './Input';
|
|
5
5
|
import { useMemo } from 'react';
|
|
@@ -7,6 +7,7 @@ import { DoubleSide, RepeatWrapping, ClampToEdgeWrapping, SRGBColorSpace, Neares
|
|
|
7
7
|
function MaterialComponentEditor({ component, onUpdate, basePath = "" }) {
|
|
8
8
|
var _a, _b, _c, _d;
|
|
9
9
|
const [textureFiles, setTextureFiles] = useState([]);
|
|
10
|
+
const [showPicker, setShowPicker] = useState(false);
|
|
10
11
|
useEffect(() => {
|
|
11
12
|
const base = basePath ? `${basePath}/` : '';
|
|
12
13
|
fetch(`/${base}textures/manifest.json`)
|
|
@@ -24,7 +25,10 @@ function MaterialComponentEditor({ component, onUpdate, basePath = "" }) {
|
|
|
24
25
|
fontFamily: 'monospace',
|
|
25
26
|
outline: 'none',
|
|
26
27
|
};
|
|
27
|
-
return (_jsxs("div", { style: { display: 'flex', flexDirection: 'column', gap: 4 }, children: [_jsxs("div", { children: [_jsx(Label, { 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: textInputStyle, value: component.properties.color, onChange: e => onUpdate({ color: e.target.value }) })] })] }), _jsxs("div", { style: { display: 'flex', alignItems: 'center', gap: 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, { children: "Texture" }),
|
|
28
|
+
return (_jsxs("div", { style: { display: 'flex', flexDirection: 'column', gap: 4 }, children: [_jsxs("div", { children: [_jsx(Label, { 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: textInputStyle, value: component.properties.color, onChange: e => onUpdate({ color: e.target.value }) })] })] }), _jsxs("div", { style: { display: 'flex', alignItems: 'center', gap: 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, { children: "Texture File" }), _jsxs("div", { style: { maxHeight: 128, overflowY: 'auto', position: 'relative', display: 'flex', alignItems: 'center' }, children: [_jsx(SingleTextureViewer, { file: component.properties.texture || undefined, basePath: basePath }), _jsx("button", { onClick: () => setShowPicker(!showPicker), style: { padding: '4px 8px', backgroundColor: '#1f2937', color: 'inherit', fontSize: 10, cursor: 'pointer', border: '1px solid rgba(34, 211, 238, 0.3)', marginTop: 4 }, children: showPicker ? 'Hide' : 'Change' }), showPicker && (_jsx("div", { style: { position: 'fixed', left: '-10px', top: '50%', transform: 'translate(-100%, -50%)', background: 'rgba(0,0,0,0.9)', padding: 16, border: '1px solid rgba(34, 211, 238, 0.3)', maxHeight: '80vh', overflowY: 'auto', zIndex: 1000 }, children: _jsx(TextureListViewer, { files: textureFiles, selected: component.properties.texture || undefined, onSelect: (file) => {
|
|
29
|
+
onUpdate({ texture: file });
|
|
30
|
+
setShowPicker(false);
|
|
31
|
+
}, 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, { children: "Repeat (X, Y)" }), _jsxs("div", { style: { display: 'flex', gap: 2 }, children: [_jsx(Input, { value: (_b = (_a = component.properties.repeatCount) === null || _a === void 0 ? void 0 : _a[0]) !== null && _b !== void 0 ? _b : 1, onChange: value => {
|
|
28
32
|
var _a, _b;
|
|
29
33
|
const y = (_b = (_a = component.properties.repeatCount) === null || _a === void 0 ? void 0 : _a[1]) !== null && _b !== void 0 ? _b : 1;
|
|
30
34
|
onUpdate({ repeatCount: [value, y] });
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
-
import { ModelListViewer } from '../../assetviewer/page';
|
|
2
|
+
import { ModelListViewer, SingleModelViewer } from '../../assetviewer/page';
|
|
3
3
|
import { useEffect, useState, useMemo } from 'react';
|
|
4
4
|
import { Label } from './Input';
|
|
5
5
|
function ModelComponentEditor({ component, node, onUpdate, basePath = "" }) {
|
|
6
6
|
const [modelFiles, setModelFiles] = useState([]);
|
|
7
|
+
const [showPicker, setShowPicker] = useState(false);
|
|
7
8
|
useEffect(() => {
|
|
8
9
|
const base = basePath ? `${basePath}/` : '';
|
|
9
10
|
fetch(`/${base}models/manifest.json`)
|
|
@@ -16,7 +17,10 @@ function ModelComponentEditor({ component, node, onUpdate, basePath = "" }) {
|
|
|
16
17
|
const filename = file.startsWith('/') ? file.slice(1) : file;
|
|
17
18
|
onUpdate({ 'filename': filename });
|
|
18
19
|
};
|
|
19
|
-
return _jsxs("div", { style: { display: 'flex', flexDirection: 'column', gap: 4 }, children: [_jsxs("div", { children: [_jsx(Label, { children: "Model" }),
|
|
20
|
+
return _jsxs("div", { style: { display: 'flex', flexDirection: 'column', gap: 4 }, children: [_jsxs("div", { children: [_jsx(Label, { children: "Model File" }), _jsxs("div", { style: { maxHeight: 128, overflowY: 'auto', position: 'relative', display: 'flex', alignItems: 'center' }, children: [_jsx(SingleModelViewer, { file: component.properties.filename ? `/${component.properties.filename}` : undefined, basePath: basePath }), _jsx("button", { onClick: () => setShowPicker(!showPicker), style: { padding: '4px 8px', backgroundColor: '#1f2937', color: 'inherit', fontSize: 10, cursor: 'pointer', border: '1px solid rgba(34, 211, 238, 0.3)', marginTop: 4 }, children: showPicker ? 'Hide' : 'Change' }), showPicker && (_jsx("div", { style: { position: 'fixed', left: '-10px', top: '50%', transform: 'translate(-100%, -50%)', background: 'rgba(0,0,0,0.9)', padding: 16, border: '1px solid rgba(34, 211, 238, 0.3)', maxHeight: '80vh', overflowY: 'auto', zIndex: 1000 }, children: _jsx(ModelListViewer, { files: modelFiles, selected: component.properties.filename ? `/${component.properties.filename}` : undefined, onSelect: (file) => {
|
|
21
|
+
handleModelSelect(file);
|
|
22
|
+
setShowPicker(false);
|
|
23
|
+
}, basePath: basePath }, node === null || node === void 0 ? void 0 : node.id) }))] })] }), _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" })] })] });
|
|
20
24
|
}
|
|
21
25
|
// View for Model component
|
|
22
26
|
function ModelComponentView({ properties, loadedModels, children }) {
|
package/package.json
CHANGED
|
@@ -48,6 +48,7 @@ function FolderTile({ name, onClick }: { name: string; onClick: () => void }) {
|
|
|
48
48
|
<div
|
|
49
49
|
onClick={onClick}
|
|
50
50
|
style={{
|
|
51
|
+
maxWidth: 60,
|
|
51
52
|
aspectRatio: '1 / 1',
|
|
52
53
|
backgroundColor: '#1f2937', /* gray-800 */
|
|
53
54
|
cursor: 'pointer',
|
|
@@ -98,25 +99,8 @@ interface AssetListViewerProps {
|
|
|
98
99
|
|
|
99
100
|
function AssetListViewer({ files, selected, onSelect, renderCard }: AssetListViewerProps) {
|
|
100
101
|
const [currentPath, setCurrentPath] = useState('');
|
|
101
|
-
const [showPicker, setShowPicker] = useState(false);
|
|
102
102
|
const { folders, filesInCurrentPath } = getItemsInPath(files, currentPath);
|
|
103
103
|
|
|
104
|
-
const showCompactView = selected && !showPicker;
|
|
105
|
-
|
|
106
|
-
if (showCompactView) {
|
|
107
|
-
return (
|
|
108
|
-
<div style={{ display: 'flex', gap: 4, alignItems: 'center' }}>
|
|
109
|
-
{renderCard(selected, onSelect)}
|
|
110
|
-
<button
|
|
111
|
-
onClick={() => setShowPicker(true)}
|
|
112
|
-
style={{ padding: '4px 8px', backgroundColor: '#1f2937', color: 'inherit', fontSize: 12, cursor: 'pointer', border: 'none' }}
|
|
113
|
-
>
|
|
114
|
-
Change
|
|
115
|
-
</button>
|
|
116
|
-
</div>
|
|
117
|
-
);
|
|
118
|
-
}
|
|
119
|
-
|
|
120
104
|
return (
|
|
121
105
|
<div>
|
|
122
106
|
{currentPath && (
|
|
@@ -141,10 +125,7 @@ function AssetListViewer({ files, selected, onSelect, renderCard }: AssetListVie
|
|
|
141
125
|
))}
|
|
142
126
|
{filesInCurrentPath.map((file) => (
|
|
143
127
|
<div key={file}>
|
|
144
|
-
{renderCard(file,
|
|
145
|
-
onSelect(f);
|
|
146
|
-
if (selected) setShowPicker(false);
|
|
147
|
-
})}
|
|
128
|
+
{renderCard(file, onSelect)}
|
|
148
129
|
</div>
|
|
149
130
|
))}
|
|
150
131
|
</div>
|
|
@@ -196,7 +177,7 @@ function TextureCard({ file, onSelect, basePath = "" }: { file: string; onSelect
|
|
|
196
177
|
return (
|
|
197
178
|
<div
|
|
198
179
|
ref={ref}
|
|
199
|
-
style={{ aspectRatio: '1 / 1', backgroundColor: '#1f2937', cursor: 'pointer', display: 'flex', flexDirection: 'column' }}
|
|
180
|
+
style={{ maxWidth: 60, aspectRatio: '1 / 1', backgroundColor: '#1f2937', cursor: 'pointer', display: 'flex', flexDirection: 'column' }}
|
|
200
181
|
onClick={() => onSelect(file)}
|
|
201
182
|
onMouseEnter={() => setIsHovered(true)}
|
|
202
183
|
onMouseLeave={() => setIsHovered(false)}
|
|
@@ -282,7 +263,7 @@ function ModelCard({ file, onSelect, basePath = "" }: { file: string; onSelect:
|
|
|
282
263
|
return (
|
|
283
264
|
<div
|
|
284
265
|
ref={ref}
|
|
285
|
-
style={{ aspectRatio: '1 / 1', backgroundColor: '#111827', cursor: 'pointer', display: 'flex', flexDirection: 'column' }}
|
|
266
|
+
style={{ maxWidth: 60, aspectRatio: '1 / 1', backgroundColor: '#111827', cursor: 'pointer', display: 'flex', flexDirection: 'column' }}
|
|
286
267
|
onClick={() => onSelect(file)}
|
|
287
268
|
>
|
|
288
269
|
<div style={styles.flexFillRelative}>
|
|
@@ -364,6 +345,32 @@ function SoundCard({ file, onSelect, basePath = "" }: { file: string; onSelect:
|
|
|
364
345
|
);
|
|
365
346
|
}
|
|
366
347
|
|
|
348
|
+
// Single Asset Viewer Components - display only one selected asset
|
|
349
|
+
export function SingleTextureViewer({ file, basePath = "" }: { file?: string; basePath?: string }) {
|
|
350
|
+
if (!file) return null;
|
|
351
|
+
return (
|
|
352
|
+
<>
|
|
353
|
+
<TextureCard file={file} basePath={basePath} onSelect={() => { }} />
|
|
354
|
+
<SharedCanvas />
|
|
355
|
+
</>
|
|
356
|
+
);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
export function SingleModelViewer({ file, basePath = "" }: { file?: string; basePath?: string }) {
|
|
360
|
+
if (!file) return null;
|
|
361
|
+
return (
|
|
362
|
+
<>
|
|
363
|
+
<ModelCard file={file} basePath={basePath} onSelect={() => { }} />
|
|
364
|
+
<SharedCanvas />
|
|
365
|
+
</>
|
|
366
|
+
);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
export function SingleSoundViewer({ file, basePath = "" }: { file?: string; basePath?: string }) {
|
|
370
|
+
if (!file) return null;
|
|
371
|
+
return <SoundCard file={file} basePath={basePath} onSelect={() => { }} />;
|
|
372
|
+
}
|
|
373
|
+
|
|
367
374
|
// Shared Canvas Component - can be used independently in any viewer
|
|
368
375
|
export function SharedCanvas() {
|
|
369
376
|
return (
|
|
@@ -372,7 +379,7 @@ export function SharedCanvas() {
|
|
|
372
379
|
dpr={[1, 1.5]}
|
|
373
380
|
camera={{ position: [0, 0, 3], fov: 45, near: 0.1, far: 1000 }}
|
|
374
381
|
style={{
|
|
375
|
-
position: '
|
|
382
|
+
position: 'absolute',
|
|
376
383
|
top: 0,
|
|
377
384
|
left: 0,
|
|
378
385
|
width: '100vw',
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { TextureListViewer } from '../../assetviewer/page';
|
|
1
|
+
import { SingleTextureViewer, TextureListViewer } from '../../assetviewer/page';
|
|
2
2
|
import { useEffect, useState } from 'react';
|
|
3
3
|
import { Component } from './ComponentRegistry';
|
|
4
4
|
import { Input, Label } from './Input';
|
|
@@ -21,6 +21,7 @@ import {
|
|
|
21
21
|
|
|
22
22
|
function MaterialComponentEditor({ component, onUpdate, basePath = "" }: { component: any; onUpdate: (newComp: any) => void; basePath?: string }) {
|
|
23
23
|
const [textureFiles, setTextureFiles] = useState<string[]>([]);
|
|
24
|
+
const [showPicker, setShowPicker] = useState(false);
|
|
24
25
|
|
|
25
26
|
useEffect(() => {
|
|
26
27
|
const base = basePath ? `${basePath}/` : '';
|
|
@@ -72,15 +73,30 @@ function MaterialComponentEditor({ component, onUpdate, basePath = "" }: { compo
|
|
|
72
73
|
</div>
|
|
73
74
|
|
|
74
75
|
<div>
|
|
75
|
-
<Label>Texture</Label>
|
|
76
|
-
<div style={{ maxHeight: 128, overflowY: 'auto' }}>
|
|
77
|
-
<
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
76
|
+
<Label>Texture File</Label>
|
|
77
|
+
<div style={{ maxHeight: 128, overflowY: 'auto', position: 'relative', display: 'flex', alignItems: 'center' }}>
|
|
78
|
+
<SingleTextureViewer file={component.properties.texture || undefined} basePath={basePath} />
|
|
79
|
+
<button
|
|
80
|
+
onClick={() => setShowPicker(!showPicker)}
|
|
81
|
+
style={{ padding: '4px 8px', backgroundColor: '#1f2937', color: 'inherit', fontSize: 10, cursor: 'pointer', border: '1px solid rgba(34, 211, 238, 0.3)', marginTop: 4 }}
|
|
82
|
+
>
|
|
83
|
+
{showPicker ? 'Hide' : 'Change'}
|
|
84
|
+
</button>
|
|
85
|
+
{showPicker && (
|
|
86
|
+
<div style={{ position: 'fixed', left: '-10px', top: '50%', transform: 'translate(-100%, -50%)', background: 'rgba(0,0,0,0.9)', padding: 16, border: '1px solid rgba(34, 211, 238, 0.3)', maxHeight: '80vh', overflowY: 'auto', zIndex: 1000 }}>
|
|
87
|
+
<TextureListViewer
|
|
88
|
+
files={textureFiles}
|
|
89
|
+
selected={component.properties.texture || undefined}
|
|
90
|
+
onSelect={(file) => {
|
|
91
|
+
onUpdate({ texture: file });
|
|
92
|
+
setShowPicker(false);
|
|
93
|
+
}}
|
|
94
|
+
basePath={basePath}
|
|
95
|
+
/>
|
|
96
|
+
</div>
|
|
97
|
+
)}
|
|
83
98
|
</div>
|
|
99
|
+
|
|
84
100
|
</div>
|
|
85
101
|
|
|
86
102
|
{component.properties.texture && (
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ModelListViewer } from '../../assetviewer/page';
|
|
1
|
+
import { ModelListViewer, SingleModelViewer } from '../../assetviewer/page';
|
|
2
2
|
import { useEffect, useState, useMemo } from 'react';
|
|
3
3
|
import { Component } from './ComponentRegistry';
|
|
4
4
|
import { Label } from './Input';
|
|
@@ -6,6 +6,7 @@ import { GameObject } from '../types';
|
|
|
6
6
|
|
|
7
7
|
function ModelComponentEditor({ component, node, onUpdate, basePath = "" }: { component: any; node?: GameObject; onUpdate: (newComp: any) => void; basePath?: string }) {
|
|
8
8
|
const [modelFiles, setModelFiles] = useState<string[]>([]);
|
|
9
|
+
const [showPicker, setShowPicker] = useState(false);
|
|
9
10
|
|
|
10
11
|
useEffect(() => {
|
|
11
12
|
const base = basePath ? `${basePath}/` : '';
|
|
@@ -23,15 +24,29 @@ function ModelComponentEditor({ component, node, onUpdate, basePath = "" }: { co
|
|
|
23
24
|
|
|
24
25
|
return <div style={{ display: 'flex', flexDirection: 'column', gap: 4 }}>
|
|
25
26
|
<div>
|
|
26
|
-
<Label>Model</Label>
|
|
27
|
-
<div style={{ maxHeight: 128, overflowY: 'auto' }}>
|
|
28
|
-
<
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
27
|
+
<Label>Model File</Label>
|
|
28
|
+
<div style={{ maxHeight: 128, overflowY: 'auto', position: 'relative', display: 'flex', alignItems: 'center' }}>
|
|
29
|
+
<SingleModelViewer file={component.properties.filename ? `/${component.properties.filename}` : undefined} basePath={basePath} />
|
|
30
|
+
<button
|
|
31
|
+
onClick={() => setShowPicker(!showPicker)}
|
|
32
|
+
style={{ padding: '4px 8px', backgroundColor: '#1f2937', color: 'inherit', fontSize: 10, cursor: 'pointer', border: '1px solid rgba(34, 211, 238, 0.3)', marginTop: 4 }}
|
|
33
|
+
>
|
|
34
|
+
{showPicker ? 'Hide' : 'Change'}
|
|
35
|
+
</button>
|
|
36
|
+
{showPicker && (
|
|
37
|
+
<div style={{ position: 'fixed', left: '-10px', top: '50%', transform: 'translate(-100%, -50%)', background: 'rgba(0,0,0,0.9)', padding: 16, border: '1px solid rgba(34, 211, 238, 0.3)', maxHeight: '80vh', overflowY: 'auto', zIndex: 1000 }}>
|
|
38
|
+
<ModelListViewer
|
|
39
|
+
key={node?.id}
|
|
40
|
+
files={modelFiles}
|
|
41
|
+
selected={component.properties.filename ? `/${component.properties.filename}` : undefined}
|
|
42
|
+
onSelect={(file) => {
|
|
43
|
+
handleModelSelect(file);
|
|
44
|
+
setShowPicker(false);
|
|
45
|
+
}}
|
|
46
|
+
basePath={basePath}
|
|
47
|
+
/>
|
|
48
|
+
</div>
|
|
49
|
+
)}
|
|
35
50
|
</div>
|
|
36
51
|
</div>
|
|
37
52
|
<div style={{ display: 'flex', alignItems: 'center', gap: 4 }}>
|