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