react-three-game 0.0.68 → 0.0.70

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 (52) hide show
  1. package/dist/helpers/SoundManager.d.ts +2 -0
  2. package/dist/helpers/SoundManager.js +6 -0
  3. package/dist/index.d.ts +10 -7
  4. package/dist/index.js +8 -4
  5. package/dist/shared/GameCanvas.js +0 -2
  6. package/dist/tools/assetviewer/page.d.ts +5 -0
  7. package/dist/tools/assetviewer/page.js +3 -0
  8. package/dist/tools/dragdrop/DragDropLoader.d.ts +3 -2
  9. package/dist/tools/dragdrop/DragDropLoader.js +18 -3
  10. package/dist/tools/dragdrop/index.d.ts +2 -2
  11. package/dist/tools/dragdrop/index.js +1 -1
  12. package/dist/tools/dragdrop/modelLoader.d.ts +10 -0
  13. package/dist/tools/dragdrop/modelLoader.js +60 -0
  14. package/dist/tools/prefabeditor/EditorTree.js +6 -30
  15. package/dist/tools/prefabeditor/EditorTreeMenus.js +3 -3
  16. package/dist/tools/prefabeditor/EditorUI.js +6 -4
  17. package/dist/tools/prefabeditor/InstanceProvider.d.ts +2 -0
  18. package/dist/tools/prefabeditor/InstanceProvider.js +54 -52
  19. package/dist/tools/prefabeditor/PrefabEditor.d.ts +22 -0
  20. package/dist/tools/prefabeditor/PrefabEditor.js +68 -27
  21. package/dist/tools/prefabeditor/PrefabRoot.d.ts +5 -1
  22. package/dist/tools/prefabeditor/PrefabRoot.js +148 -145
  23. package/dist/tools/prefabeditor/components/ClickComponent.js +10 -7
  24. package/dist/tools/prefabeditor/components/ComponentRegistry.d.ts +10 -4
  25. package/dist/tools/prefabeditor/components/ComponentRegistry.js +6 -6
  26. package/dist/tools/prefabeditor/components/GeometryComponent.js +1 -1
  27. package/dist/tools/prefabeditor/components/Input.d.ts +16 -0
  28. package/dist/tools/prefabeditor/components/Input.js +33 -0
  29. package/dist/tools/prefabeditor/components/MaterialComponent.js +10 -2
  30. package/dist/tools/prefabeditor/components/ModelComponent.js +35 -43
  31. package/dist/tools/prefabeditor/components/PhysicsComponent.d.ts +10 -1
  32. package/dist/tools/prefabeditor/components/PhysicsComponent.js +122 -28
  33. package/dist/tools/prefabeditor/components/SoundComponent.d.ts +3 -0
  34. package/dist/tools/prefabeditor/components/SoundComponent.js +240 -0
  35. package/dist/tools/prefabeditor/components/SpotLightComponent.js +6 -1
  36. package/dist/tools/prefabeditor/components/TransformComponent.js +2 -2
  37. package/dist/tools/prefabeditor/components/index.js +2 -0
  38. package/dist/tools/prefabeditor/prefabStore.d.ts +1 -0
  39. package/dist/tools/prefabeditor/prefabStore.js +11 -13
  40. package/dist/tools/prefabeditor/sceneApi.d.ts +15 -1
  41. package/dist/tools/prefabeditor/sceneApi.js +77 -32
  42. package/dist/tools/prefabeditor/styles.d.ts +1 -0
  43. package/dist/tools/prefabeditor/styles.js +9 -0
  44. package/dist/tools/prefabeditor/types.d.ts +13 -0
  45. package/dist/tools/prefabeditor/types.js +28 -1
  46. package/dist/tools/prefabeditor/useClickValid.d.ts +13 -0
  47. package/dist/tools/prefabeditor/useClickValid.js +21 -0
  48. package/dist/tools/prefabeditor/utils.d.ts +2 -0
  49. package/dist/tools/prefabeditor/utils.js +34 -35
  50. package/package.json +1 -1
  51. package/dist/tools/prefabeditor/EditorContext.d.ts +0 -16
  52. package/dist/tools/prefabeditor/EditorContext.js +0 -9
