react-three-game 0.0.64 → 0.0.66

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/LICENSE +2 -660
  2. package/README.md +164 -91
  3. package/dist/index.d.ts +4 -2
  4. package/dist/index.js +2 -1
  5. package/dist/tools/assetviewer/page.js +6 -6
  6. package/dist/tools/prefabeditor/EditorContext.d.ts +2 -2
  7. package/dist/tools/prefabeditor/EditorTree.js +17 -3
  8. package/dist/tools/prefabeditor/EditorTreeMenus.d.ts +3 -1
  9. package/dist/tools/prefabeditor/EditorTreeMenus.js +7 -8
  10. package/dist/tools/prefabeditor/EditorUI.js +3 -7
  11. package/dist/tools/prefabeditor/GameEvents.d.ts +14 -1
  12. package/dist/tools/prefabeditor/GameEvents.js +2 -1
  13. package/dist/tools/prefabeditor/InstanceProvider.d.ts +14 -0
  14. package/dist/tools/prefabeditor/InstanceProvider.js +198 -34
  15. package/dist/tools/prefabeditor/PrefabEditor.js +77 -16
  16. package/dist/tools/prefabeditor/PrefabRoot.d.ts +3 -1
  17. package/dist/tools/prefabeditor/PrefabRoot.js +88 -120
  18. package/dist/tools/prefabeditor/components/CameraComponent.js +1 -1
  19. package/dist/tools/prefabeditor/components/ClickComponent.d.ts +3 -0
  20. package/dist/tools/prefabeditor/components/ClickComponent.js +45 -0
  21. package/dist/tools/prefabeditor/components/ComponentRegistry.js +0 -3
  22. package/dist/tools/prefabeditor/components/DirectionalLightComponent.js +3 -3
  23. package/dist/tools/prefabeditor/components/Input.d.ts +5 -2
  24. package/dist/tools/prefabeditor/components/Input.js +71 -38
  25. package/dist/tools/prefabeditor/components/MaterialComponent.js +3 -3
  26. package/dist/tools/prefabeditor/components/ModelComponent.js +95 -16
  27. package/dist/tools/prefabeditor/components/PhysicsComponent.d.ts +2 -0
  28. package/dist/tools/prefabeditor/components/PhysicsComponent.js +77 -10
  29. package/dist/tools/prefabeditor/components/index.js +2 -0
  30. package/dist/tools/prefabeditor/types.d.ts +1 -0
  31. package/dist/tools/prefabeditor/utils.d.ts +7 -1
  32. package/dist/tools/prefabeditor/utils.js +40 -2
  33. package/package.json +1 -1
@@ -1,8 +1,73 @@
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
+ };
1
12
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
13
  import React, { createContext, useContext, useMemo, useRef, useState, useEffect, useCallback } from "react";
3
14
  import { Merged, useHelper } from '@react-three/drei';
4
15
  import { InstancedRigidBodies } from "@react-three/rapier";
16
+ import { ActiveCollisionTypes } from "@dimforge/rapier3d-compat";
5
17
  import { Mesh, Matrix4, Vector3, Quaternion, Euler, BoxHelper } from "three";
