react-three-game 0.0.83 → 0.0.85

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.
Files changed (26) hide show
  1. package/README.md +72 -2
  2. package/dist/index.d.ts +6 -2
  3. package/dist/index.js +3 -1
  4. package/dist/tools/prefabeditor/PrefabRoot.js +64 -54
  5. package/dist/tools/prefabeditor/components/BufferGeometryComponent.d.ts +3 -0
  6. package/dist/tools/prefabeditor/components/BufferGeometryComponent.js +96 -0
  7. package/dist/tools/prefabeditor/components/CameraComponent.js +1 -2
  8. package/dist/tools/prefabeditor/components/ClickComponent.js +1 -1
  9. package/dist/tools/prefabeditor/components/ComponentRegistry.d.ts +3 -5
  10. package/dist/tools/prefabeditor/components/DirectionalLightComponent.js +3 -9
  11. package/dist/tools/prefabeditor/components/EnvironmentComponent.js +1 -1
  12. package/dist/tools/prefabeditor/components/MaterialComponent.d.ts +10 -0
  13. package/dist/tools/prefabeditor/components/MaterialComponent.js +63 -28
  14. package/dist/tools/prefabeditor/components/ModelComponent.js +1 -1
  15. package/dist/tools/prefabeditor/components/PhysicsComponent.js +1 -1
  16. package/dist/tools/prefabeditor/components/PointLightComponent.js +1 -1
  17. package/dist/tools/prefabeditor/components/SoundComponent.js +1 -2
  18. package/dist/tools/prefabeditor/components/SpotLightComponent.js +1 -1
  19. package/dist/tools/prefabeditor/components/index.js +8 -6
  20. package/dist/tools/prefabeditor/runtime.d.ts +61 -0
  21. package/dist/tools/prefabeditor/runtime.js +184 -0
  22. package/dist/tools/prefabeditor/scene.d.ts +8 -0
  23. package/dist/tools/prefabeditor/scene.js +45 -8
  24. package/package.json +2 -2
  25. package/dist/tools/prefabeditor/runtimeContext.d.ts +0 -40
  26. package/dist/tools/prefabeditor/runtimeContext.js +0 -45
package/README.md CHANGED
@@ -110,6 +110,38 @@ That means a saved scene is just a prefab, and the same prefab can be:
110
110
  * rendered directly with `PrefabRoot`
111
111
  * loaded inside another scene as reusable content
112
112
 
113
+ `PrefabRoot` keeps the rendering model narrow and compositional:
114
+
115
+ * `Transform` is the renderer-owned outer transform
116
+ * `Geometry` or `BufferGeometry` + `Material` become the primary mesh content
117
+ * non-instanced `Model` becomes the node's primary content
118
+ * `Physics` is a renderer-owned outer wrapper
119
+ * every other component `View` wraps the current subtree
120
+
121
+ Custom component `View`s use normal React Three Fiber composition with `children`.
122
+
123
+ For agent-authored custom meshes, use `BufferGeometry` with flat numeric arrays:
124
+
125
+ ```json
126
+ {
127
+ "id": "triangle",
128
+ "components": {
129
+ "bufferGeometry": {
130
+ "type": "BufferGeometry",
131
+ "properties": {
132
+ "positions": [0, 0, 0, 1, 0, 0, 0, 1, 0],
133
+ "indices": [0, 1, 2],
134
+ "computeVertexNormals": true
135
+ }
136
+ },
137
+ "material": {
138
+ "type": "Material",
139
+ "properties": { "color": "#ff8844" }
140
+ }
141
+ }
142
+ }
143
+ ```
144
+
113
145
  ## Prefab Format
114
146
 
115
147
  ```ts
@@ -131,7 +163,7 @@ interface GameObject {
131
163
 
132
164
  ## Runtime Mutation
133
165
 
134
- When you need to change the live world, use the `Scene` API from `PrefabEditorRef`.
166
+ Use the `Scene` API from `PrefabEditorRef` for authored state changes that should stay in prefab data.
135
167
 
136
168
  ```tsx
137
169
  import { useEffect, useRef } from "react";
@@ -153,21 +185,59 @@ function RaiseBall() {
153
185
  }
