react-three-game 0.0.17 → 0.0.18
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 +4 -4
- package/dist/tools/prefabeditor/InstanceProvider.js +21 -13
- package/dist/tools/prefabeditor/PrefabRoot.js +26 -27
- package/dist/tools/prefabeditor/components/DirectionalLightComponent.d.ts +3 -0
- package/dist/tools/prefabeditor/components/DirectionalLightComponent.js +114 -0
- package/dist/tools/prefabeditor/components/ModelComponent.js +12 -4
- package/dist/tools/prefabeditor/components/SpotLightComponent.js +10 -5
- package/dist/tools/prefabeditor/components/index.js +2 -0
- package/dist/tools/prefabeditor/hooks/useModelLoader.d.ts +10 -0
- package/dist/tools/prefabeditor/hooks/useModelLoader.js +40 -0
- package/package.json +3 -3
- package/src/tools/prefabeditor/InstanceProvider.tsx +43 -32
- package/src/tools/prefabeditor/PrefabRoot.tsx +30 -41
- package/src/tools/prefabeditor/components/DirectionalLightComponent.tsx +332 -0
- package/src/tools/prefabeditor/components/ModelComponent.tsx +14 -4
- package/src/tools/prefabeditor/components/SpotLightComponent.tsx +27 -7
- package/src/tools/prefabeditor/components/index.ts +2 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import React, { createContext, useContext, useMemo, useRef, useState, useEffect, useCallback } from "react";
|
|
2
2
|
import { Merged } from '@react-three/drei';
|
|
3
|
-
import * as THREE from 'three';
|
|
4
3
|
import { InstancedRigidBodies } from "@react-three/rapier";
|
|
4
|
+
import { Mesh, Matrix4, Object3D, Group } from "three";
|
|
5
5
|
|
|
6
6
|
// --- Types ---
|
|
7
7
|
export type InstanceData = {
|
|
@@ -13,7 +13,8 @@ export type InstanceData = {
|
|
|
13
13
|
physics?: { type: 'dynamic' | 'fixed' };
|
|
14
14
|
};
|
|
15
15
|
|
|
16
|
-
|
|
16
|
+
// Helper functions for comparison
|
|
17
|
+
function arrayEquals(a: number[], b: number[]): boolean {
|
|
17
18
|
if (a === b) return true;
|
|
18
19
|
if (a.length !== b.length) return false;
|
|
19
20
|
for (let i = 0; i < a.length; i++) {
|
|
@@ -22,7 +23,7 @@ function arrayEquals(a: number[], b: number[]) {
|
|
|
22
23
|
return true;
|
|
23
24
|
}
|
|
24
25
|
|
|
25
|
-
function instanceEquals(a: InstanceData, b: InstanceData) {
|
|
26
|
+
function instanceEquals(a: InstanceData, b: InstanceData): boolean {
|
|
26
27
|
return a.id === b.id &&
|
|
27
28
|
a.meshPath === b.meshPath &&
|
|
28
29
|
arrayEquals(a.position, b.position) &&
|
|
@@ -36,7 +37,7 @@ type GameInstanceContextType = {
|
|
|
36
37
|
addInstance: (instance: InstanceData) => void;
|
|
37
38
|
removeInstance: (id: string) => void;
|
|
38
39
|
instances: InstanceData[];
|
|
39
|
-
meshes: Record<string,
|
|
40
|
+
meshes: Record<string, Mesh>;
|
|
40
41
|
instancesMap?: Record<string, React.ComponentType<any>>;
|
|
41
42
|
modelParts?: Record<string, number>;
|
|
42
43
|
};
|
|
@@ -44,13 +45,14 @@ const GameInstanceContext = createContext<GameInstanceContextType | null>(null);
|
|
|
44
45
|
|
|
45
46
|
export function GameInstanceProvider({
|
|
46
47
|
children,
|
|
47
|
-
models
|
|
48
|
-
|
|
48
|
+
models,
|
|
49
|
+
onSelect,
|
|
50
|
+
registerRef
|
|
49
51
|
}: {
|
|
50
52
|
children: React.ReactNode,
|
|
51
|
-
models: { [filename: string]:
|
|
53
|
+
models: { [filename: string]: Object3D },
|
|
52
54
|
onSelect?: (id: string | null) => void,
|
|
53
|
-
registerRef?: (id: string, obj:
|
|
55
|
+
registerRef?: (id: string, obj: Object3D | null) => void,
|
|
54
56
|
}) {
|
|
55
57
|
const [instances, setInstances] = useState<InstanceData[]>([]);
|
|
56
58
|
|
|
@@ -58,6 +60,7 @@ export function GameInstanceProvider({
|
|
|
58
60
|
setInstances(prev => {
|
|
59
61
|
const idx = prev.findIndex(i => i.id === instance.id);
|
|
60
62
|
if (idx !== -1) {
|
|
63
|
+
// Update existing if changed
|
|
61
64
|
if (instanceEquals(prev[idx], instance)) {
|
|
62
65
|
return prev;
|
|
63
66
|
}
|
|
@@ -65,6 +68,7 @@ export function GameInstanceProvider({
|
|
|
65
68
|
copy[idx] = instance;
|
|
66
69
|
return copy;
|
|
67
70
|
}
|
|
71
|
+
// Add new
|
|
68
72
|
return [...prev, instance];
|
|
69
73
|
});
|
|
70
74
|
}, []);
|
|
@@ -76,27 +80,26 @@ export function GameInstanceProvider({
|
|
|
76
80
|
});
|
|
77
81
|
}, []);
|
|
78
82
|
|
|
79
|
-
// Flatten all model meshes once
|
|
83
|
+
// Flatten all model meshes once (models → flat mesh parts)
|
|
80
84
|
const { flatMeshes, modelParts } = useMemo(() => {
|
|
81
|
-
const flatMeshes: Record<string,
|
|
85
|
+
const flatMeshes: Record<string, Mesh> = {};
|
|
82
86
|
const modelParts: Record<string, number> = {};
|
|
83
87
|
|
|
84
88
|
Object.entries(models).forEach(([modelKey, model]) => {
|
|
85
89
|
const root = model;
|
|
86
90
|
root.updateWorldMatrix(false, true);
|
|
87
|
-
const rootInverse = new
|
|
91
|
+
const rootInverse = new Matrix4().copy(root.matrixWorld).invert();
|
|
88
92
|
|
|
89
93
|
let partIndex = 0;
|
|
90
94
|
|
|
91
95
|
root.traverse((obj: any) => {
|
|
92
96
|
if (obj.isMesh) {
|
|
93
97
|
const geom = obj.geometry.clone();
|
|
94
|
-
|
|
95
98
|
const relativeTransform = obj.matrixWorld.clone().premultiply(rootInverse);
|
|
96
99
|
geom.applyMatrix4(relativeTransform);
|
|
97
100
|
|
|
98
101
|
const partKey = `${modelKey}__${partIndex}`;
|
|
99
|
-
flatMeshes[partKey] = new
|
|
102
|
+
flatMeshes[partKey] = new Mesh(geom, obj.material);
|
|
100
103
|
partIndex++;
|
|
101
104
|
}
|
|
102
105
|
});
|
|
@@ -106,7 +109,7 @@ export function GameInstanceProvider({
|
|
|
106
109
|
return { flatMeshes, modelParts };
|
|
107
110
|
}, [models]);
|
|
108
111
|
|
|
109
|
-
// Group instances by meshPath + physics type
|
|
112
|
+
// Group instances by meshPath + physics type for batch rendering
|
|
110
113
|
const grouped = useMemo(() => {
|
|
111
114
|
const groups: Record<string, { physicsType: string, instances: InstanceData[] }> = {};
|
|
112
115
|
for (const inst of instances) {
|
|
@@ -128,10 +131,10 @@ export function GameInstanceProvider({
|
|
|
128
131
|
modelParts
|
|
129
132
|
}}
|
|
130
133
|
>
|
|
131
|
-
{/*
|
|
134
|
+
{/* Render normal prefab hierarchy (non-instanced objects) */}
|
|
132
135
|
{children}
|
|
133
136
|
|
|
134
|
-
{/*
|
|
137
|
+
{/* Render physics-enabled instanced groups using InstancedRigidBodies */}
|
|
135
138
|
{Object.entries(grouped).map(([key, group]) => {
|
|
136
139
|
if (group.physicsType === 'none') return null;
|
|
137
140
|
const modelKey = group.instances[0].meshPath;
|
|
@@ -149,7 +152,7 @@ export function GameInstanceProvider({
|
|
|
149
152
|
);
|
|
150
153
|
})}
|
|
151
154
|
|
|
152
|
-
{/*
|
|
155
|
+
{/* Render non-physics instanced visuals using Merged (one per model type) */}
|
|
153
156
|
{Object.entries(grouped).map(([key, group]) => {
|
|
154
157
|
if (group.physicsType !== 'none') return null;
|
|
155
158
|
|
|
@@ -157,8 +160,8 @@ export function GameInstanceProvider({
|
|
|
157
160
|
const partCount = modelParts[modelKey] || 0;
|
|
158
161
|
if (partCount === 0) return null;
|
|
159
162
|
|
|
160
|
-
//
|
|
161
|
-
const meshesForModel: Record<string,
|
|
163
|
+
// Create mesh subset for this specific model
|
|
164
|
+
const meshesForModel: Record<string, Mesh> = {};
|
|
162
165
|
for (let i = 0; i < partCount; i++) {
|
|
163
166
|
const partKey = `${modelKey}__${i}`;
|
|
164
167
|
meshesForModel[partKey] = flatMeshes[partKey];
|
|
@@ -188,7 +191,7 @@ export function GameInstanceProvider({
|
|
|
188
191
|
);
|
|
189
192
|
}
|
|
190
193
|
|
|
191
|
-
//
|
|
194
|
+
// Render physics-enabled instances using InstancedRigidBodies
|
|
192
195
|
function InstancedRigidGroup({
|
|
193
196
|
group,
|
|
194
197
|
modelKey,
|
|
@@ -198,7 +201,7 @@ function InstancedRigidGroup({
|
|
|
198
201
|
group: { physicsType: string, instances: InstanceData[] },
|
|
199
202
|
modelKey: string,
|
|
200
203
|
partCount: number,
|
|
201
|
-
flatMeshes: Record<string,
|
|
204
|
+
flatMeshes: Record<string, Mesh>
|
|
202
205
|
}) {
|
|
203
206
|
const instances = useMemo(
|
|
204
207
|
() => group.instances.map(inst => ({
|
|
@@ -232,24 +235,33 @@ function InstancedRigidGroup({
|
|
|
232
235
|
);
|
|
233
236
|
}
|
|
234
237
|
|
|
235
|
-
//
|
|
238
|
+
// Render non-physics instances using Merged's per-instance groups
|
|
236
239
|
function NonPhysicsInstancedGroup({
|
|
237
240
|
modelKey,
|
|
238
241
|
group,
|
|
239
242
|
partCount,
|
|
240
|
-
instancesMap
|
|
241
|
-
|
|
243
|
+
instancesMap,
|
|
244
|
+
onSelect,
|
|
245
|
+
registerRef
|
|
242
246
|
}: {
|
|
243
247
|
modelKey: string;
|
|
244
248
|
group: { physicsType: string, instances: InstanceData[] };
|
|
245
249
|
partCount: number;
|
|
246
250
|
instancesMap: Record<string, React.ComponentType<any>>;
|
|
247
251
|
onSelect?: (id: string | null) => void;
|
|
248
|
-
registerRef?: (id: string, obj:
|
|
252
|
+
registerRef?: (id: string, obj: Object3D | null) => void;
|
|
249
253
|
}) {
|
|
250
254
|
const clickValid = useRef(false);
|
|
251
|
-
|
|
252
|
-
const
|
|
255
|
+
|
|
256
|
+
const handlePointerDown = (e: any) => {
|
|
257
|
+
e.stopPropagation();
|
|
258
|
+
clickValid.current = true;
|
|
259
|
+
};
|
|
260
|
+
|
|
261
|
+
const handlePointerMove = () => {
|
|
262
|
+
if (clickValid.current) clickValid.current = false;
|
|
263
|
+
};
|
|
264
|
+
|
|
253
265
|
const handlePointerUp = (e: any, id: string) => {
|
|
254
266
|
if (clickValid.current) {
|
|
255
267
|
e.stopPropagation();
|
|
@@ -263,7 +275,7 @@ function NonPhysicsInstancedGroup({
|
|
|
263
275
|
{group.instances.map(inst => (
|
|
264
276
|
<group
|
|
265
277
|
key={inst.id}
|
|
266
|
-
ref={(el) => { registerRef?.(inst.id, el as unknown as
|
|
278
|
+
ref={(el) => { registerRef?.(inst.id, el as unknown as Object3D | null); }}
|
|
267
279
|
position={inst.position}
|
|
268
280
|
rotation={inst.rotation}
|
|
269
281
|
scale={inst.scale}
|
|
@@ -283,8 +295,8 @@ function NonPhysicsInstancedGroup({
|
|
|
283
295
|
}
|
|
284
296
|
|
|
285
297
|
|
|
286
|
-
//
|
|
287
|
-
export const GameInstance = React.forwardRef<
|
|
298
|
+
// GameInstance component: registers an instance for batch rendering (renders nothing itself)
|
|
299
|
+
export const GameInstance = React.forwardRef<Group, {
|
|
288
300
|
id: string;
|
|
289
301
|
modelUrl: string;
|
|
290
302
|
position: [number, number, number];
|
|
@@ -320,7 +332,6 @@ export const GameInstance = React.forwardRef<THREE.Group, {
|
|
|
320
332
|
};
|
|
321
333
|
}, [addInstance, removeInstance, instance]);
|
|
322
334
|
|
|
323
|
-
|
|
324
|
-
// No visual here – provider will render visuals for all instances
|
|
335
|
+
// No visual rendering - provider handles all instanced visuals
|
|
325
336
|
return null;
|
|
326
337
|
});
|
|
@@ -203,11 +203,12 @@ function GameObjectRenderer({
|
|
|
203
203
|
|
|
204
204
|
// Early return if gameObject is null or undefined
|
|
205
205
|
if (!gameObject) return null;
|
|
206
|
+
if (gameObject.disabled === true || gameObject.hidden === true) return null;
|
|
206
207
|
|
|
207
|
-
// Build
|
|
208
|
+
// Build context object for passing to helper functions
|
|
208
209
|
const ctx = { gameObject, selectedId, onSelect, registerRef, loadedModels, loadedTextures, editMode };
|
|
209
210
|
|
|
210
|
-
// --- 1.
|
|
211
|
+
// --- 1. Compute transforms (local + world) ---
|
|
211
212
|
const transformProps = getNodeTransformProps(gameObject);
|
|
212
213
|
const localMatrix = new Matrix4().compose(
|
|
213
214
|
new Vector3(...transformProps.position),
|
|
@@ -216,7 +217,7 @@ function GameObjectRenderer({
|
|
|
216
217
|
);
|
|
217
218
|
const worldMatrix = parentMatrix.clone().multiply(localMatrix);
|
|
218
219
|
|
|
219
|
-
//
|
|
220
|
+
// --- 2. Handle selection interaction (edit mode only) ---
|
|
220
221
|
const clickValid = useRef(false);
|
|
221
222
|
const handlePointerDown = (e: ThreeEvent<PointerEvent>) => {
|
|
222
223
|
e.stopPropagation();
|
|
@@ -233,18 +234,19 @@ function GameObjectRenderer({
|
|
|
233
234
|
clickValid.current = false;
|
|
234
235
|
};
|
|
235
236
|
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
// --- 2. If instanced, short-circuit to a tiny clean branch ---
|
|
237
|
+
// --- 3. If instanced model, short-circuit to GameInstance (terminal node) ---
|
|
239
238
|
const isInstanced = !!gameObject.components?.model?.properties?.instanced;
|
|
240
239
|
if (isInstanced) {
|
|
241
240
|
return renderInstancedNode(gameObject, worldMatrix, ctx);
|
|
242
241
|
}
|
|
243
242
|
|
|
244
|
-
// ---
|
|
243
|
+
// --- 4. Render core content using component system ---
|
|
245
244
|
const core = renderCoreNode(gameObject, ctx, parentMatrix);
|
|
246
245
|
|
|
247
|
-
// --- 5.
|
|
246
|
+
// --- 5. Wrap with physics if needed (except in edit mode) ---
|
|
247
|
+
const physicsWrapped = wrapPhysicsIfNeeded(gameObject, core, ctx);
|
|
248
|
+
|
|
249
|
+
// --- 6. Render children recursively (always relative transforms) ---
|
|
248
250
|
const children = (gameObject.children ?? []).map((child) => (
|
|
249
251
|
<GameObjectRenderer
|
|
250
252
|
key={child.id}
|
|
@@ -259,10 +261,7 @@ function GameObjectRenderer({
|
|
|
259
261
|
/>
|
|
260
262
|
));
|
|
261
263
|
|
|
262
|
-
// ---
|
|
263
|
-
const physicsWrapped = wrapPhysicsIfNeeded(gameObject, core, ctx);
|
|
264
|
-
|
|
265
|
-
// --- 6. Final group wrapper ---
|
|
264
|
+
// --- 7. Final group wrapper with local transform ---
|
|
266
265
|
return (
|
|
267
266
|
<group
|
|
268
267
|
ref={(el) => registerRef(gameObject.id, el)}
|
|
@@ -300,18 +299,17 @@ function renderInstancedNode(gameObject: GameObjectType, worldMatrix: Matrix4, c
|
|
|
300
299
|
);
|
|
301
300
|
}
|
|
302
301
|
|
|
303
|
-
// Helper: render main
|
|
302
|
+
// Helper: render main content for a non-instanced node using the component system
|
|
304
303
|
function renderCoreNode(gameObject: GameObjectType, ctx: any, parentMatrix: Matrix4 | undefined) {
|
|
305
304
|
const geometry = gameObject.components?.geometry;
|
|
306
305
|
const material = gameObject.components?.material;
|
|
307
|
-
const
|
|
306
|
+
const model = gameObject.components?.model;
|
|
308
307
|
|
|
309
308
|
const geometryDef = geometry ? getComponent('Geometry') : undefined;
|
|
310
309
|
const materialDef = material ? getComponent('Material') : undefined;
|
|
310
|
+
const modelDef = model ? getComponent('Model') : undefined;
|
|
311
311
|
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
// Generic component views (exclude geometry/material/model/transform/physics)
|
|
312
|
+
// Context props for all component Views
|
|
315
313
|
const contextProps = {
|
|
316
314
|
loadedModels: ctx.loadedModels,
|
|
317
315
|
loadedTextures: ctx.loadedTextures,
|
|
@@ -321,20 +319,19 @@ function renderCoreNode(gameObject: GameObjectType, ctx: any, parentMatrix: Matr
|
|
|
321
319
|
registerRef: ctx.registerRef,
|
|
322
320
|
};
|
|
323
321
|
|
|
324
|
-
//
|
|
322
|
+
// Collect wrapper and leaf components (excluding transform/physics which are handled separately)
|
|
325
323
|
const wrapperComponents: Array<{ key: string; View: any; properties: any }> = [];
|
|
326
324
|
const leafComponents: React.ReactNode[] = [];
|
|
327
325
|
|
|
328
326
|
if (gameObject.components) {
|
|
329
327
|
Object.entries(gameObject.components)
|
|
330
|
-
.filter(([key]) =>
|
|
328
|
+
.filter(([key]) => !['geometry', 'material', 'model', 'transform', 'physics'].includes(key))
|
|
331
329
|
.forEach(([key, comp]) => {
|
|
332
330
|
if (!comp || !comp.type) return;
|
|
333
331
|
const def = getComponent(comp.type);
|
|
334
332
|
if (!def || !def.View) return;
|
|
335
333
|
|
|
336
|
-
//
|
|
337
|
-
// Components that wrap content should accept children prop
|
|
334
|
+
// Components that accept children are wrappers, others are leaves
|
|
338
335
|
const viewString = def.View.toString();
|
|
339
336
|
if (viewString.includes('children')) {
|
|
340
337
|
wrapperComponents.push({ key, View: def.View, properties: comp.properties });
|
|
@@ -344,49 +341,41 @@ function renderCoreNode(gameObject: GameObjectType, ctx: any, parentMatrix: Matr
|
|
|
344
341
|
});
|
|
345
342
|
}
|
|
346
343
|
|
|
347
|
-
// Build
|
|
344
|
+
// Build core content based on what components exist
|
|
348
345
|
let coreContent: React.ReactNode;
|
|
349
346
|
|
|
350
|
-
//
|
|
351
|
-
if (
|
|
352
|
-
|
|
347
|
+
// Priority: Model > Geometry + Material > Empty
|
|
348
|
+
if (model && modelDef && modelDef.View) {
|
|
349
|
+
// Model component wraps its children (including material override)
|
|
353
350
|
coreContent = (
|
|
354
|
-
<
|
|
351
|
+
<modelDef.View properties={model.properties} {...contextProps}>
|
|
355
352
|
{material && materialDef && materialDef.View && (
|
|
356
353
|
<materialDef.View
|
|
357
354
|
key="material"
|
|
358
355
|
properties={material.properties}
|
|
359
|
-
|
|
360
|
-
isSelected={ctx.selectedId === gameObject.id}
|
|
361
|
-
editMode={ctx.editMode}
|
|
362
|
-
parentMatrix={parentMatrix}
|
|
363
|
-
registerRef={ctx.registerRef}
|
|
356
|
+
{...contextProps}
|
|
364
357
|
/>
|
|
365
358
|
)}
|
|
366
359
|
{leafComponents}
|
|
367
|
-
</
|
|
360
|
+
</modelDef.View>
|
|
368
361
|
);
|
|
369
362
|
} else if (geometry && geometryDef && geometryDef.View) {
|
|
370
|
-
//
|
|
363
|
+
// Geometry + Material = mesh
|
|
371
364
|
coreContent = (
|
|
372
|
-
<mesh>
|
|
373
|
-
<geometryDef.View
|
|
365
|
+
<mesh castShadow receiveShadow>
|
|
366
|
+
<geometryDef.View properties={geometry.properties} {...contextProps} />
|
|
374
367
|
{material && materialDef && materialDef.View && (
|
|
375
368
|
<materialDef.View
|
|
376
369
|
key="material"
|
|
377
370
|
properties={material.properties}
|
|
378
|
-
|
|
379
|
-
isSelected={ctx.selectedId === gameObject.id}
|
|
380
|
-
editMode={ctx.editMode}
|
|
381
|
-
parentMatrix={parentMatrix}
|
|
382
|
-
registerRef={ctx.registerRef}
|
|
371
|
+
{...contextProps}
|
|
383
372
|
/>
|
|
384
373
|
)}
|
|
385
374
|
{leafComponents}
|
|
386
375
|
</mesh>
|
|
387
376
|
);
|
|
388
377
|
} else {
|
|
389
|
-
// No
|
|
378
|
+
// No visual component - just render leaves
|
|
390
379
|
coreContent = <>{leafComponents}</>;
|
|
391
380
|
}
|
|
392
381
|
|