react-three-game 0.0.69 → 0.0.71

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 (64) 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 +20 -13
  4. package/dist/index.js +14 -7
  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 -40
  15. package/dist/tools/prefabeditor/EditorTreeMenus.js +2 -20
  16. package/dist/tools/prefabeditor/EditorUI.js +8 -5
  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 +23 -1
  20. package/dist/tools/prefabeditor/PrefabEditor.js +79 -47
  21. package/dist/tools/prefabeditor/PrefabRoot.d.ts +26 -9
  22. package/dist/tools/prefabeditor/PrefabRoot.js +195 -159
  23. package/dist/tools/prefabeditor/RefBridge.d.ts +24 -0
  24. package/dist/tools/prefabeditor/RefBridge.js +44 -0
  25. package/dist/tools/prefabeditor/components/AmbientLightComponent.js +10 -7
  26. package/dist/tools/prefabeditor/components/CameraComponent.js +8 -14
  27. package/dist/tools/prefabeditor/components/ClickComponent.js +12 -7
  28. package/dist/tools/prefabeditor/components/ComponentRegistry.d.ts +31 -5
  29. package/dist/tools/prefabeditor/components/ComponentRegistry.js +6 -6
  30. package/dist/tools/prefabeditor/components/DirectionalLightComponent.js +124 -52
  31. package/dist/tools/prefabeditor/components/EnvironmentComponent.js +5 -3
  32. package/dist/tools/prefabeditor/components/GeometryComponent.js +1 -1
  33. package/dist/tools/prefabeditor/components/Input.d.ts +16 -0
  34. package/dist/tools/prefabeditor/components/Input.js +33 -0
  35. package/dist/tools/prefabeditor/components/MaterialComponent.js +19 -8
  36. package/dist/tools/prefabeditor/components/ModelComponent.js +39 -45
  37. package/dist/tools/prefabeditor/components/PhysicsComponent.d.ts +10 -1
  38. package/dist/tools/prefabeditor/components/PhysicsComponent.js +127 -31
  39. package/dist/tools/prefabeditor/components/PointLightComponent.d.ts +3 -0
  40. package/dist/tools/prefabeditor/components/PointLightComponent.js +55 -0
  41. package/dist/tools/prefabeditor/components/SoundComponent.d.ts +3 -0
  42. package/dist/tools/prefabeditor/components/SoundComponent.js +244 -0
  43. package/dist/tools/prefabeditor/components/SpotLightComponent.js +53 -24
  44. package/dist/tools/prefabeditor/components/TransformComponent.js +2 -2
  45. package/dist/tools/prefabeditor/components/index.js +4 -0
  46. package/dist/tools/prefabeditor/components/lightUtils.d.ts +13 -0
  47. package/dist/tools/prefabeditor/components/lightUtils.js +64 -0
  48. package/dist/tools/prefabeditor/prefab.d.ts +37 -0
  49. package/dist/tools/prefabeditor/prefab.js +229 -0
  50. package/dist/tools/prefabeditor/prefabStore.d.ts +4 -16
  51. package/dist/tools/prefabeditor/prefabStore.js +32 -173
  52. package/dist/tools/prefabeditor/{sceneApi.d.ts → scene.d.ts} +15 -1
  53. package/dist/tools/prefabeditor/{sceneApi.js → scene.js} +66 -32
  54. package/dist/tools/prefabeditor/styles.d.ts +1 -0
  55. package/dist/tools/prefabeditor/styles.js +9 -0
  56. package/dist/tools/prefabeditor/types.d.ts +13 -0
  57. package/dist/tools/prefabeditor/types.js +28 -1
  58. package/dist/tools/prefabeditor/useClickValid.d.ts +13 -0
  59. package/dist/tools/prefabeditor/useClickValid.js +21 -0
  60. package/dist/tools/prefabeditor/utils.d.ts +2 -4
  61. package/dist/tools/prefabeditor/utils.js +8 -46
  62. package/package.json +1 -1
  63. package/dist/tools/prefabeditor/EditorContext.d.ts +0 -16
  64. package/dist/tools/prefabeditor/EditorContext.js +0 -9
@@ -16,29 +16,31 @@ import { InstancedRigidBodies } from "@react-three/rapier";
16
16
  import { ActiveCollisionTypes } from "@dimforge/rapier3d-compat";
17
17
  import { Mesh, Matrix4, Vector3, Quaternion, Euler, BoxHelper } from "three";
18
18
  import { gameEvents, getEntityIdFromRigidBody } from "./GameEvents";
19
+ import { useClickValid } from "./useClickValid";
19
20
  export const DEFAULT_REPEAT_AXES = [{ axis: 'x', count: 1, offset: 1 }];
20
21
  export function normalizeRepeatAxes(value) {
21
22
  if (!Array.isArray(value)) {
22
23
  return DEFAULT_REPEAT_AXES;
23
24
  }
24
25
  const seen = new Set();
25
- const normalized = value.flatMap((entry) => {
26
+ const normalized = value.reduce((result, entry) => {
26
27
  if (!entry || typeof entry !== 'object')
27
- return [];
28
+ return result;
28
29
  const axisValue = entry.axis;
29
30
  if (axisValue !== 'x' && axisValue !== 'y' && axisValue !== 'z')
30
- return [];
31
+ return result;
31
32
  if (seen.has(axisValue))
32
- return [];
33
+ return result;
33
34
  seen.add(axisValue);
34
35
  const countValue = Number(entry.count);
35
36
  const offsetValue = Number(entry.offset);
36
- return [{
37
- axis: axisValue,
38
- count: Number.isFinite(countValue) ? Math.max(1, Math.floor(countValue)) : 1,
39
- offset: Number.isFinite(offsetValue) ? offsetValue : 1,
40
- }];
41
- });
37
+ result.push({
38
+ axis: axisValue,
39
+ count: Number.isFinite(countValue) ? Math.max(1, Math.floor(countValue)) : 1,
40
+ offset: Number.isFinite(offsetValue) ? offsetValue : 1,
41
+ });
42
+ return result;
43
+ }, []);
42
44
  return normalized.length > 0 ? normalized : DEFAULT_REPEAT_AXES;
43
45
  }
