react-three-game 0.0.47 → 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>`.
@@ -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
  ];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-three-game",
3
- "version": "0.0.47",
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 @@
1
+ Agent skill for [react-three-game](https://github.com/prnthh/react-three-game)
@@ -61,6 +61,19 @@ function AgentExporter() {
61
61
 
62
62
  ## Core Concepts
63
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`.
64
77
  ### GameObject Structure
65
78
 
66
79
  Every game object follows this schema:
@@ -108,8 +121,17 @@ Scenes are defined as JSON prefabs with a root node containing children:
108
121
  | Model | `Model` | `filename` (GLB/FBX path), `instanced?` for GPU batching |
109
122
  | SpotLight | `SpotLight` | `color`, `intensity`, `angle`, `penumbra`, `distance?`, `castShadow?` |
110
123
  | DirectionalLight | `DirectionalLight` | `color`, `intensity`, `castShadow?`, `targetOffset?: [x,y,z]` |
124
+ | AmbientLight | `AmbientLight` | `color`, `intensity` |
111
125
  | Text | `Text` | `text`, `font`, `size`, `depth`, `width`, `align`, `color` |
112
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
+
113
135
  ### Geometry Args by Type
114
136
 
115
137
  | geometryType | args array |
@@ -119,41 +141,31 @@ Scenes are defined as JSON prefabs with a root node containing children:
119
141
  | `plane` | `[width, height]` |
120
142
  | `cylinder` | `[radiusTop, radiusBottom, height, radialSegments]` |
121
143
 
122
- ### Material Texture Options
144
+ ### Material Textures
123
145
 
124
146
  ```json
125
147
  {
126
- "type": "Material",
127
- "properties": {
128
- "color": "white",
129
- "texture": "/textures/path/to/texture.png",
130
- "repeat": true,
131
- "repeatCount": [4, 4]
148
+ "material": {
149
+ "type": "Material",
150
+ "properties": {
151
+ "color": "white",
152
+ "texture": "/textures/floor.png",
153
+ "repeat": true,
154
+ "repeatCount": [4, 4]
155
+ }
132
156
  }
133
157
  }
134
158
  ```
135
159
 
136
- - Use `"color": "white"` with textures for accurate texture colors
137
- - `repeatCount: [x, y]` tiles the texture; match to geometry dimensions for proper scaling
138
-
139
- ### Rotation Reference
160
+ ### Rotations
140
161
 
141
- Rotations use radians. Common values:
142
- - `1.57` = 90° (π/2)
143
- - `3.14` = 180° (π)
144
- - `-1.57` = -90° (rotate plane flat: `rotation: [-1.57, 0, 0]`)
162
+ Use radians: `1.57` = 90°, `3.14` = 180°, `-1.57` = -90°
145
163
 
146
164
  ## Common Patterns
147
165
 
148
166
  ### Usage Modes
149
167
 
