react-three-game 0.0.59 → 0.0.61

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.
Files changed (67) hide show
  1. package/dist/tools/dragdrop/DragDropLoader.d.ts +8 -8
  2. package/dist/tools/dragdrop/DragDropLoader.js +33 -15
  3. package/dist/tools/dragdrop/index.d.ts +3 -3
  4. package/dist/tools/dragdrop/index.js +1 -1
  5. package/dist/tools/dragdrop/modelLoader.d.ts +10 -1
  6. package/dist/tools/dragdrop/modelLoader.js +39 -0
  7. package/dist/tools/prefabeditor/PrefabEditor.js +17 -26
  8. package/dist/tools/prefabeditor/PrefabRoot.d.ts +1 -1
  9. package/dist/tools/prefabeditor/PrefabRoot.js +2 -8
  10. package/package.json +9 -3
  11. package/.gitattributes +0 -2
  12. package/.github/copilot-instructions.md +0 -83
  13. package/.github/workflows/nextjs.yml +0 -99
  14. package/.gitmodules +0 -3
  15. package/assets/architecture.png +0 -0
  16. package/assets/editor.gif +0 -0
  17. package/assets/favicon.ico +0 -0
  18. package/assets/react-three-game-logo.png +0 -0
  19. package/dist/tools/dragdrop/page.d.ts +0 -1
  20. package/dist/tools/dragdrop/page.js +0 -11
  21. package/dist/tools/prefabeditor/EntityEvents.d.ts +0 -54
  22. package/dist/tools/prefabeditor/EntityEvents.js +0 -85
  23. package/dist/tools/prefabeditor/page.d.ts +0 -1
  24. package/dist/tools/prefabeditor/page.js +0 -5
  25. package/react-three-game-skill/.gitattributes +0 -2
  26. package/react-three-game-skill/README.md +0 -7
  27. package/react-three-game-skill/react-three-game/SKILL.md +0 -514
  28. package/react-three-game-skill/react-three-game/rules/ADVANCED_PHYSICS.md +0 -472
  29. package/react-three-game-skill/react-three-game/rules/LIGHTING.md +0 -6
  30. package/src/helpers/SoundManager.ts +0 -130
  31. package/src/helpers/index.ts +0 -91
  32. package/src/index.ts +0 -59
  33. package/src/shared/ContactShadow.tsx +0 -74
  34. package/src/shared/GameCanvas.tsx +0 -52
  35. package/src/tools/assetviewer/page.tsx +0 -425
  36. package/src/tools/dragdrop/DragDropLoader.tsx +0 -136
  37. package/src/tools/dragdrop/index.ts +0 -4
  38. package/src/tools/dragdrop/modelLoader.ts +0 -145
  39. package/src/tools/dragdrop/page.tsx +0 -45
  40. package/src/tools/prefabeditor/Dropdown.tsx +0 -112
  41. package/src/tools/prefabeditor/EditorContext.tsx +0 -25
  42. package/src/tools/prefabeditor/EditorTree.tsx +0 -452
  43. package/src/tools/prefabeditor/EditorTreeMenus.tsx +0 -307
  44. package/src/tools/prefabeditor/EditorUI.tsx +0 -204
  45. package/src/tools/prefabeditor/EventSystem.tsx +0 -36
  46. package/src/tools/prefabeditor/GameEvents.ts +0 -191
  47. package/src/tools/prefabeditor/InstanceProvider.tsx +0 -466
  48. package/src/tools/prefabeditor/PrefabEditor.tsx +0 -262
  49. package/src/tools/prefabeditor/PrefabRoot.tsx +0 -773
  50. package/src/tools/prefabeditor/components/AmbientLightComponent.tsx +0 -34
  51. package/src/tools/prefabeditor/components/CameraComponent.tsx +0 -117
  52. package/src/tools/prefabeditor/components/ComponentRegistry.ts +0 -40
  53. package/src/tools/prefabeditor/components/DirectionalLightComponent.tsx +0 -210
  54. package/src/tools/prefabeditor/components/EnvironmentComponent.tsx +0 -47
  55. package/src/tools/prefabeditor/components/GeometryComponent.tsx +0 -133
  56. package/src/tools/prefabeditor/components/Input.tsx +0 -820
  57. package/src/tools/prefabeditor/components/MaterialComponent.tsx +0 -431
  58. package/src/tools/prefabeditor/components/ModelComponent.tsx +0 -176
  59. package/src/tools/prefabeditor/components/PhysicsComponent.tsx +0 -188
  60. package/src/tools/prefabeditor/components/SpotLightComponent.tsx +0 -109
  61. package/src/tools/prefabeditor/components/TextComponent.tsx +0 -137
  62. package/src/tools/prefabeditor/components/TransformComponent.tsx +0 -173
  63. package/src/tools/prefabeditor/components/index.ts +0 -26
  64. package/src/tools/prefabeditor/page.tsx +0 -10
  65. package/src/tools/prefabeditor/styles.ts +0 -235
  66. package/src/tools/prefabeditor/types.ts +0 -20
  67. package/src/tools/prefabeditor/utils.ts +0 -312
