react-three-game 0.0.84 → 0.0.86

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 (40) hide show
  1. package/README.md +110 -36
  2. package/dist/index.d.ts +3 -7
  3. package/dist/index.js +1 -4
  4. package/dist/tools/prefabeditor/InstanceProvider.d.ts +0 -4
  5. package/dist/tools/prefabeditor/InstanceProvider.js +13 -44
  6. package/dist/tools/prefabeditor/PrefabEditor.d.ts +7 -2
  7. package/dist/tools/prefabeditor/PrefabEditor.js +13 -24
  8. package/dist/tools/prefabeditor/PrefabRoot.js +99 -48
  9. package/dist/tools/prefabeditor/{runtime.d.ts → assetRuntime.d.ts} +0 -25
  10. package/dist/tools/prefabeditor/assetRuntime.js +37 -0
  11. package/dist/tools/prefabeditor/components/BufferGeometryComponent.d.ts +3 -0
  12. package/dist/tools/prefabeditor/components/BufferGeometryComponent.js +98 -0
  13. package/dist/tools/prefabeditor/components/CameraComponent.js +1 -1
  14. package/dist/tools/prefabeditor/components/ComponentRegistry.d.ts +0 -3
  15. package/dist/tools/prefabeditor/components/DataComponent.d.ts +3 -0
  16. package/dist/tools/prefabeditor/components/DataComponent.js +87 -0
  17. package/dist/tools/prefabeditor/components/DirectionalLightComponent.js +1 -1
  18. package/dist/tools/prefabeditor/components/EnvironmentComponent.js +1 -1
  19. package/dist/tools/prefabeditor/components/GeometryComponent.js +4 -2
  20. package/dist/tools/prefabeditor/components/Input.d.ts +2 -13
  21. package/dist/tools/prefabeditor/components/Input.js +0 -55
  22. package/dist/tools/prefabeditor/components/MaterialComponent.d.ts +3 -0
  23. package/dist/tools/prefabeditor/components/MaterialComponent.js +49 -18
  24. package/dist/tools/prefabeditor/components/ModelComponent.js +3 -3
  25. package/dist/tools/prefabeditor/components/PhysicsComponent.d.ts +4 -0
  26. package/dist/tools/prefabeditor/components/PhysicsComponent.js +69 -132
  27. package/dist/tools/prefabeditor/components/PointLightComponent.js +1 -1
  28. package/dist/tools/prefabeditor/components/SoundComponent.js +17 -17
  29. package/dist/tools/prefabeditor/components/SpotLightComponent.js +1 -1
  30. package/dist/tools/prefabeditor/components/index.js +10 -8
  31. package/dist/tools/prefabeditor/types.d.ts +1 -0
  32. package/dist/tools/prefabeditor/types.js +18 -0
  33. package/dist/tools/prefabeditor/usePointerEvents.d.ts +27 -0
  34. package/dist/tools/prefabeditor/usePointerEvents.js +52 -0
  35. package/package.json +1 -1
  36. package/dist/tools/prefabeditor/GameEvents.d.ts +0 -128
  37. package/dist/tools/prefabeditor/GameEvents.js +0 -118
  38. package/dist/tools/prefabeditor/components/ClickComponent.d.ts +0 -3
  39. package/dist/tools/prefabeditor/components/ClickComponent.js +0 -52
  40. package/dist/tools/prefabeditor/runtime.js +0 -184
@@ -13,10 +13,9 @@ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-run
13
13
  import { useHelper } from "@react-three/drei";
14
14
  import { forwardRef, useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState } from "react";
15
15
  import { BoxHelper, Euler, Matrix4, } from "three";
16
- import { useFrame } from "@react-three/fiber";
17
16
  import { useStore } from "zustand";
18
17
  import { useClickValid } from "./useClickValid";
19
- import { findComponent } from "./types";
18
+ import { findComponent, getNodeUserData } from "./types";
20
19
  import { getComponentDef, getComponentAssetRefs, registerComponent } from "./components/ComponentRegistry";
21
20
  import { builtinComponents } from "./components";
22
21
  import { loadModel, loadSound, loadTexture } from "../dragdrop";