150
- The library supports two modes:
151
-
152
- **Play Mode** (default) - Immediate rendering without any editor UI. Use `GameCanvas` with `PrefabRoot` for a clean game experience.
153
-
154
- **Editor Mode** - Visual GUI using `PrefabEditor` for scene inspection and custom component development. See the [Editor Mode](#editor-mode) section at the end of this document.
155
-
156
- ### Basic Scene Setup (Play Mode)
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.
157
169
 
158
170
  ```jsx
159
171
  import { Physics } from '@react-three/rapier';
@@ -166,83 +178,56 @@ import { GameCanvas, PrefabRoot } from 'react-three-game';
166
178
  </GameCanvas>
167
179
  ```
168
180
 
169
- ### Tree Manipulation Utilities
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
170
190
 
171
191
  ```typescript
172
- import { findNode, updateNode, updateNodeById, deleteNode, cloneNode, saveJson, loadJson, exportGLB, exportGLBData } from 'react-three-game';
192
+ import { updateNodeById, findNode, deleteNode, cloneNode, exportGLBData } from 'react-three-game';
173
193
 
174
- // Update a node by ID (optimized - avoids unnecessary object creation)
175
194
  const updated = updateNodeById(root, nodeId, node => ({ ...node, disabled: true }));
176
-
177
- // Find a node
178
195
  const node = findNode(root, nodeId);
179
-
180
- // Delete a node
181
196
  const afterDelete = deleteNode(root, nodeId);
182
-
183
- // Clone a node
184
197
  const cloned = cloneNode(node);
185
-
186
- // Save/load JSON
187
- saveJson(prefab, 'my-scene');
188
- const loaded = await loadJson();
189
-
190
- // Export to GLB
191
- await exportGLB(sceneRoot, { filename: 'my-scene.glb' });
192
- const glbData = await exportGLBData(sceneRoot); // Returns ArrayBuffer
198
+ const glbData = await exportGLBData(sceneRoot);
193
199
  ```
194
200
 
195
- ## Building Game Levels
201
+ ## Level Patterns
196
202
 
197
- ### Complete Prefab Structure
203
+ ### Floor
198
204
 
199
205
  ```json
200
206
  {
201
- "id": "level-id",
202
- "name": "Level Name",
203
- "root": {
204
- "id": "root",
205
- "enabled": true,
206
- "visible": true,
207
- "components": { ... },
208
- "children": [ ... ]
209
- }
210
- }
211
- ```
212
-
213
- ### Floor/Ground Pattern
214
-
215
- ```json
216
- {
217
- "id": "main-floor",
207
+ "id": "floor",
218
208
  "components": {
219
209
  "transform": { "type": "Transform", "properties": { "position": [0, -0.5, 0] } },
220
210
  "geometry": { "type": "Geometry", "properties": { "geometryType": "box", "args": [40, 1, 40] } },
221
- "material": { "type": "Material", "properties": { "color": "white", "texture": "/textures/GreyboxTextures/greybox_dark_grid.png", "repeat": true, "repeatCount": [20, 20] } },
211
+ "material": { "type": "Material", "properties": { "texture": "/textures/floor.png", "repeat": true, "repeatCount": [20, 20] } },
222
212
  "physics": { "type": "Physics", "properties": { "type": "fixed" } }
223
213
  }
224
214
  }
225
215
  ```
226
216
 
227
- ### Platform Pattern
228
-
229
- Floating platforms use "fixed" physics and smaller box geometry:
217
+ ### Platform
230
218
 
231
219
  ```json
232
220
  {
233
- "id": "platform-1",
221
+ "id": "platform",
234
222
  "components": {
235
223
  "transform": { "type": "Transform", "properties": { "position": [-8, 2, -5] } },
236
224
  "geometry": { "type": "Geometry", "properties": { "geometryType": "box", "args": [6, 0.5, 4] } },
237
- "material": { "type": "Material", "properties": { "color": "white", "texture": "/textures/GreyboxTextures/greybox_teal_grid.png", "repeat": true, "repeatCount": [3, 2] } },
238
225
  "physics": { "type": "Physics", "properties": { "type": "fixed" } }
239
226
  }
240
227
  }
241
228
  ```
242
229
 
243
- ### Ramp Pattern
244
-
245
- Rotate on the Z-axis to create inclined surfaces:
230
+ ### Ramp
246
231
 
247
232
  ```json
248
233
  {
@@ -256,12 +241,11 @@ Rotate on the Z-axis to create inclined surfaces:
256
241
  ```
257
242
 
258
243
  ### Wall Pattern
259
-
260
- Tall thin boxes positioned at boundaries:
244
+ ### Wall
261
245
 
262
246
  ```json
263
247
  {
264
- "id": "wall-back",
248
+ "id": "wall",
265
249
  "components": {
266
250
  "transform": { "type": "Transform", "properties": { "position": [0, 3, -20] } },
267
251
  "geometry": { "type": "Geometry", "properties": { "geometryType": "box", "args": [40, 7, 1] } },
@@ -270,128 +254,64 @@ Tall thin boxes positioned at boundaries:
270
254
  }
271
255
  ```
272
256
 
273
- ### Three-Point Lighting Setup
274
-
275
- Good lighting uses main, fill, and accent lights:
257
+ ### Lighting
276
258
 
277
259
  ```json
278
260
  [
279
- { "id": "main-light", "components": { "transform": { "properties": { "position": [10, 15, 10] } }, "spotlight": { "type": "SpotLight", "properties": { "color": "#ffffff", "intensity": 200, "angle": 0.8, "castShadow": true } } } },
280
- { "id": "fill-light", "components": { "transform": { "properties": { "position": [-10, 12, -5] } }, "spotlight": { "type": "SpotLight", "properties": { "color": "#b0c4de", "intensity": 80, "angle": 0.9 } } } },
281
- { "id": "accent-light", "components": { "transform": { "properties": { "position": [0, 10, -15] } }, "spotlight": { "type": "SpotLight", "properties": { "color": "#ffd700", "intensity": 50, "angle": 0.4 } } } }
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 } } } }
282
263
  ]
283
264
  ```
284
265
 
285
- ### Available Greybox Textures
286
-
287
- Located in `/textures/GreyboxTextures/`:
288
- - `greybox_dark_grid.png` - dark floors
289
- - `greybox_light_grid.png` - light surfaces
290
- - `greybox_teal_grid.png`, `greybox_purple_grid.png`, `greybox_orange_grid.png` - colored platforms
291
- - `greybox_red_grid.png`, `greybox_blue_grid.png` - obstacles/hazards
292
- - `greybox_yellow_grid.png`, `greybox_lime_grid.png`, `greybox_green_grid.png` - special areas
293
-
294
- ### Metallic/Special Materials
295
-
296
- For goal platforms or special objects, use metalness and roughness:
266
+ ### Text
297
267
 
298
268
  ```json
299
269
  {
300
- "type": "Material",
301
- "properties": {
302
- "color": "#FFD700",
303
- "metalness": 0.8,
304
- "roughness": 0.2
305
- }
306
- }
307
- ```
308
-
309
- ### Text Component
310
-
311
- 3D text rendering using `three-text`. The Text component is non-composable (cannot have children).
312
-
313
- | Property | Type | Default | Description |
314
- |----------|------|---------|-------------|
315
- | `text` | string | `"Hello World"` | Text content to display |
316
- | `color` | string | `"#888888"` | Text color (hex or CSS color) |
317
- | `font` | string | `"/fonts/NotoSans-Regular.ttf"` | Path to TTF font file |
318
- | `size` | number | `0.5` | Font size in world units |
319
- | `depth` | number | `0` | 3D extrusion depth (0 for flat text) |
320
- | `width` | number | `5` | Text block width for wrapping/alignment |
321
- | `align` | string | `"center"` | Horizontal alignment: `"left"`, `"center"`, `"right"` |
322
-
323
- ```json
324
- {
325
- "id": "title-text",
270
+ "id": "text",
326
271
  "components": {
327
272
  "transform": { "type": "Transform", "properties": { "position": [0, 3, 0] } },
328
- "text": {
329
- "type": "Text",
330
- "properties": {
331
- "text": "Welcome",
332
- "color": "#ffffff",
333
- "size": 1,
334
- "depth": 0.1,
335
- "align": "center"
336
- }
337
- }
273
+ "text": { "type": "Text", "properties": { "text": "Welcome", "font": "/fonts/font.ttf", "size": 1, "depth": 0.1 } }
338
274
  }
339
275
  }
340
276
  ```
341
277
 
342
- ### Model Placement
343
-
344
- GLB models don't need geometry/material components:
278
+ ### Model
345
279
 
346
280
  ```json
347
281
  {
348
- "id": "tree-1",
282
+ "id": "model",
349
283
  "components": {
350
- "transform": { "type": "Transform", "properties": { "position": [-12, 0, 10], "scale": [1.5, 1.5, 1.5] } },
351
- "model": { "type": "Model", "properties": { "filename": "models/environment/tree.glb" } }
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" } }
352
286
  }
353
287
  }
354
288
  ```
355
289
 
356
- Available models: `models/environment/tree.glb`, `models/environment/servers.glb`, `models/environment/cubeart.glb`
290
+ ## Editor
357
291
 
358
- ## Editor Mode
359
-
360
- Use editor mode when building scenes visually or creating custom components with inspector UI.
361
-
362
- ### Using the Visual Editor
292
+ ### Basic Usage
363
293
 
364
294
  ```jsx
365
295
  import { PrefabEditor } from 'react-three-game';
366
296
 
367
- <PrefabEditor
368
- initialPrefab={sceneData}
369
- onPrefabChange={setSceneData}
370
- />
297
+ <PrefabEditor initialPrefab={sceneData} onPrefabChange={setSceneData} />
371
298
  ```
372
299
 
373
- The editor provides a full GUI with:
374
- - Scene hierarchy tree for navigating and selecting objects
375
- - Component inspector panel for editing properties
376
- - Transform gizmos for manipulating objects visually
377
- - Keyboard shortcuts: **T** (Translate), **R** (Rotate), **S** (Scale)
300
+ Keyboard shortcuts: **T** (Translate), **R** (Rotate), **S** (Scale)
378
301
 
379
- ### Programmatic Updates with PrefabEditor
380
-
381
- Use the editor ref to update prefabs programmatically:
302
+ ### Programmatic Updates
382
303
 
383
304
  ```jsx
384
305
  import { useRef } from 'react';
385
306
  import { PrefabEditor, updateNodeById } from 'react-three-game';
386
- import type { PrefabEditorRef, Prefab } from 'react-three-game';
307
+ import type { PrefabEditorRef } from 'react-three-game';
387
308
 
388
- function Game() {
309
+ function Scene() {
389
310
  const editorRef = useRef<PrefabEditorRef>(null);
390
311
 
391
- const movePlayer = () => {
392
- if (!editorRef.current) return;
393
- const prefab = editorRef.current.prefab;
394
- const newRoot = updateNodeById(prefab.root, "player", node => ({
312
+ const moveBall = () => {
313
+ const prefab = editorRef.current!.prefab;
314
+ const newRoot = updateNodeById(prefab.root, "ball", node => ({
395
315
  ...node,
396
316
  components: {
397
317
  ...node.components,
@@ -401,209 +321,74 @@ function Game() {
401
321
  }
402
322
  }
403
323
  }));
404
- editorRef.current.setPrefab({ ...prefab, root: newRoot });
324
+ editorRef.current!.setPrefab({ ...prefab, root: newRoot });
405
325
  };
406
326
 
407
- return (
408
- <PrefabEditor ref={editorRef} initialPrefab={sceneData}>
409
- {/* Children render inside the Canvas - can use useFrame here */}
410
- </PrefabEditor>
411
- );
327
+ return <PrefabEditor ref={editorRef} initialPrefab={sceneData} />;
412
328
  }
413
329
  ```
414
330
 
415
- The `PrefabEditorRef` provides:
416
- - `prefab` - current prefab state
417
- - `setPrefab(prefab)` - update the prefab
418
- - `screenshot()` - save canvas as PNG
419
- - `exportGLB()` - export scene as GLB
420
- - `rootRef` - reference to the Three.js scene root for programmatic GLB export
331
+ **PrefabEditorRef**: `prefab`, `setPrefab()`, `screenshot()`, `exportGLB()`, `rootRef`
421
332
 
422
- ### Programmatic GLB Export
423
-
424
- Export Three.js scenes to GLB format from JSON prefabs:
333
+ ### GLB Export
425
334
 
426
335
  ```tsx
427
- import { useRef, useEffect } from 'react';
428
- import { PrefabEditor, exportGLB, exportGLBData } from 'react-three-game';
429
- import type { PrefabEditorRef } from 'react-three-game';
430
-
431
- function ExportScene() {
432
- const editorRef = useRef<PrefabEditorRef>(null);
433
-
434
- const handleExport = async () => {
435
- const sceneRoot = editorRef.current?.rootRef.current?.root;
436
- if (!sceneRoot) return;
437
-
438
- // Option 1: Export and trigger browser download
439
- await exportGLB(sceneRoot, {
440
- filename: 'my-scene.glb',
441
- binary: true,
442
- onComplete: (result) => console.log('Export complete'),
443
- onError: (error) => console.error('Export failed', error)
444
- });
445
-
446
- // Option 2: Get raw ArrayBuffer without downloading
447
- const glbData = await exportGLBData(sceneRoot);
448
- // Upload to server, save to file system, etc.
449
- };
336
+ import { exportGLBData } from 'react-three-game';
450
337
 
451
- return (
452
- <>
453
- <PrefabEditor ref={editorRef} initialPrefab={jsonPrefab} />
454
- <button onClick={handleExport}>Export GLB</button>
455
- </>
456
- );
457
- }
338
+ const glbData = await exportGLBData(editorRef.current!.rootRef.current!.root);
458
339
  ```
459
340
 
460
- **Export Options:**
461
- - `filename` - Output filename (triggers download if provided)
462
- - `binary` - Export as binary GLB (true) or JSON glTF (false)
463
- - `onComplete` - Callback when export succeeds
464
- - `onError` - Callback when export fails
465
-
466
- **Common Agent Pattern:**
467
- ```tsx
468
- useEffect(() => {
469
- // Wait for scene to fully render
470
- const timer = setTimeout(async () => {
471
- const sceneRoot = editorRef.current?.rootRef.current?.root;
472
- if (sceneRoot) {
473
- const glbData = await exportGLBData(sceneRoot);
474
- // Process the ArrayBuffer
475
- }
476
- }, 1000);
477
- return () => clearTimeout(timer);
478
- }, []);
479
- ```
480
-
481
- ### Live Node Updates with useFrame
482
-
483
- To animate objects by updating the prefab JSON at runtime, pass a child component to `PrefabEditor` that uses `useFrame`:
341
+ ### Runtime Animation
484
342
 
485
343
  ```tsx
486
344
  import { useRef } from "react";
487
345
  import { useFrame } from "@react-three/fiber";
488
346
  import { PrefabEditor, updateNodeById } from "react-three-game";
489
- import type { Prefab, PrefabEditorRef } from "react-three-game";
490
-
491
- // Animation component runs inside the editor's Canvas
492
- function PlayerAnimator({ editorRef }: { editorRef: React.RefObject<PrefabEditorRef | null> }) {
493
- const velocityRef = useRef({ x: 0, z: 0 });
494
347
 
348
+ function Animator({ editorRef }) {
495
349
  useFrame(() => {
496
- if (!editorRef.current) return;
497
-
498
- const prefab = editorRef.current.prefab;
499
- const newRoot = updateNodeById(prefab.root, "player", (node) => {
500
- const transform = node.components?.transform?.properties;
501
- if (!transform) return node;
502
-
503
- const pos = transform.position as [number, number, number];
504
- return {
505
- ...node,
506
- components: {
507
- ...node.components,
508
- transform: {
509
- ...node.components!.transform!,
510
- properties: {
511
- ...transform,
512
- position: [pos[0] + velocityRef.current.x * 0.02, pos[1], pos[2] + velocityRef.current.z * 0.02],
513
- },
514
- },
515
- },
516
- };
517
- });
518
-
519
- if (newRoot !== prefab.root) {
520
- editorRef.current.setPrefab({ ...prefab, root: newRoot });
521
- }
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 });
522
362
  });
523
-
524
363
  return null;
525
364
  }
526
365
 
527
- // Usage
528
- function Game() {
529
- const editorRef = useRef<PrefabEditorRef>(null);
530
-
366
+ function Scene() {
367
+ const editorRef = useRef(null);
531
368
  return (
532
- <PrefabEditor ref={editorRef} initialPrefab={sceneData}>
533
- <PlayerAnimator editorRef={editorRef} />
369
+ <PrefabEditor ref={editorRef} initialPrefab={data}>
370
+ <Animator editorRef={editorRef} />
534
371
  </PrefabEditor>
535
372
  );
536
373
  }
537
374
  ```
538
375
 
539
- Key points:
540
- - Pass animation components as `children` to `PrefabEditor` - they render inside the Canvas
541
- - Access prefab via `editorRef.current.prefab` and update via `editorRef.current.setPrefab()`
542
- - `updateNodeById` is optimized to avoid recreating unchanged branches
543
- - Store mutable state (velocities, timers) in refs to avoid re-renders
544
-
545
- ### Creating a Custom Component
376
+ ### Custom Component
546
377
 
547
378
  ```tsx
548
- import { Component, registerComponent, FieldRenderer, FieldDefinition } from 'react-three-game';
549
-
550
- const myFields: FieldDefinition[] = [
551
- { name: 'speed', type: 'number', label: 'Speed', step: 0.1 },
552
- { name: 'enabled', type: 'boolean', label: 'Enabled' },
553
- ];
379
+ import { Component, registerComponent, FieldRenderer } from 'react-three-game';
554
380
 
555
381
  const MyComponent: Component = {
556
382
  name: 'MyComponent',
557
383
  Editor: ({ component, onUpdate }) => (
558
- <FieldRenderer fields={myFields} values={component.properties} onChange={onUpdate} />
384
+ <FieldRenderer fields={[{ name: 'speed', type: 'number', step: 0.1 }]} values={component.properties} onChange={onUpdate} />
559
385
  ),
560
- View: ({ properties, children }) => {
561
- // Runtime behavior here
562
- return <group>{children}</group>;
563
- },
564
- defaultProperties: { speed: 1, enabled: true }
386
+ View: ({ properties, children }) => <group>{children}</group>,
387
+ defaultProperties: { speed: 1 }
565
388
  };
566
389
 
567
390
  registerComponent(MyComponent);
568
391
  ```
569
392
 
570
- ### Field Types for Editor UI
571
-
572
- | Type | Description | Options |
573
- |------|-------------|---------|
574
- | `vector3` | X/Y/Z inputs | `snap?: number` |
575
- | `number` | Numeric input | `min?`, `max?`, `step?` |
576
- | `string` | Text input | `placeholder?` |
577
- | `color` | Color picker | - |
578
- | `boolean` | Checkbox | - |
579
- | `select` | Dropdown | `options: { value, label }[]` |
580
- | `custom` | Custom render function | `render: (props) => ReactNode` |
581
-
582
- ## Dependencies
583
-
584
- Required peer dependencies:
585
- - `@react-three/fiber`
586
- - `@react-three/rapier`
587
- - `three`
588
-
589
- Install with:
590
- ```bash
591
- npm i react-three-game @react-three/fiber @react-three/rapier three
592
- ```
593
-
594
- ## File Structure
595
-
596
- ```
597
- /src → library source (published to npm)
598
- /docs → Next.js demo site
599
- /dist → built output
600
- ```
601
-
602
- ## Development Commands
603
-
604
- ```bash
605
- npm run dev # tsc --watch + docs site
606
- npm run build # build to /dist
607
- npm run release # build + publish
608
- ```
393
+ **Field types**: `vector3`, `number`, `string`, `color`, `boolean`, `select`, `custom`
609
394
 
@@ -0,0 +1,40 @@
1
+ import { Component } from "./ComponentRegistry";
2
+ import { FieldRenderer, FieldDefinition } from "./Input";
3
+
4
+ const ambientLightFields: FieldDefinition[] = [
5
+ { name: 'color', type: 'color', label: 'Color' },
6
+ { name: 'intensity', type: 'number', label: 'Intensity', step: 0.1, min: 0 },
7
+ ];
8
+
9
+ function AmbientLightComponentEditor({
10
+ component,
11
+ onUpdate,
12
+ }: {
13
+ component: any;
14
+ onUpdate: (newProps: any) => void;
15
+ }) {
16
+ return (
17
+ <FieldRenderer
18
+ fields={ambientLightFields}
19
+ values={component.properties}
20
+ onChange={onUpdate}
21
+ />
22
+ );
23
+ }
24
+
25
+ function AmbientLightComponentView({ properties }: { properties: any }) {
26
+ const { color = '#ffffff', intensity = 1 } = properties;
27
+ return <ambientLight color={color} intensity={intensity} />;
28
+ }
29
+
30
+ const AmbientLightComponent: Component = {
31
+ name: 'AmbientLight',
32
+ Editor: AmbientLightComponentEditor,
33
+ View: AmbientLightComponentView,
34
+ defaultProperties: {
35
+ color: '#ffffff',
36
+ intensity: 1,
37
+ },
38
+ };
39
+
40
+ export default AmbientLightComponent;
@@ -17,6 +17,10 @@ const GEOMETRY_ARGS: Record<string, {
17
17
  labels: ["Width", "Height"],
18
18
  defaults: [1, 1],
19
19
  },
20
+ cylinder: {
21
+ labels: ["Radius Top", "Radius Bottom", "Height", "Radial Segments"],
22
+ defaults: [1, 1, 1, 32],
23
+ },
20
24
  };
