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
@@ -9,14 +9,29 @@ var __rest = (this && this.__rest) || function (s, e) {
9
9
  }
10
10
  return t;
11
11
  };
12
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
12
+ import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
13
+ import { createContext, useContext, useMemo, useRef } from 'react';
13
14
  import { extend } from '@react-three/fiber';
15
+ import { useFrame } from '@react-three/fiber';
14
16
  import { FieldRenderer, Label, NumberInput } from './Input';
15
- import { useAssetRuntime } from '../runtimeContext';
16
- import { useMemo } from 'react';
17
+ import { useAssetRuntime } from '../runtime';
17
18
  import { MeshBasicNodeMaterial, MeshStandardNodeMaterial } from 'three/webgpu';
18
19
  import { TexturePicker } from '../../assetviewer/page';
19
- import { RepeatWrapping, ClampToEdgeWrapping, SRGBColorSpace, LinearSRGBColorSpace, Vector2, NearestFilter, LinearFilter, NearestMipmapNearestFilter, NearestMipmapLinearFilter, LinearMipmapNearestFilter, LinearMipmapLinearFilter, FrontSide, BackSide, DoubleSide, } from 'three';
20
+ import { RepeatWrapping, ClampToEdgeWrapping, SRGBColorSpace, LinearSRGBColorSpace, NearestFilter, LinearFilter, NearestMipmapNearestFilter, NearestMipmapLinearFilter, LinearMipmapNearestFilter, LinearMipmapLinearFilter, FrontSide, BackSide, DoubleSide, } from 'three';
21
+ function Vector2Editor({ label, value, onChange, min, max, step, }) {
22
+ var _a, _b;
23
+ return (_jsxs("div", { style: { display: 'flex', gap: 2 }, children: [_jsxs("div", { style: { flex: 1 }, children: [_jsxs(Label, { children: [label, " X"] }), _jsx(NumberInput, { value: (_a = value === null || value === void 0 ? void 0 : value[0]) !== null && _a !== void 0 ? _a : 0, onChange: x => { var _a; return onChange([x, (_a = value === null || value === void 0 ? void 0 : value[1]) !== null && _a !== void 0 ? _a : 0]); }, min: min, max: max, step: step, style: { width: '100%', minWidth: 0, boxSizing: 'border-box' } })] }), _jsxs("div", { style: { flex: 1 }, children: [_jsxs(Label, { children: [label, " Y"] }), _jsx(NumberInput, { value: (_b = value === null || value === void 0 ? void 0 : value[1]) !== null && _b !== void 0 ? _b : 0, onChange: y => { var _a; return onChange([(_a = value === null || value === void 0 ? void 0 : value[0]) !== null && _a !== void 0 ? _a : 0, y]); }, min: min, max: max, step: step, style: { width: '100%', minWidth: 0, boxSizing: 'border-box' } })] })] }));
24
+ }
25
+ const EMPTY_MATERIAL_OVERRIDES = Object.freeze({});
26
+ const MaterialOverridesContext = createContext(EMPTY_MATERIAL_OVERRIDES);
27
+ export function useMaterialOverrides() {
28
+ return useContext(MaterialOverridesContext);
29
+ }
30
+ export function MaterialOverridesProvider({ overrides, children, }) {
31
+ const parent = useContext(MaterialOverridesContext);
32
+ const merged = useMemo(() => (Object.assign(Object.assign({}, parent), overrides)), [parent, overrides]);
33
+ return _jsx(MaterialOverridesContext.Provider, { value: merged, children: children });
34
+ }
20
35
  extend({
21
36
  MeshBasicNodeMaterial,
22
37
  MeshStandardNodeMaterial,
@@ -26,6 +41,7 @@ function MaterialComponentEditor({ component, onUpdate, basePath = "" }) {
26
41
  const materialType = (_a = component.properties.materialType) !== null && _a !== void 0 ? _a : 'standard';
27
42
  const hasTexture = !!component.properties.texture;
28
43
  const hasRepeat = component.properties.repeat;
44
+ const animateOffset = component.properties.animateOffset;
29
45
  const isStandardMaterial = materialType === 'standard';
30
46
  const fields = [
31
47
  {
@@ -72,10 +88,20 @@ function MaterialComponentEditor({ component, onUpdate, basePath = "" }) {
72
88
  name: 'repeatCount',
73
89
  type: 'custom',
74
90
  label: 'Repeat (X, Y)',
75
- render: ({ value, onChange }) => {
76
- var _a, _b;
77
- return (_jsxs("div", { style: { display: 'flex', gap: 2 }, children: [_jsxs("div", { style: { flex: 1 }, children: [_jsx(Label, { children: "X" }), _jsx(NumberInput, { value: (_a = value === null || value === void 0 ? void 0 : value[0]) !== null && _a !== void 0 ? _a : 1, onChange: v => { var _a; return onChange([v, (_a = value === null || value === void 0 ? void 0 : value[1]) !== null && _a !== void 0 ? _a : 1]); }, min: 0.01, max: 100, step: 0.1, style: { width: '100%', minWidth: 0, boxSizing: 'border-box' } })] }), _jsxs("div", { style: { flex: 1 }, children: [_jsx(Label, { children: "Y" }), _jsx(NumberInput, { value: (_b = value === null || value === void 0 ? void 0 : value[1]) !== null && _b !== void 0 ? _b : 1, onChange: v => { var _a; return onChange([(_a = value === null || value === void 0 ? void 0 : value[0]) !== null && _a !== void 0 ? _a : 1, v]); }, min: 0.01, max: 100, step: 0.1, style: { width: '100%', minWidth: 0, boxSizing: 'border-box' } })] })] }));
78
- },
91
+ render: ({ value, onChange }) => (_jsx(Vector2Editor, { label: "Repeat", value: value, onChange: onChange, min: 0.01, max: 100, step: 0.1 })),
92
+ }] : []),
93
+ {
94
+ name: 'offset',
95
+ type: 'custom',
96
+ label: 'Offset (X, Y)',
97
+ render: ({ value, onChange }) => (_jsx(Vector2Editor, { label: "Offset", value: value, onChange: onChange, step: 0.01 })),
98
+ },
99
+ { name: 'animateOffset', type: 'boolean', label: 'Animate Offset' },
100
+ ...(animateOffset ? [{
101
+ name: 'offsetSpeed',
102
+ type: 'custom',
103
+ label: 'Speed (X, Y)',
104
+ render: ({ value, onChange }) => (_jsx(Vector2Editor, { label: "Speed", value: value, onChange: onChange, step: 0.01 })),
79
105
  }] : []),
80
106
  {
81
107
  name: 'normalMapTexture',
@@ -87,10 +113,7 @@ function MaterialComponentEditor({ component, onUpdate, basePath = "" }) {
87
113
  name: 'normalScale',
88
114
  type: 'custom',
89
115
  label: 'Normal Scale (X, Y)',
90
- render: ({ value, onChange }) => {
91
- var _a, _b;
92
- return (_jsxs("div", { style: { display: 'flex', gap: 2 }, children: [_jsxs("div", { style: { flex: 1 }, children: [_jsx(Label, { children: "X" }), _jsx(NumberInput, { value: (_a = value === null || value === void 0 ? void 0 : value[0]) !== null && _a !== void 0 ? _a : 1, onChange: v => { var _a; return onChange([v, (_a = value === null || value === void 0 ? void 0 : value[1]) !== null && _a !== void 0 ? _a : 1]); }, min: 0, max: 5, step: 0.01, style: { width: '100%', minWidth: 0, boxSizing: 'border-box' } })] }), _jsxs("div", { style: { flex: 1 }, children: [_jsx(Label, { children: "Y" }), _jsx(NumberInput, { value: (_b = value === null || value === void 0 ? void 0 : value[1]) !== null && _b !== void 0 ? _b : 1, onChange: v => { var _a; return onChange([(_a = value === null || value === void 0 ? void 0 : value[0]) !== null && _a !== void 0 ? _a : 1, v]); }, min: 0, max: 5, step: 0.01, style: { width: '100%', minWidth: 0, boxSizing: 'border-box' } })] })] }));
93
- },
116
+ render: ({ value, onChange }) => (_jsx(Vector2Editor, { label: "Normal", value: value, onChange: onChange, min: 0, max: 5, step: 0.01 })),
94
117
  }] : []),
95
118
  { name: 'generateMipmaps', type: 'boolean', label: 'Generate Mipmaps' },
96
119
  {
@@ -121,13 +144,16 @@ function MaterialComponentEditor({ component, onUpdate, basePath = "" }) {
121
144
  }
122
145
  // View for Material component
123
146
  function MaterialComponentView({ properties: rawProps }) {
124
- var _a, _b, _c, _d, _e;
147
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k;
125
148
  const { getTexture } = useAssetRuntime();
126
149
  const properties = rawProps;
127
150
  const materialType = (_a = properties === null || properties === void 0 ? void 0 : properties.materialType) !== null && _a !== void 0 ? _a : 'standard';
128
151
  const textureName = properties === null || properties === void 0 ? void 0 : properties.texture;
152
+ const offset = properties === null || properties === void 0 ? void 0 : properties.offset;
129
153
  const repeat = properties === null || properties === void 0 ? void 0 : properties.repeat;
130
154
  const repeatCount = properties === null || properties === void 0 ? void 0 : properties.repeatCount;
155
+ const animateOffset = properties === null || properties === void 0 ? void 0 : properties.animateOffset;
156
+ const offsetSpeed = properties === null || properties === void 0 ? void 0 : properties.offsetSpeed;
131
157
  const generateMipmaps = (properties === null || properties === void 0 ? void 0 : properties.generateMipmaps) !== false;
132
158
  const minFilter = (properties === null || properties === void 0 ? void 0 : properties.minFilter) || 'LinearMipmapLinearFilter';
133
159
  const magFilter = (properties === null || properties === void 0 ? void 0 : properties.magFilter) || 'LinearFilter';
@@ -137,7 +163,7 @@ function MaterialComponentView({ properties: rawProps }) {
137
163
  const normalMapTexture = normalMapTextureName ? (_c = getTexture(normalMapTextureName)) !== null && _c !== void 0 ? _c : undefined : undefined;
138
164
  const materialSource = properties !== null && properties !== void 0 ? properties : {};
139
165
  // Destructure all material props and separate custom texture handling props
140
- const { texture: _texture, repeat: _repeat, repeatCount: _repeatCount, generateMipmaps: _generateMipmaps, minFilter: _minFilter, magFilter: _magFilter, map: _map, materialType: _materialType, normalMapTexture: _normalMapTexture, normalScale: _normalScale, normalMap: _normalMap, side: sideProp } = materialSource, materialProps = __rest(materialSource, ["texture", "repeat", "repeatCount", "generateMipmaps", "minFilter", "magFilter", "map", "materialType", "normalMapTexture", "normalScale", "normalMap", "side"]);
166
+ const { texture: _texture, offset: _offset, repeat: _repeat, repeatCount: _repeatCount, animateOffset: _animateOffset, offsetSpeed: _offsetSpeed, generateMipmaps: _generateMipmaps, minFilter: _minFilter, magFilter: _magFilter, map: _map, materialType: _materialType, normalMapTexture: _normalMapTexture, normalScale: _normalScale, normalMap: _normalMap, side: sideProp } = materialSource, materialProps = __rest(materialSource, ["texture", "offset", "repeat", "repeatCount", "animateOffset", "offsetSpeed", "generateMipmaps", "minFilter", "magFilter", "map", "materialType", "normalMapTexture", "normalScale", "normalMap", "side"]);
141
167
  const sideMap = { FrontSide, BackSide, DoubleSide };
142
168
  const resolvedSide = sideProp ? ((_d = sideMap[sideProp]) !== null && _d !== void 0 ? _d : FrontSide) : FrontSide;
143
169
  const minFilterMap = {
@@ -152,8 +178,9 @@ function MaterialComponentView({ properties: rawProps }) {
152
178
  NearestFilter,
153
179
  LinearFilter
154
180
  };
181
+ const animatedOffsetRef = useRef([(_e = offset === null || offset === void 0 ? void 0 : offset[0]) !== null && _e !== void 0 ? _e : 0, (_f = offset === null || offset === void 0 ? void 0 : offset[1]) !== null && _f !== void 0 ? _f : 0]);
155
182
  const finalTexture = useMemo(() => {
156
- var _a, _b;
183
+ var _a, _b, _c, _d;
157
184
  if (!texture)
158
185
  return undefined;
159
186
  const t = texture.clone();
@@ -166,13 +193,24 @@ function MaterialComponentView({ properties: rawProps }) {
166
193
  t.wrapS = t.wrapT = ClampToEdgeWrapping;
167
194
  t.repeat.set(1, 1);
168
195
  }
196
+ t.offset.set((_a = offset === null || offset === void 0 ? void 0 : offset[0]) !== null && _a !== void 0 ? _a : 0, (_b = offset === null || offset === void 0 ? void 0 : offset[1]) !== null && _b !== void 0 ? _b : 0);
169
197
  t.colorSpace = SRGBColorSpace;
170
198
  t.generateMipmaps = generateMipmaps;
171
- t.minFilter = (_a = minFilterMap[minFilter]) !== null && _a !== void 0 ? _a : LinearMipmapLinearFilter;
172
- t.magFilter = (_b = magFilterMap[magFilter]) !== null && _b !== void 0 ? _b : LinearFilter;
199
+ t.minFilter = (_c = minFilterMap[minFilter]) !== null && _c !== void 0 ? _c : LinearMipmapLinearFilter;
200
+ t.magFilter = (_d = magFilterMap[magFilter]) !== null && _d !== void 0 ? _d : LinearFilter;
173
201
  t.needsUpdate = true;
174
202
  return t;
175
- }, [texture, repeat, repeatCount === null || repeatCount === void 0 ? void 0 : repeatCount[0], repeatCount === null || repeatCount === void 0 ? void 0 : repeatCount[1], generateMipmaps, minFilter, magFilter]);
203
+ }, [texture, repeat, repeatCount === null || repeatCount === void 0 ? void 0 : repeatCount[0], repeatCount === null || repeatCount === void 0 ? void 0 : repeatCount[1], offset === null || offset === void 0 ? void 0 : offset[0], offset === null || offset === void 0 ? void 0 : offset[1], generateMipmaps, minFilter, magFilter]);
204
+ animatedOffsetRef.current = [(_g = offset === null || offset === void 0 ? void 0 : offset[0]) !== null && _g !== void 0 ? _g : 0, (_h = offset === null || offset === void 0 ? void 0 : offset[1]) !== null && _h !== void 0 ? _h : 0];
205
+ useFrame((_, delta) => {
206
+ var _a, _b;
207
+ if (!finalTexture || !animateOffset)
208
+ return;
209
+ const nextX = animatedOffsetRef.current[0] + ((_a = offsetSpeed === null || offsetSpeed === void 0 ? void 0 : offsetSpeed[0]) !== null && _a !== void 0 ? _a : 0) * delta;
210
+ const nextY = animatedOffsetRef.current[1] + ((_b = offsetSpeed === null || offsetSpeed === void 0 ? void 0 : offsetSpeed[1]) !== null && _b !== void 0 ? _b : 0) * delta;
211
+ animatedOffsetRef.current = [nextX, nextY];
212
+ finalTexture.offset.set(nextX, nextY);
213
+ });
176
214
  const finalNormalMap = useMemo(() => {
177
215
  if (!normalMapTexture)
178
216
  return undefined;
@@ -181,21 +219,15 @@ function MaterialComponentView({ properties: rawProps }) {
181
219
  t.needsUpdate = true;
182
220
  return t;
183
221
  }, [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
222
  if (!properties) {
191
223
  return _jsx("meshStandardNodeMaterial", { color: "red", wireframe: true });
192
224
  }
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);
225
+ const overrides = useMaterialOverrides();
226
+ const sharedProps = Object.assign(Object.assign({ map: finalTexture, side: resolvedSide }, materialProps), overrides);
195
227
  if (materialType === 'basic') {
196
- return _jsx("meshBasicNodeMaterial", Object.assign({}, sharedProps), materialKey);
228
+ return _jsx("meshBasicNodeMaterial", Object.assign({}, sharedProps));
197
229
  }
198
- return (_jsx("meshStandardNodeMaterial", Object.assign({}, sharedProps, { normalMap: finalNormalMap, normalScale: normalScaleVec }), materialKey));
230
+ return (_jsx("meshStandardNodeMaterial", Object.assign({}, sharedProps, { normalMap: finalNormalMap, normalScale: finalNormalMap ? [(_j = normalScaleProp === null || normalScaleProp === void 0 ? void 0 : normalScaleProp[0]) !== null && _j !== void 0 ? _j : 1, (_k = normalScaleProp === null || normalScaleProp === void 0 ? void 0 : normalScaleProp[1]) !== null && _k !== void 0 ? _k : 1] : undefined })));
199
231
  }
200
232
  const MaterialComponent = {
201
233
  name: 'Material',
@@ -208,6 +240,9 @@ const MaterialComponent = {
208
240
  wireframe: false,
209
241
  transparent: false,
210
242
  opacity: 1,
243
+ offset: [0, 0],
244
+ animateOffset: false,
245
+ offsetSpeed: [0, 0],
211
246
  metalness: 0,
212
247
  roughness: 1
213
248
  },
@@ -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";
@@ -1,28 +1,30 @@
1
- import GeometryComponent from './GeometryComponent';
2
1
  import TransformComponent from './TransformComponent';
2
+ import GeometryComponent from './GeometryComponent';
3
+ import BufferGeometryComponent from './BufferGeometryComponent';
4
+ import ModelComponent from './ModelComponent';
5
+ import TextComponent from './TextComponent';
3
6
  import MaterialComponent from './MaterialComponent';
4
7
  import PhysicsComponent from './PhysicsComponent';
5
8
  import SpotLightComponent from './SpotLightComponent';
6
9
  import PointLightComponent from './PointLightComponent';
7
10
  import DirectionalLightComponent from './DirectionalLightComponent';
8
11
  import AmbientLightComponent from './AmbientLightComponent';
9
- import ModelComponent from './ModelComponent';
10
- import TextComponent from './TextComponent';
11
12
  import EnvironmentComponent from './EnvironmentComponent';
12
13
  import CameraComponent from './CameraComponent';
13
14
  import ClickComponent from './ClickComponent';
14
15
  import SoundComponent from './SoundComponent';
15
16
  export const builtinComponents = [
16
- GeometryComponent,
17
17
  TransformComponent,
18
+ GeometryComponent,
19
+ BufferGeometryComponent,
20
+ ModelComponent,
21
+ TextComponent,
18
22
  MaterialComponent,
19
23
  PhysicsComponent,
20
24
  SpotLightComponent,
21
25
  PointLightComponent,
22
26
  DirectionalLightComponent,
23
27
  AmbientLightComponent,
24
- ModelComponent,
25
- TextComponent,
26
28
  EnvironmentComponent,
27
29
  CameraComponent,
28
30
  ClickComponent,
@@ -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
  }