react-three-game 0.0.47 → 0.0.49

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.
@@ -15,7 +15,6 @@ interface Prefab { id?: string; name?: string; root: GameObject; }
15
15
  interface GameObject {
16
16
  id: string; // Use crypto.randomUUID() for new nodes
17
17
  disabled?: boolean;
18
- hidden?: boolean;
19
18
  components?: Record<string, { type: string; properties: any }>;
20
19
  children?: GameObject[];
21
20
  }
@@ -38,11 +37,17 @@ const MyComponent: Component = {
38
37
  | File | Purpose |
39
38
  |------|---------|
40
39
  | `src/index.ts` | All public exports - add new features here |
41
- | `src/tools/prefabeditor/PrefabRoot.tsx` | Recursive scene renderer, world matrix math |
42
- | `src/tools/prefabeditor/PrefabEditor.tsx` | Edit/play mode, physics pause, JSON import/export |
40
+ | `src/tools/prefabeditor/PrefabRoot.tsx` | Pure renderer - renders prefab as Three.js objects for R3F integration |
41
+ | `src/tools/prefabeditor/PrefabEditor.tsx` | Managed scene with editor UI and play/pause controls for physics |
43
42
  | `src/tools/prefabeditor/utils.ts` | Tree helpers: `findNode`, `updateNode`, `deleteNode`, `cloneNode` |
44
43
  | `src/shared/GameCanvas.tsx` | WebGPU renderer setup (use `MeshStandardNodeMaterial`) |
45
44
 
45
+ ## Usage Modes
46
+
47
+ **GameCanvas + PrefabRoot**: Pure renderer for embedding prefab data in standard R3F applications. Minimal wrapper - just renders the prefab as Three.js objects. Requires manual `<Physics>` setup. Physics always active. Use this to integrate prefabs into larger R3F scenes.
48
+
49
+ **PrefabEditor**: Managed scene with editor UI and play/pause controls for physics. Full authoring tool for level design and prototyping. Includes canvas, physics, transform gizmos, and inspector. Physics only runs in play mode. Can pass R3F components as children.
50
+
46
51
  ## Critical Patterns
47
52
 
48
53
  ### Tree Manipulation (Immutable)
@@ -61,7 +66,7 @@ Use node materials only: `MeshStandardNodeMaterial`, `MeshBasicNodeMaterial` (no
61
66
  Set `model.properties.instanced = true` → uses `InstanceProvider.tsx` for batched rendering with physics.
62
67
 
63
68
  ## Built-in Components
64
- `Transform`, `Geometry` (box/sphere/plane), `Material` (color/texture), `Physics` (dynamic/fixed), `Model` (GLB/FBX), `SpotLight`, `DirectionalLight`
69
+ `Transform`, `Geometry` (box/sphere/plane/cylinder), `Material` (color/texture), `Physics` (dynamic/fixed), `Model` (GLB/FBX), `SpotLight`, `DirectionalLight`, `AmbientLight`, `Text`
65
70
 
66
71
  ## Custom Components (User-space)
67
72
  See `docs/app/demo/editor/RotatorComponent.tsx` for runtime behavior example using `useFrame`. Register with `registerComponent()` before rendering `<PrefabEditor>`.
package/README.md CHANGED
@@ -14,7 +14,13 @@ npm i react-three-game @react-three/fiber @react-three/rapier three
14
14
  npx skills add https://github.com/prnthh/react-three-game-skill
15
15
  ```
16
16
 
17
- ## Usage
17
+ ## Usage Modes
18
+
19
+ **GameCanvas + PrefabRoot**: Pure renderer for embedding prefab data in standard R3F applications. Minimal wrapper - just renders the prefab as Three.js objects. Requires manual `<Physics>` setup. Physics always active. Use this to integrate prefabs into larger R3F scenes.
20
+
21
+ **PrefabEditor**: Managed scene with editor UI and play/pause controls for physics. Full authoring tool for level design and prototyping. Includes canvas, physics, transform gizmos, and inspector. Physics only runs in play mode. Can pass R3F components as children.
22
+
23
+ ## Basic Usage
18
24
 
19
25
  ```jsx
20
26
  import { Physics } from '@react-three/rapier';
@@ -57,7 +63,6 @@ import { GameCanvas, PrefabRoot } from 'react-three-game';
57
63
  interface GameObject {
58
64
  id: string;
59
65
  disabled?: boolean;
60
- hidden?: boolean;
61
66
  components?: Record<string, { type: string; properties: any }>;
62
67
  children?: GameObject[];
63
68
  }
@@ -70,7 +75,7 @@ interface GameObject {
70
75
  | Transform | `position`, `rotation`, `scale` — all `[x,y,z]` arrays, rotation in radians |
71
76
  | Geometry | `geometryType`: box/sphere/plane/cylinder, `args`: dimension array |
72
77
  | Material | `color`, `texture?`, `metalness?`, `roughness?` |
73
- | Physics | `type`: dynamic/fixed |
78
+ | Physics | `type`: dynamic/fixed/kinematicPosition/kinematicVelocity, `mass?`, `restitution?` (bounciness), `friction?`, plus any Rapier props |
74
79
  | Model | `filename` (GLB/FBX path), `instanced?` for GPU batching |
75
80
  | SpotLight | `color`, `intensity`, `angle`, `penumbra` |
76
81
 
@@ -133,14 +138,21 @@ The `FieldRenderer` component auto-generates editor UI from a field schema:
133
138
  }
134
139
  ```
135
140
 
136
- ## Visual Editor
141
+ ## Prefab Editor
137
142
 
138
143
  ```jsx
139
144
  import { PrefabEditor } from 'react-three-game';
145
+
146
+ // Standalone editor
140
147
  <PrefabEditor initialPrefab={sceneData} onPrefabChange={setSceneData} />
148
+
149
+ // With custom R3F components
150
+ <PrefabEditor initialPrefab={sceneData}>
151
+ <CustomComponent />
152
+ </PrefabEditor>
141
153
  ```
142
154
 
143
- Keys: **T**ranslate / **R**otate / **S**cale. Drag tree nodes to reparent. Import/export JSON.
155
+ Keys: **T**ranslate / **R**otate / **S**cale. Drag tree nodes to reparent. Import/export JSON. Physics only runs in play mode.
144
156
 
145
157
  ## Internals
146
158
 
@@ -18,8 +18,6 @@ export interface GroundOptions {
18
18
  repeatCount?: [number, number];
19
19
  /** Physics body type. Defaults to "fixed". */
20
20
  physicsType?: "fixed" | "dynamic" | "kinematic";
21
- /** Set true to hide the node. */
22
- hidden?: boolean;
23
21
  /** Set true to disable the node. */
24
22
  disabled?: boolean;
25
23
  }
@@ -8,11 +8,10 @@
8
8
  * - Physics (fixed by default)
9
9
  */
10
10
  export function ground(options = {}) {
11
- const { id = "ground", size = 50, position = [0, 0, 0], rotation = [-Math.PI / 2, 0, 0], scale = [1, 1, 1], color = "white", texture, repeat = texture ? true : false, repeatCount = [25, 25], physicsType = "fixed", hidden = false, disabled = false, } = options;
11
+ const { id = "ground", size = 50, position = [0, 0, 0], rotation = [-Math.PI / 2, 0, 0], scale = [1, 1, 1], color = "white", texture, repeat = texture ? true : false, repeatCount = [25, 25], physicsType = "fixed", disabled = false, } = options;
12
12
  return {
13
13
  id,
14
14
  disabled,
15
- hidden,
16
15
  components: {
17
16
  transform: {
18
17
  type: "Transform",
@@ -108,7 +108,7 @@ export const PrefabRoot = forwardRef(({ editMode, data, onPrefabChange, selected
108
108
  export function GameObjectRenderer(props) {
109
109
  var _a, _b, _c;
110
110
  const node = props.gameObject;
111
- if (!node || node.hidden || node.disabled)
111
+ if (!node || node.disabled)
112
112
  return null;
113
113
  const isInstanced = (_c = (_b = (_a = node.components) === null || _a === void 0 ? void 0 : _a.model) === null || _b === void 0 ? void 0 : _b.properties) === null || _c === void 0 ? void 0 : _c.instanced;
114
114
  const prevInstancedRef = useRef(undefined);
@@ -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 });
@@ -1,10 +1,5 @@
1
+ import type { RigidBodyOptions } from "@react-three/rapier";
1
2
  import { Component } from "./ComponentRegistry";
2
- export interface PhysicsProps {
3
- type: "fixed" | "dynamic";
4
- collider?: string;
5
- mass?: number;
6
- restitution?: number;
7
- friction?: number;
8
- }
3
+ export type PhysicsProps = RigidBodyOptions;
9
4
  declare const PhysicsComponent: Component;
10
5
  export default PhysicsComponent;
@@ -1,3 +1,14 @@
1
+ var __rest = (this && this.__rest) || function (s, e) {
2
+ var t = {};
3
+ for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
4
+ t[p] = s[p];
5
+ if (s != null && typeof Object.getOwnPropertySymbols === "function")
6
+ for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
7
+ if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
8
+ t[p[i]] = s[p[i]];
9
+ }
10
+ return t;
11
+ };
1
12
  import { jsx as _jsx } from "react/jsx-runtime";
2
13
  import { RigidBody } from "@react-three/rapier";
3
14
  import { FieldRenderer } from "./Input";
@@ -9,10 +20,12 @@ const physicsFields = [
9
20
  options: [
10
21
  { value: 'dynamic', label: 'Dynamic' },
11
22
  { value: 'fixed', label: 'Fixed' },
23
+ { value: 'kinematicPosition', label: 'Kinematic Position' },
24
+ { value: 'kinematicVelocity', label: 'Kinematic Velocity' },
12
25
  ],
13
26
  },
14
27
  {
15
- name: 'collider',
28
+ name: 'colliders',
16
29
  type: 'select',
17
30
  label: 'Collider',
18
31
  options: [
@@ -22,24 +35,65 @@ const physicsFields = [
22
35
  { value: 'ball', label: 'Ball (sphere)' },
23
36
  ],
24
37
  },
38
+ {
39
+ name: 'mass',
40
+ type: 'number',
41
+ label: 'Mass',
42
+ },
43
+ {
44
+ name: 'restitution',
45
+ type: 'number',
46
+ label: 'Restitution (Bounciness)',
47
+ min: 0,
48
+ max: 1,
49
+ step: 0.1,
50
+ },
51
+ {
52
+ name: 'friction',
53
+ type: 'number',
54
+ label: 'Friction',
55
+ min: 0,
56
+ step: 0.1,
57
+ },
58
+ {
59
+ name: 'linearDamping',
60
+ type: 'number',
61
+ label: 'Linear Damping',
62
+ min: 0,
63
+ step: 0.1,
64
+ },
65
+ {
66
+ name: 'angularDamping',
67
+ type: 'number',
68
+ label: 'Angular Damping',
69
+ min: 0,
70
+ step: 0.1,
71
+ },
72
+ {
73
+ name: 'gravityScale',
74
+ type: 'number',
75
+ label: 'Gravity Scale',
76
+ step: 0.1,
77
+ },
25
78
  ];
26
79
  function PhysicsComponentEditor({ component, onUpdate }) {
27
- return (_jsx(FieldRenderer, { fields: physicsFields, values: component.properties, onChange: onUpdate }));
80
+ return (_jsx(FieldRenderer, { fields: physicsFields, values: component.properties, onChange: (props) => onUpdate(Object.assign(Object.assign({}, component), { properties: Object.assign(Object.assign({}, component.properties), props) })) }));
28
81
  }
29
82
  function PhysicsComponentView({ properties, children, position, rotation, scale, editMode }) {
30
- const colliders = properties.collider || (properties.type === 'fixed' ? 'trimesh' : 'hull');
83
+ const { type, colliders } = properties, otherProps = __rest(properties, ["type", "colliders"]);
84
+ const colliderType = colliders || (type === 'fixed' ? 'trimesh' : 'hull');
31
85
  // In edit mode, include position/rotation in key to force remount when transform changes
32
86
  // This ensures the RigidBody debug visualization updates even when physics is paused
33
87
  const rbKey = editMode
34
- ? `${properties.type || 'dynamic'}_${colliders}_${position === null || position === void 0 ? void 0 : position.join(',')}_${rotation === null || rotation === void 0 ? void 0 : rotation.join(',')}`
35
- : `${properties.type || 'dynamic'}_${colliders}`;
36
- return (_jsx(RigidBody, { type: properties.type, colliders: colliders, position: position, rotation: rotation, scale: scale, children: children }, rbKey));
88
+ ? `${type || 'dynamic'}_${colliderType}_${position === null || position === void 0 ? void 0 : position.join(',')}_${rotation === null || rotation === void 0 ? void 0 : rotation.join(',')}`
89
+ : `${type || 'dynamic'}_${colliderType}`;
90
+ return (_jsx(RigidBody, Object.assign({ type: type, colliders: colliderType, position: position, rotation: rotation, scale: scale }, otherProps, { children: children }), rbKey));
37
91
  }
38
92
  const PhysicsComponent = {
39
93
  name: 'Physics',
40
94
  Editor: PhysicsComponentEditor,
41
95
  View: PhysicsComponentView,
42
96
  nonComposable: true,
43
- defaultProperties: { type: 'dynamic', collider: 'hull' }
97
+ defaultProperties: { type: 'dynamic', colliders: 'hull' }
44
98
  };
45
99
  export default PhysicsComponent;
@@ -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
  ];
@@ -7,7 +7,6 @@ export interface GameObject {
7
7
  id: string;
8
8
  name?: string;
9
9
  disabled?: boolean;
10
- hidden?: boolean;
11
10
  children?: GameObject[];
12
11
  components?: {
13
12
  [key: string]: ComponentData | undefined;
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.49",
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,7 @@
1
+ Agent skill for [react-three-game](https://github.com/prnthh/react-three-game)
2
+
3
+ Gives your agent the ability to make 3D scenes, physics simulations and games.
4
+
5
+ ```
6
+ npx skills add https://github.com/prnthh/react-three-game-skill
7
+ ```
@@ -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:
@@ -69,7 +82,6 @@ Every game object follows this schema:
69
82
  interface GameObject {
70
83
  id: string;
71
84
  disabled?: boolean;
72
- hidden?: boolean;
73
85
  components?: Record<string, { type: string; properties: any }>;
74
86
  children?: GameObject[];
75
87
  }
@@ -104,12 +116,21 @@ Scenes are defined as JSON prefabs with a root node containing children:
104
116
  | Transform | `Transform` | `position: [x,y,z]`, `rotation: [x,y,z]` (radians), `scale: [x,y,z]` |
105
117
  | Geometry | `Geometry` | `geometryType`: box/sphere/plane/cylinder, `args`: dimension array |
106
118
  | Material | `Material` | `color`, `texture?`, `metalness?`, `roughness?`, `repeat?`, `repeatCount?` |
107
- | Physics | `Physics` | `type`: "dynamic" or "fixed" |
119
+ | Physics | `Physics` | `type`: dynamic/fixed/kinematicPosition/kinematicVelocity, `mass?`, `restitution?`, `friction?`, `linearDamping?`, `angularDamping?`, `gravityScale?`, plus any Rapier RigidBody props |
108
120
  | Model | `Model` | `filename` (GLB/FBX path), `instanced?` for GPU batching |
109
121
  | SpotLight | `SpotLight` | `color`, `intensity`, `angle`, `penumbra`, `distance?`, `castShadow?` |
110
122
  | DirectionalLight | `DirectionalLight` | `color`, `intensity`, `castShadow?`, `targetOffset?: [x,y,z]` |
123
+ | AmbientLight | `AmbientLight` | `color`, `intensity` |
111
124
  | Text | `Text` | `text`, `font`, `size`, `depth`, `width`, `align`, `color` |
112
125
 
126
+ ### Text Component
127
+
128
+ Requires `hb.wasm` and a font file (TTF/WOFF) in `/public/fonts/`:
129
+ - hb.wasm: https://github.com/prnthh/react-three-game/raw/refs/heads/main/docs/public/fonts/hb.wasm
130
+ - Sample font: https://github.com/prnthh/react-three-game/raw/refs/heads/main/docs/public/fonts/NotoSans-Regular.ttf
131
+
132
+ Font property: `"font": "/fonts/NotoSans-Regular.ttf"`
133
+
113
134
  ### Geometry Args by Type
114
135
 
115
136
  | geometryType | args array |
@@ -119,41 +140,31 @@ Scenes are defined as JSON prefabs with a root node containing children:
119
140
  | `plane` | `[width, height]` |
120
141
  | `cylinder` | `[radiusTop, radiusBottom, height, radialSegments]` |
121
142
 
122
- ### Material Texture Options
143
+ ### Material Textures
123
144
 
124
145
  ```json
125
146
  {
126
- "type": "Material",
127
- "properties": {
128
- "color": "white",
129
- "texture": "/textures/path/to/texture.png",
130
- "repeat": true,
131
- "repeatCount": [4, 4]
147
+ "material": {
148
+ "type": "Material",
149
+ "properties": {
150
+ "color": "white",
151
+ "texture": "/textures/floor.png",
152
+ "repeat": true,
153
+ "repeatCount": [4, 4]
154
+ }
132
155
  }
133
156
  }
134
157
  ```
135
158
 
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
159
+ ### Rotations
140
160
 
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]`)
161
+ Use radians: `1.57` = 90°, `3.14` = 180°, `-1.57` = -90°
145
162
 
146
163
  ## Common Patterns
147
164
 
148
165
  ### Usage Modes
149
166
 
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)
167
+ **GameCanvas + PrefabRoot**: Pure renderer for embedding prefab data in standard R3F applications. Minimal wrapper - just renders the prefab as Three.js objects. Requires manual `<Physics>` setup. Physics always active. Use this to integrate prefabs into larger R3F scenes.
157
168
 
158
169
  ```jsx
159
170
  import { Physics } from '@react-three/rapier';
@@ -162,87 +173,63 @@ import { GameCanvas, PrefabRoot } from 'react-three-game';
162
173
  <GameCanvas>
163
174
  <Physics>
164
175
  <PrefabRoot data={prefabData} />
176
+ <CustomComponent />
165
177
  </Physics>
166
178
  </GameCanvas>
167
179
  ```
168
180
 
169
- ### Tree Manipulation Utilities
181
+ **PrefabEditor**: Managed scene with editor UI and play/pause controls for physics. Full authoring tool for level design and prototyping. Includes canvas, physics, transform gizmos, and inspector. Physics only runs in play mode. Can pass R3F components as children.
182
+
183
+ ```jsx
184
+ import { PrefabEditor } from 'react-three-game';
185
+
186
+ <PrefabEditor initialPrefab={prefabData}>
187
+ <CustomComponent />
188
+ </PrefabEditor>
189
+ ```
190
+
191
+ ### Tree Utilities
170
192
 
171
193
  ```typescript
172
- import { findNode, updateNode, updateNodeById, deleteNode, cloneNode, saveJson, loadJson, exportGLB, exportGLBData } from 'react-three-game';
194
+ import { updateNodeById, findNode, deleteNode, cloneNode, exportGLBData } from 'react-three-game';
173
195
 
174
- // Update a node by ID (optimized - avoids unnecessary object creation)
175
196
  const updated = updateNodeById(root, nodeId, node => ({ ...node, disabled: true }));
176
-
177
- // Find a node
178
197
  const node = findNode(root, nodeId);
179
-
180
- // Delete a node
181
198
  const afterDelete = deleteNode(root, nodeId);
182
-
183
- // Clone a node
184
199
  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
200
+ const glbData = await exportGLBData(sceneRoot);
193
201
  ```
194
202
 
195
- ## Building Game Levels
203
+ ## Level Patterns
196
204
 
197
- ### Complete Prefab Structure
205
+ ### Floor
198
206
 
199
207
  ```json
200
208
  {
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",
209
+ "id": "floor",
218
210
  "components": {
219
211
  "transform": { "type": "Transform", "properties": { "position": [0, -0.5, 0] } },
220
212
  "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] } },
213
+ "material": { "type": "Material", "properties": { "texture": "/textures/floor.png", "repeat": true, "repeatCount": [20, 20] } },
222
214
  "physics": { "type": "Physics", "properties": { "type": "fixed" } }
223
215
  }
224
216
  }
225
217
  ```
226
218
 
227
- ### Platform Pattern
228
-
229
- Floating platforms use "fixed" physics and smaller box geometry:
219
+ ### Platform
230
220
 
231
221
  ```json
232
222
  {
233
- "id": "platform-1",
223
+ "id": "platform",
234
224
  "components": {
235
225
  "transform": { "type": "Transform", "properties": { "position": [-8, 2, -5] } },
236
226
  "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
227
  "physics": { "type": "Physics", "properties": { "type": "fixed" } }
239
228
  }
240
229
  }
241
230
  ```
242
231
 
243
- ### Ramp Pattern
244
-
245
- Rotate on the Z-axis to create inclined surfaces:
232
+ ### Ramp
246
233
 
247
234
  ```json
248
235
  {
@@ -256,12 +243,11 @@ Rotate on the Z-axis to create inclined surfaces:
256
243
  ```
257
244
 
258
245
  ### Wall Pattern
259
-
260
- Tall thin boxes positioned at boundaries:
246
+ ### Wall
261
247
 
262
248
  ```json
263
249
  {
264
- "id": "wall-back",
250
+ "id": "wall",
265
251
  "components": {
266
252
  "transform": { "type": "Transform", "properties": { "position": [0, 3, -20] } },
267
253
  "geometry": { "type": "Geometry", "properties": { "geometryType": "box", "args": [40, 7, 1] } },
@@ -270,128 +256,64 @@ Tall thin boxes positioned at boundaries:
270
256
  }
271
257
  ```
272
258
 
273
- ### Three-Point Lighting Setup
274
-
275
- Good lighting uses main, fill, and accent lights:
259
+ ### Lighting
276
260
 
277
261
  ```json
278
262
  [
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 } } } }
263
+ { "id": "spot", "components": { "transform": { "properties": { "position": [10, 15, 10] } }, "spotlight": { "type": "SpotLight", "properties": { "intensity": 200, "angle": 0.8, "castShadow": true } } } },
264
+ { "id": "ambient", "components": { "ambientlight": { "type": "AmbientLight", "properties": { "intensity": 0.4 } } } }
282
265
  ]
283
266
  ```
284
267
 
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:
268
+ ### Text
297
269
 
298
270
  ```json
299
271
  {
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",
272
+ "id": "text",
326
273
  "components": {
327
274
  "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
- }
275
+ "text": { "type": "Text", "properties": { "text": "Welcome", "font": "/fonts/font.ttf", "size": 1, "depth": 0.1 } }
338
276
  }
339
277
  }
340
278
  ```
341
279
 
342
- ### Model Placement
343
-
344
- GLB models don't need geometry/material components:
280
+ ### Model
345
281
 
346
282
  ```json
347
283
  {
348
- "id": "tree-1",
284
+ "id": "model",
349
285
  "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" } }
286
+ "transform": { "type": "Transform", "properties": { "position": [0, 0, 0], "scale": [1.5, 1.5, 1.5] } },
287
+ "model": { "type": "Model", "properties": { "filename": "/models/tree.glb" } }
352
288
  }
353
289
  }
354
290
  ```
355
291
 
356
- Available models: `models/environment/tree.glb`, `models/environment/servers.glb`, `models/environment/cubeart.glb`
292
+ ## Editor
357
293
 
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
294
+ ### Basic Usage
363
295
 
364
296
  ```jsx
365
297
  import { PrefabEditor } from 'react-three-game';
366
298
 
367
- <PrefabEditor
368
- initialPrefab={sceneData}
369
- onPrefabChange={setSceneData}
370
- />
299
+ <PrefabEditor initialPrefab={sceneData} onPrefabChange={setSceneData} />
371
300
  ```
372
301
 
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)
302
+ Keyboard shortcuts: **T** (Translate), **R** (Rotate), **S** (Scale)
378
303
 
