react-three-game 0.0.59 → 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.
Files changed (67) hide show
  1. package/dist/tools/dragdrop/DragDropLoader.d.ts +8 -8
  2. package/dist/tools/dragdrop/DragDropLoader.js +33 -15
  3. package/dist/tools/dragdrop/index.d.ts +3 -3
  4. package/dist/tools/dragdrop/index.js +1 -1
  5. package/dist/tools/dragdrop/modelLoader.d.ts +10 -1
  6. package/dist/tools/dragdrop/modelLoader.js +39 -0
  7. package/dist/tools/prefabeditor/PrefabEditor.js +17 -26
  8. package/dist/tools/prefabeditor/PrefabRoot.d.ts +1 -1
  9. package/dist/tools/prefabeditor/PrefabRoot.js +2 -8
  10. package/package.json +9 -3
  11. package/.gitattributes +0 -2
  12. package/.github/copilot-instructions.md +0 -83
  13. package/.github/workflows/nextjs.yml +0 -99
  14. package/.gitmodules +0 -3
  15. package/assets/architecture.png +0 -0
  16. package/assets/editor.gif +0 -0
  17. package/assets/favicon.ico +0 -0
  18. package/assets/react-three-game-logo.png +0 -0
  19. package/dist/tools/dragdrop/page.d.ts +0 -1
  20. package/dist/tools/dragdrop/page.js +0 -11
  21. package/dist/tools/prefabeditor/EntityEvents.d.ts +0 -54
  22. package/dist/tools/prefabeditor/EntityEvents.js +0 -85
  23. package/dist/tools/prefabeditor/page.d.ts +0 -1
  24. package/dist/tools/prefabeditor/page.js +0 -5
  25. package/react-three-game-skill/.gitattributes +0 -2
  26. package/react-three-game-skill/README.md +0 -7
  27. package/react-three-game-skill/react-three-game/SKILL.md +0 -514
  28. package/react-three-game-skill/react-three-game/rules/ADVANCED_PHYSICS.md +0 -472
  29. package/react-three-game-skill/react-three-game/rules/LIGHTING.md +0 -6
  30. package/src/helpers/SoundManager.ts +0 -130
  31. package/src/helpers/index.ts +0 -91
  32. package/src/index.ts +0 -59
  33. package/src/shared/ContactShadow.tsx +0 -74
  34. package/src/shared/GameCanvas.tsx +0 -52
  35. package/src/tools/assetviewer/page.tsx +0 -425
  36. package/src/tools/dragdrop/DragDropLoader.tsx +0 -136
  37. package/src/tools/dragdrop/index.ts +0 -4
  38. package/src/tools/dragdrop/modelLoader.ts +0 -145
  39. package/src/tools/dragdrop/page.tsx +0 -45
  40. package/src/tools/prefabeditor/Dropdown.tsx +0 -112
  41. package/src/tools/prefabeditor/EditorContext.tsx +0 -25
  42. package/src/tools/prefabeditor/EditorTree.tsx +0 -452
  43. package/src/tools/prefabeditor/EditorTreeMenus.tsx +0 -307
  44. package/src/tools/prefabeditor/EditorUI.tsx +0 -204
  45. package/src/tools/prefabeditor/EventSystem.tsx +0 -36
  46. package/src/tools/prefabeditor/GameEvents.ts +0 -191
  47. package/src/tools/prefabeditor/InstanceProvider.tsx +0 -466
  48. package/src/tools/prefabeditor/PrefabEditor.tsx +0 -262
  49. package/src/tools/prefabeditor/PrefabRoot.tsx +0 -773
  50. package/src/tools/prefabeditor/components/AmbientLightComponent.tsx +0 -34
  51. package/src/tools/prefabeditor/components/CameraComponent.tsx +0 -117
  52. package/src/tools/prefabeditor/components/ComponentRegistry.ts +0 -40
  53. package/src/tools/prefabeditor/components/DirectionalLightComponent.tsx +0 -210
  54. package/src/tools/prefabeditor/components/EnvironmentComponent.tsx +0 -47
  55. package/src/tools/prefabeditor/components/GeometryComponent.tsx +0 -133
  56. package/src/tools/prefabeditor/components/Input.tsx +0 -820
  57. package/src/tools/prefabeditor/components/MaterialComponent.tsx +0 -431
  58. package/src/tools/prefabeditor/components/ModelComponent.tsx +0 -176
  59. package/src/tools/prefabeditor/components/PhysicsComponent.tsx +0 -188
  60. package/src/tools/prefabeditor/components/SpotLightComponent.tsx +0 -109
  61. package/src/tools/prefabeditor/components/TextComponent.tsx +0 -137
  62. package/src/tools/prefabeditor/components/TransformComponent.tsx +0 -173
  63. package/src/tools/prefabeditor/components/index.ts +0 -26
  64. package/src/tools/prefabeditor/page.tsx +0 -10
  65. package/src/tools/prefabeditor/styles.ts +0 -235
  66. package/src/tools/prefabeditor/types.ts +0 -20
  67. 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
- });