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.
@@ -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, (f) => {
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: 'fixed',
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" }), _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, { 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
+ 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" }), _jsx("div", { style: { maxHeight: 128, overflowY: 'auto' }, children: _jsx(ModelListViewer, { files: modelFiles, selected: component.properties.filename ? `/${component.properties.filename}` : undefined, onSelect: handleModelSelect, 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
+ 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 }) {
@@ -24,7 +24,6 @@ export const base = {
24
24
  color: colors.text,
25
25
  border: `1px solid ${colors.border}`,
26
26
  borderRadius: 4,
27
- overflow: 'hidden',
28
27
  backdropFilter: 'blur(8px)',
29
28
  fontFamily: fonts.family,
30
29
  fontSize: fonts.size,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-three-game",
3
- "version": "0.0.39",
3
+ "version": "0.0.40",
4
4
  "description": "Batteries included React Three Fiber game engine",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.js",
@@ -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, (f) => {
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: 'fixed',
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
- <TextureListViewer
78
- files={textureFiles}
79
- selected={component.properties.texture || undefined}
80
- onSelect={file => onUpdate({ texture: file })}
81
- basePath={basePath}
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
- <ModelListViewer
29
- key={node?.id}
30
- files={modelFiles}
31
- selected={component.properties.filename ? `/${component.properties.filename}` : undefined}
32
- onSelect={handleModelSelect}
33
- basePath={basePath}
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 }}>
@@ -27,7 +27,6 @@ export const base = {
27
27
  color: colors.text,
28
28
  border: `1px solid ${colors.border}`,
29
29
  borderRadius: 4,
30
- overflow: 'hidden',
31
30
  backdropFilter: 'blur(8px)',
32
31
  fontFamily: fonts.family,
33
32
  fontSize: fonts.size,