379
- ### Programmatic Updates with PrefabEditor
380
-
381
- Use the editor ref to update prefabs programmatically:
304
+ ### Programmatic Updates
382
305
 
383
306
  ```jsx
384
307
  import { useRef } from 'react';
385
308
  import { PrefabEditor, updateNodeById } from 'react-three-game';
386
- import type { PrefabEditorRef, Prefab } from 'react-three-game';
309
+ import type { PrefabEditorRef } from 'react-three-game';
387
310
 
388
- function Game() {
311
+ function Scene() {
389
312
  const editorRef = useRef<PrefabEditorRef>(null);
390
313
 
391
- const movePlayer = () => {
392
- if (!editorRef.current) return;
393
- const prefab = editorRef.current.prefab;
394
- const newRoot = updateNodeById(prefab.root, "player", node => ({
314
+ const moveBall = () => {
315
+ const prefab = editorRef.current!.prefab;
316
+ const newRoot = updateNodeById(prefab.root, "ball", node => ({
395
317
  ...node,
396
318
  components: {
397
319
  ...node.components,
@@ -401,209 +323,74 @@ function Game() {
401
323
  }
402
324
  }
403
325
  }));
404
- editorRef.current.setPrefab({ ...prefab, root: newRoot });
326
+ editorRef.current!.setPrefab({ ...prefab, root: newRoot });
405
327
  };
406
328
 
407
- return (
408
- <PrefabEditor ref={editorRef} initialPrefab={sceneData}>
409
- {/* Children render inside the Canvas - can use useFrame here */}
410
- </PrefabEditor>
411
- );
329
+ return <PrefabEditor ref={editorRef} initialPrefab={sceneData} />;
412
330
  }
413
331
  ```