18
+ import { gameEvents, getEntityIdFromRigidBody } from "./GameEvents";
19
+ export const DEFAULT_REPEAT_AXES = [{ axis: 'x', count: 1, offset: 1 }];
20
+ export function normalizeRepeatAxes(value) {
21
+ if (!Array.isArray(value)) {
22
+ return DEFAULT_REPEAT_AXES;
23
+ }
24
+ const seen = new Set();
25
+ const normalized = value.flatMap((entry) => {
26
+ if (!entry || typeof entry !== 'object')
27
+ return [];
28
+ const axisValue = entry.axis;
29
+ if (axisValue !== 'x' && axisValue !== 'y' && axisValue !== 'z')
30
+ return [];
31
+ if (seen.has(axisValue))
32
+ return [];
33
+ seen.add(axisValue);
34
+ const countValue = Number(entry.count);
35
+ 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
+ });
42
+ return normalized.length > 0 ? normalized : DEFAULT_REPEAT_AXES;
43
+ }
44
+ function toVector3Tuple(value, fallback) {
45
+ if (!Array.isArray(value) || value.length !== 3)
46
+ return fallback;
47
+ return value.map((entry, index) => {
48
+ const next = typeof entry === 'number' ? entry : Number(entry);
49
+ return Number.isFinite(next) ? next : fallback[index];
50
+ });
51
+ }
52
+ export function getRepeatAxesFromModelProperties(properties) {
53
+ var _a;
54
+ if (Array.isArray(properties.repeatAxes)) {
55
+ return normalizeRepeatAxes(properties.repeatAxes);
56
+ }
57
+ const repeatCount = toVector3Tuple(properties.repeatCount, [1, 1, 1]).map(value => Math.max(1, Math.floor(value)));
58
+ const repeatOffset = toVector3Tuple(properties.repeatOffset, [1, 1, 1]);
59
+ const legacyAxes = [];
60
+ if ((_a = properties.repeatX) !== null && _a !== void 0 ? _a : true) {
61
+ legacyAxes.push({ axis: 'x', count: repeatCount[0], offset: repeatOffset[0] });
62
+ }
63
+ if (properties.repeatY) {
64
+ legacyAxes.push({ axis: 'y', count: repeatCount[1], offset: repeatOffset[1] });
65
+ }
66
+ if (properties.repeatZ) {
67
+ legacyAxes.push({ axis: 'z', count: repeatCount[2], offset: repeatOffset[2] });
68
+ }
69
+ return legacyAxes.length > 0 ? legacyAxes : DEFAULT_REPEAT_AXES;
70
+ }
6
71
  // Helper functions for comparison
7
72
  function arrayEquals(a, b) {
8
73
  if (a === b)
@@ -15,14 +80,77 @@ function arrayEquals(a, b) {
15
80
  }
16
81
  return true;
17
82
  }
83
+ function stableSerialize(value) {
84
+ if (Array.isArray(value)) {
85
+ return `[${value.map(stableSerialize).join(',')}]`;
86
+ }
87
+ if (value && typeof value === 'object') {
88
+ const entries = Object.entries(value)
89
+ .sort(([a], [b]) => a.localeCompare(b))
90
+ .map(([key, entry]) => `${key}:${stableSerialize(entry)}`);
91
+ return `{${entries.join(',')}}`;
92
+ }
93
+ return JSON.stringify(value);
94
+ }
95
+ function getPhysicsSignature(physics) {
96
+ return physics ? stableSerialize(physics) : 'none';
97
+ }
98
+ function hasPhysics(instance) {
99
+ return Boolean(instance.physics);
100
+ }
101
+ function getColliderType(physics) {
102
+ return physics.colliders || (physics.type === 'fixed' ? 'trimesh' : 'hull');
103
+ }
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', {
127
+ sourceEntityId: sourceId,
128
+ targetEntityId: getEntityIdFromRigidBody(payload.other.rigidBody),
129
+ targetRigidBody: payload.other.rigidBody,
130
+ });
131
+ }
132
+ function emitClick(sourceId, instanceId, event) {
133
+ gameEvents.emit('click', {
134
+ sourceEntityId: sourceId,
135
+ instanceEntityId: instanceId && instanceId !== sourceId ? instanceId : undefined,
136
+ point: [event.point.x, event.point.y, event.point.z],
137
+ button: event.button,
138
+ altKey: event.nativeEvent.altKey,
139
+ ctrlKey: event.nativeEvent.ctrlKey,
140
+ metaKey: event.nativeEvent.metaKey,
141
+ shiftKey: event.nativeEvent.shiftKey,
142
+ });
143
+ }
18
144
  function instanceEquals(a, b) {
19
- var _a, _b;
20
145
  return a.id === b.id &&
146
+ a.sourceId === b.sourceId &&
147
+ a.clickable === b.clickable &&
148
+ a.locked === b.locked &&
21
149
  a.meshPath === b.meshPath &&
22
150
  arrayEquals(a.position, b.position) &&
23
151
  arrayEquals(a.rotation, b.rotation) &&
24
152
  arrayEquals(a.scale, b.scale) &&
25
- ((_a = a.physics) === null || _a === void 0 ? void 0 : _a.type) === ((_b = b.physics) === null || _b === void 0 ? void 0 : _b.type);
153
+ getPhysicsSignature(a.physics) === getPhysicsSignature(b.physics);
26
154
  }
27
155
  const GameInstanceContext = createContext(null);
28
156
  export function GameInstanceProvider({ children, models, onSelect, registerRef, selectedId, editMode }) {
@@ -51,7 +179,7 @@ export function GameInstanceProvider({ children, models, onSelect, registerRef,
51
179
  });
52
180
  }, []);