44
46
  function toVector3Tuple(value, fallback) {
@@ -101,36 +103,17 @@ function hasPhysics(instance) {
101
103
  function getColliderType(physics) {
102
104
  return physics.colliders || (physics.type === 'fixed' ? 'trimesh' : 'hull');
103
105
  }
104
- function emitSensorEnter(sourceId, payload) {
105
- gameEvents.emit('sensor:enter', {
106
- sourceEntityId: sourceId,
107
- targetEntityId: getEntityIdFromRigidBody(payload.other.rigidBody),
108
- targetRigidBody: payload.other.rigidBody,
109
- });
110
- }
111
- function emitSensorExit(sourceId, payload) {
112
- gameEvents.emit('sensor:exit', {
113
- sourceEntityId: sourceId,
114
- targetEntityId: getEntityIdFromRigidBody(payload.other.rigidBody),
115
- targetRigidBody: payload.other.rigidBody,
116
- });
117
- }
118
- function emitCollisionEnter(sourceId, payload) {
119
- gameEvents.emit('collision:enter', {
120
- sourceEntityId: sourceId,
121
- targetEntityId: getEntityIdFromRigidBody(payload.other.rigidBody),
122
- targetRigidBody: payload.other.rigidBody,
123
- });
124
- }
125
- function emitCollisionExit(sourceId, payload) {
126
- gameEvents.emit('collision:exit', {
106
+ function emitPhysicsEvent(sourceId, eventName, payload) {
107
+ if (!eventName)
108
+ return;
109
+ gameEvents.emit(eventName, {
127
110
  sourceEntityId: sourceId,
128
111
  targetEntityId: getEntityIdFromRigidBody(payload.other.rigidBody),
129
112
  targetRigidBody: payload.other.rigidBody,
130
113
  });
131
114
  }
132
- function emitClick(sourceId, instanceId, event) {
133
- gameEvents.emit('click', {
115
+ function emitClick(sourceId, instanceId, eventName, event) {
116
+ gameEvents.emit(eventName, {
134
117
  sourceEntityId: sourceId,
135
118
  instanceEntityId: instanceId && instanceId !== sourceId ? instanceId : undefined,
136
119
  point: [event.point.x, event.point.y, event.point.z],
@@ -145,6 +128,7 @@ function instanceEquals(a, b) {
145
128
  return a.id === b.id &&
146
129
  a.sourceId === b.sourceId &&
147
130
  a.clickable === b.clickable &&
131
+ a.clickEventName === b.clickEventName &&
148
132
  a.locked === b.locked &&
149
133
  a.meshPath === b.meshPath &&
150
134
  arrayEquals(a.position, b.position) &&
@@ -261,7 +245,7 @@ function InstancedRigidGroup({ group, modelKey, partCount, flatMeshes, onSelect,
261
245
  const rigidBodiesRef = useRef(null);
262
246
  const instances = useMemo(() => group.instances.filter(hasPhysics).map(inst => {
263
247
  const _a = inst.physics, { activeCollisionTypes: _activeCollisionTypes, colliders: _colliders, userData } = _a, rigidBodyProps = __rest(_a, ["activeCollisionTypes", "colliders", "userData"]);
264
- return Object.assign(Object.assign({ key: inst.id, position: inst.position, rotation: inst.rotation, scale: inst.scale }, rigidBodyProps), { colliders: getColliderType(inst.physics), userData: Object.assign(Object.assign({}, userData), { entityId: inst.sourceId }), onIntersectionEnter: (payload) => emitSensorEnter(inst.sourceId, payload), onIntersectionExit: (payload) => emitSensorExit(inst.sourceId, payload), onCollisionEnter: (payload) => emitCollisionEnter(inst.sourceId, payload), onCollisionExit: (payload) => emitCollisionExit(inst.sourceId, payload) });
248
+ return Object.assign(Object.assign({ key: inst.id, position: inst.position, rotation: inst.rotation, scale: inst.scale }, rigidBodyProps), { colliders: getColliderType(inst.physics), userData: Object.assign(Object.assign({}, userData), { entityId: inst.sourceId }), onIntersectionEnter: (payload) => emitPhysicsEvent(inst.sourceId, inst.physics.sensorEnterEventName, payload), onIntersectionExit: (payload) => emitPhysicsEvent(inst.sourceId, inst.physics.sensorExitEventName, payload), onCollisionEnter: (payload) => emitPhysicsEvent(inst.sourceId, inst.physics.collisionEnterEventName, payload), onCollisionExit: (payload) => emitPhysicsEvent(inst.sourceId, inst.physics.collisionExitEventName, payload) });
265
249
  }), [group.instances]);
266
250
  // Apply scale to visual meshes (InstancedRigidBodies only scales colliders, not visuals)
267
251
  useEffect(() => {
@@ -336,7 +320,7 @@ function InstancedRigidGroup({ group, modelKey, partCount, flatMeshes, onSelect,
336
320
  if (!instance.clickable)
337
321
  return;
338
322
  e.stopPropagation();
339
- emitClick(instance.sourceId, instance.id, e);
323
+ emitClick(instance.sourceId, instance.id, instance.clickEventName || 'click', e);
340
324
  };
341
325
  const shouldHandleClick = editMode || group.instances.some(inst => inst.clickable);
342
326
  // Add key to force remount when instance count changes significantly (helps with cleanup)
@@ -356,12 +340,19 @@ function NonPhysicsInstancedGroup({ modelKey, group, partCount, instancesMap, on
356
340
  }
357
341
  // Individual instance item with its own click state
358
342
  function InstanceGroupItem({ instance, InstanceComponents, onSelect, registerRef, selectedId, editMode }) {
359
- const clickValid = useRef(false);
360
343
  const groupRef = useRef(null);
361
344
  const isLocked = Boolean(instance.locked);
362
345
  const isSelected = selectedId === instance.id || selectedId === instance.sourceId;
363
346
  const canSelect = editMode && !isLocked;
364
347
  const canClick = !editMode && Boolean(instance.clickable);
348
+ const pointerHandlers = useClickValid(canSelect || canClick, (e) => {
349
+ if (editMode) {
350
+ onSelect === null || onSelect === void 0 ? void 0 : onSelect(instance.sourceId);
351
+ }
352
+ else if (instance.clickable) {
353
+ emitClick(instance.sourceId, instance.id, instance.clickEventName || 'click', e);
354
+ }
355
+ });
365
356
  // Use BoxHelper when object is selected in edit mode
366
357
  useHelper(editMode && isSelected ? groupRef : null, BoxHelper, 'cyan');
367
358
  useEffect(() => {
@@ -370,18 +361,7 @@ function InstanceGroupItem({ instance, InstanceComponents, onSelect, registerRef
370
361
  registerRef === null || registerRef === void 0 ? void 0 : registerRef(instance.id, groupRef.current);
371
362
  return () => registerRef === null || registerRef === void 0 ? void 0 : registerRef(instance.id, null);
372
363
  }, [editMode, instance.id, registerRef]);
373
- return (_jsx("group", { ref: groupRef, position: instance.position, rotation: instance.rotation, scale: instance.scale, onPointerDown: canSelect || canClick ? (e) => { e.stopPropagation(); clickValid.current = true; } : undefined, onPointerMove: canSelect || canClick ? () => { clickValid.current = false; } : undefined, onPointerUp: canSelect || canClick ? (e) => {
374
- if (clickValid.current) {
375
- e.stopPropagation();
376
- if (editMode) {
377
- onSelect === null || onSelect === void 0 ? void 0 : onSelect(instance.sourceId);
378
- }
379
- else if (instance.clickable) {
380
- emitClick(instance.sourceId, instance.id, e);
381
- }
382
- }
383
- clickValid.current = false;
384
- } : undefined, children: InstanceComponents.map((Instance, i) => _jsx(Instance, {}, i)) }));
364
+ return (_jsx("group", Object.assign({ ref: groupRef, position: instance.position, rotation: instance.rotation, scale: instance.scale }, pointerHandlers, { children: InstanceComponents.map((Instance, i) => _jsx(Instance, {}, i)) })));
385
365
  }
386
366
  // Hook to check if an instance exists
387
367
  export function useInstanceCheck(id) {
@@ -390,21 +370,43 @@ export function useInstanceCheck(id) {
390
370
  return (_a = ctx === null || ctx === void 0 ? void 0 : ctx.hasInstance(id)) !== null && _a !== void 0 ? _a : false;
391
371
  }
392
372
  // GameInstance component: registers an instance for batch rendering (renders nothing itself)
393
- export const GameInstance = React.forwardRef(({ id, sourceId, clickable = false, modelUrl, locked = false, position, rotation, scale, physics = undefined, }, ref) => {
373
+ export const GameInstance = React.forwardRef(({ id, sourceId, clickable = false, clickEventName, modelUrl, locked = false, position, rotation, scale, physics = undefined, }, ref) => {
394
374
  const ctx = useContext(GameInstanceContext);
395
375
  const addInstance = ctx === null || ctx === void 0 ? void 0 : ctx.addInstance;
396
376
  const removeInstance = ctx === null || ctx === void 0 ? void 0 : ctx.removeInstance;
377
+ const [positionX, positionY, positionZ] = position;
378
+ const [rotationX, rotationY, rotationZ] = rotation;
379
+ const [scaleX, scaleY, scaleZ] = scale;
380
+ const physicsSignature = getPhysicsSignature(physics);
397
381
  const instance = useMemo(() => ({
398
382
  id,
399
383
  sourceId: sourceId !== null && sourceId !== void 0 ? sourceId : id,
400
384
  clickable,
385
+ clickEventName,
401
386
  locked,
402
387
  meshPath: modelUrl,
403
388
  position,
404
389
  rotation,
405
390
  scale,
406
391
  physics,
407
- }), [id, sourceId, clickable, locked, modelUrl, JSON.stringify(position), JSON.stringify(rotation), JSON.stringify(scale), getPhysicsSignature(physics)]);
392
+ }), [
393
+ id,
394
+ sourceId,
395
+ clickable,
396
+ clickEventName,
397
+ locked,
398
+ modelUrl,
399
+ positionX,
400
+ positionY,
401
+ positionZ,
402
+ rotationX,
403
+ rotationY,
404
+ rotationZ,
405
+ scaleX,
406
+ scaleY,
407
+ scaleZ,
408
+ physicsSignature,
409
+ ]);
408
410
  useEffect(() => {
409
411
  if (!addInstance || !removeInstance)
410
412
  return;
@@ -3,7 +3,7 @@ import { Object3D, Texture } from "three";
3
3
  import { GameObject, Prefab } from "./types";
4
4
  import { PrefabRootRef } from "./PrefabRoot";
5
5
  import type { ExportGLBOptions } from "./utils";
6
- import { type Scene, type SpawnOptions } from "./sceneApi";
6
+ import { type Scene, type SpawnOptions } from "./scene";
7
7
  export interface PrefabEditorRef {
8
8
  screenshot: () => void;
9
9
  exportGLB: (options?: ExportGLBOptions) => Promise<ArrayBuffer | undefined>;
@@ -19,10 +19,32 @@ export interface PrefabEditorRef {
19
19
  addTexture: (path: string, texture: Texture, options?: SpawnOptions) => GameObject;
20
20
  viewRef: React.RefObject<PrefabRootRef | null>;
21
21
  }
22
+ export declare enum PrefabEditorMode {
23
+ Edit = "edit",
24
+ Play = "play"
25
+ }
26
+ export interface EditorContextType {
27
+ mode: PrefabEditorMode;
28
+ setMode: (mode: PrefabEditorMode) => void;
29
+ transformMode: "translate" | "rotate" | "scale";
30
+ setTransformMode: (mode: "translate" | "rotate" | "scale") => void;
31
+ scaleSnap: number;
32
+ setScaleSnap: (resolution: number) => void;
33
+ positionSnap: number;
34
+ setPositionSnap: (resolution: number) => void;
35
+ rotationSnap: number;
36
+ setRotationSnap: (resolution: number) => void;
37
+ onFocusNode?: (nodeId: string) => void;
38
+ onScreenshot?: () => void;
39
+ onExportGLB?: () => void;
40
+ }
41
+ export declare const EditorContext: import("react").Context<EditorContextType | null>;
42
+ export declare function useEditorContext(): EditorContextType;
22
43
  export interface PrefabEditorProps {
23
44
  basePath?: string;
24
45
  initialPrefab?: Prefab;
25
46
  physics?: boolean;
47
+ mode?: PrefabEditorMode;
26
48
  onChange?: (prefab: Prefab) => void;
27
49
  showUI?: boolean;
28
50
  enableWindowDrop?: boolean;
@@ -10,31 +10,39 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
10
10
  import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
11
11
  import { MapControls, TransformControls } from "@react-three/drei";
12
12
  import GameCanvas from "../../shared/GameCanvas";
13
- import { useCallback, useEffect, useMemo, useRef, useState, forwardRef, useImperativeHandle } from "react";
13
+ import { useCallback, useEffect, useMemo, useRef, useState, forwardRef, useImperativeHandle, createContext, useContext } from "react";
14
+ import { findComponentEntry } from "./types";
14
15
  import PrefabRoot from "./PrefabRoot";
15
16
  import { Physics } from "@react-three/rapier";
16
17
  import EditorUI from "./EditorUI";
17
18
  import { base, toolbar } from "./styles";
18
- import { EditorContext } from "./EditorContext";
19
- import { createImageNode, createModelNode, computeParentWorldMatrix, decompose, exportGLB as exportGLBFile, exportGLBData, focusCameraOnObject, regenerateIds } from "./utils";
19
+ import { computeParentWorldMatrix, decompose, exportGLB as exportGLBFile, exportGLBData, focusCameraOnObject, regenerateIds } from "./utils";
20
20
  import { loadFiles } from "../dragdrop";
21
- import { createPrefabStore, PrefabStoreProvider, prefabStoreToPrefab } from "./prefabStore";
22
- import { createScene } from "./sceneApi";
21
+ import { denormalizePrefab, createImageNode, createModelNode, createNode } from './prefab';
22
+ import { createPrefabStore, PrefabStoreProvider } from "./prefabStore";
23
+ import { createScene } from "./scene";
24
+ export var PrefabEditorMode;
25
+ (function (PrefabEditorMode) {
26
+ PrefabEditorMode["Edit"] = "edit";
27
+ PrefabEditorMode["Play"] = "play";
28
+ })(PrefabEditorMode || (PrefabEditorMode = {}));
29
+ export const EditorContext = createContext(null);
30
+ export function useEditorContext() {
31
+ const context = useContext(EditorContext);
32
+ if (!context) {
33
+ throw new Error("useEditorContext must be used within EditorContext.Provider");
34
+ }
35
+ return context;
36
+ }
37
+ const MAX_HISTORY_LENGTH = 50;
38
+ const HISTORY_DEBOUNCE_MS = 500;
23
39
  const DEFAULT_PREFAB = {
24
40
  id: "prefab-default",
25
41
  name: "New Prefab",
26
- root: {
27
- id: "root",
28
- components: {
29
- transform: {
30
- type: "Transform",
31
- properties: { position: [0, 0, 0], rotation: [0, 0, 0], scale: [1, 1, 1] }
32
- }
33
- }
34
- }
42
+ root: createNode('Root', {}, { id: 'root' })
35
43
  };
36
- const PrefabEditor = forwardRef(({ basePath, initialPrefab, physics = true, onChange, showUI = true, enableWindowDrop = true, canvasProps, uiPlugins, children }, ref) => {
37
- const [editMode, setEditMode] = useState(true);
44
+ const PrefabEditor = forwardRef(({ basePath, initialPrefab, physics = true, mode: initialMode = PrefabEditorMode.Edit, onChange, showUI = true, enableWindowDrop = true, canvasProps, uiPlugins, children }, ref) => {
45
+ const [mode, setMode] = useState(initialMode);
38
46
  const [selectedId, setSelectedId] = useState(null);
39
47
  const [transformMode, setTransformMode] = useState("translate");
40
48
  const [scaleSnap, setScaleSnap] = useState(0);
@@ -51,9 +59,8 @@ const PrefabEditor = forwardRef(({ basePath, initialPrefab, physics = true, onCh
51
59
  const canvasRef = useRef(null);
52
60
  const controlsRef = useRef(null);
53
61
  const onChangeRef = useRef(onChange);
54
- const [injectedModels, setInjectedModels] = useState({});
55
- const [injectedTextures, setInjectedTextures] = useState({});
56
- const getPrefab = useCallback(() => prefabStoreToPrefab(prefabStore.getState()), [prefabStore]);
62
+ const isEditMode = mode === PrefabEditorMode.Edit;
63
+ const getPrefab = useCallback(() => denormalizePrefab(prefabStore.getState()), [prefabStore]);
57
64
  onChangeRef.current = onChange;
58
65
  const setSelection = useCallback((nodeId) => {
59
66
  const nextNode = nodeId ? prefabStore.getState().nodesById[nodeId] : null;
@@ -62,24 +69,29 @@ const PrefabEditor = forwardRef(({ basePath, initialPrefab, physics = true, onCh
62
69
  }
63
70
  setSelectedId(nodeId);
64
71
  }, [prefabStore]);
65
- const toggleEditMode = () => {
66
- setEditMode(prev => {
67
- const next = !prev;
68
- if (!next) {
72
+ const updateMode = useCallback((nextMode) => {
73
+ setMode(prev => {
74
+ if (prev === nextMode)
75
+ return prev;
76
+ if (nextMode === PrefabEditorMode.Play) {
69
77
  setSelectedId(null);
70
78
  setSelectedObject(null);
71
79
  }
72
- return next;
80
+ return nextMode;
73
81
  });
82
+ }, []);
83
+ const toggleMode = () => {
84
+ updateMode(isEditMode ? PrefabEditorMode.Play : PrefabEditorMode.Edit);
74
85
  };
86
+ useEffect(() => {
87
+ updateMode(initialMode);
88
+ }, [initialMode, updateMode]);
75
89
  const loadPrefab = useCallback((prefab, options) => {
76
90
  changeOriginRef.current = (options === null || options === void 0 ? void 0 : options.notifyChange) === false ? "replace-silent" : "replace";
77
91
  prefabStore.getState().replacePrefab(prefab);
78
92
  setSelectedObject(null);
79
93
  if (options === null || options === void 0 ? void 0 : options.resetHistory) {
80
94
  setSelectedId(null);
81
- setInjectedModels({});
82
- setInjectedTextures({});
83
95
  setHistory([prefab]);
84
96
  historyIndexRef.current = 0;
85
97
  setHistoryIndex(0);
@@ -101,7 +113,7 @@ const PrefabEditor = forwardRef(({ basePath, initialPrefab, physics = true, onCh
101
113
  return;
102
114
  }
103
115
  lastRevision = state.revision;
104
- const nextPrefab = prefabStoreToPrefab(state);
116
+ const nextPrefab = denormalizePrefab(state);
105
117
  const changeOrigin = changeOriginRef.current;
106
118
  if (changeOrigin !== "replace-silent") {
107
119
  (_a = onChangeRef.current) === null || _a === void 0 ? void 0 : _a.call(onChangeRef, nextPrefab);
@@ -110,7 +122,7 @@ const PrefabEditor = forwardRef(({ basePath, initialPrefab, physics = true, onCh
110
122
  clearTimeout(historyTimeout);
111
123
  historyTimeout = null;
112
124
  }
113
- if (changeOrigin || !editMode) {
125
+ if (changeOrigin || !isEditMode) {
114
126
  changeOriginRef.current = null;
115
127
  return;
116
128
  }
@@ -118,13 +130,13 @@ const PrefabEditor = forwardRef(({ basePath, initialPrefab, physics = true, onCh
118
130
  const currentHistoryIndex = historyIndexRef.current;
119
131
  setHistory(prev => {
120
132
  const nextHistory = [...prev.slice(0, currentHistoryIndex + 1), nextPrefab];
121
- return nextHistory.length > 50 ? nextHistory.slice(1) : nextHistory;
133
+ return nextHistory.length > MAX_HISTORY_LENGTH ? nextHistory.slice(1) : nextHistory;
122
134
  });
123
- const nextHistoryIndex = Math.min(currentHistoryIndex + 1, 49);
135
+ const nextHistoryIndex = Math.min(currentHistoryIndex + 1, MAX_HISTORY_LENGTH - 1);
124
136
  historyIndexRef.current = nextHistoryIndex;
125
137
  setHistoryIndex(nextHistoryIndex);
126
138
  historyTimeout = null;
127
- }, 500);
139
+ }, HISTORY_DEBOUNCE_MS);
128
140
  });
129
141
  return () => {
130
142
  if (historyTimeout) {
@@ -132,7 +144,7 @@ const PrefabEditor = forwardRef(({ basePath, initialPrefab, physics = true, onCh
132
144
  }
133
145
  unsubscribe();
134
146
  };
135
- }, [editMode, prefabStore]);
147
+ }, [isEditMode, prefabStore]);
136
148
  useEffect(() => {
137
149
  if (!selectedId)
138
150
  return;
@@ -165,15 +177,17 @@ const PrefabEditor = forwardRef(({ basePath, initialPrefab, physics = true, onCh
165
177
  addNode(regenerateIds(prefab.root), { select: false });
166
178
  }, [addNode]);
167
179
  const addModel = useCallback((path, model, options) => {
180
+ var _a;
168
181
  const node = createModelNode(path, options === null || options === void 0 ? void 0 : options.name);
169
182
  addNode(node, options);
170
- setInjectedModels(prev => (Object.assign(Object.assign({}, prev), { [path]: model })));
183
+ (_a = prefabRootRef.current) === null || _a === void 0 ? void 0 : _a.addModel(path, model);
171
184
  return node;
172
185
  }, [addNode]);
173
186
  const addTexture = useCallback((path, texture, options) => {
187
+ var _a;
174
188
  const node = createImageNode(path, options === null || options === void 0 ? void 0 : options.name);
175
189
  addNode(node, options);
176
- setInjectedTextures(prev => (Object.assign(Object.assign({}, prev), { [path]: texture })));
190
+ (_a = prefabRootRef.current) === null || _a === void 0 ? void 0 : _a.addTexture(path, texture);
177
191
  return node;
178
192
  }, [addNode]);
179
193
  const applyHistory = (index) => {
@@ -187,7 +201,7 @@ const PrefabEditor = forwardRef(({ basePath, initialPrefab, physics = true, onCh
187
201
  const undo = () => historyIndex > 0 && applyHistory(historyIndex - 1);
188
202
  const redo = () => historyIndex < history.length - 1 && applyHistory(historyIndex + 1);
189
203
  useEffect(() => {
190
- if (!editMode)
204
+ if (!isEditMode)
191
205
  return;
192
206
  const handleKeyDown = (e) => {
193
207
  if (!(e.ctrlKey || e.metaKey))
@@ -203,7 +217,7 @@ const PrefabEditor = forwardRef(({ basePath, initialPrefab, physics = true, onCh
203
217
  };
204
218
  window.addEventListener('keydown', handleKeyDown);
205
219
  return () => window.removeEventListener('keydown', handleKeyDown);
206
- }, [editMode, historyIndex, history]);
220
+ }, [isEditMode, historyIndex, history]);
207
221
  const handleScreenshot = useCallback(() => {
208
222
  const canvas = canvasRef.current;
209
223
  if (!canvas)
@@ -253,6 +267,18 @@ const PrefabEditor = forwardRef(({ basePath, initialPrefab, physics = true, onCh
253
267
  const scene = useMemo(() => createScene({
254
268
  getRootId: () => prefabStore.getState().rootId,
255
269
  getNode: (id) => { var _a; return (_a = prefabStore.getState().nodesById[id]) !== null && _a !== void 0 ? _a : null; },
270
+ getChildIds: (id) => { var _a; return (_a = prefabStore.getState().childIdsById[id]) !== null && _a !== void 0 ? _a : []; },
271
+ getParentId: (id) => { var _a; return (_a = prefabStore.getState().parentIdById[id]) !== null && _a !== void 0 ? _a : null; },
272
+ findByName: (name) => {
273
+ var _a;
274
+ const state = prefabStore.getState();
275
+ const normalized = name.toLowerCase();
276
+ for (const [id, node] of Object.entries(state.nodesById)) {
277
+ if (((_a = node.name) === null || _a === void 0 ? void 0 : _a.toLowerCase()) === normalized)
278
+ return id;
279
+ }
280
+ return null;
281
+ },
256
282
  updateNode: (id, update) => prefabStore.getState().updateNode(id, update),
257
283
  updateNodes: (updates) => prefabStore.getState().updateNodes(Object.entries(updates).map(([id, update]) => ({ id, update }))),
258
284
  addNode: (node, options) => addNode(node, options).id,
@@ -268,14 +294,19 @@ const PrefabEditor = forwardRef(({ basePath, initialPrefab, physics = true, onCh
268
294
  const parentWorld = computeParentWorldMatrix(prefabStore.getState(), selectedId);
269
295
  const local = parentWorld.clone().invert().multiply(object.matrixWorld);
270
296
  const { position, rotation, scale } = decompose(local);
271
- prefabStore.getState().updateNode(selectedId, node => (Object.assign(Object.assign({}, node), { components: Object.assign(Object.assign({}, node.components), { transform: {
272
- type: "Transform",
273
- properties: { position, rotation, scale },
274
- } }) })));
297
+ prefabStore.getState().updateNode(selectedId, node => {
298
+ var _a;
299
+ const entry = findComponentEntry(node, "Transform");
300
+ const key = (_a = entry === null || entry === void 0 ? void 0 : entry[0]) !== null && _a !== void 0 ? _a : "transform";
301
+ return Object.assign(Object.assign({}, node), { components: Object.assign(Object.assign({}, node.components), { [key]: {
302
+ type: "Transform",
303
+ properties: { position, rotation, scale },
304
+ } }) });
305
+ });
275
306
  };
276
307
  // --- Drag & drop files to add nodes ---
277
308
  useEffect(() => {
278
- if (!enableWindowDrop || !editMode)
309
+ if (!enableWindowDrop || !isEditMode)
279
310
  return;
280
311
  function handleDragOver(e) {
281
312
  e.preventDefault();
@@ -308,7 +339,7 @@ const PrefabEditor = forwardRef(({ basePath, initialPrefab, physics = true, onCh
308
339
  window.removeEventListener('dragover', handleDragOver);
309
340
  window.removeEventListener('drop', handleDrop);
310
341
  };
311
- }, [addModel, addTexture, editMode, enableWindowDrop]);
342
+ }, [addModel, addTexture, isEditMode, enableWindowDrop]);
312
343
  useImperativeHandle(ref, () => ({
313
344
  screenshot: handleScreenshot,
314
345
  exportGLB: handleExportGLB,
@@ -321,9 +352,10 @@ const PrefabEditor = forwardRef(({ basePath, initialPrefab, physics = true, onCh
321
352
  addTexture,
322
353
  viewRef: prefabRootRef
323
354
  }), [addModel, addTexture, clearSelection, getPrefab, handleExportGLB, handleExportGLBData, handleScreenshot, loadPrefab, scene]);
324
- const content = (_jsxs(_Fragment, { children: [_jsx("ambientLight", { intensity: 1.5 }), _jsx("gridHelper", { args: [10, 10], position: [0, -1, 0] }), _jsx(PrefabRoot, { ref: prefabRootRef, store: prefabStore, editMode: editMode, selectedId: selectedId, onSelect: setSelection, onSelectedObjectChange: editMode ? setSelectedObject : undefined, onFocusNode: editMode ? handleFocusNode : undefined, basePath: basePath, injectedModels: injectedModels, injectedTextures: injectedTextures }), children] }));
355
+ const content = (_jsxs(_Fragment, { children: [_jsx("gridHelper", { args: [10, 10], position: [0, -1, 0] }), _jsx(PrefabRoot, { ref: prefabRootRef, store: prefabStore, editMode: isEditMode, selectedId: selectedId, onSelect: setSelection, basePath: basePath }), children] }));
325
356
  return _jsx(PrefabStoreProvider, { store: prefabStore, children: _jsxs(EditorContext.Provider, { value: {
326
- editMode,
357
+ mode,
358
+ setMode: updateMode,
327
359
  transformMode,
328
360
  setTransformMode,
329
361
  scaleSnap,
@@ -332,10 +364,10 @@ const PrefabEditor = forwardRef(({ basePath, initialPrefab, physics = true, onCh
332
364
  setPositionSnap,
333
365
  rotationSnap,
334
366
  setRotationSnap,
335
- onFocusNode: editMode ? handleFocusNode : undefined,
367
+ onFocusNode: isEditMode ? handleFocusNode : undefined,
336
368
  onScreenshot: handleScreenshot,
337
369
  onExportGLB: handleExportGLB
338
- }, children: [_jsxs(GameCanvas, Object.assign({ camera: { position: [0, 5, 15] }, canvasRef: canvasRef }, canvasProps, { onPointerMissed: editMode
370
+ }, children: [_jsxs(GameCanvas, Object.assign({ camera: { position: [0, 5, 15] }, canvasRef: canvasRef }, canvasProps, { onPointerMissed: isEditMode
339
371
  ? (event) => {
340
372
  var _a, _b, _c, _d;
341
373
  const button = (_c = (_a = event.button) !== null && _a !== void 0 ? _a : (_b = event.sourceEvent) === null || _b === void 0 ? void 0 : _b.button) !== null && _c !== void 0 ? _c : 0;
@@ -344,7 +376,7 @@ const PrefabEditor = forwardRef(({ basePath, initialPrefab, physics = true, onCh
344
376
  }
345
377
  (_d = canvasProps === null || canvasProps === void 0 ? void 0 : canvasProps.onPointerMissed) === null || _d === void 0 ? void 0 : _d.call(canvasProps, event);
346
378
  }
347
- : canvasProps === null || canvasProps === void 0 ? void 0 : canvasProps.onPointerMissed, children: [physics ? (_jsx(Physics, { debug: editMode, paused: editMode, children: content })) : content, editMode && (_jsxs(_Fragment, { children: [_jsx(MapControls, { ref: controlsRef, makeDefault: true }), selectedObject && (_jsx(TransformControls, { object: selectedObject, mode: transformMode, space: "local", onObjectChange: handleTransformChange, translationSnap: positionSnap > 0 ? positionSnap : undefined, rotationSnap: rotationSnap > 0 ? rotationSnap : undefined, scaleSnap: scaleSnap > 0 ? scaleSnap : undefined }, `transform-${transformMode}-${positionSnap}-${rotationSnap}-${scaleSnap}`))] }))] })), showUI && (_jsxs(_Fragment, { children: [_jsxs("div", { style: toolbar.panel, children: [_jsx("button", { style: base.btn, onClick: toggleEditMode, children: editMode ? "▶" : "⏸" }), uiPlugins] }), editMode && (_jsx(EditorUI, { selectedId: selectedId, setSelectedId: setSelection, getPrefab: getPrefab, onReplacePrefab: (prefab) => loadPrefab(prefab, { resetHistory: true }), onImportPrefab: importPrefab, basePath: basePath, onUndo: undo, onRedo: redo, canUndo: historyIndex > 0, canRedo: historyIndex < history.length - 1 }))] }))] }) });
379
+ : canvasProps === null || canvasProps === void 0 ? void 0 : canvasProps.onPointerMissed, children: [physics ? (_jsx(Physics, { debug: isEditMode, paused: isEditMode, children: content })) : content, isEditMode && (_jsxs(_Fragment, { children: [_jsx(MapControls, { ref: controlsRef, makeDefault: true }), selectedObject && (_jsx(TransformControls, { object: selectedObject, mode: transformMode, space: "local", onObjectChange: handleTransformChange, translationSnap: positionSnap > 0 ? positionSnap : undefined, rotationSnap: rotationSnap > 0 ? rotationSnap : undefined, scaleSnap: scaleSnap > 0 ? scaleSnap : undefined }, `transform-${transformMode}-${positionSnap}-${rotationSnap}-${scaleSnap}`))] }))] })), showUI && (_jsxs(_Fragment, { children: [_jsxs("div", { style: toolbar.panel, children: [_jsx("button", { style: base.btn, onClick: toggleMode, children: isEditMode ? "▶" : "⏸" }), uiPlugins] }), isEditMode && (_jsx(EditorUI, { selectedId: selectedId, setSelectedId: setSelection, getPrefab: getPrefab, onReplacePrefab: (prefab) => loadPrefab(prefab, { resetHistory: true }), onImportPrefab: importPrefab, basePath: basePath, onUndo: undo, onRedo: redo, canUndo: historyIndex > 0, canRedo: historyIndex < history.length - 1 }))] }))] }) });
348
380
  });
349
381
  PrefabEditor.displayName = "PrefabEditor";
350
382
  export default PrefabEditor;
@@ -1,13 +1,36 @@
1
- import { Group, Matrix4, Object3D } from "three";
1
+ import { Group, Matrix4, Object3D, Texture } from "three";
2
2
  import { ThreeEvent } from "@react-three/fiber";
3
3
  import { GameObject as GameObjectType, Prefab } from "./types";
4
- import { LoadedModels, LoadedTextures } from "../dragdrop";
4
+ import { LoadedModels } from "../dragdrop";
5
5
  import { PrefabStoreApi } from "./prefabStore";
6
+ import { type RefBridge } from "./RefBridge";
7
+ /** Runtime context available to all component Views inside a PrefabRoot. */
8
+ export interface SceneRuntime {
9
+ refBridge: RefBridge;
10
+ getRigidBody: (id: string) => any;
11
+ /** @internal Used by PhysicsComponent. */
12
+ registerRigidBodyRef: (id: string, rb: any) => void;
13
+ editMode?: boolean;
14
+ /** Get a loaded model by asset path. */
15
+ getModel: (path: string) => Object3D | null;
16
+ /** Get a loaded texture by asset path. */
17
+ getTexture: (path: string) => Texture | null;
18
+ /** Get a loaded sound buffer by asset path. */
19
+ getSound: (path: string) => AudioBuffer | null;
20
+ /** Get a revision string that changes when loaded assets change (for cache-busting keys). */
21
+ getAssetRevision: () => string;
22
+ }
23
+ /** Access the scene runtime (refBridge, getRigidBody, editMode) from within a component View. */
24
+ export declare function useSceneRuntime(): SceneRuntime;
6
25
  export interface PrefabRootRef {
7
26
  root: Group | null;
27
+ refBridge: RefBridge;
8
28
  rigidBodyRefs: Map<string, any>;
9
29
  getObject: (nodeId: string) => Object3D | null;
10
- focusNode: (nodeId: string) => void;
30
+ getRigidBody: (nodeId: string) => any;
31
+ addModel: (path: string, model: Object3D) => void;
32
+ addTexture: (path: string, texture: Texture) => void;
33
+ addSound: (path: string, sound: AudioBuffer) => void;
11
34
  }
12
35
  export interface PrefabRootProps {
13
36
  editMode?: boolean;
@@ -16,11 +39,7 @@ export interface PrefabRootProps {
16
39
  selectedId?: string | null;
17
40
  onSelect?: (id: string | null) => void;
18
41
  onClick?: (event: ThreeEvent<PointerEvent>, entity: GameObjectType) => void;
19
- onSelectedObjectChange?: (object: Object3D | null) => void;
20
- onFocusNode?: (nodeId: string) => void;
21
42
  basePath?: string;
22
- injectedModels?: LoadedModels;
23
- injectedTextures?: LoadedTextures;
24
43
  }
25
44
  export declare const PrefabRoot: import("react").ForwardRefExoticComponent<PrefabRootProps & import("react").RefAttributes<PrefabRootRef>>;
26
45
  export declare function GameObjectRenderer(props: RendererProps): import("react/jsx-runtime").JSX.Element | null;
@@ -30,9 +49,7 @@ interface RendererProps {
30
49
  onSelect?: (id: string) => void;
31
50
  onClick?: (event: ThreeEvent<PointerEvent>, entity: GameObjectType) => void;
32
51
  registerRef: (id: string, obj: Object3D | null) => void;
33
- registerRigidBodyRef: (id: string, rb: any) => void;
34
52
  loadedModels: LoadedModels;
35
- loadedTextures: LoadedTextures;
36
53
  editMode?: boolean;
37
54
  parentMatrix?: Matrix4;
38
55
  }