414
332
 
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
333
+ **PrefabEditorRef**: `prefab`, `setPrefab()`, `screenshot()`, `exportGLB()`, `rootRef`
421
334
 
422
- ### Programmatic GLB Export
423
-
424
- Export Three.js scenes to GLB format from JSON prefabs:
335
+ ### GLB Export
425
336
 
426
337
  ```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
- };
338
+ import { exportGLBData } from 'react-three-game';
450
339
 
451
- return (
452
- <>
453
- <PrefabEditor ref={editorRef} initialPrefab={jsonPrefab} />
454
- <button onClick={handleExport}>Export GLB</button>
455
- </>
456
- );
457
- }
340
+ const glbData = await exportGLBData(editorRef.current!.rootRef.current!.root);
458
341
  ```
459
342
 
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`:
343
+ ### Runtime Animation
484
344
 
485
345
  ```tsx
486
346
  import { useRef } from "react";
487
347
  import { useFrame } from "@react-three/fiber";
488
348
  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
349
 
350
+ function Animator({ editorRef }) {
495
351
  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
- }
352
+ const prefab = editorRef.current!.prefab;
353
+ const newRoot = updateNodeById(prefab.root, "ball", node => ({
354
+ ...node,
355
+ components: {
356
+ ...node.components,
357
+ transform: {
358
+ ...node.components!.transform!,
359
+ properties: { ...node.components!.transform!.properties, position: [x, y, z] }
360
+ }
361
+ }
362
+ }));
363
+ editorRef.current!.setPrefab({ ...prefab, root: newRoot });
522
364
  });