53
181
  const hasInstance = useCallback((id) => {
54
- return instances.some(i => i.id === id);
182
+ return instances.some(i => i.id === id || i.sourceId === id);
55
183
  }, [instances]);
56
184
  // Flatten all model meshes once (models → flat mesh parts)
57
185
  // Note: Geometry is cloned with baked transforms for instancing
@@ -82,17 +210,18 @@ export function GameInstanceProvider({ children, models, onSelect, registerRef,
82
210
  Object.values(flatMeshes).forEach(mesh => mesh.geometry.dispose());
83
211
  };
84
212
  }, [flatMeshes]);
85
- // Group instances by meshPath + physics type for batch rendering
213
+ // Group instances by meshPath and physics presence for batch rendering.
86
214
  const grouped = useMemo(() => {
87
- var _a;
88
215
  const groups = {};
89
216
  for (const inst of instances) {
90
- const type = ((_a = inst.physics) === null || _a === void 0 ? void 0 : _a.type) || 'none';
91
- const key = `${inst.meshPath}__${type}`;
217
+ const key = `${inst.meshPath}__${inst.physics ? 'physics' : 'visual'}`;
92
218
  if (!groups[key])
93
- groups[key] = { physicsType: type, instances: [] };
219
+ groups[key] = { hasPhysics: Boolean(inst.physics), instances: [] };
94
220
  groups[key].instances.push(inst);
95
221
  }
222
+ Object.values(groups).forEach(group => {
223
+ group.instances.sort((a, b) => a.id.localeCompare(b.id));
224
+ });
96
225
  return groups;
97
226
  }, [instances]);
