react-three-game 0.0.23 → 0.0.24

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.
@@ -2,30 +2,30 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
2
2
  import { createContext, useContext, useMemo, useRef, useState, useEffect, useCallback } from "react";
3
3
  import { Merged } from '@react-three/drei';
4
4
  import { InstancedRigidBodies } from "@react-three/rapier";
5
- import { useFrame } from "@react-three/fiber";
6
- import { Mesh, Matrix4, Euler, Quaternion, Vector3 } from "three";
5
+ import { Mesh, Matrix4 } from "three";
7
6
  // --- Helpers ---
8
- const arraysEqual = (a, b) => a.length === b.length && a.every((v, i) => v === b[i]);
9
- const instancesEqual = (a, b) => {
7
+ const tupleEqual = (a, b) => a.length === b.length && a.every((v, i) => v === b[i]);
8
+ const instanceChanged = (a, b) => {
10
9
  var _a, _b;
11
- return a.id === b.id &&
12
- a.meshPath === b.meshPath &&
13
- ((_a = a.physics) === null || _a === void 0 ? void 0 : _a.type) === ((_b = b.physics) === null || _b === void 0 ? void 0 : _b.type) &&
14
- arraysEqual(a.position, b.position) &&
15
- arraysEqual(a.rotation, b.rotation) &&
16
- arraysEqual(a.scale, b.scale);
10
+ return a.meshPath !== b.meshPath ||
11
+ ((_a = a.physics) === null || _a === void 0 ? void 0 : _a.type) !== ((_b = b.physics) === null || _b === void 0 ? void 0 : _b.type) ||
12
+ !tupleEqual(a.position, b.position) ||
13
+ !tupleEqual(a.rotation, b.rotation) ||
14
+ !tupleEqual(a.scale, b.scale);
17
15
  };
18
- // Reusable objects for matrix computation (avoid allocations in hot paths)
19
- const _matrix = new Matrix4();
20
- const _position = new Vector3();
21
- const _quaternion = new Quaternion();
22
- const _euler = new Euler();
23
- const _scale = new Vector3();
24
- function composeMatrix(position, rotation, scale, target = _matrix) {
25
- _position.set(...position);
26
- _quaternion.setFromEuler(_euler.set(...rotation));
27
- _scale.set(...scale);
28
- return target.compose(_position, _quaternion, _scale);
16
+ function extractMeshParts(model) {
17
+ model.updateWorldMatrix(false, true);
18
+ const rootInverse = new Matrix4().copy(model.matrixWorld).invert();
19
+ const parts = [];
20
+ model.traverse(child => {
21
+ if (child.isMesh) {
22
+ const mesh = child;
23
+ const geometry = mesh.geometry.clone();
24
+ geometry.applyMatrix4(mesh.matrixWorld.clone().premultiply(rootInverse));
25
+ parts.push(new Mesh(geometry, mesh.material));
26
+ }
27
+ });
28
+ return parts;
29
29
  }
30
30
  // --- Context ---
31
31
  const GameInstanceContext = createContext(null);
@@ -37,7 +37,7 @@ export function GameInstanceProvider({ children, models, onSelect, registerRef }
37
37
  const idx = prev.findIndex(i => i.id === instance.id);
38
38
  if (idx === -1)
39
39
  return [...prev, instance];
40
- if (instancesEqual(prev[idx], instance))
40
+ if (!instanceChanged(prev[idx], instance))
41
41
  return prev;
42
42
  const updated = [...prev];
43
43
  updated[idx] = instance;
@@ -52,19 +52,11 @@ export function GameInstanceProvider({ children, models, onSelect, registerRef }
52
52
  const meshParts = {};
53
53
  const partCounts = {};
54
54
  Object.entries(models).forEach(([modelKey, model]) => {
55
- model.updateWorldMatrix(false, true);
56
- const rootInverse = new Matrix4().copy(model.matrixWorld).invert();
57
- let partIndex = 0;
58
- model.traverse((child) => {
59
- if (child.isMesh) {
60
- const mesh = child;
61
- const geometry = mesh.geometry.clone();
62
- geometry.applyMatrix4(mesh.matrixWorld.clone().premultiply(rootInverse));
63
- meshParts[`${modelKey}__${partIndex}`] = new Mesh(geometry, mesh.material);
64
- partIndex++;
65
- }
55
+ const parts = extractMeshParts(model);
56
+ parts.forEach((mesh, i) => {
57
+ meshParts[`${modelKey}__${i}`] = mesh;
66
58
  });
67
- partCounts[modelKey] = partIndex;
59
+ partCounts[modelKey] = parts.length;
68
60
  });
69
61
  return { meshParts, partCounts };
70
62
  }, [models]);
@@ -99,23 +91,14 @@ export function GameInstanceProvider({ children, models, onSelect, registerRef }
99
91
  })] }));
100
92
  }
101
93
  // --- Physics Instances ---