package/src/index.ts DELETED
@@ -1,59 +0,0 @@
1
- // Core
2
- export { default as GameCanvas } from './shared/GameCanvas';
3
-
4
- // Helpers
5
- export * from './helpers';
6
- export { sound as soundManager } from './helpers/SoundManager';
7
-
8
- // Prefab Editor - Components
9
- export { default as PrefabEditor } from './tools/prefabeditor/PrefabEditor';
10
- export { default as PrefabRoot } from './tools/prefabeditor/PrefabRoot';
11
-
12
- // Prefab Editor - Component Registry
13
- export { registerComponent } from './tools/prefabeditor/components/ComponentRegistry';
14
-
15
- // Prefab Editor - Input Components
16
- export {
17
- FieldRenderer,
18
- FieldGroup,
19
- Input,
20
- Label,
21
- Vector3Input,
22
- Vector3Field,
23
- NumberField,
24
- ColorInput,
25
- ColorField,
26
- StringInput,
27
- StringField,
28
- BooleanInput,
29
- BooleanField,
30
- SelectInput,
31
- SelectField,
32
- } from './tools/prefabeditor/components/Input';
33
-
34
- // Prefab Editor - Styles & Utils
35
- export * from './tools/prefabeditor/utils';
36
- export type { ExportGLBOptions } from './tools/prefabeditor/utils';
37
-
38
- // Prefab Editor - Types
39
- export type { PrefabEditorRef } from './tools/prefabeditor/PrefabEditor';
40
- export type { PrefabRootRef } from './tools/prefabeditor/PrefabRoot';
41
- export type { Component } from './tools/prefabeditor/components/ComponentRegistry';
42
- export type { FieldDefinition, FieldType } from './tools/prefabeditor/components/Input';
43
- export type { Prefab, GameObject, ComponentData } from './tools/prefabeditor/types';
44
-
45
- // Game Events (physics + custom events)
46
- export { gameEvents, useGameEvent, getEntityIdFromRigidBody } from './tools/prefabeditor/GameEvents';
47
- export type { GameEventType, GameEventMap, GameEventPayload, PhysicsEventType, PhysicsEventPayload } from './tools/prefabeditor/GameEvents';
48
- // Backward compatibility aliases
49
- export { entityEvents, useEntityEvent } from './tools/prefabeditor/GameEvents';
50
- export type { EntityEventType, EntityEventPayload } from './tools/prefabeditor/GameEvents';
51
-
52
- // Asset Tools
53
- export * from './tools/dragdrop';
54
- export {
55
- TextureListViewer,
56
- ModelListViewer,
57
- SoundListViewer,
58
- SharedCanvas,
59
- } from './tools/assetviewer/page';
@@ -1,74 +0,0 @@
1
- "use client";
2
-
3
- import { useMemo } from "react";
4
- import * as THREE from "three/webgpu";
5
- import {
6
- float,
7
- uv,
8
- vec3,
9
- smoothstep,
10
- uniform,
11
- length,
12
- } from "three/tsl";
13
-
14
- interface ContactShadowProps {
15
- opacity?: number;
16
- blur?: number;
17
- scale?: number;
18
- yOffset?: number;
19
- }
20
-
21
- const ContactShadow = ({
22
- opacity = 0.4,
23
- blur = 2.5,
24
- scale = 1.2,
25
- yOffset = 0.05,
26
- }: ContactShadowProps) => {
27
- const material = useMemo(() => {
28
- const mat = new THREE.MeshBasicNodeMaterial();
29
- mat.transparent = true;
30
- mat.depthWrite = false;
31
- mat.depthTest = true;
32
- mat.side = THREE.DoubleSide;
33
- mat.polygonOffset = true;
34
- mat.polygonOffsetFactor = -1;
35
- mat.polygonOffsetUnits = -1;
36
-
37
- const uOpacity = uniform(opacity);
38
- const uBlur = uniform(blur);
39
-
40
- // UVs centered around origin
41
- const centeredUV = uv().sub(0.5).mul(2.0);
42
-
43
- // IMPORTANT: use functional length(), not .length()
44
- const dist = length(centeredUV);
45
-
46
- const innerRadius = float(0.0);
47
- const outerRadius = float(1.0);
48
- const blurAmount = uBlur.div(10.0);
49
-
50
- const alpha = smoothstep(
51
- outerRadius,
52
- innerRadius.add(blurAmount),
53
- dist
54
- ).mul(uOpacity);
55
-
56
- mat.colorNode = vec3(0.0, 0.0, 0.0);
57
- mat.opacityNode = alpha;
58
-
59
- return mat;
60
- }, [opacity, blur]);
61
-
62
- return (
63
- <mesh
64
- rotation={[-Math.PI / 2, 0, 0]}
65
- position={[0, yOffset, 0]}
66
- material={material}
67
- renderOrder={1}
68
- >
69
- <planeGeometry args={[scale, scale]} />
70
- </mesh>
71
- );
72
- };
73
-
74
- export default ContactShadow;
@@ -1,52 +0,0 @@
1
- import { Canvas, extend, CanvasProps } from "@react-three/fiber";
2
- import { WebGPURenderer, MeshBasicNodeMaterial, MeshStandardNodeMaterial, SpriteNodeMaterial, PCFShadowMap } from "three/webgpu";
3
- import { Suspense, useState } from "react";
4
- import { WebGPURendererParameters } from "three/src/renderers/webgpu/WebGPURenderer.Nodes.js";
5
- import { Loader } from "@react-three/drei";
6
-
7
- // generic version
8
- // extend(THREE as any)
9
-
10
- extend({
11
- MeshBasicNodeMaterial: MeshBasicNodeMaterial,
12
- MeshStandardNodeMaterial: MeshStandardNodeMaterial,
13
- SpriteNodeMaterial: SpriteNodeMaterial,
14
- });
15
-
16
- interface GameCanvasProps extends Omit<CanvasProps, 'children'> {
17
- loader?: boolean;
18
- children: React.ReactNode;
19
- glConfig?: WebGPURendererParameters;
20
- }
21
-
22
- export default function GameCanvas({ loader = false, children, glConfig, ...props }: GameCanvasProps) {
23
- const [frameloop, setFrameloop] = useState<"never" | "always">("never");
24
-
25
- return <>
26
- <Canvas
27
- style={{ touchAction: 'none', userSelect: 'none' }}
28
- shadows={{ type: PCFShadowMap, }}
29
- frameloop={frameloop}
30
- gl={async ({ canvas }) => {
31
- const renderer = new WebGPURenderer({
32
- canvas: canvas as HTMLCanvasElement,
33
- // @ts-expect-error futuristic
34
- shadowMap: true,
35
- antialias: true,
36
- ...glConfig,
37
- });
38
- await renderer.init().then(() => {
39
- setFrameloop("always");
40
- });
41
- return renderer
42
- }}
43
- {...props}
44
- >
45
- <Suspense>
46
- {children}
47
- </Suspense>
48
-
49
- {loader ? <Loader /> : null}
50
- </Canvas>
51
- </>;
52
- }
@@ -1,425 +0,0 @@
1
- import { Canvas } from "@react-three/fiber";
2
- import { OrbitControls, Stage, View, PerspectiveCamera } from "@react-three/drei";
3
- import { Component as ReactComponent, Suspense, useEffect, useState, useRef } from "react";
4
- import { TextureLoader } from "three";
5
- import { loadModel } from "../dragdrop";
6
-
7
- class ErrorBoundary extends ReactComponent<{ onError?: () => void; children: React.ReactNode }, { hasError: boolean }> {
8
- constructor(props: any) {
9
- super(props);
10
- this.state = { hasError: false };
11
- }
12
- static getDerivedStateFromError() { return { hasError: true }; }
13
- componentDidCatch() { this.props.onError?.(); }
14
- render() { return this.state.hasError ? null : this.props.children; }
15
- }
16
-
17
- // view models and textures in manifest, onselect callback
18
-
19
- const styles: Record<string, any> = {
20
- errorIcon: { color: '#fca5a5', fontSize: 12 }, // text-red-400 text-xs
21
- flexFillRelative: { flex: 1, position: 'relative' },
22
- bottomLabel: { backgroundColor: 'rgba(0,0,0,0.6)', color: '#f9fafb', fontSize: 10, padding: '0 4px', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', textAlign: 'center' },
23
- textLight: { color: '#f9fafb' },
24
- iconLarge: { fontSize: 20 }
25
- };
26
-
27
- function getItemsInPath(files: string[], currentPath: string) {
28
- // Remove the leading category folder (e.g., /textures/, /models/, /sounds/)
29
- const filesWithoutCategory = files.map(file => {
30
- const parts = file.split('/').filter(Boolean);
31
- return parts.length > 1 ? '/' + parts.slice(1).join('/') : '';
32
- }).filter(Boolean);
33
-
34
- const prefix = currentPath ? `/${currentPath}/` : '/';
35
- const relevantFiles = filesWithoutCategory.filter(file => file.startsWith(prefix));
36
-
37
- const folders = new Set<string>();
38
- const filesInCurrentPath: string[] = [];
39
-
40
- relevantFiles.forEach((file, index) => {
41
- const relativePath = file.slice(prefix.length);
42
- const parts = relativePath.split('/').filter(Boolean);
43
-
44
- if (parts.length > 1) {
45
- folders.add(parts[0]);
46
- } else if (parts[0]) {
47
- // Return the original file path
48
- filesInCurrentPath.push(files[filesWithoutCategory.indexOf(file)]);
49
- }
50
- });
51
-
52
- return { folders: Array.from(folders), filesInCurrentPath };
53
- }
54
-
55
- function FolderTile({ name, onClick }: { name: string; onClick: () => void }) {
56
- return (
57
- <div
58
- onClick={onClick}
59
- style={{
60
- maxWidth: 60,
61
- aspectRatio: '1 / 1',
62
- backgroundColor: '#1f2937', /* gray-800 */
63
- color: '#f9fafb',
64
- cursor: 'pointer',
65
- display: 'flex',
66
- flexDirection: 'column',
67
- alignItems: 'center',
68
- justifyContent: 'center'
69
- }}
70
- >
71
- <div style={{ fontSize: 24 }}>📁</div>
72
- <div style={{ fontSize: 10, textAlign: 'center', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', width: '100%', padding: '0 4px', marginTop: 4 }}>{name}</div>
73
- </div>
74
- );
75
- }
76
-
77
- function useInView() {
78
- const [isInView, setIsInView] = useState(false);
79
- const ref = useRef<HTMLDivElement>(null);
80
-
81
- useEffect(() => {
82
- const observer = new IntersectionObserver(
83
- ([entry]) => {
84
- setIsInView(entry.isIntersecting);
85
- },
86
- { rootMargin: '100px' }
87
- );
88
-
89
- if (ref.current) {
90
- observer.observe(ref.current);
91
- }
92
-
93
- return () => {
94
- if (ref.current) {
95
- observer.unobserve(ref.current);
96
- }
97
- };
98
- }, []);
99
-
100
- return { ref, isInView };
101
- }
102
-
103
- interface AssetListViewerProps {
104
- files: string[];
105
- selected?: string;
106
- onSelect: (file: string) => void;
107
- renderCard: (file: string, onSelect: (file: string) => void) => React.ReactNode;
108
- }
109
-
110
- function AssetListViewer({ files, selected, onSelect, renderCard }: AssetListViewerProps) {
111
- const [currentPath, setCurrentPath] = useState('');
112
- const { folders, filesInCurrentPath } = getItemsInPath(files, currentPath);
113
-
114
- return (
115
- <div style={styles.textLight}>
116
- {currentPath && (
117
- <button
118
- onClick={() => {
119
- const pathParts = currentPath.split('/').filter(Boolean);
120
- pathParts.pop();
121
- setCurrentPath(pathParts.join('/'));
122
- }}
123
- style={{ marginBottom: 4, padding: '4px 8px', backgroundColor: '#1f2937', color: 'inherit', fontSize: 12, cursor: 'pointer', border: 'none' }}
124
- >
125
- ← Back
126
- </button>
127
- )}
128
- <div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: 4 }}>
129
- {folders.map((folder) => (
130
- <FolderTile
131
- key={folder}
132
- name={folder}
133
- onClick={() => setCurrentPath(currentPath ? `${currentPath}/${folder}` : folder)}
134
- />
135
- ))}
136
- {filesInCurrentPath.map((file) => (
137
- <div key={file}>
138
- {renderCard(file, onSelect)}
139
- </div>
140
- ))}
141
- </div>
142
- </div>
143
- );
144
- }
145
-
146
- interface TextureListViewerProps {
147
- files: string[];
148
- selected?: string;
149
- onSelect: (file: string) => void;
150
- basePath?: string;
151
- }
152
-
153
- export function TextureListViewer({ files, selected, onSelect, basePath = "" }: TextureListViewerProps) {
154
- return (
155
- <div style={{ position: 'relative', width: '100%', height: '100%' }}>
156
- <div style={{ width: '100%', height: '100%', overflowY: 'auto', overflowX: 'hidden', paddingRight: 4 }}>
157
- <AssetListViewer
158
- files={files}
159
- selected={selected}
160
- onSelect={onSelect}
161
- renderCard={(file, onSelectHandler) => (
162
- <TextureCard file={file} basePath={basePath} onSelect={onSelectHandler} />
163
- )}
164
- />
165
- </div>
166
- <SharedCanvas />
167
- </div>
168
- );
169
- }
170
-
171
- function TextureCard({ file, onSelect, basePath = "" }: { file: string; onSelect: (file: string) => void; basePath?: string }) {
172
- const [error, setError] = useState(false);
173
- const [isHovered, setIsHovered] = useState(false);
174
- const { ref, isInView } = useInView();
175
- const fullPath = basePath ? `/${basePath}${file}` : file;
176
-
177
- if (error) {
178
- return (
179
- <div
180
- ref={ref}
181
- style={{ aspectRatio: '1 / 1', backgroundColor: '#374151', cursor: 'pointer', display: 'flex', alignItems: 'center', justifyContent: 'center' }}
182
- onClick={() => onSelect(file)}
183
- >
184
- <div style={styles.errorIcon}>✗</div>
185
- </div>
186
- );
187
- }
188
-
189
- return (
190
- <div
191
- ref={ref}
192
- style={{ maxWidth: 60, aspectRatio: '1 / 1', backgroundColor: '#1f2937', color: '#f9fafb', cursor: 'pointer', display: 'flex', flexDirection: 'column' }}
193
- onClick={() => onSelect(file)}
194
- onMouseEnter={() => setIsHovered(true)}
195
- onMouseLeave={() => setIsHovered(false)}
196
- >
197
- <div style={{ flex: 1, position: 'relative' }}>
198
- {isInView ? (
199
- <View style={{ width: '100%', height: '100%' }}>
200
- <PerspectiveCamera makeDefault position={[0, 0, 2.5]} fov={50} />
201
- <ambientLight intensity={0.8} />
202
- <pointLight position={[5, 5, 5]} intensity={0.5} />
203
- <TextureSphere url={fullPath} onError={() => setError(true)} />
204
- <OrbitControls
205
- enableZoom={false}
206
- enablePan={false}
207
- autoRotate={isHovered}
208
- autoRotateSpeed={2}
209
- />
210
- </View>
211
- ) : null}
212
- </div>
213
- <div style={styles.bottomLabel}>
214
- {file.split('/').pop()}
215
- </div>
216
- </div>
217
- );
218
- }
219
-
220
- function TextureSphere({ url, onError }: { url: string; onError?: () => void }) {
221
- const [texture, setTexture] = useState<any>(null);
222
-
223
- useEffect(() => {
224
- setTexture(null);
225
- const loader = new TextureLoader();
226
- loader.load(
227
- url,
228
- (tex) => setTexture(tex),
229
- undefined,
230
- (err) => {
231
- console.warn('Failed to load texture:', url, err);
232
- onError?.();
233
- }
234
- );
235
- }, [url]);
236
-
237
- if (!texture) return null;
238
- return (
239
- <mesh position={[0, 0, 0]}>
240
- <sphereGeometry args={[1, 32, 32]} />
241
- <meshStandardMaterial map={texture} />
242
- </mesh>
243
- );
244
- }
245
-
246
- interface ModelListViewerProps {
247
- files: string[];
248
- selected?: string;
249
- onSelect: (file: string) => void;
250
- basePath?: string;
251
- }
252
-
253
- export function ModelListViewer({ files, selected, onSelect, basePath = "" }: ModelListViewerProps) {
254
- return (
255
- <div style={{ position: 'relative', width: '100%', height: '100%' }}>
256
- <div style={{ width: '100%', height: '100%', overflowY: 'auto', overflowX: 'hidden', paddingRight: 4 }}>
257
- <AssetListViewer
258
- files={files}
259
- selected={selected}
260
- onSelect={onSelect}
261
- renderCard={(file, onSelectHandler) => (
262
- <ModelCard file={file} basePath={basePath} onSelect={onSelectHandler} />
263
- )}
264
- />
265
- </div>
266
- <SharedCanvas />
267
- </div>
268
- );
269
- }
270
-
271
- function ModelCard({ file, onSelect, basePath = "" }: { file: string; onSelect: (file: string) => void; basePath?: string }) {
272
- const [error, setError] = useState(false);
273
- const { ref, isInView } = useInView();
274
- const fullPath = basePath ? `/${basePath}${file}` : file;
275
-
276
- if (error) {
277
- return (
278
- <div
279
- ref={ref}
280
- style={{ aspectRatio: '1 / 1', backgroundColor: '#374151', cursor: 'pointer', display: 'flex', alignItems: 'center', justifyContent: 'center' }}
281
- onClick={() => onSelect(file)}
282
- >
283
- <div style={styles.errorIcon}>✗</div>
284
- </div>
285
- );
286
- }
287
-
288
- return (
289
- <div
290
- ref={ref}
291
- style={{ maxWidth: 60, aspectRatio: '1 / 1', backgroundColor: '#111827', color: '#f9fafb', cursor: 'pointer', display: 'flex', flexDirection: 'column' }}
292
- onClick={() => onSelect(file)}
293
- >
294
- <div style={styles.flexFillRelative}>
295
- {isInView ? (
296
- <View style={{ width: '100%', height: '100%' }}>
297
- <PerspectiveCamera makeDefault position={[0, 1, 3]} fov={50} />
298
- <Suspense fallback={null}>
299
- <ambientLight intensity={1} />
300
- <pointLight position={[5, 5, 5]} intensity={0.5} />
301
- <ModelPreview url={fullPath} onError={() => setError(true)} />
302
- <OrbitControls enableZoom={false} />
303
- </Suspense>
304
- </View>
305
- ) : null}
306
- </div>
307
- <div style={styles.bottomLabel}>
308
- {file.split('/').pop()}
309
- </div>
310
- </div>
311
- );
312
- }
313
-
314
- function ModelPreview({ url, onError }: { url: string; onError?: () => void }) {
315
- const [model, setModel] = useState<any>(null);
316
- const onErrorRef = useRef(onError);
317
- onErrorRef.current = onError;
318
-
319
- useEffect(() => {
320
- let cancelled = false;
321
- setModel(null);
322
-
323
- loadModel(url).then((result) => {
324
- if (cancelled) return;
325
- if (result.success && result.model) {
326
- setModel(result.model);
327
- } else {
328
- onErrorRef.current?.();
329
- }
330
- });
331
-
332
- return () => { cancelled = true; };
333
- }, [url]);
334
-
335
- if (!model) return null;
336
- return <primitive object={model} />;
337
- }
338
-
339
- interface SoundListViewerProps {
340
- files: string[];
341
- selected?: string;
342
- onSelect: (file: string) => void;
343
- basePath?: string;
344
- }
345
-
346
- export function SoundListViewer({ files, selected, onSelect, basePath = "" }: SoundListViewerProps) {
347
- return (
348
- <AssetListViewer
349
- files={files}
350
- selected={selected}
351
- onSelect={onSelect}
352
- renderCard={(file, onSelectHandler) => (
353
- <SoundCard file={file} basePath={basePath} onSelect={onSelectHandler} />
354
- )}
355
- />
356
- );
357
- }
358
-
359
- function SoundCard({ file, onSelect, basePath = "" }: { file: string; onSelect: (file: string) => void; basePath?: string }) {
360
- const fileName = file.split('/').pop() || '';
361
- const fullPath = basePath ? `/${basePath}${file}` : file;
362
- return (
363
- <div
364
- onClick={() => onSelect(file)}
365
- style={{ aspectRatio: '1 / 1', backgroundColor: '#374151', color: '#f9fafb', cursor: 'pointer', display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center' }}
366
- >
367
- <div style={styles.iconLarge}>🔊</div>
368
- <div style={{ color: '#f9fafb', fontSize: 12, padding: '0 4px', marginTop: 4, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', textAlign: 'center', width: '100%' }}>{fileName}</div>
369
- </div>
370
- );
371
- }
372
-
373
- // Single Asset Viewer Components - display only one selected asset
374
- export function SingleTextureViewer({ file, basePath = "" }: { file?: string; basePath?: string }) {
375
- if (!file) return null;
376
- return (
377
- <>
378
- <TextureCard file={file} basePath={basePath} onSelect={() => { }} />
379
- <SharedCanvas />
380
- </>
381
- );
382
- }
383
-
384
- export function SingleModelViewer({ file, basePath = "" }: { file?: string; basePath?: string }) {
385
- if (!file) return null;
386
- return (
387
- <>
388
- <ModelCard file={file} basePath={basePath} onSelect={() => { }} />
389
- <SharedCanvas />
390
- </>
391
- );
392
- }
393
-
394
- export function SingleSoundViewer({ file, basePath = "" }: { file?: string; basePath?: string }) {
395
- if (!file) return null;
396
- return <SoundCard file={file} basePath={basePath} onSelect={() => { }} />;
397
- }
398
-
399
- // Shared Canvas Component - can be used independently in any viewer
400
- export function SharedCanvas() {
401
- return (
402
- <Canvas
403
- shadows
404
- dpr={[1, 1.5]}
405
- gl={{ alpha: true }}
406
- camera={{ position: [0, 0, 3], fov: 45, near: 0.1, far: 1000 }}
407
- onCreated={({ gl }) => {
408
- gl.setClearAlpha(0);
409
- }}
410
- style={{
411
- position: 'fixed',
412
- top: 0,
413
- left: 0,
414
- width: '100vw',
415
- height: '100vh',
416
- pointerEvents: 'none',
417
- background: 'transparent',
418
- }}
419
- eventSource={typeof document !== 'undefined' ? document.getElementById('root') || undefined : undefined}
420
- eventPrefix="client"
421
- >
422
- <View.Port />
423
- </Canvas>
424
- );
425
- }