98
227
  return (_jsxs(GameInstanceContext.Provider, { value: {
@@ -103,7 +232,7 @@ export function GameInstanceProvider({ children, models, onSelect, registerRef,
103
232
  modelParts,
104
233
  hasInstance
105
234
  }, children: [children, Object.entries(grouped).map(([key, group]) => {
106
- if (group.physicsType === 'none')
235
+ if (!group.hasPhysics)
107
236
  return null;
108
237
  const modelKey = group.instances[0].meshPath;
109
238
  const partCount = modelParts[modelKey] || 0;
@@ -111,7 +240,7 @@ export function GameInstanceProvider({ children, models, onSelect, registerRef,
111
240
  return null;
112
241
  return (_jsx(InstancedRigidGroup, { group: group, modelKey: modelKey, partCount: partCount, flatMeshes: flatMeshes, onSelect: onSelect, editMode: editMode }, key));
113
242
  }), Object.entries(grouped).map(([key, group]) => {
114
- if (group.physicsType !== 'none')
243
+ if (group.hasPhysics)
115
244
  return null;
116
245
  const modelKey = group.instances[0].meshPath;
117
246
  const partCount = modelParts[modelKey] || 0;
@@ -130,12 +259,10 @@ export function GameInstanceProvider({ children, models, onSelect, registerRef,
130
259
  function InstancedRigidGroup({ group, modelKey, partCount, flatMeshes, onSelect, editMode }) {
131
260
  const meshRefs = useRef([]);
132
261
  const rigidBodiesRef = useRef(null);
133
- const instances = useMemo(() => group.instances.map(inst => ({
134
- key: inst.id,
135
- position: inst.position,
136
- rotation: inst.rotation,
137
- scale: inst.scale,
138
- })), [group.instances]);
262
+ const instances = useMemo(() => group.instances.filter(hasPhysics).map(inst => {
263
+ 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) });
265
+ }), [group.instances]);
139
266
  // Apply scale to visual meshes (InstancedRigidBodies only scales colliders, not visuals)
140
267
  useEffect(() => {
141
268
  const matrix = new Matrix4();
@@ -161,7 +288,7 @@ function InstancedRigidGroup({ group, modelKey, partCount, flatMeshes, onSelect,
161
288
  try {
162
289
  group.instances.forEach((inst, i) => {
163
290
  var _a;
164
- const body = (_a = rigidBodiesRef.current) === null || _a === void 0 ? void 0 : _a.at(i);
291
+ const body = (_a = rigidBodiesRef.current) === null || _a === void 0 ? void 0 : _a[i];
165
292
  if (body && body.setTranslation && body.setRotation) {
166
293
  pos.set(...inst.position);
167
294
  euler.set(...inst.rotation);
@@ -177,25 +304,48 @@ function InstancedRigidGroup({ group, modelKey, partCount, flatMeshes, onSelect,
177
304
  }
178
305
  }
179
306
  }, [group.instances]);
180
- const colliders = group.physicsType === 'fixed' ? 'trimesh' : 'hull';
307
+ useEffect(() => {
308
+ group.instances.forEach((inst, i) => {
309
+ var _a, _b;
310
+ if (!inst.physics || inst.physics.activeCollisionTypes !== 'all')
311
+ return;
312
+ const body = (_a = rigidBodiesRef.current) === null || _a === void 0 ? void 0 : _a[i];
313
+ if (!body || !body.numColliders || !body.collider)
314
+ return;
315
+ for (let colliderIndex = 0; colliderIndex < body.numColliders(); colliderIndex++) {
316
+ const collider = body.collider(colliderIndex);
317
+ (_b = collider.setActiveCollisionTypes) === null || _b === void 0 ? void 0 : _b.call(collider, ActiveCollisionTypes.DEFAULT |
318
+ ActiveCollisionTypes.KINEMATIC_FIXED |
319
+ ActiveCollisionTypes.KINEMATIC_KINEMATIC);
320
+ }
321
+ });
322
+ }, [group.instances]);
181
323
  // Handle click on instanced mesh in edit mode
182
324
  const handleClick = (e) => {
183
- if (!editMode || !onSelect)
184
- return;
185
- e.stopPropagation();
186
- // Get the instance index from the intersection
187
325
  const instanceId = e.instanceId;
188
- if (instanceId !== undefined && group.instances[instanceId]) {
189
- onSelect(group.instances[instanceId].id);
326
+ const instance = instanceId !== undefined ? group.instances[instanceId] : undefined;
327
+ if (!instance)
328
+ return;
329
+ if (editMode) {
330
+ if (!onSelect || instance.locked)
331
+ return;
332
+ e.stopPropagation();
333
+ onSelect(instance.sourceId);
334
+ return;
190
335
  }
336
+ if (!instance.clickable)
337
+ return;
338
+ e.stopPropagation();
339
+ emitClick(instance.sourceId, instance.id, e);
191
340
  };
341
+ const shouldHandleClick = editMode || group.instances.some(inst => inst.clickable);
192
342
  // Add key to force remount when instance count changes significantly (helps with cleanup)
193
- const rigidBodyKey = `rb_${modelKey}_${group.physicsType}_${group.instances.length}`;
194
- return (_jsx(InstancedRigidBodies, { ref: rigidBodiesRef, instances: instances, colliders: colliders, type: group.physicsType, children: Array.from({ length: partCount }).map((_, i) => {
343
+ const rigidBodyKey = `rb_${modelKey}_${group.instances.map(inst => `${inst.id}:${getPhysicsSignature(inst.physics)}`).join('|')}`;
344
+ return (_jsx(InstancedRigidBodies, { ref: rigidBodiesRef, instances: instances, children: Array.from({ length: partCount }).map((_, i) => {
195
345
  const mesh = flatMeshes[`${modelKey}__${i}`];
196
346
  if (!mesh)
197
347
  return null;
198
- return (_jsx("instancedMesh", { ref: el => { meshRefs.current[i] = el; }, args: [mesh.geometry, mesh.material, group.instances.length], castShadow: true, receiveShadow: true, frustumCulled: false, onClick: editMode ? handleClick : undefined }, i));
348
+ 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));
199
349
  }) }, rigidBodyKey));
200
350
  }
201
351
  // Render non-physics instances using Merged (instancing without rigid bodies)
@@ -208,19 +358,30 @@ function NonPhysicsInstancedGroup({ modelKey, group, partCount, instancesMap, on
208
358
  function InstanceGroupItem({ instance, InstanceComponents, onSelect, registerRef, selectedId, editMode }) {
209
359
  const clickValid = useRef(false);
210
360
  const groupRef = useRef(null);
211
- const isSelected = selectedId === instance.id;
361
+ const isLocked = Boolean(instance.locked);
362
+ const isSelected = selectedId === instance.id || selectedId === instance.sourceId;
363
+ const canSelect = editMode && !isLocked;
364
+ const canClick = !editMode && Boolean(instance.clickable);
212
365
  // Use BoxHelper when object is selected in edit mode
213
366
  useHelper(editMode && isSelected ? groupRef : null, BoxHelper, 'cyan');
214
367
  useEffect(() => {
368
+ if (editMode)
369
+ return;
215
370
  registerRef === null || registerRef === void 0 ? void 0 : registerRef(instance.id, groupRef.current);
216
- }, [instance.id, registerRef]);
217
- return (_jsx("group", { ref: groupRef, position: instance.position, rotation: instance.rotation, scale: instance.scale, onPointerDown: (e) => { e.stopPropagation(); clickValid.current = true; }, onPointerMove: () => { clickValid.current = false; }, onPointerUp: (e) => {
371
+ return () => registerRef === null || registerRef === void 0 ? void 0 : registerRef(instance.id, null);
372
+ }, [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) => {
218
374
  if (clickValid.current) {
219
375
  e.stopPropagation();
220
- onSelect === null || onSelect === void 0 ? void 0 : onSelect(instance.id);
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
+ }
221
382
  }
222
383
  clickValid.current = false;
223
- }, children: InstanceComponents.map((Instance, i) => _jsx(Instance, {}, i)) }));
384
+ } : undefined, children: InstanceComponents.map((Instance, i) => _jsx(Instance, {}, i)) }));
224
385
  }
225
386
  // Hook to check if an instance exists
226
387
  export function useInstanceCheck(id) {
@@ -229,18 +390,21 @@ export function useInstanceCheck(id) {
229
390
  return (_a = ctx === null || ctx === void 0 ? void 0 : ctx.hasInstance(id)) !== null && _a !== void 0 ? _a : false;
230
391
  }
231
392
  // GameInstance component: registers an instance for batch rendering (renders nothing itself)
232
- export const GameInstance = React.forwardRef(({ id, modelUrl, position, rotation, scale, physics = undefined, }, ref) => {
393
+ export const GameInstance = React.forwardRef(({ id, sourceId, clickable = false, modelUrl, locked = false, position, rotation, scale, physics = undefined, }, ref) => {
233
394
  const ctx = useContext(GameInstanceContext);
234
395
  const addInstance = ctx === null || ctx === void 0 ? void 0 : ctx.addInstance;
235
396
  const removeInstance = ctx === null || ctx === void 0 ? void 0 : ctx.removeInstance;
236
397
  const instance = useMemo(() => ({
237
398
  id,
399
+ sourceId: sourceId !== null && sourceId !== void 0 ? sourceId : id,
400
+ clickable,
401
+ locked,
238
402
  meshPath: modelUrl,
239
403
  position,
240
404
  rotation,
241
405
  scale,
242
406
  physics,
243
- }), [id, modelUrl, JSON.stringify(position), JSON.stringify(rotation), JSON.stringify(scale), JSON.stringify(physics)]);
407
+ }), [id, sourceId, clickable, locked, modelUrl, JSON.stringify(position), JSON.stringify(rotation), JSON.stringify(scale), getPhysicsSignature(physics)]);
244
408
  useEffect(() => {
245
409
  if (!addInstance || !removeInstance)
246
410
  return;
@@ -8,6 +8,7 @@ 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
12
  import GameCanvas from "../../shared/GameCanvas";
12
13
  import { useState, useRef, useEffect, forwardRef, useImperativeHandle } from "react";
13
14
  import PrefabRoot from "./PrefabRoot";
@@ -15,7 +16,7 @@ import { Physics } from "@react-three/rapier";
15
16
  import EditorUI from "./EditorUI";
16
17
  import { base, toolbar } from "./styles";
17
18
  import { EditorContext } from "./EditorContext";
18
- import { createImageNode, createModelNode, exportGLB as exportSceneGLB, exportGLBData, insertNode } from "./utils";
19
+ import { computeParentWorldMatrix, createImageNode, createModelNode, decompose, exportGLB as exportSceneGLB, exportGLBData, findNode, focusCameraOnObject, insertNode, updateNode } from "./utils";
19
20
  import { loadFiles } from "../dragdrop";
20
21
  const DEFAULT_PREFAB = {
21
22
  id: "prefab-default",
@@ -40,29 +41,57 @@ const PrefabEditor = forwardRef(({ basePath, initialPrefab, physics = true, onPr
40
41
  const [rotationSnap, setRotationSnap] = useState(Math.PI / 4);
41
42
  const [history, setHistory] = useState([loadedPrefab]);
42
43
  const [historyIndex, setHistoryIndex] = useState(0);
44
+ const [selectedObject, setSelectedObject] = useState(null);
43
45
  const throttleRef = useRef(null);
44
46
  const lastDataRef = useRef(JSON.stringify(loadedPrefab));
45
47
  const prefabRootRef = useRef(null);
46
48
  const canvasRef = useRef(null);
49
+ const controlsRef = useRef(null);
47
50
  const onPrefabChangeRef = useRef(onPrefabChange);
48
51
  const pendingPrefabChangeRef = useRef(null);
49
52
  const [injectedModels, setInjectedModels] = useState({});
50
53
  const [injectedTextures, setInjectedTextures] = useState({});
51
- useEffect(() => {
52
- onPrefabChangeRef.current = onPrefabChange;
53
- }, [onPrefabChange]);
54
+ onPrefabChangeRef.current = onPrefabChange;
55
+ const setSelection = (nodeId) => {
56
+ var _a, _b;
57
+ const nextNode = nodeId ? findNode(loadedPrefab.root, nodeId) : null;
58
+ if (nextNode === null || nextNode === void 0 ? void 0 : nextNode.locked) {
59
+ return;
60
+ }
61
+ setSelectedId(nodeId);
62
+ setSelectedObject(nodeId ? (_b = (_a = prefabRootRef.current) === null || _a === void 0 ? void 0 : _a.getObject(nodeId)) !== null && _b !== void 0 ? _b : null : null);
63
+ };
64
+ const toggleEditMode = () => {
65
+ setEditMode(prev => {
66
+ const next = !prev;
67
+ if (!next) {
68
+ setSelectedId(null);
69
+ setSelectedObject(null);
70
+ }
71
+ return next;
72
+ });
73
+ };
74
+ const setSelectedIdState = (value) => {
75
+ setSelection(typeof value === 'function' ? value(selectedId) : value);
76
+ };
54
77
  const replacePrefab = (prefab, options) => {
55
78
  if (throttleRef.current)
56
79
  clearTimeout(throttleRef.current);
57
80
  lastDataRef.current = JSON.stringify(prefab);
58
81
  pendingPrefabChangeRef.current = (options === null || options === void 0 ? void 0 : options.notifyChange) === false ? null : prefab;
59
- setSelectedId(null);
82
+ setSelection(null);
60
83
  setInjectedModels({});
61
84
  setInjectedTextures({});
62
85
  setHistory([prefab]);
63
86
  setHistoryIndex(0);
64
87
  setLoadedPrefab(prefab);
65
88
  };
89
+ const setPrefab = (prefab) => {
90
+ if (selectedId && !findNode(prefab.root, selectedId)) {
91
+ setSelection(null);
92
+ }
93
+ updatePrefab(prefab);
94
+ };
66
95
  useEffect(() => {
67
96
  if (initialPrefab)
68
97
  replacePrefab(initialPrefab, { notifyChange: false });
@@ -90,7 +119,7 @@ const PrefabEditor = forwardRef(({ basePath, initialPrefab, physics = true, onPr
90
119
  return Object.assign(Object.assign({}, prev), { root: insertNode(prev.root, node, options === null || options === void 0 ? void 0 : options.parentId) });
91
120
  });
92
121
  if ((options === null || options === void 0 ? void 0 : options.select) !== false) {
93
- setSelectedId(node.id);
122
+ setSelection(node.id);
94
123
  }
95
124
  return node;
96
125
  };
@@ -115,6 +144,8 @@ const PrefabEditor = forwardRef(({ basePath, initialPrefab, physics = true, onPr
115
144
  const undo = () => historyIndex > 0 && applyHistory(historyIndex - 1);
116
145
  const redo = () => historyIndex < history.length - 1 && applyHistory(historyIndex + 1);
117
146
  useEffect(() => {
147
+ if (!editMode)
148
+ return;
118
149
  const handleKeyDown = (e) => {
119
150
  if (!(e.ctrlKey || e.metaKey))
120
151
  return;
@@ -129,7 +160,7 @@ const PrefabEditor = forwardRef(({ basePath, initialPrefab, physics = true, onPr
129
160
  };
130
161
  window.addEventListener('keydown', handleKeyDown);
131
162
  return () => window.removeEventListener('keydown', handleKeyDown);
132
- }, [historyIndex, history]);
163
+ }, [editMode, historyIndex, history]);
133
164
  useEffect(() => {
134
165
  const currentStr = JSON.stringify(loadedPrefab);
135
166
  if (currentStr === lastDataRef.current)
@@ -165,7 +196,7 @@ const PrefabEditor = forwardRef(({ basePath, initialPrefab, physics = true, onPr
165
196
  const clearSelection = () => __awaiter(void 0, void 0, void 0, function* () {
166
197
  if (!selectedId)
167
198
  return;
168
- setSelectedId(null);
199
+ setSelection(null);
169
200
  yield new Promise(resolve => {
170
201
  requestAnimationFrame(() => {
171
202
  requestAnimationFrame(() => resolve());
@@ -190,11 +221,31 @@ const PrefabEditor = forwardRef(({ basePath, initialPrefab, physics = true, onPr
190
221
  });
191
222
  const handleFocusNode = (nodeId) => {
192
223
  var _a;
193
- (_a = prefabRootRef.current) === null || _a === void 0 ? void 0 : _a.focusNode(nodeId);
224
+ const object = (_a = prefabRootRef.current) === null || _a === void 0 ? void 0 : _a.getObject(nodeId);
225
+ const controls = controlsRef.current;
226
+ const camera = controls === null || controls === void 0 ? void 0 : controls.object;
227
+ if (!object || !controls || !camera)
228
+ return;
229
+ focusCameraOnObject(object, camera, controls.target, () => { var _a; return (_a = controls.update) === null || _a === void 0 ? void 0 : _a.call(controls); });
230
+ };
231
+ const handleTransformChange = () => {
232
+ var _a;
233
+ if (!selectedId)
234
+ return;
235
+ const object = (_a = prefabRootRef.current) === null || _a === void 0 ? void 0 : _a.getObject(selectedId);
236
+ if (!object)
237
+ return;
238
+ const parentWorld = computeParentWorldMatrix(loadedPrefab.root, selectedId);
239
+ const local = parentWorld.clone().invert().multiply(object.matrixWorld);
240
+ const { position, rotation, scale } = decompose(local);
241
+ updatePrefab(prev => (Object.assign(Object.assign({}, prev), { root: updateNode(prev.root, selectedId, node => (Object.assign(Object.assign({}, node), { components: Object.assign(Object.assign({}, node.components), { transform: {
242
+ type: "Transform",
243
+ properties: { position, rotation, scale },
244
+ } }) }))) })));
194
245
  };
195
246
  // --- Drag & drop files to add nodes ---
196
247
  useEffect(() => {
197
- if (!enableWindowDrop)
248
+ if (!enableWindowDrop || !editMode)
198
249
  return;
199
250
  function handleDragOver(e) {
200
251
  e.preventDefault();
@@ -227,21 +278,22 @@ const PrefabEditor = forwardRef(({ basePath, initialPrefab, physics = true, onPr
227
278
  window.removeEventListener('dragover', handleDragOver);
228
279
  window.removeEventListener('drop', handleDrop);
229
280
  };
230
- }, [enableWindowDrop]);
281
+ }, [editMode, enableWindowDrop]);
231
282
  useImperativeHandle(ref, () => ({
232
283
  screenshot: handleScreenshot,
233
284
  exportGLB: handleExportGLB,
234
285
  exportGLBData: handleExportGLBData,
235
286
  clearSelection,
236
287
  prefab: loadedPrefab,
237
- setPrefab: replacePrefab,
288
+ setPrefab,
238
289
  replacePrefab,
239
290
  addModel,
240
291
  addTexture,
241
292
  rootRef: prefabRootRef
242
- }), [loadedPrefab]);
243
- const content = (_jsxs(_Fragment, { children: [_jsx("ambientLight", { intensity: 1.5 }), _jsx("gridHelper", { args: [10, 10], position: [0, -1, 0] }), _jsx(PrefabRoot, { ref: prefabRootRef, data: loadedPrefab, editMode: editMode, onPrefabChange: updatePrefab, selectedId: selectedId, onSelect: setSelectedId, basePath: basePath, injectedModels: injectedModels, injectedTextures: injectedTextures }), children] }));
293
+ }));
294
+ const content = (_jsxs(_Fragment, { children: [_jsx("ambientLight", { intensity: 1.5 }), _jsx("gridHelper", { args: [10, 10], position: [0, -1, 0] }), _jsx(PrefabRoot, { ref: prefabRootRef, data: loadedPrefab, editMode: editMode, selectedId: selectedId, onSelect: setSelection, onSelectedObjectChange: editMode ? setSelectedObject : undefined, onFocusNode: editMode ? handleFocusNode : undefined, basePath: basePath, injectedModels: injectedModels, injectedTextures: injectedTextures }), children] }));
244
295
  return _jsxs(EditorContext.Provider, { value: {
296
+ editMode,
245
297
  transformMode,
246
298
  setTransformMode,
247
299
  snapResolution,
@@ -250,10 +302,19 @@ const PrefabEditor = forwardRef(({ basePath, initialPrefab, physics = true, onPr
250
302
  setPositionSnap,
251
303
  rotationSnap,
252
304
  setRotationSnap,
253
- onFocusNode: handleFocusNode,
305
+ onFocusNode: editMode ? handleFocusNode : undefined,
254
306
  onScreenshot: handleScreenshot,
255
307
  onExportGLB: handleExportGLB
256
- }, children: [_jsx(GameCanvas, Object.assign({ camera: { position: [0, 5, 15] }, canvasRef: canvasRef }, canvasProps, { children: physics ? (_jsx(Physics, { debug: editMode, paused: editMode, children: content })) : content })), showUI && (_jsxs(_Fragment, { children: [_jsxs("div", { style: toolbar.panel, children: [_jsx("button", { style: base.btn, onClick: () => setEditMode(!editMode), children: editMode ? "▶" : "⏸" }), uiPlugins] }), _jsx(EditorUI, { prefabData: loadedPrefab, setPrefabData: updatePrefab, selectedId: selectedId, setSelectedId: setSelectedId, basePath: basePath, onUndo: undo, onRedo: redo, canUndo: historyIndex > 0, canRedo: historyIndex < history.length - 1 })] }))] });
308
+ }, children: [_jsxs(GameCanvas, Object.assign({ camera: { position: [0, 5, 15] }, canvasRef: canvasRef }, canvasProps, { onPointerMissed: editMode
309
+ ? (event) => {
310
+ var _a, _b, _c, _d;
311
+ 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;
312
+ if (button === 0 && selectedId) {
313
+ setSelection(null);
314
+ }
315
+ (_d = canvasProps === null || canvasProps === void 0 ? void 0 : canvasProps.onPointerMissed) === null || _d === void 0 ? void 0 : _d.call(canvasProps, event);
316
+ }
317
+ : 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: snapResolution > 0 ? snapResolution : undefined }, `transform-${transformMode}-${positionSnap}-${rotationSnap}-${snapResolution}`))] }))] })), showUI && (_jsxs(_Fragment, { children: [_jsxs("div", { style: toolbar.panel, children: [_jsx("button", { style: base.btn, onClick: toggleEditMode, children: editMode ? "▶" : "⏸" }), uiPlugins] }), editMode && (_jsx(EditorUI, { prefabData: loadedPrefab, setPrefabData: updatePrefab, selectedId: selectedId, setSelectedId: setSelectedIdState, basePath: basePath, onUndo: undo, onRedo: redo, canUndo: historyIndex > 0, canRedo: historyIndex < history.length - 1 }))] }))] });
257
318
  });
258
319
  PrefabEditor.displayName = "PrefabEditor";
259
320
  export default PrefabEditor;
@@ -4,15 +4,17 @@ import { Prefab, GameObject as GameObjectType } from "./types";
4
4
  export interface PrefabRootRef {
5
5
  root: Group | null;
6
6
  rigidBodyRefs: Map<string, any>;
7
+ getObject: (nodeId: string) => Object3D | null;
7
8
  focusNode: (nodeId: string) => void;
8
9
  }
9
10
  export declare const PrefabRoot: import("react").ForwardRefExoticComponent<{
10
11
  editMode?: boolean;
11
12
  data: Prefab;
12
- onPrefabChange?: (data: Prefab) => void;
13
13
  selectedId?: string | null;
14
14
  onSelect?: (id: string | null) => void;
15
15
  onClick?: (event: ThreeEvent<PointerEvent>, entity: GameObjectType) => void;
16
+ onSelectedObjectChange?: (object: Object3D | null) => void;
17
+ onFocusNode?: (nodeId: string) => void;
16
18
  basePath?: string;
17
19
  injectedModels?: Record<string, Object3D>;
18
20
  injectedTextures?: Record<string, Texture>;