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.
- package/dist/tools/dragdrop/DragDropLoader.d.ts +8 -8
- package/dist/tools/dragdrop/DragDropLoader.js +33 -15
- package/dist/tools/dragdrop/index.d.ts +3 -3
- package/dist/tools/dragdrop/index.js +1 -1
- package/dist/tools/dragdrop/modelLoader.d.ts +10 -1
- package/dist/tools/dragdrop/modelLoader.js +39 -0
- package/dist/tools/prefabeditor/PrefabEditor.js +17 -26
- package/dist/tools/prefabeditor/PrefabRoot.d.ts +1 -1
- package/dist/tools/prefabeditor/PrefabRoot.js +2 -8
- package/package.json +9 -3
- package/.gitattributes +0 -2
- package/.github/copilot-instructions.md +0 -83
- package/.github/workflows/nextjs.yml +0 -99
- package/.gitmodules +0 -3
- package/assets/architecture.png +0 -0
- package/assets/editor.gif +0 -0
- package/assets/favicon.ico +0 -0
- package/assets/react-three-game-logo.png +0 -0
- package/dist/tools/dragdrop/page.d.ts +0 -1
- package/dist/tools/dragdrop/page.js +0 -11
- package/dist/tools/prefabeditor/EntityEvents.d.ts +0 -54
- package/dist/tools/prefabeditor/EntityEvents.js +0 -85
- package/dist/tools/prefabeditor/page.d.ts +0 -1
- package/dist/tools/prefabeditor/page.js +0 -5
- package/react-three-game-skill/.gitattributes +0 -2
- package/react-three-game-skill/README.md +0 -7
- package/react-three-game-skill/react-three-game/SKILL.md +0 -514
- package/react-three-game-skill/react-three-game/rules/ADVANCED_PHYSICS.md +0 -472
- package/react-three-game-skill/react-three-game/rules/LIGHTING.md +0 -6
- package/src/helpers/SoundManager.ts +0 -130
- package/src/helpers/index.ts +0 -91
- package/src/index.ts +0 -59
- package/src/shared/ContactShadow.tsx +0 -74
- package/src/shared/GameCanvas.tsx +0 -52
- package/src/tools/assetviewer/page.tsx +0 -425
- package/src/tools/dragdrop/DragDropLoader.tsx +0 -136
- package/src/tools/dragdrop/index.ts +0 -4
- package/src/tools/dragdrop/modelLoader.ts +0 -145
- package/src/tools/dragdrop/page.tsx +0 -45
- package/src/tools/prefabeditor/Dropdown.tsx +0 -112
- package/src/tools/prefabeditor/EditorContext.tsx +0 -25
- package/src/tools/prefabeditor/EditorTree.tsx +0 -452
- package/src/tools/prefabeditor/EditorTreeMenus.tsx +0 -307
- package/src/tools/prefabeditor/EditorUI.tsx +0 -204
- package/src/tools/prefabeditor/EventSystem.tsx +0 -36
- package/src/tools/prefabeditor/GameEvents.ts +0 -191
- package/src/tools/prefabeditor/InstanceProvider.tsx +0 -466
- package/src/tools/prefabeditor/PrefabEditor.tsx +0 -262
- package/src/tools/prefabeditor/PrefabRoot.tsx +0 -773
- package/src/tools/prefabeditor/components/AmbientLightComponent.tsx +0 -34
- package/src/tools/prefabeditor/components/CameraComponent.tsx +0 -117
- package/src/tools/prefabeditor/components/ComponentRegistry.ts +0 -40
- package/src/tools/prefabeditor/components/DirectionalLightComponent.tsx +0 -210
- package/src/tools/prefabeditor/components/EnvironmentComponent.tsx +0 -47
- package/src/tools/prefabeditor/components/GeometryComponent.tsx +0 -133
- package/src/tools/prefabeditor/components/Input.tsx +0 -820
- package/src/tools/prefabeditor/components/MaterialComponent.tsx +0 -431
- package/src/tools/prefabeditor/components/ModelComponent.tsx +0 -176
- package/src/tools/prefabeditor/components/PhysicsComponent.tsx +0 -188
- package/src/tools/prefabeditor/components/SpotLightComponent.tsx +0 -109
- package/src/tools/prefabeditor/components/TextComponent.tsx +0 -137
- package/src/tools/prefabeditor/components/TransformComponent.tsx +0 -173
- package/src/tools/prefabeditor/components/index.ts +0 -26
- package/src/tools/prefabeditor/page.tsx +0 -10
- package/src/tools/prefabeditor/styles.ts +0 -235
- package/src/tools/prefabeditor/types.ts +0 -20
- 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
|
-
}
|