react-three-game 0.0.107 → 0.0.109

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 (49) hide show
  1. package/README.md +12 -5
  2. package/dist/editor.d.ts +22 -0
  3. package/dist/editor.js +15 -0
  4. package/dist/plugins/crashcat/CrashcatPhysicsComponent.js +75 -47
  5. package/dist/plugins/crashcat/CrashcatRagdoll.d.ts +58 -0
  6. package/dist/plugins/crashcat/CrashcatRagdoll.js +410 -0
  7. package/dist/plugins/crashcat/CrashcatRuntime.js +19 -15
  8. package/dist/plugins/crashcat/index.d.ts +1 -0
  9. package/dist/plugins/crashcat/index.js +1 -0
  10. package/dist/tools/assetviewer/page.js +4 -4
  11. package/dist/tools/prefabeditor/EditorContext.d.ts +36 -0
  12. package/dist/tools/prefabeditor/EditorContext.js +17 -0
  13. package/dist/tools/prefabeditor/EditorTree.js +6 -3
  14. package/dist/tools/prefabeditor/EditorTreeMenus.d.ts +2 -1
  15. package/dist/tools/prefabeditor/EditorTreeMenus.js +18 -6
  16. package/dist/tools/prefabeditor/EditorUI.js +1 -1
  17. package/dist/tools/prefabeditor/GameEvents.d.ts +1 -0
  18. package/dist/tools/prefabeditor/PrefabEditor.d.ts +5 -37
  19. package/dist/tools/prefabeditor/PrefabEditor.js +41 -43
  20. package/dist/tools/prefabeditor/PrefabRoot.d.ts +5 -27
  21. package/dist/tools/prefabeditor/PrefabRoot.js +133 -78
  22. package/dist/tools/prefabeditor/SceneContext.d.ts +28 -0
  23. package/dist/tools/prefabeditor/SceneContext.js +14 -0
  24. package/dist/tools/prefabeditor/assetRuntime.d.ts +4 -0
  25. package/dist/tools/prefabeditor/components/ComponentRegistry.d.ts +16 -1
  26. package/dist/tools/prefabeditor/components/ModelComponent.js +1 -1
  27. package/dist/tools/prefabeditor/components/PrefabRefComponent.d.ts +3 -0
  28. package/dist/tools/prefabeditor/components/PrefabRefComponent.js +72 -0
  29. package/dist/tools/prefabeditor/components/TextComponent.js +8 -5
  30. package/dist/tools/prefabeditor/components/TransformComponent.js +1 -1
  31. package/dist/tools/prefabeditor/components/index.d.ts +1 -0
  32. package/dist/tools/prefabeditor/components/index.js +10 -0
  33. package/dist/tools/prefabeditor/components/runtime.d.ts +4 -0
  34. package/dist/tools/prefabeditor/components/runtime.js +372 -0
  35. package/dist/tools/prefabeditor/modelPrefab.d.ts +3 -0
  36. package/dist/tools/prefabeditor/modelPrefab.js +44 -11
  37. package/dist/tools/prefabeditor/prefab.d.ts +5 -4
  38. package/dist/tools/prefabeditor/prefab.js +47 -29
  39. package/dist/tools/prefabeditor/prefabStore.d.ts +1 -1
  40. package/dist/tools/prefabeditor/prefabStore.js +5 -1
  41. package/dist/tools/prefabeditor/runtimeUtils.d.ts +10 -0
  42. package/dist/tools/prefabeditor/runtimeUtils.js +30 -0
  43. package/dist/tools/prefabeditor/utils.d.ts +7 -8
  44. package/dist/tools/prefabeditor/utils.js +63 -36
  45. package/dist/viewer.d.ts +22 -0
  46. package/dist/viewer.js +14 -0
  47. package/package.json +22 -20
  48. package/dist/index.d.ts +0 -40
  49. package/dist/index.js +0 -32
