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.
@@ -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
@@ -0,0 +1,3 @@
1
+ [submodule "react-three-game-skill"]
2
+ path = react-three-game-skill
3
+ url = https://github.com/prnthh/react-three-game-skill
package/README.md CHANGED
@@ -9,6 +9,11 @@ npm i react-three-game @react-three/fiber @react-three/rapier three
9
9
  ![Prefab Editor](assets/editor.gif)
10
10
  ![Architecture](assets/architecture.png)
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 { GLTFExporter } from "three/examples/jsm/exporters/GLTFExporter.js";
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
- const exporter = new GLTFExporter();
106
- exporter.parse(sceneRoot, (result) => {
107
- const blob = new Blob([result], { type: 'application/octet-stream' });
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,3 @@
1
+ import { Component } from "./ComponentRegistry";
2
+ declare const AmbientLightComponent: Component;
3
+ export default AmbientLightComponent;
@@ -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
- return (_jsx("input", { type: "number", value: value, onChange: (e) => onChange(parseFloat(e.target.value)), step: step, min: min, max: max, style: Object.assign(Object.assign({}, styles.input), style) }));
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-three-game",
3
- "version": "0.0.46",
3
+ "version": "0.0.48",
4
4
  "description": "Batteries included React Three Fiber game engine",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.js",
@@ -0,0 +1,2 @@
1
+ # Auto detect text files and perform LF normalization
2
+ * text=auto
@@ -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';