react-three-game 0.0.108 → 0.0.110

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 (55) hide show
  1. package/README.md +9 -16
  2. package/dist/editor.d.ts +22 -0
  3. package/dist/editor.js +15 -0
  4. package/dist/plugins/crashcat/CrashcatPhysicsComponent.js +47 -9
  5. package/dist/plugins/crashcat/CrashcatRagdoll.d.ts +1 -1
  6. package/dist/plugins/crashcat/CrashcatRuntime.d.ts +1 -1
  7. package/dist/plugins/crashcat/CrashcatRuntime.js +1 -1
  8. package/dist/shared/ContactShadow.d.ts +1 -1
  9. package/dist/shared/GameCanvas.d.ts +1 -1
  10. package/dist/tools/assetviewer/page.d.ts +10 -10
  11. package/dist/tools/dragdrop/DragDropLoader.d.ts +2 -2
  12. package/dist/tools/prefabeditor/Dropdown.d.ts +1 -1
  13. package/dist/tools/prefabeditor/EditorContext.d.ts +36 -0
  14. package/dist/tools/prefabeditor/EditorContext.js +17 -0
  15. package/dist/tools/prefabeditor/EditorTree.d.ts +1 -1
  16. package/dist/tools/prefabeditor/EditorTree.js +1 -1
  17. package/dist/tools/prefabeditor/EditorTreeMenus.d.ts +2 -2
  18. package/dist/tools/prefabeditor/EditorTreeMenus.js +1 -1
  19. package/dist/tools/prefabeditor/EditorUI.d.ts +1 -1
  20. package/dist/tools/prefabeditor/EditorUI.js +1 -1
  21. package/dist/tools/prefabeditor/InstanceProvider.d.ts +1 -1
  22. package/dist/tools/prefabeditor/PrefabEditor.d.ts +4 -37
  23. package/dist/tools/prefabeditor/PrefabEditor.js +72 -71
  24. package/dist/tools/prefabeditor/PrefabRoot.d.ts +5 -29
  25. package/dist/tools/prefabeditor/PrefabRoot.js +72 -188
  26. package/dist/tools/prefabeditor/SceneContext.d.ts +28 -0
  27. package/dist/tools/prefabeditor/SceneContext.js +14 -0
  28. package/dist/tools/prefabeditor/SceneProvider.d.ts +14 -0
  29. package/dist/tools/prefabeditor/SceneProvider.js +68 -0
  30. package/dist/tools/prefabeditor/assetRuntime.d.ts +29 -2
  31. package/dist/tools/prefabeditor/assetRuntime.js +115 -1
  32. package/dist/tools/prefabeditor/components/ComponentRegistry.d.ts +1 -1
  33. package/dist/tools/prefabeditor/components/EnvironmentComponent.js +3 -3
  34. package/dist/tools/prefabeditor/components/Input.d.ts +19 -19
  35. package/dist/tools/prefabeditor/components/MaterialComponent.d.ts +1 -1
  36. package/dist/tools/prefabeditor/components/MaterialComponent.js +13 -14
  37. package/dist/tools/prefabeditor/components/ModelComponent.js +3 -4
  38. package/dist/tools/prefabeditor/components/PrefabRefComponent.js +43 -19
  39. package/dist/tools/prefabeditor/components/SoundComponent.js +6 -2
  40. package/dist/tools/prefabeditor/components/SpotLightComponent.js +2 -5
  41. package/dist/tools/prefabeditor/components/TransformComponent.js +1 -1
  42. package/dist/tools/prefabeditor/components/index.d.ts +1 -0
  43. package/dist/tools/prefabeditor/components/index.js +8 -0
  44. package/dist/tools/prefabeditor/components/lightUtils.d.ts +2 -2
  45. package/dist/tools/prefabeditor/prefabStore.d.ts +2 -1
  46. package/dist/tools/prefabeditor/prefabStore.js +16 -1
  47. package/dist/tools/prefabeditor/runtimeUtils.d.ts +10 -0
  48. package/dist/tools/prefabeditor/runtimeUtils.js +30 -0
  49. package/dist/tools/prefabeditor/utils.d.ts +1 -9
  50. package/dist/tools/prefabeditor/utils.js +3 -28
  51. package/dist/viewer.d.ts +21 -0
  52. package/dist/viewer.js +12 -0
  53. package/package.json +18 -16
  54. package/dist/index.d.ts +0 -40
  55. package/dist/index.js +0 -32
@@ -19,9 +19,8 @@ var __rest = (this && this.__rest) || function (s, e) {
19
19
  return t;
20
20
  };
21
21
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
22
- import { createContext, forwardRef, useCallback, useContext, useEffect, useImperativeHandle, useMemo, useRef, useState } from "react";
22
+ import { forwardRef, useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState } from "react";
23
23
  import { Euler, Matrix4 } from "three";
24
- import { useThree } from "@react-three/fiber";
25
24
  import { useStore } from "zustand";
26
25
  import { useClickValid } from "./useClickValid";
27
26
  import { findComponent, getNodeUserData } from "./types";
@@ -29,16 +28,27 @@ import { getComponentDef, registerComponent } from "./components/ComponentRegist
29
28
  import { builtinComponents } from "./components";
30
29
  import { loadModel, loadSound, loadTexture } from "../dragdrop";