523
-
524
365
  return null;
525
366
  }
526
367
 
527
- // Usage
528
- function Game() {
529
- const editorRef = useRef<PrefabEditorRef>(null);
530
-
368
+ function Scene() {
369
+ const editorRef = useRef(null);
531
370
  return (
532
- <PrefabEditor ref={editorRef} initialPrefab={sceneData}>
533
- <PlayerAnimator editorRef={editorRef} />
371
+ <PrefabEditor ref={editorRef} initialPrefab={data}>
372
+ <Animator editorRef={editorRef} />
534
373
  </PrefabEditor>
535
374
  );
536
375
  }
537
376
  ```
538
377
 
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
378
+ ### Custom Component
546
379
 
547
380
  ```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
- ];
381
+ import { Component, registerComponent, FieldRenderer } from 'react-three-game';
554
382
 
555
383
  const MyComponent: Component = {
556
384
  name: 'MyComponent',
557
385
  Editor: ({ component, onUpdate }) => (
558
- <FieldRenderer fields={myFields} values={component.properties} onChange={onUpdate} />
386
+ <FieldRenderer fields={[{ name: 'speed', type: 'number', step: 0.1 }]} values={component.properties} onChange={onUpdate} />
559
387
  ),
560
- View: ({ properties, children }) => {
561
- // Runtime behavior here
562
- return <group>{children}</group>;
563
- },
564
- defaultProperties: { speed: 1, enabled: true }
388
+ View: ({ properties, children }) => <group>{children}</group>,
389
+ defaultProperties: { speed: 1 }
565
390
  };