@@ -1,12 +1,3 @@
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
1
  var __rest = (this && this.__rest) || function (s, e) {
11
2
  var t = {};
12
3
  for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
@@ -21,35 +12,63 @@ var __rest = (this && this.__rest) || function (s, e) {
21
12
  import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
22
13
  import { useHelper } from "@react-three/drei";
23
14
  import { forwardRef, useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState } from "react";
24
- import { BoxHelper, Euler, Matrix4, Quaternion, Vector3, } from "three";
15
+ import { BoxHelper, Euler, Matrix4, } from "three";
25
16
  import { useStore } from "zustand";
26
- import { getComponent, registerComponent } from "./components/ComponentRegistry";
17
+ import { useClickValid } from "./useClickValid";
18
+ import { findComponent } from "./types";
19
+ import { getComponentDef, getComponentAssetRefs, registerComponent } from "./components/ComponentRegistry";
27
20
  import components from "./components";
28
- import { loadModel, loadTexture } from "../dragdrop";
21
+ import { loadModel, loadSound, loadTexture } from "../dragdrop";
29
22
  import { GameInstance, GameInstanceProvider, getRepeatAxesFromModelProperties, useInstanceCheck } from "./InstanceProvider";
30
- import { decompose } from "./utils";
31
- import { createPrefabStore, PrefabStoreProvider, prefabStoreToPrefab, usePrefabChildIds, usePrefabNode, usePrefabRootId } from "./prefabStore";
23
+ import { composeTransform, decompose } from "./utils";
24
+ import { isPhysicsProps } from "./components/PhysicsComponent";
25
+ import { createPrefabStore, PrefabStoreProvider, prefabStoreToPrefab, useOptionalPrefabStoreApi, usePrefabChildIds, usePrefabNode, usePrefabRootId } from "./prefabStore";
26
+ import { sound as soundManager } from "../../helpers/SoundManager";
32
27
  components.forEach(registerComponent);
33
28
  const IDENTITY = new Matrix4();
34
29
  const EMPTY_MODELS = {};
35
30
  const EMPTY_TEXTURES = {};
36
- export const PrefabRoot = forwardRef(({ editMode, data, store, selectedId, onSelect, onClick, onSelectedObjectChange, onFocusNode, basePath = "", injectedModels = EMPTY_MODELS, injectedTextures = EMPTY_TEXTURES }, ref) => {
31
+ const EMPTY_SOUNDS = {};
32
+ /** Resolve a relative or absolute asset file path against a base path. */
33
+ function resolveAssetPath(basePath, file) {
34
+ if (file.startsWith("http://") || file.startsWith("https://"))
35
+ return file;
36
+ return file.startsWith("/") ? `${basePath}${file}` : `${basePath}/${file}`;
37
+ }
38
+ /** Check if all model assets required by a node are loaded. */
39
+ function isNodeReady(node, loadedModels) {
40
+ var _a;
41
+ const model = findComponent(node, "Model");
42
+ if (!((_a = model === null || model === void 0 ? void 0 : model.properties) === null || _a === void 0 ? void 0 : _a.filename))
43
+ return true;
44
+ return Boolean(loadedModels[model.properties.filename]);
45
+ }
46
+ export const PrefabRoot = forwardRef(({ editMode, data, store, selectedId, onSelect, onClick, onSelectedObjectChange, onFocusNode, basePath = "", injectedModels = EMPTY_MODELS, injectedTextures = EMPTY_TEXTURES, injectedSounds = EMPTY_SOUNDS }, ref) => {
47
+ var _a;
37
48
  const [models, setModels] = useState({});
38
49
  const [textures, setTextures] = useState({});
50
+ const [sounds, setSounds] = useState({});
39
51
  const loading = useRef(new Set());
52
+ const failedModels = useRef(new Set());
40
53
  const failedTextures = useRef(new Set());
54
+ const failedSounds = useRef(new Set());
41
55
  const objectRefs = useRef({});
42
56
  const rigidBodyRefs = useRef(new Map());
43
57
  const rootRef = useRef(null);
44
- const [internalStore] = useState(() => { var _a; return createPrefabStore(data !== null && data !== void 0 ? data : prefabStoreToPrefab((_a = store === null || store === void 0 ? void 0 : store.getState()) !== null && _a !== void 0 ? _a : missingStoreState())); });
45
- const prefabStore = store !== null && store !== void 0 ? store : internalStore;
46
- const assetManifestKey = useStore(prefabStore, state => state.assetManifestKey);
58
+ const parentStore = useOptionalPrefabStoreApi();
59
+ const [ownedStore] = useState(() => { var _a, _b; return createPrefabStore(data !== null && data !== void 0 ? data : prefabStoreToPrefab((_b = (_a = store === null || store === void 0 ? void 0 : store.getState()) !== null && _a !== void 0 ? _a : parentStore === null || parentStore === void 0 ? void 0 : parentStore.getState()) !== null && _b !== void 0 ? _b : missingStoreState())); });
60
+ const resolvedStore = (_a = store !== null && store !== void 0 ? store : parentStore) !== null && _a !== void 0 ? _a : ownedStore;
61
+ const usesOwnedStore = resolvedStore === ownedStore;
62
+ const shouldProvideStoreContext = !parentStore || parentStore !== resolvedStore;
63
+ const assetManifestKey = useStore(resolvedStore, state => state.assetManifestKey);
47
64
  const availableModels = useMemo(() => (Object.assign(Object.assign({}, models), injectedModels)), [models, injectedModels]);
48
65
  const availableTextures = useMemo(() => (Object.assign(Object.assign({}, textures), injectedTextures)), [textures, injectedTextures]);
66
+ const availableSounds = useMemo(() => (Object.assign(Object.assign({}, sounds), injectedSounds)), [sounds, injectedSounds]);
49
67
  useImperativeHandle(ref, () => ({
50
68
  root: rootRef.current,
51
69
  rigidBodyRefs: rigidBodyRefs.current,
52
70
  getObject: (nodeId) => { var _a; return (_a = objectRefs.current[nodeId]) !== null && _a !== void 0 ? _a : null; },
71
+ getRigidBody: (nodeId) => { var _a; return (_a = rigidBodyRefs.current.get(nodeId)) !== null && _a !== void 0 ? _a : null; },
53
72
  focusNode: (nodeId) => onFocusNode === null || onFocusNode === void 0 ? void 0 : onFocusNode(nodeId),
54
73
  }), [onFocusNode]);
55
74
  const registerRef = useCallback((id, obj) => {
@@ -61,6 +80,10 @@ export const PrefabRoot = forwardRef(({ editMode, data, store, selectedId, onSel
61
80
  const registerRigidBodyRef = useCallback((id, rb) => {
62
81
  rigidBodyRefs.current.set(id, rb);
63
82
  }, []);
83
+ const getRigidBody = useCallback((id) => {
84
+ var _a;
85
+ return (_a = rigidBodyRefs.current.get(id)) !== null && _a !== void 0 ? _a : null;
86
+ }, []);
64
87
  useEffect(() => {
65
88
  const originalError = console.error;
66
89
  console.error = (...args) => {
@@ -71,78 +94,81 @@ export const PrefabRoot = forwardRef(({ editMode, data, store, selectedId, onSel
71
94
  return () => { console.error = originalError; };
72
95
  }, []);
73
96
  useEffect(() => {
74
- if (!store && data) {
75
- prefabStore.getState().replacePrefab(data);
97
+ if (usesOwnedStore && data) {
98
+ resolvedStore.getState().replacePrefab(data);
76
99
  }
77
- }, [data, prefabStore, store]);
100
+ }, [data, resolvedStore, usesOwnedStore]);
78
101
  useEffect(() => {
79
- const syncAssets = (snapshot = prefabStore.getState()) => {
102
+ const syncAssets = (snapshot = resolvedStore.getState()) => {
80
103
  const modelsToLoad = new Set();
81
104
  const texturesToLoad = new Set();
105
+ const soundsToLoad = new Set();
82
106
  Object.values(snapshot.nodesById).forEach(node => {
83
107
  var _a;
84
108
  Object.values((_a = node.components) !== null && _a !== void 0 ? _a : {}).forEach(component => {
85
- var _a, _b, _c, _d;
109
+ var _a;
86
110
  if (!(component === null || component === void 0 ? void 0 : component.type))
87
111
  return;
88
- if (component.type === 'Model' && ((_a = component.properties) === null || _a === void 0 ? void 0 : _a.filename)) {
89
- modelsToLoad.add(component.properties.filename);
90
- }
91
- if (component.type === 'Material') {
92
- ((_b = component.properties) === null || _b === void 0 ? void 0 : _b.texture) && texturesToLoad.add(component.properties.texture);
93
- ((_c = component.properties) === null || _c === void 0 ? void 0 : _c.normalMapTexture) && texturesToLoad.add(component.properties.normalMapTexture);
94
- }
95
- if (component.type === 'SpotLight' && ((_d = component.properties) === null || _d === void 0 ? void 0 : _d.map)) {
96
- texturesToLoad.add(component.properties.map);
112
+ for (const ref of getComponentAssetRefs(component.type, (_a = component.properties) !== null && _a !== void 0 ? _a : {})) {
113
+ if (ref.type === 'model')
114
+ modelsToLoad.add(ref.path);
115
+ else if (ref.type === 'texture')
116
+ texturesToLoad.add(ref.path);
117
+ else if (ref.type === 'sound')
118
+ soundsToLoad.add(ref.path);
97
119
  }
98
120
  });
99
121
  });
100
- modelsToLoad.forEach((file) => __awaiter(void 0, void 0, void 0, function* () {
101
- if (models[file] || injectedModels[file] || loading.current.has(file))
122
+ const loadAsset = (file, loaded, injected, failed, loader) => {
123
+ if (loaded[file] || injected[file] || loading.current.has(file) || failed.has(file))
102
124
  return;
103
125
  loading.current.add(file);
104
- const path = file.startsWith("/")
105
- ? `${basePath}${file}`
106
- : `${basePath}/${file}`;
107
- const res = yield loadModel(path);
108
- const model = res.model;
109
- if (res.success && model) {
126
+ void loader(resolveAssetPath(basePath, file)).then(result => {
127
+ if (!result.success) {
128
+ console.warn(`Failed to load asset: ${file}`, result.error);
129
+ loading.current.delete(file);
130
+ failed.add(file);
131
+ }
132
+ });
133
+ };
134
+ modelsToLoad.forEach(file => loadAsset(file, models, injectedModels, failedModels.current, (path) => loadModel(path).then(result => {
135
+ const model = result.model;
136
+ if (result.success && model) {
110
137
  setModels(m => (Object.assign(Object.assign({}, m), { [file]: model })));
111
138
  }
112
- }));
113
- texturesToLoad.forEach(file => {
114
- if (textures[file] || injectedTextures[file] || loading.current.has(file) || failedTextures.current.has(file))
115
- return;
116
- loading.current.add(file);
117
- // Handle full URLs (http/https) or regular paths
118
- const path = file.startsWith("http://") || file.startsWith("https://")
119
- ? file
120
- : file.startsWith("/")
121
- ? `${basePath}${file}`
122
- : `${basePath}/${file}`;
123
- void loadTexture(path).then(result => {
124
- if (result.success && result.texture) {
125
- setTextures(t => (Object.assign(Object.assign({}, t), { [file]: result.texture })));
126
- return;
127
- }
128
- console.warn(`Failed to load texture: ${path}`, result.error);
139
+ return result;
140
+ })));
141
+ texturesToLoad.forEach(file => loadAsset(file, textures, injectedTextures, failedTextures.current, (path) => loadTexture(path).then(result => {
142
+ if (result.success && result.texture) {
143
+ setTextures(t => (Object.assign(Object.assign({}, t), { [file]: result.texture })));
144
+ }
145
+ return result;
146
+ })));
147
+ soundsToLoad.forEach(file => loadAsset(file, sounds, injectedSounds, failedSounds.current, (path) => loadSound(path).then(result => {
148
+ if (result.success && result.sound) {
149
+ soundManager.setBuffer(file, result.sound);
150
+ setSounds(current => (Object.assign(Object.assign({}, current), { [file]: result.sound })));
129
151
  loading.current.delete(file);
130
- failedTextures.current.add(file);
131
- });
132
- });
152
+ }
153
+ return result;
154
+ })));
133
155
  };
134
156
  syncAssets();
135
- }, [prefabStore, assetManifestKey, basePath, injectedModels, injectedTextures]);
136
- return (_jsx(PrefabStoreProvider, { store: prefabStore, children: _jsx("group", { ref: rootRef, children: _jsx(GameInstanceProvider, { models: availableModels, selectedId: selectedId, editMode: editMode, onSelect: editMode ? onSelect : undefined, registerRef: registerRef, children: _jsx(StoreRootNode, { selectedId: selectedId, onSelect: editMode ? onSelect : undefined, onClick: onClick, registerRef: registerRef, registerRigidBodyRef: registerRigidBodyRef, loadedModels: availableModels, loadedTextures: availableTextures, editMode: editMode, parentMatrix: IDENTITY }) }) }) }));
157
+ }, [resolvedStore, assetManifestKey, basePath, injectedModels, injectedSounds, injectedTextures, models, sounds, textures]);
158
+ const content = (_jsx("group", { ref: rootRef, children: _jsx(GameInstanceProvider, { models: availableModels, selectedId: selectedId, editMode: editMode, onSelect: editMode ? onSelect : undefined, registerRef: registerRef, children: _jsx(StoreRootNode, { selectedId: selectedId, onSelect: editMode ? onSelect : undefined, onClick: onClick, registerRef: registerRef, registerRigidBodyRef: registerRigidBodyRef, getRigidBody: getRigidBody, loadedModels: availableModels, loadedSounds: availableSounds, loadedTextures: availableTextures, editMode: editMode, parentMatrix: IDENTITY }) }) }));
159
+ if (!shouldProvideStoreContext) {
160
+ return content;
161
+ }
162
+ return _jsx(PrefabStoreProvider, { store: resolvedStore, children: content });
137
163
  });
138
164
  function StoreRootNode(props) {
139
165
  const rootId = usePrefabRootId();
140
166
  return _jsx(GameObjectRenderer, Object.assign({}, props, { nodeId: rootId }));
141
167
  }
142
168
  export function GameObjectRenderer(props) {
143
- var _a, _b, _c;
169
+ var _a, _b;
144
170
  const node = usePrefabNode(props.nodeId);
145
- const isInstanced = (_c = (_b = (_a = node === null || node === void 0 ? void 0 : node.components) === null || _a === void 0 ? void 0 : _a.model) === null || _b === void 0 ? void 0 : _b.properties) === null || _c === void 0 ? void 0 : _c.instanced;
171
+ const isInstanced = (_b = (_a = findComponent(node, "Model")) === null || _a === void 0 ? void 0 : _a.properties) === null || _b === void 0 ? void 0 : _b.instanced;
146
172
  const prevInstancedRef = useRef(undefined);
147
173
  const [isTransitioning, setIsTransitioning] = useState(false);
148
174
  useEffect(() => {
@@ -160,24 +186,27 @@ export function GameObjectRenderer(props) {
160
186
  ? _jsx(InstancedNode, Object.assign({}, props), key)
161
187
  : _jsx(StandardNode, Object.assign({}, props), key);
162
188
  }
163
- function isPhysicsProps(v) {
164
- return (v === null || v === void 0 ? void 0 : v.type) === "fixed" || (v === null || v === void 0 ? void 0 : v.type) === "dynamic" || (v === null || v === void 0 ? void 0 : v.type) === "kinematicPosition" || (v === null || v === void 0 ? void 0 : v.type) === "kinematicVelocity";
165
- }
166
189
  function InstancedNode({ nodeId, parentMatrix = IDENTITY, editMode, registerRef, onSelect, onClick }) {
167
- var _a, _b, _c, _d, _e, _f, _g, _h;
190
+ var _a, _b, _c;
168
191
  const gameObject = usePrefabNode(nodeId);
169
192
  if (!gameObject)
170
193
  return null;
171
194
  const localTransform = getNodeTransformProps(gameObject);
172
195
  const isLocked = Boolean(gameObject.locked);
173
- const clickable = Object.values((_a = gameObject.components) !== null && _a !== void 0 ? _a : {}).some(component => (component === null || component === void 0 ? void 0 : component.type) === 'Click');
174
- const physicsProps = isPhysicsProps((_c = (_b = gameObject.components) === null || _b === void 0 ? void 0 : _b.physics) === null || _c === void 0 ? void 0 : _c.properties)
175
- ? (_e = (_d = gameObject.components) === null || _d === void 0 ? void 0 : _d.physics) === null || _e === void 0 ? void 0 : _e.properties
196
+ const clickComponent = findComponent(gameObject, "Click");
197
+ const clickable = Boolean(clickComponent);
198
+ const clickEventName = (_a = clickComponent === null || clickComponent === void 0 ? void 0 : clickComponent.properties) === null || _a === void 0 ? void 0 : _a.eventName;
199
+ const physicsData = findComponent(gameObject, "Physics");
200
+ const physicsProps = isPhysicsProps(physicsData === null || physicsData === void 0 ? void 0 : physicsData.properties)
201
+ ? physicsData === null || physicsData === void 0 ? void 0 : physicsData.properties
176
202
  : undefined;
177
- const modelUrl = (_h = (_g = (_f = gameObject.components) === null || _f === void 0 ? void 0 : _f.model) === null || _g === void 0 ? void 0 : _g.properties) === null || _h === void 0 ? void 0 : _h.filename;
203
+ const modelUrl = (_c = (_b = findComponent(gameObject, "Model")) === null || _b === void 0 ? void 0 : _b.properties) === null || _c === void 0 ? void 0 : _c.filename;
178
204
  const instances = useMemo(() => buildRepeatedInstances(gameObject, parentMatrix, modelUrl, physicsProps), [gameObject, modelUrl, parentMatrix, physicsProps]);
179
205
  const groupRef = useRef(null);
180
- const clickValid = useRef(false);
206
+ const editClickHandlers = useClickValid(!!editMode && !isLocked, (e) => {
207
+ onSelect === null || onSelect === void 0 ? void 0 : onSelect(nodeId);
208
+ onClick === null || onClick === void 0 ? void 0 : onClick(e, gameObject);
209
+ });
181
210
  useEffect(() => {
182
211
  if (editMode) {
183
212
  registerRef(nodeId, groupRef.current);
@@ -185,19 +214,12 @@ function InstancedNode({ nodeId, parentMatrix = IDENTITY, editMode, registerRef,
185
214
  }
186
215
  }, [nodeId, registerRef, editMode]);
187
216
  if (editMode) {
188
- return (_jsxs(_Fragment, { children: [_jsx("group", { ref: groupRef, position: localTransform.position, rotation: localTransform.rotation, scale: localTransform.scale, onPointerDown: isLocked ? undefined : (e) => { e.stopPropagation(); clickValid.current = true; }, onPointerMove: isLocked ? undefined : () => { clickValid.current = false; }, onPointerUp: isLocked ? undefined : (e) => {
189
- if (clickValid.current) {
190
- e.stopPropagation();
191
- onSelect === null || onSelect === void 0 ? void 0 : onSelect(nodeId);
192
- onClick === null || onClick === void 0 ? void 0 : onClick(e, gameObject);
193
- }
194
- clickValid.current = false;
195
- }, children: _jsx("mesh", { visible: false, children: _jsx("boxGeometry", { args: [0.01, 0.01, 0.01] }) }) }), instances.map(instance => (_jsx(GameInstance, { id: instance.id, sourceId: gameObject.id, clickable: clickable, modelUrl: instance.modelUrl, position: instance.position, rotation: instance.rotation, scale: instance.scale, locked: isLocked, physics: instance.physics }, instance.id)))] }));
217
+ return (_jsxs(_Fragment, { children: [_jsx("group", Object.assign({ ref: groupRef, position: localTransform.position, rotation: localTransform.rotation, scale: localTransform.scale }, editClickHandlers, { children: _jsx("mesh", { visible: false, children: _jsx("boxGeometry", { args: [0.01, 0.01, 0.01] }) }) })), instances.map(instance => (_jsx(GameInstance, { id: instance.id, sourceId: gameObject.id, clickable: clickable, clickEventName: clickEventName, modelUrl: instance.modelUrl, position: instance.position, rotation: instance.rotation, scale: instance.scale, locked: isLocked, physics: instance.physics }, instance.id)))] }));
196
218
  }
197
- return (_jsx(_Fragment, { children: instances.map(instance => (_jsx(GameInstance, { id: instance.id, sourceId: gameObject.id, clickable: clickable, modelUrl: instance.modelUrl, position: instance.position, rotation: instance.rotation, scale: instance.scale, locked: isLocked, physics: instance.physics }, instance.id))) }));
219
+ return (_jsx(_Fragment, { children: instances.map(instance => (_jsx(GameInstance, { id: instance.id, sourceId: gameObject.id, clickable: clickable, clickEventName: clickEventName, modelUrl: instance.modelUrl, position: instance.position, rotation: instance.rotation, scale: instance.scale, locked: isLocked, physics: instance.physics }, instance.id))) }));
198
220
  }
199
- function StandardNode({ nodeId, selectedId, onSelect, onClick, registerRef, registerRigidBodyRef, loadedModels, loadedTextures, editMode, parentMatrix = IDENTITY, }) {
200
- var _a, _b, _c, _d, _e;
221
+ function StandardNode({ nodeId, selectedId, onSelect, onClick, registerRef, registerRigidBodyRef, getRigidBody, loadedModels, loadedSounds, loadedTextures, editMode, parentMatrix = IDENTITY, }) {
222
+ var _a, _b;
201
223
  const gameObject = usePrefabNode(nodeId);
202
224
  const childIds = usePrefabChildIds(nodeId);
203
225
  const isSelected = selectedId === nodeId;
@@ -205,40 +227,30 @@ function StandardNode({ nodeId, selectedId, onSelect, onClick, registerRef, regi
205
227
  return null;
206
228
  const groupRef = useRef(null);
207
229
  const helperRef = useRef(null);
208
- const clickValid = useRef(false);
209
230
  const isLocked = Boolean(gameObject.locked);
210
231
  const stillInstanced = useInstanceCheck(nodeId);
232
+ const clickHandlers = useClickValid(!!editMode && !isLocked, (e) => {
233
+ onSelect === null || onSelect === void 0 ? void 0 : onSelect(nodeId);
234
+ onClick === null || onClick === void 0 ? void 0 : onClick(e, gameObject);
235
+ });
211
236
  useHelper(editMode && isSelected ? helperRef : null, BoxHelper, "cyan");
212
237
  useEffect(() => {
213
238
  registerRef(nodeId, groupRef.current);
214
239
  return () => registerRef(nodeId, null);
215
240
  }, [nodeId, registerRef]);
216
241
  const world = parentMatrix.clone().multiply(compose(gameObject));
217
- const onDown = (e) => {
218
- e.stopPropagation();
219
- clickValid.current = true;
220
- };
221
- const onUp = (e) => {
222
- if (clickValid.current) {
223
- e.stopPropagation();
224
- onSelect === null || onSelect === void 0 ? void 0 : onSelect(nodeId);
225
- onClick === null || onClick === void 0 ? void 0 : onClick(e, gameObject);
226
- }
227
- clickValid.current = false;
228
- };
229
- const physics = (_a = gameObject.components) === null || _a === void 0 ? void 0 : _a.physics;
230
- const ready = !((_b = gameObject.components) === null || _b === void 0 ? void 0 : _b.model) ||
231
- loadedModels[gameObject.components.model.properties.filename];
242
+ const physics = findComponent(gameObject, "Physics");
243
+ const ready = isNodeReady(gameObject, loadedModels);
232
244
  const hasPhysics = physics && ready && !stillInstanced;
233
245
  const transform = getNodeTransformProps(gameObject);
234
- const physicsDef = hasPhysics ? getComponent("Physics") : null;
235
- const isInstanced = (_e = (_d = (_c = gameObject.components) === null || _c === void 0 ? void 0 : _c.model) === null || _d === void 0 ? void 0 : _d.properties) === null || _e === void 0 ? void 0 : _e.instanced;
246
+ const physicsDef = hasPhysics ? getComponentDef(physics.type) : null;
247
+ const isInstanced = (_b = (_a = findComponent(gameObject, "Model")) === null || _a === void 0 ? void 0 : _a.properties) === null || _b === void 0 ? void 0 : _b.instanced;
236
248
  const physicsKey = `physics_${nodeId}_${isInstanced ? 'instanced' : 'standard'}`;
237
- const renderCtx = { loadedModels, loadedTextures, editMode, registerRef };
249
+ const renderCtx = { loadedModels, loadedSounds, loadedTextures, editMode, registerRef, getRigidBody };
238
250
  const childNodes = getChildHostComponents(gameObject).length > 0
239
251
  ? _jsx(CompositionChildren, { childIds: childIds, selectedId: selectedId, ctx: renderCtx, parentMatrix: world })
240
- : _jsx(ChildNodes, { childIds: childIds, parentMatrix: world, selectedId: selectedId, onSelect: onSelect, onClick: onClick, registerRef: registerRef, registerRigidBodyRef: registerRigidBodyRef, loadedModels: loadedModels, loadedTextures: loadedTextures, editMode: editMode });
241
- const inner = (_jsx("group", { onPointerDown: editMode && !isLocked ? onDown : undefined, onPointerMove: editMode && !isLocked ? () => (clickValid.current = false) : undefined, onPointerUp: editMode && !isLocked ? onUp : undefined, children: renderCompositionNode(gameObject, renderCtx, isSelected, parentMatrix, childNodes) }));
252
+ : _jsx(ChildNodes, { childIds: childIds, parentMatrix: world, selectedId: selectedId, onSelect: onSelect, onClick: onClick, registerRef: registerRef, registerRigidBodyRef: registerRigidBodyRef, getRigidBody: getRigidBody, loadedModels: loadedModels, loadedSounds: loadedSounds, loadedTextures: loadedTextures, editMode: editMode });
253
+ const inner = (_jsx("group", Object.assign({}, clickHandlers, { children: renderCompositionNode(gameObject, renderCtx, isSelected, parentMatrix, childNodes) })));
242
254
  if (editMode) {
243
255
  return (_jsxs(_Fragment, { children: [_jsx("group", { ref: groupRef, position: transform.position, rotation: transform.rotation, scale: transform.scale, children: _jsx("mesh", { visible: false, children: _jsx("boxGeometry", { args: [0.01, 0.01, 0.01] }) }) }), _jsx("group", { ref: helperRef, position: transform.position, rotation: transform.rotation, scale: transform.scale, children: inner }), hasPhysics && (physicsDef === null || physicsDef === void 0 ? void 0 : physicsDef.View) ? (_jsx(physicsDef.View, { properties: physics.properties, position: transform.position, rotation: transform.rotation, scale: transform.scale, editMode: editMode, nodeId: nodeId, registerRigidBodyRef: registerRigidBodyRef, children: inner }, physicsKey)) : null] }));
244
256
  }
@@ -247,17 +259,17 @@ function StandardNode({ nodeId, selectedId, onSelect, onClick, registerRef, regi
247
259
  }
248
260
  return (_jsx("group", { ref: groupRef, position: transform.position, rotation: transform.rotation, scale: transform.scale, children: inner }));
249
261
  }
250
- const LEAF_COMPONENT_TYPES = new Set(["Geometry", "Material", "Physics"]);
251
262
  function getChildHostComponents(gameObject) {
252
263
  var _a;
253
- return Object.entries((_a = gameObject.components) !== null && _a !== void 0 ? _a : {}).flatMap(([key, comp]) => {
264
+ return Object.entries((_a = gameObject.components) !== null && _a !== void 0 ? _a : {}).reduce((result, [key, comp]) => {
254
265
  if (!(comp === null || comp === void 0 ? void 0 : comp.type))
255
- return [];
256
- const def = getComponent(comp.type);
257
- if (!(def === null || def === void 0 ? void 0 : def.View) || LEAF_COMPONENT_TYPES.has(comp.type))
258
- return [];
259
- return { key, View: def.View, properties: comp.properties };
260
- });
266
+ return result;
267
+ const def = getComponentDef(comp.type);
268
+ if (!(def === null || def === void 0 ? void 0 : def.View) || def.isWrapper)
269
+ return result;
270
+ result.push({ key, View: def.View, properties: comp.properties });
271
+ return result;
272
+ }, []);
261
273
  }
262
274
  function ChildNodes(_a) {
263
275
  var { childIds, parentMatrix } = _a, props = __rest(_a, ["childIds", "parentMatrix"]);
@@ -265,11 +277,11 @@ function ChildNodes(_a) {
265
277
  }
266
278
  function compose(node) {
267
279
  const { position, rotation, scale } = getNodeTransformProps(node);
268
- return new Matrix4().compose(new Vector3(...position), new Quaternion().setFromEuler(new Euler(...rotation)), new Vector3(...scale));
280
+ return composeTransform(position, rotation, scale);
269
281
  }
270
282
  function getModelRepeatSettings(node) {
271
- var _a, _b, _c;
272
- const properties = (_c = (_b = (_a = node === null || node === void 0 ? void 0 : node.components) === null || _a === void 0 ? void 0 : _a.model) === null || _b === void 0 ? void 0 : _b.properties) !== null && _c !== void 0 ? _c : {};
283
+ var _a, _b;
284
+ const properties = (_b = (_a = findComponent(node, "Model")) === null || _a === void 0 ? void 0 : _a.properties) !== null && _b !== void 0 ? _b : {};
273
285
  return {
274
286
  repeat: Boolean(properties.repeat),
275
287
  repeatAxes: getRepeatAxesFromModelProperties(properties),
@@ -320,12 +332,12 @@ function buildRepeatedInstances(gameObject, parentMatrix, modelUrl, physics) {
320
332
  return instances;
321
333
  }
322
334
  function getNodeTransformProps(node) {
323
- var _a, _b, _c, _d, _e;
324
- const t = (_b = (_a = node === null || node === void 0 ? void 0 : node.components) === null || _a === void 0 ? void 0 : _a.transform) === null || _b === void 0 ? void 0 : _b.properties;
335
+ var _a, _b, _c, _d;
336
+ const t = (_a = findComponent(node, "Transform")) === null || _a === void 0 ? void 0 : _a.properties;
325
337
  return {
326
- position: (_c = t === null || t === void 0 ? void 0 : t.position) !== null && _c !== void 0 ? _c : [0, 0, 0],
327
- rotation: (_d = t === null || t === void 0 ? void 0 : t.rotation) !== null && _d !== void 0 ? _d : [0, 0, 0],
328
- scale: (_e = t === null || t === void 0 ? void 0 : t.scale) !== null && _e !== void 0 ? _e : [1, 1, 1],
338
+ position: (_b = t === null || t === void 0 ? void 0 : t.position) !== null && _b !== void 0 ? _b : [0, 0, 0],
339
+ rotation: (_c = t === null || t === void 0 ? void 0 : t.rotation) !== null && _c !== void 0 ? _c : [0, 0, 0],
340
+ scale: (_d = t === null || t === void 0 ? void 0 : t.scale) !== null && _d !== void 0 ? _d : [1, 1, 1],
329
341
  };
330
342
  }
331
343
  function renderCompositionSubtree(gameObject, ctx, isSelected, childIds, parentMatrix = IDENTITY) {
@@ -351,40 +363,31 @@ function renderCompositionNode(gameObject, ctx, isSelected, parentMatrix, childN
351
363
  const ownContent = renderNodeOwnContent(gameObject, ctx, isSelected, parentMatrix);
352
364
  return wrapWithChildHosts(gameObject, ctx, isSelected, parentMatrix, _jsxs(_Fragment, { children: [ownContent, childNodes] }));
353
365
  }
354
- function renderNodeOwnContent(gameObject, ctx, isSelected, parentMatrix) {
355
- var _a, _b;
356
- const geometry = (_a = gameObject.components) === null || _a === void 0 ? void 0 : _a.geometry;
357
- const material = (_b = gameObject.components) === null || _b === void 0 ? void 0 : _b.material;
358
- const geometryDef = geometry && getComponent("Geometry");
359
- const materialDef = material && getComponent("Material");
360
- const contextProps = {
366
+ function buildContextProps(gameObject, ctx, isSelected, parentMatrix) {
367
+ return {
361
368
  loadedModels: ctx.loadedModels,
369
+ loadedSounds: ctx.loadedSounds,
362
370
  loadedTextures: ctx.loadedTextures,
363
371
  editMode: ctx.editMode,
364
372
  isSelected,
365
373
  nodeId: gameObject.id,
366
374
  parentMatrix,
367
375
  registerRef: ctx.registerRef,
376
+ getRigidBody: ctx.getRigidBody,
368
377
  };
369
- let core;
370
- if (geometry && (geometryDef === null || geometryDef === void 0 ? void 0 : geometryDef.View)) {
371
- core = (_jsxs("mesh", { castShadow: true, receiveShadow: true, children: [_jsx(geometryDef.View, Object.assign({ properties: geometry.properties }, contextProps)), material && (materialDef === null || materialDef === void 0 ? void 0 : materialDef.View) && (_jsx(materialDef.View, Object.assign({ properties: material.properties }, contextProps), "material"))] }));
372
- }
373
- else {
374
- core = null;
375
- }
376
- return core;
378
+ }
379
+ function renderNodeOwnContent(gameObject, ctx, isSelected, parentMatrix) {
380
+ const geometry = findComponent(gameObject, "Geometry");
381
+ const material = findComponent(gameObject, "Material");
382
+ const geometryDef = geometry && getComponentDef(geometry.type);
383
+ const materialDef = material && getComponentDef(material.type);
384
+ if (!geometry || !(geometryDef === null || geometryDef === void 0 ? void 0 : geometryDef.View))
385
+ return null;
386
+ const contextProps = buildContextProps(gameObject, ctx, isSelected, parentMatrix);
387
+ return (_jsxs("mesh", { castShadow: true, receiveShadow: true, children: [_jsx(geometryDef.View, Object.assign({ properties: geometry.properties }, contextProps)), material && (materialDef === null || materialDef === void 0 ? void 0 : materialDef.View) && (_jsx(materialDef.View, Object.assign({ properties: material.properties }, contextProps), "material"))] }));
377
388
  }
378
389
  function wrapWithChildHosts(gameObject, ctx, isSelected, parentMatrix, subtree) {
379
- const contextProps = {
380
- loadedModels: ctx.loadedModels,
381
- loadedTextures: ctx.loadedTextures,
382
- editMode: ctx.editMode,
383
- isSelected,
384
- nodeId: gameObject.id,
385
- parentMatrix,
386
- registerRef: ctx.registerRef,
387
- };
390
+ const contextProps = buildContextProps(gameObject, ctx, isSelected, parentMatrix);
388
391
  const childHosts = getChildHostComponents(gameObject);
389
392
  return childHosts.reduce((acc, { key, View, properties }) => (_jsx(View, Object.assign({ properties: properties }, contextProps, { children: acc }), key)), subtree);
390
393
  }
@@ -1,16 +1,17 @@
1
- import { jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  import { useRef } from 'react';
3
3
  import { gameEvents } from '../GameEvents';
4
- import { FieldGroup } from './Input';
5
- function ClickComponentEditor() {
6
- return (_jsx(FieldGroup, { children: _jsx("div", { style: { fontSize: 12, opacity: 0.8 }, children: "Emits a click game event in play mode when this entity is clicked." }) }));
4
+ import { FieldGroup, StringField } from './Input';
5
+ function ClickComponentEditor({ component, onUpdate }) {
6
+ return (_jsxs(FieldGroup, { children: [_jsx("div", { style: { fontSize: 12, opacity: 0.8 }, children: "Emits a game event in play mode when this entity is clicked." }), _jsx(StringField, { name: "eventName", label: "Emit Event", values: component.properties, onChange: onUpdate, placeholder: "click" })] }));
7
7
  }
8
- function ClickComponentView({ children, editMode, nodeId }) {
8
+ function ClickComponentView({ children, editMode, nodeId, properties }) {
9
9
  const clickValid = useRef(false);
10
+ const eventName = (properties === null || properties === void 0 ? void 0 : properties.eventName) || 'click';
10
11
  const emitClick = (event) => {
11
12
  if (!nodeId)
12
13
  return;
13
- gameEvents.emit('click', {
14
+ gameEvents.emit(eventName, {
14
15
  sourceEntityId: nodeId,
15
16
  point: [event.point.x, event.point.y, event.point.z],
16
17
  button: event.button,
@@ -40,6 +41,8 @@ const ClickComponent = {
40
41
  name: 'Click',
41
42
  Editor: ClickComponentEditor,
42
43
  View: ClickComponentView,
43
- defaultProperties: {},
44
+ defaultProperties: {
45
+ eventName: 'click',
46
+ },
44
47
  };
45
48
  export default ClickComponent;
@@ -1,5 +1,9 @@
1
1
  import { FC } from "react";
2
2
  import { ComponentData, GameObject } from "../types";
3
+ export type AssetRef = {
4
+ type: "model" | "texture" | "sound";
5
+ path: string;
6
+ };
3
7
  export interface Component {
4
8
  name: string;
5
9
  Editor: FC<{
@@ -10,9 +14,11 @@ export interface Component {
10
14
  }>;
11
15
  defaultProperties: any;
12
16
  View?: FC<any>;
13
- nonComposable?: boolean;
17
+ /** When true, this component wraps child entities (e.g. Physics wraps children in RigidBody). */
18
+ isWrapper?: boolean;
19
+ getAssetRefs?: (properties: Record<string, any>) => AssetRef[];
14
20
  }
15
21
  export declare function registerComponent(component: Component): void;
16
- export declare function getComponent(name: string): Component | undefined;
17
- export declare function getAllComponents(): Record<string, Component>;
18
- export declare function getNonComposableKeys(): string[];
22
+ export declare function getComponentDef(name: string): Component | undefined;
23
+ export declare function getAllComponentDefs(): Record<string, Component>;
24
+ export declare function getComponentAssetRefs(componentType: string, properties: Record<string, any>): AssetRef[];
@@ -2,14 +2,14 @@ const REGISTRY = {};
2
2
  export function registerComponent(component) {
3
3
  REGISTRY[component.name] = component;
4
4
  }
5
- export function getComponent(name) {
5
+ export function getComponentDef(name) {
6
6
  return REGISTRY[name];
7
7
  }
8
- export function getAllComponents() {
8
+ export function getAllComponentDefs() {
9
9
  return Object.assign({}, REGISTRY);
10
10
  }
11
- export function getNonComposableKeys() {
12
- return Object.values(REGISTRY)
13
- .filter(c => c.nonComposable)
14
- .map(c => c.name.toLowerCase());
11
+ export function getComponentAssetRefs(componentType, properties) {
12
+ var _a, _b;
13
+ const component = REGISTRY[componentType];
14
+ return (_b = (_a = component === null || component === void 0 ? void 0 : component.getAssetRefs) === null || _a === void 0 ? void 0 : _a.call(component, properties)) !== null && _b !== void 0 ? _b : [];
15
15
  }
@@ -84,7 +84,7 @@ const GeometryComponent = {
84
84
  name: 'Geometry',
85
85
  Editor: GeometryComponentEditor,
86
86
  View: GeometryComponentView,
87
- nonComposable: true,
87
+ isWrapper: true,
88
88
  defaultProperties: {
89
89
  geometryType: 'box',
90
90
  args: getDefaultArgs('box'),
@@ -136,6 +136,22 @@ interface BoundVector3FieldProps extends BoundFieldProps {
136
136
  export declare function FieldGroup({ children }: {
137
137
  children: React.ReactNode;
138
138
  }): import("react/jsx-runtime").JSX.Element;
139
+ interface ListEditorOption {
140
+ value: string;
141
+ label: string;
142
+ }
143
+ interface ListEditorProps<T> {
144
+ label: string;
145
+ items: T[];
146
+ renderItem: (item: T, index: number) => React.ReactNode;
147
+ onAdd: (value: string) => void;
148
+ addOptions?: ListEditorOption[];
149
+ emptyMessage?: string;
150
+ canAdd?: boolean;
151
+ addButtonTitle?: string;
152
+ addDisabledTitle?: string;
153
+ }
154
+ export declare function ListEditor<T>({ label, items, renderItem, onAdd, addOptions, emptyMessage, canAdd, addButtonTitle, addDisabledTitle, }: ListEditorProps<T>): import("react/jsx-runtime").JSX.Element;
139
155
  export declare function NumberField({ name, label, values, onChange, fallback, step, min, max, style, }: BoundNumberFieldProps): import("react/jsx-runtime").JSX.Element;
140
156
  export declare function StringField({ name, label, values, onChange, fallback, placeholder, }: BoundStringFieldProps): import("react/jsx-runtime").JSX.Element;
141
157
  export declare function ColorField({ name, label, values, onChange, fallback, }: BoundColorFieldProps): import("react/jsx-runtime").JSX.Element;
@@ -294,6 +294,39 @@ function bindFieldChange(name, onChange) {
294
294
  export function FieldGroup({ children }) {
295
295
  return _jsx("div", { style: { display: 'flex', flexDirection: 'column', gap: 8 }, children: children });
296
296
  }
297
+ export function ListEditor({ label, items, renderItem, onAdd, addOptions = [], emptyMessage = 'No items added.', canAdd = true, addButtonTitle = 'Add item', addDisabledTitle = 'No more items available', }) {
298
+ var _a;
299
+ const [selectedAddValue, setSelectedAddValue] = useState('');
300
+ const hasAddSelector = addOptions.length > 0;
301
+ const resolvedAddValue = hasAddSelector ? (selectedAddValue || ((_a = addOptions[0]) === null || _a === void 0 ? void 0 : _a.value) || '') : '';
302
+ const canAddItem = canAdd && (!hasAddSelector || resolvedAddValue !== '');
303
+ useEffect(() => {
304
+ var _a, _b;
305
+ if (!hasAddSelector) {
306
+ if (selectedAddValue !== '') {
307
+ setSelectedAddValue('');
308
+ }
309
+ return;
310
+ }
311
+ const stillAvailable = addOptions.some(option => option.value === selectedAddValue);
312
+ if (!stillAvailable) {
313
+ setSelectedAddValue((_b = (_a = addOptions[0]) === null || _a === void 0 ? void 0 : _a.value) !== null && _b !== void 0 ? _b : '');
314
+ }
315
+ }, [addOptions, hasAddSelector, selectedAddValue]);
316
+ return (_jsxs("div", { style: { display: 'flex', flexDirection: 'column', gap: 8 }, children: [_jsxs("div", { style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between' }, children: [_jsx(Label, { children: label }), _jsxs("div", { style: { display: 'flex', gap: 6, alignItems: 'center' }, children: [hasAddSelector ? (_jsx("div", { style: { minWidth: 140 }, children: _jsx(SelectInput, { value: resolvedAddValue, onChange: setSelectedAddValue, options: canAdd ? addOptions : [{ value: '', label: 'All items added' }] }) })) : null, _jsx("button", { type: "button", onClick: () => onAdd(resolvedAddValue), disabled: !canAddItem, style: {
317
+ width: 22,
318
+ height: 22,
319
+ borderRadius: 3,
320
+ border: `1px solid ${canAddItem ? colors.accentBorder : colors.border}`,
321
+ background: canAddItem ? colors.accentBg : colors.bgSurface,
322
+ color: canAddItem ? colors.accent : colors.textMuted,
323
+ cursor: canAddItem ? 'pointer' : 'not-allowed',
324
+ fontSize: 14,
325
+ lineHeight: 1,
326
+ padding: 0,
327
+ flexShrink: 0,
328
+ }, title: canAddItem ? addButtonTitle : addDisabledTitle, children: "+" })] })] }), items.length === 0 ? (_jsx("div", { style: { fontSize: 11, color: colors.textMuted }, children: emptyMessage })) : null, items.map(renderItem)] }));
329
+ }
297
330
  export function NumberField({ name, label, values, onChange, fallback = 0, step, min, max, style, }) {
298
331
  var _a;
299
332
  return (_jsx(FieldRow, { label: label, children: _jsx(NumberInput, { value: (_a = values[name]) !== null && _a !== void 0 ? _a : fallback, onChange: bindFieldChange(name, onChange), step: step, min: min, max: max, style: style }) }));