react-three-game 0.0.91 → 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 (34) 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/assetviewer/page.js +38 -10
  7. package/dist/tools/prefabeditor/EditorTree.js +2 -2
  8. package/dist/tools/prefabeditor/GameEvents.d.ts +6 -12
  9. package/dist/tools/prefabeditor/GameEvents.js +0 -8
  10. package/dist/tools/prefabeditor/InstanceProvider.d.ts +6 -4
  11. package/dist/tools/prefabeditor/InstanceProvider.js +84 -199
  12. package/dist/tools/prefabeditor/PrefabEditor.d.ts +18 -6
  13. package/dist/tools/prefabeditor/PrefabEditor.js +67 -30
  14. package/dist/tools/prefabeditor/PrefabRoot.d.ts +15 -9
  15. package/dist/tools/prefabeditor/PrefabRoot.js +142 -129
  16. package/dist/tools/prefabeditor/assetRuntime.d.ts +13 -11
  17. package/dist/tools/prefabeditor/assetRuntime.js +15 -15
  18. package/dist/tools/prefabeditor/components/BufferGeometryComponent.js +1 -1
  19. package/dist/tools/prefabeditor/components/CameraComponent.js +2 -2
  20. package/dist/tools/prefabeditor/components/ComponentRegistry.d.ts +3 -3
  21. package/dist/tools/prefabeditor/components/DirectionalLightComponent.js +3 -3
  22. package/dist/tools/prefabeditor/components/ModelComponent.js +1 -1
  23. package/dist/tools/prefabeditor/components/PointLightComponent.js +2 -2
  24. package/dist/tools/prefabeditor/components/SoundComponent.js +2 -2
  25. package/dist/tools/prefabeditor/components/SpotLightComponent.js +2 -2
  26. package/dist/tools/prefabeditor/components/index.js +0 -2
  27. package/dist/tools/prefabeditor/types.d.ts +1 -0
  28. package/dist/tools/prefabeditor/usePointerEvents.d.ts +3 -3
  29. package/dist/tools/prefabeditor/usePointerEvents.js +5 -5
  30. package/package.json +1 -3
  31. package/dist/tools/prefabeditor/components/PhysicsComponent.d.ts +0 -26
  32. package/dist/tools/prefabeditor/components/PhysicsComponent.js +0 -287
  33. package/dist/tools/prefabeditor/scene.d.ts +0 -70
  34. 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;
@@ -8,12 +8,12 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
8
8
  });
9
9
  };
10
10
  import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
11
- import { MapControls, TransformControls } from "@react-three/drei";
11
+ import { MapControls, TransformControls, useHelper } from "@react-three/drei";
12
12
  import GameCanvas from "../../shared/GameCanvas";
13
- import { useCallback, useEffect, useReducer, useRef, useState, forwardRef, useImperativeHandle, createContext, useContext } from "react";
13
+ import { useCallback, useEffect, useRef, useState, forwardRef, useImperativeHandle, createContext, useContext } from "react";
14
+ import { BoxHelper } from "three";
14
15
  import { findComponentEntry } from "./types";
15
- import PrefabRoot from "./PrefabRoot";
16
- import { Physics } from "@react-three/rapier";
16
+ import { PrefabRootInternal } from "./PrefabRoot";
17
17
  import EditorUI from "./EditorUI";
18
18
  import { base, toolbar } from "./styles";
19
19
  import { computeParentWorldMatrix, decompose, exportGLB as exportGLBFile, exportGLBData, focusCameraOnObject, regenerateIds } from "./utils";
@@ -31,6 +31,12 @@ function isObjectAttachedToRoot(root, object) {
31
31
  }
32
32
  return false;
33
33
  }
34
+ function SelectionHelper({ object }) {
35
+ const objectRef = useRef(null);
36
+ objectRef.current = object;
37
+ useHelper(object ? objectRef : null, BoxHelper, "cyan");
38
+ return null;
39
+ }
34
40
  export var PrefabEditorMode;