566
391
 
567
392
  registerComponent(MyComponent);
568
393
  ```
569
394
 
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
- ```
395
+ **Field types**: `vector3`, `number`, `string`, `color`, `boolean`, `select`, `custom`
609
396
 
@@ -25,8 +25,6 @@ export interface GroundOptions {
25
25
  /** Physics body type. Defaults to "fixed". */
26
26
  physicsType?: "fixed" | "dynamic" | "kinematic";
27
27
 
28
- /** Set true to hide the node. */
29
- hidden?: boolean;
30
28
  /** Set true to disable the node. */
31
29
  disabled?: boolean;
32
30
  }
@@ -52,14 +50,12 @@ export function ground(options: GroundOptions = {}): GameObject {
52
50
  repeat = texture ? true : false,
53
51
  repeatCount = [25, 25],
54
52
  physicsType = "fixed",
55
- hidden = false,
56
53
  disabled = false,
57
54
  } = options;
58
55
 
59
56
  return {
60
57
  id,
61
58
  disabled,
62
- hidden,
63
59
  components: {
64
60
  transform: {
65
61
  type: "Transform",
@@ -175,7 +175,7 @@ export const PrefabRoot = forwardRef<PrefabRootRef, {
175
175
 
176
176
  export function GameObjectRenderer(props: RendererProps) {
177
177
  const node = props.gameObject;
178
- if (!node || node.hidden || node.disabled) return null;
178
+ if (!node || node.disabled) return null;
179
179
 
180
180
  const isInstanced = node.components?.model?.properties?.instanced;
181
181
  const prevInstancedRef = useRef<boolean | undefined>(undefined);
@@ -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}
@@ -1,17 +1,11 @@
1
1
  import { RigidBody, RapierRigidBody } from "@react-three/rapier";
2
+ import type { RigidBodyOptions } from "@react-three/rapier";
2
3
  import type { ReactNode } from 'react';
3
- import { useEffect, useRef } from 'react';
4
4
  import { Component } from "./ComponentRegistry";
5
5
  import { FieldRenderer, FieldDefinition } from "./Input";
6
- import { Quaternion, Euler } from 'three';
6
+ import { ComponentData } from "../types";
7
7
 
8
- export interface PhysicsProps {
9
- type: "fixed" | "dynamic";
10
- collider?: string;
11
- mass?: number;
12
- restitution?: number;
13
- friction?: number;
14
- }
8
+ export type PhysicsProps = RigidBodyOptions;
15
9
 
16
10
  const physicsFields: FieldDefinition[] = [
17
11
  {
@@ -21,10 +15,12 @@ const physicsFields: FieldDefinition[] = [
21
15
  options: [
22
16
  { value: 'dynamic', label: 'Dynamic' },
23
17
  { value: 'fixed', label: 'Fixed' },
18
+ { value: 'kinematicPosition', label: 'Kinematic Position' },
19
+ { value: 'kinematicVelocity', label: 'Kinematic Velocity' },
24
20
  ],
25
21
  },
26
22
  {
27
- name: 'collider',
23
+ name: 'colliders',
28
24
  type: 'select',
29
25
  label: 'Collider',
30
26
  options: [
@@ -34,20 +30,60 @@ const physicsFields: FieldDefinition[] = [
34
30
  { value: 'ball', label: 'Ball (sphere)' },
35
31
  ],
36
32
  },
33
+ {
34
+ name: 'mass',
35
+ type: 'number',
36
+ label: 'Mass',
37
+ },
38
+ {
39
+ name: 'restitution',
40
+ type: 'number',
41
+ label: 'Restitution (Bounciness)',
42
+ min: 0,
43
+ max: 1,
44
+ step: 0.1,
45
+ },
46
+ {
47
+ name: 'friction',
48
+ type: 'number',
49
+ label: 'Friction',
50
+ min: 0,
51
+ step: 0.1,
52
+ },
53
+ {
54
+ name: 'linearDamping',
55
+ type: 'number',
56
+ label: 'Linear Damping',
57
+ min: 0,
58
+ step: 0.1,
59
+ },
60
+ {
61
+ name: 'angularDamping',
62
+ type: 'number',
63
+ label: 'Angular Damping',
64
+ min: 0,
65
+ step: 0.1,
66
+ },
67
+ {
68
+ name: 'gravityScale',
69
+ type: 'number',
70
+ label: 'Gravity Scale',
71
+ step: 0.1,
72
+ },
37
73
  ];
38
74
 
39
- function PhysicsComponentEditor({ component, onUpdate }: { component: { properties: { type?: 'dynamic' | 'fixed'; collider?: string;[k: string]: any } }; onUpdate: (props: Partial<Record<string, any>>) => void }) {
75
+ function PhysicsComponentEditor({ component, onUpdate }: { component: ComponentData; onUpdate: (newComp: any) => void }) {
40
76
  return (
41
77
  <FieldRenderer
42
78
  fields={physicsFields}
43
79
  values={component.properties}
44
- onChange={onUpdate}
80
+ onChange={(props) => onUpdate({ ...component, properties: { ...component.properties, ...props } })}
45
81
  />
46
82
  );
47
83
  }
48
84
 
49
85
  interface PhysicsViewProps {
50
- properties: { type?: 'dynamic' | 'fixed'; collider?: string };
86
+ properties: PhysicsProps;
51
87
  editMode?: boolean;
52
88
  children?: ReactNode;
53
89
  position?: [number, number, number];
@@ -56,22 +92,24 @@ interface PhysicsViewProps {
56
92
  }
57
93
 
58
94
  function PhysicsComponentView({ properties, children, position, rotation, scale, editMode }: PhysicsViewProps) {
59
- const colliders = properties.collider || (properties.type === 'fixed' ? 'trimesh' : 'hull');
95
+ const { type, colliders, ...otherProps } = properties;
96
+ const colliderType = colliders || (type === 'fixed' ? 'trimesh' : 'hull');
60
97
 
61
98
  // In edit mode, include position/rotation in key to force remount when transform changes
62
99
  // This ensures the RigidBody debug visualization updates even when physics is paused
63
100
  const rbKey = editMode
64
- ? `${properties.type || 'dynamic'}_${colliders}_${position?.join(',')}_${rotation?.join(',')}`
65
- : `${properties.type || 'dynamic'}_${colliders}`;
101
+ ? `${type || 'dynamic'}_${colliderType}_${position?.join(',')}_${rotation?.join(',')}`
102
+ : `${type || 'dynamic'}_${colliderType}`;
66
103
 
67
104
  return (
68
105
  <RigidBody
69
106
  key={rbKey}
70
- type={properties.type}
71
- colliders={colliders as any}
107
+ type={type}
108
+ colliders={colliderType as any}
72
109
  position={position}
73
110
  rotation={rotation}
74
111
  scale={scale}
112
+ {...otherProps}
75
113
  >
76
114
  {children}
77
115
  </RigidBody>
@@ -83,7 +121,7 @@ const PhysicsComponent: Component = {
83
121
  Editor: PhysicsComponentEditor,
84
122
  View: PhysicsComponentView,
85
123
  nonComposable: true,
86
- defaultProperties: { type: 'dynamic', collider: 'hull' }
124
+ defaultProperties: { type: 'dynamic', colliders: 'hull' }
87
125
  };
88
126
 
89
- export default PhysicsComponent;
127
+ export default PhysicsComponent;
@@ -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
  ];
@@ -8,7 +8,6 @@ export interface GameObject {
8
8
  id: string;
9
9
  name?: string;
10
10
  disabled?: boolean;
11
- hidden?: boolean;
12
11
  children?: GameObject[];
13
12
  components?: {
14
13
  [key: string]: ComponentData | undefined;