react-three-game 0.0.92 → 0.0.93

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 (33) hide show
  1. package/README.md +68 -33
  2. package/dist/helpers/index.d.ts +0 -3
  3. package/dist/helpers/index.js +1 -8
  4. package/dist/index.d.ts +5 -8
  5. package/dist/index.js +3 -4
  6. package/dist/tools/prefabeditor/EditorTree.js +2 -2
  7. package/dist/tools/prefabeditor/GameEvents.d.ts +6 -12
  8. package/dist/tools/prefabeditor/GameEvents.js +0 -8
  9. package/dist/tools/prefabeditor/InstanceProvider.d.ts +6 -4
  10. package/dist/tools/prefabeditor/InstanceProvider.js +84 -199
  11. package/dist/tools/prefabeditor/PrefabEditor.d.ts +18 -6
  12. package/dist/tools/prefabeditor/PrefabEditor.js +55 -39
  13. package/dist/tools/prefabeditor/PrefabRoot.d.ts +15 -8
  14. package/dist/tools/prefabeditor/PrefabRoot.js +141 -117
  15. package/dist/tools/prefabeditor/assetRuntime.d.ts +13 -11
  16. package/dist/tools/prefabeditor/assetRuntime.js +15 -15
  17. package/dist/tools/prefabeditor/components/BufferGeometryComponent.js +1 -1
  18. package/dist/tools/prefabeditor/components/CameraComponent.js +2 -2
  19. package/dist/tools/prefabeditor/components/ComponentRegistry.d.ts +3 -3
  20. package/dist/tools/prefabeditor/components/DirectionalLightComponent.js +2 -2
  21. package/dist/tools/prefabeditor/components/ModelComponent.js +1 -1
  22. package/dist/tools/prefabeditor/components/PointLightComponent.js +2 -2
  23. package/dist/tools/prefabeditor/components/SoundComponent.js +2 -2
  24. package/dist/tools/prefabeditor/components/SpotLightComponent.js +2 -2
  25. package/dist/tools/prefabeditor/components/index.js +0 -2
  26. package/dist/tools/prefabeditor/types.d.ts +1 -0
  27. package/dist/tools/prefabeditor/usePointerEvents.d.ts +3 -3
  28. package/dist/tools/prefabeditor/usePointerEvents.js +5 -5
  29. package/package.json +1 -3
  30. package/dist/tools/prefabeditor/components/PhysicsComponent.d.ts +0 -26
  31. package/dist/tools/prefabeditor/components/PhysicsComponent.js +0 -302
  32. package/dist/tools/prefabeditor/scene.d.ts +0 -70
  33. package/dist/tools/prefabeditor/scene.js +0 -237
@@ -1,20 +1,9 @@
1
- var __rest = (this && this.__rest) || function (s, e) {
2
- var t = {};
3
- for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
4
- t[p] = s[p];
5
- if (s != null && typeof Object.getOwnPropertySymbols === "function")
6
- for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
7
- if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
8
- t[p[i]] = s[p[i]];
9
- }
10
- return t;
11
- };
12
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
13
- import React, { createContext, useContext, useMemo, useRef, useState, useEffect, useCallback } from "react";
2
+ import React, { createContext, useContext, useMemo, useRef, useState, useEffect } from "react";
14
3
  import { Merged, useHelper } from '@react-three/drei';
15
- import { InstancedRigidBodies } from "@react-three/rapier";
16
- import { ActiveCollisionTypes } from "@dimforge/rapier3d-compat";
17
- import { Mesh, Matrix4, Vector3, Quaternion, Euler, BoxHelper } from "three";
4
+ import { Mesh, Matrix4, BoxHelper } from "three";
5
+ import { useStore } from "zustand";
6
+ import { createStore } from "zustand/vanilla";
18
7
  import { usePointerEvents } from "./usePointerEvents";
19
8
  export const DEFAULT_REPEAT_AXES = [{ axis: 'x', count: 1, offset: 1 }];
20
9
  export function normalizeRepeatAxes(value) {
@@ -81,66 +70,67 @@ function arrayEquals(a, b) {
81
70
  }
82
71
  return true;
83
72
  }
84
- function stableSerialize(value) {
85
- if (Array.isArray(value)) {
86
- return `[${value.map(stableSerialize).join(',')}]`;
87
- }
88
- if (value && typeof value === 'object') {
89
- const entries = Object.entries(value)
90
- .sort(([a], [b]) => a.localeCompare(b))
91
- .map(([key, entry]) => `${key}:${stableSerialize(entry)}`);
92
- return `{${entries.join(',')}}`;
93
- }
94
- return JSON.stringify(value);
95
- }
96
- function getPhysicsSignature(physics) {
97
- return physics ? stableSerialize(physics) : 'none';
98
- }
99
- function hasPhysics(instance) {
100
- return Boolean(instance.physics);
101
- }
102
- function getColliderType(physics) {
103
- return physics.colliders || (physics.type === 'fixed' ? 'trimesh' : 'hull');
104
- }
105
73
  function instanceEquals(a, b) {
106
74
  return a.id === b.id &&
107
75
  a.sourceId === b.sourceId &&
108
76
  a.locked === b.locked &&
77
+ a.visible === b.visible &&
109
78
  a.meshPath === b.meshPath &&
110
79
  arrayEquals(a.position, b.position) &&
111
80
  arrayEquals(a.rotation, b.rotation) &&
112
- arrayEquals(a.scale, b.scale) &&
113
- getPhysicsSignature(a.physics) === getPhysicsSignature(b.physics);
81
+ arrayEquals(a.scale, b.scale);
114
82
  }
