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.
- package/README.md +68 -33
- package/dist/helpers/index.d.ts +0 -3
- package/dist/helpers/index.js +1 -8
- package/dist/index.d.ts +5 -8
- package/dist/index.js +3 -4
- package/dist/tools/assetviewer/page.js +38 -10
- package/dist/tools/prefabeditor/EditorTree.js +2 -2
- package/dist/tools/prefabeditor/GameEvents.d.ts +6 -12
- package/dist/tools/prefabeditor/GameEvents.js +0 -8
- package/dist/tools/prefabeditor/InstanceProvider.d.ts +6 -4
- package/dist/tools/prefabeditor/InstanceProvider.js +84 -199
- package/dist/tools/prefabeditor/PrefabEditor.d.ts +18 -6
- package/dist/tools/prefabeditor/PrefabEditor.js +67 -30
- package/dist/tools/prefabeditor/PrefabRoot.d.ts +15 -9
- package/dist/tools/prefabeditor/PrefabRoot.js +142 -129
- package/dist/tools/prefabeditor/assetRuntime.d.ts +13 -11
- package/dist/tools/prefabeditor/assetRuntime.js +15 -15
- package/dist/tools/prefabeditor/components/BufferGeometryComponent.js +1 -1
- package/dist/tools/prefabeditor/components/CameraComponent.js +2 -2
- package/dist/tools/prefabeditor/components/ComponentRegistry.d.ts +3 -3
- package/dist/tools/prefabeditor/components/DirectionalLightComponent.js +3 -3
- package/dist/tools/prefabeditor/components/ModelComponent.js +1 -1
- package/dist/tools/prefabeditor/components/PointLightComponent.js +2 -2
- package/dist/tools/prefabeditor/components/SoundComponent.js +2 -2
- package/dist/tools/prefabeditor/components/SpotLightComponent.js +2 -2
- package/dist/tools/prefabeditor/components/index.js +0 -2
- package/dist/tools/prefabeditor/types.d.ts +1 -0
- package/dist/tools/prefabeditor/usePointerEvents.d.ts +3 -3
- package/dist/tools/prefabeditor/usePointerEvents.js +5 -5
- package/package.json +1 -3
- package/dist/tools/prefabeditor/components/PhysicsComponent.d.ts +0 -26
- package/dist/tools/prefabeditor/components/PhysicsComponent.js +0 -287
- package/dist/tools/prefabeditor/scene.d.ts +0 -70
- 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
|
|
2
|
+
import React, { createContext, useContext, useMemo, useRef, useState, useEffect } from "react";
|
|
14
3
|
import { Merged, useHelper } from '@react-three/drei';
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
17
|
-
import {
|
|
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
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
const
|
|
121
|
-
if (
|
|
122
|
-
|
|
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
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
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
|
-
|
|
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 =
|
|
168
|
+
const key = inst.meshPath;
|
|
178
169
|
if (!groups[key])
|
|
179
|
-
groups[key] = {
|
|
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
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
207
|
+
const canClick = !editMode && Boolean(onClick);
|
|
320
208
|
const pointerHandlers = usePointerEvents({
|
|
321
209
|
enabled: canSelect || canClick,
|
|
322
|
-
|
|
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
|
|
231
|
+
return ctx ? useStore(ctx.store, state => Boolean(state.instancesById[id] || state.sourceInstanceIdsById[id])) : false;
|
|
344
232
|
}
|
|
345
|
-
|
|
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 (!
|
|
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
|
-
}, [
|
|
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
|
-
|
|
10
|
-
|
|
11
|
-
|
|
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,
|
|
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
|
|
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,
|
|
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
|
|
75
|
-
const
|
|
76
|
-
const
|
|
77
|
-
const
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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 ?
|
|
175
|
-
const transformObject =
|
|
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 =
|
|
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,
|
|
282
|
+
}), [clearSelection, getSceneRootObject, prefabStore]);
|
|
259
283
|
const handleExportGLBData = useCallback(() => __awaiter(void 0, void 0, void 0, function* () {
|
|
260
284
|
yield clearSelection();
|
|
261
|
-
const rootObject =
|
|
285
|
+
const rootObject = getSceneRootObject();
|
|
262
286
|
if (!rootObject)
|
|
263
287
|
return;
|
|
264
288
|
return exportGLBData(rootObject);
|
|
265
|
-
}), [clearSelection,
|
|
289
|
+
}), [clearSelection, getSceneRootObject]);
|
|
266
290
|
const handleFocusNode = useCallback((nodeId) => {
|
|
267
|
-
const object =
|
|
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
|
-
}, [
|
|
297
|
+
}, [getNodeObject]);
|
|
274
298
|
const handleTransformChange = () => {
|
|
275
299
|
if (!selectedId)
|
|
276
300
|
return;
|
|
277
|
-
const object =
|
|
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
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
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,
|
|
344
|
-
const content = (_jsxs(_Fragment, { children: [isEditMode ? _jsx("gridHelper", { args: [10, 10], position: [0, -1, 0] }) : null, _jsx(
|
|
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: [
|
|
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;
|