@@ -24,7 +23,7 @@ import { GameInstance, GameInstanceProvider, getRepeatAxesFromModelProperties, u
24
23
  import { composeTransform, decompose } from "./utils";
25
24
  import { isPhysicsProps } from "./components/PhysicsComponent";
26
25
  import { createPrefabStore, PrefabStoreProvider, useOptionalPrefabStoreApi, usePrefabChildIds, usePrefabNode, usePrefabRootId } from "./prefabStore";
27
- import { AssetRuntimeContext, EntityRuntimeScope, createRuntimeEngine } from "./runtime";
26
+ import { AssetRuntimeContext, EntityRuntimeScope } from "./assetRuntime";
28
27
  import { sound as soundManager } from "../../helpers/SoundManager";
29
28
  builtinComponents.forEach(registerComponent);
30
29
  const IDENTITY = new Matrix4();
@@ -45,6 +44,36 @@ function isNodeReady(node, loadedModels) {
45
44
  return true;
46
45
  return Boolean(loadedModels[model.properties.filename]);
47
46
  }
47
+ function emitNativeEvent(type, detail) {
48
+ const trimmedType = type === null || type === void 0 ? void 0 : type.trim();
49
+ if (!trimmedType || typeof window === 'undefined')
50
+ return;
51
+ window.dispatchEvent(new CustomEvent(trimmedType, { detail }));
52
+ }
53
+ function getNodeClickEventName(node) {
54
+ var _a;
55
+ const clickComponents = [
56
+ findComponent(node, 'BufferGeometry'),
57
+ findComponent(node, 'Geometry'),
58
+ ];
59
+ for (const component of clickComponents) {
60
+ if (!((_a = component === null || component === void 0 ? void 0 : component.properties) === null || _a === void 0 ? void 0 : _a.emitClickEvent))
61
+ continue;
62
+ const eventName = component.properties.clickEventName;
63
+ if (typeof eventName === 'string' && eventName.trim()) {
64
+ return eventName.trim();
65
+ }
66
+ }
67
+ return null;
68
+ }
69
+ function getNodeMetadataProps(node) {
70
+ var _a, _b;
71
+ const nodeName = (_b = (_a = node.name) === null || _a === void 0 ? void 0 : _a.trim()) !== null && _b !== void 0 ? _b : '';
72
+ return {
73
+ name: nodeName,
74
+ userData: Object.assign(Object.assign({ prefabNodeId: node.id }, (nodeName ? { prefabNodeName: nodeName } : {})), getNodeUserData(node)),
75
+ };
76
+ }
48
77
  export const PrefabRoot = forwardRef(({ editMode, data, store, selectedId, onSelect, onClick, onObjectRefChange, basePath = "" }, ref) => {
49
78
  var _a;
50
79
  const [models, setModels] = useState({});
@@ -94,29 +123,13 @@ export const PrefabRoot = forwardRef(({ editMode, data, store, selectedId, onSel
94
123
  setInjectedSounds(prev => (Object.assign(Object.assign({}, prev), { [path]: sound })));
95
124
  },
96
125
  }), [getObject]);
97
- const runtimeEngine = useMemo(() => createRuntimeEngine({
98
- store: resolvedStore,
99
- getObject,
100
- getRigidBody,
101
- }), [resolvedStore, getObject, getRigidBody]);
102
126
  const registerRef = useCallback((id, obj) => {
103
127
  objectRefs.current[id] = obj;
104
- runtimeEngine.invalidate();
105
128
  onObjectRefChange === null || onObjectRefChange === void 0 ? void 0 : onObjectRefChange(id, obj);
106
- }, [onObjectRefChange, runtimeEngine]);
129
+ }, [onObjectRefChange]);
107
130
  const registerRigidBodyRef = useCallback((id, rb) => {
108
131
  rigidBodyRefs.current.set(id, rb);
109
- runtimeEngine.invalidate();
110
- }, [runtimeEngine]);
111
- useEffect(() => {
112
- runtimeEngine.setActive(!editMode);
113
- }, [editMode, runtimeEngine]);
114
- useEffect(() => {
115
- return () => runtimeEngine.dispose();
116
- }, [runtimeEngine]);
117
- useFrame((_, dt) => {
118
- runtimeEngine.tick(dt);
119
- });
132
+ }, []);
120
133
  useEffect(() => {
121
134
  if (usesOwnedStore && data) {
122
135
  resolvedStore.getState().replacePrefab(data);
@@ -189,15 +202,32 @@ export const PrefabRoot = forwardRef(({ editMode, data, store, selectedId, onSel
189
202
  getAssetRevision: () => `${Object.keys(availableTextures).sort().join('|')}::${Object.keys(availableModels).sort().join('|')}`,
190
203
  }), [getObject, getRigidBody, registerRigidBodyRef, availableModels, availableTextures, availableSounds]);
191
204
  const content = (_jsx("group", { ref: rootRef, children: _jsx(GameInstanceProvider, { models: availableModels, selectedId: selectedId, editMode: editMode, onSelect: editMode ? onSelect : undefined, registerRef: registerRef, children: _jsx(StoreRootNode, { selectedId: selectedId, onSelect: editMode ? onSelect : undefined, onClick: onClick, registerRef: registerRef, loadedModels: availableModels, editMode: editMode, parentMatrix: IDENTITY }) }) }));
205
+ const runtimeContent = _jsx(AssetRuntimeContext.Provider, { value: assetRuntime, children: content });
192
206
  if (!shouldProvideStoreContext) {
193
- return _jsx(AssetRuntimeContext.Provider, { value: assetRuntime, children: content });
207
+ return runtimeContent;
194
208
  }
195
- return _jsx(PrefabStoreProvider, { store: resolvedStore, children: _jsx(AssetRuntimeContext.Provider, { value: assetRuntime, children: content }) });
209
+ return _jsx(PrefabStoreProvider, { store: resolvedStore, children: runtimeContent });
196
210
  });
197
211
  function StoreRootNode(props) {
198
212
  const rootId = usePrefabRootId();
199
213
  return _jsx(GameObjectRenderer, Object.assign({}, props, { nodeId: rootId }));
200
214
  }
215
+ function emitNodePointerEvent(eventName, event, nodeId, node, fallbackObject) {
216
+ var _a;
217
+ if (!eventName)
218
+ return;
219
+ emitNativeEvent(eventName, {
220
+ nodeId,
221
+ node,
222
+ object: (_a = event.object) !== null && _a !== void 0 ? _a : fallbackObject,
223
+ button: event.button,
224
+ altKey: event.nativeEvent.altKey,
225
+ ctrlKey: event.nativeEvent.ctrlKey,
226
+ metaKey: event.nativeEvent.metaKey,
227
+ shiftKey: event.nativeEvent.shiftKey,
228
+ r3fEvent: event,
229
+ });
230
+ }
201
231
  export function GameObjectRenderer(props) {
202
232
  var _a, _b;
203
233
  const node = usePrefabNode(props.nodeId);
@@ -221,20 +251,19 @@ export function GameObjectRenderer(props) {
221
251
  : _jsx(StandardNode, Object.assign({}, props), key);
222
252
  }
223
253
  function InstancedNode({ nodeId, parentMatrix = IDENTITY, editMode, registerRef, onSelect, onClick }) {
224
- var _a, _b, _c;
254
+ var _a, _b;
225
255
  const gameObject = usePrefabNode(nodeId);
226
256
  if (!gameObject)
227
257
  return null;
228
258
  const localTransform = getNodeTransformProps(gameObject);
229
259
  const isLocked = Boolean(gameObject.locked);
230
- const clickComponent = findComponent(gameObject, "Click");
231
- const clickable = Boolean(clickComponent);
232
- const clickEventName = (_a = clickComponent === null || clickComponent === void 0 ? void 0 : clickComponent.properties) === null || _a === void 0 ? void 0 : _a.eventName;
260
+ const metadataProps = getNodeMetadataProps(gameObject);
261
+ const groupProps = Object.assign(Object.assign({}, metadataProps), { position: localTransform.position, rotation: localTransform.rotation, scale: localTransform.scale });
233
262
  const physicsData = findComponent(gameObject, "Physics");
234
263
  const physicsProps = isPhysicsProps(physicsData === null || physicsData === void 0 ? void 0 : physicsData.properties)
235
264
  ? physicsData === null || physicsData === void 0 ? void 0 : physicsData.properties
236
265
  : undefined;
237
- const modelUrl = (_c = (_b = findComponent(gameObject, "Model")) === null || _b === void 0 ? void 0 : _b.properties) === null || _c === void 0 ? void 0 : _c.filename;
266
+ const modelUrl = (_b = (_a = findComponent(gameObject, "Model")) === null || _a === void 0 ? void 0 : _a.properties) === null || _b === void 0 ? void 0 : _b.filename;
238
267
  const instances = useMemo(() => buildRepeatedInstances(gameObject, parentMatrix, modelUrl, physicsProps), [gameObject, modelUrl, parentMatrix, physicsProps]);
239
268
  const groupRef = useRef(null);
240
269
  const handleGroupRef = useCallback((object) => {
@@ -243,22 +272,27 @@ function InstancedNode({ nodeId, parentMatrix = IDENTITY, editMode, registerRef,
243
272
  registerRef(nodeId, object);
244
273
  }
245
274
  }, [editMode, nodeId, registerRef]);
246
- const editClickHandlers = useClickValid(!!editMode && !isLocked, (e) => {
275
+ const editClickHandlers = useClickValid(!!editMode && !isLocked, (event) => {
247
276
  onSelect === null || onSelect === void 0 ? void 0 : onSelect(nodeId);
248
- onClick === null || onClick === void 0 ? void 0 : onClick(e, gameObject);
277
+ onClick === null || onClick === void 0 ? void 0 : onClick(event, gameObject);
249
278
  });
279
+ const renderedInstances = instances.map(instance => (_jsx(GameInstance, { id: instance.id, sourceId: gameObject.id, modelUrl: instance.modelUrl, position: instance.position, rotation: instance.rotation, scale: instance.scale, locked: isLocked, physics: instance.physics }, instance.id)));
250
280
  if (editMode) {
251
- return (_jsxs(_Fragment, { children: [_jsx("group", Object.assign({ ref: handleGroupRef, position: localTransform.position, rotation: localTransform.rotation, scale: localTransform.scale }, editClickHandlers, { children: _jsx("mesh", { visible: false, children: _jsx("boxGeometry", { args: [0.01, 0.01, 0.01] }) }) })), instances.map(instance => (_jsx(GameInstance, { id: instance.id, sourceId: gameObject.id, clickable: clickable, clickEventName: clickEventName, modelUrl: instance.modelUrl, position: instance.position, rotation: instance.rotation, scale: instance.scale, locked: isLocked, physics: instance.physics }, instance.id)))] }));
281
+ return (_jsxs(_Fragment, { children: [_jsx("group", Object.assign({ ref: handleGroupRef }, groupProps, editClickHandlers, { children: _jsx("mesh", { visible: false, children: _jsx("boxGeometry", { args: [0.01, 0.01, 0.01] }) }) })), renderedInstances] }));
252
282
  }
253
- return (_jsx(_Fragment, { children: instances.map(instance => (_jsx(GameInstance, { id: instance.id, sourceId: gameObject.id, clickable: clickable, clickEventName: clickEventName, modelUrl: instance.modelUrl, position: instance.position, rotation: instance.rotation, scale: instance.scale, locked: isLocked, physics: instance.physics }, instance.id))) }));
283
+ return _jsx(_Fragment, { children: renderedInstances });
254
284
  }
255
285
  function StandardNode({ nodeId, selectedId, onSelect, onClick, registerRef, loadedModels, editMode, parentMatrix = IDENTITY, }) {
256
286
  var _a, _b;
257
287
  const gameObject = usePrefabNode(nodeId);
258
288
  const childIds = usePrefabChildIds(nodeId);
289
+ if (!gameObject)
290
+ return null;
259
291
  const isSelected = selectedId === nodeId;
260
- const isLocked = Boolean(gameObject === null || gameObject === void 0 ? void 0 : gameObject.locked);
292
+ const isLocked = Boolean(gameObject.locked);
261
293
  const stillInstanced = useInstanceCheck(nodeId);
294
+ const clickEventName = getNodeClickEventName(gameObject);
295
+ const metadataProps = getNodeMetadataProps(gameObject);
262
296
  const groupRef = useRef(null);
263
297
  const helperRef = useRef(null);
264
298
  const handleGroupRef = useCallback((object) => {
@@ -268,30 +302,47 @@ function StandardNode({ nodeId, selectedId, onSelect, onClick, registerRef, load
268
302
  const handleHelperRef = useCallback((object) => {
269
303
  helperRef.current = object;
270
304
  }, []);
271
- const clickHandlers = useClickValid(!!editMode && !isLocked, (e) => {
305
+ const handleEditGroupRef = useCallback((object) => {
306
+ handleGroupRef(object);
307
+ handleHelperRef(object);
308
+ }, [handleGroupRef, handleHelperRef]);
309
+ const editClickHandlers = useClickValid(!!editMode && !isLocked, (event) => {
272
310
  onSelect === null || onSelect === void 0 ? void 0 : onSelect(nodeId);
273
- if (gameObject)
274
- onClick === null || onClick === void 0 ? void 0 : onClick(e, gameObject);
311
+ onClick === null || onClick === void 0 ? void 0 : onClick(event, gameObject);
275
312
  });
313
+ const primaryClickHandlers = !editMode && (clickEventName || onClick)
314
+ ? {
315
+ onClick: (event) => {
316
+ event.stopPropagation();
317
+ emitNodePointerEvent(clickEventName, event, nodeId, gameObject, groupRef.current);
318
+ onClick === null || onClick === void 0 ? void 0 : onClick(event, gameObject);
319
+ },
320
+ }
321
+ : undefined;
276
322
  useHelper(editMode && isSelected ? helperRef : null, BoxHelper, "cyan");
277
- if (!gameObject)
278
- return null;
279
323
  const world = parentMatrix.clone().multiply(compose(gameObject));
280
324
  const physics = findComponent(gameObject, "Physics");
281
325
  const ready = isNodeReady(gameObject, loadedModels);
282
326
  const hasPhysics = physics && ready && !stillInstanced;
283
327
  const transform = getNodeTransformProps(gameObject);
328
+ const transformProps = {
329
+ position: transform.position,
330
+ rotation: transform.rotation,
331
+ scale: transform.scale,
332
+ };
333
+ const groupProps = Object.assign(Object.assign({}, metadataProps), transformProps);
284
334
  const physicsDef = hasPhysics ? getComponentDef(physics.type) : null;
285
335
  const isInstanced = (_b = (_a = findComponent(gameObject, "Model")) === null || _a === void 0 ? void 0 : _a.properties) === null || _b === void 0 ? void 0 : _b.instanced;
286
336
  const physicsKey = `physics_${nodeId}_${isInstanced ? 'instanced' : 'standard'}`;
287
337
  const renderCtx = { loadedModels, editMode, registerRef };
288
338
  const childNodes = _jsx(ChildNodes, { childIds: childIds, parentMatrix: world, selectedId: selectedId, onSelect: onSelect, onClick: onClick, registerRef: registerRef, loadedModels: loadedModels, editMode: editMode });
289
- const inner = (_jsx("group", Object.assign({}, clickHandlers, { children: renderCompositionNode(gameObject, renderCtx, childNodes) })));
339
+ const inner = renderCompositionNode(gameObject, renderCtx, primaryClickHandlers, childNodes);
290
340
  const physicsInner = editMode ? _jsx("group", { visible: false, children: inner }) : inner;
291
- return (_jsx(EntityRuntimeScope, { nodeId: nodeId, editMode: editMode, isSelected: isSelected, children: editMode ? (_jsxs(_Fragment, { children: [_jsx("group", { ref: handleGroupRef, position: transform.position, rotation: transform.rotation, scale: transform.scale, children: _jsx("mesh", { visible: false, children: _jsx("boxGeometry", { args: [0.01, 0.01, 0.01] }) }) }), _jsx("group", { ref: handleHelperRef, position: transform.position, rotation: transform.rotation, scale: transform.scale, children: inner }), hasPhysics && (physicsDef === null || physicsDef === void 0 ? void 0 : physicsDef.View) ? (_jsx(physicsDef.View, { properties: physics.properties, position: transform.position, rotation: transform.rotation, scale: transform.scale, children: physicsInner }, physicsKey)) : null] })) : hasPhysics && (physicsDef === null || physicsDef === void 0 ? void 0 : physicsDef.View) ? (_jsx(physicsDef.View, { properties: physics.properties, position: transform.position, rotation: transform.rotation, scale: transform.scale, children: inner }, physicsKey)) : (_jsx("group", { ref: handleGroupRef, position: transform.position, rotation: transform.rotation, scale: transform.scale, children: inner })) }));
341
+ return (_jsx(EntityRuntimeScope, { nodeId: nodeId, editMode: editMode, isSelected: isSelected, children: editMode ? (_jsxs(_Fragment, { children: [_jsxs("group", Object.assign({ ref: handleEditGroupRef }, groupProps, editClickHandlers, { children: [_jsx("mesh", { visible: false, children: _jsx("boxGeometry", { args: [0.01, 0.01, 0.01] }) }), inner] })), hasPhysics && (physicsDef === null || physicsDef === void 0 ? void 0 : physicsDef.View) ? (_jsx(physicsDef.View, Object.assign({ properties: physics.properties }, transformProps, { children: physicsInner }), physicsKey)) : null] })) : hasPhysics && (physicsDef === null || physicsDef === void 0 ? void 0 : physicsDef.View) ? (_jsx(physicsDef.View, Object.assign({ properties: physics.properties }, transformProps, { children: _jsx("group", Object.assign({ ref: handleGroupRef }, metadataProps, { children: inner })) }), physicsKey)) : (_jsx("group", Object.assign({ ref: handleGroupRef }, groupProps, { children: inner }))) }));
292
342
  }
293
343
  function isRendererHandledComponent(componentType) {
294
344
  return componentType === "Transform"
345
+ || componentType === "BufferGeometry"
295
346
  || componentType === "Geometry"
296
347
  || componentType === "Material"
297
348
  || componentType === "Physics"
@@ -382,26 +433,26 @@ function getNodeTransformProps(node) {
382
433
  scale: (_d = t === null || t === void 0 ? void 0 : t.scale) !== null && _d !== void 0 ? _d : [1, 1, 1],
383
434
  };
384
435
  }
385
- function renderCompositionNode(gameObject, ctx, childNodes) {
386
- const primaryContent = renderNodePrimaryContent(gameObject, ctx);
436
+ function renderCompositionNode(gameObject, ctx, primaryClickHandlers, childNodes) {
437
+ const primaryContent = renderNodePrimaryContent(gameObject, ctx, primaryClickHandlers);
387
438
  return applyNodeComposition(gameObject, _jsxs(_Fragment, { children: [primaryContent, childNodes] }));
388
439
  }
389
- function renderNodePrimaryContent(gameObject, ctx) {
390
- var _a, _b;
391
- const geometry = findComponent(gameObject, "Geometry");
440
+ function renderNodePrimaryContent(gameObject, ctx, primaryClickHandlers) {
441
+ var _a, _b, _c;
442
+ const geometry = (_a = findComponent(gameObject, "BufferGeometry")) !== null && _a !== void 0 ? _a : findComponent(gameObject, "Geometry");
392
443
  const material = findComponent(gameObject, "Material");
393
444
  const model = findComponent(gameObject, "Model");
394
445
  const geometryDef = geometry && getComponentDef(geometry.type);
395
446
  const materialDef = material && getComponentDef(material.type);
396
447
  const modelDef = model && getComponentDef(model.type);
397
- const geometryProperties = (_a = geometry === null || geometry === void 0 ? void 0 : geometry.properties) !== null && _a !== void 0 ? _a : {};
448
+ const geometryProperties = (_b = geometry === null || geometry === void 0 ? void 0 : geometry.properties) !== null && _b !== void 0 ? _b : {};
398
449
  const meshVisible = geometryProperties.visible !== false;
399
450
  const meshCastShadow = meshVisible && geometryProperties.castShadow !== false;
400
451
  const meshReceiveShadow = meshVisible && geometryProperties.receiveShadow !== false;
401
452
  if ((geometry === null || geometry === void 0 ? void 0 : geometry.type) && (geometryDef === null || geometryDef === void 0 ? void 0 : geometryDef.View)) {
402
- return (_jsxs("mesh", { visible: meshVisible, castShadow: meshCastShadow, receiveShadow: meshReceiveShadow, children: [_jsx(geometryDef.View, { properties: geometry.properties }), material && (materialDef === null || materialDef === void 0 ? void 0 : materialDef.View) && (_jsx(materialDef.View, { properties: material.properties }, "material"))] }));
453
+ return (_jsxs("mesh", Object.assign({ visible: meshVisible, castShadow: meshCastShadow, receiveShadow: meshReceiveShadow }, primaryClickHandlers, { children: [_jsx(geometryDef.View, { properties: geometry.properties }), material && (materialDef === null || materialDef === void 0 ? void 0 : materialDef.View) && (_jsx(materialDef.View, { properties: material.properties }, "material"))] })));
403
454
  }
404
- if ((model === null || model === void 0 ? void 0 : model.type) && (modelDef === null || modelDef === void 0 ? void 0 : modelDef.View) && !((_b = model.properties) === null || _b === void 0 ? void 0 : _b.instanced) && isNodeReady(gameObject, ctx.loadedModels)) {
455
+ if ((model === null || model === void 0 ? void 0 : model.type) && (modelDef === null || modelDef === void 0 ? void 0 : modelDef.View) && !((_c = model.properties) === null || _c === void 0 ? void 0 : _c.instanced) && isNodeReady(gameObject, ctx.loadedModels)) {
405
456
  return _jsx(modelDef.View, { properties: model.properties });
406
457
  }
407
458
  return null;
@@ -1,7 +1,5 @@
1
1
  import { type ReactNode } from "react";
2
2
  import type { Object3D, Texture } from "three";
3
- import type { PrefabStoreApi } from "./prefabStore";
4
- import { type EntityComponent, type Scene } from "./scene";
5
3
  export interface AssetRuntime {
6
4
  registerRigidBodyRef: (id: string, rb: any) => void;
7
5
  getModel: (path: string) => Object3D | null;
@@ -36,26 +34,3 @@ export declare function EntityRuntimeScope({ nodeId, editMode, isSelected, child
36
34
  isSelected?: boolean;
37
35
  children: ReactNode;
38
36
  }): import("react/jsx-runtime").JSX.Element;
39
- /** Runtime behaviour produced by `Component.create(ctx)`. All methods optional. */
40
- export interface ComponentInstance {
41
- start?(): void;
42
- update?(dt: number): void;
43
- destroy?(): void;
44
- }
45
- export interface ComponentRuntimeContext<TProperties = Record<string, any>> {
46
- scene: Scene;
47
- component: EntityComponent<TProperties>;
48
- object: Object3D;
49
- rigidBody: any;
50
- }
51
- export interface RuntimeEngine {
52
- tick: (dt: number) => void;
53
- setActive: (active: boolean) => void;
54
- invalidate: () => void;
55
- dispose: () => void;
56
- }
57
- export declare function createRuntimeEngine({ store, getObject, getRigidBody, }: {
58
- store: PrefabStoreApi;
59
- getObject: (id: string) => Object3D | null;
60
- getRigidBody: (id: string) => any;
61
- }): RuntimeEngine;
@@ -0,0 +1,37 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { createContext, useContext, useMemo } from "react";
3
+ export const AssetRuntimeContext = createContext(null);
4
+ const EntityRuntimeContext = createContext(null);
5
+ export function useAssetRuntime() {
6
+ const ctx = useContext(AssetRuntimeContext);
7
+ if (!ctx)
8
+ throw new Error("useAssetRuntime must be used inside <PrefabRoot>");
9
+ return ctx;
10
+ }
11
+ export function useEntityRuntime() {
12
+ const ctx = useContext(EntityRuntimeContext);
13
+ if (!ctx)
14
+ throw new Error("useEntityRuntime must be used inside a component View rendered by <PrefabRoot>");
15
+ return ctx;
16
+ }
17
+ export function useEntityObjectRef() {
18
+ const { getObject } = useEntityRuntime();
19
+ return useMemo(() => ({ get current() { return getObject(); } }), [getObject]);
20
+ }
21
+ export function useEntityRigidBodyRef() {
22
+ const { getRigidBody } = useEntityRuntime();
23
+ return useMemo(() => ({ get current() { return getRigidBody(); } }), [getRigidBody]);
24
+ }
25
+ export function EntityRuntimeScope({ nodeId, editMode, isSelected, children, }) {
26
+ const asset = useContext(AssetRuntimeContext);
27
+ if (!asset)
28
+ throw new Error("EntityRuntimeScope must be used inside <PrefabRoot>");
29
+ const value = useMemo(() => ({
30
+ nodeId,
31
+ editMode,
32
+ isSelected,
33
+ getObject: () => asset.getObject(nodeId),
34
+ getRigidBody: () => asset.getRigidBody(nodeId),
35
+ }), [asset, editMode, isSelected, nodeId]);
36
+ return _jsx(EntityRuntimeContext.Provider, { value: value, children: children });
37
+ }
@@ -0,0 +1,3 @@
1
+ import { Component } from "./ComponentRegistry";
2
+ declare const BufferGeometryComponent: Component;
3
+ export default BufferGeometryComponent;
@@ -0,0 +1,98 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { BooleanField, FieldGroup, StringField } from "./Input";
3
+ const DEFAULT_TRIANGLE_POSITIONS = [
4
+ 0, 0, 0,
5
+ 1, 0, 0,
6
+ 0, 1, 0,
7
+ ];
8
+ const DEFAULT_TRIANGLE_INDICES = [0, 1, 2];
9
+ const DEFAULT_TRIANGLE_UVS = [
10
+ 0, 0,
11
+ 1, 0,
12
+ 0, 1,
13
+ ];
14
+ function isFiniteNumberArray(value) {
15
+ return Array.isArray(value) && value.every(entry => typeof entry === 'number' && Number.isFinite(entry));
16
+ }
17
+ function normalizeNumberArray(value, fallback) {
18
+ return isFiniteNumberArray(value) ? value : fallback;
19
+ }
20
+ function toAttributeText(value, fallback) {
21
+ return JSON.stringify(normalizeNumberArray(value, fallback));
22
+ }
23
+ function parseArrayInput(raw) {
24
+ const trimmed = raw.trim();
25
+ if (!trimmed)
26
+ return [];
27
+ const parsed = JSON.parse(trimmed);
28
+ if (!isFiniteNumberArray(parsed)) {
29
+ throw new Error('Expected a JSON array of numbers');
30
+ }
31
+ return parsed;
32
+ }
33
+ function getIndexArray(indices) {
34
+ if (indices.length === 0)
35
+ return null;
36
+ const maxIndex = Math.max(...indices);
37
+ return maxIndex > 65535 ? new Uint32Array(indices) : new Uint16Array(indices);
38
+ }
39
+ function BufferArrayField({ label, value, fallback, onChange, rows = 4, }) {
40
+ return (_jsxs("label", { style: { display: 'grid', gap: 4 }, children: [_jsx("span", { style: { fontSize: '10px', color: '#888', textTransform: 'uppercase', letterSpacing: '0.05em', fontWeight: 500 }, children: label }), _jsx("textarea", { rows: rows, spellCheck: false, defaultValue: toAttributeText(value, fallback), onBlur: (event) => {
41
+ try {
42
+ onChange(parseArrayInput(event.target.value));
43
+ event.target.setCustomValidity('');
44
+ }
45
+ catch (_a) {
46
+ event.target.setCustomValidity('Expected a JSON array of numbers');
47
+ event.target.reportValidity();
48
+ }
49
+ }, style: {
50
+ width: '100%',
51
+ backgroundColor: '#171717',
52
+ border: '1px solid #333',
53
+ padding: '6px 8px',
54
+ fontSize: '11px',
55
+ color: '#eee',
56
+ fontFamily: 'monospace',
57
+ outline: 'none',
58
+ borderRadius: 3,
59
+ resize: 'vertical',
60
+ boxSizing: 'border-box',
61
+ } })] }));
62
+ }
63
+ function BufferGeometryComponentEditor({ component, onUpdate, }) {
64
+ var _a;
65
+ const properties = (_a = component.properties) !== null && _a !== void 0 ? _a : {};
66
+ return (_jsxs(FieldGroup, { children: [_jsx(BufferArrayField, { label: "Positions", value: properties.positions, fallback: DEFAULT_TRIANGLE_POSITIONS, rows: 5, onChange: (positions) => onUpdate({ positions }) }), _jsx(BufferArrayField, { label: "Indices", value: properties.indices, fallback: DEFAULT_TRIANGLE_INDICES, onChange: (indices) => onUpdate({ indices }) }), _jsx(BufferArrayField, { label: "Normals", value: properties.normals, fallback: [], onChange: (normals) => onUpdate({ normals }) }), _jsx(BufferArrayField, { label: "UVs", value: properties.uvs, fallback: DEFAULT_TRIANGLE_UVS, onChange: (uvs) => onUpdate({ uvs }) }), _jsx(BooleanField, { name: "computeVertexNormals", label: "Compute Normals", values: properties, onChange: onUpdate, fallback: true }), _jsx(BooleanField, { name: "visible", label: "Visible", values: properties, onChange: onUpdate, fallback: true }), _jsx(BooleanField, { name: "castShadow", label: "Cast Shadow", values: properties, onChange: onUpdate, fallback: true }), _jsx(BooleanField, { name: "receiveShadow", label: "Receive Shadow", values: properties, onChange: onUpdate, fallback: true }), _jsx(BooleanField, { name: "emitClickEvent", label: "Emit Click Event", values: properties, onChange: onUpdate, fallback: false }), properties.emitClickEvent ? (_jsx(StringField, { name: "clickEventName", label: "Click Event Name", values: properties, onChange: onUpdate, placeholder: "entity:click" })) : null] }));
67
+ }
68
+ function BufferGeometryComponentView({ properties }) {
69
+ const positions = normalizeNumberArray(properties.positions, DEFAULT_TRIANGLE_POSITIONS);
70
+ const indices = normalizeNumberArray(properties.indices, DEFAULT_TRIANGLE_INDICES);
71
+ const normals = normalizeNumberArray(properties.normals, []);
72
+ const uvs = normalizeNumberArray(properties.uvs, DEFAULT_TRIANGLE_UVS);
73
+ const indexArray = getIndexArray(indices);
74
+ const hasNormals = normals.length >= 3 && normals.length % 3 === 0;
75
+ const hasUvs = uvs.length >= 2 && uvs.length % 2 === 0;
76
+ return (_jsxs("bufferGeometry", { onUpdate: (geometry) => {
77
+ if (properties.computeVertexNormals !== false && !hasNormals) {
78
+ geometry.computeVertexNormals();
79
+ }
80
+ geometry.computeBoundingBox();
81
+ geometry.computeBoundingSphere();
82
+ }, children: [_jsx("bufferAttribute", { attach: "attributes-position", args: [new Float32Array(positions), 3] }), indexArray ? (_jsx("bufferAttribute", { attach: "index", args: [indexArray, 1] })) : null, hasNormals ? (_jsx("bufferAttribute", { attach: "attributes-normal", args: [new Float32Array(normals), 3] })) : null, hasUvs ? (_jsx("bufferAttribute", { attach: "attributes-uv", args: [new Float32Array(uvs), 2] })) : null] }));
83
+ }
84
+ const BufferGeometryComponent = {
85
+ name: 'BufferGeometry',
86
+ Editor: BufferGeometryComponentEditor,
87
+ View: BufferGeometryComponentView,
88
+ defaultProperties: {
89
+ positions: DEFAULT_TRIANGLE_POSITIONS,
90
+ indices: DEFAULT_TRIANGLE_INDICES,
91
+ normals: [],
92
+ uvs: DEFAULT_TRIANGLE_UVS,
93
+ computeVertexNormals: true,
94
+ emitClickEvent: false,
95
+ clickEventName: '',
96
+ },
97
+ };
98
+ export default BufferGeometryComponent;
@@ -3,7 +3,7 @@ import { OrthographicCamera, PerspectiveCamera, useHelper } from '@react-three/d
3
3
  import { useRef } from 'react';
4
4
  import { CameraHelper } from 'three';
5
5
  import { useFrame, useThree } from '@react-three/fiber';
6
- import { useEntityRuntime } from '../runtime';
6
+ import { useEntityRuntime } from '../assetRuntime';
7
7
  import { FieldGroup, NumberField, SelectField } from './Input';
8
8
  const CAMERA_PROJECTION_OPTIONS = [
9
9
  { value: 'perspective', label: 'Perspective' },
@@ -1,6 +1,5 @@
1
1
  import { FC } from "react";
2
2
  import { ComponentData, GameObject } from "../types";
3
- import type { ComponentInstance, ComponentRuntimeContext } from "../runtime";
4
3
  export type AssetRef = {
5
4
  type: "model" | "texture" | "sound";
6
5
  path: string;
@@ -28,8 +27,6 @@ export interface Component {
28
27
  }>;
29
28
  defaultProperties: any;
30
29
  View?: FC<ComponentViewProps>;
31
- /** Optional runtime factory for the non-React game loop. */
32
- create?: (ctx: ComponentRuntimeContext) => ComponentInstance | void;
33
30
  /** Declare which asset paths this component references (for asset loading). */
34
31
  getAssetRefs?: (properties: Record<string, any>) => AssetRef[];
35
32
  }
@@ -0,0 +1,3 @@
1
+ import { Component } from "./ComponentRegistry";
2
+ declare const DataComponent: Component;
3
+ export default DataComponent;
@@ -0,0 +1,87 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useEffect, useState } from "react";
3
+ import { colors } from "../styles";
4
+ const RESERVED_USER_DATA_KEYS = new Set([
5
+ 'prefabNodeId',
6
+ 'prefabNodeName',
7
+ ]);
8
+ const inputStyle = {
9
+ width: '100%',
10
+ backgroundColor: colors.bgInput,
11
+ border: `1px solid ${colors.border}`,
12
+ padding: '6px 8px',
13
+ fontSize: '11px',
14
+ color: colors.text,
15
+ fontFamily: 'monospace',
16
+ outline: 'none',
17
+ borderRadius: 3,
18
+ boxSizing: 'border-box',
19
+ };
20
+ function isRecord(value) {
21
+ return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
22
+ }
23
+ function normalizeData(value) {
24
+ if (!isRecord(value))
25
+ return {};
26
+ return Object.entries(value).reduce((result, [key, entry]) => {
27
+ if (!key.trim() || entry === undefined) {
28
+ return result;
29
+ }
30
+ result[key] = entry;
31
+ return result;
32
+ }, {});
33
+ }
34
+ function formatData(value) {
35
+ return JSON.stringify(normalizeData(value), null, 2);
36
+ }
37
+ function parseData(raw) {
38
+ const trimmed = raw.trim();
39
+ if (!trimmed) {
40
+ return { ok: true, value: {} };
41
+ }
42
+ try {
43
+ const parsed = JSON.parse(trimmed);
44
+ if (!isRecord(parsed)) {
45
+ return { ok: false, error: 'Data must be a JSON object' };
46
+ }
47
+ const nextData = normalizeData(parsed);
48
+ for (const key of Object.keys(nextData)) {
49
+ if (RESERVED_USER_DATA_KEYS.has(key)) {
50
+ return { ok: false, error: `Reserved key: ${key}` };
51
+ }
52
+ }
53
+ return { ok: true, value: nextData };
54
+ }
55
+ catch (_a) {
56
+ return { ok: false, error: 'Data must be valid JSON' };
57
+ }
58
+ }
59
+ function DataComponentEditor({ component, onUpdate }) {
60
+ var _a;
61
+ const [draft, setDraft] = useState(() => { var _a; return formatData((_a = component.properties) === null || _a === void 0 ? void 0 : _a.data); });
62
+ const [error, setError] = useState(null);
63
+ useEffect(() => {
64
+ var _a;
65
+ setDraft(formatData((_a = component.properties) === null || _a === void 0 ? void 0 : _a.data));
66
+ setError(null);
67
+ }, [(_a = component.properties) === null || _a === void 0 ? void 0 : _a.data]);
68
+ const commitDraft = () => {
69
+ const parsed = parseData(draft);
70
+ if (!parsed.ok) {
71
+ setError(parsed.error);
72
+ return;
73
+ }
74
+ setError(null);
75
+ setDraft(formatData(parsed.value));
76
+ onUpdate({ data: parsed.value });
77
+ };
78
+ return (_jsxs("div", { style: { display: 'flex', flexDirection: 'column', gap: 8 }, children: [_jsx("textarea", { rows: 10, spellCheck: false, value: draft, onChange: (event) => setDraft(event.target.value), onBlur: commitDraft, style: Object.assign(Object.assign({}, inputStyle), { resize: 'vertical', minHeight: 140 }) }), error ? (_jsx("div", { style: { fontSize: 10, color: colors.accent, fontFamily: 'monospace' }, children: error })) : null, _jsx("div", { style: { fontSize: 10, color: colors.textMuted, lineHeight: 1.4 }, children: "Enter a JSON object. Keys map directly onto `object.userData`." })] }));
79
+ }
80
+ const DataComponent = {
81
+ name: 'Data',
82
+ Editor: DataComponentEditor,
83
+ defaultProperties: {
84
+ data: {},
85
+ },
86
+ };
87
+ export default DataComponent;
@@ -3,7 +3,7 @@ import { useHelper } from "@react-three/drei";
3
3
  import { useRef, useEffect, useState } from "react";
4
4
  import { useFrame } from "@react-three/fiber";
5
5
  import { CameraHelper } from "three";
6
- import { useEntityRuntime } from "../runtime";
6
+ import { useEntityRuntime } from "../assetRuntime";
7
7
  import { BooleanField, ColorField, NumberField, NumberInput, Vector3Input } from "./Input";
8
8
  import { LightSection, ShadowBiasField, mergeWithDefaults } from "./lightUtils";
9
9
  import { colors } from "../styles";
@@ -1,7 +1,7 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { Environment } from '@react-three/drei';
3
3
  import { FieldGroup, NumberField } from './Input';
4
- import { useAssetRuntime } from '../runtime';
4
+ import { useAssetRuntime } from '../assetRuntime';
5
5
  function EnvironmentView({ properties, children, }) {
6
6
  const { getAssetRevision } = useAssetRuntime();
7
7
  const { intensity = 1, resolution = 256 } = properties;
@@ -1,5 +1,5 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { BooleanField, FieldGroup, NumberField, SelectField } from "./Input";
2
+ import { BooleanField, FieldGroup, NumberField, SelectField, StringField } from "./Input";
3
3
  const GEOMETRY_ARGS = {
4
4
  box: {
5
5
  fields: [
@@ -61,7 +61,7 @@ function GeometryComponentEditor({ component, onUpdate, }) {
61
61
  ] }), schema.fields.map((field, index) => {
62
62
  var _a;
63
63
  return (_jsx(NumberField, { name: field.name, label: field.label, values: { [field.name]: (_a = args[index]) !== null && _a !== void 0 ? _a : field.defaultValue }, onChange: (next) => updateArg(index, next[field.name]), fallback: field.defaultValue, min: field.min, step: field.step }, field.name));
64
- }), _jsx(BooleanField, { name: "visible", label: "Visible", values: component.properties, onChange: handleChange, fallback: true }), _jsx(BooleanField, { name: "castShadow", label: "Cast Shadow", values: component.properties, onChange: handleChange, fallback: true }), _jsx(BooleanField, { name: "receiveShadow", label: "Receive Shadow", values: component.properties, onChange: handleChange, fallback: true })] }));
64
+ }), _jsx(BooleanField, { name: "visible", label: "Visible", values: component.properties, onChange: handleChange, fallback: true }), _jsx(BooleanField, { name: "castShadow", label: "Cast Shadow", values: component.properties, onChange: handleChange, fallback: true }), _jsx(BooleanField, { name: "receiveShadow", label: "Receive Shadow", values: component.properties, onChange: handleChange, fallback: true }), _jsx(BooleanField, { name: "emitClickEvent", label: "Emit Click Event", values: component.properties, onChange: handleChange, fallback: false }), component.properties.emitClickEvent ? (_jsx(StringField, { name: "clickEventName", label: "Click Event Name", values: component.properties, onChange: handleChange, placeholder: "cannon:fire" })) : null] }));
65
65
  }
66
66
  // View for Geometry component
67
67
  function GeometryComponentView({ properties, children }) {
@@ -87,6 +87,8 @@ const GeometryComponent = {
87
87
  defaultProperties: {
88
88
  geometryType: 'box',
89
89
  args: getDefaultArgs('box'),
90
+ emitClickEvent: false,
91
+ clickEventName: '',
90
92
  }
91
93
  };
92
94
  export default GeometryComponent;