@@ -1,3 +1,12 @@
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
+ };
1
10
  var __rest = (this && this.__rest) || function (s, e) {
2
11
  var t = {};
3
12
  for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
@@ -10,25 +19,22 @@ var __rest = (this && this.__rest) || function (s, e) {
10
19
  return t;
11
20
  };
12
21
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
13
- import { createContext, forwardRef, useCallback, useContext, useEffect, useImperativeHandle, useMemo, useRef, useState } from "react";
22
+ import { forwardRef, useCallback, useContext, useEffect, useImperativeHandle, useMemo, useRef, useState } from "react";
14
23
  import { Euler, Matrix4 } from "three";
24
+ import { useThree } from "@react-three/fiber";
15
25
  import { useStore } from "zustand";
16
26
  import { useClickValid } from "./useClickValid";
17
27
  import { findComponent, getNodeUserData } from "./types";
18
- import { getComponentDef, registerComponent } from "./components/ComponentRegistry";
19
- import { builtinComponents } from "./components";
28
+ import { getComponentDef } from "./components/ComponentRegistry";
20
29
  import { loadModel, loadSound, loadTexture } from "../dragdrop";
21
30
  import { GameInstance, GameInstanceProvider, getRepeatAxesFromModelProperties } from "./InstanceProvider";
22
- import { composeTransform, decompose } from "./utils";
31
+ import { composeTransform, decompose, withBasePath } from "./runtimeUtils";
23
32
  import { createPrefabStore, PrefabStoreProvider, usePrefabChildIds, usePrefabNode, usePrefabRootId } from "./prefabStore";
24
33
  import { AssetRuntimeContext, NodeScope } from "./assetRuntime";
25
34
  import { gameEvents } from "./GameEvents";
26
35
  import { sound as soundManager } from "../../helpers/SoundManager";
27
- builtinComponents.forEach(registerComponent);
36
+ import { PrefabEditorMode, SceneContext } from "./SceneContext";
28
37
  const IDENTITY = new Matrix4();
29
- const EMPTY_MODELS = {};
30
- const EMPTY_TEXTURES = {};
31
- const EMPTY_SOUNDS = {};
32
38
  const EMPTY_NODE_COMPONENTS = {
33
39
  geometry: undefined,
34
40
  materials: [],
@@ -37,13 +43,19 @@ const EMPTY_NODE_COMPONENTS = {
37
43
  clickEventName: null,
38
44
  composition: [],
39
45
  };
40
- /** Resolve a relative or absolute asset file path against a base path. */
41
- function resolveAssetPath(basePath, file) {
42
- if (file.startsWith("data:"))
43
- return file;
44
- if (file.startsWith("http://") || file.startsWith("https://"))
45
- return file;
46
- return file.startsWith("/") ? `${basePath}${file}` : `${basePath}/${file}`;
46
+ function precompileModel(model, renderer, camera) {
47
+ return __awaiter(this, void 0, void 0, function* () {
48
+ try {
49
+ if (typeof renderer.compileAsync === "function") {
50
+ yield renderer.compileAsync(model, camera);
51
+ return;
52
+ }
53
+ renderer.compile(model, camera);
54
+ }
55
+ catch (error) {
56
+ console.warn("Failed to precompile model before adding it to the scene", error);
57
+ }
58
+ });
47
59
  }
48
60
  /** Check if a model component's assets are loaded. */
49
61
  function isNodeReady(model, loadedModels) {
@@ -60,30 +72,19 @@ function getNodeMetadataProps(node) {
60
72
  userData: Object.assign(Object.assign({ prefabNodeId: node.id }, (nodeName ? { prefabNodeName: nodeName } : {})), getNodeUserData(node)),
61
73
  };
62
74
  }
63
- export var PrefabEditorMode;
64
- (function (PrefabEditorMode) {
65
- PrefabEditorMode["Edit"] = "edit";
66
- PrefabEditorMode["Play"] = "play";
67
- })(PrefabEditorMode || (PrefabEditorMode = {}));
68
- export const SceneContext = createContext(null);
69
- export function useScene() {
70
- const scene = useContext(SceneContext);
71
- if (!scene) {
72
- throw new Error("useScene must be used within a PrefabRoot or PrefabEditor scene provider");
73
- }
74
- return scene;
75
- }
76
75
  export const PrefabRoot = forwardRef(({ editMode, data, store, selectedId, onSelect, onClick, onEditNodeClick, basePath = "", children }, ref) => {
76
+ const parentScene = useContext(SceneContext);
77
+ const parentAssetRuntime = useContext(AssetRuntimeContext);
78
+ const renderer = useThree(state => state.gl);
79
+ const camera = useThree(state => state.camera);
77
80
  const [models, setModels] = useState({});
78
81
  const [textures, setTextures] = useState({});
79
82
  const [sounds, setSounds] = useState({});
80
- const [injectedModels, setInjectedModels] = useState(EMPTY_MODELS);
81
- const [injectedTextures, setInjectedTextures] = useState(EMPTY_TEXTURES);
82
- const [injectedSounds, setInjectedSounds] = useState(EMPTY_SOUNDS);
83
83
  const loading = useRef(new Set());
84
84
  const failedModels = useRef(new Set());
85
85
  const failedTextures = useRef(new Set());
86
86
  const failedSounds = useRef(new Set());
87
+ const injectedModelVersions = useRef({});
87
88
  const objectRefs = useRef({});
88
89
  const nodeHandles = useRef(new Map());
89
90
  const [ownedStore] = useState(() => {
@@ -100,19 +101,18 @@ export const PrefabRoot = forwardRef(({ editMode, data, store, selectedId, onSel
100
101
  const usesOwnedStore = resolvedStore === ownedStore;
101
102
  const rootId = useStore(resolvedStore, state => state.rootId);
102
103
  const assetRefCounts = useStore(resolvedStore, state => state.assetRefCounts);
103
- const availableModels = useMemo(() => (Object.assign(Object.assign({}, models), injectedModels)), [models, injectedModels]);
104
- const getModel = useCallback((path) => { var _a; return (_a = availableModels[path]) !== null && _a !== void 0 ? _a : null; }, [availableModels]);
105
- 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]);
106
- 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]);
107
- const assetRevision = useMemo(() => `${Object.keys(textures).concat(Object.keys(injectedTextures)).sort().join('|')}::${Object.keys(availableModels).sort().join('|')}`, [availableModels, injectedTextures, textures]);
104
+ const getModel = useCallback((path) => { var _a, _b; return (_b = (_a = models[path]) !== null && _a !== void 0 ? _a : parentAssetRuntime === null || parentAssetRuntime === void 0 ? void 0 : parentAssetRuntime.getModel(path)) !== null && _b !== void 0 ? _b : null; }, [models, parentAssetRuntime]);
105
+ const getTexture = useCallback((path) => { var _a, _b; return (_b = (_a = textures[path]) !== null && _a !== void 0 ? _a : parentAssetRuntime === null || parentAssetRuntime === void 0 ? void 0 : parentAssetRuntime.getTexture(path)) !== null && _b !== void 0 ? _b : null; }, [parentAssetRuntime, textures]);
106
+ const getSound = useCallback((path) => { var _a, _b; return (_b = (_a = sounds[path]) !== null && _a !== void 0 ? _a : parentAssetRuntime === null || parentAssetRuntime === void 0 ? void 0 : parentAssetRuntime.getSound(path)) !== null && _b !== void 0 ? _b : null; }, [parentAssetRuntime, sounds]);
107
+ const assetRevision = useMemo(() => { var _a; return `${(_a = parentAssetRuntime === null || parentAssetRuntime === void 0 ? void 0 : parentAssetRuntime.getAssetRevision()) !== null && _a !== void 0 ? _a : ""}::${Object.keys(textures).sort().join('|')}::${Object.keys(models).sort().join('|')}`; }, [models, parentAssetRuntime, textures]);
108
108
  const getObject = useCallback((id) => {
109
- var _a;
110
- return (_a = objectRefs.current[id]) !== null && _a !== void 0 ? _a : null;
111
- }, []);
112
- const getHandle = useCallback((id, kind) => {
113
109
  var _a, _b;
114
- return (_b = (_a = nodeHandles.current.get(id)) === null || _a === void 0 ? void 0 : _a.get(kind)) !== null && _b !== void 0 ? _b : null;
115
- }, []);
110
+ return (_b = (_a = objectRefs.current[id]) !== null && _a !== void 0 ? _a : parentAssetRuntime === null || parentAssetRuntime === void 0 ? void 0 : parentAssetRuntime.getObject(id)) !== null && _b !== void 0 ? _b : null;
111
+ }, [parentAssetRuntime]);
112
+ const getHandle = useCallback((id, kind) => {
113
+ var _a, _b, _c;
114
+ return (_c = (_b = (_a = nodeHandles.current.get(id)) === null || _a === void 0 ? void 0 : _a.get(kind)) !== null && _b !== void 0 ? _b : parentAssetRuntime === null || parentAssetRuntime === void 0 ? void 0 : parentAssetRuntime.getHandle(id, kind)) !== null && _c !== void 0 ? _c : null;
115
+ }, [parentAssetRuntime]);
116
116
  const getNode = useCallback((nodeId) => {
117
117
  var _a;
118
118
  return (_a = resolvedStore.getState().nodesById[nodeId]) !== null && _a !== void 0 ? _a : null;
@@ -120,6 +120,7 @@ export const PrefabRoot = forwardRef(({ editMode, data, store, selectedId, onSel
120
120
  const registerHandle = useCallback((id, kind, handle) => {
121
121
  const current = nodeHandles.current.get(id);
122
122
  if (handle == null) {
123
+ parentAssetRuntime === null || parentAssetRuntime === void 0 ? void 0 : parentAssetRuntime.registerHandle(id, kind, null);
123
124
  if (!current)
124
125
  return;
125
126
  current.delete(kind);
@@ -130,16 +131,41 @@ export const PrefabRoot = forwardRef(({ editMode, data, store, selectedId, onSel
130
131
  }
131
132
  if (current) {
132
133
  current.set(kind, handle);
134
+ parentAssetRuntime === null || parentAssetRuntime === void 0 ? void 0 : parentAssetRuntime.registerHandle(id, kind, handle);
133
135
  return;
134
136
  }
135
137
  nodeHandles.current.set(id, new Map([[kind, handle]]));
136
- }, []);
138
+ parentAssetRuntime === null || parentAssetRuntime === void 0 ? void 0 : parentAssetRuntime.registerHandle(id, kind, handle);
139
+ }, [parentAssetRuntime]);
140
+ const registerObject = useCallback((id, obj) => {
141
+ if (obj) {
142
+ objectRefs.current[id] = obj;
143
+ }
144
+ else {
145
+ delete objectRefs.current[id];
146
+ }
147
+ parentAssetRuntime === null || parentAssetRuntime === void 0 ? void 0 : parentAssetRuntime.registerObject(id, obj);
148
+ }, [parentAssetRuntime]);
149
+ const registerModel = useCallback((path, model) => {
150
+ parentAssetRuntime === null || parentAssetRuntime === void 0 ? void 0 : parentAssetRuntime.registerModel(path, model);
151
+ setModels(prev => prev[path] === model ? prev : Object.assign(Object.assign({}, prev), { [path]: model }));
152
+ }, [parentAssetRuntime]);
153
+ const registerTexture = useCallback((path, texture) => {
154
+ parentAssetRuntime === null || parentAssetRuntime === void 0 ? void 0 : parentAssetRuntime.registerTexture(path, texture);
155
+ setTextures(prev => prev[path] === texture ? prev : Object.assign(Object.assign({}, prev), { [path]: texture }));
156
+ }, [parentAssetRuntime]);
157
+ const registerSound = useCallback((path, sound) => {
158
+ parentAssetRuntime === null || parentAssetRuntime === void 0 ? void 0 : parentAssetRuntime.registerSound(path, sound);
159
+ soundManager.setBuffer(path, sound);
160
+ setSounds(prev => prev[path] === sound ? prev : Object.assign(Object.assign({}, prev), { [path]: sound }));
161
+ }, [parentAssetRuntime]);
137
162
  const sceneValue = useMemo(() => ({
138
163
  get root() {
139
164
  var _a;
140
165
  return (_a = objectRefs.current[rootId]) !== null && _a !== void 0 ? _a : null;
141
166
  },
142
167
  mode: editMode ? PrefabEditorMode.Edit : PrefabEditorMode.Play,
168
+ basePath,
143
169
  get: getNode,
144
170
  getObject,
145
171
  getHandle,
@@ -155,28 +181,31 @@ export const PrefabRoot = forwardRef(({ editMode, data, store, selectedId, onSel
155
181
  duplicate: (id) => resolvedStore.getState().duplicateNode(id),
156
182
  move: (draggedId, targetId, position) => resolvedStore.getState().moveNode(draggedId, targetId, position),
157
183
  replace: (prefab) => resolvedStore.getState().replacePrefab(prefab),
158
- addModel: (path, model) => setInjectedModels(prev => (Object.assign(Object.assign({}, prev), { [path]: model }))),
159
- addTexture: (path, texture) => setInjectedTextures(prev => (Object.assign(Object.assign({}, prev), { [path]: texture }))),
160
- addSound: (path, sound) => {
161
- soundManager.setBuffer(path, sound);
162
- setInjectedSounds(prev => (Object.assign(Object.assign({}, prev), { [path]: sound })));
184
+ addModel: (path, model) => {
185
+ var _a;
186
+ const version = ((_a = injectedModelVersions.current[path]) !== null && _a !== void 0 ? _a : 0) + 1;
187
+ injectedModelVersions.current[path] = version;
188
+ void precompileModel(model, renderer, camera).then(() => {
189
+ if (injectedModelVersions.current[path] !== version)
190
+ return;
191
+ registerModel(path, model);
192
+ });
163
193
  },
164
- }), [editMode, getHandle, getModel, getNode, getObject, resolvedStore, rootId]);
194
+ addTexture: registerTexture,
195
+ addSound: registerSound,
196
+ }), [basePath, camera, editMode, getHandle, getModel, getNode, getObject, registerModel, registerSound, registerTexture, renderer, resolvedStore, rootId]);
165
197
  useImperativeHandle(ref, () => sceneValue, [sceneValue]);
166
- const registerRef = useCallback((id, obj) => {
167
- objectRefs.current[id] = obj;
168
- }, []);
169
198
  useEffect(() => {
170
199
  if (usesOwnedStore && data) {
171
200
  resolvedStore.getState().replacePrefab(data);
172
201
  }
173
202
  }, [data, resolvedStore, usesOwnedStore]);
174
203
  useEffect(() => {
175
- const loadAsset = (file, loaded, injected, failed, loader) => {
176
- if (loaded[file] || injected[file] || loading.current.has(file) || failed.has(file))
204
+ const loadAsset = (file, loaded, failed, loader) => {
205
+ if (loaded[file] || loading.current.has(file) || failed.has(file))
177
206
  return;
178
207
  loading.current.add(file);
179
- void loader(resolveAssetPath(basePath, file)).then(result => {
208
+ void loader(withBasePath(basePath, file)).then(result => {
180
209
  loading.current.delete(file);
181
210
  if (!result.success) {
182
211
  console.warn(`Failed to load asset: ${file}`, result.error);
@@ -189,44 +218,63 @@ export const PrefabRoot = forwardRef(({ editMode, data, store, selectedId, onSel
189
218
  const type = entry.slice(0, separator);
190
219
  const file = entry.slice(separator + 1);
191
220
  if (type === 'model') {
192
- loadAsset(file, models, injectedModels, failedModels.current, path => loadModel(path).then(result => {
221
+ const inheritedModel = models[file] ? null : parentAssetRuntime === null || parentAssetRuntime === void 0 ? void 0 : parentAssetRuntime.getModel(file);
222
+ if (inheritedModel) {
223
+ registerModel(file, inheritedModel);
224
+ return;
225
+ }
226
+ loadAsset(file, models, failedModels.current, path => loadModel(path).then((result) => __awaiter(void 0, void 0, void 0, function* () {
193
227
  const loadedModel = result.model;
194
228
  if (result.success && loadedModel) {
195
- setModels(currentModels => (Object.assign(Object.assign({}, currentModels), { [file]: loadedModel })));
229
+ yield precompileModel(loadedModel, renderer, camera);
230
+ registerModel(file, loadedModel);
196
231
  }
197
232
  return result;
198
- }));
233
+ })));
199
234
  }
200
235
  else if (type === 'texture') {
201
- loadAsset(file, textures, injectedTextures, failedTextures.current, path => loadTexture(path).then(result => {
236
+ const inheritedTexture = textures[file] ? null : parentAssetRuntime === null || parentAssetRuntime === void 0 ? void 0 : parentAssetRuntime.getTexture(file);
237
+ if (inheritedTexture) {
238
+ registerTexture(file, inheritedTexture);
239
+ return;
240
+ }
241
+ loadAsset(file, textures, failedTextures.current, path => loadTexture(path).then(result => {
202
242
  const loadedTexture = result.texture;
203
243
  if (result.success && loadedTexture) {
204
- setTextures(currentTextures => (Object.assign(Object.assign({}, currentTextures), { [file]: loadedTexture })));
244
+ registerTexture(file, loadedTexture);
205
245
  }
206
246
  return result;
207
247
  }));
208
248
  }
209
249
  else if (type === 'sound') {
210
- loadAsset(file, sounds, injectedSounds, failedSounds.current, path => loadSound(path).then(result => {
250
+ const inheritedSound = sounds[file] ? null : parentAssetRuntime === null || parentAssetRuntime === void 0 ? void 0 : parentAssetRuntime.getSound(file);
251
+ if (inheritedSound) {
252
+ registerSound(file, inheritedSound);
253
+ return;
254
+ }
255
+ loadAsset(file, sounds, failedSounds.current, path => loadSound(path).then(result => {
211
256
  const loadedSound = result.sound;
212
257
  if (result.success && loadedSound) {
213
- soundManager.setBuffer(file, loadedSound);
214
- setSounds(currentSounds => (Object.assign(Object.assign({}, currentSounds), { [file]: loadedSound })));
258
+ registerSound(file, loadedSound);
215
259
  }
216
260
  return result;
217
261
  }));
218
262
  }
219
263
  });
220
- }, [assetRefCounts, basePath, injectedModels, injectedSounds, injectedTextures, models, sounds, textures]);
264
+ }, [assetRefCounts, basePath, camera, models, parentAssetRuntime, registerModel, registerSound, registerTexture, renderer, sounds, textures]);
221
265
  const assetRuntime = useMemo(() => ({
266
+ registerObject,
222
267
  registerHandle,
268
+ registerModel,
269
+ registerTexture,
270
+ registerSound,
223
271
  getHandle,
224
272
  getObject,
225
273
  getModel,
226
274
  getTexture,
227
275
  getSound,
228
276
  getAssetRevision: () => assetRevision,
229
- }), [registerHandle, getHandle, getObject, getModel, getTexture, getSound, assetRevision]);
277
+ }), [registerObject, registerHandle, registerModel, registerTexture, registerSound, getHandle, getObject, getModel, getTexture, getSound, assetRevision]);
230
278
  const handleNodeClick = useCallback((event, nodeId, fallbackObject) => {
231
279
  const node = resolvedStore.getState().nodesById[nodeId];
232
280
  if (!node)
@@ -235,9 +283,10 @@ export const PrefabRoot = forwardRef(({ editMode, data, store, selectedId, onSel
235
283
  emitNodePointerEvent(clickEventName, event, nodeId, node, fallbackObject);
236
284
  onClick === null || onClick === void 0 ? void 0 : onClick(event, node);
237
285
  }, [onClick, resolvedStore]);
238
- 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 }), children] }));
239
- const runtimeContent = (_jsx(SceneContext.Provider, { value: sceneValue, children: _jsx(AssetRuntimeContext.Provider, { value: assetRuntime, children: content }) }));
240
- return _jsx(PrefabStoreProvider, { store: resolvedStore, children: runtimeContent });
286
+ const content = (_jsxs(GameInstanceProvider, { models: models, selectedId: selectedId, editMode: editMode, onSelect: editMode ? onSelect : undefined, onClick: editMode ? undefined : handleNodeClick, registerRef: registerObject, children: [_jsx(StoreRootNode, { selectedId: selectedId, onSelect: editMode ? onSelect : undefined, onClick: editMode ? undefined : handleNodeClick, onEditNodeClick: editMode ? onEditNodeClick : undefined, registerRef: registerObject, loadedModels: models, editMode: editMode, parentMatrix: IDENTITY, basePath: basePath }), children] }));
287
+ const runtimeContent = parentAssetRuntime ? content : (_jsx(AssetRuntimeContext.Provider, { value: assetRuntime, children: content }));
288
+ const sceneContent = parentScene ? runtimeContent : (_jsx(SceneContext.Provider, { value: sceneValue, children: runtimeContent }));
289
+ return _jsx(PrefabStoreProvider, { store: resolvedStore, children: sceneContent });
241
290
  });
242
291
  function StoreRootNode(props) {
243
292
  const rootId = usePrefabRootId();
@@ -303,10 +352,7 @@ function analyzeNodeComponents(node) {
303
352
  }
304
353
  function emitNodePointerEvent(eventName, event, nodeId, node, fallbackObject) {
305
354
  var _a;
306
- const trimmedEventName = eventName === null || eventName === void 0 ? void 0 : eventName.trim();
307
- if (!trimmedEventName)
308
- return;
309
- gameEvents.emit(trimmedEventName, {
355
+ const payload = {
310
356
  sourceEntityId: nodeId,
311
357
  sourceNodeId: nodeId,
312
358
  nodeId,
@@ -319,7 +365,12 @@ function emitNodePointerEvent(eventName, event, nodeId, node, fallbackObject) {
319
365
  metaKey: event.nativeEvent.metaKey,
320
366
  shiftKey: event.nativeEvent.shiftKey,
321
367
  r3fEvent: event,
322
- });
368
+ };
369
+ gameEvents.emit('click', payload);
370
+ const trimmedEventName = eventName === null || eventName === void 0 ? void 0 : eventName.trim();
371
+ if (!trimmedEventName)
372
+ return;
373
+ gameEvents.emit(trimmedEventName, payload);
323
374
  }
324
375
  export function GameObjectRenderer(props) {
325
376
  var _a, _b;
@@ -374,7 +425,7 @@ function InstancedNode({ nodeId, parentMatrix = IDENTITY, editMode, registerRef,
374
425
  }
375
426
  return _jsx(_Fragment, { children: renderedInstances });
376
427
  }
377
- function StandardNode({ nodeId, selectedId, onSelect, onClick, onEditNodeClick, registerRef, loadedModels, editMode, parentMatrix = IDENTITY, isVisible = true, }) {
428
+ function StandardNode({ nodeId, selectedId, onSelect, onClick, onEditNodeClick, registerRef, loadedModels, editMode, parentMatrix = IDENTITY, isVisible = true, basePath = "", }) {
378
429
  const gameObject = usePrefabNode(nodeId);
379
430
  const childIds = usePrefabChildIds(nodeId);
380
431
  const analyzedComponents = useMemo(() => gameObject ? analyzeNodeComponents(gameObject) : EMPTY_NODE_COMPONENTS, [gameObject]);
@@ -410,9 +461,13 @@ function StandardNode({ nodeId, selectedId, onSelect, onClick, onEditNodeClick,
410
461
  rotation: transform.rotation,
411
462
  scale: transform.scale,
412
463
  };
464
+ const worldTransform = decompose(world);
413
465
  const groupProps = Object.assign(Object.assign({}, metadataProps), transformProps);
414
- const childNodes = _jsx(ChildNodes, { childIds: childIds, parentMatrix: world, selectedId: selectedId, onSelect: onSelect, onClick: onClick, onEditNodeClick: onEditNodeClick, registerRef: registerRef, loadedModels: loadedModels, editMode: editMode, isVisible: nodeVisible });
415
- const inner = renderNodeContent(analyzedComponents, loadedModels, primaryClickHandlers, childNodes);
466
+ const childNodes = _jsx(ChildNodes, { childIds: childIds, parentMatrix: world, selectedId: selectedId, onSelect: onSelect, onClick: onClick, onEditNodeClick: onEditNodeClick, registerRef: registerRef, loadedModels: loadedModels, editMode: editMode, isVisible: nodeVisible, basePath: basePath });
467
+ const nodeInteractionHandlers = editMode ? editClickHandlers : primaryClickHandlers;
468
+ const componentRuntimeProps = Object.assign(Object.assign({ editMode,
469
+ nodeInteractionHandlers }, transformProps), { worldPosition: worldTransform.position });
470
+ const inner = renderNodeContent(analyzedComponents, loadedModels, primaryClickHandlers, childNodes, basePath, componentRuntimeProps);
416
471
  const editAnchor = editMode ? (_jsx("mesh", { visible: false, children: _jsx("boxGeometry", { args: [0.01, 0.01, 0.01] }) })) : null;
417
472
  const standardNode = (_jsxs("group", Object.assign({ ref: handleGroupRef }, groupProps, { visible: nodeVisible }, (editMode ? editClickHandlers : undefined), { children: [editAnchor, inner] })));
418
473
  return (_jsx(NodeScope, { nodeId: nodeId, editMode: editMode, isSelected: isSelected, children: standardNode }));
@@ -485,7 +540,7 @@ function getNodeTransformProps(node) {
485
540
  scale: (_d = t === null || t === void 0 ? void 0 : t.scale) !== null && _d !== void 0 ? _d : [1, 1, 1],
486
541
  };
487
542
  }
488
- function renderNodeContent(analyzedComponents, loadedModels, primaryClickHandlers, childNodes) {
543
+ function renderNodeContent(analyzedComponents, loadedModels, primaryClickHandlers, childNodes, basePath = "", componentRuntimeProps) {
489
544
  var _a, _b, _c, _d, _e, _f;
490
545
  const geometry = analyzedComponents.geometry;
491
546
  const models = analyzedComponents.models;
@@ -550,7 +605,7 @@ function renderNodeContent(analyzedComponents, loadedModels, primaryClickHandler
550
605
  }
551
606
  let content = _jsxs(_Fragment, { children: [primaryContent, contentChildren] });
552
607
  for (const { key, View, properties } of analyzedComponents.composition) {
553
- content = (_jsx(View, { properties: properties, children: content }, key));
608
+ content = (_jsx(View, Object.assign({ properties: properties, basePath: basePath }, componentRuntimeProps, { children: content }), key));
554
609
  }
555
610
  return content;
556
611
  }
@@ -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
+ }
@@ -1,7 +1,11 @@
1
1
  import { type ReactNode } from "react";
2
2
  import type { Object3D, Texture } from "three";
3
3
  export interface AssetRuntime {
4
+ registerObject: (id: string, object: Object3D | null) => void;
4
5
  registerHandle: (id: string, kind: string, handle: unknown) => void;
6
+ registerModel: (path: string, model: Object3D) => void;
7
+ registerTexture: (path: string, texture: Texture) => void;
8
+ registerSound: (path: string, sound: AudioBuffer) => void;
5
9
  getHandle: <T = unknown>(id: string, kind: string) => T | null;
6
10
  getModel: (path: string) => Object3D | null;
7
11
  getTexture: (path: string) => Texture | null;
@@ -1,4 +1,5 @@
1
1
  import type { FC } from "react";
2
+ import type { ThreeEvent } from "@react-three/fiber";
2
3
  import type { ComponentData, GameObject } from "../types";
3
4
  export type AssetRef = {
4
5
  type: "model" | "texture" | "sound";
@@ -12,18 +13,32 @@ export interface ComponentViewProps<P = Record<string, unknown>> {
12
13
  properties: P;
13
14
  /** Children to render for components that wrap the current subtree. */
14
15
  children?: React.ReactNode;
16
+ /** Whether this node is currently rendered in editor mode. */
17
+ editMode?: boolean;
18
+ /** Node-level pointer/click handlers for custom components that render their own pickable objects. */
19
+ nodeInteractionHandlers?: NodeInteractionHandlers;
15
20
  /** Current node local position for wrapper components. */
16
21
  position?: [number, number, number];
17
22
  /** Current node local rotation in radians for wrapper components. */
18
23
  rotation?: [number, number, number];
19
24
  /** Current node local scale for wrapper components. */
20
25
  scale?: [number, number, number];
26
+ /** Current node world position. Components that create world-space resources should prefer this. */
27
+ worldPosition?: [number, number, number];
28
+ /** Public asset URL prefix, such as a Next.js basePath. */
29
+ basePath?: string;
21
30
  }
31
+ export type NodeInteractionHandlers = {
32
+ onClick?: (event: ThreeEvent<PointerEvent>) => void;
33
+ onPointerDown?: (event: ThreeEvent<PointerEvent>) => void;
34
+ onPointerMove?: (event: ThreeEvent<PointerEvent>) => void;
35
+ onPointerUp?: (event: ThreeEvent<PointerEvent>) => void;
36
+ };
22
37
  export interface Component {
23
38
  name: string;
24
39
  /** Set when this component occupies a single slot on a node. Use a string to share a slot across component types. */
25
40
  disableSiblingComposition?: boolean | string;
26
- Editor: FC<{
41
+ Editor?: FC<{
27
42
  node?: GameObject;
28
43
  component: ComponentData;
29
44
  onUpdate: (newComp: Record<string, unknown>) => void;
@@ -5,7 +5,7 @@ import { Mesh } from 'three';
5
5
  import { assetRef, assetRefs } from './ComponentRegistry';
6
6
  import { BooleanField, FieldGroup, Label, ListEditor, NumberInput, SelectInput, StringField } from './Input';
7
7
  import { useAssetRuntime } from '../assetRuntime';
8
- import { useEditorContext, useEditorRef } from '../PrefabEditor';
8
+ import { useEditorContext, useEditorRef } from '../EditorContext';
9
9
  import { getRepeatAxesFromModelProperties, normalizeRepeatAxes } from '../InstanceProvider';
10
10
  import { base, colors, ui } from '../styles';
11
11
  import { decomposeModelToPrefabNodes } from '../modelPrefab';
@@ -0,0 +1,3 @@
1
+ import type { Component } from './ComponentRegistry';
2
+ declare const PrefabRefComponent: Component;
3
+ export default PrefabRefComponent;
@@ -0,0 +1,72 @@
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 { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
11
+ import { lazy, Suspense, useEffect, useState } from 'react';
12
+ import { useEditorRef } from '../EditorContext';
13
+ import { withBasePath } from '../utils';
14
+ import { base, colors } from '../styles';
15
+ import { FieldGroup, Label } from './Input';
16
+ const PrefabRoot = lazy(() => import('../PrefabRoot'));
17
+ function PrefabRefView({ properties, children, basePath = '' }) {
18
+ var _a;
19
+ const [loadedPrefab, setLoadedPrefab] = useState(null);
20
+ const url = (_a = properties.url) !== null && _a !== void 0 ? _a : '';
21
+ const resolvedUrl = url ? withBasePath(basePath, url) : '';
22
+ useEffect(() => {
23
+ if (!resolvedUrl)
24
+ return;
25
+ let cancelled = false;
26
+ fetch(resolvedUrl)
27
+ .then(r => r.json())
28
+ .then(data => { if (!cancelled)
29
+ setLoadedPrefab(data); })
30
+ .catch(err => console.warn('[PrefabRef] Failed to load:', resolvedUrl, err));
31
+ return () => { cancelled = true; };
32
+ }, [resolvedUrl]);
33
+ return (_jsxs(_Fragment, { children: [loadedPrefab && (_jsx(Suspense, { fallback: null, children: _jsx(PrefabRoot, { data: loadedPrefab, editMode: false, basePath: basePath }) })), children] }));
34
+ }
35
+ function PrefabRefEditor({ node, component, onUpdate, basePath = '', }) {
36
+ var _a, _b;
37
+ const url = (_b = (_a = component.properties) === null || _a === void 0 ? void 0 : _a.url) !== null && _b !== void 0 ? _b : '';
38
+ const [manifest, setManifest] = useState([]);
39
+ const [unpacking, setUnpacking] = useState(false);
40
+ const editor = useEditorRef();
41
+ useEffect(() => {
42
+ fetch(withBasePath(basePath, '/prefabs/manifest.json'))
43
+ .then(r => r.json())
44
+ .then(data => setManifest(data))
45
+ .catch(() => setManifest([]));
46
+ }, []);
47
+ const handleUnpack = () => __awaiter(this, void 0, void 0, function* () {
48
+ if (!node || !url)
49
+ return;
50
+ setUnpacking(true);
51
+ try {
52
+ const prefab = yield fetch(withBasePath(basePath, url)).then(r => r.json());
53
+ editor.replaceNode(node.id, prefab.root);
54
+ }
55
+ catch (err) {
56
+ console.error('[PrefabRef] Unpack failed:', err);
57
+ }
58
+ finally {
59
+ setUnpacking(false);
60
+ }
61
+ });
62
+ return (_jsxs(FieldGroup, { children: [_jsxs("div", { children: [_jsx(Label, { children: "Prefab URL" }), _jsx("input", { type: "text", style: Object.assign(Object.assign({}, base.input), { width: '100%', boxSizing: 'border-box', fontFamily: 'monospace' }), value: url, onChange: e => onUpdate({ url: e.target.value }), placeholder: "/prefabs/my-prefab.json" }), manifest.length > 0 && (_jsxs("select", { style: Object.assign(Object.assign({}, base.input), { width: '100%', marginTop: 4, background: colors.bgInput, boxSizing: 'border-box' }), value: url, onChange: e => onUpdate({ url: e.target.value }), children: [_jsx("option", { value: "", children: "\u2014 pick from manifest \u2014" }), manifest.map(entry => (_jsx("option", { value: entry, children: entry.replace(/^.*\//, '') }, entry)))] }))] }), _jsx("button", { type: "button", style: Object.assign(Object.assign({}, base.btn), { width: '100%', opacity: unpacking || !url ? 0.5 : 1 }), disabled: unpacking || !url, onClick: handleUnpack, children: unpacking ? 'Unpacking…' : 'Unpack' })] }));
63
+ }
64
+ const PrefabRefComponent = {
65
+ name: 'PrefabRef',
66
+ Editor: PrefabRefEditor,
67
+ View: PrefabRefView,
68
+ defaultProperties: {
69
+ url: '',
70
+ },
71
+ };
72
+ export default PrefabRefComponent;
@@ -1,9 +1,8 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { ColorField, FieldGroup, NumberField, SelectField, StringField } from "./Input";
3
3
  import { Text } from 'three-text/three/react';
4
- import { useRef, useState, useCallback } from 'react';
5
- // Initialize HarfBuzz path for font shaping
6
- Text.setHarfBuzzPath('/fonts/hb.wasm');
4
+ import { useRef, useState, useCallback, useEffect } from 'react';
5
+ import { withBasePath } from "../utils";
7
6
  function TextComponentEditor({ component, onUpdate, }) {
8
7
  return (_jsxs(FieldGroup, { children: [_jsx(StringField, { name: "text", label: "Text", values: component.properties, onChange: onUpdate, placeholder: "Enter text..." }), _jsx(ColorField, { name: "color", label: "Color", values: component.properties, onChange: onUpdate }), _jsx(StringField, { name: "font", label: "Font", values: component.properties, onChange: onUpdate, placeholder: "/fonts/NotoSans-Regular.ttf" }), _jsx(NumberField, { name: "size", label: "Size", values: component.properties, onChange: onUpdate, min: 0.01, step: 0.1 }), _jsx(NumberField, { name: "depth", label: "Depth", values: component.properties, onChange: onUpdate, min: 0, step: 0.1 }), _jsx(NumberField, { name: "width", label: "Width", values: component.properties, onChange: onUpdate, min: 0, step: 0.5 }), _jsx(SelectField, { name: "align", label: "Align", values: component.properties, onChange: onUpdate, options: [
9
8
  { value: 'left', label: 'Left' },
@@ -11,11 +10,15 @@ function TextComponentEditor({ component, onUpdate, }) {
11
10
  { value: 'right', label: 'Right' },
12
11
  ] })] }));
13
12
  }
14
- function TextComponentView({ properties, children }) {
13
+ function TextComponentView({ properties, children, basePath = "" }) {
15
14
  const { text = '', font, size, depth, width, align, color } = properties;
16
15
  const textContent = String(text || '');
16
+ const resolvedFont = font ? withBasePath(basePath, font) : font;
17
17
  const meshRef = useRef(null);
18
18
  const [offset, setOffset] = useState([0, 0, 0]);
19
+ useEffect(() => {
20
+ Text.setHarfBuzzPath(withBasePath(basePath, '/fonts/hb.wasm'));
21
+ }, [basePath]);
19
22
  const handleLoad = useCallback((_geometry, info) => {
20
23
  if (info === null || info === void 0 ? void 0 : info.planeBounds) {
21
24
  const bounds = info.planeBounds;
@@ -37,7 +40,7 @@ function TextComponentView({ properties, children }) {
37
40
  }, [align]);
38
41
  if (!textContent)
39
42
  return null;
40
- return (_jsxs("group", { position: offset, children: [_jsx(Text, { ref: meshRef, font: font, size: size, depth: depth, layout: { align, width }, color: color, onLoad: handleLoad, children: textContent }), children] }));
43
+ return (_jsxs("group", { position: offset, children: [_jsx(Text, { ref: meshRef, font: resolvedFont, size: size, depth: depth, layout: { align, width }, color: color, onLoad: handleLoad, children: textContent }), children] }));
41
44
  }
42
45
  const TextComponent = {
43
46
  name: 'Text',
@@ -1,6 +1,6 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { Label, Vector3Input } from "./Input";
3
- import { useEditorContext } from "../PrefabEditor";
3
+ import { useEditorContext } from "../EditorContext";
4
4
  import { colors } from "../styles";
5
5
  const buttonStyle = {
6
6
  padding: '2px 6px',
@@ -1 +1,2 @@
1
1
  export declare const builtinComponents: import("./ComponentRegistry").Component[];
2
+ export declare function registerBuiltinComponents(): void;