react-three-game 0.0.21 → 0.0.23
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/dist/tools/prefabeditor/InstanceProvider.d.ts +8 -9
- package/dist/tools/prefabeditor/InstanceProvider.js +100 -130
- package/dist/tools/prefabeditor/PrefabRoot.js +6 -15
- package/dist/tools/prefabeditor/components/DirectionalLightComponent.js +10 -46
- package/dist/tools/prefabeditor/components/MaterialComponent.js +1 -1
- package/dist/tools/prefabeditor/components/ModelComponent.js +13 -9
- package/dist/tools/prefabeditor/components/PhysicsComponent.js +2 -14
- package/dist/tools/prefabeditor/components/SpotLightComponent.js +1 -1
- package/package.json +1 -1
- package/src/tools/prefabeditor/InstanceProvider.tsx +197 -207
- package/src/tools/prefabeditor/PrefabRoot.tsx +20 -33
- package/src/tools/prefabeditor/components/DirectionalLightComponent.tsx +20 -61
- package/src/tools/prefabeditor/components/MaterialComponent.tsx +10 -8
- package/src/tools/prefabeditor/components/ModelComponent.tsx +13 -9
- package/src/tools/prefabeditor/components/PhysicsComponent.tsx +6 -5
- package/src/tools/prefabeditor/components/SpotLightComponent.tsx +2 -0
|
@@ -1,185 +1,170 @@
|
|
|
1
1
|
import React, { createContext, useContext, useMemo, useRef, useState, useEffect, useCallback } from "react";
|
|
2
2
|
import { Merged } from '@react-three/drei';
|
|
3
|
-
import { InstancedRigidBodies } from "@react-three/rapier";
|
|
4
|
-
import {
|
|
3
|
+
import { InstancedRigidBodies, RigidBodyProps } from "@react-three/rapier";
|
|
4
|
+
import { useFrame } from "@react-three/fiber";
|
|
5
|
+
import { Mesh, Matrix4, Object3D, Euler, Quaternion, Vector3, InstancedMesh } from "three";
|
|
5
6
|
|
|
6
7
|
// --- Types ---
|
|
7
8
|
export type InstanceData = {
|
|
8
9
|
id: string;
|
|
10
|
+
meshPath: string;
|
|
9
11
|
position: [number, number, number];
|
|
10
12
|
rotation: [number, number, number];
|
|
11
13
|
scale: [number, number, number];
|
|
12
|
-
|
|
13
|
-
physics?: { type: 'dynamic' | 'fixed' };
|
|
14
|
+
physics?: { type: RigidBodyProps['type'] };
|
|
14
15
|
};
|
|
15
16
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
for (let i = 0; i < a.length; i++) {
|
|
21
|
-
if (a[i] !== b[i]) return false;
|
|
22
|
-
}
|
|
23
|
-
return true;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
function instanceEquals(a: InstanceData, b: InstanceData): boolean {
|
|
27
|
-
return a.id === b.id &&
|
|
28
|
-
a.meshPath === b.meshPath &&
|
|
29
|
-
arrayEquals(a.position, b.position) &&
|
|
30
|
-
arrayEquals(a.rotation, b.rotation) &&
|
|
31
|
-
arrayEquals(a.scale, b.scale) &&
|
|
32
|
-
a.physics?.type === b.physics?.type;
|
|
33
|
-
}
|
|
17
|
+
type GroupedInstances = Record<string, {
|
|
18
|
+
physicsType: string;
|
|
19
|
+
instances: InstanceData[];
|
|
20
|
+
}>;
|
|
34
21
|
|
|
35
|
-
// --- Context ---
|
|
36
22
|
type GameInstanceContextType = {
|
|
37
23
|
addInstance: (instance: InstanceData) => void;
|
|
38
24
|
removeInstance: (id: string) => void;
|
|
39
|
-
instances: InstanceData[];
|
|
40
|
-
meshes: Record<string, Mesh>;
|
|
41
|
-
instancesMap?: Record<string, React.ComponentType<any>>;
|
|
42
|
-
modelParts?: Record<string, number>;
|
|
43
25
|
};
|
|
26
|
+
|
|
27
|
+
// --- Helpers ---
|
|
28
|
+
const arraysEqual = (a: number[], b: number[]) =>
|
|
29
|
+
a.length === b.length && a.every((v, i) => v === b[i]);
|
|
30
|
+
|
|
31
|
+
const instancesEqual = (a: InstanceData, b: InstanceData) =>
|
|
32
|
+
a.id === b.id &&
|
|
33
|
+
a.meshPath === b.meshPath &&
|
|
34
|
+
a.physics?.type === b.physics?.type &&
|
|
35
|
+
arraysEqual(a.position, b.position) &&
|
|
36
|
+
arraysEqual(a.rotation, b.rotation) &&
|
|
37
|
+
arraysEqual(a.scale, b.scale);
|
|
38
|
+
|
|
39
|
+
// Reusable objects for matrix computation (avoid allocations in hot paths)
|
|
40
|
+
const _matrix = new Matrix4();
|
|
41
|
+
const _position = new Vector3();
|
|
42
|
+
const _quaternion = new Quaternion();
|
|
43
|
+
const _euler = new Euler();
|
|
44
|
+
const _scale = new Vector3();
|
|
45
|
+
|
|
46
|
+
function composeMatrix(
|
|
47
|
+
position: [number, number, number],
|
|
48
|
+
rotation: [number, number, number],
|
|
49
|
+
scale: [number, number, number],
|
|
50
|
+
target: Matrix4 = _matrix
|
|
51
|
+
): Matrix4 {
|
|
52
|
+
_position.set(...position);
|
|
53
|
+
_quaternion.setFromEuler(_euler.set(...rotation));
|
|
54
|
+
_scale.set(...scale);
|
|
55
|
+
return target.compose(_position, _quaternion, _scale);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// --- Context ---
|
|
44
59
|
const GameInstanceContext = createContext<GameInstanceContextType | null>(null);
|
|
45
60
|
|
|
61
|
+
// --- Provider ---
|
|
46
62
|
export function GameInstanceProvider({
|
|
47
63
|
children,
|
|
48
64
|
models,
|
|
49
65
|
onSelect,
|
|
50
66
|
registerRef
|
|
51
67
|
}: {
|
|
52
|
-
children: React.ReactNode
|
|
53
|
-
models:
|
|
54
|
-
onSelect?: (id: string | null) => void
|
|
55
|
-
registerRef?: (id: string, obj: Object3D | null) => void
|
|
68
|
+
children: React.ReactNode;
|
|
69
|
+
models: Record<string, Object3D>;
|
|
70
|
+
onSelect?: (id: string | null) => void;
|
|
71
|
+
registerRef?: (id: string, obj: Object3D | null) => void;
|
|
56
72
|
}) {
|
|
57
73
|
const [instances, setInstances] = useState<InstanceData[]>([]);
|
|
58
74
|
|
|
59
75
|
const addInstance = useCallback((instance: InstanceData) => {
|
|
60
76
|
setInstances(prev => {
|
|
61
77
|
const idx = prev.findIndex(i => i.id === instance.id);
|
|
62
|
-
if (idx
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
const copy = [...prev];
|
|
68
|
-
copy[idx] = instance;
|
|
69
|
-
return copy;
|
|
70
|
-
}
|
|
71
|
-
// Add new
|
|
72
|
-
return [...prev, instance];
|
|
78
|
+
if (idx === -1) return [...prev, instance];
|
|
79
|
+
if (instancesEqual(prev[idx], instance)) return prev;
|
|
80
|
+
const updated = [...prev];
|
|
81
|
+
updated[idx] = instance;
|
|
82
|
+
return updated;
|
|
73
83
|
});
|
|
74
84
|
}, []);
|
|
75
85
|
|
|
76
86
|
const removeInstance = useCallback((id: string) => {
|
|
77
|
-
setInstances(prev =>
|
|
78
|
-
if (!prev.find(i => i.id === id)) return prev;
|
|
79
|
-
return prev.filter(i => i.id !== id);
|
|
80
|
-
});
|
|
87
|
+
setInstances(prev => prev.filter(i => i.id !== id));
|
|
81
88
|
}, []);
|
|
82
89
|
|
|
83
|
-
//
|
|
84
|
-
const {
|
|
85
|
-
const
|
|
86
|
-
const
|
|
90
|
+
// Extract mesh parts from models with baked local transforms
|
|
91
|
+
const { meshParts, partCounts } = useMemo(() => {
|
|
92
|
+
const meshParts: Record<string, Mesh> = {};
|
|
93
|
+
const partCounts: Record<string, number> = {};
|
|
87
94
|
|
|
88
95
|
Object.entries(models).forEach(([modelKey, model]) => {
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
const rootInverse = new Matrix4().copy(root.matrixWorld).invert();
|
|
92
|
-
|
|
96
|
+
model.updateWorldMatrix(false, true);
|
|
97
|
+
const rootInverse = new Matrix4().copy(model.matrixWorld).invert();
|
|
93
98
|
let partIndex = 0;
|
|
94
99
|
|
|
95
|
-
|
|
96
|
-
if (
|
|
97
|
-
const
|
|
98
|
-
const
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
const partKey = `${modelKey}__${partIndex}`;
|
|
102
|
-
flatMeshes[partKey] = new Mesh(geom, obj.material);
|
|
100
|
+
model.traverse((child: Object3D) => {
|
|
101
|
+
if ((child as Mesh).isMesh) {
|
|
102
|
+
const mesh = child as Mesh;
|
|
103
|
+
const geometry = mesh.geometry.clone();
|
|
104
|
+
geometry.applyMatrix4(mesh.matrixWorld.clone().premultiply(rootInverse));
|
|
105
|
+
meshParts[`${modelKey}__${partIndex}`] = new Mesh(geometry, mesh.material);
|
|
103
106
|
partIndex++;
|
|
104
107
|
}
|
|
105
108
|
});
|
|
106
|
-
|
|
109
|
+
partCounts[modelKey] = partIndex;
|
|
107
110
|
});
|
|
108
111
|
|
|
109
|
-
return {
|
|
112
|
+
return { meshParts, partCounts };
|
|
110
113
|
}, [models]);
|
|
111
114
|
|
|
112
|
-
//
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
115
|
+
// Cleanup cloned geometries
|
|
116
|
+
useEffect(() => () => {
|
|
117
|
+
Object.values(meshParts).forEach(mesh => mesh.geometry.dispose());
|
|
118
|
+
}, [meshParts]);
|
|
119
|
+
|
|
120
|
+
// Group instances by model + physics type
|
|
121
|
+
const grouped = useMemo<GroupedInstances>(() => {
|
|
122
|
+
const groups: GroupedInstances = {};
|
|
123
|
+
instances.forEach(inst => {
|
|
124
|
+
const physicsType = inst.physics?.type ?? 'none';
|
|
125
|
+
const key = `${inst.meshPath}__${physicsType}`;
|
|
126
|
+
groups[key] ??= { physicsType, instances: [] };
|
|
119
127
|
groups[key].instances.push(inst);
|
|
120
|
-
}
|
|
128
|
+
});
|
|
121
129
|
return groups;
|
|
122
130
|
}, [instances]);
|
|
123
131
|
|
|
132
|
+
const contextValue = useMemo(() => ({ addInstance, removeInstance }), [addInstance, removeInstance]);
|
|
133
|
+
|
|
124
134
|
return (
|
|
125
|
-
<GameInstanceContext.Provider
|
|
126
|
-
value={{
|
|
127
|
-
addInstance,
|
|
128
|
-
removeInstance,
|
|
129
|
-
instances,
|
|
130
|
-
meshes: flatMeshes,
|
|
131
|
-
modelParts
|
|
132
|
-
}}
|
|
133
|
-
>
|
|
134
|
-
{/* Render normal prefab hierarchy (non-instanced objects) */}
|
|
135
|
+
<GameInstanceContext.Provider value={contextValue}>
|
|
135
136
|
{children}
|
|
136
137
|
|
|
137
|
-
{/* Render physics-enabled instanced groups using InstancedRigidBodies */}
|
|
138
138
|
{Object.entries(grouped).map(([key, group]) => {
|
|
139
|
-
if (group.physicsType === 'none') return null;
|
|
140
139
|
const modelKey = group.instances[0].meshPath;
|
|
141
|
-
const partCount =
|
|
140
|
+
const partCount = partCounts[modelKey] ?? 0;
|
|
142
141
|
if (partCount === 0) return null;
|
|
143
142
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
{/* Render non-physics instanced visuals using Merged (one per model type) */}
|
|
156
|
-
{Object.entries(grouped).map(([key, group]) => {
|
|
157
|
-
if (group.physicsType !== 'none') return null;
|
|
158
|
-
|
|
159
|
-
const modelKey = group.instances[0].meshPath;
|
|
160
|
-
const partCount = modelParts[modelKey] || 0;
|
|
161
|
-
if (partCount === 0) return null;
|
|
162
|
-
|
|
163
|
-
// Create mesh subset for this specific model
|
|
164
|
-
const meshesForModel: Record<string, Mesh> = {};
|
|
165
|
-
for (let i = 0; i < partCount; i++) {
|
|
166
|
-
const partKey = `${modelKey}__${i}`;
|
|
167
|
-
meshesForModel[partKey] = flatMeshes[partKey];
|
|
143
|
+
if (group.physicsType !== 'none') {
|
|
144
|
+
return (
|
|
145
|
+
<PhysicsInstances
|
|
146
|
+
key={key}
|
|
147
|
+
instances={group.instances}
|
|
148
|
+
physicsType={group.physicsType as RigidBodyProps['type']}
|
|
149
|
+
modelKey={modelKey}
|
|
150
|
+
partCount={partCount}
|
|
151
|
+
meshParts={meshParts}
|
|
152
|
+
/>
|
|
153
|
+
);
|
|
168
154
|
}
|
|
169
155
|
|
|
156
|
+
const modelMeshes = Object.fromEntries(
|
|
157
|
+
Array.from({ length: partCount }, (_, i) => [`${modelKey}__${i}`, meshParts[`${modelKey}__${i}`]])
|
|
158
|
+
);
|
|
159
|
+
|
|
170
160
|
return (
|
|
171
|
-
<Merged
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
receiveShadow
|
|
176
|
-
>
|
|
177
|
-
{(instancesMap: any) => (
|
|
178
|
-
<NonPhysicsInstancedGroup
|
|
161
|
+
<Merged key={key} meshes={modelMeshes} castShadow receiveShadow>
|
|
162
|
+
{(Components: Record<string, React.ComponentType>) => (
|
|
163
|
+
<StaticInstances
|
|
164
|
+
instances={group.instances}
|
|
179
165
|
modelKey={modelKey}
|
|
180
|
-
group={group}
|
|
181
166
|
partCount={partCount}
|
|
182
|
-
|
|
167
|
+
Components={Components}
|
|
183
168
|
onSelect={onSelect}
|
|
184
169
|
registerRef={registerRef}
|
|
185
170
|
/>
|
|
@@ -191,129 +176,137 @@ export function GameInstanceProvider({
|
|
|
191
176
|
);
|
|
192
177
|
}
|
|
193
178
|
|
|
194
|
-
//
|
|
195
|
-
function
|
|
196
|
-
|
|
179
|
+
// --- Physics Instances ---
|
|
180
|
+
function PhysicsInstances({
|
|
181
|
+
instances,
|
|
182
|
+
physicsType,
|
|
197
183
|
modelKey,
|
|
198
184
|
partCount,
|
|
199
|
-
|
|
185
|
+
meshParts
|
|
200
186
|
}: {
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
187
|
+
instances: InstanceData[];
|
|
188
|
+
physicsType: RigidBodyProps['type'];
|
|
189
|
+
modelKey: string;
|
|
190
|
+
partCount: number;
|
|
191
|
+
meshParts: Record<string, Mesh>;
|
|
205
192
|
}) {
|
|
206
|
-
const
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
scale: inst.scale,
|
|
212
|
-
})),
|
|
213
|
-
[group.instances]
|
|
193
|
+
const meshRefs = useRef<(InstancedMesh | null)[]>([]);
|
|
194
|
+
|
|
195
|
+
const rigidBodyInstances = useMemo(() =>
|
|
196
|
+
instances.map(({ id, position, rotation, scale }) => ({ key: id, position, rotation, scale })),
|
|
197
|
+
[instances]
|
|
214
198
|
);
|
|
215
199
|
|
|
200
|
+
// Sync visual matrices each frame (physics updates position/rotation, we need to apply scale)
|
|
201
|
+
useFrame(() => {
|
|
202
|
+
meshRefs.current.forEach(mesh => {
|
|
203
|
+
if (!mesh) return;
|
|
204
|
+
instances.forEach((inst, i) => {
|
|
205
|
+
mesh.setMatrixAt(i, composeMatrix(inst.position, inst.rotation, inst.scale));
|
|
206
|
+
});
|
|
207
|
+
mesh.instanceMatrix.needsUpdate = true;
|
|
208
|
+
});
|
|
209
|
+
});
|
|
210
|
+
|
|
216
211
|
return (
|
|
217
212
|
<InstancedRigidBodies
|
|
218
|
-
instances={
|
|
219
|
-
|
|
220
|
-
|
|
213
|
+
instances={rigidBodyInstances}
|
|
214
|
+
type={physicsType}
|
|
215
|
+
colliders={physicsType === 'fixed' ? 'trimesh' : 'hull'}
|
|
221
216
|
>
|
|
222
|
-
{Array.from({ length: partCount }
|
|
223
|
-
const mesh =
|
|
224
|
-
return (
|
|
217
|
+
{Array.from({ length: partCount }, (_, i) => {
|
|
218
|
+
const mesh = meshParts[`${modelKey}__${i}`];
|
|
219
|
+
return mesh ? (
|
|
225
220
|
<instancedMesh
|
|
226
221
|
key={i}
|
|
227
|
-
|
|
222
|
+
ref={el => { meshRefs.current[i] = el; }}
|
|
223
|
+
args={[mesh.geometry, mesh.material, instances.length]}
|
|
224
|
+
frustumCulled={false}
|
|
228
225
|
castShadow
|
|
229
226
|
receiveShadow
|
|
230
|
-
frustumCulled={false}
|
|
231
227
|
/>
|
|
232
|
-
);
|
|
228
|
+
) : null;
|
|
233
229
|
})}
|
|
234
230
|
</InstancedRigidBodies>
|
|
235
231
|
);
|
|
236
232
|
}
|
|
237
233
|
|
|
238
|
-
//
|
|
239
|
-
function
|
|
234
|
+
// --- Static Instances (non-physics) ---
|
|
235
|
+
function StaticInstances({
|
|
236
|
+
instances,
|
|
240
237
|
modelKey,
|
|
241
|
-
group,
|
|
242
238
|
partCount,
|
|
243
|
-
|
|
239
|
+
Components,
|
|
244
240
|
onSelect,
|
|
245
241
|
registerRef
|
|
246
242
|
}: {
|
|
243
|
+
instances: InstanceData[];
|
|
247
244
|
modelKey: string;
|
|
248
|
-
group: { physicsType: string, instances: InstanceData[] };
|
|
249
245
|
partCount: number;
|
|
250
|
-
|
|
246
|
+
Components: Record<string, React.ComponentType>;
|
|
251
247
|
onSelect?: (id: string | null) => void;
|
|
252
248
|
registerRef?: (id: string, obj: Object3D | null) => void;
|
|
253
249
|
}) {
|
|
254
|
-
const
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
clickValid.current = true;
|
|
259
|
-
};
|
|
260
|
-
|
|
261
|
-
const handlePointerMove = () => {
|
|
262
|
-
if (clickValid.current) clickValid.current = false;
|
|
263
|
-
};
|
|
264
|
-
|
|
265
|
-
const handlePointerUp = (e: any, id: string) => {
|
|
266
|
-
if (clickValid.current) {
|
|
267
|
-
e.stopPropagation();
|
|
268
|
-
onSelect?.(id);
|
|
269
|
-
}
|
|
270
|
-
clickValid.current = false;
|
|
271
|
-
};
|
|
250
|
+
const Parts = useMemo(() =>
|
|
251
|
+
Array.from({ length: partCount }, (_, i) => Components[`${modelKey}__${i}`]).filter(Boolean),
|
|
252
|
+
[Components, modelKey, partCount]
|
|
253
|
+
);
|
|
272
254
|
|
|
273
255
|
return (
|
|
274
256
|
<>
|
|
275
|
-
{
|
|
276
|
-
<
|
|
277
|
-
key={inst.id}
|
|
278
|
-
ref={(el) => { registerRef?.(inst.id, el as unknown as Object3D | null); }}
|
|
279
|
-
position={inst.position}
|
|
280
|
-
rotation={inst.rotation}
|
|
281
|
-
scale={inst.scale}
|
|
282
|
-
onPointerDown={handlePointerDown}
|
|
283
|
-
onPointerMove={handlePointerMove}
|
|
284
|
-
onPointerUp={(e) => handlePointerUp(e, inst.id)}
|
|
285
|
-
>
|
|
286
|
-
{Array.from({ length: partCount }).map((_, i) => {
|
|
287
|
-
const Instance = instancesMap[`${modelKey}__${i}`];
|
|
288
|
-
if (!Instance) return null;
|
|
289
|
-
return <Instance key={i} />;
|
|
290
|
-
})}
|
|
291
|
-
</group>
|
|
257
|
+
{instances.map(inst => (
|
|
258
|
+
<InstanceItem key={inst.id} instance={inst} Parts={Parts} onSelect={onSelect} registerRef={registerRef} />
|
|
292
259
|
))}
|
|
293
260
|
</>
|
|
294
261
|
);
|
|
295
262
|
}
|
|
296
263
|
|
|
264
|
+
// --- Single Instance ---
|
|
265
|
+
function InstanceItem({
|
|
266
|
+
instance,
|
|
267
|
+
Parts,
|
|
268
|
+
onSelect,
|
|
269
|
+
registerRef
|
|
270
|
+
}: {
|
|
271
|
+
instance: InstanceData;
|
|
272
|
+
Parts: React.ComponentType[];
|
|
273
|
+
onSelect?: (id: string | null) => void;
|
|
274
|
+
registerRef?: (id: string, obj: Object3D | null) => void;
|
|
275
|
+
}) {
|
|
276
|
+
const moved = useRef(false);
|
|
277
|
+
|
|
278
|
+
return (
|
|
279
|
+
<group
|
|
280
|
+
ref={el => registerRef?.(instance.id, el)}
|
|
281
|
+
position={instance.position}
|
|
282
|
+
rotation={instance.rotation}
|
|
283
|
+
scale={instance.scale}
|
|
284
|
+
onPointerDown={e => { e.stopPropagation(); moved.current = false; }}
|
|
285
|
+
onPointerMove={() => { moved.current = true; }}
|
|
286
|
+
onPointerUp={e => { e.stopPropagation(); if (!moved.current) onSelect?.(instance.id); }}
|
|
287
|
+
>
|
|
288
|
+
{Parts.map((Part, i) => <Part key={i} />)}
|
|
289
|
+
</group>
|
|
290
|
+
);
|
|
291
|
+
}
|
|
297
292
|
|
|
298
|
-
// GameInstance
|
|
299
|
-
export
|
|
300
|
-
id: string;
|
|
301
|
-
modelUrl: string;
|
|
302
|
-
position: [number, number, number];
|
|
303
|
-
rotation: [number, number, number];
|
|
304
|
-
scale: [number, number, number];
|
|
305
|
-
physics?: { type: 'dynamic' | 'fixed' };
|
|
306
|
-
}>(({
|
|
293
|
+
// --- GameInstance (declarative registration) ---
|
|
294
|
+
export function GameInstance({
|
|
307
295
|
id,
|
|
308
296
|
modelUrl,
|
|
309
297
|
position,
|
|
310
298
|
rotation,
|
|
311
299
|
scale,
|
|
312
|
-
physics
|
|
313
|
-
}
|
|
300
|
+
physics
|
|
301
|
+
}: {
|
|
302
|
+
id: string;
|
|
303
|
+
modelUrl: string;
|
|
304
|
+
position: [number, number, number];
|
|
305
|
+
rotation: [number, number, number];
|
|
306
|
+
scale: [number, number, number];
|
|
307
|
+
physics?: { type: RigidBodyProps['type'] };
|
|
308
|
+
}) {
|
|
314
309
|
const ctx = useContext(GameInstanceContext);
|
|
315
|
-
const addInstance = ctx?.addInstance;
|
|
316
|
-
const removeInstance = ctx?.removeInstance;
|
|
317
310
|
|
|
318
311
|
const instance = useMemo<InstanceData>(() => ({
|
|
319
312
|
id,
|
|
@@ -325,13 +318,10 @@ export const GameInstance = React.forwardRef<Group, {
|
|
|
325
318
|
}), [id, modelUrl, position, rotation, scale, physics]);
|
|
326
319
|
|
|
327
320
|
useEffect(() => {
|
|
328
|
-
if (!
|
|
329
|
-
addInstance(instance);
|
|
330
|
-
return () =>
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
}, [addInstance, removeInstance, instance]);
|
|
334
|
-
|
|
335
|
-
// No visual rendering - provider handles all instanced visuals
|
|
321
|
+
if (!ctx) return;
|
|
322
|
+
ctx.addInstance(instance);
|
|
323
|
+
return () => ctx.removeInstance(id);
|
|
324
|
+
}, [ctx, instance, id]);
|
|
325
|
+
|
|
336
326
|
return null;
|
|
337
|
-
}
|
|
327
|
+
}
|
|
@@ -237,49 +237,36 @@ function GameObjectRenderer({
|
|
|
237
237
|
/>
|
|
238
238
|
));
|
|
239
239
|
|
|
240
|
-
// --- 6.
|
|
241
|
-
const
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
240
|
+
// --- 6. Inner content group with full transform ---
|
|
241
|
+
const innerGroup = (
|
|
242
|
+
<group
|
|
243
|
+
ref={(el) => registerRef(gameObject.id, el)}
|
|
244
|
+
position={transformProps.position}
|
|
245
|
+
rotation={transformProps.rotation}
|
|
246
|
+
scale={transformProps.scale}
|
|
247
|
+
onPointerDown={handlePointerDown}
|
|
248
|
+
onPointerMove={handlePointerMove}
|
|
249
|
+
onPointerUp={handlePointerUp}
|
|
250
|
+
>
|
|
251
|
+
{core}
|
|
252
|
+
{children}
|
|
253
|
+
</group>
|
|
254
|
+
);
|
|
248
255
|
|
|
249
|
-
// --- 7.
|
|
256
|
+
// --- 7. Wrap with physics if needed (RigidBody as outer parent, no transform) ---
|
|
250
257
|
const physics = gameObject.components?.physics;
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
// --- 8. Final structure: RigidBody outside with position/rotation, scale inside ---
|
|
254
|
-
if (hasPhysics) {
|
|
258
|
+
if (physics && !editMode) {
|
|
255
259
|
const physicsDef = getComponent('Physics');
|
|
256
260
|
if (physicsDef?.View) {
|
|
257
261
|
return (
|
|
258
|
-
<physicsDef.View
|
|
259
|
-
|
|
260
|
-
ref={(obj: Object3D | null) => registerRef(gameObject.id, obj)}
|
|
261
|
-
{...wrapperProps}
|
|
262
|
-
>
|
|
263
|
-
<group scale={transformProps.scale}>
|
|
264
|
-
{core}
|
|
265
|
-
{children}
|
|
266
|
-
</group>
|
|
262
|
+
<physicsDef.View properties={physics.properties}>
|
|
263
|
+
{innerGroup}
|
|
267
264
|
</physicsDef.View>
|
|
268
265
|
);
|
|
269
266
|
}
|
|
270
267
|
}
|
|
271
268
|
|
|
272
|
-
|
|
273
|
-
return (
|
|
274
|
-
<group
|
|
275
|
-
ref={(el) => registerRef(gameObject.id, el)}
|
|
276
|
-
scale={transformProps.scale}
|
|
277
|
-
{...wrapperProps}
|
|
278
|
-
>
|
|
279
|
-
{core}
|
|
280
|
-
{children}
|
|
281
|
-
</group>
|
|
282
|
-
);
|
|
269
|
+
return innerGroup;
|
|
283
270
|
}
|
|
284
271
|
|
|
285
272
|
// Helper: render an instanced GameInstance (terminal node)
|