154
186
  ```
155
187
 
188
+ Batch related entity changes so they flush as one store revision:
189
+
190
+ ```tsx
191
+ scene.batch(() => {
192
+ scene.find("orb1")?.getComponent("Transform")?.set("position", [1, 0, 0]);
193
+ scene.find("orb2")?.getComponent("Transform")?.set("position", [-1, 0, 0]);
194
+ });
195
+ ```
196
+
197
+ Define a component runtime factory with `create(ctx)` for hot runtime behavior that mutates the live Three.js object directly:
198
+
199
+ ```tsx
200
+ import type { Component } from "react-three-game";
201
+
202
+ const Rotator: Component = {
203
+ name: "Rotator",
204
+ Editor: () => null,
205
+ defaultProperties: { speed: 1 },
206
+ create(ctx) {
207
+ return {
208
+ update(dt) {
209
+ const speed = ctx.component.get<number>("speed") ?? 1;
210
+ ctx.object.rotation.y += dt * speed;
211
+ },
212
+ };
213
+ },
214
+ };
215
+ ```
216
+
217
+ `View` handles composition and rendering. `create(ctx)` handles imperative runtime behavior.
218
+
156
219
  ## Useful Exports
157
220
 
158
221
  * `GameCanvas`
159
222
  * `PrefabRoot`
160
223
  * `PrefabEditor`
224
+ * `PrefabEditorMode`
161
225
  * `Prefab`
162
226
  * `GameObject`
163
227
  * `Scene`
164
228
  * `Entity`
165
229
  * `EntityComponent`
166
230
  * `registerComponent`
231
+ * `createPrefabStore`
232
+ * `usePrefabStoreApi`
233
+ * `useAssetRuntime()` / `useEntityRuntime()`
234
+ * `useEntityObjectRef()` / `useEntityRigidBodyRef()`
167
235
  * `ground(...)`
168
236
  * `loadJson()` / `saveJson()`
169
237
  * `loadModel()` / `loadTexture()`
170
- * `exportGLB()` / `exportGLBData()`
238
+ * `loadSound()` / `loadFiles()`
239
+ * `exportGLB()`
240
+ * `computeParentWorldMatrix()`
171
241
 
172
242
  ## Development
173
243
 
package/dist/index.d.ts CHANGED
@@ -20,12 +20,16 @@ export { createModelNode, createImageNode, } from './tools/prefabeditor/prefab';
20
20
  export type { PrefabEditorProps, PrefabEditorRef, } from './tools/prefabeditor/PrefabEditor';
21
21
  export type { SpawnOptions, Scene, Entity, EntityComponent, EntityData, EntityUpdate, PropertyPath, SceneUpdates, } from './tools/prefabeditor/scene';
22
22
  export type { PrefabRootProps } from './tools/prefabeditor/PrefabRoot';
23
- export type { AssetRuntime, EntityRuntime, LiveObjectRef, LiveRigidBodyRef } from './tools/prefabeditor/runtimeContext';
24
- export { useAssetRuntime, useEntityRuntime, useEntityObjectRef, useEntityRigidBodyRef } from './tools/prefabeditor/runtimeContext';
23
+ export type { AssetRuntime, EntityRuntime, LiveObjectRef, LiveRigidBodyRef } from './tools/prefabeditor/runtime';
24
+ export { useAssetRuntime, useEntityRuntime, useEntityObjectRef, useEntityRigidBodyRef } from './tools/prefabeditor/runtime';
25
+ export type { ComponentInstance, ComponentRuntimeContext } from './tools/prefabeditor/runtime';
25
26
  export type { Component, ComponentViewProps } from './tools/prefabeditor/components/ComponentRegistry';
26
27
  export type { FieldDefinition, FieldType } from './tools/prefabeditor/components/Input';
28
+ export { MaterialOverridesProvider, useMaterialOverrides } from './tools/prefabeditor/components/MaterialComponent';
29
+ export type { MaterialOverrides } from './tools/prefabeditor/components/MaterialComponent';
27
30
  export type { Prefab, GameObject, ComponentData } from './tools/prefabeditor/types';
28
31
  export { findComponent, findComponentEntry, hasComponent } from './tools/prefabeditor/types';
32
+ export { float, positionLocal, sin, time, uniform, vec3, } from 'three/tsl';
29
33
  export { gameEvents, useGameEvent, usePhysicsEvent, useClickEvent, getEntityIdFromRigidBody } from './tools/prefabeditor/GameEvents';
30
34
  export type { GameEventType, GameEventMap, GameEventPayload, PhysicsEventType, InteractionEventType, PhysicsEventPayload, ClickEventPayload } from './tools/prefabeditor/GameEvents';
31
35
  export { loadFiles } from './tools/dragdrop/DragDropLoader';
package/dist/index.js CHANGED
@@ -19,8 +19,10 @@ export { FieldRenderer, FieldGroup, ListEditor, Label, Vector3Input, Vector3Fiel
19
19
  // Prefab Editor - Utils
20
20
  export { loadJson, saveJson, exportGLB, exportGLBData, regenerateIds, computeParentWorldMatrix, } from './tools/prefabeditor/utils';
21
21
  export { createModelNode, createImageNode, } from './tools/prefabeditor/prefab';
22
- export { useAssetRuntime, useEntityRuntime, useEntityObjectRef, useEntityRigidBodyRef } from './tools/prefabeditor/runtimeContext';
22
+ export { useAssetRuntime, useEntityRuntime, useEntityObjectRef, useEntityRigidBodyRef } from './tools/prefabeditor/runtime';
23
+ export { MaterialOverridesProvider, useMaterialOverrides } from './tools/prefabeditor/components/MaterialComponent';
23
24
  export { findComponent, findComponentEntry, hasComponent } from './tools/prefabeditor/types';
25
+ export { float, positionLocal, sin, time, uniform, vec3, } from 'three/tsl';
24
26
  // Game Events (physics + custom events)
25
27
  export { gameEvents, useGameEvent, usePhysicsEvent, useClickEvent, getEntityIdFromRigidBody } from './tools/prefabeditor/GameEvents';
26
28
  // Asset Loading
@@ -13,6 +13,7 @@ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-run
13
13
  import { useHelper } from "@react-three/drei";
14
14
  import { forwardRef, useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState } from "react";
15
15
  import { BoxHelper, Euler, Matrix4, } from "three";
16
+ import { useFrame } from "@react-three/fiber";
16
17
  import { useStore } from "zustand";
17
18
  import { useClickValid } from "./useClickValid";
18
19
  import { findComponent } from "./types";
@@ -22,9 +23,8 @@ import { loadModel, loadSound, loadTexture } from "../dragdrop";
22
23
  import { GameInstance, GameInstanceProvider, getRepeatAxesFromModelProperties, useInstanceCheck } from "./InstanceProvider";
23
24
  import { composeTransform, decompose } from "./utils";
24
25
  import { isPhysicsProps } from "./components/PhysicsComponent";
25
- import { denormalizePrefab } from "./prefab";
26
26
  import { createPrefabStore, PrefabStoreProvider, useOptionalPrefabStoreApi, usePrefabChildIds, usePrefabNode, usePrefabRootId } from "./prefabStore";
27
- import { AssetRuntimeContext, EntityRuntimeScope } from "./runtimeContext";
27
+ import { AssetRuntimeContext, EntityRuntimeScope, createRuntimeEngine } from "./runtime";
28
28
  import { sound as soundManager } from "../../helpers/SoundManager";
29
29
  builtinComponents.forEach(registerComponent);
30
30
  const IDENTITY = new Matrix4();
@@ -61,7 +61,13 @@ export const PrefabRoot = forwardRef(({ editMode, data, store, selectedId, onSel
61
61
  const rigidBodyRefs = useRef(new Map());
62
62
  const rootRef = useRef(null);
63
63
  const parentStore = useOptionalPrefabStoreApi();
64
- const [ownedStore] = useState(() => { var _a, _b; return createPrefabStore(data !== null && data !== void 0 ? data : denormalizePrefab((_b = (_a = store === null || store === void 0 ? void 0 : store.getState()) !== null && _a !== void 0 ? _a : parentStore === null || parentStore === void 0 ? void 0 : parentStore.getState()) !== null && _b !== void 0 ? _b : missingStoreState())); });
64
+ const [ownedStore] = useState(() => {
65
+ if (data)
66
+ return createPrefabStore(data);
67
+ if (store || parentStore)
68
+ return null;
69
+ throw new Error("PrefabRoot requires either a `data` or `store` prop");
70
+ });
65
71
  const resolvedStore = (_a = store !== null && store !== void 0 ? store : parentStore) !== null && _a !== void 0 ? _a : ownedStore;
66
72
  const usesOwnedStore = resolvedStore === ownedStore;
67
73
  const shouldProvideStoreContext = !parentStore || parentStore !== resolvedStore;
@@ -73,6 +79,10 @@ export const PrefabRoot = forwardRef(({ editMode, data, store, selectedId, onSel
73
79
  var _a;
74
80
  return (_a = objectRefs.current[id]) !== null && _a !== void 0 ? _a : null;
75
81
  }, []);
82
+ const getRigidBody = useCallback((id) => {
83
+ var _a;
84
+ return (_a = rigidBodyRefs.current.get(id)) !== null && _a !== void 0 ? _a : null;
85
+ }, []);
76
86
  useImperativeHandle(ref, () => ({
77
87
  root: rootRef.current,
78
88
  getObject,
@@ -84,17 +94,29 @@ export const PrefabRoot = forwardRef(({ editMode, data, store, selectedId, onSel
84
94
  setInjectedSounds(prev => (Object.assign(Object.assign({}, prev), { [path]: sound })));
85
95
  },
86
96
  }), [getObject]);
97
+ const runtimeEngine = useMemo(() => createRuntimeEngine({
98
+ store: resolvedStore,
99
+ getObject,
100
+ getRigidBody,
101
+ }), [resolvedStore, getObject, getRigidBody]);
87
102
  const registerRef = useCallback((id, obj) => {
88
103
  objectRefs.current[id] = obj;
104
+ runtimeEngine.invalidate();
89
105
  onObjectRefChange === null || onObjectRefChange === void 0 ? void 0 : onObjectRefChange(id, obj);
90
- }, [onObjectRefChange]);
106
+ }, [onObjectRefChange, runtimeEngine]);
91
107
  const registerRigidBodyRef = useCallback((id, rb) => {
92
108
  rigidBodyRefs.current.set(id, rb);
93
- }, []);
94
- const getRigidBody = useCallback((id) => {
95
- var _a;
96
- return (_a = rigidBodyRefs.current.get(id)) !== null && _a !== void 0 ? _a : null;
97
- }, []);
109
+ runtimeEngine.invalidate();
110
+ }, [runtimeEngine]);
111
+ useEffect(() => {
112
+ runtimeEngine.setActive(!editMode);
113
+ }, [editMode, runtimeEngine]);
114
+ useEffect(() => {
115
+ return () => runtimeEngine.dispose();
116
+ }, [runtimeEngine]);
117
+ useFrame((_, dt) => {
118
+ runtimeEngine.tick(dt);
119
+ });
98
120
  useEffect(() => {
99
121
  if (usesOwnedStore && data) {
100
122
  resolvedStore.getState().replacePrefab(data);
@@ -157,26 +179,15 @@ export const PrefabRoot = forwardRef(({ editMode, data, store, selectedId, onSel
157
179
  };
158
180
  syncAssets();
159
181
  }, [resolvedStore, assetManifestKey, basePath, injectedModels, injectedSounds, injectedTextures, models, sounds, textures]);
160
- // Keep refs current so context getters are always fresh without changing context identity
161
- const availableModelsRef = useRef(availableModels);
162
- availableModelsRef.current = availableModels;
163
- const availableTexturesRef = useRef(availableTextures);
164
- availableTexturesRef.current = availableTextures;
165
- const availableSoundsRef = useRef(availableSounds);
166
- availableSoundsRef.current = availableSounds;
167
182
  const assetRuntime = useMemo(() => ({
168
183
  getObject,
169
184
  getRigidBody,
170
185
  registerRigidBodyRef,
171
- getModel: (path) => { var _a; return (_a = availableModelsRef.current[path]) !== null && _a !== void 0 ? _a : null; },
172
- getTexture: (path) => { var _a; return (_a = availableTexturesRef.current[path]) !== null && _a !== void 0 ? _a : null; },
173
- getSound: (path) => { var _a; return (_a = availableSoundsRef.current[path]) !== null && _a !== void 0 ? _a : null; },
174
- getAssetRevision: () => {
175
- const modelKeys = Object.keys(availableModelsRef.current).sort().join('|');
176
- const textureKeys = Object.keys(availableTexturesRef.current).sort().join('|');
177
- return `${textureKeys}::${modelKeys}`;
178
- },
179
- }), [getObject, getRigidBody, registerRigidBodyRef]);
186
+ getModel: (path) => { var _a; return (_a = availableModels[path]) !== null && _a !== void 0 ? _a : null; },
187
+ getTexture: (path) => { var _a; return (_a = availableTextures[path]) !== null && _a !== void 0 ? _a : null; },
188
+ getSound: (path) => { var _a; return (_a = availableSounds[path]) !== null && _a !== void 0 ? _a : null; },
189
+ getAssetRevision: () => `${Object.keys(availableTextures).sort().join('|')}::${Object.keys(availableModels).sort().join('|')}`,
190
+ }), [getObject, getRigidBody, registerRigidBodyRef, availableModels, availableTextures, availableSounds]);
180
191
  const content = (_jsx("group", { ref: rootRef, children: _jsx(GameInstanceProvider, { models: availableModels, selectedId: selectedId, editMode: editMode, onSelect: editMode ? onSelect : undefined, registerRef: registerRef, children: _jsx(StoreRootNode, { selectedId: selectedId, onSelect: editMode ? onSelect : undefined, onClick: onClick, registerRef: registerRef, loadedModels: availableModels, editMode: editMode, parentMatrix: IDENTITY }) }) }));
181
192
  if (!shouldProvideStoreContext) {
182
193
  return _jsx(AssetRuntimeContext.Provider, { value: assetRuntime, children: content });
@@ -197,6 +208,7 @@ export function GameObjectRenderer(props) {
197
208
  if (prevInstancedRef.current !== undefined && prevInstancedRef.current !== isInstanced) {
198
209
  setIsTransitioning(true);
199
210
  const timer = setTimeout(() => setIsTransitioning(false), 100);
211
+ prevInstancedRef.current = isInstanced;
200
212
  return () => clearTimeout(timer);
201
213
  }
202
214
  prevInstancedRef.current = isInstanced;
@@ -225,18 +237,18 @@ function InstancedNode({ nodeId, parentMatrix = IDENTITY, editMode, registerRef,
225
237
  const modelUrl = (_c = (_b = findComponent(gameObject, "Model")) === null || _b === void 0 ? void 0 : _b.properties) === null || _c === void 0 ? void 0 : _c.filename;
226
238
  const instances = useMemo(() => buildRepeatedInstances(gameObject, parentMatrix, modelUrl, physicsProps), [gameObject, modelUrl, parentMatrix, physicsProps]);
227
239
  const groupRef = useRef(null);
240
+ const handleGroupRef = useCallback((object) => {
241
+ groupRef.current = object;
242
+ if (editMode) {
243
+ registerRef(nodeId, object);
244
+ }
245
+ }, [editMode, nodeId, registerRef]);
228
246
  const editClickHandlers = useClickValid(!!editMode && !isLocked, (e) => {
229
247
  onSelect === null || onSelect === void 0 ? void 0 : onSelect(nodeId);
230
248
  onClick === null || onClick === void 0 ? void 0 : onClick(e, gameObject);
231
249
  });
232
- useEffect(() => {
233
- if (editMode) {
234
- registerRef(nodeId, groupRef.current);
235
- return () => registerRef(nodeId, null);
236
- }
237
- }, [nodeId, registerRef, editMode]);
238
250
  if (editMode) {
239
- return (_jsxs(_Fragment, { children: [_jsx("group", Object.assign({ ref: groupRef, position: localTransform.position, rotation: localTransform.rotation, scale: localTransform.scale }, editClickHandlers, { children: _jsx("mesh", { visible: false, children: _jsx("boxGeometry", { args: [0.01, 0.01, 0.01] }) }) })), instances.map(instance => (_jsx(GameInstance, { id: instance.id, sourceId: gameObject.id, clickable: clickable, clickEventName: clickEventName, modelUrl: instance.modelUrl, position: instance.position, rotation: instance.rotation, scale: instance.scale, locked: isLocked, physics: instance.physics }, instance.id)))] }));
251
+ return (_jsxs(_Fragment, { children: [_jsx("group", Object.assign({ ref: handleGroupRef, position: localTransform.position, rotation: localTransform.rotation, scale: localTransform.scale }, editClickHandlers, { children: _jsx("mesh", { visible: false, children: _jsx("boxGeometry", { args: [0.01, 0.01, 0.01] }) }) })), instances.map(instance => (_jsx(GameInstance, { id: instance.id, sourceId: gameObject.id, clickable: clickable, clickEventName: clickEventName, modelUrl: instance.modelUrl, position: instance.position, rotation: instance.rotation, scale: instance.scale, locked: isLocked, physics: instance.physics }, instance.id)))] }));
240
252
  }
241
253
  return (_jsx(_Fragment, { children: instances.map(instance => (_jsx(GameInstance, { id: instance.id, sourceId: gameObject.id, clickable: clickable, clickEventName: clickEventName, modelUrl: instance.modelUrl, position: instance.position, rotation: instance.rotation, scale: instance.scale, locked: isLocked, physics: instance.physics }, instance.id))) }));
242
254
  }
@@ -245,21 +257,25 @@ function StandardNode({ nodeId, selectedId, onSelect, onClick, registerRef, load
245
257
  const gameObject = usePrefabNode(nodeId);
246
258
  const childIds = usePrefabChildIds(nodeId);
247
259
  const isSelected = selectedId === nodeId;
248
- if (!gameObject)
249
- return null;
260
+ const isLocked = Boolean(gameObject === null || gameObject === void 0 ? void 0 : gameObject.locked);
261
+ const stillInstanced = useInstanceCheck(nodeId);
250
262
  const groupRef = useRef(null);
251
263
  const helperRef = useRef(null);
252
- const isLocked = Boolean(gameObject.locked);
253
- const stillInstanced = useInstanceCheck(nodeId);
264
+ const handleGroupRef = useCallback((object) => {
265
+ groupRef.current = object;
266
+ registerRef(nodeId, object);
267
+ }, [nodeId, registerRef]);
268
+ const handleHelperRef = useCallback((object) => {
269
+ helperRef.current = object;
270
+ }, []);
254
271
  const clickHandlers = useClickValid(!!editMode && !isLocked, (e) => {
255
272
  onSelect === null || onSelect === void 0 ? void 0 : onSelect(nodeId);
256
- onClick === null || onClick === void 0 ? void 0 : onClick(e, gameObject);
273
+ if (gameObject)
274
+ onClick === null || onClick === void 0 ? void 0 : onClick(e, gameObject);
257
275
  });
258
276
  useHelper(editMode && isSelected ? helperRef : null, BoxHelper, "cyan");
259
- useEffect(() => {
260
- registerRef(nodeId, groupRef.current);
261
- return () => registerRef(nodeId, null);
262
- }, [nodeId, registerRef]);
277
+ if (!gameObject)
278
+ return null;
263
279
  const world = parentMatrix.clone().multiply(compose(gameObject));
264
280
  const physics = findComponent(gameObject, "Physics");
265
281
  const ready = isNodeReady(gameObject, loadedModels);
@@ -272,10 +288,11 @@ function StandardNode({ nodeId, selectedId, onSelect, onClick, registerRef, load
272
288
  const childNodes = _jsx(ChildNodes, { childIds: childIds, parentMatrix: world, selectedId: selectedId, onSelect: onSelect, onClick: onClick, registerRef: registerRef, loadedModels: loadedModels, editMode: editMode });
273
289
  const inner = (_jsx("group", Object.assign({}, clickHandlers, { children: renderCompositionNode(gameObject, renderCtx, childNodes) })));
274
290
  const physicsInner = editMode ? _jsx("group", { visible: false, children: inner }) : inner;
275
- return (_jsx(EntityRuntimeScope, { nodeId: nodeId, editMode: editMode, isSelected: isSelected, children: editMode ? (_jsxs(_Fragment, { children: [_jsx("group", { ref: groupRef, position: transform.position, rotation: transform.rotation, scale: transform.scale, children: _jsx("mesh", { visible: false, children: _jsx("boxGeometry", { args: [0.01, 0.01, 0.01] }) }) }), _jsx("group", { ref: helperRef, position: transform.position, rotation: transform.rotation, scale: transform.scale, children: inner }), hasPhysics && (physicsDef === null || physicsDef === void 0 ? void 0 : physicsDef.View) ? (_jsx(physicsDef.View, { properties: physics.properties, position: transform.position, rotation: transform.rotation, scale: transform.scale, children: physicsInner }, physicsKey)) : null] })) : hasPhysics && (physicsDef === null || physicsDef === void 0 ? void 0 : physicsDef.View) ? (_jsx(physicsDef.View, { properties: physics.properties, position: transform.position, rotation: transform.rotation, scale: transform.scale, children: inner }, physicsKey)) : (_jsx("group", { ref: groupRef, position: transform.position, rotation: transform.rotation, scale: transform.scale, children: inner })) }));
291
+ return (_jsx(EntityRuntimeScope, { nodeId: nodeId, editMode: editMode, isSelected: isSelected, children: editMode ? (_jsxs(_Fragment, { children: [_jsx("group", { ref: handleGroupRef, position: transform.position, rotation: transform.rotation, scale: transform.scale, children: _jsx("mesh", { visible: false, children: _jsx("boxGeometry", { args: [0.01, 0.01, 0.01] }) }) }), _jsx("group", { ref: handleHelperRef, position: transform.position, rotation: transform.rotation, scale: transform.scale, children: inner }), hasPhysics && (physicsDef === null || physicsDef === void 0 ? void 0 : physicsDef.View) ? (_jsx(physicsDef.View, { properties: physics.properties, position: transform.position, rotation: transform.rotation, scale: transform.scale, children: physicsInner }, physicsKey)) : null] })) : hasPhysics && (physicsDef === null || physicsDef === void 0 ? void 0 : physicsDef.View) ? (_jsx(physicsDef.View, { properties: physics.properties, position: transform.position, rotation: transform.rotation, scale: transform.scale, children: inner }, physicsKey)) : (_jsx("group", { ref: handleGroupRef, position: transform.position, rotation: transform.rotation, scale: transform.scale, children: inner })) }));
276
292
  }
277
293
  function isRendererHandledComponent(componentType) {
278
294
  return componentType === "Transform"
295
+ || componentType === "BufferGeometry"
279
296
  || componentType === "Geometry"
280
297
  || componentType === "Material"
281
298
  || componentType === "Physics"
@@ -284,7 +301,6 @@ function isRendererHandledComponent(componentType) {
284
301
  function getCompositionComponents(gameObject) {
285
302
  var _a;
286
303
  return Object.entries((_a = gameObject.components) !== null && _a !== void 0 ? _a : {}).reduce((result, [key, comp]) => {
287
- var _a;
288
304
  if (!(comp === null || comp === void 0 ? void 0 : comp.type) || isRendererHandledComponent(comp.type))
289
305
  return result;
290
306
  const def = getComponentDef(comp.type);
@@ -294,7 +310,6 @@ function getCompositionComponents(gameObject) {
294
310
  key,
295
311
  View: def.View,
296
312
  properties: comp.properties,
297
- composition: (_a = def.composition) !== null && _a !== void 0 ? _a : "wrap",
298
313
  });
299
314
  return result;
300
315
  }, []);
@@ -373,32 +388,27 @@ function renderCompositionNode(gameObject, ctx, childNodes) {
373
388
  return applyNodeComposition(gameObject, _jsxs(_Fragment, { children: [primaryContent, childNodes] }));
374
389
  }
375
390
  function renderNodePrimaryContent(gameObject, ctx) {
376
- var _a, _b;
377
- const geometry = findComponent(gameObject, "Geometry");
391
+ var _a, _b, _c;
392
+ const geometry = (_a = findComponent(gameObject, "BufferGeometry")) !== null && _a !== void 0 ? _a : findComponent(gameObject, "Geometry");
378
393
  const material = findComponent(gameObject, "Material");
379
394
  const model = findComponent(gameObject, "Model");
380
395
  const geometryDef = geometry && getComponentDef(geometry.type);
381
396
  const materialDef = material && getComponentDef(material.type);
382
397
  const modelDef = model && getComponentDef(model.type);
383
- const geometryProperties = (_a = geometry === null || geometry === void 0 ? void 0 : geometry.properties) !== null && _a !== void 0 ? _a : {};
398
+ const geometryProperties = (_b = geometry === null || geometry === void 0 ? void 0 : geometry.properties) !== null && _b !== void 0 ? _b : {};
384
399
  const meshVisible = geometryProperties.visible !== false;
385
400
  const meshCastShadow = meshVisible && geometryProperties.castShadow !== false;
386
401
  const meshReceiveShadow = meshVisible && geometryProperties.receiveShadow !== false;
387
402
  if ((geometry === null || geometry === void 0 ? void 0 : geometry.type) && (geometryDef === null || geometryDef === void 0 ? void 0 : geometryDef.View)) {
388
403
  return (_jsxs("mesh", { visible: meshVisible, castShadow: meshCastShadow, receiveShadow: meshReceiveShadow, children: [_jsx(geometryDef.View, { properties: geometry.properties }), material && (materialDef === null || materialDef === void 0 ? void 0 : materialDef.View) && (_jsx(materialDef.View, { properties: material.properties }, "material"))] }));
389
404
  }
390
- if ((model === null || model === void 0 ? void 0 : model.type) && (modelDef === null || modelDef === void 0 ? void 0 : modelDef.View) && !((_b = model.properties) === null || _b === void 0 ? void 0 : _b.instanced) && isNodeReady(gameObject, ctx.loadedModels)) {
405
+ if ((model === null || model === void 0 ? void 0 : model.type) && (modelDef === null || modelDef === void 0 ? void 0 : modelDef.View) && !((_c = model.properties) === null || _c === void 0 ? void 0 : _c.instanced) && isNodeReady(gameObject, ctx.loadedModels)) {
391
406
  return _jsx(modelDef.View, { properties: model.properties });
392
407
  }
393
408
  return null;
394
409
  }
395
410
  function applyNodeComposition(gameObject, subtree) {
396
411
  const components = getCompositionComponents(gameObject);
397
- return components.reduce((acc, { key, View, properties, composition }) => composition === "sibling"
398
- ? (_jsxs(_Fragment, { children: [_jsx(View, { properties: properties }, key), acc] }))
399
- : (_jsx(View, { properties: properties, children: acc }, key)), subtree);
412
+ return components.reduce((acc, { key, View, properties }) => (_jsx(View, { properties: properties, children: acc }, key)), subtree);
400
413
  }
401
414
  export default PrefabRoot;
402
- function missingStoreState() {
403
- throw new Error("PrefabRoot requires either data or store");
404
- }
@@ -0,0 +1,3 @@
1
+ import { Component } from "./ComponentRegistry";
2
+ declare const BufferGeometryComponent: Component;
3
+ export default BufferGeometryComponent;
@@ -0,0 +1,96 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { BooleanField, FieldGroup } from "./Input";
3
+ const DEFAULT_TRIANGLE_POSITIONS = [
4
+ 0, 0, 0,
5
+ 1, 0, 0,
6
+ 0, 1, 0,
7
+ ];
8
+ const DEFAULT_TRIANGLE_INDICES = [0, 1, 2];
9
+ const DEFAULT_TRIANGLE_UVS = [
10
+ 0, 0,
11
+ 1, 0,
12
+ 0, 1,
13
+ ];
14
+ function isFiniteNumberArray(value) {
15
+ return Array.isArray(value) && value.every(entry => typeof entry === 'number' && Number.isFinite(entry));
16
+ }
17
+ function normalizeNumberArray(value, fallback) {
18
+ return isFiniteNumberArray(value) ? value : fallback;
19
+ }
20
+ function toAttributeText(value, fallback) {
21
+ return JSON.stringify(normalizeNumberArray(value, fallback));
22
+ }
23
+ function parseArrayInput(raw) {
24
+ const trimmed = raw.trim();
25
+ if (!trimmed)
26
+ return [];
27
+ const parsed = JSON.parse(trimmed);
28
+ if (!isFiniteNumberArray(parsed)) {
29
+ throw new Error('Expected a JSON array of numbers');
30
+ }
31
+ return parsed;
32
+ }
33
+ function getIndexArray(indices) {
34
+ if (indices.length === 0)
35
+ return null;
36
+ const maxIndex = Math.max(...indices);
37
+ return maxIndex > 65535 ? new Uint32Array(indices) : new Uint16Array(indices);
38
+ }
39
+ function BufferArrayField({ label, value, fallback, onChange, rows = 4, }) {
40
+ return (_jsxs("label", { style: { display: 'grid', gap: 4 }, children: [_jsx("span", { style: { fontSize: '10px', color: '#888', textTransform: 'uppercase', letterSpacing: '0.05em', fontWeight: 500 }, children: label }), _jsx("textarea", { rows: rows, spellCheck: false, defaultValue: toAttributeText(value, fallback), onBlur: (event) => {
41
+ try {
42
+ onChange(parseArrayInput(event.target.value));
43
+ event.target.setCustomValidity('');
44
+ }
45
+ catch (_a) {
46
+ event.target.setCustomValidity('Expected a JSON array of numbers');
47
+ event.target.reportValidity();
48
+ }
49
+ }, style: {
50
+ width: '100%',
51
+ backgroundColor: '#171717',
52
+ border: '1px solid #333',
53
+ padding: '6px 8px',
54
+ fontSize: '11px',
55
+ color: '#eee',
56
+ fontFamily: 'monospace',
57
+ outline: 'none',
58
+ borderRadius: 3,
59
+ resize: 'vertical',
60
+ boxSizing: 'border-box',
61
+ } })] }));
62
+ }
63
+ function BufferGeometryComponentEditor({ component, onUpdate, }) {
64
+ var _a;
65
+ const properties = (_a = component.properties) !== null && _a !== void 0 ? _a : {};
66
+ return (_jsxs(FieldGroup, { children: [_jsx(BufferArrayField, { label: "Positions", value: properties.positions, fallback: DEFAULT_TRIANGLE_POSITIONS, rows: 5, onChange: (positions) => onUpdate({ positions }) }), _jsx(BufferArrayField, { label: "Indices", value: properties.indices, fallback: DEFAULT_TRIANGLE_INDICES, onChange: (indices) => onUpdate({ indices }) }), _jsx(BufferArrayField, { label: "Normals", value: properties.normals, fallback: [], onChange: (normals) => onUpdate({ normals }) }), _jsx(BufferArrayField, { label: "UVs", value: properties.uvs, fallback: DEFAULT_TRIANGLE_UVS, onChange: (uvs) => onUpdate({ uvs }) }), _jsx(BooleanField, { name: "computeVertexNormals", label: "Compute Normals", values: properties, onChange: onUpdate, fallback: true }), _jsx(BooleanField, { name: "visible", label: "Visible", values: properties, onChange: onUpdate, fallback: true }), _jsx(BooleanField, { name: "castShadow", label: "Cast Shadow", values: properties, onChange: onUpdate, fallback: true }), _jsx(BooleanField, { name: "receiveShadow", label: "Receive Shadow", values: properties, onChange: onUpdate, fallback: true })] }));
67
+ }
68
+ function BufferGeometryComponentView({ properties }) {
69
+ const positions = normalizeNumberArray(properties.positions, DEFAULT_TRIANGLE_POSITIONS);
70
+ const indices = normalizeNumberArray(properties.indices, DEFAULT_TRIANGLE_INDICES);
71
+ const normals = normalizeNumberArray(properties.normals, []);
72
+ const uvs = normalizeNumberArray(properties.uvs, DEFAULT_TRIANGLE_UVS);
73
+ const indexArray = getIndexArray(indices);
74
+ const hasNormals = normals.length >= 3 && normals.length % 3 === 0;
75
+ const hasUvs = uvs.length >= 2 && uvs.length % 2 === 0;
76
+ return (_jsxs("bufferGeometry", { onUpdate: (geometry) => {
77
+ if (properties.computeVertexNormals !== false && !hasNormals) {
78
+ geometry.computeVertexNormals();
79
+ }
80
+ geometry.computeBoundingBox();
81
+ geometry.computeBoundingSphere();
82
+ }, children: [_jsx("bufferAttribute", { attach: "attributes-position", args: [new Float32Array(positions), 3] }), indexArray ? (_jsx("bufferAttribute", { attach: "index", args: [indexArray, 1] })) : null, hasNormals ? (_jsx("bufferAttribute", { attach: "attributes-normal", args: [new Float32Array(normals), 3] })) : null, hasUvs ? (_jsx("bufferAttribute", { attach: "attributes-uv", args: [new Float32Array(uvs), 2] })) : null] }));
83
+ }
84
+ const BufferGeometryComponent = {
85
+ name: 'BufferGeometry',
86
+ Editor: BufferGeometryComponentEditor,
87
+ View: BufferGeometryComponentView,
88
+ defaultProperties: {
89
+ positions: DEFAULT_TRIANGLE_POSITIONS,
90
+ indices: DEFAULT_TRIANGLE_INDICES,
91
+ normals: [],
92
+ uvs: DEFAULT_TRIANGLE_UVS,
93
+ computeVertexNormals: true,
94
+ },
95
+ };
96
+ export default BufferGeometryComponent;
@@ -3,7 +3,7 @@ import { OrthographicCamera, PerspectiveCamera, useHelper } from '@react-three/d
3
3
  import { useRef } from 'react';
4
4
  import { CameraHelper } from 'three';
5
5
  import { useFrame, useThree } from '@react-three/fiber';
6
- import { useEntityRuntime } from '../runtimeContext';
6
+ import { useEntityRuntime } from '../runtime';
7
7
  import { FieldGroup, NumberField, SelectField } from './Input';
8
8
  const CAMERA_PROJECTION_OPTIONS = [
9
9
  { value: 'perspective', label: 'Perspective' },
@@ -66,7 +66,6 @@ const CameraComponent = {
66
66
  name: 'Camera',
67
67
  Editor: CameraComponentEditor,
68
68
  View: CameraComponentView,
69
- composition: 'wrap',
70
69
  defaultProperties: cameraDefaults,
71
70
  };
72
71
  export default CameraComponent;
@@ -1,7 +1,7 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  import { useRef } from 'react';
3
3
  import { gameEvents } from '../GameEvents';
4
- import { useEntityRuntime } from '../runtimeContext';
4
+ import { useEntityRuntime } from '../runtime';
5
5
  import { EventField, FieldGroup } from './Input';
6
6
  function ClickComponentEditor({ component, onUpdate }) {
7
7
  return (_jsxs(FieldGroup, { children: [_jsx("div", { style: { fontSize: 12, opacity: 0.8 }, children: "Emits a game event in play mode when this entity is clicked." }), _jsx(EventField, { name: "eventName", label: "Emit Event", values: component.properties, onChange: onUpdate, placeholder: "click" })] }));
@@ -1,5 +1,6 @@
1
1
  import { FC } from "react";
2
2
  import { ComponentData, GameObject } from "../types";
3
+ import type { ComponentInstance, ComponentRuntimeContext } from "../runtime";
3
4
  export type AssetRef = {
4
5
  type: "model" | "texture" | "sound";
5
6
  path: string;
@@ -27,11 +28,8 @@ export interface Component {
27
28
  }>;
28
29
  defaultProperties: any;
29
30
  View?: FC<ComponentViewProps>;
30
- /**
31
- * How this component participates in the implicit node composition pipeline.
32
- * Defaults to `wrap`; `sibling` renders next to the current subtree.
33
- */
34
- composition?: "wrap" | "sibling";
31
+ /** Optional runtime factory for the non-React game loop. */
32
+ create?: (ctx: ComponentRuntimeContext) => ComponentInstance | void;
35
33
  /** Declare which asset paths this component references (for asset loading). */
36
34
  getAssetRefs?: (properties: Record<string, any>) => AssetRef[];
37
35
  }
@@ -2,8 +2,8 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
2
2
  import { useHelper } from "@react-three/drei";
3
3
  import { useRef, useEffect, useState } from "react";
4
4
  import { useFrame } from "@react-three/fiber";
5
- import { CameraHelper, Vector3 } from "three";
6
- import { useEntityRuntime } from "../runtimeContext";
5
+ import { CameraHelper } from "three";
6
+ import { useEntityRuntime } from "../runtime";
7
7
  import { BooleanField, ColorField, NumberField, NumberInput, Vector3Input } from "./Input";
8
8
  import { LightSection, ShadowBiasField, mergeWithDefaults } from "./lightUtils";
9
9
  import { colors } from "../styles";
@@ -161,13 +161,7 @@ function DirectionalLightView({ properties, children }) {
161
161
  shadowCamera.updateMatrixWorld();
162
162
  }
163
163
  });
164
- return (_jsxs(_Fragment, { children: [_jsx("directionalLight", { ref: directionalLightRef, color: color, intensity: intensity, castShadow: castShadow, "shadow-mapSize-width": shadowMapSize, "shadow-mapSize-height": shadowMapSize, "shadow-camera-near": shadowCameraNear, "shadow-camera-far": shadowCameraFar, "shadow-camera-top": shadowCameraTop, "shadow-camera-bottom": shadowCameraBottom, "shadow-camera-left": shadowCameraLeft, "shadow-camera-right": shadowCameraRight, "shadow-bias": shadowBias, "shadow-normalBias": shadowNormalBias, "shadow-autoUpdate": shadowAutoUpdate }), _jsx("object3D", { ref: targetRef, position: targetOffset }), editMode && isSelected && (_jsxs(_Fragment, { children: [_jsxs("mesh", { children: [_jsx("sphereGeometry", { args: [0.3, 8, 6] }), _jsx("meshBasicMaterial", { color: color, wireframe: true })] }), _jsxs("mesh", { position: targetOffset, children: [_jsx("sphereGeometry", { args: [0.2, 8, 6] }), _jsx("meshBasicMaterial", { color: color, wireframe: true, opacity: 0.5, transparent: true })] }), _jsxs("line", { children: [_jsx("bufferGeometry", { onUpdate: (geo) => {
165
- const points = [
166
- new Vector3(0, 0, 0),
167
- new Vector3(targetOffset[0], targetOffset[1], targetOffset[2])
168
- ];
169
- geo.setFromPoints(points);
170
- } }), _jsx("lineBasicMaterial", { color: color, opacity: 0.6, transparent: true })] })] })), children] }));
164
+ return (_jsxs(_Fragment, { children: [_jsx("directionalLight", { ref: directionalLightRef, color: color, intensity: intensity, castShadow: castShadow, "shadow-mapSize-width": shadowMapSize, "shadow-mapSize-height": shadowMapSize, "shadow-camera-near": shadowCameraNear, "shadow-camera-far": shadowCameraFar, "shadow-camera-top": shadowCameraTop, "shadow-camera-bottom": shadowCameraBottom, "shadow-camera-left": shadowCameraLeft, "shadow-camera-right": shadowCameraRight, "shadow-bias": shadowBias, "shadow-normalBias": shadowNormalBias, "shadow-autoUpdate": shadowAutoUpdate }), _jsx("object3D", { ref: targetRef, position: targetOffset }), editMode && isSelected && (_jsxs(_Fragment, { children: [_jsxs("mesh", { children: [_jsx("sphereGeometry", { args: [0.3, 8, 6] }), _jsx("meshBasicMaterial", { color: color, wireframe: true })] }), _jsxs("mesh", { position: targetOffset, children: [_jsx("sphereGeometry", { args: [0.2, 8, 6] }), _jsx("meshBasicMaterial", { color: color, wireframe: true, opacity: 0.5, transparent: true })] }), _jsxs("line", { children: [_jsx("bufferGeometry", { children: _jsx("bufferAttribute", { attach: "attributes-position", args: [new Float32Array([0, 0, 0, targetOffset[0], targetOffset[1], targetOffset[2]]), 3] }) }), _jsx("lineBasicMaterial", { color: color, opacity: 0.6, transparent: true })] })] })), children] }));
171
165
  }
172
166
  const DirectionalLightComponent = {
173
167
  name: 'DirectionalLight',
@@ -1,7 +1,7 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { Environment } from '@react-three/drei';
3
3
  import { FieldGroup, NumberField } from './Input';
4
- import { useAssetRuntime } from '../runtimeContext';
4
+ import { useAssetRuntime } from '../runtime';
5
5
  function EnvironmentView({ properties, children, }) {
6
6
  const { getAssetRevision } = useAssetRuntime();
7
7
  const { intensity = 1, resolution = 256 } = properties;
@@ -1,3 +1,4 @@
1
+ import { type ReactNode } from 'react';
1
2
  import type { ThreeElement } from '@react-three/fiber';
2
3
  import { Component } from './ComponentRegistry';
3
4
  import { MeshBasicNodeMaterial, MeshStandardNodeMaterial } from 'three/webgpu';
@@ -14,13 +15,22 @@ export interface MaterialProps extends Omit<MeshStandardMaterialProperties & Mes
14
15
  thickness?: number;
15
16
  ior?: number;
16
17
  texture?: string;
18
+ offset?: [number, number];
17
19
  repeat?: boolean;
18
20
  repeatCount?: [number, number];
21
+ animateOffset?: boolean;
22
+ offsetSpeed?: [number, number];
19
23
  generateMipmaps?: boolean;
20
24
  minFilter?: string;
21
25
  magFilter?: string;
22
26
  normalMapTexture?: string;
23
27
  normalScale?: [number, number];
24
28
  }
29
+ export type MaterialOverrides = Record<string, unknown>;
30
+ export declare function useMaterialOverrides(): MaterialOverrides;
31
+ export declare function MaterialOverridesProvider({ overrides, children, }: {
32
+ overrides: MaterialOverrides;
33
+ children: ReactNode;
34
+ }): import("react/jsx-runtime").JSX.Element;
25
35
  declare const MaterialComponent: Component;
26
36
  export default MaterialComponent;