react-three-game 0.0.46 → 0.0.48
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/.github/copilot-instructions.md +1 -1
- package/.gitmodules +3 -0
- package/README.md +5 -0
- package/dist/index.d.ts +1 -0
- package/dist/tools/prefabeditor/PrefabEditor.js +4 -13
- package/dist/tools/prefabeditor/components/AmbientLightComponent.d.ts +3 -0
- package/dist/tools/prefabeditor/components/AmbientLightComponent.js +23 -0
- package/dist/tools/prefabeditor/components/GeometryComponent.js +7 -0
- package/dist/tools/prefabeditor/components/Input.js +23 -1
- package/dist/tools/prefabeditor/components/index.js +2 -0
- package/dist/tools/prefabeditor/utils.d.ts +20 -0
- package/dist/tools/prefabeditor/utils.js +51 -0
- package/package.json +1 -1
- package/react-three-game-skill/.gitattributes +2 -0
- package/react-three-game-skill/README.md +1 -0
- package/react-three-game-skill/react-three-game/SKILL.md +394 -0
- package/src/index.ts +1 -0
- package/src/tools/prefabeditor/PrefabEditor.tsx +4 -19
- package/src/tools/prefabeditor/components/AmbientLightComponent.tsx +40 -0
- package/src/tools/prefabeditor/components/GeometryComponent.tsx +7 -0
- package/src/tools/prefabeditor/components/Input.tsx +32 -3
- package/src/tools/prefabeditor/components/index.ts +2 -0
- package/src/tools/prefabeditor/utils.ts +69 -0
- package/skill/SKILL.md +0 -491
- package/skill/package.json +0 -17
|
@@ -61,7 +61,7 @@ Use node materials only: `MeshStandardNodeMaterial`, `MeshBasicNodeMaterial` (no
|
|
|
61
61
|
Set `model.properties.instanced = true` → uses `InstanceProvider.tsx` for batched rendering with physics.
|
|
62
62
|
|
|
63
63
|
## Built-in Components
|
|
64
|
-
`Transform`, `Geometry` (box/sphere/plane), `Material` (color/texture), `Physics` (dynamic/fixed), `Model` (GLB/FBX), `SpotLight`, `DirectionalLight`
|
|
64
|
+
`Transform`, `Geometry` (box/sphere/plane/cylinder), `Material` (color/texture), `Physics` (dynamic/fixed), `Model` (GLB/FBX), `SpotLight`, `DirectionalLight`, `AmbientLight`, `Text`
|
|
65
65
|
|
|
66
66
|
## Custom Components (User-space)
|
|
67
67
|
See `docs/app/demo/editor/RotatorComponent.tsx` for runtime behavior example using `useFrame`. Register with `registerComponent()` before rendering `<PrefabEditor>`.
|
package/.gitmodules
ADDED
package/README.md
CHANGED
|
@@ -9,6 +9,11 @@ npm i react-three-game @react-three/fiber @react-three/rapier three
|
|
|
9
9
|

|
|
10
10
|

|
|
11
11
|
|
|
12
|
+
## Agent Skill
|
|
13
|
+
```bash
|
|
14
|
+
npx skills add https://github.com/prnthh/react-three-game-skill
|
|
15
|
+
```
|
|
16
|
+
|
|
12
17
|
## Usage
|
|
13
18
|
|
|
14
19
|
```jsx
|
package/dist/index.d.ts
CHANGED
|
@@ -6,6 +6,7 @@ export { default as PrefabRoot } from './tools/prefabeditor/PrefabRoot';
|
|
|
6
6
|
export { registerComponent } from './tools/prefabeditor/components/ComponentRegistry';
|
|
7
7
|
export { FieldRenderer, Input, Label, Vector3Input, ColorInput, StringInput, BooleanInput, SelectInput, } from './tools/prefabeditor/components/Input';
|
|
8
8
|
export * from './tools/prefabeditor/utils';
|
|
9
|
+
export type { ExportGLBOptions } from './tools/prefabeditor/utils';
|
|
9
10
|
export type { PrefabEditorRef } from './tools/prefabeditor/PrefabEditor';
|
|
10
11
|
export type { PrefabRootRef } from './tools/prefabeditor/PrefabRoot';
|
|
11
12
|
export type { Component } from './tools/prefabeditor/components/ComponentRegistry';
|
|
@@ -6,7 +6,7 @@ import { Physics } from "@react-three/rapier";
|
|
|
6
6
|
import EditorUI from "./EditorUI";
|
|
7
7
|
import { base, toolbar } from "./styles";
|
|
8
8
|
import { EditorContext } from "./EditorContext";
|
|
9
|
-
import {
|
|
9
|
+
import { exportGLB } from "./utils";
|
|
10
10
|
const DEFAULT_PREFAB = {
|
|
11
11
|
id: "prefab-default",
|
|
12
12
|
name: "New Prefab",
|
|
@@ -102,18 +102,9 @@ const PrefabEditor = forwardRef(({ basePath, initialPrefab, onPrefabChange, chil
|
|
|
102
102
|
const sceneRoot = (_a = prefabRootRef.current) === null || _a === void 0 ? void 0 : _a.root;
|
|
103
103
|
if (!sceneRoot)
|
|
104
104
|
return;
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
const url = URL.createObjectURL(blob);
|
|
109
|
-
const a = document.createElement('a');
|
|
110
|
-
a.href = url;
|
|
111
|
-
a.download = `${loadedPrefab.name || 'scene'}.glb`;
|
|
112
|
-
a.click();
|
|
113
|
-
URL.revokeObjectURL(url);
|
|
114
|
-
}, (error) => {
|
|
115
|
-
console.error('Error exporting GLB:', error);
|
|
116
|
-
}, { binary: true });
|
|
105
|
+
exportGLB(sceneRoot, {
|
|
106
|
+
filename: `${loadedPrefab.name || 'scene'}.glb`
|
|
107
|
+
});
|
|
117
108
|
};
|
|
118
109
|
useEffect(() => {
|
|
119
110
|
const canvas = document.querySelector('canvas');
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { FieldRenderer } from "./Input";
|
|
3
|
+
const ambientLightFields = [
|
|
4
|
+
{ name: 'color', type: 'color', label: 'Color' },
|
|
5
|
+
{ name: 'intensity', type: 'number', label: 'Intensity', step: 0.1, min: 0 },
|
|
6
|
+
];
|
|
7
|
+
function AmbientLightComponentEditor({ component, onUpdate, }) {
|
|
8
|
+
return (_jsx(FieldRenderer, { fields: ambientLightFields, values: component.properties, onChange: onUpdate }));
|
|
9
|
+
}
|
|
10
|
+
function AmbientLightComponentView({ properties }) {
|
|
11
|
+
const { color = '#ffffff', intensity = 1 } = properties;
|
|
12
|
+
return _jsx("ambientLight", { color: color, intensity: intensity });
|
|
13
|
+
}
|
|
14
|
+
const AmbientLightComponent = {
|
|
15
|
+
name: 'AmbientLight',
|
|
16
|
+
Editor: AmbientLightComponentEditor,
|
|
17
|
+
View: AmbientLightComponentView,
|
|
18
|
+
defaultProperties: {
|
|
19
|
+
color: '#ffffff',
|
|
20
|
+
intensity: 1,
|
|
21
|
+
},
|
|
22
|
+
};
|
|
23
|
+
export default AmbientLightComponent;
|
|
@@ -13,6 +13,10 @@ const GEOMETRY_ARGS = {
|
|
|
13
13
|
labels: ["Width", "Height"],
|
|
14
14
|
defaults: [1, 1],
|
|
15
15
|
},
|
|
16
|
+
cylinder: {
|
|
17
|
+
labels: ["Radius Top", "Radius Bottom", "Height", "Radial Segments"],
|
|
18
|
+
defaults: [1, 1, 1, 32],
|
|
19
|
+
},
|
|
16
20
|
};
|
|
17
21
|
function GeometryComponentEditor({ component, onUpdate, }) {
|
|
18
22
|
const { geometryType, args = [] } = component.properties;
|
|
@@ -26,6 +30,7 @@ function GeometryComponentEditor({ component, onUpdate, }) {
|
|
|
26
30
|
{ value: 'box', label: 'Box' },
|
|
27
31
|
{ value: 'sphere', label: 'Sphere' },
|
|
28
32
|
{ value: 'plane', label: 'Plane' },
|
|
33
|
+
{ value: 'cylinder', label: 'Cylinder' },
|
|
29
34
|
],
|
|
30
35
|
},
|
|
31
36
|
{
|
|
@@ -69,6 +74,8 @@ function GeometryComponentView({ properties, children }) {
|
|
|
69
74
|
return _jsx("sphereGeometry", { args: args });
|
|
70
75
|
case "plane":
|
|
71
76
|
return _jsx("planeGeometry", { args: args });
|
|
77
|
+
case "cylinder":
|
|
78
|
+
return _jsx("cylinderGeometry", { args: args });
|
|
72
79
|
default:
|
|
73
80
|
return _jsx("boxGeometry", { args: [1, 1, 1] });
|
|
74
81
|
}
|
|
@@ -25,7 +25,29 @@ const styles = {
|
|
|
25
25
|
},
|
|
26
26
|
};
|
|
27
27
|
export function Input({ value, onChange, step, min, max, style }) {
|
|
28
|
-
|
|
28
|
+
const [draft, setDraft] = useState(() => value.toString());
|
|
29
|
+
useEffect(() => {
|
|
30
|
+
setDraft(value.toString());
|
|
31
|
+
}, [value]);
|
|
32
|
+
const handleChange = (e) => {
|
|
33
|
+
const inputValue = e.target.value;
|
|
34
|
+
setDraft(inputValue);
|
|
35
|
+
const num = parseFloat(inputValue);
|
|
36
|
+
if (Number.isFinite(num)) {
|
|
37
|
+
onChange(num);
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
const handleBlur = () => {
|
|
41
|
+
const num = parseFloat(draft);
|
|
42
|
+
if (!Number.isFinite(num)) {
|
|
43
|
+
setDraft(value.toString());
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
return (_jsx("input", { type: "text", value: draft, onChange: handleChange, onBlur: handleBlur, onKeyDown: e => {
|
|
47
|
+
if (e.key === 'Enter') {
|
|
48
|
+
e.target.blur();
|
|
49
|
+
}
|
|
50
|
+
}, step: step, min: min, max: max, style: Object.assign(Object.assign({}, styles.input), style) }));
|
|
29
51
|
}
|
|
30
52
|
export function Label({ children }) {
|
|
31
53
|
return _jsx("label", { style: styles.label, children: children });
|
|
@@ -4,6 +4,7 @@ import MaterialComponent from './MaterialComponent';
|
|
|
4
4
|
import PhysicsComponent from './PhysicsComponent';
|
|
5
5
|
import SpotLightComponent from './SpotLightComponent';
|
|
6
6
|
import DirectionalLightComponent from './DirectionalLightComponent';
|
|
7
|
+
import AmbientLightComponent from './AmbientLightComponent';
|
|
7
8
|
import ModelComponent from './ModelComponent';
|
|
8
9
|
import TextComponent from './TextComponent';
|
|
9
10
|
export default [
|
|
@@ -13,6 +14,7 @@ export default [
|
|
|
13
14
|
PhysicsComponent,
|
|
14
15
|
SpotLightComponent,
|
|
15
16
|
DirectionalLightComponent,
|
|
17
|
+
AmbientLightComponent,
|
|
16
18
|
ModelComponent,
|
|
17
19
|
TextComponent
|
|
18
20
|
];
|
|
@@ -1,8 +1,28 @@
|
|
|
1
1
|
import { GameObject, Prefab } from "./types";
|
|
2
|
+
import { Object3D } from 'three';
|
|
3
|
+
export interface ExportGLBOptions {
|
|
4
|
+
filename?: string;
|
|
5
|
+
binary?: boolean;
|
|
6
|
+
onComplete?: (result: ArrayBuffer | object) => void;
|
|
7
|
+
onError?: (error: any) => void;
|
|
8
|
+
}
|
|
2
9
|
/** Save a prefab as JSON file */
|
|
3
10
|
export declare function saveJson(data: Prefab, filename: string): void;
|
|
4
11
|
/** Load a prefab from JSON file */
|
|
5
12
|
export declare function loadJson(): Promise<Prefab | undefined>;
|
|
13
|
+
/**
|
|
14
|
+
* Export a Three.js scene or object to GLB format
|
|
15
|
+
* @param sceneRoot - The Three.js Object3D to export
|
|
16
|
+
* @param options - Export options
|
|
17
|
+
* @returns Promise that resolves when export is complete
|
|
18
|
+
*/
|
|
19
|
+
export declare function exportGLB(sceneRoot: Object3D, options?: ExportGLBOptions): Promise<ArrayBuffer | object>;
|
|
20
|
+
/**
|
|
21
|
+
* Export a Three.js scene to GLB and return the ArrayBuffer without downloading
|
|
22
|
+
* @param sceneRoot - The Three.js Object3D to export
|
|
23
|
+
* @returns Promise that resolves with the GLB data as ArrayBuffer
|
|
24
|
+
*/
|
|
25
|
+
export declare function exportGLBData(sceneRoot: Object3D): Promise<ArrayBuffer>;
|
|
6
26
|
/** Find a node by ID in the tree */
|
|
7
27
|
export declare function findNode(root: GameObject, id: string): GameObject | null;
|
|
8
28
|
/** Find the parent of a node by ID */
|
|
@@ -1,3 +1,13 @@
|
|
|
1
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
2
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
3
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
4
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
5
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
6
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
7
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
8
|
+
});
|
|
9
|
+
};
|
|
10
|
+
import { GLTFExporter } from 'three/examples/jsm/exporters/GLTFExporter.js';
|
|
1
11
|
/** Save a prefab as JSON file */
|
|
2
12
|
export function saveJson(data, filename) {
|
|
3
13
|
const a = document.createElement('a');
|
|
@@ -34,6 +44,47 @@ export function loadJson() {
|
|
|
34
44
|
input.click();
|
|
35
45
|
});
|
|
36
46
|
}
|
|
47
|
+
/**
|
|
48
|
+
* Export a Three.js scene or object to GLB format
|
|
49
|
+
* @param sceneRoot - The Three.js Object3D to export
|
|
50
|
+
* @param options - Export options
|
|
51
|
+
* @returns Promise that resolves when export is complete
|
|
52
|
+
*/
|
|
53
|
+
export function exportGLB(sceneRoot, options = {}) {
|
|
54
|
+
const { filename = 'scene.glb', binary = true, onComplete, onError } = options;
|
|
55
|
+
return new Promise((resolve, reject) => {
|
|
56
|
+
const exporter = new GLTFExporter();
|
|
57
|
+
exporter.parse(sceneRoot, (result) => {
|
|
58
|
+
onComplete === null || onComplete === void 0 ? void 0 : onComplete(result);
|
|
59
|
+
resolve(result);
|
|
60
|
+
// Trigger download if filename is provided
|
|
61
|
+
if (filename) {
|
|
62
|
+
const blob = new Blob([result], { type: binary ? 'application/octet-stream' : 'application/json' });
|
|
63
|
+
const url = URL.createObjectURL(blob);
|
|
64
|
+
const a = document.createElement('a');
|
|
65
|
+
a.href = url;
|
|
66
|
+
a.download = filename;
|
|
67
|
+
a.click();
|
|
68
|
+
URL.revokeObjectURL(url);
|
|
69
|
+
}
|
|
70
|
+
}, (error) => {
|
|
71
|
+
console.error('Error exporting GLB:', error);
|
|
72
|
+
onError === null || onError === void 0 ? void 0 : onError(error);
|
|
73
|
+
reject(error);
|
|
74
|
+
}, { binary });
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Export a Three.js scene to GLB and return the ArrayBuffer without downloading
|
|
79
|
+
* @param sceneRoot - The Three.js Object3D to export
|
|
80
|
+
* @returns Promise that resolves with the GLB data as ArrayBuffer
|
|
81
|
+
*/
|
|
82
|
+
export function exportGLBData(sceneRoot) {
|
|
83
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
84
|
+
const result = yield exportGLB(sceneRoot, { filename: '', binary: true });
|
|
85
|
+
return result;
|
|
86
|
+
});
|
|
87
|
+
}
|
|
37
88
|
/** Find a node by ID in the tree */
|
|
38
89
|
export function findNode(root, id) {
|
|
39
90
|
var _a;
|
package/package.json
CHANGED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
Agent skill for [react-three-game](https://github.com/prnthh/react-three-game)
|
|
@@ -0,0 +1,394 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: react-three-game
|
|
3
|
+
description: react-three-game, a JSON-first 3D game engine built on React Three Fiber, WebGPU, and Rapier Physics.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# react-three-game
|
|
7
|
+
|
|
8
|
+
Instructions for the agent to follow when this skill is activated.
|
|
9
|
+
|
|
10
|
+
## When to use
|
|
11
|
+
|
|
12
|
+
generate 3D scenes, games and physics simulations in React.
|
|
13
|
+
|
|
14
|
+
## Agent Workflow: JSON → GLB
|
|
15
|
+
|
|
16
|
+
Agents can programmatically generate 3D assets:
|
|
17
|
+
|
|
18
|
+
1. Create a JSON prefab following the GameObject schema
|
|
19
|
+
2. Load it in `PrefabEditor` to render the Three.js scene
|
|
20
|
+
3. Export the scene to GLB format using `exportGLB` or `exportGLBData`
|
|
21
|
+
|
|
22
|
+
```tsx
|
|
23
|
+
import { useRef, useEffect } from 'react';
|
|
24
|
+
import { PrefabEditor, exportGLBData } from 'react-three-game';
|
|
25
|
+
import type { PrefabEditorRef } from 'react-three-game';
|
|
26
|
+
|
|
27
|
+
const jsonPrefab = {
|
|
28
|
+
root: {
|
|
29
|
+
id: "scene",
|
|
30
|
+
children: [
|
|
31
|
+
{
|
|
32
|
+
id: "cube",
|
|
33
|
+
components: {
|
|
34
|
+
transform: { type: "Transform", properties: { position: [0, 0, 0] } },
|
|
35
|
+
geometry: { type: "Geometry", properties: { geometryType: "box", args: [1, 1, 1] } },
|
|
36
|
+
material: { type: "Material", properties: { color: "#ff0000" } }
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
]
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
function AgentExporter() {
|
|
44
|
+
const editorRef = useRef<PrefabEditorRef>(null);
|
|
45
|
+
|
|
46
|
+
useEffect(() => {
|
|
47
|
+
const timer = setTimeout(async () => {
|
|
48
|
+
const sceneRoot = editorRef.current?.rootRef.current?.root;
|
|
49
|
+
if (!sceneRoot) return;
|
|
50
|
+
|
|
51
|
+
const glbData = await exportGLBData(sceneRoot);
|
|
52
|
+
// glbData is an ArrayBuffer ready for upload/storage
|
|
53
|
+
}, 1000); // Wait for scene to render
|
|
54
|
+
|
|
55
|
+
return () => clearTimeout(timer);
|
|
56
|
+
}, []);
|
|
57
|
+
|
|
58
|
+
return <PrefabEditor ref={editorRef} initialPrefab={jsonPrefab} />;
|
|
59
|
+
}
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## Core Concepts
|
|
63
|
+
|
|
64
|
+
### Asset Paths and Public Directory
|
|
65
|
+
|
|
66
|
+
**All asset paths are relative to `/public`** and omit the `/public` prefix:
|
|
67
|
+
|
|
68
|
+
```json
|
|
69
|
+
{
|
|
70
|
+
"texture": "/textures/floor.png",
|
|
71
|
+
"model": "/models/car.glb",
|
|
72
|
+
"font": "/fonts/font.ttf"
|
|
73
|
+
}
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Path `"/any/path/file.ext"` refers to `/public/any/path/file.ext`.
|
|
77
|
+
### GameObject Structure
|
|
78
|
+
|
|
79
|
+
Every game object follows this schema:
|
|
80
|
+
|
|
81
|
+
```typescript
|
|
82
|
+
interface GameObject {
|
|
83
|
+
id: string;
|
|
84
|
+
disabled?: boolean;
|
|
85
|
+
hidden?: boolean;
|
|
86
|
+
components?: Record<string, { type: string; properties: any }>;
|
|
87
|
+
children?: GameObject[];
|
|
88
|
+
}
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
### Prefab JSON Format
|
|
92
|
+
|
|
93
|
+
Scenes are defined as JSON prefabs with a root node containing children:
|
|
94
|
+
|
|
95
|
+
```json
|
|
96
|
+
{
|
|
97
|
+
"root": {
|
|
98
|
+
"id": "scene",
|
|
99
|
+
"children": [
|
|
100
|
+
{
|
|
101
|
+
"id": "my-object",
|
|
102
|
+
"components": {
|
|
103
|
+
"transform": { "type": "Transform", "properties": { "position": [0, 0, 0] } },
|
|
104
|
+
"geometry": { "type": "Geometry", "properties": { "geometryType": "box" } },
|
|
105
|
+
"material": { "type": "Material", "properties": { "color": "#ff0000" } }
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
]
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
## Built-in Components
|
|
114
|
+
|
|
115
|
+
| Component | Type | Key Properties |
|
|
116
|
+
|-----------|------|----------------|
|
|
117
|
+
| Transform | `Transform` | `position: [x,y,z]`, `rotation: [x,y,z]` (radians), `scale: [x,y,z]` |
|
|
118
|
+
| Geometry | `Geometry` | `geometryType`: box/sphere/plane/cylinder, `args`: dimension array |
|
|
119
|
+
| Material | `Material` | `color`, `texture?`, `metalness?`, `roughness?`, `repeat?`, `repeatCount?` |
|
|
120
|
+
| Physics | `Physics` | `type`: "dynamic" or "fixed" |
|
|
121
|
+
| Model | `Model` | `filename` (GLB/FBX path), `instanced?` for GPU batching |
|
|
122
|
+
| SpotLight | `SpotLight` | `color`, `intensity`, `angle`, `penumbra`, `distance?`, `castShadow?` |
|
|
123
|
+
| DirectionalLight | `DirectionalLight` | `color`, `intensity`, `castShadow?`, `targetOffset?: [x,y,z]` |
|
|
124
|
+
| AmbientLight | `AmbientLight` | `color`, `intensity` |
|
|
125
|
+
| Text | `Text` | `text`, `font`, `size`, `depth`, `width`, `align`, `color` |
|
|
126
|
+
|
|
127
|
+
### Text Component
|
|
128
|
+
|
|
129
|
+
Requires `hb.wasm` and a font file (TTF/WOFF) in `/public/fonts/`:
|
|
130
|
+
- hb.wasm: https://github.com/prnthh/react-three-game/raw/refs/heads/main/docs/public/fonts/hb.wasm
|
|
131
|
+
- Sample font: https://github.com/prnthh/react-three-game/raw/refs/heads/main/docs/public/fonts/NotoSans-Regular.ttf
|
|
132
|
+
|
|
133
|
+
Font property: `"font": "/fonts/NotoSans-Regular.ttf"`
|
|
134
|
+
|
|
135
|
+
### Geometry Args by Type
|
|
136
|
+
|
|
137
|
+
| geometryType | args array |
|
|
138
|
+
|--------------|------------|
|
|
139
|
+
| `box` | `[width, height, depth]` |
|
|
140
|
+
| `sphere` | `[radius, widthSegments, heightSegments]` |
|
|
141
|
+
| `plane` | `[width, height]` |
|
|
142
|
+
| `cylinder` | `[radiusTop, radiusBottom, height, radialSegments]` |
|
|
143
|
+
|
|
144
|
+
### Material Textures
|
|
145
|
+
|
|
146
|
+
```json
|
|
147
|
+
{
|
|
148
|
+
"material": {
|
|
149
|
+
"type": "Material",
|
|
150
|
+
"properties": {
|
|
151
|
+
"color": "white",
|
|
152
|
+
"texture": "/textures/floor.png",
|
|
153
|
+
"repeat": true,
|
|
154
|
+
"repeatCount": [4, 4]
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
### Rotations
|
|
161
|
+
|
|
162
|
+
Use radians: `1.57` = 90°, `3.14` = 180°, `-1.57` = -90°
|
|
163
|
+
|
|
164
|
+
## Common Patterns
|
|
165
|
+
|
|
166
|
+
### Usage Modes
|
|
167
|
+
|
|
168
|
+
**GameCanvas + PrefabRoot**: Production gameplay. Requires explicit `<Physics>` wrapper. Physics always active. Can compose with other R3F components. For headless mode, use `<PrefabRoot>` without GameCanvas.
|
|
169
|
+
|
|
170
|
+
```jsx
|
|
171
|
+
import { Physics } from '@react-three/rapier';
|
|
172
|
+
import { GameCanvas, PrefabRoot } from 'react-three-game';
|
|
173
|
+
|
|
174
|
+
<GameCanvas>
|
|
175
|
+
<Physics>
|
|
176
|
+
<PrefabRoot data={prefabData} />
|
|
177
|
+
</Physics>
|
|
178
|
+
</GameCanvas>
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
**PrefabEditor**: Level editors, scene authoring, prototyping. Includes canvas, physics, UI. Physics activates in play mode only.
|
|
182
|
+
|
|
183
|
+
```jsx
|
|
184
|
+
import { PrefabEditor } from 'react-three-game';
|
|
185
|
+
|
|
186
|
+
<PrefabEditor initialPrefab={prefabData} />
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
### Tree Utilities
|
|
190
|
+
|
|
191
|
+
```typescript
|
|
192
|
+
import { updateNodeById, findNode, deleteNode, cloneNode, exportGLBData } from 'react-three-game';
|
|
193
|
+
|
|
194
|
+
const updated = updateNodeById(root, nodeId, node => ({ ...node, disabled: true }));
|
|
195
|
+
const node = findNode(root, nodeId);
|
|
196
|
+
const afterDelete = deleteNode(root, nodeId);
|
|
197
|
+
const cloned = cloneNode(node);
|
|
198
|
+
const glbData = await exportGLBData(sceneRoot);
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
## Level Patterns
|
|
202
|
+
|
|
203
|
+
### Floor
|
|
204
|
+
|
|
205
|
+
```json
|
|
206
|
+
{
|
|
207
|
+
"id": "floor",
|
|
208
|
+
"components": {
|
|
209
|
+
"transform": { "type": "Transform", "properties": { "position": [0, -0.5, 0] } },
|
|
210
|
+
"geometry": { "type": "Geometry", "properties": { "geometryType": "box", "args": [40, 1, 40] } },
|
|
211
|
+
"material": { "type": "Material", "properties": { "texture": "/textures/floor.png", "repeat": true, "repeatCount": [20, 20] } },
|
|
212
|
+
"physics": { "type": "Physics", "properties": { "type": "fixed" } }
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
### Platform
|
|
218
|
+
|
|
219
|
+
```json
|
|
220
|
+
{
|
|
221
|
+
"id": "platform",
|
|
222
|
+
"components": {
|
|
223
|
+
"transform": { "type": "Transform", "properties": { "position": [-8, 2, -5] } },
|
|
224
|
+
"geometry": { "type": "Geometry", "properties": { "geometryType": "box", "args": [6, 0.5, 4] } },
|
|
225
|
+
"physics": { "type": "Physics", "properties": { "type": "fixed" } }
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
### Ramp
|
|
231
|
+
|
|
232
|
+
```json
|
|
233
|
+
{
|
|
234
|
+
"id": "ramp",
|
|
235
|
+
"components": {
|
|
236
|
+
"transform": { "type": "Transform", "properties": { "position": [-12, 1, -5], "rotation": [0, 0, 0.3] } },
|
|
237
|
+
"geometry": { "type": "Geometry", "properties": { "geometryType": "box", "args": [5, 0.3, 3] } },
|
|
238
|
+
"physics": { "type": "Physics", "properties": { "type": "fixed" } }
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
### Wall Pattern
|
|
244
|
+
### Wall
|
|
245
|
+
|
|
246
|
+
```json
|
|
247
|
+
{
|
|
248
|
+
"id": "wall",
|
|
249
|
+
"components": {
|
|
250
|
+
"transform": { "type": "Transform", "properties": { "position": [0, 3, -20] } },
|
|
251
|
+
"geometry": { "type": "Geometry", "properties": { "geometryType": "box", "args": [40, 7, 1] } },
|
|
252
|
+
"physics": { "type": "Physics", "properties": { "type": "fixed" } }
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
### Lighting
|
|
258
|
+
|
|
259
|
+
```json
|
|
260
|
+
[
|
|
261
|
+
{ "id": "spot", "components": { "transform": { "properties": { "position": [10, 15, 10] } }, "spotlight": { "type": "SpotLight", "properties": { "intensity": 200, "angle": 0.8, "castShadow": true } } } },
|
|
262
|
+
{ "id": "ambient", "components": { "ambientlight": { "type": "AmbientLight", "properties": { "intensity": 0.4 } } } }
|
|
263
|
+
]
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
### Text
|
|
267
|
+
|
|
268
|
+
```json
|
|
269
|
+
{
|
|
270
|
+
"id": "text",
|
|
271
|
+
"components": {
|
|
272
|
+
"transform": { "type": "Transform", "properties": { "position": [0, 3, 0] } },
|
|
273
|
+
"text": { "type": "Text", "properties": { "text": "Welcome", "font": "/fonts/font.ttf", "size": 1, "depth": 0.1 } }
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
### Model
|
|
279
|
+
|
|
280
|
+
```json
|
|
281
|
+
{
|
|
282
|
+
"id": "model",
|
|
283
|
+
"components": {
|
|
284
|
+
"transform": { "type": "Transform", "properties": { "position": [0, 0, 0], "scale": [1.5, 1.5, 1.5] } },
|
|
285
|
+
"model": { "type": "Model", "properties": { "filename": "/models/tree.glb" } }
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
## Editor
|
|
291
|
+
|
|
292
|
+
### Basic Usage
|
|
293
|
+
|
|
294
|
+
```jsx
|
|
295
|
+
import { PrefabEditor } from 'react-three-game';
|
|
296
|
+
|
|
297
|
+
<PrefabEditor initialPrefab={sceneData} onPrefabChange={setSceneData} />
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
Keyboard shortcuts: **T** (Translate), **R** (Rotate), **S** (Scale)
|
|
301
|
+
|
|
302
|
+
### Programmatic Updates
|
|
303
|
+
|
|
304
|
+
```jsx
|
|
305
|
+
import { useRef } from 'react';
|
|
306
|
+
import { PrefabEditor, updateNodeById } from 'react-three-game';
|
|
307
|
+
import type { PrefabEditorRef } from 'react-three-game';
|
|
308
|
+
|
|
309
|
+
function Scene() {
|
|
310
|
+
const editorRef = useRef<PrefabEditorRef>(null);
|
|
311
|
+
|
|
312
|
+
const moveBall = () => {
|
|
313
|
+
const prefab = editorRef.current!.prefab;
|
|
314
|
+
const newRoot = updateNodeById(prefab.root, "ball", node => ({
|
|
315
|
+
...node,
|
|
316
|
+
components: {
|
|
317
|
+
...node.components,
|
|
318
|
+
transform: {
|
|
319
|
+
...node.components!.transform!,
|
|
320
|
+
properties: { ...node.components!.transform!.properties, position: [5, 0, 0] }
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
}));
|
|
324
|
+
editorRef.current!.setPrefab({ ...prefab, root: newRoot });
|
|
325
|
+
};
|
|
326
|
+
|
|
327
|
+
return <PrefabEditor ref={editorRef} initialPrefab={sceneData} />;
|
|
328
|
+
}
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
**PrefabEditorRef**: `prefab`, `setPrefab()`, `screenshot()`, `exportGLB()`, `rootRef`
|
|
332
|
+
|
|
333
|
+
### GLB Export
|
|
334
|
+
|
|
335
|
+
```tsx
|
|
336
|
+
import { exportGLBData } from 'react-three-game';
|
|
337
|
+
|
|
338
|
+
const glbData = await exportGLBData(editorRef.current!.rootRef.current!.root);
|
|
339
|
+
```
|
|
340
|
+
|
|
341
|
+
### Runtime Animation
|
|
342
|
+
|
|
343
|
+
```tsx
|
|
344
|
+
import { useRef } from "react";
|
|
345
|
+
import { useFrame } from "@react-three/fiber";
|
|
346
|
+
import { PrefabEditor, updateNodeById } from "react-three-game";
|
|
347
|
+
|
|
348
|
+
function Animator({ editorRef }) {
|
|
349
|
+
useFrame(() => {
|
|
350
|
+
const prefab = editorRef.current!.prefab;
|
|
351
|
+
const newRoot = updateNodeById(prefab.root, "ball", node => ({
|
|
352
|
+
...node,
|
|
353
|
+
components: {
|
|
354
|
+
...node.components,
|
|
355
|
+
transform: {
|
|
356
|
+
...node.components!.transform!,
|
|
357
|
+
properties: { ...node.components!.transform!.properties, position: [x, y, z] }
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
}));
|
|
361
|
+
editorRef.current!.setPrefab({ ...prefab, root: newRoot });
|
|
362
|
+
});
|
|
363
|
+
return null;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
function Scene() {
|
|
367
|
+
const editorRef = useRef(null);
|
|
368
|
+
return (
|
|
369
|
+
<PrefabEditor ref={editorRef} initialPrefab={data}>
|
|
370
|
+
<Animator editorRef={editorRef} />
|
|
371
|
+
</PrefabEditor>
|
|
372
|
+
);
|
|
373
|
+
}
|
|
374
|
+
```
|
|
375
|
+
|
|
376
|
+
### Custom Component
|
|
377
|
+
|
|
378
|
+
```tsx
|
|
379
|
+
import { Component, registerComponent, FieldRenderer } from 'react-three-game';
|
|
380
|
+
|
|
381
|
+
const MyComponent: Component = {
|
|
382
|
+
name: 'MyComponent',
|
|
383
|
+
Editor: ({ component, onUpdate }) => (
|
|
384
|
+
<FieldRenderer fields={[{ name: 'speed', type: 'number', step: 0.1 }]} values={component.properties} onChange={onUpdate} />
|
|
385
|
+
),
|
|
386
|
+
View: ({ properties, children }) => <group>{children}</group>,
|
|
387
|
+
defaultProperties: { speed: 1 }
|
|
388
|
+
};
|
|
389
|
+
|
|
390
|
+
registerComponent(MyComponent);
|
|
391
|
+
```
|
|
392
|
+
|
|
393
|
+
**Field types**: `vector3`, `number`, `string`, `color`, `boolean`, `select`, `custom`
|
|
394
|
+
|
package/src/index.ts
CHANGED
|
@@ -26,6 +26,7 @@ export {
|
|
|
26
26
|
|
|
27
27
|
// Prefab Editor - Styles & Utils
|
|
28
28
|
export * from './tools/prefabeditor/utils';
|
|
29
|
+
export type { ExportGLBOptions } from './tools/prefabeditor/utils';
|
|
29
30
|
|
|
30
31
|
// Prefab Editor - Types
|
|
31
32
|
export type { PrefabEditorRef } from './tools/prefabeditor/PrefabEditor';
|