react-three-game 0.0.23 → 0.0.25

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, Quaternion, Vector3 } 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,20 +91,31 @@ export function GameInstanceProvider({ children, models, onSelect, registerRef }
99
91
  })] }));
100
92
  }
101
93
  // --- Physics Instances ---
94
+ // InstancedRigidBodies manages position/rotation from physics simulation.
95
+ // We apply scale once when instances change via useEffect.
102
96
  function PhysicsInstances({ instances, physicsType, modelKey, partCount, meshParts }) {
103
97
  const meshRefs = useRef([]);
104
- 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(() => {
98
+ // InstancedRigidBodies needs position/rotation (scale handled separately)
99
+ const rigidBodyInstances = useMemo(() => instances.map(({ id, position, rotation }) => ({ key: id, position, rotation })), [instances]);
100
+ // Apply scale when instances change
101
+ useEffect(() => {
102
+ const matrix = new Matrix4();
103
+ const pos = new Vector3();
104
+ const quat = new Quaternion();
105
+ const scl = new Vector3();
107
106
  meshRefs.current.forEach(mesh => {
108
107
  if (!mesh)
109
108
  return;
110
109
  instances.forEach((inst, i) => {
111
- mesh.setMatrixAt(i, composeMatrix(inst.position, inst.rotation, inst.scale));
110
+ mesh.getMatrixAt(i, matrix);
111
+ matrix.decompose(pos, quat, scl);
112
+ scl.set(...inst.scale);
113
+ matrix.compose(pos, quat, scl);
114
+ mesh.setMatrixAt(i, matrix);
112
115
  });
113
116
  mesh.instanceMatrix.needsUpdate = true;
114
117
  });
115
- });
118
+ }, [instances]);
116
119
  return (_jsx(InstancedRigidBodies, { instances: rigidBodyInstances, type: physicsType, colliders: physicsType === 'fixed' ? 'trimesh' : 'hull', children: Array.from({ length: partCount }, (_, i) => {
117
120
  const mesh = meshParts[`${modelKey}__${i}`];
118
121
  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;
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.25",
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, Quaternion, Vector3, InstancedMesh } 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 manages position/rotation from physics simulation.
168
+ // We apply scale once when instances change via useEffect.
180
169
  function PhysicsInstances({
181
170
  instances,
182
171
  physicsType,
@@ -192,21 +181,32 @@ function PhysicsInstances({
192
181
  }) {
193
182
  const meshRefs = useRef<(InstancedMesh | null)[]>([]);
194
183
 
184
+ // InstancedRigidBodies needs position/rotation (scale handled separately)
195
185
  const rigidBodyInstances = useMemo(() =>
196
- instances.map(({ id, position, rotation, scale }) => ({ key: id, position, rotation, scale })),
186
+ instances.map(({ id, position, rotation }) => ({ key: id, position, rotation })),
197
187
  [instances]
198
188
  );
199
189
 
200
- // Sync visual matrices each frame (physics updates position/rotation, we need to apply scale)
201
- useFrame(() => {
190
+ // Apply scale when instances change
191
+ useEffect(() => {
192
+ const matrix = new Matrix4();
193
+ const pos = new Vector3();
194
+ const quat = new Quaternion();
195
+ const scl = new Vector3();
196
+
202
197
  meshRefs.current.forEach(mesh => {
203
198
  if (!mesh) return;
199
+
204
200
  instances.forEach((inst, i) => {
205
- mesh.setMatrixAt(i, composeMatrix(inst.position, inst.rotation, inst.scale));
201
+ mesh.getMatrixAt(i, matrix);
202
+ matrix.decompose(pos, quat, scl);
203
+ scl.set(...inst.scale);
204
+ matrix.compose(pos, quat, scl);
205
+ mesh.setMatrixAt(i, matrix);
206
206
  });
207
207
  mesh.instanceMatrix.needsUpdate = true;
208
208
  });
209
- });
209
+ }, [instances]);
210
210
 
211
211
  return (
212
212
  <InstancedRigidBodies