115
- const GameInstanceContext = createContext(null);
116
- export function GameInstanceProvider({ children, models, onSelect, registerRef, selectedId, editMode }) {
117
- const [instances, setInstances] = useState([]);
118
- const addInstance = useCallback((instance) => {
119
- setInstances(prev => {
120
- const idx = prev.findIndex(i => i.id === instance.id);
121
- if (idx !== -1) {
122
- // Update existing if changed
123
- if (instanceEquals(prev[idx], instance)) {
124
- return prev;
125
- }
126
- const copy = [...prev];
127
- copy[idx] = instance;
128
- return copy;
83
+ function createInstanceRegistryStore() {
84
+ return createStore()((set, get) => ({
85
+ instancesById: {},
86
+ sourceInstanceIdsById: {},
87
+ addInstance: (instance) => {
88
+ const previous = get().instancesById[instance.id];
89
+ if (previous && instanceEquals(previous, instance)) {
90
+ return;
129
91
  }
130
- // Add new
131
- return [...prev, instance];
132
- });
133
- }, []);
134
- const removeInstance = useCallback((id) => {
135
- setInstances(prev => {
136
- if (!prev.find(i => i.id === id))
137
- return prev;
138
- return prev.filter(i => i.id !== id);
139
- });
140
- }, []);
141
- const hasInstance = useCallback((id) => {
142
- return instances.some(i => i.id === id || i.sourceId === id);
143
- }, [instances]);
92
+ set(state => {
93
+ var _a, _b;
94
+ const instancesById = Object.assign(Object.assign({}, state.instancesById), { [instance.id]: instance });
95
+ const sourceInstanceIdsById = Object.assign({}, state.sourceInstanceIdsById);
96
+ if (previous && previous.sourceId !== previous.id) {
97
+ const previousSourceInstances = Object.assign({}, ((_a = sourceInstanceIdsById[previous.sourceId]) !== null && _a !== void 0 ? _a : {}));
98
+ delete previousSourceInstances[previous.id];
99
+ sourceInstanceIdsById[previous.sourceId] = Object.keys(previousSourceInstances).length > 0
100
+ ? previousSourceInstances
101
+ : undefined;
102
+ }
103
+ if (instance.sourceId !== instance.id) {
104
+ sourceInstanceIdsById[instance.sourceId] = Object.assign(Object.assign({}, ((_b = sourceInstanceIdsById[instance.sourceId]) !== null && _b !== void 0 ? _b : {})), { [instance.id]: true });
105
+ }
106
+ return { instancesById, sourceInstanceIdsById };
107
+ });
108
+ },
109
+ removeInstance: (id) => {
110
+ const previous = get().instancesById[id];
111
+ if (!previous)
112
+ return;
113
+ set(state => {
114
+ var _a;
115
+ const instancesById = Object.assign({}, state.instancesById);
116
+ const sourceInstanceIdsById = Object.assign({}, state.sourceInstanceIdsById);
117
+ delete instancesById[id];
118
+ if (previous.sourceId !== previous.id) {
119
+ const sourceInstances = Object.assign({}, ((_a = sourceInstanceIdsById[previous.sourceId]) !== null && _a !== void 0 ? _a : {}));
120
+ delete sourceInstances[id];
121
+ sourceInstanceIdsById[previous.sourceId] = Object.keys(sourceInstances).length > 0
122
+ ? sourceInstances
123
+ : undefined;
124
+ }
125
+ return { instancesById, sourceInstanceIdsById };
126
+ });
127
+ },
128
+ }));
129
+ }
130
+ const GameInstanceContext = createContext(null);
131
+ export function GameInstanceProvider({ children, models, onSelect, onClick, registerRef, selectedId, editMode }) {
132
+ const [instanceStore] = useState(createInstanceRegistryStore);
133
+ const instancesById = useStore(instanceStore, state => state.instancesById);
144
134
  // Flatten all model meshes once (models → flat mesh parts)
145
135
  // Note: Geometry is cloned with baked transforms for instancing
146
136
  const { flatMeshes, modelParts } = useMemo(() => {
@@ -170,13 +160,14 @@ export function GameInstanceProvider({ children, models, onSelect, registerRef,
170
160
  Object.values(flatMeshes).forEach(mesh => mesh.geometry.dispose());
171
161
  };
172
162
  }, [flatMeshes]);
173
- // Group instances by meshPath and physics presence for batch rendering.
163
+ const instances = useMemo(() => Object.values(instancesById), [instancesById]);
164
+ // Group instances by meshPath for batched rendering.
174
165
  const grouped = useMemo(() => {
175
166
  const groups = {};
176
167
  for (const inst of instances) {
177
- const key = `${inst.meshPath}__${inst.physics ? 'physics' : 'visual'}`;
168
+ const key = inst.meshPath;
178
169
  if (!groups[key])
179
- groups[key] = { hasPhysics: Boolean(inst.physics), instances: [] };
170
+ groups[key] = { instances: [] };
180
171
  groups[key].instances.push(inst);
181
172
  }
182
173
  Object.values(groups).forEach(group => {
@@ -184,146 +175,45 @@ export function GameInstanceProvider({ children, models, onSelect, registerRef,
184
175
  });
185
176
  return groups;
186
177
  }, [instances]);
187
- return (_jsxs(GameInstanceContext.Provider, { value: {
188
- addInstance,
189
- removeInstance,
190
- instances,
191
- meshes: flatMeshes,
192
- modelParts,
193
- hasInstance
194
- }, children: [children, Object.entries(grouped).map(([key, group]) => {
195
- if (!group.hasPhysics)
196
- return null;
178
+ const contextValue = useMemo(() => ({
179
+ store: instanceStore,
180
+ meshes: flatMeshes,
181
+ modelParts,
182
+ }), [instanceStore, flatMeshes, modelParts]);
183
+ return (_jsxs(GameInstanceContext.Provider, { value: contextValue, children: [children, Object.entries(grouped).map(([key, group]) => {
197
184
  const modelKey = group.instances[0].meshPath;
198
185
  const partCount = modelParts[modelKey] || 0;
199
186
  if (partCount === 0)
200
187
  return null;
201
- return (_jsx(InstancedRigidGroup, { group: group, modelKey: modelKey, partCount: partCount, flatMeshes: flatMeshes, onSelect: onSelect, editMode: editMode }, key));
202
- }), Object.entries(grouped).map(([key, group]) => {
203
- if (group.hasPhysics)
204
- return null;
205
- const modelKey = group.instances[0].meshPath;
206
- const partCount = modelParts[modelKey] || 0;
207
- if (partCount === 0)
208
- return null;
209
- // Create mesh subset for this specific model
210
188
  const meshesForModel = {};
211
189
  for (let i = 0; i < partCount; i++) {
212
190
  const partKey = `${modelKey}__${i}`;
213
191
  meshesForModel[partKey] = flatMeshes[partKey];
214
192
  }
215
- return (_jsx(Merged, { meshes: meshesForModel, castShadow: true, receiveShadow: true, children: (instancesMap) => (_jsx(NonPhysicsInstancedGroup, { modelKey: modelKey, group: group, partCount: partCount, instancesMap: instancesMap, onSelect: onSelect, registerRef: registerRef, selectedId: selectedId, editMode: editMode })) }, key));
193
+ return (_jsx(Merged, { meshes: meshesForModel, castShadow: true, receiveShadow: true, children: (instancesMap) => (_jsx(InstancedGroup, { modelKey: modelKey, group: group, partCount: partCount, instancesMap: instancesMap, onSelect: onSelect, onClick: onClick, registerRef: registerRef, selectedId: selectedId, editMode: editMode })) }, key));
216
194
  })] }));
217
195
  }
218
- // Render physics-enabled instances using InstancedRigidBodies
219
- function InstancedRigidGroup({ group, modelKey, partCount, flatMeshes, onSelect, editMode }) {
220
- const meshRefs = useRef([]);
221
- const rigidBodiesRef = useRef(null);
222
- const instances = useMemo(() => group.instances.filter(hasPhysics).map(inst => {
223
- const _a = inst.physics, { activeCollisionTypes: _activeCollisionTypes, colliders: _colliders, userData } = _a, rigidBodyProps = __rest(_a, ["activeCollisionTypes", "colliders", "userData"]);
224
- 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 }) });
225
- }), [group.instances]);
226
- // Apply scale to visual meshes (InstancedRigidBodies only scales colliders, not visuals)
227
- useEffect(() => {
228
- const matrix = new Matrix4();
229
- const pos = new Vector3();
230
- const quat = new Quaternion();
231
- const euler = new Euler();
232
- const scl = new Vector3();
233
- meshRefs.current.forEach(mesh => {
234
- if (!mesh)
235
- return;
236
- group.instances.forEach((inst, i) => {
237
- pos.set(...inst.position);
238
- euler.set(...inst.rotation);
239
- quat.setFromEuler(euler);
240
- scl.set(...inst.scale);
241
- matrix.compose(pos, quat, scl);
242
- mesh.setMatrixAt(i, matrix);
243
- });
244
- mesh.instanceMatrix.needsUpdate = true;
245
- });
246
- // Update rigid body positions when instances change
247
- if (rigidBodiesRef.current) {
248
- try {
249
- group.instances.forEach((inst, i) => {
250
- var _a;
251
- const body = (_a = rigidBodiesRef.current) === null || _a === void 0 ? void 0 : _a[i];
252
- if (body && body.setTranslation && body.setRotation) {
253
- pos.set(...inst.position);
254
- euler.set(...inst.rotation);
255
- quat.setFromEuler(euler);
256
- body.setTranslation(pos, false);
257
- body.setRotation(quat, false);
258
- }
259
- });
260
- }
261
- catch (error) {
262
- // Ignore errors when switching between instanced/non-instanced states
263
- console.warn('Failed to update rigidbody positions:', error);
264
- }
265
- }
266
- }, [group.instances]);
267
- useEffect(() => {
268
- group.instances.forEach((inst, i) => {
269
- var _a, _b;
270
- if (!inst.physics || inst.physics.activeCollisionTypes !== 'all')
271
- return;
272
- const body = (_a = rigidBodiesRef.current) === null || _a === void 0 ? void 0 : _a[i];
273
- if (!body || !body.numColliders || !body.collider)
274
- return;
275
- for (let colliderIndex = 0; colliderIndex < body.numColliders(); colliderIndex++) {
276
- const collider = body.collider(colliderIndex);
277
- (_b = collider.setActiveCollisionTypes) === null || _b === void 0 ? void 0 : _b.call(collider, ActiveCollisionTypes.DEFAULT |
278
- ActiveCollisionTypes.KINEMATIC_FIXED |
279
- ActiveCollisionTypes.KINEMATIC_KINEMATIC);
280
- }
281
- });
282
- }, [group.instances]);
283
- // Handle click on instanced mesh in edit mode
284
- const handleClick = (e) => {
285
- const instanceId = e.instanceId;
286
- const instance = instanceId !== undefined ? group.instances[instanceId] : undefined;
287
- if (!instance)
288
- return;
289
- if (editMode) {
290
- if (!onSelect || instance.locked)
291
- return;
292
- e.stopPropagation();
293
- onSelect(instance.sourceId);
294
- return;
295
- }
296
- };
297
- const shouldHandleClick = editMode;
298
- // Add key to force remount when instance count changes significantly (helps with cleanup)
299
- const rigidBodyKey = `rb_${modelKey}_${group.instances.map(inst => `${inst.id}:${getPhysicsSignature(inst.physics)}`).join('|')}`;
300
- return (_jsx(InstancedRigidBodies, { ref: rigidBodiesRef, instances: instances, children: Array.from({ length: partCount }).map((_, i) => {
301
- const mesh = flatMeshes[`${modelKey}__${i}`];
302
- if (!mesh)
303
- return null;
304
- return (_jsx("instancedMesh", { ref: el => { meshRefs.current[i] = el; }, args: [mesh.geometry, mesh.material, group.instances.length], castShadow: true, receiveShadow: true, frustumCulled: false, onClick: shouldHandleClick ? handleClick : undefined }, i));
305
- }) }, rigidBodyKey));
306
- }
307
- // Render non-physics instances using Merged (instancing without rigid bodies)
308
- function NonPhysicsInstancedGroup({ modelKey, group, partCount, instancesMap, onSelect, registerRef, selectedId, editMode }) {
309
- // Pre-compute which Instance components exist for this model
196
+ function InstancedGroup({ modelKey, group, partCount, instancesMap, onSelect, onClick, registerRef, selectedId, editMode }) {
310
197
  const InstanceComponents = useMemo(() => Array.from({ length: partCount }, (_, i) => instancesMap[`${modelKey}__${i}`]).filter(Boolean), [instancesMap, modelKey, partCount]);
311
- return (_jsx(_Fragment, { children: group.instances.map(inst => (_jsx(InstanceGroupItem, { instance: inst, InstanceComponents: InstanceComponents, onSelect: onSelect, registerRef: registerRef, selectedId: selectedId, editMode: editMode }, inst.id))) }));
198
+ const visibleInstances = useMemo(() => group.instances.filter(instance => instance.visible !== false), [group.instances]);
199
+ return (_jsx(_Fragment, { children: visibleInstances.map(inst => (_jsx(InstanceGroupItem, { instance: inst, InstanceComponents: InstanceComponents, onSelect: onSelect, onClick: onClick, registerRef: registerRef, selectedId: selectedId, editMode: editMode }, inst.id))) }));
312
200
  }
313
201
  // Individual instance item with its own click state
314
- function InstanceGroupItem({ instance, InstanceComponents, onSelect, registerRef, selectedId, editMode }) {
202
+ function InstanceGroupItem({ instance, InstanceComponents, onSelect, onClick, registerRef, selectedId, editMode }) {
315
203
  const groupRef = useRef(null);
316
204
  const isLocked = Boolean(instance.locked);
317
205
  const isSelected = selectedId === instance.id || selectedId === instance.sourceId;
318
206
  const canSelect = editMode && !isLocked;
319
- const canClick = false;
207
+ const canClick = !editMode && Boolean(onClick);
320
208
  const pointerHandlers = usePointerEvents({
321
209
  enabled: canSelect || canClick,
322
- entity: instance,
323
- onClick: () => {
210
+ node: instance,
211
+ onClick: (event) => {
324
212
  if (editMode) {
325
213
  onSelect === null || onSelect === void 0 ? void 0 : onSelect(instance.sourceId);
214
+ return;
326
215
  }
216
+ onClick === null || onClick === void 0 ? void 0 : onClick(event, instance.sourceId, groupRef.current);
327
217
  },
328
218
  });
329
219
  // Use BoxHelper when object is selected in edit mode
@@ -336,34 +226,29 @@ function InstanceGroupItem({ instance, InstanceComponents, onSelect, registerRef
336
226
  }, [editMode, instance.id, registerRef]);
337
227
  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)) })));
338
228
  }
339
- // Hook to check if an instance exists
340
229
  export function useInstanceCheck(id) {
341
- var _a;
342
230
  const ctx = useContext(GameInstanceContext);
343
- return (_a = ctx === null || ctx === void 0 ? void 0 : ctx.hasInstance(id)) !== null && _a !== void 0 ? _a : false;
231
+ return ctx ? useStore(ctx.store, state => Boolean(state.instancesById[id] || state.sourceInstanceIdsById[id])) : false;
344
232
  }
345
- // GameInstance component: registers an instance for batch rendering (renders nothing itself)
346
- export const GameInstance = React.forwardRef(({ id, sourceId, modelUrl, locked = false, position, rotation, scale, physics = undefined, }, ref) => {
233
+ export const GameInstance = React.forwardRef(({ id, sourceId, modelUrl, locked = false, position, rotation, scale, visible = true, onClick: _onClick, }, ref) => {
347
234
  const ctx = useContext(GameInstanceContext);
348
- const addInstance = ctx === null || ctx === void 0 ? void 0 : ctx.addInstance;
349
- const removeInstance = ctx === null || ctx === void 0 ? void 0 : ctx.removeInstance;
350
235
  const [positionX, positionY, positionZ] = position;
351
236
  const [rotationX, rotationY, rotationZ] = rotation;
352
237
  const [scaleX, scaleY, scaleZ] = scale;
353
- const physicsSignature = getPhysicsSignature(physics);
354
238
  const instance = useMemo(() => ({
355
239
  id,
356
240
  sourceId: sourceId !== null && sourceId !== void 0 ? sourceId : id,
357
241
  locked,
242
+ visible,
358
243
  meshPath: modelUrl,
359
244
  position,
360
245
  rotation,
361
246
  scale,
362
- physics,
363
247
  }), [
364
248
  id,
365
249
  sourceId,
366
250
  locked,
251
+ visible,
367
252
  modelUrl,
368
253
  positionX,
369
254
  positionY,
@@ -374,16 +259,16 @@ export const GameInstance = React.forwardRef(({ id, sourceId, modelUrl, locked =
374
259
  scaleX,
375
260
  scaleY,
376
261
  scaleZ,
377
- physicsSignature,
378
262
  ]);
379
263
  useEffect(() => {
380
- if (!addInstance || !removeInstance)
264
+ if (!ctx)
381
265
  return;
266
+ const store = ctx.store;
267
+ const { addInstance, removeInstance } = store.getState();
382
268
  addInstance(instance);
383
269
  return () => {
384
270
  removeInstance(instance.id);
385
271
  };
386
- }, [addInstance, removeInstance, instance]);
387
- // No visual rendering - provider handles all instanced visuals
272
+ }, [ctx === null || ctx === void 0 ? void 0 : ctx.store, instance]);
388
273
  return null;
389
274
  });
@@ -2,13 +2,12 @@ import GameCanvas from "../../shared/GameCanvas";
2
2
  import { Object3D, Texture } from "three";
3
3
  import { GameObject, Prefab } from "./types";
4
4
  import type { ExportGLBOptions } from "./utils";
5
- import { type PrefabStoreApi } from "./prefabStore";
6
- import type { SpawnOptions } from "./scene";
7
5
  export interface PrefabEditorRef {
8
6
  root: Object3D | null;
9
- store: PrefabStoreApi;
10
- getObject: (nodeId: string) => Object3D | null;
11
- getRigidBody: (nodeId: string) => any;
7
+ getNode: (nodeId: string) => PrefabNode | null;
8
+ getNodeObject: (nodeId: string) => Object3D | null;
9
+ getNodeHandle: <T = unknown>(nodeId: string, kind: string) => T | null;
10
+ onSceneChange: (listener: (revision: number) => void) => () => void;
12
11
  screenshot: () => void;
13
12
  exportGLB: (options?: ExportGLBOptions) => Promise<ArrayBuffer | undefined>;
14
13
  exportGLBData: () => Promise<ArrayBuffer | undefined>;
@@ -18,10 +17,24 @@ export interface PrefabEditorRef {
18
17
  resetHistory?: boolean;
19
18
  notifyChange?: boolean;
20
19
  }) => void;
20
+ updateNode: (nodeId: string, update: (node: PrefabNode) => PrefabNode) => void;
21
+ updateNodes: (updates: Array<{
22
+ id: string;
23
+ update: (node: PrefabNode) => PrefabNode;
24
+ }>) => void;
25
+ deleteNode: (nodeId: string) => void;
26
+ duplicateNode: (nodeId: string) => string | null;
27
+ moveNode: (draggedId: string, targetId: string, position: "before" | "inside") => void;
21
28
  addNode: (node: GameObject, options?: SpawnOptions) => GameObject;
22
29
  addModel: (path: string, model: Object3D, options?: SpawnOptions) => GameObject;
23
30
  addTexture: (path: string, texture: Texture, options?: SpawnOptions) => GameObject;
24
31
  }
32
+ export interface SpawnOptions {
33
+ name?: string;
34
+ parentId?: string;
35
+ select?: boolean;
36
+ }
37
+ export type PrefabNode = Omit<GameObject, "children">;
25
38
  export declare enum PrefabEditorMode {
26
39
  Edit = "edit",
27
40
  Play = "play"
@@ -46,7 +59,6 @@ export declare function useEditorContext(): EditorContextType;
46
59
  export interface PrefabEditorProps {
47
60
  basePath?: string;
48
61
  initialPrefab?: Prefab;
49
- physics?: boolean;
50
62
  mode?: PrefabEditorMode;
51
63
  onChange?: (prefab: Prefab) => void;
52
64
  showUI?: boolean;
@@ -10,11 +10,10 @@ 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, useHelper } from "@react-three/drei";
12
12
  import GameCanvas from "../../shared/GameCanvas";
13
- import { useCallback, useEffect, useLayoutEffect, useRef, useState, forwardRef, useImperativeHandle, createContext, useContext } from "react";
13
+ import { useCallback, useEffect, useRef, useState, forwardRef, useImperativeHandle, createContext, useContext } from "react";
14
14
  import { BoxHelper } from "three";
15
15
  import { findComponentEntry } from "./types";
16
- import PrefabRoot from "./PrefabRoot";
17
- import { Physics } from "@react-three/rapier";
16
+ import { PrefabRootInternal } from "./PrefabRoot";
18
17
  import EditorUI from "./EditorUI";
19
18
  import { base, toolbar } from "./styles";
20
19
  import { computeParentWorldMatrix, decompose, exportGLB as exportGLBFile, exportGLBData, focusCameraOnObject, regenerateIds } from "./utils";
@@ -58,8 +57,7 @@ const DEFAULT_PREFAB = {
58
57
  name: "New Prefab",
59
58
  root: createNode('Root', {}, { id: 'root' })
60
59
  };
61
- const PrefabEditor = forwardRef(({ basePath, initialPrefab, physics = true, mode: initialMode = PrefabEditorMode.Edit, onChange, showUI = true, enableWindowDrop = true, canvasProps, uiPlugins, children }, ref) => {
62
- var _a, _b;
60
+ const PrefabEditor = forwardRef(({ basePath, initialPrefab, mode: initialMode = PrefabEditorMode.Edit, onChange, showUI = true, enableWindowDrop = true, canvasProps, uiPlugins, children }, ref) => {
63
61
  const [mode, setMode] = useState(initialMode);
64
62
  const [selectedId, setSelectedId] = useState(null);
65
63
  const [transformMode, setTransformMode] = useState("translate");
@@ -76,13 +74,29 @@ const PrefabEditor = forwardRef(({ basePath, initialPrefab, physics = true, mode
76
74
  const canvasRef = useRef(null);
77
75
  const controlsRef = useRef(null);
78
76
  const transformControlsRef = useRef(null);
79
- const transformProxyRef = useRef(null);
80
77
  const onChangeRef = useRef(onChange);
78
+ const sceneChangeListenersRef = useRef(new Set());
81
79
  const isEditMode = mode === PrefabEditorMode.Edit;
82
80
  const getPrefab = useCallback(() => denormalizePrefab(prefabStore.getState()), [prefabStore]);
83
- const getRootObject = useCallback(() => { var _a, _b; return (_b = (_a = prefabRootRef.current) === null || _a === void 0 ? void 0 : _a.root) !== null && _b !== void 0 ? _b : null; }, []);
84
- const getObject = useCallback((nodeId) => { var _a, _b; return (_b = (_a = prefabRootRef.current) === null || _a === void 0 ? void 0 : _a.getObject(nodeId)) !== null && _b !== void 0 ? _b : null; }, []);
85
- const getRigidBody = useCallback((nodeId) => { var _a, _b; return (_b = (_a = prefabRootRef.current) === null || _a === void 0 ? void 0 : _a.getRigidBody(nodeId)) !== null && _b !== void 0 ? _b : null; }, []);
81
+ const getNode = useCallback((nodeId) => { var _a; return (_a = prefabStore.getState().nodesById[nodeId]) !== null && _a !== void 0 ? _a : null; }, [prefabStore]);
82
+ const getSceneRootObject = useCallback(() => { var _a, _b; return (_b = (_a = prefabRootRef.current) === null || _a === void 0 ? void 0 : _a.root) !== null && _b !== void 0 ? _b : null; }, []);
83
+ const getNodeObject = useCallback((nodeId) => { var _a, _b; return (_b = (_a = prefabRootRef.current) === null || _a === void 0 ? void 0 : _a.getNodeObject(nodeId)) !== null && _b !== void 0 ? _b : null; }, []);
84
+ const getNodeHandle = useCallback((nodeId, kind) => { var _a, _b; return (_b = (_a = prefabRootRef.current) === null || _a === void 0 ? void 0 : _a.getNodeHandle(nodeId, kind)) !== null && _b !== void 0 ? _b : null; }, []);
85
+ const updateNode = useCallback((nodeId, update) => {
86
+ prefabStore.getState().updateNode(nodeId, update);
87
+ }, [prefabStore]);
88
+ const updateNodes = useCallback((updates) => {
89
+ prefabStore.getState().updateNodes(updates);
90
+ }, [prefabStore]);
91
+ const deleteNode = useCallback((nodeId) => {
92
+ prefabStore.getState().deleteNode(nodeId);
93
+ }, [prefabStore]);
94
+ const duplicateNode = useCallback((nodeId) => {
95
+ return prefabStore.getState().duplicateNode(nodeId);
96
+ }, [prefabStore]);
97
+ const moveNode = useCallback((draggedId, targetId, position) => {
98
+ prefabStore.getState().moveNode(draggedId, targetId, position);
99
+ }, [prefabStore]);
86
100
  onChangeRef.current = onChange;
87
101
  const setSelection = useCallback((nodeId) => {
88
102
  const nextNode = nodeId ? prefabStore.getState().nodesById[nodeId] : null;
@@ -135,6 +149,7 @@ const PrefabEditor = forwardRef(({ basePath, initialPrefab, physics = true, mode
135
149
  return;
136
150
  }
137
151
  lastRevision = state.revision;
152
+ sceneChangeListenersRef.current.forEach(listener => listener(state.revision));
138
153
  const nextPrefab = denormalizePrefab(state);
139
154
  const changeOrigin = changeOriginRef.current;
140
155
  if (changeOrigin !== "replace-silent") {
@@ -177,23 +192,11 @@ const PrefabEditor = forwardRef(({ basePath, initialPrefab, physics = true, mode
177
192
  });
178
193
  return () => unsubscribe();
179
194
  }, [prefabStore, selectedId]);
180
- const selectedNode = selectedId ? (_a = prefabStore.getState().nodesById[selectedId]) !== null && _a !== void 0 ? _a : null : null;
181
- const selectedObject = selectedId ? getObject(selectedId) : null;
182
- const selectedHasPhysics = Object.values((_b = selectedNode === null || selectedNode === void 0 ? void 0 : selectedNode.components) !== null && _b !== void 0 ? _b : {}).some(component => (component === null || component === void 0 ? void 0 : component.type) === "Physics");
183
- const transformObject = isEditMode && (selectedHasPhysics ? transformProxyRef.current : selectedObject)
184
- && isObjectAttachedToRoot(getRootObject(), selectedObject)
185
- ? (selectedHasPhysics ? transformProxyRef.current : selectedObject)
195
+ const selectedObject = selectedId ? getNodeObject(selectedId) : null;
196
+ const transformObject = isEditMode && selectedObject
197
+ && isObjectAttachedToRoot(getSceneRootObject(), selectedObject)
198
+ ? selectedObject
186
199
  : null;
187
- useLayoutEffect(() => {
188
- if (!isEditMode || !selectedHasPhysics || !selectedObject || !transformProxyRef.current) {
189
- return;
190
- }
191
- selectedObject.updateMatrixWorld(true);
192
- transformProxyRef.current.matrixAutoUpdate = true;
193
- selectedObject.matrixWorld.decompose(transformProxyRef.current.position, transformProxyRef.current.quaternion, transformProxyRef.current.scale);
194
- transformProxyRef.current.updateMatrix();
195
- transformProxyRef.current.updateMatrixWorld(true);
196
- }, [isEditMode, selectedHasPhysics, selectedId, selectedObject]);
197
200
  const addNode = useCallback((node, options) => {
198
201
  var _a;
199
202
  const { addChild, rootId } = prefabStore.getState();
@@ -272,30 +275,30 @@ const PrefabEditor = forwardRef(({ basePath, initialPrefab, physics = true, mode
272
275
  }), [setSelection]);
273
276
  const handleExportGLB = useCallback((...args_1) => __awaiter(void 0, [...args_1], void 0, function* (options = {}) {
274
277
  yield clearSelection();
275
- const rootObject = getRootObject();
278
+ const rootObject = getSceneRootObject();
276
279
  if (!rootObject)
277
280
  return;
278
281
  return exportGLBFile(rootObject, Object.assign({ filename: `${prefabStore.getState().prefabName || 'prefab'}.glb` }, options));
279
- }), [clearSelection, getRootObject, prefabStore]);
282
+ }), [clearSelection, getSceneRootObject, prefabStore]);
280
283
  const handleExportGLBData = useCallback(() => __awaiter(void 0, void 0, void 0, function* () {
281
284
  yield clearSelection();
282
- const rootObject = getRootObject();
285
+ const rootObject = getSceneRootObject();
283
286
  if (!rootObject)
284
287
  return;
285
288
  return exportGLBData(rootObject);
286
- }), [clearSelection, getRootObject]);
289
+ }), [clearSelection, getSceneRootObject]);
287
290
  const handleFocusNode = useCallback((nodeId) => {
288
- const object = getObject(nodeId);
291
+ const object = getNodeObject(nodeId);
289
292
  const controls = controlsRef.current;
290
293
  const camera = controls === null || controls === void 0 ? void 0 : controls.object;
291
294
  if (!object || !controls || !camera)
292
295
  return;
293
296
  focusCameraOnObject(object, camera, controls.target, () => { var _a; return (_a = controls.update) === null || _a === void 0 ? void 0 : _a.call(controls); });
294
- }, [getObject]);
297
+ }, [getNodeObject]);
295
298
  const handleTransformChange = () => {
296
299
  if (!selectedId)
297
300
  return;
298
- const object = selectedHasPhysics ? transformProxyRef.current : getObject(selectedId);
301
+ const object = getNodeObject(selectedId);
299
302
  if (!object)
300
303
  return;
301
304
  const parentWorld = computeParentWorldMatrix(prefabStore.getState(), selectedId);
@@ -348,21 +351,34 @@ const PrefabEditor = forwardRef(({ basePath, initialPrefab, physics = true, mode
348
351
  };
349
352
  }, [addModel, addTexture, isEditMode, enableWindowDrop]);
350
353
  useImperativeHandle(ref, () => ({
351
- root: getRootObject(),
352
- store: prefabStore,
353
- getObject,
354
- getRigidBody,
354
+ get root() {
355
+ return getSceneRootObject();
356
+ },
357
+ getNode,
358
+ getNodeObject,
359
+ getNodeHandle,
360
+ onSceneChange: (listener) => {
361
+ sceneChangeListenersRef.current.add(listener);
362
+ return () => {
363
+ sceneChangeListenersRef.current.delete(listener);
364
+ };
365
+ },
355
366
  screenshot: handleScreenshot,
356
367
  exportGLB: handleExportGLB,
357
368
  exportGLBData: handleExportGLBData,
358
369
  clearSelection,
359
370
  save: getPrefab,
360
371
  load: loadPrefab,
372
+ updateNode,
373
+ updateNodes,
374
+ deleteNode,
375
+ duplicateNode,
376
+ moveNode,
361
377
  addNode,
362
378
  addModel,
363
379
  addTexture
364
- }), [addModel, addNode, addTexture, clearSelection, getObject, getPrefab, getRigidBody, getRootObject, handleExportGLB, handleExportGLBData, handleScreenshot, loadPrefab, prefabStore]);
365
- const content = (_jsxs(_Fragment, { children: [isEditMode ? _jsx("gridHelper", { args: [10, 10], position: [0, -1, 0] }) : null, _jsx(PrefabRoot, { ref: prefabRootRef, store: prefabStore, editMode: isEditMode, selectedId: selectedId, onSelect: setSelection, basePath: basePath }), children] }));
380
+ }), [addModel, addNode, addTexture, clearSelection, deleteNode, duplicateNode, getNode, getNodeHandle, getNodeObject, getPrefab, getSceneRootObject, handleExportGLB, handleExportGLBData, handleScreenshot, loadPrefab, moveNode, updateNode, updateNodes]);
381
+ const content = (_jsxs(_Fragment, { children: [isEditMode ? _jsx("gridHelper", { args: [10, 10], position: [0, -1, 0] }) : null, _jsx(PrefabRootInternal, { ref: prefabRootRef, store: prefabStore, editMode: isEditMode, selectedId: selectedId, onSelect: setSelection, basePath: basePath }), children] }));
366
382
  const handleCanvasCreated = useCallback((state) => {
367
383
  var _a;
368
384
  canvasRef.current = state.gl.domElement;
@@ -391,7 +407,7 @@ const PrefabEditor = forwardRef(({ basePath, initialPrefab, physics = true, mode
391
407
  }
392
408
  (_d = canvasProps === null || canvasProps === void 0 ? void 0 : canvasProps.onPointerMissed) === null || _d === void 0 ? void 0 : _d.call(canvasProps, event);
393
409
  }
394
- : canvasProps === null || canvasProps === void 0 ? void 0 : canvasProps.onPointerMissed, children: [physics ? (_jsx(Physics, { colliders: false, debug: isEditMode, paused: isEditMode, children: content })) : content, _jsx("group", { ref: transformProxyRef, visible: false }), isEditMode ? _jsx(SelectionHelper, { object: transformObject }) : null, isEditMode && (_jsxs(_Fragment, { children: [_jsx(MapControls, { ref: controlsRef, enableDamping: false, makeDefault: true }), transformObject && (_jsx(TransformControls, { ref: transformControlsRef, object: transformObject, mode: transformMode, space: "local", onObjectChange: handleTransformChange, translationSnap: positionSnap > 0 ? positionSnap : undefined, rotationSnap: rotationSnap > 0 ? rotationSnap : undefined, scaleSnap: scaleSnap > 0 ? scaleSnap : undefined }, `transform-${selectedId}-${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 }))] }))] }) });
410
+ : canvasProps === null || canvasProps === void 0 ? void 0 : canvasProps.onPointerMissed, children: [content, isEditMode ? _jsx(SelectionHelper, { object: transformObject }) : null, isEditMode && (_jsxs(_Fragment, { children: [_jsx(MapControls, { ref: controlsRef, enableDamping: false, makeDefault: true }), transformObject && (_jsx(TransformControls, { ref: transformControlsRef, object: transformObject, mode: transformMode, space: "local", onObjectChange: handleTransformChange, translationSnap: positionSnap > 0 ? positionSnap : undefined, rotationSnap: rotationSnap > 0 ? rotationSnap : undefined, scaleSnap: scaleSnap > 0 ? scaleSnap : undefined }, `transform-${selectedId}-${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 }))] }))] }) });
395
411
  });
396
412
  PrefabEditor.displayName = "PrefabEditor";
397
413
  export default PrefabEditor;