31
30
  import { GameInstance, GameInstanceProvider, getRepeatAxesFromModelProperties } from "./InstanceProvider";
32
- import { composeTransform, decompose, withBasePath } from "./utils";
33
- import { createPrefabStore, PrefabStoreProvider, usePrefabChildIds, usePrefabNode, usePrefabRootId } from "./prefabStore";
34
- import { AssetRuntimeContext, NodeScope } from "./assetRuntime";
31
+ import { composeTransform, decompose, withBasePath } from "./runtimeUtils";
32
+ import { createPrefabStore, PrefabStoreProvider, usePrefabChildIds, usePrefabNode, usePrefabRootId, usePrefabStoreApi } from "./prefabStore";
33
+ import { AssetRuntimeProvider, NodeScope, useAllModels, useAssetRuntime } from "./assetRuntime";
35
34
  import { gameEvents } from "./GameEvents";
36
- import { sound as soundManager } from "../../helpers/SoundManager";
37
- builtinComponents.forEach(registerComponent);
35
+ import { useScene } from "./SceneContext";
36
+ import { SceneProvider } from "./SceneProvider";
38
37
  const IDENTITY = new Matrix4();
39
- const EMPTY_MODELS = {};
40
- const EMPTY_TEXTURES = {};
41
- const EMPTY_SOUNDS = {};
38
+ // Reusable scratch objects for buildRepeatedInstances. The matrices are pure
39
+ // intermediates (their results are read out into plain arrays per instance),
40
+ // so they can be shared across calls instead of allocating fresh Matrix4s in
41
+ // nested loops for every repeated instance.
42
+ const _scratchTranslation = new Matrix4();
43
+ const _scratchRotation = new Matrix4();
44
+ const _scratchScale = new Matrix4();
45
+ const _scratchOffset = new Matrix4();
46
+ const _scratchWorld = new Matrix4();
47
+ const _scratchEuler = new Euler();
48
+ builtinComponents.forEach((component) => {
49
+ if (!getComponentDef(component.name))
50
+ registerComponent(component);
51
+ });
42
52
  const EMPTY_NODE_COMPONENTS = {
43
53
  geometry: undefined,
44
54
  materials: [],
@@ -47,20 +57,6 @@ const EMPTY_NODE_COMPONENTS = {
47
57
  clickEventName: null,
48
58
  composition: [],
49
59
  };
50
- function precompileModel(model, renderer, camera) {
51
- return __awaiter(this, void 0, void 0, function* () {
52
- try {
53
- if (typeof renderer.compileAsync === "function") {
54
- yield renderer.compileAsync(model, camera);
55
- return;
56
- }
57
- renderer.compile(model, camera);
58
- }
59
- catch (error) {
60
- console.warn("Failed to precompile model before adding it to the scene", error);
61
- }
62
- });
63
- }
64
60
  /** Check if a model component's assets are loaded. */
65
61
  function isNodeReady(model, loadedModels) {
66
62
  var _a;
@@ -76,35 +72,8 @@ function getNodeMetadataProps(node) {
76
72
  userData: Object.assign(Object.assign({ prefabNodeId: node.id }, (nodeName ? { prefabNodeName: nodeName } : {})), getNodeUserData(node)),
77
73
  };
78
74
  }
79
- export var PrefabEditorMode;
80
- (function (PrefabEditorMode) {
81
- PrefabEditorMode["Edit"] = "edit";
82
- PrefabEditorMode["Play"] = "play";
83
- })(PrefabEditorMode || (PrefabEditorMode = {}));
84
- export const SceneContext = createContext(null);
85
- export function useScene() {
86
- const scene = useContext(SceneContext);
87
- if (!scene) {
88
- throw new Error("useScene must be used within a PrefabRoot or PrefabEditor scene provider");
89
- }
90
- return scene;
91
- }
92
- export const PrefabRoot = forwardRef(({ editMode, data, store, selectedId, onSelect, onClick, onEditNodeClick, basePath = "", children }, ref) => {
93
- const renderer = useThree(state => state.gl);
94
- const camera = useThree(state => state.camera);
95
- const [models, setModels] = useState({});
96
- const [textures, setTextures] = useState({});
97
- const [sounds, setSounds] = useState({});
98
- const [injectedModels, setInjectedModels] = useState(EMPTY_MODELS);
99
- const [injectedTextures, setInjectedTextures] = useState(EMPTY_TEXTURES);
100
- const [injectedSounds, setInjectedSounds] = useState(EMPTY_SOUNDS);
101
- const loading = useRef(new Set());
102
- const failedModels = useRef(new Set());
103
- const failedTextures = useRef(new Set());
104
- const failedSounds = useRef(new Set());
105
- const injectedModelVersions = useRef({});
106
- const objectRefs = useRef({});
107
- const nodeHandles = useRef(new Map());
75
+ export const PrefabRoot = forwardRef((props, ref) => {
76
+ const { data, store } = props;
108
77
  const [ownedStore] = useState(() => {
109
78
  if (store)
110
79
  return null;
@@ -113,161 +82,76 @@ export const PrefabRoot = forwardRef(({ editMode, data, store, selectedId, onSel
113
82
  throw new Error("PrefabRoot requires either a `data` or `store` prop");
114
83
  });
115
84
  const resolvedStore = store !== null && store !== void 0 ? store : ownedStore;
116
- if (!resolvedStore) {
85
+ if (!resolvedStore)
117
86
  throw new Error("PrefabRoot requires either a `data` or `store` prop");
118
- }
119
- const usesOwnedStore = resolvedStore === ownedStore;
120
- const rootId = useStore(resolvedStore, state => state.rootId);
121
- const assetRefCounts = useStore(resolvedStore, state => state.assetRefCounts);
122
- const availableModels = useMemo(() => (Object.assign(Object.assign({}, models), injectedModels)), [models, injectedModels]);
123
- const getModel = useCallback((path) => { var _a; return (_a = availableModels[path]) !== null && _a !== void 0 ? _a : null; }, [availableModels]);
124
- const getTexture = useCallback((path) => { var _a, _b; return (_b = (_a = injectedTextures[path]) !== null && _a !== void 0 ? _a : textures[path]) !== null && _b !== void 0 ? _b : null; }, [injectedTextures, textures]);
125
- const getSound = useCallback((path) => { var _a, _b; return (_b = (_a = injectedSounds[path]) !== null && _a !== void 0 ? _a : sounds[path]) !== null && _b !== void 0 ? _b : null; }, [injectedSounds, sounds]);
126
- const assetRevision = useMemo(() => `${Object.keys(textures).concat(Object.keys(injectedTextures)).sort().join('|')}::${Object.keys(availableModels).sort().join('|')}`, [availableModels, injectedTextures, textures]);
127
- const getObject = useCallback((id) => {
128
- var _a;
129
- return (_a = objectRefs.current[id]) !== null && _a !== void 0 ? _a : null;
130
- }, []);
131
- const getHandle = useCallback((id, kind) => {
132
- var _a, _b;
133
- return (_b = (_a = nodeHandles.current.get(id)) === null || _a === void 0 ? void 0 : _a.get(kind)) !== null && _b !== void 0 ? _b : null;
134
- }, []);
135
- const getNode = useCallback((nodeId) => {
136
- var _a;
137
- return (_a = resolvedStore.getState().nodesById[nodeId]) !== null && _a !== void 0 ? _a : null;
138
- }, [resolvedStore]);
139
- const registerHandle = useCallback((id, kind, handle) => {
140
- const current = nodeHandles.current.get(id);
141
- if (handle == null) {
142
- if (!current)
143
- return;
144
- current.delete(kind);
145
- if (current.size === 0) {
146
- nodeHandles.current.delete(id);
147
- }
148
- return;
149
- }
150
- if (current) {
151
- current.set(kind, handle);
152
- return;
153
- }
154
- nodeHandles.current.set(id, new Map([[kind, handle]]));
155
- }, []);
156
- const sceneValue = useMemo(() => ({
157
- get root() {
158
- var _a;
159
- return (_a = objectRefs.current[rootId]) !== null && _a !== void 0 ? _a : null;
160
- },
161
- mode: editMode ? PrefabEditorMode.Edit : PrefabEditorMode.Play,
162
- basePath,
163
- get: getNode,
164
- getObject,
165
- getHandle,
166
- getModel,
167
- add: (node, parentId) => {
168
- const state = resolvedStore.getState();
169
- state.addChild(parentId !== null && parentId !== void 0 ? parentId : state.rootId, node);
170
- return node;
171
- },
172
- update: (id, fn) => resolvedStore.getState().updateNode(id, fn),
173
- replaceNode: (id, node) => resolvedStore.getState().replaceNode(id, node),
174
- remove: (id) => resolvedStore.getState().deleteNode(id),
175
- duplicate: (id) => resolvedStore.getState().duplicateNode(id),
176
- move: (draggedId, targetId, position) => resolvedStore.getState().moveNode(draggedId, targetId, position),
177
- replace: (prefab) => resolvedStore.getState().replacePrefab(prefab),
178
- addModel: (path, model) => {
179
- var _a;
180
- const version = ((_a = injectedModelVersions.current[path]) !== null && _a !== void 0 ? _a : 0) + 1;
181
- injectedModelVersions.current[path] = version;
182
- void precompileModel(model, renderer, camera).then(() => {
183
- if (injectedModelVersions.current[path] !== version)
184
- return;
185
- setInjectedModels(prev => (Object.assign(Object.assign({}, prev), { [path]: model })));
186
- });
187
- },
188
- addTexture: (path, texture) => setInjectedTextures(prev => (Object.assign(Object.assign({}, prev), { [path]: texture }))),
189
- addSound: (path, sound) => {
190
- soundManager.setBuffer(path, sound);
191
- setInjectedSounds(prev => (Object.assign(Object.assign({}, prev), { [path]: sound })));
192
- },
193
- }), [basePath, camera, editMode, getHandle, getModel, getNode, getObject, renderer, resolvedStore, rootId]);
194
- useImperativeHandle(ref, () => sceneValue, [sceneValue]);
195
- const registerRef = useCallback((id, obj) => {
196
- objectRefs.current[id] = obj;
197
- }, []);
198
87
  useEffect(() => {
199
- if (usesOwnedStore && data) {
88
+ if (!store && data)
200
89
  resolvedStore.getState().replacePrefab(data);
201
- }
202
- }, [data, resolvedStore, usesOwnedStore]);
90
+ }, [data, resolvedStore, store]);
91
+ return (_jsx(PrefabStoreProvider, { store: resolvedStore, children: _jsx(AssetRuntimeProvider, { children: _jsx(SceneProvider, { store: resolvedStore, editMode: props.editMode, basePath: props.basePath, children: _jsx(PrefabRootBody, Object.assign({ ref: ref }, props)) }) }) }));
92
+ });
93
+ const PrefabRootBody = forwardRef(({ editMode, selectedId, onSelect, onClick, onEditNodeClick, basePath = "", children }, ref) => {
94
+ const scene = useScene();
95
+ const runtime = useAssetRuntime();
96
+ const models = useAllModels();
97
+ const storeApi = usePrefabStoreApi();
98
+ const assetRefCounts = useStore(storeApi, state => state.assetRefCounts);
99
+ useImperativeHandle(ref, () => scene, [scene]);
100
+ const loading = useRef(new Set());
101
+ const failed = useRef(new Set());
203
102
  useEffect(() => {
204
- const loadAsset = (file, loaded, injected, failed, loader) => {
205
- if (loaded[file] || injected[file] || loading.current.has(file) || failed.has(file))
103
+ const tryLoad = (key, hasLoaded, run) => {
104
+ if (hasLoaded || loading.current.has(key) || failed.current.has(key))
206
105
  return;
207
- loading.current.add(file);
208
- void loader(withBasePath(basePath, file)).then(result => {
209
- loading.current.delete(file);
106
+ loading.current.add(key);
107
+ void run().then(result => {
108
+ loading.current.delete(key);
210
109
  if (!result.success) {
211
- console.warn(`Failed to load asset: ${file}`, result.error);
212
- failed.add(file);
110
+ console.warn(`Failed to load asset: ${key}`, result.error);
111
+ failed.current.add(key);
213
112
  }
214
113
  });
215
114
  };
216
115
  Object.keys(assetRefCounts).forEach(entry => {
217
- const separator = entry.indexOf(':');
218
- const type = entry.slice(0, separator);
219
- const file = entry.slice(separator + 1);
116
+ const sep = entry.indexOf(':');
117
+ const type = entry.slice(0, sep);
118
+ const file = entry.slice(sep + 1);
119
+ const path = withBasePath(basePath, file);
220
120
  if (type === 'model') {
221
- loadAsset(file, models, injectedModels, failedModels.current, path => loadModel(path).then((result) => __awaiter(void 0, void 0, void 0, function* () {
222
- const loadedModel = result.model;
223
- if (result.success && loadedModel) {
224
- yield precompileModel(loadedModel, renderer, camera);
225
- setModels(currentModels => (Object.assign(Object.assign({}, currentModels), { [file]: loadedModel })));
226
- }
227
- return result;
228
- })));
121
+ tryLoad(entry, !!runtime.getModel(file), () => __awaiter(void 0, void 0, void 0, function* () {
122
+ const r = yield loadModel(path);
123
+ if (r.success && r.model)
124
+ runtime.registerModel(file, r.model);
125
+ return r;
126
+ }));
229
127
  }
230
128
  else if (type === 'texture') {
231
- loadAsset(file, textures, injectedTextures, failedTextures.current, path => loadTexture(path).then(result => {
232
- const loadedTexture = result.texture;
233
- if (result.success && loadedTexture) {
234
- setTextures(currentTextures => (Object.assign(Object.assign({}, currentTextures), { [file]: loadedTexture })));
235
- }
236
- return result;
129
+ tryLoad(entry, !!runtime.getTexture(file), () => __awaiter(void 0, void 0, void 0, function* () {
130
+ const r = yield loadTexture(path);
131
+ if (r.success && r.texture)
132
+ runtime.registerTexture(file, r.texture);
133
+ return r;
237
134
  }));
238
135
  }
239
136
  else if (type === 'sound') {
240
- loadAsset(file, sounds, injectedSounds, failedSounds.current, path => loadSound(path).then(result => {
241
- const loadedSound = result.sound;
242
- if (result.success && loadedSound) {
243
- soundManager.setBuffer(file, loadedSound);
244
- setSounds(currentSounds => (Object.assign(Object.assign({}, currentSounds), { [file]: loadedSound })));
245
- }
246
- return result;
137
+ tryLoad(entry, !!runtime.getSound(file), () => __awaiter(void 0, void 0, void 0, function* () {
138
+ const r = yield loadSound(path);
139
+ if (r.success && r.sound)
140
+ runtime.registerSound(file, r.sound);
141
+ return r;
247
142
  }));
248
143
  }
249
144
  });
250
- }, [assetRefCounts, basePath, camera, injectedModels, injectedSounds, injectedTextures, models, renderer, sounds, textures]);
251
- const assetRuntime = useMemo(() => ({
252
- registerHandle,
253
- getHandle,
254
- getObject,
255
- getModel,
256
- getTexture,
257
- getSound,
258
- getAssetRevision: () => assetRevision,
259
- }), [registerHandle, getHandle, getObject, getModel, getTexture, getSound, assetRevision]);
145
+ }, [assetRefCounts, basePath, runtime]);
260
146
  const handleNodeClick = useCallback((event, nodeId, fallbackObject) => {
261
- const node = resolvedStore.getState().nodesById[nodeId];
147
+ const node = storeApi.getState().nodesById[nodeId];
262
148
  if (!node)
263
149
  return;
264
150
  const { clickEventName } = analyzeNodeComponents(node);
265
151
  emitNodePointerEvent(clickEventName, event, nodeId, node, fallbackObject);
266
152
  onClick === null || onClick === void 0 ? void 0 : onClick(event, node);
267
- }, [onClick, resolvedStore]);
268
- const content = (_jsxs(GameInstanceProvider, { models: availableModels, selectedId: selectedId, editMode: editMode, onSelect: editMode ? onSelect : undefined, onClick: editMode ? undefined : handleNodeClick, registerRef: registerRef, children: [_jsx(StoreRootNode, { selectedId: selectedId, onSelect: editMode ? onSelect : undefined, onClick: editMode ? undefined : handleNodeClick, onEditNodeClick: editMode ? onEditNodeClick : undefined, registerRef: registerRef, loadedModels: availableModels, editMode: editMode, parentMatrix: IDENTITY, basePath: basePath }), children] }));
269
- const runtimeContent = (_jsx(SceneContext.Provider, { value: sceneValue, children: _jsx(AssetRuntimeContext.Provider, { value: assetRuntime, children: content }) }));
270
- return _jsx(PrefabStoreProvider, { store: resolvedStore, children: runtimeContent });
153
+ }, [onClick, storeApi]);
154
+ return (_jsxs(GameInstanceProvider, { models: models, selectedId: selectedId, editMode: editMode, onSelect: editMode ? onSelect : undefined, onClick: editMode ? undefined : handleNodeClick, registerRef: runtime.registerObject, children: [_jsx(StoreRootNode, { selectedId: selectedId, onSelect: editMode ? onSelect : undefined, onClick: editMode ? undefined : handleNodeClick, onEditNodeClick: editMode ? onEditNodeClick : undefined, registerRef: runtime.registerObject, loadedModels: models, editMode: editMode, parentMatrix: IDENTITY, basePath: basePath }), children] }));
271
155
  });
272
156
  function StoreRootNode(props) {
273
157
  const rootId = usePrefabRootId();
@@ -483,11 +367,11 @@ function buildRepeatedInstances(gameObject, parentMatrix, modelUrl) {
483
367
  offsets[axisIndex] = entry.offset;
484
368
  }
485
369
  }
486
- const baseTranslation = new Matrix4().makeTranslation(transform.position[0], transform.position[1], transform.position[2]);
487
- const baseRotation = new Matrix4().makeRotationFromEuler(new Euler(...transform.rotation));
488
- const baseScale = new Matrix4().makeScale(transform.scale[0], transform.scale[1], transform.scale[2]);
489
- const offsetMatrix = new Matrix4();
490
- const worldMatrix = new Matrix4();
370
+ const baseTranslation = _scratchTranslation.makeTranslation(transform.position[0], transform.position[1], transform.position[2]);
371
+ const baseRotation = _scratchRotation.makeRotationFromEuler(_scratchEuler.set(transform.rotation[0], transform.rotation[1], transform.rotation[2]));
372
+ const baseScale = _scratchScale.makeScale(transform.scale[0], transform.scale[1], transform.scale[2]);
373
+ const offsetMatrix = _scratchOffset;
374
+ const worldMatrix = _scratchWorld;
491
375
  const instances = [];
492
376
  for (let x = 0; x < counts[0]; x++) {
493
377
  for (let y = 0; y < counts[1]; y++) {
@@ -0,0 +1,28 @@
1
+ import type { Object3D, Texture } from "three";
2
+ import type { GameObject, Prefab } from "./types";
3
+ export declare enum PrefabEditorMode {
4
+ Edit = "edit",
5
+ Play = "play"
6
+ }
7
+ export type PrefabNode = Omit<GameObject, "children">;
8
+ export interface Scene {
9
+ root: Object3D | null;
10
+ mode: PrefabEditorMode;
11
+ basePath: string;
12
+ get(id: string): GameObject | null;
13
+ getObject(id: string): Object3D | null;
14
+ getHandle<T = unknown>(id: string, kind: string): T | null;
15
+ getModel(path: string): Object3D | null;
16
+ add(node: GameObject, parentId?: string): GameObject;
17
+ update(id: string, fn: (node: PrefabNode) => PrefabNode): void;
18
+ replaceNode(id: string, node: GameObject): void;
19
+ remove(id: string): void;
20
+ duplicate(id: string): string | null;
21
+ move(draggedId: string, targetId: string, position: "before" | "inside"): void;
22
+ replace(prefab: Prefab): void;
23
+ addModel(path: string, model: Object3D): void;
24
+ addTexture(path: string, texture: Texture): void;
25
+ addSound(path: string, sound: AudioBuffer): void;
26
+ }
27
+ export declare const SceneContext: import("react").Context<Scene | null>;
28
+ export declare function useScene(): Scene;
@@ -0,0 +1,14 @@
1
+ import { createContext, useContext } from "react";
2
+ export var PrefabEditorMode;
3
+ (function (PrefabEditorMode) {
4
+ PrefabEditorMode["Edit"] = "edit";
5
+ PrefabEditorMode["Play"] = "play";
6
+ })(PrefabEditorMode || (PrefabEditorMode = {}));
7
+ export const SceneContext = createContext(null);
8
+ export function useScene() {
9
+ const scene = useContext(SceneContext);
10
+ if (!scene) {
11
+ throw new Error("useScene must be used within a PrefabRoot or PrefabEditor scene provider");
12
+ }
13
+ return scene;
14
+ }
@@ -0,0 +1,14 @@
1
+ import { type ReactNode } from "react";
2
+ import type { PrefabStoreApi } from "./prefabStore";
3
+ export interface SceneProviderProps {
4
+ store: PrefabStoreApi;
5
+ editMode?: boolean;
6
+ basePath?: string;
7
+ children: ReactNode;
8
+ }
9
+ /**
10
+ * Recursive provider: if a Scene is already present above, this is a
11
+ * pass-through. Otherwise this layer becomes the owner and builds a default
12
+ * Scene bound to the given store + asset runtime.
13
+ */
14
+ export declare function SceneProvider({ store, editMode, basePath, children }: SceneProviderProps): import("react").JSX.Element;
@@ -0,0 +1,68 @@
1
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
2
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
3
+ return new (P || (P = Promise))(function (resolve, reject) {
4
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
5
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
6
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
7
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
8
+ });
9
+ };
10
+ import { Fragment as _Fragment, jsx as _jsx } from "react/jsx-runtime";
11
+ import { useContext, useMemo } from "react";
12
+ import { useThree } from "@react-three/fiber";
13
+ import { useStore } from "zustand";
14
+ import { PrefabEditorMode, SceneContext } from "./SceneContext";
15
+ import { useAssetRuntime } from "./assetRuntime";
16
+ /**
17
+ * Recursive provider: if a Scene is already present above, this is a
18
+ * pass-through. Otherwise this layer becomes the owner and builds a default
19
+ * Scene bound to the given store + asset runtime.
20
+ */
21
+ export function SceneProvider({ store, editMode, basePath = "", children }) {
22
+ const inherited = useContext(SceneContext);
23
+ if (inherited !== null)
24
+ return _jsx(_Fragment, { children: children });
25
+ return (_jsx(SceneOwner, { store: store, editMode: editMode, basePath: basePath, children: children }));
26
+ }
27
+ function SceneOwner({ store, editMode, basePath, children }) {
28
+ const runtime = useAssetRuntime();
29
+ const renderer = useThree(s => s.gl);
30
+ const camera = useThree(s => s.camera);
31
+ const rootId = useStore(store, s => s.rootId);
32
+ const scene = useMemo(() => ({
33
+ get root() { return runtime.getObject(rootId); },
34
+ mode: editMode ? PrefabEditorMode.Edit : PrefabEditorMode.Play,
35
+ basePath,
36
+ get: (id) => { var _a; return (_a = store.getState().nodesById[id]) !== null && _a !== void 0 ? _a : null; },
37
+ getObject: runtime.getObject,
38
+ getHandle: runtime.getHandle,
39
+ getModel: runtime.getModel,
40
+ add: (node, parentId) => {
41
+ const s = store.getState();
42
+ s.addChild(parentId !== null && parentId !== void 0 ? parentId : s.rootId, node);
43
+ return node;
44
+ },
45
+ update: (id, fn) => store.getState().updateNode(id, fn),
46
+ replaceNode: (id, node) => store.getState().replaceNode(id, node),
47
+ remove: (id) => store.getState().deleteNode(id),
48
+ duplicate: (id) => store.getState().duplicateNode(id),
49
+ move: (a, b, p) => store.getState().moveNode(a, b, p),
50
+ replace: (p) => store.getState().replacePrefab(p),
51
+ addModel: (path, model) => {
52
+ void precompile(model, renderer, camera).then(() => runtime.registerModel(path, model));
53
+ },
54
+ addTexture: runtime.registerTexture,
55
+ addSound: runtime.registerSound,
56
+ }), [store, editMode, basePath, runtime, rootId, renderer, camera]);
57
+ return _jsx(SceneContext.Provider, { value: scene, children: children });
58
+ }
59
+ function precompile(model, renderer, camera) {
60
+ return __awaiter(this, void 0, void 0, function* () {
61
+ try {
62
+ yield renderer.compileAsync(model, camera);
63
+ }
64
+ catch (error) {
65
+ console.warn("Failed to precompile model before adding it to the scene", error);
66
+ }
67
+ });
68
+ }
@@ -1,12 +1,20 @@
1
1
  import { type ReactNode } from "react";
2
2
  import type { Object3D, Texture } from "three";
3
+ import type { LoadedModels, LoadedSounds, LoadedTextures } from "../dragdrop";
3
4
  export interface AssetRuntime {
5
+ models: LoadedModels;
6
+ textures: LoadedTextures;
7
+ sounds: LoadedSounds;
8
+ registerObject: (id: string, object: Object3D | null) => void;
4
9
  registerHandle: (id: string, kind: string, handle: unknown) => void;
10
+ registerModel: (path: string, model: Object3D) => void;
11
+ registerTexture: (path: string, texture: Texture) => void;
12
+ registerSound: (path: string, sound: AudioBuffer) => void;
5
13
  getHandle: <T = unknown>(id: string, kind: string) => T | null;
6
14
  getModel: (path: string) => Object3D | null;
7
15
  getTexture: (path: string) => Texture | null;
8
16
  getSound: (path: string) => AudioBuffer | null;
9
- getAssetRevision: () => string;
17
+ getAssetRevision: () => number;
10
18
  getObject: (id: string) => Object3D | null;
11
19
  }
12
20
  export interface NodeApi {
@@ -19,7 +27,21 @@ export interface NodeApi {
19
27
  export interface LiveRef<T> {
20
28
  readonly current: T | null;
21
29
  }
30
+ export interface AssetRuntimeProviderProps {
31
+ children: ReactNode;
32
+ runtimeRef?: React.MutableRefObject<AssetRuntime | null>;
33
+ }
22
34
  export declare const AssetRuntimeContext: import("react").Context<AssetRuntime | null>;
35
+ /** Subscribe to a single loaded model; re-renders only when that model changes. */
36
+ export declare function useModelAsset(path?: string | null): Object3D | null;
37
+ /** Subscribe to a single loaded texture; re-renders only when that texture changes. */
38
+ export declare function useTextureAsset(path?: string | null): Texture | null;
39
+ /** Subscribe to a single loaded sound; re-renders only when that sound changes. */
40
+ export declare function useSoundAsset(path?: string | null): AudioBuffer | null;
41
+ /** Subscribe to the full model map (only needed by the instancing root). */
42
+ export declare function useAllModels(): LoadedModels;
43
+ /** Coarse "an asset was (un)registered" signal. */
44
+ export declare function useAssetRevision(): number;
23
45
  export declare function useAssetRuntime(): AssetRuntime;
24
46
  export declare function useNode(): NodeApi;
25
47
  export declare function useNodeObject<T extends Object3D = Object3D>(): LiveRef<T>;
@@ -29,4 +51,9 @@ export declare function NodeScope({ nodeId, editMode, isSelected, children, }: {
29
51
  editMode?: boolean;
30
52
  isSelected?: boolean;
31
53
  children: ReactNode;
32
- }): import("react/jsx-runtime").JSX.Element;
54
+ }): import("react").JSX.Element;
55
+ /**
56
+ * Recursive provider: if an AssetRuntime is already present above, this is a
57
+ * pass-through. Otherwise this layer becomes the owner and allocates state.
58
+ */
59
+ export declare function AssetRuntimeProvider({ children, runtimeRef }: AssetRuntimeProviderProps): string | number | bigint | boolean | Iterable<ReactNode> | Promise<string | number | bigint | boolean | import("react").ReactPortal | import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>> | Iterable<ReactNode> | null | undefined> | import("react").JSX.Element | null | undefined;
@@ -1,7 +1,40 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
- import { createContext, useContext, useMemo } from "react";
2
+ import { createContext, useCallback, useContext, useMemo, useRef, useState } from "react";
3
+ import { useStore } from "zustand";
4
+ import { createStore } from "zustand/vanilla";
5
+ import { sound as soundManager } from "../../helpers/SoundManager";
3
6
  export const AssetRuntimeContext = createContext(null);
4
7
  const NodeContext = createContext(null);
8
+ function createAssetStore() {
9
+ return createStore(() => ({ models: {}, textures: {}, sounds: {}, version: 0 }));
10
+ }
11
+ const AssetStoreContext = createContext(null);
12
+ function useAssetStore() {
13
+ const store = useContext(AssetStoreContext);
14
+ if (!store)
15
+ throw new Error("Asset hooks must be used inside <PrefabRoot>");
16
+ return store;
17
+ }
18
+ /** Subscribe to a single loaded model; re-renders only when that model changes. */
19
+ export function useModelAsset(path) {
20
+ return useStore(useAssetStore(), s => { var _a; return (path ? (_a = s.models[path]) !== null && _a !== void 0 ? _a : null : null); });
21
+ }
22
+ /** Subscribe to a single loaded texture; re-renders only when that texture changes. */
23
+ export function useTextureAsset(path) {
24
+ return useStore(useAssetStore(), s => { var _a; return (path ? (_a = s.textures[path]) !== null && _a !== void 0 ? _a : null : null); });
25
+ }
26
+ /** Subscribe to a single loaded sound; re-renders only when that sound changes. */
27
+ export function useSoundAsset(path) {
28
+ return useStore(useAssetStore(), s => { var _a; return (path ? (_a = s.sounds[path]) !== null && _a !== void 0 ? _a : null : null); });
29
+ }
30
+ /** Subscribe to the full model map (only needed by the instancing root). */
31
+ export function useAllModels() {
32
+ return useStore(useAssetStore(), s => s.models);
33
+ }
34
+ /** Coarse "an asset was (un)registered" signal. */
35
+ export function useAssetRevision() {
36
+ return useStore(useAssetStore(), s => s.version);
37
+ }
5
38
  export function useAssetRuntime() {
6
39
  const ctx = useContext(AssetRuntimeContext);
7
40
  if (!ctx)
@@ -35,3 +68,84 @@ export function NodeScope({ nodeId, editMode, isSelected, children, }) {
35
68
  }), [asset, editMode, isSelected, nodeId]);
36
69
  return _jsx(NodeContext.Provider, { value: value, children: children });
37
70
  }
71
+ /**
72
+ * Recursive provider: if an AssetRuntime is already present above, this is a
73
+ * pass-through. Otherwise this layer becomes the owner and allocates state.
74
+ */
75
+ export function AssetRuntimeProvider({ children, runtimeRef }) {
76
+ const inherited = useContext(AssetRuntimeContext);
77
+ if (inherited !== null) {
78
+ if (runtimeRef)
79
+ runtimeRef.current = inherited;
80
+ return children;
81
+ }
82
+ return _jsx(AssetRuntimeOwner, { runtimeRef: runtimeRef, children: children });
83
+ }
84
+ function AssetRuntimeOwner({ children, runtimeRef }) {
85
+ const [assetStore] = useState(createAssetStore);
86
+ const objectRefs = useRef({});
87
+ const nodeHandles = useRef(new Map());
88
+ const registerObject = useCallback((id, obj) => {
89
+ if (obj)
90
+ objectRefs.current[id] = obj;
91
+ else
92
+ delete objectRefs.current[id];
93
+ }, []);
94
+ const registerHandle = useCallback((id, kind, handle) => {
95
+ const current = nodeHandles.current.get(id);
96
+ if (handle == null) {
97
+ if (!current)
98
+ return;
99
+ current.delete(kind);
100
+ if (current.size === 0)
101
+ nodeHandles.current.delete(id);
102
+ return;
103
+ }
104
+ if (current) {
105
+ current.set(kind, handle);
106
+ return;
107
+ }
108
+ nodeHandles.current.set(id, new Map([[kind, handle]]));
109
+ }, []);
110
+ const registerModel = useCallback((path, model) => {
111
+ if (assetStore.getState().models[path] === model)
112
+ return;
113
+ assetStore.setState(s => ({ models: Object.assign(Object.assign({}, s.models), { [path]: model }), version: s.version + 1 }));
114
+ }, [assetStore]);
115
+ const registerTexture = useCallback((path, texture) => {
116
+ if (assetStore.getState().textures[path] === texture)
117
+ return;
118
+ assetStore.setState(s => ({ textures: Object.assign(Object.assign({}, s.textures), { [path]: texture }), version: s.version + 1 }));
119
+ }, [assetStore]);
120
+ const registerSound = useCallback((path, sound) => {
121
+ soundManager.setBuffer(path, sound);
122
+ if (assetStore.getState().sounds[path] === sound)
123
+ return;
124
+ assetStore.setState(s => ({ sounds: Object.assign(Object.assign({}, s.sounds), { [path]: sound }), version: s.version + 1 }));
125
+ }, [assetStore]);
126
+ const getObject = useCallback((id) => { var _a; return (_a = objectRefs.current[id]) !== null && _a !== void 0 ? _a : null; }, []);
127
+ const getHandle = useCallback((id, kind) => {
128
+ var _a, _b;
129
+ return (_b = (_a = nodeHandles.current.get(id)) === null || _a === void 0 ? void 0 : _a.get(kind)) !== null && _b !== void 0 ? _b : null;
130
+ }, []);
131
+ const getModel = useCallback((path) => { var _a; return (_a = assetStore.getState().models[path]) !== null && _a !== void 0 ? _a : null; }, [assetStore]);
132
+ const getTexture = useCallback((path) => { var _a; return (_a = assetStore.getState().textures[path]) !== null && _a !== void 0 ? _a : null; }, [assetStore]);
133
+ const getSound = useCallback((path) => { var _a; return (_a = assetStore.getState().sounds[path]) !== null && _a !== void 0 ? _a : null; }, [assetStore]);
134
+ const getAssetRevision = useCallback(() => assetStore.getState().version, [assetStore]);
135
+ // Stable runtime: all members have stable identity, so consumers that only
136
+ // use imperative getters/registrars never re-render on asset loads. The
137
+ // live maps are exposed as snapshot getters for imperative readers; reactive
138
+ // consumers use the selector hooks (useModelAsset, useAllModels, ...).
139
+ const runtime = useMemo(() => ({
140
+ get models() { return assetStore.getState().models; },
141
+ get textures() { return assetStore.getState().textures; },
142
+ get sounds() { return assetStore.getState().sounds; },
143
+ registerObject, registerHandle,
144
+ registerModel, registerTexture, registerSound,
145
+ getObject, getHandle, getModel, getTexture, getSound,
146
+ getAssetRevision,
147
+ }), [assetStore, registerObject, registerHandle, registerModel, registerTexture, registerSound, getObject, getHandle, getModel, getTexture, getSound, getAssetRevision]);
148
+ if (runtimeRef)
149
+ runtimeRef.current = runtime;
150
+ return (_jsx(AssetStoreContext.Provider, { value: assetStore, children: _jsx(AssetRuntimeContext.Provider, { value: runtime, children: children }) }));
151
+ }
@@ -38,7 +38,7 @@ export interface Component {
38
38
  name: string;
39
39
  /** Set when this component occupies a single slot on a node. Use a string to share a slot across component types. */
40
40
  disableSiblingComposition?: boolean | string;
41
- Editor: FC<{
41
+ Editor?: FC<{
42
42
  node?: GameObject;
43
43
  component: ComponentData;
44
44
  onUpdate: (newComp: Record<string, unknown>) => void;