react-three-game 0.0.83 → 0.0.84

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -110,6 +110,16 @@ 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` + `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
+
113
123
  ## Prefab Format
114
124
 
115
125
  ```ts
@@ -131,7 +141,7 @@ interface GameObject {
131
141
 
132
142
  ## Runtime Mutation
133
143
 
134
- When you need to change the live world, use the `Scene` API from `PrefabEditorRef`.
144
+ Use the `Scene` API from `PrefabEditorRef` for authored state changes that should stay in prefab data.
135
145
 
136
146
  ```tsx
137
147
  import { useEffect, useRef } from "react";
@@ -153,21 +163,59 @@ function RaiseBall() {
153
163
  }
154
164
  ```
155
165
 
166
+ Batch related entity changes so they flush as one store revision:
167
+
168
+ ```tsx
169
+ scene.batch(() => {
170
+ scene.find("orb1")?.getComponent("Transform")?.set("position", [1, 0, 0]);
171
+ scene.find("orb2")?.getComponent("Transform")?.set("position", [-1, 0, 0]);
172
+ });
173
+ ```
174
+
175
+ Define a component runtime factory with `create(ctx)` for hot runtime behavior that mutates the live Three.js object directly:
176
+
177
+ ```tsx
178
+ import type { Component } from "react-three-game";
179
+
180
+ const Rotator: Component = {
181
+ name: "Rotator",
182
+ Editor: () => null,
183
+ defaultProperties: { speed: 1 },
184
+ create(ctx) {
185
+ return {
186
+ update(dt) {
187
+ const speed = ctx.component.get<number>("speed") ?? 1;
188
+ ctx.object.rotation.y += dt * speed;
189
+ },
190
+ };
191
+ },
192
+ };
193
+ ```
194
+
195
+ `View` handles composition and rendering. `create(ctx)` handles imperative runtime behavior.
196
+
156
197
  ## Useful Exports
157
198
 
158
199
  * `GameCanvas`
159
200
  * `PrefabRoot`
160
201
  * `PrefabEditor`
202
+ * `PrefabEditorMode`
161
203
  * `Prefab`
162
204
  * `GameObject`
163
205
  * `Scene`
164
206
  * `Entity`
165
207
  * `EntityComponent`
166
208
  * `registerComponent`
209
+ * `createPrefabStore`
210
+ * `usePrefabStoreApi`
211
+ * `useAssetRuntime()` / `useEntityRuntime()`
212
+ * `useEntityObjectRef()` / `useEntityRigidBodyRef()`
167
213
  * `ground(...)`
168
214
  * `loadJson()` / `saveJson()`
169
215
  * `loadModel()` / `loadTexture()`
170
- * `exportGLB()` / `exportGLBData()`
216
+ * `loadSound()` / `loadFiles()`
217
+ * `exportGLB()`
218
+ * `computeParentWorldMatrix()`
171
219
 
172
220
  ## Development
173
221
 
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,7 +288,7 @@ 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"
@@ -284,7 +300,6 @@ function isRendererHandledComponent(componentType) {
284
300
  function getCompositionComponents(gameObject) {
285
301
  var _a;
286
302
  return Object.entries((_a = gameObject.components) !== null && _a !== void 0 ? _a : {}).reduce((result, [key, comp]) => {
287
- var _a;
288
303
  if (!(comp === null || comp === void 0 ? void 0 : comp.type) || isRendererHandledComponent(comp.type))
289
304
  return result;
290
305
  const def = getComponentDef(comp.type);
@@ -294,7 +309,6 @@ function getCompositionComponents(gameObject) {
294
309
  key,
295
310
  View: def.View,
296
311
  properties: comp.properties,
297
- composition: (_a = def.composition) !== null && _a !== void 0 ? _a : "wrap",
298
312
  });
299
313
  return result;
300
314
  }, []);
@@ -394,11 +408,6 @@ function renderNodePrimaryContent(gameObject, ctx) {
394
408
  }
395
409
  function applyNodeComposition(gameObject, subtree) {
396
410
  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);
411
+ return components.reduce((acc, { key, View, properties }) => (_jsx(View, { properties: properties, children: acc }, key)), subtree);
400
412
  }
401
413
  export default PrefabRoot;
402
- function missingStoreState() {
403
- throw new Error("PrefabRoot requires either data or store");
404
- }
@@ -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';
@@ -22,5 +23,11 @@ export interface MaterialProps extends Omit<MeshStandardMaterialProperties & Mes
22
23
  normalMapTexture?: string;
23
24
  normalScale?: [number, number];
24
25
  }
26
+ export type MaterialOverrides = Record<string, unknown>;
27
+ export declare function useMaterialOverrides(): MaterialOverrides;
28
+ export declare function MaterialOverridesProvider({ overrides, children, }: {
29
+ overrides: MaterialOverrides;
30
+ children: ReactNode;
31
+ }): import("react/jsx-runtime").JSX.Element;
25
32
  declare const MaterialComponent: Component;
26
33
  export default MaterialComponent;
@@ -10,13 +10,23 @@ var __rest = (this && this.__rest) || function (s, e) {
10
10
  return t;
11
11
  };
12
12
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
13
+ import { createContext, useContext, useMemo } from 'react';
13
14
  import { extend } from '@react-three/fiber';
14
15
  import { FieldRenderer, Label, NumberInput } from './Input';
15
- import { useAssetRuntime } from '../runtimeContext';
16
- import { useMemo } from 'react';
16
+ import { useAssetRuntime } from '../runtime';
17
17
  import { MeshBasicNodeMaterial, MeshStandardNodeMaterial } from 'three/webgpu';
18
18
  import { TexturePicker } from '../../assetviewer/page';
19
- import { RepeatWrapping, ClampToEdgeWrapping, SRGBColorSpace, LinearSRGBColorSpace, Vector2, NearestFilter, LinearFilter, NearestMipmapNearestFilter, NearestMipmapLinearFilter, LinearMipmapNearestFilter, LinearMipmapLinearFilter, FrontSide, BackSide, DoubleSide, } from 'three';
19
+ import { RepeatWrapping, ClampToEdgeWrapping, SRGBColorSpace, LinearSRGBColorSpace, NearestFilter, LinearFilter, NearestMipmapNearestFilter, NearestMipmapLinearFilter, LinearMipmapNearestFilter, LinearMipmapLinearFilter, FrontSide, BackSide, DoubleSide, } from 'three';
20
+ const EMPTY_MATERIAL_OVERRIDES = Object.freeze({});
21
+ const MaterialOverridesContext = createContext(EMPTY_MATERIAL_OVERRIDES);
22
+ export function useMaterialOverrides() {
23
+ return useContext(MaterialOverridesContext);
24
+ }
25
+ export function MaterialOverridesProvider({ overrides, children, }) {
26
+ const parent = useContext(MaterialOverridesContext);
27
+ const merged = useMemo(() => (Object.assign(Object.assign({}, parent), overrides)), [parent, overrides]);
28
+ return _jsx(MaterialOverridesContext.Provider, { value: merged, children: children });
29
+ }
20
30
  extend({
21
31
  MeshBasicNodeMaterial,
22
32
  MeshStandardNodeMaterial,
@@ -121,7 +131,7 @@ function MaterialComponentEditor({ component, onUpdate, basePath = "" }) {
121
131
  }
122
132
  // View for Material component
123
133
  function MaterialComponentView({ properties: rawProps }) {
124
- var _a, _b, _c, _d, _e;
134
+ var _a, _b, _c, _d, _e, _f;
125
135
  const { getTexture } = useAssetRuntime();
126
136
  const properties = rawProps;
127
137
  const materialType = (_a = properties === null || properties === void 0 ? void 0 : properties.materialType) !== null && _a !== void 0 ? _a : 'standard';
@@ -181,21 +191,15 @@ function MaterialComponentView({ properties: rawProps }) {
181
191
  t.needsUpdate = true;
182
192
  return t;
183
193
  }, [normalMapTexture]);
184
- const normalScaleVec = useMemo(() => {
185
- var _a, _b;
186
- if (!finalNormalMap)
187
- return undefined;
188
- return new Vector2((_a = normalScaleProp === null || normalScaleProp === void 0 ? void 0 : normalScaleProp[0]) !== null && _a !== void 0 ? _a : 1, (_b = normalScaleProp === null || normalScaleProp === void 0 ? void 0 : normalScaleProp[1]) !== null && _b !== void 0 ? _b : 1);
189
- }, [finalNormalMap, normalScaleProp === null || normalScaleProp === void 0 ? void 0 : normalScaleProp[0], normalScaleProp === null || normalScaleProp === void 0 ? void 0 : normalScaleProp[1]]);
190
194
  if (!properties) {
191
195
  return _jsx("meshStandardNodeMaterial", { color: "red", wireframe: true });
192
196
  }
193
- const materialKey = `${(_e = finalTexture === null || finalTexture === void 0 ? void 0 : finalTexture.uuid) !== null && _e !== void 0 ? _e : 'no-texture'}:${materialProps.transparent ? 'transparent' : 'opaque'}`;
194
- const sharedProps = Object.assign({ map: finalTexture, side: resolvedSide }, materialProps);
197
+ const overrides = useMaterialOverrides();
198
+ const sharedProps = Object.assign(Object.assign({ map: finalTexture, side: resolvedSide }, materialProps), overrides);
195
199
  if (materialType === 'basic') {
196
- return _jsx("meshBasicNodeMaterial", Object.assign({}, sharedProps), materialKey);
200
+ return _jsx("meshBasicNodeMaterial", Object.assign({}, sharedProps));
197
201
  }
198
- return (_jsx("meshStandardNodeMaterial", Object.assign({}, sharedProps, { normalMap: finalNormalMap, normalScale: normalScaleVec }), materialKey));
202
+ return (_jsx("meshStandardNodeMaterial", Object.assign({}, sharedProps, { normalMap: finalNormalMap, normalScale: finalNormalMap ? [(_e = normalScaleProp === null || normalScaleProp === void 0 ? void 0 : normalScaleProp[0]) !== null && _e !== void 0 ? _e : 1, (_f = normalScaleProp === null || normalScaleProp === void 0 ? void 0 : normalScaleProp[1]) !== null && _f !== void 0 ? _f : 1] : undefined })));
199
203
  }
200
204
  const MaterialComponent = {
201
205
  name: 'Material',
@@ -2,7 +2,7 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
2
2
  import { ModelPicker } from '../../assetviewer/page';
3
3
  import { useContext, useMemo } from 'react';
4
4
  import { BooleanField, FieldGroup, Label, ListEditor, NumberInput, SelectInput } from './Input';
5
- import { useAssetRuntime } from '../runtimeContext';
5
+ import { useAssetRuntime } from '../runtime';
6
6
  import { EditorContext } from '../PrefabEditor';
7
7
  import { getRepeatAxesFromModelProperties, normalizeRepeatAxes } from '../InstanceProvider';
8
8
  import { colors } from '../styles';
@@ -12,7 +12,7 @@ var __rest = (this && this.__rest) || function (s, e) {
12
12
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
13
13
  import { CapsuleCollider, RigidBody, useRapier } from "@react-three/rapier";
14
14
  import { useRef, useEffect, useCallback } from 'react';
15
- import { useAssetRuntime, useEntityRuntime } from "../runtimeContext";
15
+ import { useAssetRuntime, useEntityRuntime } from "../runtime";
16
16
  import { BooleanField, EventInput, FieldGroup, ListEditor, NumberField, SelectField, SelectInput, Vector3Field } from "./Input";
17
17
  import { gameEvents, getEntityIdFromRigidBody } from "../GameEvents";
18
18
  import { colors } from "../styles";
@@ -2,7 +2,7 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
2
2
  import { useEffect, useRef } from 'react';
3
3
  import { useHelper } from '@react-three/drei';
4
4
  import { PointLightHelper } from 'three';
5
- import { useEntityRuntime } from '../runtimeContext';
5
+ import { useEntityRuntime } from '../runtime';
6
6
  import { BooleanField, ColorField, NumberField } from './Input';
7
7
  import { LightSection, ShadowBiasField, mergeWithDefaults } from './lightUtils';
8
8
  const pointLightDefaults = {
@@ -4,7 +4,7 @@ import { useThree } from '@react-three/fiber';
4
4
  import { SoundPicker } from '../../assetviewer/page';
5
5
  import { sound as soundManager } from '../../../helpers/SoundManager';
6
6
  import { gameEvents } from '../GameEvents';
7
- import { useAssetRuntime, useEntityRuntime } from '../runtimeContext';
7
+ import { useAssetRuntime, useEntityRuntime } from '../runtime';
8
8
  import { BooleanField, EventField, FieldGroup, FieldRenderer, ListEditor, NumberField, SelectField } from './Input';
9
9
  import { colors } from '../styles';
10
10
  import { AudioListener } from 'three';
@@ -212,7 +212,6 @@ const SoundComponent = {
212
212
  name: 'Sound',
213
213
  Editor: SoundComponentEditor,
214
214
  View: SoundComponentView,
215
- composition: 'sibling',
216
215
  defaultProperties: {
217
216
  eventName: '',
218
217
  clips: [],
@@ -3,7 +3,7 @@ import { useHelper } from "@react-three/drei";
3
3
  import { useRef, useEffect } from "react";
4
4
  import { BooleanField, ColorField, Label, NumberField, Vector3Input } from "./Input";
5
5
  import { SpotLightHelper } from "three";
6
- import { useAssetRuntime, useEntityRuntime } from "../runtimeContext";
6
+ import { useAssetRuntime, useEntityRuntime } from "../runtime";
7
7
  import { useFrame } from "@react-three/fiber";
8
8
  import { TexturePicker } from "../../assetviewer/page";
9
9
  import { LightSection, ShadowBiasField, mergeWithDefaults } from "./lightUtils";
@@ -0,0 +1,61 @@
1
+ import { type ReactNode } from "react";
2
+ import type { Object3D, Texture } from "three";
3
+ import type { PrefabStoreApi } from "./prefabStore";
4
+ import { type EntityComponent, type Scene } from "./scene";
5
+ export interface AssetRuntime {
6
+ registerRigidBodyRef: (id: string, rb: any) => void;
7
+ getModel: (path: string) => Object3D | null;
8
+ getTexture: (path: string) => Texture | null;
9
+ getSound: (path: string) => AudioBuffer | null;
10
+ getAssetRevision: () => string;
11
+ getObject: (id: string) => Object3D | null;
12
+ getRigidBody: (id: string) => any;
13
+ }
14
+ export interface AssetRuntimeContextValue extends AssetRuntime {
15
+ }
16
+ export interface EntityRuntime {
17
+ nodeId: string;
18
+ editMode?: boolean;
19
+ isSelected?: boolean;
20
+ getObject: <T extends Object3D = Object3D>() => T | null;
21
+ getRigidBody: <T = any>() => T | null;
22
+ }
23
+ export interface LiveRef<T> {
24
+ readonly current: T | null;
25
+ }
26
+ export type LiveObjectRef<T extends Object3D = Object3D> = LiveRef<T>;
27
+ export type LiveRigidBodyRef<T = any> = LiveRef<T>;
28
+ export declare const AssetRuntimeContext: import("react").Context<AssetRuntimeContextValue | null>;
29
+ export declare function useAssetRuntime(): AssetRuntime;
30
+ export declare function useEntityRuntime(): EntityRuntime;
31
+ export declare function useEntityObjectRef<T extends Object3D = Object3D>(): LiveRef<T>;
32
+ export declare function useEntityRigidBodyRef<T = any>(): LiveRef<T>;
33
+ export declare function EntityRuntimeScope({ nodeId, editMode, isSelected, children, }: {
34
+ nodeId: string;
35
+ editMode?: boolean;
36
+ isSelected?: boolean;
37
+ children: ReactNode;
38
+ }): import("react/jsx-runtime").JSX.Element;
39
+ /** Runtime behaviour produced by `Component.create(ctx)`. All methods optional. */
40
+ export interface ComponentInstance {
41
+ start?(): void;
42
+ update?(dt: number): void;
43
+ destroy?(): void;
44
+ }
45
+ export interface ComponentRuntimeContext<TProperties = Record<string, any>> {
46
+ scene: Scene;
47
+ component: EntityComponent<TProperties>;
48
+ object: Object3D;
49
+ rigidBody: any;
50
+ }
51
+ export interface RuntimeEngine {
52
+ tick: (dt: number) => void;
53
+ setActive: (active: boolean) => void;
54
+ invalidate: () => void;
55
+ dispose: () => void;
56
+ }
57
+ export declare function createRuntimeEngine({ store, getObject, getRigidBody, }: {
58
+ store: PrefabStoreApi;
59
+ getObject: (id: string) => Object3D | null;
60
+ getRigidBody: (id: string) => any;
61
+ }): RuntimeEngine;
@@ -0,0 +1,184 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { createContext, useContext, useMemo } from "react";
3
+ import { getComponentDef } from "./components/ComponentRegistry";
4
+ import { createScene } from "./scene";
5
+ export const AssetRuntimeContext = createContext(null);
6
+ const EntityRuntimeContext = createContext(null);
7
+ export function useAssetRuntime() {
8
+ const ctx = useContext(AssetRuntimeContext);
9
+ if (!ctx)
10
+ throw new Error("useAssetRuntime must be used inside <PrefabRoot>");
11
+ return ctx;
12
+ }
13
+ export function useEntityRuntime() {
14
+ const ctx = useContext(EntityRuntimeContext);
15
+ if (!ctx)
16
+ throw new Error("useEntityRuntime must be used inside a component View rendered by <PrefabRoot>");
17
+ return ctx;
18
+ }
19
+ export function useEntityObjectRef() {
20
+ const { getObject } = useEntityRuntime();
21
+ return useMemo(() => ({ get current() { return getObject(); } }), [getObject]);
22
+ }
23
+ export function useEntityRigidBodyRef() {
24
+ const { getRigidBody } = useEntityRuntime();
25
+ return useMemo(() => ({ get current() { return getRigidBody(); } }), [getRigidBody]);
26
+ }
27
+ export function EntityRuntimeScope({ nodeId, editMode, isSelected, children, }) {
28
+ const asset = useContext(AssetRuntimeContext);
29
+ if (!asset)
30
+ throw new Error("EntityRuntimeScope must be used inside <PrefabRoot>");
31
+ const value = useMemo(() => ({
32
+ nodeId,
33
+ editMode,
34
+ isSelected,
35
+ getObject: () => asset.getObject(nodeId),
36
+ getRigidBody: () => asset.getRigidBody(nodeId),
37
+ }), [asset, editMode, isSelected, nodeId]);
38
+ return _jsx(EntityRuntimeContext.Provider, { value: value, children: children });
39
+ }
40
+ export function createRuntimeEngine({ store, getObject, getRigidBody, }) {
41
+ const scene = createScene({
42
+ getRootId: () => store.getState().rootId,
43
+ getNode: (id) => { var _a; return (_a = store.getState().nodesById[id]) !== null && _a !== void 0 ? _a : null; },
44
+ getChildIds: (id) => { var _a; return (_a = store.getState().childIdsById[id]) !== null && _a !== void 0 ? _a : []; },
45
+ getParentId: (id) => { var _a; return (_a = store.getState().parentIdById[id]) !== null && _a !== void 0 ? _a : null; },
46
+ updateNode: (id, update) => store.getState().updateNode(id, update),
47
+ updateNodes: (updates) => {
48
+ store.getState().updateNodes(Object.entries(updates).map(([id, update]) => ({ id, update })));
49
+ },
50
+ addNode: (node, options) => {
51
+ var _a;
52
+ const parentId = (_a = options === null || options === void 0 ? void 0 : options.parentId) !== null && _a !== void 0 ? _a : store.getState().rootId;
53
+ store.getState().addChild(parentId, node);
54
+ return node.id;
55
+ },
56
+ removeNode: (id) => store.getState().deleteNode(id),
57
+ getObject,
58
+ getRigidBody,
59
+ });
60
+ // key = `${nodeId}:${componentKey}:${componentType}`
61
+ const instances = new Map();
62
+ let active = false;
63
+ let dirty = true;
64
+ let lastRevision = store.getState().revision;
65
+ const unsubscribe = store.subscribe((state) => {
66
+ if (state.revision === lastRevision)
67
+ return;
68
+ lastRevision = state.revision;
69
+ dirty = true;
70
+ });
71
+ function destroy(key) {
72
+ var _a, _b;
73
+ const record = instances.get(key);
74
+ if (!record)
75
+ return;
76
+ try {
77
+ (_b = (_a = record.instance).destroy) === null || _b === void 0 ? void 0 : _b.call(_a);
78
+ }
79
+ catch (error) {
80
+ console.error(`[runtime] destroy ${key}`, error);
81
+ }
82
+ instances.delete(key);
83
+ }
84
+ function sync() {
85
+ const state = store.getState();
86
+ const live = new Set();
87
+ let hasPendingMount = false;
88
+ const visit = (nodeId) => {
89
+ var _a, _b, _c;
90
+ const node = state.nodesById[nodeId];
91
+ if (!node)
92
+ return;
93
+ if (!node.disabled && node.components) {
94
+ for (const componentKey in node.components) {
95
+ const data = node.components[componentKey];
96
+ if (!(data === null || data === void 0 ? void 0 : data.type))
97
+ continue;
98
+ const def = getComponentDef(data.type);
99
+ if (!(def === null || def === void 0 ? void 0 : def.create))
100
+ continue;
101
+ const key = `${nodeId}:${componentKey}:${data.type}`;
102
+ live.add(key);
103
+ const object = getObject(nodeId);
104
+ if (!object) {
105
+ hasPendingMount = true;
106
+ continue;
107
+ }
108
+ const rigidBody = getRigidBody(nodeId);
109
+ const existing = instances.get(key);
110
+ if (existing && existing.object === object && existing.rigidBody === rigidBody) {
111
+ continue;
112
+ }
113
+ if (existing) {
114
+ destroy(key);
115
+ }
116
+ const entity = scene.find(nodeId);
117
+ const component = entity === null || entity === void 0 ? void 0 : entity.getComponent(componentKey);
118
+ if (!entity || !component)
119
+ continue;
120
+ let instance;
121
+ try {
122
+ instance = (_a = def.create({ scene, component, object, rigidBody })) !== null && _a !== void 0 ? _a : {};
123
+ }
124
+ catch (error) {
125
+ console.error(`[runtime] create ${key}`, error);
126
+ continue;
127
+ }
128
+ instances.set(key, { instance, object, rigidBody });
129
+ try {
130
+ (_b = instance.start) === null || _b === void 0 ? void 0 : _b.call(instance);
131
+ }
132
+ catch (error) {
133
+ console.error(`[runtime] start ${key}`, error);
134
+ }
135
+ }
136
+ }
137
+ for (const childId of (_c = state.childIdsById[nodeId]) !== null && _c !== void 0 ? _c : [])
138
+ visit(childId);
139
+ };
140
+ visit(state.rootId);
141
+ for (const key of Array.from(instances.keys())) {
142
+ if (!live.has(key))
143
+ destroy(key);
144
+ }
145
+ dirty = hasPendingMount;
146
+ }
147
+ return {
148
+ tick(dt) {
149
+ if (!active)
150
+ return;
151
+ if (dirty)
152
+ sync();
153
+ for (const [key, record] of instances) {
154
+ if (!record.instance.update)
155
+ continue;
156
+ try {
157
+ record.instance.update(dt);
158
+ }
159
+ catch (error) {
160
+ console.error(`[runtime] update ${key}`, error);
161
+ }
162
+ }
163
+ },
164
+ setActive(nextActive) {
165
+ if (active === nextActive)
166
+ return;
167
+ active = nextActive;
168
+ dirty = true;
169
+ if (!active) {
170
+ for (const key of Array.from(instances.keys()))
171
+ destroy(key);
172
+ }
173
+ },
174
+ invalidate() {
175
+ dirty = true;
176
+ },
177
+ dispose() {
178
+ active = false;
179
+ for (const key of Array.from(instances.keys()))
180
+ destroy(key);
181
+ unsubscribe();
182
+ },
183
+ };
184
+ }
@@ -45,6 +45,14 @@ export interface Scene {
45
45
  };
46
46
  add: (node: GameObject, options?: SpawnOptions) => Entity;
47
47
  remove: (id: string) => void;
48
+ /**
49
+ * Coalesce many entity / component updates into a single store revision.
50
+ * Entity `update`/`set`, `EntityComponent` `set`/`update`, `addComponent`,
51
+ * and `removeComponent` calls inside the callback are buffered and flushed
52
+ * as one batched write. `add`, `remove`, and `destroy` (structural tree ops)
53
+ * still commit immediately.
54
+ */
55
+ batch: (fn: () => void) => void;
48
56
  }
49
57
  interface SceneAdapter {
50
58
  getRootId: () => string;
@@ -57,6 +57,42 @@ function setValueAtPath(value, path, nextValue) {
57
57
  return cloneBranch(value, 0);
58
58
  }
59
59
  export function createScene(adapter) {
60
+ let batchBuffer = null;
61
+ function routeUpdate(id, update) {
62
+ if (batchBuffer) {
63
+ const prev = batchBuffer.get(id);
64
+ batchBuffer.set(id, prev ? (node) => update(prev(node)) : update);
65
+ return;
66
+ }
67
+ adapter.updateNode(id, update);
68
+ }
69
+ function routeUpdates(updates) {
70
+ if (batchBuffer) {
71
+ for (const id in updates)
72
+ routeUpdate(id, updates[id]);
73
+ return;
74
+ }
75
+ adapter.updateNodes(updates);
76
+ }
77
+ function batch(fn) {
78
+ if (batchBuffer) {
79
+ fn();
80
+ return;
81
+ }
82
+ batchBuffer = new Map();
83
+ try {
84
+ fn();
85
+ if (batchBuffer.size > 0) {
86
+ const updates = {};
87
+ for (const [id, update] of batchBuffer)
88
+ updates[id] = update;
89
+ adapter.updateNodes(updates);
90
+ }
91
+ }
92
+ finally {
93
+ batchBuffer = null;
94
+ }
95
+ }
60
96
  const getNode = (id) => {
61
97
  if (!adapter.getNode(id))
62
98
  missingNode(id);
@@ -76,7 +112,7 @@ export function createScene(adapter) {
76
112
  return getValueAtPath(component.properties, path);
77
113
  },
78
114
  set(path, value) {
79
- adapter.updateNode(entityId, node => {
115
+ routeUpdate(entityId, node => {
80
116
  var _a;
81
117
  const component = (_a = node.components) === null || _a === void 0 ? void 0 : _a[componentKey];
82
118
  if (!component) {
@@ -86,7 +122,7 @@ export function createScene(adapter) {
86
122
  });
87
123
  },
88
124
  update(update) {
89
- adapter.updateNode(entityId, node => {
125
+ routeUpdate(entityId, node => {
90
126
  var _a;
91
127
  const component = (_a = node.components) === null || _a === void 0 ? void 0 : _a[componentKey];
92
128
  if (!component) {
@@ -128,10 +164,10 @@ export function createScene(adapter) {
128
164
  return (_b = (_a = adapter.getRigidBody) === null || _a === void 0 ? void 0 : _a.call(adapter, id)) !== null && _b !== void 0 ? _b : null;
129
165
  },
130
166
  set(data) {
131
- adapter.updateNode(id, () => data);
167
+ routeUpdate(id, () => data);
132
168
  },
133
169
  update(update) {
134
- adapter.updateNode(id, update);
170
+ routeUpdate(id, update);
135
171
  },
136
172
  getComponent(name) {
137
173
  const node = adapter.getNode(id);
@@ -145,11 +181,11 @@ export function createScene(adapter) {
145
181
  },
146
182
  addComponent(type, properties) {
147
183
  const key = type.toLowerCase();
148
- adapter.updateNode(id, node => (Object.assign(Object.assign({}, node), { components: Object.assign(Object.assign({}, node.components), { [key]: createComponentData(type, properties) }) })));
184
+ routeUpdate(id, node => (Object.assign(Object.assign({}, node), { components: Object.assign(Object.assign({}, node.components), { [key]: createComponentData(type, properties) }) })));
149
185
  return createComponent(id, key, type);
150
186
  },
151
187
  removeComponent(name) {
152
- adapter.updateNode(id, node => {
188
+ routeUpdate(id, node => {
153
189
  var _a;
154
190
  const entry = findComponentEntry(node, name);
155
191
  if (!entry)
@@ -169,13 +205,13 @@ export function createScene(adapter) {
169
205
  if (!mutate) {
170
206
  return;
171
207
  }
172
- adapter.updateNode(idOrUpdates, mutate);
208
+ routeUpdate(idOrUpdates, mutate);
173
209
  return;
174
210
  }
175
211
  if (Object.keys(idOrUpdates).length === 0) {
176
212
  return;
177
213
  }
178
- adapter.updateNodes(idOrUpdates);
214
+ routeUpdates(idOrUpdates);
179
215
  }
180
216
  return {
181
217
  get rootId() {
@@ -196,5 +232,6 @@ export function createScene(adapter) {
196
232
  remove(id) {
197
233
  adapter.removeNode(id);
198
234
  },
235
+ batch,
199
236
  };
200
237
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-three-game",
3
- "version": "0.0.83",
3
+ "version": "0.0.84",
4
4
  "description": "high performance 3D game engine built in React",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.js",
@@ -43,7 +43,7 @@
43
43
  "concurrently": "^9.2.1",
44
44
  "react": "^19.2.4",
45
45
  "react-dom": "^19.2.4",
46
- "three": "^0.182.0",
46
+ "three": "^0.184.0",
47
47
  "typescript": "^5.9.3",
48
48
  "vite": "^7.3.1"
49
49
  },
@@ -1,40 +0,0 @@
1
- import { type ReactNode } from "react";
2
- import { Object3D, Texture } from "three";
3
- type RuntimeScopeProps = {
4
- nodeId: string;
5
- editMode?: boolean;
6
- isSelected?: boolean;
7
- children: ReactNode;
8
- };
9
- export interface AssetRuntime {
10
- registerRigidBodyRef: (id: string, rb: any) => void;
11
- getModel: (path: string) => Object3D | null;
12
- getTexture: (path: string) => Texture | null;
13
- getSound: (path: string) => AudioBuffer | null;
14
- getAssetRevision: () => string;
15
- getObject: (id: string) => Object3D | null;
16
- getRigidBody: (id: string) => any;
17
- }
18
- export interface AssetRuntimeContextValue extends AssetRuntime {
19
- }
20
- export interface EntityRuntime {
21
- nodeId: string;
22
- editMode?: boolean;
23
- isSelected?: boolean;
24
- getObject: <T extends Object3D = Object3D>() => T | null;
25
- getRigidBody: <T = any>() => T | null;
26
- }
27
- export interface LiveObjectRef<T extends Object3D = Object3D> {
28
- readonly current: T | null;
29
- }
30
- export interface LiveRigidBodyRef<T = any> {
31
- readonly current: T | null;
32
- }
33
- export declare const AssetRuntimeContext: import("react").Context<AssetRuntimeContextValue | null>;
34
- export declare const EntityRuntimeContext: import("react").Context<EntityRuntime | null>;
35
- export declare function useAssetRuntime(): AssetRuntime;
36
- export declare function useEntityRuntime(): EntityRuntime;
37
- export declare function useEntityObjectRef<T extends Object3D = Object3D>(): LiveObjectRef<T>;
38
- export declare function useEntityRigidBodyRef<T = any>(): LiveRigidBodyRef<T>;
39
- export declare function EntityRuntimeScope({ nodeId, editMode, isSelected, children, }: RuntimeScopeProps): import("react/jsx-runtime").JSX.Element;
40
- export {};
@@ -1,45 +0,0 @@
1
- import { jsx as _jsx } from "react/jsx-runtime";
2
- import { createContext, useContext, useMemo } from "react";
3
- function createLiveRef(getCurrent) {
4
- return {
5
- get current() {
6
- return getCurrent();
7
- },
8
- };
9
- }
10
- export const AssetRuntimeContext = createContext(null);
11
- export const EntityRuntimeContext = createContext(null);
12
- export function useAssetRuntime() {
13
- const ctx = useContext(AssetRuntimeContext);
14
- if (!ctx)
15
- throw new Error("useAssetRuntime must be used inside <PrefabRoot>");
16
- return ctx;
17
- }
18
- export function useEntityRuntime() {
19
- const ctx = useContext(EntityRuntimeContext);
20
- if (!ctx)
21
- throw new Error("useEntityRuntime must be used inside a component View rendered by <PrefabRoot>");
22
- return ctx;
23
- }
24
- export function useEntityObjectRef() {
25
- const { getObject } = useEntityRuntime();
26
- return useMemo(() => createLiveRef(() => getObject()), [getObject]);
27
- }
28
- export function useEntityRigidBodyRef() {
29
- const { getRigidBody } = useEntityRuntime();
30
- return useMemo(() => createLiveRef(() => getRigidBody()), [getRigidBody]);
31
- }
32
- export function EntityRuntimeScope({ nodeId, editMode, isSelected, children, }) {
33
- const assetRuntime = useContext(AssetRuntimeContext);
34
- if (!assetRuntime)
35
- throw new Error("EntityRuntimeScope must be used inside <PrefabRoot>");
36
- const { getObject, getRigidBody } = assetRuntime;
37
- const value = useMemo(() => ({
38
- nodeId,
39
- editMode,
40
- isSelected,
41
- getObject: () => getObject(nodeId),
42
- getRigidBody: () => getRigidBody(nodeId),
43
- }), [editMode, getObject, getRigidBody, isSelected, nodeId]);
44
- return _jsx(EntityRuntimeContext.Provider, { value: value, children: children });
45
- }