94
+ // InstancedRigidBodies handles position/rotation/scale via the instances prop.
95
+ // We pass scale in instances and let the library manage matrix updates.
102
96
  function PhysicsInstances({ instances, physicsType, modelKey, partCount, meshParts }) {
103
- const meshRefs = useRef([]);
97
+ // InstancedRigidBodies expects { key, position, rotation, scale }
104
98
  const rigidBodyInstances = useMemo(() => instances.map(({ id, position, rotation, scale }) => ({ key: id, position, rotation, scale })), [instances]);
105
- // Sync visual matrices each frame (physics updates position/rotation, we need to apply scale)
106
- useFrame(() => {
107
- meshRefs.current.forEach(mesh => {
108
- if (!mesh)
109
- return;
110
- instances.forEach((inst, i) => {
111
- mesh.setMatrixAt(i, composeMatrix(inst.position, inst.rotation, inst.scale));
112
- });
113
- mesh.instanceMatrix.needsUpdate = true;
114
- });
115
- });
116
99
  return (_jsx(InstancedRigidBodies, { instances: rigidBodyInstances, type: physicsType, colliders: physicsType === 'fixed' ? 'trimesh' : 'hull', children: Array.from({ length: partCount }, (_, i) => {
117
100
  const mesh = meshParts[`${modelKey}__${i}`];
118
- return mesh ? (_jsx("instancedMesh", { ref: el => { meshRefs.current[i] = el; }, args: [mesh.geometry, mesh.material, instances.length], frustumCulled: false, castShadow: true, receiveShadow: true }, i)) : null;
101
+ return mesh ? (_jsx("instancedMesh", { args: [mesh.geometry, mesh.material, instances.length], frustumCulled: false, castShadow: true, receiveShadow: true }, i)) : null;
119
102
  }) }));
120
103
  }
121
104
  // --- Static Instances (non-physics) ---
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-three-game",
3
- "version": "0.0.23",
3
+ "version": "0.0.24",
4
4
  "description": "Batteries included React Three Fiber game engine",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.js",
@@ -1,8 +1,7 @@
1
1
  import React, { createContext, useContext, useMemo, useRef, useState, useEffect, useCallback } from "react";
2
2
  import { Merged } from '@react-three/drei';
3
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";
4
+ import { Mesh, Matrix4, Object3D } from "three";
6
5
 
7
6
  // --- Types ---
8
7
  export type InstanceData = {
@@ -25,34 +24,31 @@ type GameInstanceContextType = {
25
24
  };
26
25
 
27
26
  // --- Helpers ---
28
- const arraysEqual = (a: number[], b: number[]) =>
27
+ const tupleEqual = (a: readonly number[], b: readonly number[]) =>
29
28
  a.length === b.length && a.every((v, i) => v === b[i]);
30
29
 
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);
30
+ const instanceChanged = (a: InstanceData, b: InstanceData) =>
31
+ a.meshPath !== b.meshPath ||
32
+ a.physics?.type !== b.physics?.type ||
33
+ !tupleEqual(a.position, b.position) ||
34
+ !tupleEqual(a.rotation, b.rotation) ||
35
+ !tupleEqual(a.scale, b.scale);
36
+
37
+ function extractMeshParts(model: Object3D): Mesh[] {
38
+ model.updateWorldMatrix(false, true);
39
+ const rootInverse = new Matrix4().copy(model.matrixWorld).invert();
40
+ const parts: Mesh[] = [];
41
+
42
+ model.traverse(child => {
43
+ if ((child as Mesh).isMesh) {
44
+ const mesh = child as Mesh;
45
+ const geometry = mesh.geometry.clone();
46
+ geometry.applyMatrix4(mesh.matrixWorld.clone().premultiply(rootInverse));
47
+ parts.push(new Mesh(geometry, mesh.material));
48
+ }
49
+ });
50
+
51
+ return parts;
56
52
  }
57
53
 
58
54
  // --- Context ---
@@ -76,7 +72,7 @@ export function GameInstanceProvider({
76
72
  setInstances(prev => {
77
73
  const idx = prev.findIndex(i => i.id === instance.id);
78
74
  if (idx === -1) return [...prev, instance];
79
- if (instancesEqual(prev[idx], instance)) return prev;
75
+ if (!instanceChanged(prev[idx], instance)) return prev;
80
76
  const updated = [...prev];
81
77
  updated[idx] = instance;
82
78
  return updated;
@@ -93,20 +89,11 @@ export function GameInstanceProvider({
93
89
  const partCounts: Record<string, number> = {};
94
90
 
95
91
  Object.entries(models).forEach(([modelKey, model]) => {
96
- model.updateWorldMatrix(false, true);
97
- const rootInverse = new Matrix4().copy(model.matrixWorld).invert();
98
- let partIndex = 0;
99
-
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);
106
- partIndex++;
107
- }
92
+ const parts = extractMeshParts(model);
93
+ parts.forEach((mesh, i) => {
94
+ meshParts[`${modelKey}__${i}`] = mesh;
108
95
  });
109
- partCounts[modelKey] = partIndex;
96
+ partCounts[modelKey] = parts.length;
110
97
  });
111
98
 
112
99
  return { meshParts, partCounts };
@@ -177,6 +164,8 @@ export function GameInstanceProvider({
177
164
  }
178
165
 
179
166
  // --- Physics Instances ---
167
+ // InstancedRigidBodies handles position/rotation/scale via the instances prop.
168
+ // We pass scale in instances and let the library manage matrix updates.
180
169
  function PhysicsInstances({
181
170
  instances,
182
171
  physicsType,
@@ -190,24 +179,12 @@ function PhysicsInstances({
190
179
  partCount: number;
191
180
  meshParts: Record<string, Mesh>;
192
181
  }) {
193
- const meshRefs = useRef<(InstancedMesh | null)[]>([]);
194
-
182
+ // InstancedRigidBodies expects { key, position, rotation, scale }
195
183
  const rigidBodyInstances = useMemo(() =>
196
184
  instances.map(({ id, position, rotation, scale }) => ({ key: id, position, rotation, scale })),
197
185
  [instances]
198
186
  );
199
187
 
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
-
211
188
  return (
212
189
  <InstancedRigidBodies
213
190
  instances={rigidBodyInstances}
@@ -219,7 +196,6 @@ function PhysicsInstances({
219
196
  return mesh ? (
220
197
  <instancedMesh
221
198
  key={i}
222
- ref={el => { meshRefs.current[i] = el; }}
223
199
  args={[mesh.geometry, mesh.material, instances.length]}
224
200
  frustumCulled={false}
225
201
  castShadow