react-three-game 0.0.92 → 0.0.93
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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/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 +55 -39
- package/dist/tools/prefabeditor/PrefabRoot.d.ts +15 -8
- package/dist/tools/prefabeditor/PrefabRoot.js +141 -117
- 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 +2 -2
- 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 -302
- 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;
|
|
@@ -10,11 +10,10 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
|
|
|
10
10
|
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
11
11
|
import { MapControls, TransformControls, useHelper } from "@react-three/drei";
|
|
12
12
|
import GameCanvas from "../../shared/GameCanvas";
|
|
13
|
-
import { useCallback, useEffect,
|
|
13
|
+
import { useCallback, useEffect, useRef, useState, forwardRef, useImperativeHandle, createContext, useContext } from "react";
|
|
14
14
|
import { BoxHelper } from "three";
|
|
15
15
|
import { findComponentEntry } from "./types";
|
|
16
|
-
import
|
|
17
|
-
import { Physics } from "@react-three/rapier";
|
|
16
|
+
import { PrefabRootInternal } from "./PrefabRoot";
|
|
18
17
|
import EditorUI from "./EditorUI";
|
|
19
18
|
import { base, toolbar } from "./styles";
|
|
20
19
|
import { computeParentWorldMatrix, decompose, exportGLB as exportGLBFile, exportGLBData, focusCameraOnObject, regenerateIds } from "./utils";
|
|
@@ -58,8 +57,7 @@ const DEFAULT_PREFAB = {
|
|
|
58
57
|
name: "New Prefab",
|
|
59
58
|
root: createNode('Root', {}, { id: 'root' })
|
|
60
59
|
};
|
|
61
|
-
const PrefabEditor = forwardRef(({ basePath, initialPrefab,
|
|
62
|
-
var _a, _b;
|
|
60
|
+
const PrefabEditor = forwardRef(({ basePath, initialPrefab, mode: initialMode = PrefabEditorMode.Edit, onChange, showUI = true, enableWindowDrop = true, canvasProps, uiPlugins, children }, ref) => {
|
|
63
61
|
const [mode, setMode] = useState(initialMode);
|
|
64
62
|
const [selectedId, setSelectedId] = useState(null);
|
|
65
63
|
const [transformMode, setTransformMode] = useState("translate");
|
|
@@ -76,13 +74,29 @@ const PrefabEditor = forwardRef(({ basePath, initialPrefab, physics = true, mode
|
|
|
76
74
|
const canvasRef = useRef(null);
|
|
77
75
|
const controlsRef = useRef(null);
|
|
78
76
|
const transformControlsRef = useRef(null);
|
|
79
|
-
const transformProxyRef = useRef(null);
|
|
80
77
|
const onChangeRef = useRef(onChange);
|
|
78
|
+
const sceneChangeListenersRef = useRef(new Set());
|
|
81
79
|
const isEditMode = mode === PrefabEditorMode.Edit;
|
|
82
80
|
const getPrefab = useCallback(() => denormalizePrefab(prefabStore.getState()), [prefabStore]);
|
|
83
|
-
const
|
|
84
|
-
const
|
|
85
|
-
const
|
|
81
|
+
const getNode = useCallback((nodeId) => { var _a; return (_a = prefabStore.getState().nodesById[nodeId]) !== null && _a !== void 0 ? _a : null; }, [prefabStore]);
|
|
82
|
+
const getSceneRootObject = useCallback(() => { var _a, _b; return (_b = (_a = prefabRootRef.current) === null || _a === void 0 ? void 0 : _a.root) !== null && _b !== void 0 ? _b : null; }, []);
|
|
83
|
+
const getNodeObject = useCallback((nodeId) => { var _a, _b; return (_b = (_a = prefabRootRef.current) === null || _a === void 0 ? void 0 : _a.getNodeObject(nodeId)) !== null && _b !== void 0 ? _b : null; }, []);
|
|
84
|
+
const getNodeHandle = useCallback((nodeId, kind) => { var _a, _b; return (_b = (_a = prefabRootRef.current) === null || _a === void 0 ? void 0 : _a.getNodeHandle(nodeId, kind)) !== null && _b !== void 0 ? _b : null; }, []);
|
|
85
|
+
const updateNode = useCallback((nodeId, update) => {
|
|
86
|
+
prefabStore.getState().updateNode(nodeId, update);
|
|
87
|
+
}, [prefabStore]);
|
|
88
|
+
const updateNodes = useCallback((updates) => {
|
|
89
|
+
prefabStore.getState().updateNodes(updates);
|
|
90
|
+
}, [prefabStore]);
|
|
91
|
+
const deleteNode = useCallback((nodeId) => {
|
|
92
|
+
prefabStore.getState().deleteNode(nodeId);
|
|
93
|
+
}, [prefabStore]);
|
|
94
|
+
const duplicateNode = useCallback((nodeId) => {
|
|
95
|
+
return prefabStore.getState().duplicateNode(nodeId);
|
|
96
|
+
}, [prefabStore]);
|
|
97
|
+
const moveNode = useCallback((draggedId, targetId, position) => {
|
|
98
|
+
prefabStore.getState().moveNode(draggedId, targetId, position);
|
|
99
|
+
}, [prefabStore]);
|
|
86
100
|
onChangeRef.current = onChange;
|
|
87
101
|
const setSelection = useCallback((nodeId) => {
|
|
88
102
|
const nextNode = nodeId ? prefabStore.getState().nodesById[nodeId] : null;
|
|
@@ -135,6 +149,7 @@ const PrefabEditor = forwardRef(({ basePath, initialPrefab, physics = true, mode
|
|
|
135
149
|
return;
|
|
136
150
|
}
|
|
137
151
|
lastRevision = state.revision;
|
|
152
|
+
sceneChangeListenersRef.current.forEach(listener => listener(state.revision));
|
|
138
153
|
const nextPrefab = denormalizePrefab(state);
|
|
139
154
|
const changeOrigin = changeOriginRef.current;
|
|
140
155
|
if (changeOrigin !== "replace-silent") {
|
|
@@ -177,23 +192,11 @@ const PrefabEditor = forwardRef(({ basePath, initialPrefab, physics = true, mode
|
|
|
177
192
|
});
|
|
178
193
|
return () => unsubscribe();
|
|
179
194
|
}, [prefabStore, selectedId]);
|
|
180
|
-
const
|
|
181
|
-
const
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
&& isObjectAttachedToRoot(getRootObject(), selectedObject)
|
|
185
|
-
? (selectedHasPhysics ? transformProxyRef.current : selectedObject)
|
|
195
|
+
const selectedObject = selectedId ? getNodeObject(selectedId) : null;
|
|
196
|
+
const transformObject = isEditMode && selectedObject
|
|
197
|
+
&& isObjectAttachedToRoot(getSceneRootObject(), selectedObject)
|
|
198
|
+
? selectedObject
|
|
186
199
|
: null;
|
|
187
|
-
useLayoutEffect(() => {
|
|
188
|
-
if (!isEditMode || !selectedHasPhysics || !selectedObject || !transformProxyRef.current) {
|
|
189
|
-
return;
|
|
190
|
-
}
|
|
191
|
-
selectedObject.updateMatrixWorld(true);
|
|
192
|
-
transformProxyRef.current.matrixAutoUpdate = true;
|
|
193
|
-
selectedObject.matrixWorld.decompose(transformProxyRef.current.position, transformProxyRef.current.quaternion, transformProxyRef.current.scale);
|
|
194
|
-
transformProxyRef.current.updateMatrix();
|
|
195
|
-
transformProxyRef.current.updateMatrixWorld(true);
|
|
196
|
-
}, [isEditMode, selectedHasPhysics, selectedId, selectedObject]);
|
|
197
200
|
const addNode = useCallback((node, options) => {
|
|
198
201
|
var _a;
|
|
199
202
|
const { addChild, rootId } = prefabStore.getState();
|
|
@@ -272,30 +275,30 @@ const PrefabEditor = forwardRef(({ basePath, initialPrefab, physics = true, mode
|
|
|
272
275
|
}), [setSelection]);
|
|
273
276
|
const handleExportGLB = useCallback((...args_1) => __awaiter(void 0, [...args_1], void 0, function* (options = {}) {
|
|
274
277
|
yield clearSelection();
|
|
275
|
-
const rootObject =
|
|
278
|
+
const rootObject = getSceneRootObject();
|
|
276
279
|
if (!rootObject)
|
|
277
280
|
return;
|
|
278
281
|
return exportGLBFile(rootObject, Object.assign({ filename: `${prefabStore.getState().prefabName || 'prefab'}.glb` }, options));
|
|
279
|
-
}), [clearSelection,
|
|
282
|
+
}), [clearSelection, getSceneRootObject, prefabStore]);
|
|
280
283
|
const handleExportGLBData = useCallback(() => __awaiter(void 0, void 0, void 0, function* () {
|
|
281
284
|
yield clearSelection();
|
|
282
|
-
const rootObject =
|
|
285
|
+
const rootObject = getSceneRootObject();
|
|
283
286
|
if (!rootObject)
|
|
284
287
|
return;
|
|
285
288
|
return exportGLBData(rootObject);
|
|
286
|
-
}), [clearSelection,
|
|
289
|
+
}), [clearSelection, getSceneRootObject]);
|
|
287
290
|
const handleFocusNode = useCallback((nodeId) => {
|
|
288
|
-
const object =
|
|
291
|
+
const object = getNodeObject(nodeId);
|
|
289
292
|
const controls = controlsRef.current;
|
|
290
293
|
const camera = controls === null || controls === void 0 ? void 0 : controls.object;
|
|
291
294
|
if (!object || !controls || !camera)
|
|
292
295
|
return;
|
|
293
296
|
focusCameraOnObject(object, camera, controls.target, () => { var _a; return (_a = controls.update) === null || _a === void 0 ? void 0 : _a.call(controls); });
|
|
294
|
-
}, [
|
|
297
|
+
}, [getNodeObject]);
|
|
295
298
|
const handleTransformChange = () => {
|
|
296
299
|
if (!selectedId)
|
|
297
300
|
return;
|
|
298
|
-
const object =
|
|
301
|
+
const object = getNodeObject(selectedId);
|
|
299
302
|
if (!object)
|
|
300
303
|
return;
|
|
301
304
|
const parentWorld = computeParentWorldMatrix(prefabStore.getState(), selectedId);
|
|
@@ -348,21 +351,34 @@ const PrefabEditor = forwardRef(({ basePath, initialPrefab, physics = true, mode
|
|
|
348
351
|
};
|
|
349
352
|
}, [addModel, addTexture, isEditMode, enableWindowDrop]);
|
|
350
353
|
useImperativeHandle(ref, () => ({
|
|
351
|
-
root
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
354
|
+
get root() {
|
|
355
|
+
return getSceneRootObject();
|
|
356
|
+
},
|
|
357
|
+
getNode,
|
|
358
|
+
getNodeObject,
|
|
359
|
+
getNodeHandle,
|
|
360
|
+
onSceneChange: (listener) => {
|
|
361
|
+
sceneChangeListenersRef.current.add(listener);
|
|
362
|
+
return () => {
|
|
363
|
+
sceneChangeListenersRef.current.delete(listener);
|
|
364
|
+
};
|
|
365
|
+
},
|
|
355
366
|
screenshot: handleScreenshot,
|
|
356
367
|
exportGLB: handleExportGLB,
|
|
357
368
|
exportGLBData: handleExportGLBData,
|
|
358
369
|
clearSelection,
|
|
359
370
|
save: getPrefab,
|
|
360
371
|
load: loadPrefab,
|
|
372
|
+
updateNode,
|
|
373
|
+
updateNodes,
|
|
374
|
+
deleteNode,
|
|
375
|
+
duplicateNode,
|
|
376
|
+
moveNode,
|
|
361
377
|
addNode,
|
|
362
378
|
addModel,
|
|
363
379
|
addTexture
|
|
364
|
-
}), [addModel, addNode, addTexture, clearSelection,
|
|
365
|
-
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] }));
|
|
366
382
|
const handleCanvasCreated = useCallback((state) => {
|
|
367
383
|
var _a;
|
|
368
384
|
canvasRef.current = state.gl.domElement;
|
|
@@ -391,7 +407,7 @@ const PrefabEditor = forwardRef(({ basePath, initialPrefab, physics = true, mode
|
|
|
391
407
|
}
|
|
392
408
|
(_d = canvasProps === null || canvasProps === void 0 ? void 0 : canvasProps.onPointerMissed) === null || _d === void 0 ? void 0 : _d.call(canvasProps, event);
|
|
393
409
|
}
|
|
394
|
-
: canvasProps === null || canvasProps === void 0 ? void 0 : canvasProps.onPointerMissed, children: [
|
|
410
|
+
: canvasProps === null || canvasProps === void 0 ? void 0 : canvasProps.onPointerMissed, children: [content, isEditMode ? _jsx(SelectionHelper, { object: transformObject }) : null, isEditMode && (_jsxs(_Fragment, { children: [_jsx(MapControls, { ref: controlsRef, enableDamping: false, makeDefault: true }), transformObject && (_jsx(TransformControls, { ref: transformControlsRef, object: transformObject, mode: transformMode, space: "local", onObjectChange: handleTransformChange, translationSnap: positionSnap > 0 ? positionSnap : undefined, rotationSnap: rotationSnap > 0 ? rotationSnap : undefined, scaleSnap: scaleSnap > 0 ? scaleSnap : undefined }, `transform-${selectedId}-${transformMode}-${positionSnap}-${rotationSnap}-${scaleSnap}`))] }))] })), showUI && (_jsxs(_Fragment, { children: [_jsxs("div", { style: toolbar.panel, children: [_jsx("button", { style: base.btn, onClick: toggleMode, children: isEditMode ? "▶" : "⏸" }), uiPlugins] }), isEditMode && (_jsx(EditorUI, { selectedId: selectedId, setSelectedId: setSelection, getPrefab: getPrefab, onReplacePrefab: (prefab) => loadPrefab(prefab, { resetHistory: true }), onImportPrefab: importPrefab, basePath: basePath, onUndo: undo, onRedo: redo, canUndo: historyIndex > 0, canRedo: historyIndex < history.length - 1 }))] }))] }) });
|
|
395
411
|
});
|
|
396
412
|
PrefabEditor.displayName = "PrefabEditor";
|
|
397
413
|
export default PrefabEditor;
|