35
41
  (function (PrefabEditorMode) {
36
42
  PrefabEditorMode["Edit"] = "edit";
@@ -51,7 +57,7 @@ const DEFAULT_PREFAB = {
51
57
  name: "New Prefab",
52
58
  root: createNode('Root', {}, { id: 'root' })
53
59
  };
54
- const PrefabEditor = forwardRef(({ basePath, initialPrefab, physics = true, mode: initialMode = PrefabEditorMode.Edit, onChange, showUI = true, enableWindowDrop = true, canvasProps, uiPlugins, children }, ref) => {
60
+ const PrefabEditor = forwardRef(({ basePath, initialPrefab, mode: initialMode = PrefabEditorMode.Edit, onChange, showUI = true, enableWindowDrop = true, canvasProps, uiPlugins, children }, ref) => {
55
61
  const [mode, setMode] = useState(initialMode);
56
62
  const [selectedId, setSelectedId] = useState(null);
57
63
  const [transformMode, setTransformMode] = useState("translate");
@@ -64,21 +70,33 @@ const PrefabEditor = forwardRef(({ basePath, initialPrefab, physics = true, mode
64
70
  const [historyIndex, setHistoryIndex] = useState(0);
65
71
  const changeOriginRef = useRef(null);
66
72
  const historyIndexRef = useRef(0);
67
- const [, bumpSelectedObjectVersion] = useReducer((value) => value + 1, 0);
68
73
  const prefabRootRef = useRef(null);
69
74
  const canvasRef = useRef(null);
70
75
  const controlsRef = useRef(null);
76
+ const transformControlsRef = useRef(null);
71
77
  const onChangeRef = useRef(onChange);
78
+ const sceneChangeListenersRef = useRef(new Set());
72
79
  const isEditMode = mode === PrefabEditorMode.Edit;
73
80
  const getPrefab = useCallback(() => denormalizePrefab(prefabStore.getState()), [prefabStore]);
74
- 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; }, []);
75
- 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; }, []);
76
- 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; }, []);
77
- const handleObjectRefChange = useCallback((nodeId) => {
78
- if (nodeId !== selectedId)
79
- return;
80
- bumpSelectedObjectVersion();
81
- }, [selectedId]);
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]);
82
100
  onChangeRef.current = onChange;
83
101
  const setSelection = useCallback((nodeId) => {
84
102
  const nextNode = nodeId ? prefabStore.getState().nodesById[nodeId] : null;
@@ -104,7 +122,9 @@ const PrefabEditor = forwardRef(({ basePath, initialPrefab, physics = true, mode
104
122
  updateMode(initialMode);
105
123
  }, [initialMode, updateMode]);
106
124
  const loadPrefab = useCallback((prefab, options) => {
125
+ var _a;
107
126
  changeOriginRef.current = (options === null || options === void 0 ? void 0 : options.notifyChange) === false ? "replace-silent" : "replace";
127
+ (_a = transformControlsRef.current) === null || _a === void 0 ? void 0 : _a.detach();
108
128
  prefabStore.getState().replacePrefab(prefab);
109
129
  if (options === null || options === void 0 ? void 0 : options.resetHistory) {
110
130
  setSelectedId(null);
@@ -129,6 +149,7 @@ const PrefabEditor = forwardRef(({ basePath, initialPrefab, physics = true, mode
129
149
  return;
130
150
  }
131
151
  lastRevision = state.revision;
152
+ sceneChangeListenersRef.current.forEach(listener => listener(state.revision));
132
153
  const nextPrefab = denormalizePrefab(state);
133
154
  const changeOrigin = changeOriginRef.current;
134
155
  if (changeOrigin !== "replace-silent") {
@@ -171,8 +192,9 @@ const PrefabEditor = forwardRef(({ basePath, initialPrefab, physics = true, mode
171
192
  });
172
193
  return () => unsubscribe();
173
194
  }, [prefabStore, selectedId]);
174
- const selectedObject = selectedId ? getObject(selectedId) : null;
175
- const transformObject = isObjectAttachedToRoot(getRootObject(), selectedObject)
195
+ const selectedObject = selectedId ? getNodeObject(selectedId) : null;
196
+ const transformObject = isEditMode && selectedObject
197
+ && isObjectAttachedToRoot(getSceneRootObject(), selectedObject)
176
198
  ? selectedObject
177
199
  : null;
178
200
  const addNode = useCallback((node, options) => {
@@ -202,7 +224,9 @@ const PrefabEditor = forwardRef(({ basePath, initialPrefab, physics = true, mode
202
224
  return node;
203
225
  }, [addNode]);
204
226
  const applyHistory = (index) => {
227
+ var _a;
205
228
  changeOriginRef.current = "history";
229
+ (_a = transformControlsRef.current) === null || _a === void 0 ? void 0 : _a.detach();
206
230
  prefabStore.getState().replacePrefab(history[index]);
207
231
  historyIndexRef.current = index;
208
232
  setHistoryIndex(index);
@@ -251,30 +275,30 @@ const PrefabEditor = forwardRef(({ basePath, initialPrefab, physics = true, mode
251
275
  }), [setSelection]);
252
276
  const handleExportGLB = useCallback((...args_1) => __awaiter(void 0, [...args_1], void 0, function* (options = {}) {
253
277
  yield clearSelection();
254
- const rootObject = getRootObject();
278
+ const rootObject = getSceneRootObject();
255
279
  if (!rootObject)
256
280
  return;
257
281
  return exportGLBFile(rootObject, Object.assign({ filename: `${prefabStore.getState().prefabName || 'prefab'}.glb` }, options));
258
- }), [clearSelection, getRootObject, prefabStore]);
282
+ }), [clearSelection, getSceneRootObject, prefabStore]);
259
283
  const handleExportGLBData = useCallback(() => __awaiter(void 0, void 0, void 0, function* () {
260
284
  yield clearSelection();
261
- const rootObject = getRootObject();
285
+ const rootObject = getSceneRootObject();
262
286
  if (!rootObject)
263
287
  return;
264
288
  return exportGLBData(rootObject);
265
- }), [clearSelection, getRootObject]);
289
+ }), [clearSelection, getSceneRootObject]);
266
290
  const handleFocusNode = useCallback((nodeId) => {
267
- const object = getObject(nodeId);
291
+ const object = getNodeObject(nodeId);
268
292
  const controls = controlsRef.current;
269
293
  const camera = controls === null || controls === void 0 ? void 0 : controls.object;
270
294
  if (!object || !controls || !camera)
271
295
  return;
272
296
  focusCameraOnObject(object, camera, controls.target, () => { var _a; return (_a = controls.update) === null || _a === void 0 ? void 0 : _a.call(controls); });
273
- }, [getObject]);
297
+ }, [getNodeObject]);
274
298
  const handleTransformChange = () => {
275
299
  if (!selectedId)
276
300
  return;
277
- const object = getObject(selectedId);
301
+ const object = getNodeObject(selectedId);
278
302
  if (!object)
279
303
  return;
280
304
  const parentWorld = computeParentWorldMatrix(prefabStore.getState(), selectedId);
@@ -327,21 +351,34 @@ const PrefabEditor = forwardRef(({ basePath, initialPrefab, physics = true, mode
327
351
  };
328
352
  }, [addModel, addTexture, isEditMode, enableWindowDrop]);
329
353
  useImperativeHandle(ref, () => ({
330
- root: getRootObject(),
331
- store: prefabStore,
332
- getObject,
333
- 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
+ },
334
366
  screenshot: handleScreenshot,
335
367
  exportGLB: handleExportGLB,
336
368
  exportGLBData: handleExportGLBData,
337
369
  clearSelection,
338
370
  save: getPrefab,
339
371
  load: loadPrefab,
372
+ updateNode,
373
+ updateNodes,
374
+ deleteNode,
375
+ duplicateNode,
376
+ moveNode,
340
377
  addNode,
341
378
  addModel,
342
379
  addTexture
343
- }), [addModel, addNode, addTexture, clearSelection, getObject, getPrefab, getRigidBody, getRootObject, handleExportGLB, handleExportGLBData, handleScreenshot, loadPrefab, prefabStore]);
344
- 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, onObjectRefChange: handleObjectRefChange, 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] }));
345
382
  const handleCanvasCreated = useCallback((state) => {
346
383
  var _a;
347
384
  canvasRef.current = state.gl.domElement;
@@ -370,7 +407,7 @@ const PrefabEditor = forwardRef(({ basePath, initialPrefab, physics = true, mode
370
407
  }
371
408
  (_d = canvasProps === null || canvasProps === void 0 ? void 0 : canvasProps.onPointerMissed) === null || _d === void 0 ? void 0 : _d.call(canvasProps, event);
372
409
  }
373
- : canvasProps === null || canvasProps === void 0 ? void 0 : canvasProps.onPointerMissed, children: [physics ? (_jsx(Physics, { colliders: false, debug: isEditMode, paused: isEditMode, children: content })) : content, isEditMode && (_jsxs(_Fragment, { children: [_jsx(MapControls, { ref: controlsRef, enableDamping: false, makeDefault: true }), transformObject && (_jsx(TransformControls, { 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 }))] }))] }) });
374
411
  });
375
412
  PrefabEditor.displayName = "PrefabEditor";
376
413
  export default PrefabEditor;