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.
- package/LICENSE +2 -660
- package/README.md +164 -91
- package/dist/index.d.ts +4 -2
- package/dist/index.js +2 -1
- package/dist/tools/assetviewer/page.js +6 -6
- package/dist/tools/prefabeditor/EditorContext.d.ts +2 -2
- package/dist/tools/prefabeditor/EditorTree.js +17 -3
- package/dist/tools/prefabeditor/EditorTreeMenus.d.ts +3 -1
- package/dist/tools/prefabeditor/EditorTreeMenus.js +7 -8
- package/dist/tools/prefabeditor/EditorUI.js +3 -7
- package/dist/tools/prefabeditor/GameEvents.d.ts +14 -1
- package/dist/tools/prefabeditor/GameEvents.js +2 -1
- package/dist/tools/prefabeditor/InstanceProvider.d.ts +14 -0
- package/dist/tools/prefabeditor/InstanceProvider.js +198 -34
- package/dist/tools/prefabeditor/PrefabEditor.js +77 -16
- package/dist/tools/prefabeditor/PrefabRoot.d.ts +3 -1
- package/dist/tools/prefabeditor/PrefabRoot.js +88 -120
- package/dist/tools/prefabeditor/components/CameraComponent.js +1 -1
- package/dist/tools/prefabeditor/components/ClickComponent.d.ts +3 -0
- package/dist/tools/prefabeditor/components/ClickComponent.js +45 -0
- package/dist/tools/prefabeditor/components/ComponentRegistry.js +0 -3
- package/dist/tools/prefabeditor/components/DirectionalLightComponent.js +3 -3
- package/dist/tools/prefabeditor/components/Input.d.ts +5 -2
- package/dist/tools/prefabeditor/components/Input.js +71 -38
- package/dist/tools/prefabeditor/components/MaterialComponent.js +3 -3
- package/dist/tools/prefabeditor/components/ModelComponent.js +95 -16
- package/dist/tools/prefabeditor/components/PhysicsComponent.d.ts +2 -0
- package/dist/tools/prefabeditor/components/PhysicsComponent.js +77 -10
- package/dist/tools/prefabeditor/components/index.js +2 -0
- package/dist/tools/prefabeditor/types.d.ts +1 -0
- package/dist/tools/prefabeditor/utils.d.ts +7 -1
- package/dist/tools/prefabeditor/utils.js +40 -2
- 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
|
-
(
|
|
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
|
|
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
|
|
91
|
-
const key = `${inst.meshPath}__${type}`;
|
|
217
|
+
const key = `${inst.meshPath}__${inst.physics ? 'physics' : 'visual'}`;
|
|
92
218
|
if (!groups[key])
|
|
93
|
-
groups[key] = {
|
|
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.
|
|
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.
|
|
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
|
-
|
|
135
|
-
position: inst.position,
|
|
136
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
189
|
-
|
|
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.
|
|
194
|
-
return (_jsx(InstancedRigidBodies, { ref: rigidBodiesRef, instances: instances,
|
|
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:
|
|
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
|
|
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
|
-
|
|
217
|
-
|
|
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
|
-
|
|
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),
|
|
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
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
|
288
|
+
setPrefab,
|
|
238
289
|
replacePrefab,
|
|
239
290
|
addModel,
|
|
240
291
|
addTexture,
|
|
241
292
|
rootRef: prefabRootRef
|
|
242
|
-
})
|
|
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,
|
|
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: [
|
|
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>;
|