react-three-game 0.0.60 → 0.0.62

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 (66) hide show
  1. package/README.md +56 -0
  2. package/dist/index.d.ts +1 -1
  3. package/dist/shared/GameCanvas.d.ts +2 -1
  4. package/dist/shared/GameCanvas.js +7 -2
  5. package/dist/tools/prefabeditor/PrefabEditor.d.ts +18 -4
  6. package/dist/tools/prefabeditor/PrefabEditor.js +90 -36
  7. package/dist/tools/prefabeditor/utils.d.ts +2 -0
  8. package/dist/tools/prefabeditor/utils.js +15 -0
  9. package/package.json +9 -3
  10. package/.gitattributes +0 -2
  11. package/.github/copilot-instructions.md +0 -83
  12. package/.github/workflows/nextjs.yml +0 -99
  13. package/.gitmodules +0 -3
  14. package/assets/architecture.png +0 -0
  15. package/assets/editor.gif +0 -0
  16. package/assets/favicon.ico +0 -0
  17. package/assets/react-three-game-logo.png +0 -0
  18. package/dist/tools/dragdrop/page.d.ts +0 -1
  19. package/dist/tools/dragdrop/page.js +0 -11
  20. package/dist/tools/prefabeditor/EntityEvents.d.ts +0 -54
  21. package/dist/tools/prefabeditor/EntityEvents.js +0 -85
  22. package/dist/tools/prefabeditor/page.d.ts +0 -1
  23. package/dist/tools/prefabeditor/page.js +0 -5
  24. package/react-three-game-skill/.gitattributes +0 -2
  25. package/react-three-game-skill/README.md +0 -7
  26. package/react-three-game-skill/react-three-game/SKILL.md +0 -514
  27. package/react-three-game-skill/react-three-game/rules/ADVANCED_PHYSICS.md +0 -472
  28. package/react-three-game-skill/react-three-game/rules/LIGHTING.md +0 -6
  29. package/src/helpers/SoundManager.ts +0 -130
  30. package/src/helpers/index.ts +0 -91
  31. package/src/index.ts +0 -59
  32. package/src/shared/ContactShadow.tsx +0 -74
  33. package/src/shared/GameCanvas.tsx +0 -52
  34. package/src/tools/assetviewer/page.tsx +0 -425
  35. package/src/tools/dragdrop/DragDropLoader.tsx +0 -159
  36. package/src/tools/dragdrop/index.ts +0 -4
  37. package/src/tools/dragdrop/modelLoader.ts +0 -204
  38. package/src/tools/dragdrop/page.tsx +0 -45
  39. package/src/tools/prefabeditor/Dropdown.tsx +0 -112
  40. package/src/tools/prefabeditor/EditorContext.tsx +0 -25
  41. package/src/tools/prefabeditor/EditorTree.tsx +0 -452
  42. package/src/tools/prefabeditor/EditorTreeMenus.tsx +0 -307
  43. package/src/tools/prefabeditor/EditorUI.tsx +0 -204
  44. package/src/tools/prefabeditor/EventSystem.tsx +0 -36
  45. package/src/tools/prefabeditor/GameEvents.ts +0 -191
  46. package/src/tools/prefabeditor/InstanceProvider.tsx +0 -466
  47. package/src/tools/prefabeditor/PrefabEditor.tsx +0 -256
  48. package/src/tools/prefabeditor/PrefabRoot.tsx +0 -767
  49. package/src/tools/prefabeditor/components/AmbientLightComponent.tsx +0 -34
  50. package/src/tools/prefabeditor/components/CameraComponent.tsx +0 -117
  51. package/src/tools/prefabeditor/components/ComponentRegistry.ts +0 -40
  52. package/src/tools/prefabeditor/components/DirectionalLightComponent.tsx +0 -210
  53. package/src/tools/prefabeditor/components/EnvironmentComponent.tsx +0 -47
  54. package/src/tools/prefabeditor/components/GeometryComponent.tsx +0 -133
  55. package/src/tools/prefabeditor/components/Input.tsx +0 -820
  56. package/src/tools/prefabeditor/components/MaterialComponent.tsx +0 -431
  57. package/src/tools/prefabeditor/components/ModelComponent.tsx +0 -176
  58. package/src/tools/prefabeditor/components/PhysicsComponent.tsx +0 -188
  59. package/src/tools/prefabeditor/components/SpotLightComponent.tsx +0 -109
  60. package/src/tools/prefabeditor/components/TextComponent.tsx +0 -137
  61. package/src/tools/prefabeditor/components/TransformComponent.tsx +0 -173
  62. package/src/tools/prefabeditor/components/index.ts +0 -26
  63. package/src/tools/prefabeditor/page.tsx +0 -10
  64. package/src/tools/prefabeditor/styles.ts +0 -235
  65. package/src/tools/prefabeditor/types.ts +0 -20
  66. 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
- }