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.
@@ -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
- function arrayEquals(a: number[], b: number[]) {
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, THREE.Mesh>;
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
- , onSelect, registerRef
48
+ models,
49
+ onSelect,
50
+ registerRef
49
51
  }: {
50
52
  children: React.ReactNode,
51
- models: { [filename: string]: THREE.Object3D },
53
+ models: { [filename: string]: Object3D },
52
54
  onSelect?: (id: string | null) => void,
53
- registerRef?: (id: string, obj: THREE.Object3D | null) => void,
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, THREE.Mesh> = {};
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 THREE.Matrix4().copy(root.matrixWorld).invert();
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 THREE.Mesh(geom, obj.material);
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
- {/* 1) Normal prefab hierarchy: NOT inside any <Merged> */}
134
+ {/* Render normal prefab hierarchy (non-instanced objects) */}
132
135
  {children}
133
136
 
134
- {/* 2) Physics instanced groups: no <Merged>, just InstancedRigidBodies */}
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
- {/* 3) Non-physics instanced visuals: own <Merged> per model */}
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
- // Restrict meshes to just this model's parts for this Merged
161
- const meshesForModel: Record<string, THREE.Mesh> = {};
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
- // Physics instancing stays the same
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, THREE.Mesh>
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
- // Non-physics instanced visuals: per-instance group using Merged's Instance components
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
- , onSelect, registerRef
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: THREE.Object3D | null) => void;
252
+ registerRef?: (id: string, obj: Object3D | null) => void;
249
253
  }) {
250
254
  const clickValid = useRef(false);
251
- const handlePointerDown = (e: any) => { e.stopPropagation(); clickValid.current = true; };
252
- const handlePointerMove = () => { if (clickValid.current) clickValid.current = false; };
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 THREE.Object3D | null); }}
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
- // --- GameInstance: just registers an instance, renders nothing ---
287
- export const GameInstance = React.forwardRef<THREE.Group, {
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 a small context object to avoid long param lists
208
+ // Build context object for passing to helper functions
208
209
  const ctx = { gameObject, selectedId, onSelect, registerRef, loadedModels, loadedTextures, editMode };
209
210
 
210
- // --- 1. Transform (local + world) ---
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
- // preserve click/drag detection from previous implementation
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
- if (gameObject.disabled === true || gameObject.hidden === true) return null;
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
- // --- 3. Core content decided by component registry ---
243
+ // --- 4. Render core content using component system ---
245
244
  const core = renderCoreNode(gameObject, ctx, parentMatrix);
246
245
 
247
- // --- 5. Render children (always relative transforms) ---
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
- // --- 4. Wrap with physics if needed ---
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 model/geometry content for a non-instanced node
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 modelComp = gameObject.components?.model;
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
- const isModelAvailable = !!(modelComp && modelComp.properties && modelComp.properties.filename && ctx.loadedModels[modelComp.properties.filename]);
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
- // Separate wrapper components (that accept children) from leaf components
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]) => key !== 'geometry' && key !== 'material' && key !== 'model' && key !== 'transform' && key !== 'physics')
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
- // Check if the component View accepts children by checking function signature
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 the core content (model or mesh)
344
+ // Build core content based on what components exist
348
345
  let coreContent: React.ReactNode;
349
346
 
350
- // If we have a model (non-instanced) render it as a primitive with material override
351
- if (isModelAvailable) {
352
- const modelObj = ctx.loadedModels[modelComp.properties.filename].clone();
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
- <primitive object={modelObj}>
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
- loadedTextures={ctx.loadedTextures}
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
- </primitive>
360
+ </modelDef.View>
368
361
  );
369
362
  } else if (geometry && geometryDef && geometryDef.View) {
370
- // Otherwise, if geometry present, render a mesh
363
+ // Geometry + Material = mesh
371
364
  coreContent = (
372
- <mesh>
373
- <geometryDef.View key="geometry" properties={geometry.properties} {...contextProps} />
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
- loadedTextures={ctx.loadedTextures}
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 geometry or model, just render leaf components
378
+ // No visual component - just render leaves
390
379
  coreContent = <>{leafComponents}</>;
391
380
  }
392
381