react-three-game 0.0.60 → 0.0.61
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/package.json +9 -3
- package/.gitattributes +0 -2
- package/.github/copilot-instructions.md +0 -83
- package/.github/workflows/nextjs.yml +0 -99
- package/.gitmodules +0 -3
- package/assets/architecture.png +0 -0
- package/assets/editor.gif +0 -0
- package/assets/favicon.ico +0 -0
- package/assets/react-three-game-logo.png +0 -0
- package/dist/tools/dragdrop/page.d.ts +0 -1
- package/dist/tools/dragdrop/page.js +0 -11
- package/dist/tools/prefabeditor/EntityEvents.d.ts +0 -54
- package/dist/tools/prefabeditor/EntityEvents.js +0 -85
- package/dist/tools/prefabeditor/page.d.ts +0 -1
- package/dist/tools/prefabeditor/page.js +0 -5
- package/react-three-game-skill/.gitattributes +0 -2
- package/react-three-game-skill/README.md +0 -7
- package/react-three-game-skill/react-three-game/SKILL.md +0 -514
- package/react-three-game-skill/react-three-game/rules/ADVANCED_PHYSICS.md +0 -472
- package/react-three-game-skill/react-three-game/rules/LIGHTING.md +0 -6
- package/src/helpers/SoundManager.ts +0 -130
- package/src/helpers/index.ts +0 -91
- package/src/index.ts +0 -59
- package/src/shared/ContactShadow.tsx +0 -74
- package/src/shared/GameCanvas.tsx +0 -52
- package/src/tools/assetviewer/page.tsx +0 -425
- package/src/tools/dragdrop/DragDropLoader.tsx +0 -159
- package/src/tools/dragdrop/index.ts +0 -4
- package/src/tools/dragdrop/modelLoader.ts +0 -204
- package/src/tools/dragdrop/page.tsx +0 -45
- package/src/tools/prefabeditor/Dropdown.tsx +0 -112
- package/src/tools/prefabeditor/EditorContext.tsx +0 -25
- package/src/tools/prefabeditor/EditorTree.tsx +0 -452
- package/src/tools/prefabeditor/EditorTreeMenus.tsx +0 -307
- package/src/tools/prefabeditor/EditorUI.tsx +0 -204
- package/src/tools/prefabeditor/EventSystem.tsx +0 -36
- package/src/tools/prefabeditor/GameEvents.ts +0 -191
- package/src/tools/prefabeditor/InstanceProvider.tsx +0 -466
- package/src/tools/prefabeditor/PrefabEditor.tsx +0 -256
- package/src/tools/prefabeditor/PrefabRoot.tsx +0 -767
- package/src/tools/prefabeditor/components/AmbientLightComponent.tsx +0 -34
- package/src/tools/prefabeditor/components/CameraComponent.tsx +0 -117
- package/src/tools/prefabeditor/components/ComponentRegistry.ts +0 -40
- package/src/tools/prefabeditor/components/DirectionalLightComponent.tsx +0 -210
- package/src/tools/prefabeditor/components/EnvironmentComponent.tsx +0 -47
- package/src/tools/prefabeditor/components/GeometryComponent.tsx +0 -133
- package/src/tools/prefabeditor/components/Input.tsx +0 -820
- package/src/tools/prefabeditor/components/MaterialComponent.tsx +0 -431
- package/src/tools/prefabeditor/components/ModelComponent.tsx +0 -176
- package/src/tools/prefabeditor/components/PhysicsComponent.tsx +0 -188
- package/src/tools/prefabeditor/components/SpotLightComponent.tsx +0 -109
- package/src/tools/prefabeditor/components/TextComponent.tsx +0 -137
- package/src/tools/prefabeditor/components/TransformComponent.tsx +0 -173
- package/src/tools/prefabeditor/components/index.ts +0 -26
- package/src/tools/prefabeditor/page.tsx +0 -10
- package/src/tools/prefabeditor/styles.ts +0 -235
- package/src/tools/prefabeditor/types.ts +0 -20
- package/src/tools/prefabeditor/utils.ts +0 -312
|
@@ -1,191 +0,0 @@
|
|
|
1
|
-
import { useEffect, useCallback } from 'react';
|
|
2
|
-
import type { RapierRigidBody } from '@react-three/rapier';
|
|
3
|
-
|
|
4
|
-
// ============================================================================
|
|
5
|
-
// Built-in Event Types & Payloads
|
|
6
|
-
// ============================================================================
|
|
7
|
-
|
|
8
|
-
/** Physics event types (built-in) */
|
|
9
|
-
export type PhysicsEventType =
|
|
10
|
-
| 'sensor:enter'
|
|
11
|
-
| 'sensor:exit'
|
|
12
|
-
| 'collision:enter'
|
|
13
|
-
| 'collision:exit';
|
|
14
|
-
|
|
15
|
-
/** Payload for physics events */
|
|
16
|
-
export interface PhysicsEventPayload {
|
|
17
|
-
sourceEntityId: string;
|
|
18
|
-
targetEntityId: string | null;
|
|
19
|
-
targetRigidBody: RapierRigidBody | null | undefined;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
// ============================================================================
|
|
23
|
-
// Event Type Registry - Maps event names to their payload types
|
|
24
|
-
// ============================================================================
|
|
25
|
-
|
|
26
|
-
/**
|
|
27
|
-
* Register your custom event types here by extending this interface:
|
|
28
|
-
*
|
|
29
|
-
* declare module 'react-three-game' {
|
|
30
|
-
* interface GameEventMap {
|
|
31
|
-
* 'player:death': { playerId: string; cause: string };
|
|
32
|
-
* 'score:change': { delta: number; total: number };
|
|
33
|
-
* }
|
|
34
|
-
* }
|
|
35
|
-
*/
|
|
36
|
-
export interface GameEventMap {
|
|
37
|
-
'sensor:enter': PhysicsEventPayload;
|
|
38
|
-
'sensor:exit': PhysicsEventPayload;
|
|
39
|
-
'collision:enter': PhysicsEventPayload;
|
|
40
|
-
'collision:exit': PhysicsEventPayload;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
/** All registered event types */
|
|
44
|
-
export type GameEventType = keyof GameEventMap | (string & {});
|
|
45
|
-
|
|
46
|
-
/** Get payload type for an event, or fallback to generic */
|
|
47
|
-
export type GameEventPayload<T extends string> = T extends keyof GameEventMap
|
|
48
|
-
? GameEventMap[T]
|
|
49
|
-
: Record<string, unknown>;
|
|
50
|
-
|
|
51
|
-
// ============================================================================
|
|
52
|
-
// Event System Implementation
|
|
53
|
-
// ============================================================================
|
|
54
|
-
|
|
55
|
-
type EventHandler<T = unknown> = (payload: T) => void;
|
|
56
|
-
|
|
57
|
-
// Internal subscriber storage
|
|
58
|
-
const subscribers = new Map<string, Set<EventHandler<any>>>();
|
|
59
|
-
|
|
60
|
-
/**
|
|
61
|
-
* Game event system for all game interactions.
|
|
62
|
-
*
|
|
63
|
-
* Built-in physics events:
|
|
64
|
-
* - sensor:enter - Something entered a sensor collider
|
|
65
|
-
* - sensor:exit - Something exited a sensor collider
|
|
66
|
-
* - collision:enter - A collision started
|
|
67
|
-
* - collision:exit - A collision ended
|
|
68
|
-
*
|
|
69
|
-
* Custom events:
|
|
70
|
-
* - Emit any event type with any payload
|
|
71
|
-
* - Extend GameEventMap interface for type safety
|
|
72
|
-
*
|
|
73
|
-
* @example
|
|
74
|
-
* // Physics events (typed)
|
|
75
|
-
* gameEvents.emit('sensor:enter', { sourceEntityId: 'zone', targetEntityId: 'player', targetRigidBody: rb });
|
|
76
|
-
*
|
|
77
|
-
* // Custom events
|
|
78
|
-
* gameEvents.emit('player:death', { playerId: 'p1', cause: 'lava' });
|
|
79
|
-
* gameEvents.emit('level:complete', { levelId: 3, time: 45.2 });
|
|
80
|
-
*/
|
|
81
|
-
export const gameEvents = {
|
|
82
|
-
/**
|
|
83
|
-
* Emit an event to all subscribers
|
|
84
|
-
*/
|
|
85
|
-
emit<T extends string>(type: T, payload: GameEventPayload<T>): void {
|
|
86
|
-
const handlers = subscribers.get(type);
|
|
87
|
-
if (handlers) {
|
|
88
|
-
handlers.forEach(handler => {
|
|
89
|
-
try {
|
|
90
|
-
handler(payload);
|
|
91
|
-
} catch (e) {
|
|
92
|
-
console.error(`Error in gameEvents handler for ${type}:`, e);
|
|
93
|
-
}
|
|
94
|
-
});
|
|
95
|
-
}
|
|
96
|
-
},
|
|
97
|
-
|
|
98
|
-
/**
|
|
99
|
-
* Subscribe to an event type
|
|
100
|
-
* @returns Unsubscribe function
|
|
101
|
-
*/
|
|
102
|
-
on<T extends string>(type: T, handler: EventHandler<GameEventPayload<T>>): () => void {
|
|
103
|
-
if (!subscribers.has(type)) {
|
|
104
|
-
subscribers.set(type, new Set());
|
|
105
|
-
}
|
|
106
|
-
subscribers.get(type)!.add(handler);
|
|
107
|
-
|
|
108
|
-
return () => {
|
|
109
|
-
subscribers.get(type)?.delete(handler);
|
|
110
|
-
};
|
|
111
|
-
},
|
|
112
|
-
|
|
113
|
-
/**
|
|
114
|
-
* Unsubscribe from an event type
|
|
115
|
-
*/
|
|
116
|
-
off<T extends string>(type: T, handler: EventHandler<GameEventPayload<T>>): void {
|
|
117
|
-
subscribers.get(type)?.delete(handler);
|
|
118
|
-
},
|
|
119
|
-
|
|
120
|
-
/**
|
|
121
|
-
* Remove all subscribers (useful for cleanup/reset)
|
|
122
|
-
*/
|
|
123
|
-
clear(): void {
|
|
124
|
-
subscribers.clear();
|
|
125
|
-
},
|
|
126
|
-
|
|
127
|
-
/**
|
|
128
|
-
* Check if an event type has any subscribers
|
|
129
|
-
*/
|
|
130
|
-
hasListeners(type: string): boolean {
|
|
131
|
-
return (subscribers.get(type)?.size ?? 0) > 0;
|
|
132
|
-
}
|
|
133
|
-
};
|
|
134
|
-
|
|
135
|
-
/**
|
|
136
|
-
* React hook to subscribe to game events.
|
|
137
|
-
* Automatically cleans up on unmount.
|
|
138
|
-
*
|
|
139
|
-
* @example
|
|
140
|
-
* // Physics event
|
|
141
|
-
* useGameEvent('sensor:enter', (payload) => {
|
|
142
|
-
* if (payload.sourceEntityId === 'coin') collectCoin();
|
|
143
|
-
* }, []);
|
|
144
|
-
*
|
|
145
|
-
* // Custom event
|
|
146
|
-
* useGameEvent('player:death', (payload) => {
|
|
147
|
-
* showGameOver(payload.cause);
|
|
148
|
-
* }, []);
|
|
149
|
-
*/
|
|
150
|
-
export function useGameEvent<T extends string>(
|
|
151
|
-
type: T,
|
|
152
|
-
handler: EventHandler<GameEventPayload<T>>,
|
|
153
|
-
deps: unknown[] = []
|
|
154
|
-
): void {
|
|
155
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
156
|
-
const stableHandler = useCallback(handler, deps);
|
|
157
|
-
|
|
158
|
-
useEffect(() => {
|
|
159
|
-
return gameEvents.on(type, stableHandler);
|
|
160
|
-
}, [type, stableHandler]);
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
// ============================================================================
|
|
164
|
-
// Helpers
|
|
165
|
-
// ============================================================================
|
|
166
|
-
|
|
167
|
-
/**
|
|
168
|
-
* Helper to extract entity ID from Rapier collision data.
|
|
169
|
-
* Entity IDs are stored in RigidBody userData.
|
|
170
|
-
*/
|
|
171
|
-
export function getEntityIdFromRigidBody(rigidBody: RapierRigidBody | null | undefined): string | null {
|
|
172
|
-
if (!rigidBody) return null;
|
|
173
|
-
const userData = rigidBody.userData as { entityId?: string } | undefined;
|
|
174
|
-
return userData?.entityId ?? null;
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
// ============================================================================
|
|
178
|
-
// Backward Compatibility Aliases
|
|
179
|
-
// ============================================================================
|
|
180
|
-
|
|
181
|
-
/** @deprecated Use gameEvents instead */
|
|
182
|
-
export const entityEvents = gameEvents;
|
|
183
|
-
|
|
184
|
-
/** @deprecated Use useGameEvent instead */
|
|
185
|
-
export const useEntityEvent = useGameEvent;
|
|
186
|
-
|
|
187
|
-
/** @deprecated Use GameEventType instead */
|
|
188
|
-
export type EntityEventType = PhysicsEventType;
|
|
189
|
-
|
|
190
|
-
/** @deprecated Use PhysicsEventPayload instead */
|
|
191
|
-
export type EntityEventPayload = PhysicsEventPayload;
|
|
@@ -1,466 +0,0 @@
|
|
|
1
|
-
import React, { createContext, useContext, useMemo, useRef, useState, useEffect, useCallback } from "react";
|
|
2
|
-
import { Merged, useHelper } from '@react-three/drei';
|
|
3
|
-
import { InstancedRigidBodies } from "@react-three/rapier";
|
|
4
|
-
import { Mesh, Matrix4, Object3D, Group, Vector3, Quaternion, Euler, InstancedMesh, BoxHelper } from "three";
|
|
5
|
-
import { PhysicsProps } from "./components/PhysicsComponent";
|
|
6
|
-
|
|
7
|
-
// --- Types ---
|
|
8
|
-
export type InstanceData = {
|
|
9
|
-
id: string;
|
|
10
|
-
position: [number, number, number];
|
|
11
|
-
rotation: [number, number, number];
|
|
12
|
-
scale: [number, number, number];
|
|
13
|
-
meshPath: string;
|
|
14
|
-
physics?: PhysicsProps | undefined;
|
|
15
|
-
};
|
|
16
|
-
|
|
17
|
-
// Helper functions for comparison
|
|
18
|
-
function arrayEquals(a: number[], b: number[]): boolean {
|
|
19
|
-
if (a === b) return true;
|
|
20
|
-
if (a.length !== b.length) return false;
|
|
21
|
-
for (let i = 0; i < a.length; i++) {
|
|
22
|
-
if (a[i] !== b[i]) return false;
|
|
23
|
-
}
|
|
24
|
-
return true;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
function instanceEquals(a: InstanceData, b: InstanceData): boolean {
|
|
28
|
-
return a.id === b.id &&
|
|
29
|
-
a.meshPath === b.meshPath &&
|
|
30
|
-
arrayEquals(a.position, b.position) &&
|
|
31
|
-
arrayEquals(a.rotation, b.rotation) &&
|
|
32
|
-
arrayEquals(a.scale, b.scale) &&
|
|
33
|
-
a.physics?.type === b.physics?.type;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
// --- Context ---
|
|
37
|
-
type GameInstanceContextType = {
|
|
38
|
-
addInstance: (instance: InstanceData) => void;
|
|
39
|
-
removeInstance: (id: string) => void;
|
|
40
|
-
instances: InstanceData[];
|
|
41
|
-
meshes: Record<string, Mesh>;
|
|
42
|
-
modelParts?: Record<string, number>;
|
|
43
|
-
hasInstance: (id: string) => boolean;
|
|
44
|
-
};
|
|
45
|
-
const GameInstanceContext = createContext<GameInstanceContextType | null>(null);
|
|
46
|
-
|
|
47
|
-
export function GameInstanceProvider({
|
|
48
|
-
children,
|
|
49
|
-
models,
|
|
50
|
-
onSelect,
|
|
51
|
-
registerRef,
|
|
52
|
-
selectedId,
|
|
53
|
-
editMode
|
|
54
|
-
}: {
|
|
55
|
-
children: React.ReactNode,
|
|
56
|
-
models: { [filename: string]: Object3D },
|
|
57
|
-
onSelect?: (id: string | null) => void,
|
|
58
|
-
registerRef?: (id: string, obj: Object3D | null) => void,
|
|
59
|
-
selectedId?: string | null,
|
|
60
|
-
editMode?: boolean
|
|
61
|
-
}) {
|
|
62
|
-
const [instances, setInstances] = useState<InstanceData[]>([]);
|
|
63
|
-
|
|
64
|
-
const addInstance = useCallback((instance: InstanceData) => {
|
|
65
|
-
setInstances(prev => {
|
|
66
|
-
const idx = prev.findIndex(i => i.id === instance.id);
|
|
67
|
-
if (idx !== -1) {
|
|
68
|
-
// Update existing if changed
|
|
69
|
-
if (instanceEquals(prev[idx], instance)) {
|
|
70
|
-
return prev;
|
|
71
|
-
}
|
|
72
|
-
const copy = [...prev];
|
|
73
|
-
copy[idx] = instance;
|
|
74
|
-
return copy;
|
|
75
|
-
}
|
|
76
|
-
// Add new
|
|
77
|
-
return [...prev, instance];
|
|
78
|
-
});
|
|
79
|
-
}, []);
|
|
80
|
-
|
|
81
|
-
const removeInstance = useCallback((id: string) => {
|
|
82
|
-
setInstances(prev => {
|
|
83
|
-
if (!prev.find(i => i.id === id)) return prev;
|
|
84
|
-
return prev.filter(i => i.id !== id);
|
|
85
|
-
});
|
|
86
|
-
}, []);
|
|
87
|
-
|
|
88
|
-
const hasInstance = useCallback((id: string) => {
|
|
89
|
-
return instances.some(i => i.id === id);
|
|
90
|
-
}, [instances]);
|
|
91
|
-
|
|
92
|
-
// Flatten all model meshes once (models → flat mesh parts)
|
|
93
|
-
// Note: Geometry is cloned with baked transforms for instancing
|
|
94
|
-
const { flatMeshes, modelParts } = useMemo(() => {
|
|
95
|
-
const flatMeshes: Record<string, Mesh> = {};
|
|
96
|
-
const modelParts: Record<string, number> = {};
|
|
97
|
-
|
|
98
|
-
Object.entries(models).forEach(([modelKey, model]) => {
|
|
99
|
-
model.updateWorldMatrix(false, true);
|
|
100
|
-
const rootInverse = new Matrix4().copy(model.matrixWorld).invert();
|
|
101
|
-
|
|
102
|
-
let partIndex = 0;
|
|
103
|
-
model.traverse((obj: any) => {
|
|
104
|
-
if (obj.isMesh) {
|
|
105
|
-
// Clone geometry and bake relative transform
|
|
106
|
-
const geom = obj.geometry.clone();
|
|
107
|
-
geom.applyMatrix4(obj.matrixWorld.clone().premultiply(rootInverse));
|
|
108
|
-
|
|
109
|
-
const partKey = `${modelKey}__${partIndex}`;
|
|
110
|
-
flatMeshes[partKey] = new Mesh(geom, obj.material);
|
|
111
|
-
partIndex++;
|
|
112
|
-
}
|
|
113
|
-
});
|
|
114
|
-
modelParts[modelKey] = partIndex;
|
|
115
|
-
});
|
|
116
|
-
|
|
117
|
-
return { flatMeshes, modelParts };
|
|
118
|
-
}, [models]);
|
|
119
|
-
|
|
120
|
-
// Cleanup geometries when models change
|
|
121
|
-
useEffect(() => {
|
|
122
|
-
return () => {
|
|
123
|
-
Object.values(flatMeshes).forEach(mesh => mesh.geometry.dispose());
|
|
124
|
-
};
|
|
125
|
-
}, [flatMeshes]);
|
|
126
|
-
|
|
127
|
-
// Group instances by meshPath + physics type for batch rendering
|
|
128
|
-
const grouped = useMemo(() => {
|
|
129
|
-
const groups: Record<string, { physicsType: string, instances: InstanceData[] }> = {};
|
|
130
|
-
for (const inst of instances) {
|
|
131
|
-
const type = inst.physics?.type || 'none';
|
|
132
|
-
const key = `${inst.meshPath}__${type}`;
|
|
133
|
-
if (!groups[key]) groups[key] = { physicsType: type, instances: [] };
|
|
134
|
-
groups[key].instances.push(inst);
|
|
135
|
-
}
|
|
136
|
-
return groups;
|
|
137
|
-
}, [instances]);
|
|
138
|
-
|
|
139
|
-
return (
|
|
140
|
-
<GameInstanceContext.Provider
|
|
141
|
-
value={{
|
|
142
|
-
addInstance,
|
|
143
|
-
removeInstance,
|
|
144
|
-
instances,
|
|
145
|
-
meshes: flatMeshes,
|
|
146
|
-
modelParts,
|
|
147
|
-
hasInstance
|
|
148
|
-
}}
|
|
149
|
-
>
|
|
150
|
-
{/* Render normal prefab hierarchy (non-instanced objects) */}
|
|
151
|
-
{children}
|
|
152
|
-
|
|
153
|
-
{/* Render physics-enabled instanced groups using InstancedRigidBodies */}
|
|
154
|
-
{Object.entries(grouped).map(([key, group]) => {
|
|
155
|
-
if (group.physicsType === 'none') return null;
|
|
156
|
-
const modelKey = group.instances[0].meshPath;
|
|
157
|
-
const partCount = modelParts[modelKey] || 0;
|
|
158
|
-
if (partCount === 0) return null;
|
|
159
|
-
|
|
160
|
-
return (
|
|
161
|
-
<InstancedRigidGroup
|
|
162
|
-
key={key}
|
|
163
|
-
group={group}
|
|
164
|
-
modelKey={modelKey}
|
|
165
|
-
partCount={partCount}
|
|
166
|
-
flatMeshes={flatMeshes}
|
|
167
|
-
onSelect={onSelect}
|
|
168
|
-
editMode={editMode}
|
|
169
|
-
/>
|
|
170
|
-
);
|
|
171
|
-
})}
|
|
172
|
-
|
|
173
|
-
{/* Render non-physics instanced visuals using Merged (one per model type) */}
|
|
174
|
-
{Object.entries(grouped).map(([key, group]) => {
|
|
175
|
-
if (group.physicsType !== 'none') return null;
|
|
176
|
-
|
|
177
|
-
const modelKey = group.instances[0].meshPath;
|
|
178
|
-
const partCount = modelParts[modelKey] || 0;
|
|
179
|
-
if (partCount === 0) return null;
|
|
180
|
-
|
|
181
|
-
// Create mesh subset for this specific model
|
|
182
|
-
const meshesForModel: Record<string, Mesh> = {};
|
|
183
|
-
for (let i = 0; i < partCount; i++) {
|
|
184
|
-
const partKey = `${modelKey}__${i}`;
|
|
185
|
-
meshesForModel[partKey] = flatMeshes[partKey];
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
return (
|
|
189
|
-
<Merged
|
|
190
|
-
key={key}
|
|
191
|
-
meshes={meshesForModel}
|
|
192
|
-
castShadow
|
|
193
|
-
receiveShadow
|
|
194
|
-
>
|
|
195
|
-
{(instancesMap: any) => (
|
|
196
|
-
<NonPhysicsInstancedGroup
|
|
197
|
-
modelKey={modelKey}
|
|
198
|
-
group={group}
|
|
199
|
-
partCount={partCount}
|
|
200
|
-
instancesMap={instancesMap}
|
|
201
|
-
onSelect={onSelect}
|
|
202
|
-
registerRef={registerRef}
|
|
203
|
-
selectedId={selectedId}
|
|
204
|
-
editMode={editMode}
|
|
205
|
-
/>
|
|
206
|
-
)}
|
|
207
|
-
</Merged>
|
|
208
|
-
);
|
|
209
|
-
})}
|
|
210
|
-
</GameInstanceContext.Provider>
|
|
211
|
-
);
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
// Render physics-enabled instances using InstancedRigidBodies
|
|
215
|
-
function InstancedRigidGroup({
|
|
216
|
-
group,
|
|
217
|
-
modelKey,
|
|
218
|
-
partCount,
|
|
219
|
-
flatMeshes,
|
|
220
|
-
onSelect,
|
|
221
|
-
editMode
|
|
222
|
-
}: {
|
|
223
|
-
group: { physicsType: string, instances: InstanceData[] },
|
|
224
|
-
modelKey: string,
|
|
225
|
-
partCount: number,
|
|
226
|
-
flatMeshes: Record<string, Mesh>,
|
|
227
|
-
onSelect?: (id: string | null) => void,
|
|
228
|
-
editMode?: boolean
|
|
229
|
-
}) {
|
|
230
|
-
const meshRefs = useRef<(InstancedMesh | null)[]>([]);
|
|
231
|
-
const rigidBodiesRef = useRef<any>(null);
|
|
232
|
-
|
|
233
|
-
const instances = useMemo(
|
|
234
|
-
() => group.instances.map(inst => ({
|
|
235
|
-
key: inst.id,
|
|
236
|
-
position: inst.position,
|
|
237
|
-
rotation: inst.rotation,
|
|
238
|
-
scale: inst.scale,
|
|
239
|
-
})),
|
|
240
|
-
[group.instances]
|
|
241
|
-
);
|
|
242
|
-
|
|
243
|
-
// Apply scale to visual meshes (InstancedRigidBodies only scales colliders, not visuals)
|
|
244
|
-
useEffect(() => {
|
|
245
|
-
const matrix = new Matrix4();
|
|
246
|
-
const pos = new Vector3();
|
|
247
|
-
const quat = new Quaternion();
|
|
248
|
-
const euler = new Euler();
|
|
249
|
-
const scl = new Vector3();
|
|
250
|
-
|
|
251
|
-
meshRefs.current.forEach(mesh => {
|
|
252
|
-
if (!mesh) return;
|
|
253
|
-
|
|
254
|
-
group.instances.forEach((inst, i) => {
|
|
255
|
-
pos.set(...inst.position);
|
|
256
|
-
euler.set(...inst.rotation);
|
|
257
|
-
quat.setFromEuler(euler);
|
|
258
|
-
scl.set(...inst.scale);
|
|
259
|
-
matrix.compose(pos, quat, scl);
|
|
260
|
-
mesh.setMatrixAt(i, matrix);
|
|
261
|
-
});
|
|
262
|
-
mesh.instanceMatrix.needsUpdate = true;
|
|
263
|
-
});
|
|
264
|
-
|
|
265
|
-
// Update rigid body positions when instances change
|
|
266
|
-
if (rigidBodiesRef.current) {
|
|
267
|
-
try {
|
|
268
|
-
group.instances.forEach((inst, i) => {
|
|
269
|
-
const body = rigidBodiesRef.current?.at(i);
|
|
270
|
-
if (body && body.setTranslation && body.setRotation) {
|
|
271
|
-
pos.set(...inst.position);
|
|
272
|
-
euler.set(...inst.rotation);
|
|
273
|
-
quat.setFromEuler(euler);
|
|
274
|
-
body.setTranslation(pos, false);
|
|
275
|
-
body.setRotation(quat, false);
|
|
276
|
-
}
|
|
277
|
-
});
|
|
278
|
-
} catch (error) {
|
|
279
|
-
// Ignore errors when switching between instanced/non-instanced states
|
|
280
|
-
console.warn('Failed to update rigidbody positions:', error);
|
|
281
|
-
}
|
|
282
|
-
}
|
|
283
|
-
}, [group.instances]);
|
|
284
|
-
|
|
285
|
-
const colliders = group.physicsType === 'fixed' ? 'trimesh' : 'hull';
|
|
286
|
-
|
|
287
|
-
// Handle click on instanced mesh in edit mode
|
|
288
|
-
const handleClick = (e: any) => {
|
|
289
|
-
if (!editMode || !onSelect) return;
|
|
290
|
-
e.stopPropagation();
|
|
291
|
-
|
|
292
|
-
// Get the instance index from the intersection
|
|
293
|
-
const instanceId = e.instanceId;
|
|
294
|
-
if (instanceId !== undefined && group.instances[instanceId]) {
|
|
295
|
-
onSelect(group.instances[instanceId].id);
|
|
296
|
-
}
|
|
297
|
-
};
|
|
298
|
-
|
|
299
|
-
// Add key to force remount when instance count changes significantly (helps with cleanup)
|
|
300
|
-
const rigidBodyKey = `rb_${modelKey}_${group.physicsType}_${group.instances.length}`;
|
|
301
|
-
|
|
302
|
-
return (
|
|
303
|
-
<InstancedRigidBodies
|
|
304
|
-
key={rigidBodyKey}
|
|
305
|
-
ref={rigidBodiesRef}
|
|
306
|
-
instances={instances}
|
|
307
|
-
colliders={colliders}
|
|
308
|
-
type={group.physicsType as 'dynamic' | 'fixed'}
|
|
309
|
-
>
|
|
310
|
-
{Array.from({ length: partCount }).map((_, i) => {
|
|
311
|
-
const mesh = flatMeshes[`${modelKey}__${i}`];
|
|
312
|
-
if (!mesh) return null;
|
|
313
|
-
return (
|
|
314
|
-
<instancedMesh
|
|
315
|
-
key={i}
|
|
316
|
-
ref={el => { meshRefs.current[i] = el; }}
|
|
317
|
-
args={[mesh.geometry, mesh.material, group.instances.length]}
|
|
318
|
-
castShadow
|
|
319
|
-
receiveShadow
|
|
320
|
-
frustumCulled={false}
|
|
321
|
-
onClick={editMode ? handleClick : undefined}
|
|
322
|
-
/>
|
|
323
|
-
);
|
|
324
|
-
})}
|
|
325
|
-
</InstancedRigidBodies>
|
|
326
|
-
);
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
// Render non-physics instances using Merged (instancing without rigid bodies)
|
|
330
|
-
function NonPhysicsInstancedGroup({
|
|
331
|
-
modelKey,
|
|
332
|
-
group,
|
|
333
|
-
partCount,
|
|
334
|
-
instancesMap,
|
|
335
|
-
onSelect,
|
|
336
|
-
registerRef,
|
|
337
|
-
selectedId,
|
|
338
|
-
editMode
|
|
339
|
-
}: {
|
|
340
|
-
modelKey: string;
|
|
341
|
-
group: { physicsType: string, instances: InstanceData[] };
|
|
342
|
-
partCount: number;
|
|
343
|
-
instancesMap: Record<string, React.ComponentType<any>>;
|
|
344
|
-
onSelect?: (id: string | null) => void;
|
|
345
|
-
registerRef?: (id: string, obj: Object3D | null) => void;
|
|
346
|
-
selectedId?: string | null;
|
|
347
|
-
editMode?: boolean;
|
|
348
|
-
}) {
|
|
349
|
-
// Pre-compute which Instance components exist for this model
|
|
350
|
-
const InstanceComponents = useMemo(() =>
|
|
351
|
-
Array.from({ length: partCount }, (_, i) => instancesMap[`${modelKey}__${i}`]).filter(Boolean),
|
|
352
|
-
[instancesMap, modelKey, partCount]
|
|
353
|
-
);
|
|
354
|
-
|
|
355
|
-
return (
|
|
356
|
-
<>
|
|
357
|
-
{group.instances.map(inst => (
|
|
358
|
-
<InstanceGroupItem
|
|
359
|
-
key={inst.id}
|
|
360
|
-
instance={inst}
|
|
361
|
-
InstanceComponents={InstanceComponents}
|
|
362
|
-
onSelect={onSelect}
|
|
363
|
-
registerRef={registerRef}
|
|
364
|
-
selectedId={selectedId}
|
|
365
|
-
editMode={editMode}
|
|
366
|
-
/>
|
|
367
|
-
))}
|
|
368
|
-
</>
|
|
369
|
-
);
|
|
370
|
-
}
|
|
371
|
-
|
|
372
|
-
// Individual instance item with its own click state
|
|
373
|
-
function InstanceGroupItem({
|
|
374
|
-
instance,
|
|
375
|
-
InstanceComponents,
|
|
376
|
-
onSelect,
|
|
377
|
-
registerRef,
|
|
378
|
-
selectedId,
|
|
379
|
-
editMode
|
|
380
|
-
}: {
|
|
381
|
-
instance: InstanceData;
|
|
382
|
-
InstanceComponents: React.ComponentType<any>[];
|
|
383
|
-
onSelect?: (id: string | null) => void;
|
|
384
|
-
registerRef?: (id: string, obj: Object3D | null) => void;
|
|
385
|
-
selectedId?: string | null;
|
|
386
|
-
editMode?: boolean;
|
|
387
|
-
}) {
|
|
388
|
-
const clickValid = useRef(false);
|
|
389
|
-
const groupRef = useRef<Group>(null!);
|
|
390
|
-
const isSelected = selectedId === instance.id;
|
|
391
|
-
|
|
392
|
-
// Use BoxHelper when object is selected in edit mode
|
|
393
|
-
useHelper(editMode && isSelected ? groupRef : null, BoxHelper, 'cyan');
|
|
394
|
-
|
|
395
|
-
useEffect(() => {
|
|
396
|
-
registerRef?.(instance.id, groupRef.current);
|
|
397
|
-
}, [instance.id, registerRef]);
|
|
398
|
-
|
|
399
|
-
return (
|
|
400
|
-
<group
|
|
401
|
-
ref={groupRef}
|
|
402
|
-
position={instance.position}
|
|
403
|
-
rotation={instance.rotation}
|
|
404
|
-
scale={instance.scale}
|
|
405
|
-
onPointerDown={(e) => { e.stopPropagation(); clickValid.current = true; }}
|
|
406
|
-
onPointerMove={() => { clickValid.current = false; }}
|
|
407
|
-
onPointerUp={(e) => {
|
|
408
|
-
if (clickValid.current) {
|
|
409
|
-
e.stopPropagation();
|
|
410
|
-
onSelect?.(instance.id);
|
|
411
|
-
}
|
|
412
|
-
clickValid.current = false;
|
|
413
|
-
}}
|
|
414
|
-
>
|
|
415
|
-
{InstanceComponents.map((Instance, i) => <Instance key={i} />)}
|
|
416
|
-
</group>
|
|
417
|
-
);
|
|
418
|
-
}
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
// Hook to check if an instance exists
|
|
422
|
-
export function useInstanceCheck(id: string): boolean {
|
|
423
|
-
const ctx = useContext(GameInstanceContext);
|
|
424
|
-
return ctx?.hasInstance(id) ?? false;
|
|
425
|
-
}
|
|
426
|
-
|
|
427
|
-
// GameInstance component: registers an instance for batch rendering (renders nothing itself)
|
|
428
|
-
export const GameInstance = React.forwardRef<Group, {
|
|
429
|
-
id: string;
|
|
430
|
-
modelUrl: string;
|
|
431
|
-
position: [number, number, number];
|
|
432
|
-
rotation: [number, number, number];
|
|
433
|
-
scale: [number, number, number];
|
|
434
|
-
physics?: PhysicsProps | undefined;
|
|
435
|
-
}>(({
|
|
436
|
-
id,
|
|
437
|
-
modelUrl,
|
|
438
|
-
position,
|
|
439
|
-
rotation,
|
|
440
|
-
scale,
|
|
441
|
-
physics = undefined,
|
|
442
|
-
}, ref) => {
|
|
443
|
-
const ctx = useContext(GameInstanceContext);
|
|
444
|
-
const addInstance = ctx?.addInstance;
|
|
445
|
-
const removeInstance = ctx?.removeInstance;
|
|
446
|
-
|
|
447
|
-
const instance = useMemo<InstanceData>(() => ({
|
|
448
|
-
id,
|
|
449
|
-
meshPath: modelUrl,
|
|
450
|
-
position,
|
|
451
|
-
rotation,
|
|
452
|
-
scale,
|
|
453
|
-
physics,
|
|
454
|
-
}), [id, modelUrl, JSON.stringify(position), JSON.stringify(rotation), JSON.stringify(scale), JSON.stringify(physics)]);
|
|
455
|
-
|
|
456
|
-
useEffect(() => {
|
|
457
|
-
if (!addInstance || !removeInstance) return;
|
|
458
|
-
addInstance(instance);
|
|
459
|
-
return () => {
|
|
460
|
-
removeInstance(instance.id);
|
|
461
|
-
};
|
|
462
|
-
}, [addInstance, removeInstance, instance]);
|
|
463
|
-
|
|
464
|
-
// No visual rendering - provider handles all instanced visuals
|
|
465
|
-
return null;
|
|
466
|
-
});
|