21
25
 
22
26
  function GeometryComponentEditor({
@@ -38,6 +42,7 @@ function GeometryComponentEditor({
38
42
  { value: 'box', label: 'Box' },
39
43
  { value: 'sphere', label: 'Sphere' },
40
44
  { value: 'plane', label: 'Plane' },
45
+ { value: 'cylinder', label: 'Cylinder' },
41
46
  ],
42
47
  },
43
48
  {
@@ -101,6 +106,8 @@ function GeometryComponentView({ properties, children }: { properties: any, chil
101
106
  return <sphereGeometry args={args as [number, number?, number?]} />;
102
107
  case "plane":
103
108
  return <planeGeometry args={args as [number, number]} />;
109
+ case "cylinder":
110
+ return <cylinderGeometry args={args as [number, number, number, number?]} />;
104
111
  default:
105
112
  return <boxGeometry args={[1, 1, 1]} />;
106
113
  }
@@ -96,11 +96,40 @@ interface InputProps {
96
96
  }
97
97
 
98
98
  export function Input({ value, onChange, step, min, max, style }: InputProps) {
99
+ const [draft, setDraft] = useState<string>(() => value.toString());
100
+
101
+ useEffect(() => {
102
+ setDraft(value.toString());
103
+ }, [value]);
104
+
105
+ const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
106
+ const inputValue = e.target.value;
107
+ setDraft(inputValue);
108
+
109
+ const num = parseFloat(inputValue);
110
+ if (Number.isFinite(num)) {
111
+ onChange(num);
112
+ }
113
+ };
114
+
115
+ const handleBlur = () => {
116
+ const num = parseFloat(draft);
117
+ if (!Number.isFinite(num)) {
118
+ setDraft(value.toString());
119
+ }
120
+ };
121
+
99
122
  return (
100
123
  <input
101
- type="number"
102
- value={value}
103
- onChange={(e) => onChange(parseFloat(e.target.value))}
124
+ type="text"
125
+ value={draft}
126
+ onChange={handleChange}
127
+ onBlur={handleBlur}
128
+ onKeyDown={e => {
129
+ if (e.key === 'Enter') {
130
+ (e.target as HTMLInputElement).blur();
131
+ }
132
+ }}
104
133
  step={step}
105
134
  min={min}
106
135
  max={max}
@@ -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
 
@@ -14,6 +15,7 @@ export default [
14
15
  PhysicsComponent,
15
16
  SpotLightComponent,
16
17
  DirectionalLightComponent,
18
+ AmbientLightComponent,
17
19
  ModelComponent,
18
20
  TextComponent
19
21
  ];