react-three-game 0.0.6 → 0.0.8
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/README.md +2 -0
- package/assets/editor.gif +0 -0
- package/dist/tools/prefabeditor/PrefabRoot.js +5 -3
- package/package.json +2 -2
- package/src/index.ts +15 -0
- package/src/shared/GameCanvas.tsx +48 -0
- package/src/tools/assetviewer/page.tsx +411 -0
- package/src/tools/dragdrop/DragDropLoader.tsx +105 -0
- package/src/tools/dragdrop/modelLoader.ts +65 -0
- package/src/tools/dragdrop/page.tsx +42 -0
- package/src/tools/prefabeditor/EditorTree.tsx +277 -0
- package/src/tools/prefabeditor/EditorUI.tsx +273 -0
- package/src/tools/prefabeditor/EventSystem.tsx +36 -0
- package/src/tools/prefabeditor/InstanceProvider.tsx +326 -0
- package/src/tools/prefabeditor/PrefabEditor.tsx +130 -0
- package/src/tools/prefabeditor/PrefabRoot.tsx +460 -0
- package/src/tools/prefabeditor/components/ComponentRegistry.ts +26 -0
- package/src/tools/prefabeditor/components/GeometryComponent.tsx +43 -0
- package/src/tools/prefabeditor/components/MaterialComponent.tsx +153 -0
- package/src/tools/prefabeditor/components/ModelComponent.tsx +68 -0
- package/src/tools/prefabeditor/components/PhysicsComponent.tsx +47 -0
- package/src/tools/prefabeditor/components/SpotLightComponent.tsx +53 -0
- package/src/tools/prefabeditor/components/TransformComponent.tsx +49 -0
- package/src/tools/prefabeditor/components/index.ts +16 -0
- package/src/tools/prefabeditor/page.tsx +10 -0
- package/src/tools/prefabeditor/types.ts +28 -0
- package/tsconfig.json +17 -17
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { ModelListViewer } from '../../assetviewer/page';
|
|
2
|
+
import { useEffect, useState } from 'react';
|
|
3
|
+
import { Component } from './ComponentRegistry';
|
|
4
|
+
|
|
5
|
+
function ModelComponentEditor({ component, onUpdate, basePath = "" }: { component: any; onUpdate: (newComp: any) => void; basePath?: string }) {
|
|
6
|
+
const [modelFiles, setModelFiles] = useState<string[]>([]);
|
|
7
|
+
|
|
8
|
+
useEffect(() => {
|
|
9
|
+
const base = basePath ? `${basePath}/` : '';
|
|
10
|
+
fetch(`/${base}models/manifest.json`)
|
|
11
|
+
.then(r => r.json())
|
|
12
|
+
.then(data => setModelFiles(Array.isArray(data) ? data : data.files || []))
|
|
13
|
+
.catch(console.error);
|
|
14
|
+
}, [basePath]);
|
|
15
|
+
|
|
16
|
+
const handleModelSelect = (file: string) => {
|
|
17
|
+
// Remove leading slash for prefab compatibility
|
|
18
|
+
const filename = file.startsWith('/') ? file.slice(1) : file;
|
|
19
|
+
onUpdate({ 'filename': filename });
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
return <div>
|
|
23
|
+
<div className="mb-1">
|
|
24
|
+
<label className="block text-[9px] text-cyan-400/60 uppercase tracking-wider mb-0.5">Model</label>
|
|
25
|
+
<div className="max-h-32 overflow-y-auto">
|
|
26
|
+
<ModelListViewer
|
|
27
|
+
files={modelFiles}
|
|
28
|
+
selected={component.properties.filename ? `/${component.properties.filename}` : undefined}
|
|
29
|
+
onSelect={handleModelSelect}
|
|
30
|
+
basePath={basePath}
|
|
31
|
+
/>
|
|
32
|
+
</div>
|
|
33
|
+
</div>
|
|
34
|
+
<div className="flex items-center gap-1">
|
|
35
|
+
<input
|
|
36
|
+
type="checkbox"
|
|
37
|
+
id="instanced-checkbox"
|
|
38
|
+
checked={component.properties.instanced || false}
|
|
39
|
+
onChange={e => onUpdate({ 'instanced': e.target.checked })}
|
|
40
|
+
className="w-3 h-3"
|
|
41
|
+
/>
|
|
42
|
+
<label htmlFor="instanced-checkbox" className="text-[9px] text-cyan-400/60">Instanced</label>
|
|
43
|
+
</div>
|
|
44
|
+
</div>;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// View for Model component
|
|
48
|
+
function ModelComponentView({ properties, loadedModels, children }: { properties: any, loadedModels?: Record<string, any>, children?: React.ReactNode }) {
|
|
49
|
+
// Instanced models are handled elsewhere (GameInstance), so only render non-instanced here
|
|
50
|
+
if (!properties.filename || properties.instanced) return children || null;
|
|
51
|
+
if (loadedModels && loadedModels[properties.filename]) {
|
|
52
|
+
return <>{<primitive object={loadedModels[properties.filename].clone()} />}{children}</>;
|
|
53
|
+
}
|
|
54
|
+
// Optionally, render a placeholder if model is not loaded
|
|
55
|
+
return children || null;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const ModelComponent: Component = {
|
|
59
|
+
name: 'Model',
|
|
60
|
+
Editor: ModelComponentEditor,
|
|
61
|
+
View: ModelComponentView,
|
|
62
|
+
defaultProperties: {
|
|
63
|
+
filename: '',
|
|
64
|
+
instanced: false
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
export default ModelComponent;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { Component } from "./ComponentRegistry";
|
|
2
|
+
|
|
3
|
+
function PhysicsComponentEditor({ component, onUpdate }: { component: any; onUpdate: (newComp: any) => void }) {
|
|
4
|
+
return <div>
|
|
5
|
+
<label className="block text-[9px] text-cyan-400/60 uppercase tracking-wider mb-0.5">Type</label>
|
|
6
|
+
<select
|
|
7
|
+
className="w-full bg-black/40 border border-cyan-500/30 px-1 py-0.5 text-[10px] text-cyan-300 font-mono focus:outline-none focus:border-cyan-400/50"
|
|
8
|
+
value={component.properties.type}
|
|
9
|
+
onChange={e => onUpdate({ type: e.target.value })}
|
|
10
|
+
>
|
|
11
|
+
<option value="dynamic">Dynamic</option>
|
|
12
|
+
<option value="fixed">Fixed</option>
|
|
13
|
+
</select>
|
|
14
|
+
</div>;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
import { RigidBody } from "@react-three/rapier";
|
|
19
|
+
import { Object3D } from "three";
|
|
20
|
+
import { useRef } from "react";
|
|
21
|
+
|
|
22
|
+
function PhysicsComponentView({ properties, children, registerRef, transform, editMode }: any) {
|
|
23
|
+
if (editMode) return children;
|
|
24
|
+
return (
|
|
25
|
+
<RigidBody
|
|
26
|
+
ref={el => registerRef && registerRef(properties.id, el as unknown as Object3D)}
|
|
27
|
+
position={transform?.position}
|
|
28
|
+
rotation={transform?.rotation}
|
|
29
|
+
scale={transform?.scale}
|
|
30
|
+
type={properties.type}
|
|
31
|
+
colliders="cuboid"
|
|
32
|
+
>
|
|
33
|
+
{children}
|
|
34
|
+
</RigidBody>
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const PhysicsComponent: Component = {
|
|
39
|
+
name: 'Physics',
|
|
40
|
+
Editor: PhysicsComponentEditor,
|
|
41
|
+
View: PhysicsComponentView,
|
|
42
|
+
defaultProperties: {
|
|
43
|
+
type: 'dynamic'
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
export default PhysicsComponent;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
|
|
2
|
+
import { Component } from "./ComponentRegistry";
|
|
3
|
+
|
|
4
|
+
function SpotLightComponentEditor({ component, onUpdate }: { component: any; onUpdate: (newComp: any) => void }) {
|
|
5
|
+
return <div className="flex flex-col">
|
|
6
|
+
<div className="mb-1">
|
|
7
|
+
<label className="block text-[9px] text-cyan-400/60 uppercase tracking-wider mb-0.5">Color</label>
|
|
8
|
+
<div className="flex gap-0.5">
|
|
9
|
+
<input
|
|
10
|
+
type="color"
|
|
11
|
+
className="h-5 w-5 bg-transparent border-none cursor-pointer"
|
|
12
|
+
value={component.properties.color}
|
|
13
|
+
onChange={e => onUpdate({ 'color': e.target.value })}
|
|
14
|
+
/>
|
|
15
|
+
<input
|
|
16
|
+
type="text"
|
|
17
|
+
className="flex-1 bg-black/40 border border-cyan-500/30 px-1 py-0.5 text-[10px] text-cyan-300 font-mono focus:outline-none focus:border-cyan-400/50"
|
|
18
|
+
value={component.properties.color}
|
|
19
|
+
onChange={e => onUpdate({ 'color': e.target.value })}
|
|
20
|
+
/>
|
|
21
|
+
</div>
|
|
22
|
+
</div>
|
|
23
|
+
<div>
|
|
24
|
+
<label className="block text-[9px] text-cyan-400/60 uppercase tracking-wider mb-0.5">Intensity</label>
|
|
25
|
+
<input
|
|
26
|
+
type="number"
|
|
27
|
+
step="0.1"
|
|
28
|
+
className="w-full bg-black/40 border border-cyan-500/30 px-1 py-0.5 text-[10px] text-cyan-300 font-mono focus:outline-none focus:border-cyan-400/50"
|
|
29
|
+
value={component.properties.intensity}
|
|
30
|
+
onChange={e => onUpdate({ 'intensity': parseFloat(e.target.value) })}
|
|
31
|
+
/>
|
|
32
|
+
</div>
|
|
33
|
+
</div>;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
// The view component for SpotLight
|
|
38
|
+
function SpotLightView({ properties }: { properties: any }) {
|
|
39
|
+
// You can expand this with more spotlight properties as needed
|
|
40
|
+
return <spotLight color={properties.color} intensity={properties.intensity} />;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const SpotLightComponent: Component = {
|
|
44
|
+
name: 'SpotLight',
|
|
45
|
+
Editor: SpotLightComponentEditor,
|
|
46
|
+
View: SpotLightView,
|
|
47
|
+
defaultProperties: {
|
|
48
|
+
color: '#ffffff',
|
|
49
|
+
intensity: 1.0
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
export default SpotLightComponent;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
|
|
2
|
+
import { Component } from "./ComponentRegistry";
|
|
3
|
+
|
|
4
|
+
function TransformComponentEditor({ component, onUpdate }: { component: any; onUpdate: (newComp: any) => void }) {
|
|
5
|
+
return <div className="flex flex-col">
|
|
6
|
+
<Vector3Input label="Position" value={component.properties.position} onChange={v => onUpdate({ position: v })} />
|
|
7
|
+
<Vector3Input label="Rotation" value={component.properties.rotation} onChange={v => onUpdate({ rotation: v })} />
|
|
8
|
+
<Vector3Input label="Scale" value={component.properties.scale} onChange={v => onUpdate({ scale: v })} />
|
|
9
|
+
</div>;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const TransformComponent: Component = {
|
|
13
|
+
name: 'Transform',
|
|
14
|
+
Editor: TransformComponentEditor,
|
|
15
|
+
defaultProperties: {
|
|
16
|
+
position: [0, 0, 0],
|
|
17
|
+
rotation: [0, 0, 0],
|
|
18
|
+
scale: [1, 1, 1]
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export default TransformComponent;
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
export function Vector3Input({ label, value, onChange }: { label: string, value: [number, number, number], onChange: (v: [number, number, number]) => void }) {
|
|
26
|
+
const handleChange = (index: number, val: string) => {
|
|
27
|
+
const newValue = [...value] as [number, number, number];
|
|
28
|
+
newValue[index] = parseFloat(val) || 0;
|
|
29
|
+
onChange(newValue);
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
return <div className="mb-1">
|
|
33
|
+
<label className="block text-[9px] text-cyan-400/60 uppercase tracking-wider mb-0.5">{label}</label>
|
|
34
|
+
<div className="flex gap-0.5">
|
|
35
|
+
<div className="relative flex-1">
|
|
36
|
+
<span className="absolute left-0.5 top-0 text-[8px] text-red-400/80 font-mono">X</span>
|
|
37
|
+
<input className="w-full bg-black/40 border border-cyan-500/30 pl-3 pr-0.5 py-0.5 text-[10px] text-cyan-300 font-mono focus:outline-none focus:border-cyan-400/50" type="number" step="0.1" value={value[0]} onChange={e => handleChange(0, e.target.value)} />
|
|
38
|
+
</div>
|
|
39
|
+
<div className="relative flex-1">
|
|
40
|
+
<span className="absolute left-0.5 top-0 text-[8px] text-green-400/80 font-mono">Y</span>
|
|
41
|
+
<input className="w-full bg-black/40 border border-cyan-500/30 pl-3 pr-0.5 py-0.5 text-[10px] text-cyan-300 font-mono focus:outline-none focus:border-cyan-400/50" type="number" step="0.1" value={value[1]} onChange={e => handleChange(1, e.target.value)} />
|
|
42
|
+
</div>
|
|
43
|
+
<div className="relative flex-1">
|
|
44
|
+
<span className="absolute left-0.5 top-0 text-[8px] text-blue-400/80 font-mono">Z</span>
|
|
45
|
+
<input className="w-full bg-black/40 border border-cyan-500/30 pl-3 pr-0.5 py-0.5 text-[10px] text-cyan-300 font-mono focus:outline-none focus:border-cyan-400/50" type="number" step="0.1" value={value[2]} onChange={e => handleChange(2, e.target.value)} />
|
|
46
|
+
</div>
|
|
47
|
+
</div>
|
|
48
|
+
</div>
|
|
49
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import GeometryComponent from './GeometryComponent';
|
|
2
|
+
import TransformComponent from './TransformComponent';
|
|
3
|
+
import MaterialComponent from './MaterialComponent';
|
|
4
|
+
import PhysicsComponent from './PhysicsComponent';
|
|
5
|
+
import SpotLightComponent from './SpotLightComponent';
|
|
6
|
+
import ModelComponent from './ModelComponent';
|
|
7
|
+
|
|
8
|
+
export default [
|
|
9
|
+
GeometryComponent,
|
|
10
|
+
TransformComponent,
|
|
11
|
+
MaterialComponent,
|
|
12
|
+
PhysicsComponent,
|
|
13
|
+
SpotLightComponent,
|
|
14
|
+
ModelComponent
|
|
15
|
+
];
|
|
16
|
+
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { ThreeElements } from "@react-three/fiber"
|
|
2
|
+
|
|
3
|
+
export interface Prefab {
|
|
4
|
+
id: string;
|
|
5
|
+
name: string;
|
|
6
|
+
description?: string;
|
|
7
|
+
author?: string;
|
|
8
|
+
version?: string;
|
|
9
|
+
assets?: string[] | {[assetName: string]: string}; // List of asset URLs or a mapping of asset names to URLs
|
|
10
|
+
onStart?: (target: any) => void; // The logic function to run when the map starts
|
|
11
|
+
root: GameObject; // The root node of the scene graph
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface GameObject {
|
|
15
|
+
id: string;
|
|
16
|
+
enabled: boolean;
|
|
17
|
+
visible: boolean;
|
|
18
|
+
ref?: any;
|
|
19
|
+
children?: GameObject[];
|
|
20
|
+
components?: {
|
|
21
|
+
[uuid: string]: Component | undefined;
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
interface Component {
|
|
26
|
+
type: string;
|
|
27
|
+
properties: { [key: string]: any };
|
|
28
|
+
}
|
package/tsconfig.json
CHANGED
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES6",
|
|
4
|
+
"lib": ["ES2017", "DOM"],
|
|
5
|
+
"module": "ESNext",
|
|
6
|
+
"jsx": "react-jsx",
|
|
7
|
+
"declaration": true,
|
|
8
|
+
"declarationDir": "dist",
|
|
9
|
+
"outDir": "dist",
|
|
10
|
+
"strict": true,
|
|
11
|
+
"esModuleInterop": true,
|
|
12
|
+
"skipLibCheck": true,
|
|
13
|
+
"moduleResolution": "bundler",
|
|
14
|
+
"allowSyntheticDefaultImports": true
|
|
15
|
+
},
|
|
16
|
+
"include": ["src"],
|
|
17
|
+
"exclude": ["node_modules", "dist"]
|
|
18
|
+
}
|