@flighthq/geometry 0.3.0 → 0.3.1-next.1041.35b950c
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/dist/aabb.d.ts +1 -0
- package/dist/aabb.d.ts.map +1 -1
- package/dist/aabb.js +58 -38
- package/dist/aabb.js.map +1 -1
- package/dist/boundingSphere.d.ts.map +1 -1
- package/dist/boundingSphere.js +3 -2
- package/dist/boundingSphere.js.map +1 -1
- package/dist/capsule.d.ts +6 -5
- package/dist/capsule.d.ts.map +1 -1
- package/dist/capsule.js +38 -15
- package/dist/capsule.js.map +1 -1
- package/dist/contract.d.ts +1 -0
- package/dist/contract.d.ts.map +1 -1
- package/dist/contract.js +1 -0
- package/dist/contract.js.map +1 -1
- package/dist/enableGeometryPoolGuards.d.ts +4 -0
- package/dist/enableGeometryPoolGuards.d.ts.map +1 -0
- package/dist/enableGeometryPoolGuards.js +32 -0
- package/dist/enableGeometryPoolGuards.js.map +1 -0
- package/dist/frustum.d.ts +2 -1
- package/dist/frustum.d.ts.map +1 -1
- package/dist/frustum.js +4 -1
- package/dist/frustum.js.map +1 -1
- package/dist/geometryPoolGuards.d.ts +6 -0
- package/dist/geometryPoolGuards.d.ts.map +1 -0
- package/dist/geometryPoolGuards.js +7 -0
- package/dist/geometryPoolGuards.js.map +1 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/matrix.d.ts +12 -9
- package/dist/matrix.d.ts.map +1 -1
- package/dist/matrix.js +103 -106
- package/dist/matrix.js.map +1 -1
- package/dist/matrix3.d.ts +17 -2
- package/dist/matrix3.d.ts.map +1 -1
- package/dist/matrix3.js +88 -44
- package/dist/matrix3.js.map +1 -1
- package/dist/matrix3Pool.d.ts.map +1 -1
- package/dist/matrix3Pool.js +6 -0
- package/dist/matrix3Pool.js.map +1 -1
- package/dist/matrix4.d.ts +23 -10
- package/dist/matrix4.d.ts.map +1 -1
- package/dist/matrix4.js +74 -53
- package/dist/matrix4.js.map +1 -1
- package/dist/matrix4Pool.d.ts.map +1 -1
- package/dist/matrix4Pool.js +6 -0
- package/dist/matrix4Pool.js.map +1 -1
- package/dist/matrixPool.d.ts.map +1 -1
- package/dist/matrixPool.js +6 -0
- package/dist/matrixPool.js.map +1 -1
- package/dist/obb.d.ts +4 -2
- package/dist/obb.d.ts.map +1 -1
- package/dist/obb.js +164 -88
- package/dist/obb.js.map +1 -1
- package/dist/quaternion.d.ts +7 -4
- package/dist/quaternion.d.ts.map +1 -1
- package/dist/quaternion.js +40 -25
- package/dist/quaternion.js.map +1 -1
- package/dist/quaternionPool.d.ts.map +1 -1
- package/dist/quaternionPool.js +6 -0
- package/dist/quaternionPool.js.map +1 -1
- package/dist/ray3d.d.ts +5 -4
- package/dist/ray3d.d.ts.map +1 -1
- package/dist/ray3d.js +23 -8
- package/dist/ray3d.js.map +1 -1
- package/dist/rectangle.d.ts.map +1 -1
- package/dist/rectangle.js +66 -43
- package/dist/rectangle.js.map +1 -1
- package/dist/rectanglePool.d.ts.map +1 -1
- package/dist/rectanglePool.js +6 -0
- package/dist/rectanglePool.js.map +1 -1
- package/dist/transform2d.d.ts.map +1 -1
- package/dist/transform2d.js +5 -2
- package/dist/transform2d.js.map +1 -1
- package/dist/typedarray.js +3 -6
- package/dist/typedarray.js.map +1 -1
- package/dist/vector2.d.ts.map +1 -1
- package/dist/vector2.js +44 -26
- package/dist/vector2.js.map +1 -1
- package/dist/vector2Pool.d.ts.map +1 -1
- package/dist/vector2Pool.js +6 -0
- package/dist/vector2Pool.js.map +1 -1
- package/dist/vector3.d.ts +2 -2
- package/dist/vector3.d.ts.map +1 -1
- package/dist/vector3.js +55 -37
- package/dist/vector3.js.map +1 -1
- package/dist/vector3Pool.d.ts.map +1 -1
- package/dist/vector3Pool.js +6 -0
- package/dist/vector3Pool.js.map +1 -1
- package/dist/vector4.d.ts +2 -2
- package/dist/vector4.d.ts.map +1 -1
- package/dist/vector4.js +66 -48
- package/dist/vector4.js.map +1 -1
- package/dist/vector4Pool.d.ts.map +1 -1
- package/dist/vector4Pool.js +6 -0
- package/dist/vector4Pool.js.map +1 -1
- package/package.json +4 -3
- package/src/aabb.test.ts +18 -0
- package/src/boundingSphere.test.ts +39 -0
- package/src/capsule.test.ts +158 -0
- package/src/enableGeometryPoolGuards.test.ts +121 -0
- package/src/frustum.test.ts +38 -0
- package/src/geometryPoolGuards.test.ts +16 -0
- package/src/matrix.test.ts +52 -6
- package/src/matrix3.test.ts +46 -6
- package/src/matrix3Pool.test.ts +11 -0
- package/src/matrix4.test.ts +553 -135
- package/src/matrix4Pool.test.ts +11 -0
- package/src/matrixPool.test.ts +11 -0
- package/src/obb.test.ts +277 -0
- package/src/plane.test.ts +20 -0
- package/src/quaternion.test.ts +222 -4
- package/src/quaternionPool.test.ts +12 -0
- package/src/ray3d.test.ts +204 -0
- package/src/rectangle.test.ts +58 -14
- package/src/rectanglePool.test.ts +11 -0
- package/src/transform2d.test.ts +38 -0
- package/src/vector2.test.ts +170 -0
- package/src/vector2Pool.test.ts +11 -0
- package/src/vector3.test.ts +210 -0
- package/src/vector3Pool.test.ts +11 -0
- package/src/vector4.test.ts +231 -0
- package/src/vector4Pool.test.ts +11 -0
package/src/capsule.test.ts
CHANGED
|
@@ -62,6 +62,49 @@ describe('getClosestPointOnCapsule', () => {
|
|
|
62
62
|
expect(p.x).toBeCloseTo(1, 5);
|
|
63
63
|
expect(p.y).toBeCloseTo(0, 5);
|
|
64
64
|
});
|
|
65
|
+
|
|
66
|
+
it('answers a point on the axis with a surface point, whichever way the axis runs', () => {
|
|
67
|
+
// A point on the axis has no single nearest surface point, so the answer is a pick from the
|
|
68
|
+
// ring around it — but it still has to be a point on the surface. Each capsule below is
|
|
69
|
+
// centered on the origin and queried at the origin, so a correct answer sits one radius out
|
|
70
|
+
// and square to the axis; an answer pointing along the axis is inside the capsule, not on it.
|
|
71
|
+
const out = createVector3();
|
|
72
|
+
|
|
73
|
+
getClosestPointOnCapsule(out, createCapsule(-5, 0, 0, 5, 0, 0, 1), createVector3(0, 0, 0));
|
|
74
|
+
expect(Math.hypot(out.x, out.y, out.z)).toBeCloseTo(1, 5);
|
|
75
|
+
expect(out.x).toBeCloseTo(0, 5);
|
|
76
|
+
|
|
77
|
+
getClosestPointOnCapsule(out, createCapsule(0, -5, 0, 0, 5, 0, 1), createVector3(0, 0, 0));
|
|
78
|
+
expect(Math.hypot(out.x, out.y, out.z)).toBeCloseTo(1, 5);
|
|
79
|
+
expect(out.y).toBeCloseTo(0, 5);
|
|
80
|
+
|
|
81
|
+
getClosestPointOnCapsule(out, createCapsule(0, 0, -5, 0, 0, 5, 1), createVector3(0, 0, 0));
|
|
82
|
+
expect(Math.hypot(out.x, out.y, out.z)).toBeCloseTo(1, 5);
|
|
83
|
+
expect(out.z).toBeCloseTo(0, 5);
|
|
84
|
+
|
|
85
|
+
// A capsule with no length is a sphere: its center has a whole sphere of nearest points and
|
|
86
|
+
// every direction is square to a zero-length axis, so only the distance is fixed.
|
|
87
|
+
getClosestPointOnCapsule(out, createCapsule(0, 0, 0, 0, 0, 0, 1), createVector3(0, 0, 0));
|
|
88
|
+
expect(Math.hypot(out.x, out.y, out.z)).toBeCloseTo(1, 5);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('answers a point on a diagonal axis with a surface point too', () => {
|
|
92
|
+
// The axis here lines up with no world direction, so the perpendicular is a genuine cross
|
|
93
|
+
// product rather than a swap of coordinates. A correct answer is one radius from the axis
|
|
94
|
+
// point, which for a query at the middle of a centered capsule means square to the axis.
|
|
95
|
+
const evenlyDiagonal = createCapsule(-3, -3, -3, 3, 3, 3, 2);
|
|
96
|
+
const out = createVector3();
|
|
97
|
+
getClosestPointOnCapsule(out, evenlyDiagonal, createVector3(0, 0, 0));
|
|
98
|
+
expect(Math.hypot(out.x, out.y, out.z)).toBeCloseTo(2, 5);
|
|
99
|
+
expect(out.x + out.y + out.z).toBeCloseTo(0, 5);
|
|
100
|
+
|
|
101
|
+
// An axis leaning mostly along x, least along z, so the direction it is crossed with differs
|
|
102
|
+
// again — every ordering of the axis components has to produce a perpendicular.
|
|
103
|
+
const leaningDiagonal = createCapsule(-3, -2, -1, 3, 2, 1, 2);
|
|
104
|
+
getClosestPointOnCapsule(out, leaningDiagonal, createVector3(0, 0, 0));
|
|
105
|
+
expect(Math.hypot(out.x, out.y, out.z)).toBeCloseTo(2, 5);
|
|
106
|
+
expect(out.x * 3 + out.y * 2 + out.z * 1).toBeCloseTo(0, 5);
|
|
107
|
+
});
|
|
65
108
|
});
|
|
66
109
|
|
|
67
110
|
describe('intersectRay3DCapsule', () => {
|
|
@@ -93,6 +136,60 @@ describe('intersectRay3DCapsule', () => {
|
|
|
93
136
|
const t = intersectRay3DCapsule(ray, c);
|
|
94
137
|
expect(t).toBeCloseTo(4, 5);
|
|
95
138
|
});
|
|
139
|
+
|
|
140
|
+
it('returns -1 for a ray with no direction', () => {
|
|
141
|
+
// Matches intersectRay3DSphere: a zero-length direction is not a ray and never hits, even
|
|
142
|
+
// from a point inside the capsule.
|
|
143
|
+
const c = createCapsule(0, -1, 0, 0, 1, 0, 1);
|
|
144
|
+
expect(intersectRay3DCapsule(createRay3D(0, 0, 0, 0, 0, 0), c)).toBe(-1);
|
|
145
|
+
expect(intersectRay3DCapsule(createRay3D(9, 9, 9, 0, 0, 0), c)).toBe(-1);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it('returns -1 for an empty capsule with a negative radius', () => {
|
|
149
|
+
const empty = createCapsule(0, -1, 0, 0, 1, 0, -1);
|
|
150
|
+
expect(intersectRay3DCapsule(createRay3D(5, 0, 0, -1, 0, 0), empty)).toBe(-1);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it('returns -1 for a ray that passes wide of the capsule', () => {
|
|
154
|
+
// Parallel to nothing and outside the cylinder radius, so neither the body nor either cap has
|
|
155
|
+
// a real intersection at all — a different miss from a ray aimed away from a capsule in reach.
|
|
156
|
+
const c = createCapsule(0, -1, 0, 0, 1, 0, 1);
|
|
157
|
+
expect(intersectRay3DCapsule(createRay3D(5, 0, 5, -1, 0, 0), c)).toBe(-1);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it('hits a cap when the ray runs along the capsule axis', () => {
|
|
161
|
+
// Straight down the axis the body test has nothing to solve — the ray never crosses the
|
|
162
|
+
// cylinder wall — so the answer has to come from the cap.
|
|
163
|
+
const c = createCapsule(0, -1, 0, 0, 1, 0, 1);
|
|
164
|
+
expect(intersectRay3DCapsule(createRay3D(0, 5, 0, 0, -1, 0), c)).toBeCloseTo(3, 5);
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it('returns 0 for a ray starting inside a cap and pointing away', () => {
|
|
168
|
+
// Inside the rounded end, heading out. The near root of the cap sphere is behind the origin
|
|
169
|
+
// and the far one ahead, which is the definition of starting inside.
|
|
170
|
+
const c = createCapsule(0, -1, 0, 0, 1, 0, 1);
|
|
171
|
+
expect(intersectRay3DCapsule(createRay3D(0, 1.5, 0, 0, 1, 0), c)).toBe(0);
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it('falls back to a cap when the body hit lands past the end of the axis', () => {
|
|
175
|
+
// These rays cross the infinite cylinder, but above or below the segment the cylinder is not
|
|
176
|
+
// capsule any more, so the body hit must be discarded and the rounded end answered instead.
|
|
177
|
+
// The capsule spans y in [-1, 1] with radius 1; a ray at y = 1.8 crosses the end sphere at
|
|
178
|
+
// x = sqrt(1 - 0.8^2) = 0.6, so it enters 4.4 along from x = 5.
|
|
179
|
+
const c = createCapsule(0, -1, 0, 0, 1, 0, 1);
|
|
180
|
+
expect(intersectRay3DCapsule(createRay3D(5, 1.8, 0, -1, 0, 0), c)).toBeCloseTo(4.4, 5);
|
|
181
|
+
expect(intersectRay3DCapsule(createRay3D(5, -1.8, 0, -1, 0, 0), c)).toBeCloseTo(4.4, 5);
|
|
182
|
+
|
|
183
|
+
// Beyond the reach of the cap, the same arrangement is a clean miss.
|
|
184
|
+
expect(intersectRay3DCapsule(createRay3D(5, 2.5, 0, -1, 0, 0), c)).toBe(-1);
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it('returns -1 from inside the infinite cylinder but past the end cap', () => {
|
|
188
|
+
// The origin is in line with the capsule body yet beyond its end, so being inside the endless
|
|
189
|
+
// cylinder means nothing: from a point outside the capsule heading away, there is no hit.
|
|
190
|
+
const c = createCapsule(0, -1, 0, 0, 1, 0, 1);
|
|
191
|
+
expect(intersectRay3DCapsule(createRay3D(0, 3, 0, 1, 1, 0), c)).toBe(-1);
|
|
192
|
+
});
|
|
96
193
|
});
|
|
97
194
|
|
|
98
195
|
describe('isCapsuleIntersectingCapsule', () => {
|
|
@@ -114,6 +211,33 @@ describe('isCapsuleIntersectingCapsule', () => {
|
|
|
114
211
|
expect(isCapsuleIntersectingCapsule(a, empty)).toBe(false);
|
|
115
212
|
expect(isCapsuleIntersectingCapsule(empty, a)).toBe(false);
|
|
116
213
|
});
|
|
214
|
+
|
|
215
|
+
it('switches over at the true axis distance for every segment arrangement', () => {
|
|
216
|
+
// The segment-to-segment distance behind this test has a separate branch for each way two
|
|
217
|
+
// segments can be arranged — both degenerate to points, one degenerate, parallel, skew, and
|
|
218
|
+
// the two cases where the nearest approach falls beyond an endpoint of the second segment.
|
|
219
|
+
// Each is checked against a distance measured by walking both segments, so a branch that
|
|
220
|
+
// returns the wrong closest pair shows up as an answer that flips at the wrong radius.
|
|
221
|
+
const arrangements: Array<[string, readonly number[], readonly number[]]> = [
|
|
222
|
+
['two points', [1, 2, 3, 1, 2, 3], [4, 6, 3, 4, 6, 3]],
|
|
223
|
+
['point and segment', [0, 5, 0, 0, 5, 0], [-10, 0, 0, 10, 0, 0]],
|
|
224
|
+
['segment and point', [-10, 0, 0, 10, 0, 0], [0, 5, 0, 0, 5, 0]],
|
|
225
|
+
['parallel', [0, 0, 0, 0, 10, 0], [3, 0, 0, 3, 10, 0]],
|
|
226
|
+
['skew, nearest approach inside both', [-5, 0, 0, 5, 0, 0], [0, -5, 2, 0, 5, 2]],
|
|
227
|
+
['nearest approach before the second start', [0, 0, 0, 10, 0, 0], [5, 5, 3, 5, 15, 3]],
|
|
228
|
+
['nearest approach past the second end', [0, 0, 0, 10, 0, 0], [5, -15, 3, 5, -5, 3]],
|
|
229
|
+
];
|
|
230
|
+
|
|
231
|
+
for (const [name, first, second] of arrangements) {
|
|
232
|
+
const gap = measureSegmentDistance(first, second);
|
|
233
|
+
const tooShort = createCapsule(...(first as SegmentArguments), gap * 0.45);
|
|
234
|
+
const longEnough = createCapsule(...(first as SegmentArguments), gap * 0.55);
|
|
235
|
+
const reach = createCapsule(...(second as SegmentArguments), gap * 0.45);
|
|
236
|
+
|
|
237
|
+
expect([name, isCapsuleIntersectingCapsule(tooShort, reach)]).toEqual([name, false]);
|
|
238
|
+
expect([name, isCapsuleIntersectingCapsule(longEnough, reach)]).toEqual([name, true]);
|
|
239
|
+
}
|
|
240
|
+
});
|
|
117
241
|
});
|
|
118
242
|
|
|
119
243
|
describe('isCapsuleIntersectingSphere', () => {
|
|
@@ -140,6 +264,14 @@ describe('isCapsuleIntersectingSphere', () => {
|
|
|
140
264
|
const s = createBoundingSphere(0, 0, 0, 1);
|
|
141
265
|
expect(isCapsuleIntersectingSphere(c, s)).toBe(false);
|
|
142
266
|
});
|
|
267
|
+
|
|
268
|
+
it('treats a capsule with no length as a sphere', () => {
|
|
269
|
+
// Both endpoints in the same place leaves no axis to project onto, and the test reduces to
|
|
270
|
+
// two spheres: they touch when the centers are closer than the radii add up to.
|
|
271
|
+
const point = createCapsule(0, 0, 0, 0, 0, 0, 1);
|
|
272
|
+
expect(isCapsuleIntersectingSphere(point, createBoundingSphere(2.5, 0, 0, 1))).toBe(false);
|
|
273
|
+
expect(isCapsuleIntersectingSphere(point, createBoundingSphere(1.5, 0, 0, 1))).toBe(true);
|
|
274
|
+
});
|
|
143
275
|
});
|
|
144
276
|
|
|
145
277
|
describe('setCapsule', () => {
|
|
@@ -155,3 +287,29 @@ describe('setCapsule', () => {
|
|
|
155
287
|
expect(c.radius).toBe(7);
|
|
156
288
|
});
|
|
157
289
|
});
|
|
290
|
+
|
|
291
|
+
// The six coordinates of a capsule axis: start x, y, z then end x, y, z.
|
|
292
|
+
type SegmentArguments = [number, number, number, number, number, number];
|
|
293
|
+
|
|
294
|
+
// The smallest distance between two segments, found by walking both of them. Deliberately not the
|
|
295
|
+
// closed-form solution the source uses — it is slow and approximate, but it is arrived at
|
|
296
|
+
// independently, so it can disagree with the formula it checks.
|
|
297
|
+
function measureSegmentDistance(first: readonly number[], second: readonly number[]): number {
|
|
298
|
+
const steps = 600;
|
|
299
|
+
let smallest = Number.POSITIVE_INFINITY;
|
|
300
|
+
for (let i = 0; i <= steps; i++) {
|
|
301
|
+
const s = i / steps;
|
|
302
|
+
const px = first[0] + s * (first[3] - first[0]);
|
|
303
|
+
const py = first[1] + s * (first[4] - first[1]);
|
|
304
|
+
const pz = first[2] + s * (first[5] - first[2]);
|
|
305
|
+
for (let j = 0; j <= steps; j++) {
|
|
306
|
+
const t = j / steps;
|
|
307
|
+
const qx = second[0] + t * (second[3] - second[0]);
|
|
308
|
+
const qy = second[1] + t * (second[4] - second[1]);
|
|
309
|
+
const qz = second[2] + t * (second[5] - second[2]);
|
|
310
|
+
const distance = Math.hypot(px - qx, py - qy, pz - qz);
|
|
311
|
+
if (distance < smallest) smallest = distance;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
return smallest;
|
|
315
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { addLogSink, createMemoryLogSink, getMemoryLogSinkEntries, removeLogSink } from '@flighthq/log/contract';
|
|
2
|
+
import type { LogEntry } from '@flighthq/types/contract';
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
areGeometryPoolGuardsEnabled,
|
|
6
|
+
disableGeometryPoolGuards,
|
|
7
|
+
enableGeometryPoolGuards,
|
|
8
|
+
} from './enableGeometryPoolGuards';
|
|
9
|
+
import { acquireMatrix3, clearMatrix3Pool, releaseMatrix3 } from './matrix3Pool';
|
|
10
|
+
import { acquireMatrix4, clearMatrix4Pool, releaseMatrix4 } from './matrix4Pool';
|
|
11
|
+
import { acquireMatrix, clearMatrixPool, releaseMatrix } from './matrixPool';
|
|
12
|
+
import { acquireQuaternion, clearQuaternionPool, releaseQuaternion } from './quaternionPool';
|
|
13
|
+
import { acquireRectangle, clearRectanglePool, releaseRectangle } from './rectanglePool';
|
|
14
|
+
import { acquireVector2, clearVector2Pool, releaseVector2 } from './vector2Pool';
|
|
15
|
+
import { acquireVector3, clearVector3Pool, releaseVector3 } from './vector3Pool';
|
|
16
|
+
import { acquireVector4, clearVector4Pool, releaseVector4 } from './vector4Pool';
|
|
17
|
+
|
|
18
|
+
function captureLog(run: () => void): readonly LogEntry[] {
|
|
19
|
+
const sink = createMemoryLogSink(16);
|
|
20
|
+
addLogSink(sink.sink);
|
|
21
|
+
try {
|
|
22
|
+
run();
|
|
23
|
+
return getMemoryLogSinkEntries(sink);
|
|
24
|
+
} finally {
|
|
25
|
+
removeLogSink(sink.sink);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function clearPools(): void {
|
|
30
|
+
clearMatrixPool();
|
|
31
|
+
clearMatrix3Pool();
|
|
32
|
+
clearMatrix4Pool();
|
|
33
|
+
clearQuaternionPool();
|
|
34
|
+
clearRectanglePool();
|
|
35
|
+
clearVector2Pool();
|
|
36
|
+
clearVector3Pool();
|
|
37
|
+
clearVector4Pool();
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function doubleRelease<Value>(acquire: () => Value, release: (value: Value) => void): void {
|
|
41
|
+
const value = acquire();
|
|
42
|
+
release(value);
|
|
43
|
+
release(value);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function messageOf(entry: Readonly<LogEntry>): string {
|
|
47
|
+
const data = entry.data;
|
|
48
|
+
return typeof data === 'string' ? data : String(data.message);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
beforeEach(clearPools);
|
|
52
|
+
|
|
53
|
+
afterEach(() => {
|
|
54
|
+
disableGeometryPoolGuards();
|
|
55
|
+
clearPools();
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
describe('areGeometryPoolGuardsEnabled', () => {
|
|
59
|
+
it('reports whether the pool release diagnostic is installed', () => {
|
|
60
|
+
expect(areGeometryPoolGuardsEnabled()).toBe(false);
|
|
61
|
+
enableGeometryPoolGuards();
|
|
62
|
+
expect(areGeometryPoolGuardsEnabled()).toBe(true);
|
|
63
|
+
disableGeometryPoolGuards();
|
|
64
|
+
expect(areGeometryPoolGuardsEnabled()).toBe(false);
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
describe('disableGeometryPoolGuards', () => {
|
|
69
|
+
it('restores the production-default silent release path', () => {
|
|
70
|
+
enableGeometryPoolGuards();
|
|
71
|
+
disableGeometryPoolGuards();
|
|
72
|
+
const entries = captureLog(() => doubleRelease(acquireVector2, releaseVector2));
|
|
73
|
+
expect(entries).toHaveLength(0);
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
describe('enableGeometryPoolGuards', () => {
|
|
78
|
+
// ONE firing test deliberately consumes all eight process-lifetime logOnce keys.
|
|
79
|
+
it('warns once for a double release from every geometry pool and stays silent for valid brackets', () => {
|
|
80
|
+
enableGeometryPoolGuards();
|
|
81
|
+
|
|
82
|
+
const pairedEntries = captureLog(() => {
|
|
83
|
+
releaseMatrix(acquireMatrix());
|
|
84
|
+
releaseMatrix3(acquireMatrix3());
|
|
85
|
+
releaseMatrix4(acquireMatrix4());
|
|
86
|
+
releaseQuaternion(acquireQuaternion());
|
|
87
|
+
releaseRectangle(acquireRectangle());
|
|
88
|
+
releaseVector2(acquireVector2());
|
|
89
|
+
releaseVector3(acquireVector3());
|
|
90
|
+
releaseVector4(acquireVector4());
|
|
91
|
+
});
|
|
92
|
+
expect(pairedEntries).toHaveLength(0);
|
|
93
|
+
|
|
94
|
+
const entries = captureLog(() => {
|
|
95
|
+
doubleRelease(acquireMatrix, releaseMatrix);
|
|
96
|
+
doubleRelease(acquireMatrix3, releaseMatrix3);
|
|
97
|
+
doubleRelease(acquireMatrix4, releaseMatrix4);
|
|
98
|
+
doubleRelease(acquireQuaternion, releaseQuaternion);
|
|
99
|
+
doubleRelease(acquireRectangle, releaseRectangle);
|
|
100
|
+
doubleRelease(acquireVector2, releaseVector2);
|
|
101
|
+
doubleRelease(acquireVector3, releaseVector3);
|
|
102
|
+
doubleRelease(acquireVector4, releaseVector4);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
expect(entries).toHaveLength(8);
|
|
106
|
+
expect(entries.every((entry) => entry.channel === 'geometry')).toBe(true);
|
|
107
|
+
const messages = entries.map(messageOf);
|
|
108
|
+
for (const releaseFunction of [
|
|
109
|
+
'releaseMatrix',
|
|
110
|
+
'releaseMatrix3',
|
|
111
|
+
'releaseMatrix4',
|
|
112
|
+
'releaseQuaternion',
|
|
113
|
+
'releaseRectangle',
|
|
114
|
+
'releaseVector2',
|
|
115
|
+
'releaseVector3',
|
|
116
|
+
'releaseVector4',
|
|
117
|
+
]) {
|
|
118
|
+
expect(messages.some((message) => message.includes(releaseFunction))).toBe(true);
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
});
|
package/src/frustum.test.ts
CHANGED
|
@@ -79,6 +79,19 @@ describe('getFrustumCorners', () => {
|
|
|
79
79
|
expect(isFinite(corners[0].x)).toBe(true);
|
|
80
80
|
expect(isFinite(corners[1].x)).toBe(true);
|
|
81
81
|
});
|
|
82
|
+
|
|
83
|
+
it('writes finite corners for a matrix that divides a corner by zero', () => {
|
|
84
|
+
// The perspective divide has no answer when the matrix sends a corner to a w of zero — a
|
|
85
|
+
// point at infinity. Passing the coordinates through undivided keeps the corner finite, so a
|
|
86
|
+
// caller fitting bounds around these corners gets a wrong box rather than an infinite one.
|
|
87
|
+
const corners = Array.from({ length: 8 }, () => createVector3());
|
|
88
|
+
getFrustumCorners(corners, createMatrix4(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0));
|
|
89
|
+
for (const corner of corners) {
|
|
90
|
+
expect(Number.isFinite(corner.x)).toBe(true);
|
|
91
|
+
expect(Number.isFinite(corner.y)).toBe(true);
|
|
92
|
+
expect(Number.isFinite(corner.z)).toBe(true);
|
|
93
|
+
}
|
|
94
|
+
});
|
|
82
95
|
});
|
|
83
96
|
|
|
84
97
|
describe('isFrustumContainingPoint', () => {
|
|
@@ -119,6 +132,16 @@ describe('isFrustumIntersectingAabb', () => {
|
|
|
119
132
|
setFrustumFromMatrix4(f, createTestViewProjection());
|
|
120
133
|
expect(isFrustumIntersectingAabb(f, createAabb(50, 50, -1, 60, 60, 1))).toBe(false);
|
|
121
134
|
});
|
|
135
|
+
|
|
136
|
+
it.each([
|
|
137
|
+
['x', createAabb(0.5, -0.5, -0.5, -0.5, 0.5, 0.5)],
|
|
138
|
+
['y', createAabb(-0.5, 0.5, -0.5, 0.5, -0.5, 0.5)],
|
|
139
|
+
['z', createAabb(-0.5, -0.5, 0.5, 0.5, 0.5, -0.5)],
|
|
140
|
+
])('rejects an AABB empty on %s', (_axis, empty) => {
|
|
141
|
+
const f = createFrustum();
|
|
142
|
+
setFrustumFromMatrix4(f, createMatrix4());
|
|
143
|
+
expect(isFrustumIntersectingAabb(f, empty)).toBe(false);
|
|
144
|
+
});
|
|
122
145
|
});
|
|
123
146
|
|
|
124
147
|
describe('isFrustumIntersectingSphere', () => {
|
|
@@ -157,4 +180,19 @@ describe('setFrustumFromMatrix4', () => {
|
|
|
157
180
|
const len = Math.sqrt(near.a * near.a + near.b * near.b + near.c * near.c);
|
|
158
181
|
expect(len).toBeCloseTo(1, 5);
|
|
159
182
|
});
|
|
183
|
+
|
|
184
|
+
it('writes finite coefficients for a matrix with no frustum in it', () => {
|
|
185
|
+
// Every plane comes out of a matrix row, and a row of zeros has no direction to scale to unit
|
|
186
|
+
// length. The coefficients stay as they are rather than dividing by zero, so a caller that
|
|
187
|
+
// culls against this frustum gets consistent rejections instead of NaN comparisons, which are
|
|
188
|
+
// false in both directions and would let a box pass and fail the same test.
|
|
189
|
+
const f = createFrustum();
|
|
190
|
+
setFrustumFromMatrix4(f, createMatrix4(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0));
|
|
191
|
+
for (const plane of [f.near, f.far, f.left, f.right, f.top, f.bottom]) {
|
|
192
|
+
expect(Number.isFinite(plane.a)).toBe(true);
|
|
193
|
+
expect(Number.isFinite(plane.b)).toBe(true);
|
|
194
|
+
expect(Number.isFinite(plane.c)).toBe(true);
|
|
195
|
+
expect(Number.isFinite(plane.d)).toBe(true);
|
|
196
|
+
}
|
|
197
|
+
});
|
|
160
198
|
});
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { geometryPoolReleaseGuard, setGeometryPoolReleaseGuard } from './geometryPoolGuards';
|
|
2
|
+
|
|
3
|
+
afterEach(() => {
|
|
4
|
+
setGeometryPoolReleaseGuard(null);
|
|
5
|
+
});
|
|
6
|
+
|
|
7
|
+
describe('setGeometryPoolReleaseGuard', () => {
|
|
8
|
+
it('installs and removes the internal release callback', () => {
|
|
9
|
+
const guard = (): void => {};
|
|
10
|
+
setGeometryPoolReleaseGuard(guard);
|
|
11
|
+
expect(geometryPoolReleaseGuard).toBe(guard);
|
|
12
|
+
|
|
13
|
+
setGeometryPoolReleaseGuard(null);
|
|
14
|
+
expect(geometryPoolReleaseGuard).toBeNull();
|
|
15
|
+
});
|
|
16
|
+
});
|
package/src/matrix.test.ts
CHANGED
|
@@ -68,6 +68,12 @@ describe('copyMatrix', () => {
|
|
|
68
68
|
expect(m2.tx).toBe(6);
|
|
69
69
|
expect(m2.ty).toBe(7);
|
|
70
70
|
});
|
|
71
|
+
|
|
72
|
+
it('supports out === source', () => {
|
|
73
|
+
const matrix = createMatrix(2, 3, 4, 5, 6, 7);
|
|
74
|
+
copyMatrix(matrix, matrix);
|
|
75
|
+
expect(matrix).toMatchObject({ a: 2, b: 3, c: 4, d: 5, tx: 6, ty: 7 });
|
|
76
|
+
});
|
|
71
77
|
});
|
|
72
78
|
|
|
73
79
|
describe('copyMatrixColumnFromVector3', () => {
|
|
@@ -95,10 +101,11 @@ describe('copyMatrixColumnFromVector3', () => {
|
|
|
95
101
|
expect(m.ty).toBe(6);
|
|
96
102
|
});
|
|
97
103
|
|
|
98
|
-
it('
|
|
104
|
+
it('throws when column is outside 0 through 2', () => {
|
|
99
105
|
const m = createMatrix();
|
|
100
106
|
const v = createVector3();
|
|
101
|
-
expect(() => copyMatrixColumnFromVector3(m,
|
|
107
|
+
expect(() => copyMatrixColumnFromVector3(m, -1, v)).toThrow(RangeError);
|
|
108
|
+
expect(() => copyMatrixColumnFromVector3(m, 3, v)).toThrow(RangeError);
|
|
102
109
|
});
|
|
103
110
|
});
|
|
104
111
|
|
|
@@ -130,10 +137,11 @@ describe('copyMatrixColumnToVector3', () => {
|
|
|
130
137
|
expect(v.z).toBe(1);
|
|
131
138
|
});
|
|
132
139
|
|
|
133
|
-
it('
|
|
140
|
+
it('throws when column is outside 0 through 2', () => {
|
|
134
141
|
const m = createMatrix();
|
|
135
142
|
const v = createVector3();
|
|
136
|
-
expect(() => copyMatrixColumnToVector3(v,
|
|
143
|
+
expect(() => copyMatrixColumnToVector3(v, -1, m)).toThrow(RangeError);
|
|
144
|
+
expect(() => copyMatrixColumnToVector3(v, 3, m)).toThrow(RangeError);
|
|
137
145
|
});
|
|
138
146
|
});
|
|
139
147
|
|
|
@@ -156,10 +164,17 @@ describe('copyMatrixRowFromVector3', () => {
|
|
|
156
164
|
expect(m.ty).toBe(6);
|
|
157
165
|
});
|
|
158
166
|
|
|
159
|
-
it('
|
|
167
|
+
it('leaves the fixed identity row unchanged for row 2', () => {
|
|
168
|
+
const m = createMatrix(1, 2, 3, 4, 5, 6);
|
|
169
|
+
copyMatrixRowFromVector3(m, 2, createVector3(7, 8, 9));
|
|
170
|
+
expect(m).toMatchObject({ a: 1, b: 2, c: 3, d: 4, tx: 5, ty: 6 });
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it('throws when row is outside 0 through 2', () => {
|
|
160
174
|
const m = createMatrix();
|
|
161
175
|
const v = createVector3();
|
|
162
|
-
expect(() => copyMatrixRowFromVector3(m,
|
|
176
|
+
expect(() => copyMatrixRowFromVector3(m, -1, v)).toThrow(RangeError);
|
|
177
|
+
expect(() => copyMatrixRowFromVector3(m, 3, v)).toThrow(RangeError);
|
|
163
178
|
});
|
|
164
179
|
});
|
|
165
180
|
|
|
@@ -190,6 +205,13 @@ describe('copyMatrixRowToVector3', () => {
|
|
|
190
205
|
expect(v.y).toBe(0);
|
|
191
206
|
expect(v.z).toBe(1);
|
|
192
207
|
});
|
|
208
|
+
|
|
209
|
+
it('throws when row is outside 0 through 2', () => {
|
|
210
|
+
const m = createMatrix();
|
|
211
|
+
const v = createVector3();
|
|
212
|
+
expect(() => copyMatrixRowToVector3(v, -1, m)).toThrow(RangeError);
|
|
213
|
+
expect(() => copyMatrixRowToVector3(v, 3, m)).toThrow(RangeError);
|
|
214
|
+
});
|
|
193
215
|
});
|
|
194
216
|
|
|
195
217
|
describe('createGradientTransformMatrix', () => {
|
|
@@ -472,6 +494,12 @@ describe('inverseMatrixTransformVectorXY', () => {
|
|
|
472
494
|
});
|
|
473
495
|
|
|
474
496
|
describe('matrixTransformBounds', () => {
|
|
497
|
+
it('maps coincident corners to a zero-size rectangle at the matrix translation', () => {
|
|
498
|
+
const out = createRectangle(1, 2, 3, 4);
|
|
499
|
+
matrixTransformBounds(out, createMatrix(2, 0, 0, 3, 7, 11), 5, 5, 5, 5);
|
|
500
|
+
expect(out).toMatchObject({ x: 7, y: 11, width: 0, height: 0 });
|
|
501
|
+
});
|
|
502
|
+
|
|
475
503
|
it('should work when ax > bx or ay > by (flipped input)', () => {
|
|
476
504
|
const m = createMatrix();
|
|
477
505
|
const out = createRectangle();
|
|
@@ -499,6 +527,18 @@ describe('matrixTransformBounds', () => {
|
|
|
499
527
|
expect(out.height).toBeCloseTo(10);
|
|
500
528
|
});
|
|
501
529
|
|
|
530
|
+
it('tracks the fourth corner as a new minimum after positive skew', () => {
|
|
531
|
+
const out = createRectangle();
|
|
532
|
+
matrixTransformBounds(out, createMatrix(1, 1, 1, 1), 0, 1, 1, 0);
|
|
533
|
+
expect(out).toMatchObject({ x: 0, y: 0, width: 2, height: 2 });
|
|
534
|
+
});
|
|
535
|
+
|
|
536
|
+
it('tracks the second and fourth corners after a reflected skew', () => {
|
|
537
|
+
const out = createRectangle();
|
|
538
|
+
matrixTransformBounds(out, createMatrix(-1, -1, 1, 1), 0, 0, 1, 1);
|
|
539
|
+
expect(out).toMatchObject({ x: -1, y: -1, width: 2, height: 2 });
|
|
540
|
+
});
|
|
541
|
+
|
|
502
542
|
it('should handle flipped input coordinates', () => {
|
|
503
543
|
const rect = createRectangle(10, 20, -10, -20);
|
|
504
544
|
const mat = createMatrix();
|
|
@@ -1079,6 +1119,12 @@ describe('setMatrixIdentity', () => {
|
|
|
1079
1119
|
});
|
|
1080
1120
|
|
|
1081
1121
|
describe('setTransformMatrix', () => {
|
|
1122
|
+
it('builds scale and translation without rotation', () => {
|
|
1123
|
+
const out = createMatrix(9, 9, 9, 9, 9, 9);
|
|
1124
|
+
setTransformMatrix(out, 2, 4, 0, 10, 100);
|
|
1125
|
+
expect(out).toMatchObject({ a: 2, b: 0, c: 0, d: 4, tx: 10, ty: 100 });
|
|
1126
|
+
});
|
|
1127
|
+
|
|
1082
1128
|
it('should apply rotate, scale and translation', () => {
|
|
1083
1129
|
const m1 = createMatrix();
|
|
1084
1130
|
rotateMatrix(m1, m1, 45);
|
package/src/matrix3.test.ts
CHANGED
|
@@ -87,6 +87,12 @@ describe('copyMatrix3', () => {
|
|
|
87
87
|
expect(getMatrix3Element(m2, 2, 1)).toBe(9);
|
|
88
88
|
expect(getMatrix3Element(m2, 2, 2)).toBe(10);
|
|
89
89
|
});
|
|
90
|
+
|
|
91
|
+
it('supports out === source', () => {
|
|
92
|
+
const matrix = createMatrix3(2, 3, 4, 5, 6, 7, 8, 9, 10);
|
|
93
|
+
copyMatrix3(matrix, matrix);
|
|
94
|
+
expect(Array.from(matrix.m)).toEqual([2, 5, 8, 3, 6, 9, 4, 7, 10]);
|
|
95
|
+
});
|
|
90
96
|
});
|
|
91
97
|
|
|
92
98
|
describe('copyMatrix3ColumnFromVector3', () => {
|
|
@@ -117,10 +123,11 @@ describe('copyMatrix3ColumnFromVector3', () => {
|
|
|
117
123
|
expect(getMatrix3Element(m, 2, 2)).toBe(7);
|
|
118
124
|
});
|
|
119
125
|
|
|
120
|
-
it('
|
|
126
|
+
it('throws when column is outside 0 through 2', () => {
|
|
121
127
|
const m = createMatrix3();
|
|
122
128
|
const v = createVector3();
|
|
123
|
-
expect(() => copyMatrix3ColumnFromVector3(m,
|
|
129
|
+
expect(() => copyMatrix3ColumnFromVector3(m, -1, v)).toThrow(RangeError);
|
|
130
|
+
expect(() => copyMatrix3ColumnFromVector3(m, 3, v)).toThrow(RangeError);
|
|
124
131
|
});
|
|
125
132
|
});
|
|
126
133
|
|
|
@@ -152,10 +159,11 @@ describe('copyMatrix3ColumnToVector3', () => {
|
|
|
152
159
|
expect(v.z).toBe(9);
|
|
153
160
|
});
|
|
154
161
|
|
|
155
|
-
it('
|
|
162
|
+
it('throws when column is outside 0 through 2', () => {
|
|
156
163
|
const m = createMatrix3();
|
|
157
164
|
const v = createVector3();
|
|
158
|
-
expect(() => copyMatrix3ColumnToVector3(v,
|
|
165
|
+
expect(() => copyMatrix3ColumnToVector3(v, -1, m)).toThrow(RangeError);
|
|
166
|
+
expect(() => copyMatrix3ColumnToVector3(v, 3, m)).toThrow(RangeError);
|
|
159
167
|
});
|
|
160
168
|
|
|
161
169
|
it('should allow matrix-like and vector-like objects', () => {
|
|
@@ -188,10 +196,11 @@ describe('copyMatrix3RowFromVector3', () => {
|
|
|
188
196
|
expect(getMatrix3Element(m, 1, 2)).toBe(6);
|
|
189
197
|
});
|
|
190
198
|
|
|
191
|
-
it('
|
|
199
|
+
it('throws when row is outside 0 through 2', () => {
|
|
192
200
|
const m = createMatrix3();
|
|
193
201
|
const v = createVector3();
|
|
194
|
-
expect(() => copyMatrix3RowFromVector3(m,
|
|
202
|
+
expect(() => copyMatrix3RowFromVector3(m, -1, v)).toThrow(RangeError);
|
|
203
|
+
expect(() => copyMatrix3RowFromVector3(m, 3, v)).toThrow(RangeError);
|
|
195
204
|
});
|
|
196
205
|
|
|
197
206
|
it('should allow matrix-like and vector-like objects', () => {
|
|
@@ -232,6 +241,13 @@ describe('copyMatrix3RowToVector3', () => {
|
|
|
232
241
|
expect(v.z).toBe(1);
|
|
233
242
|
});
|
|
234
243
|
|
|
244
|
+
it('throws when row is outside 0 through 2', () => {
|
|
245
|
+
const m = createMatrix3();
|
|
246
|
+
const v = createVector3();
|
|
247
|
+
expect(() => copyMatrix3RowToVector3(v, -1, m)).toThrow(RangeError);
|
|
248
|
+
expect(() => copyMatrix3RowToVector3(v, 3, m)).toThrow(RangeError);
|
|
249
|
+
});
|
|
250
|
+
|
|
235
251
|
it('should allow matrix-like and vector-like objects', () => {
|
|
236
252
|
// Column-major storage of rows [1,2,3],[4,5,6],[7,8,9]; row 0 = (1,2,3).
|
|
237
253
|
const m = { m: new Float32Array([1, 4, 7, 2, 5, 8, 3, 6, 9]) };
|
|
@@ -458,6 +474,18 @@ describe('isAffineMatrix3', () => {
|
|
|
458
474
|
});
|
|
459
475
|
|
|
460
476
|
describe('multiplyMatrix3', () => {
|
|
477
|
+
it('multiplies a non-affine matrix into distinct and aliased outputs', () => {
|
|
478
|
+
const a = createMatrix3(1, 2, 3, 4, 5, 6, 7, 8, 10);
|
|
479
|
+
const b = createMatrix3(2, 0, 1, 1, 3, 0, 0, 2, 4);
|
|
480
|
+
const expected = createMatrix3(4, 12, 13, 13, 27, 28, 22, 44, 47);
|
|
481
|
+
const out = createMatrix3();
|
|
482
|
+
multiplyMatrix3(out, a, b);
|
|
483
|
+
expect(equalsMatrix3(out, expected)).toBe(true);
|
|
484
|
+
|
|
485
|
+
multiplyMatrix3(a, a, b);
|
|
486
|
+
expect(equalsMatrix3(a, expected)).toBe(true);
|
|
487
|
+
});
|
|
488
|
+
|
|
461
489
|
it('should support out === a', () => {
|
|
462
490
|
const a = createMatrix3(2, 3, 4, 5, 7, 8, 0, 0, 1);
|
|
463
491
|
const b = createMatrix3(11, 13, 17, 19, 23, 29, 0, 0, 1);
|
|
@@ -556,6 +584,12 @@ describe('scaleMatrix3', () => {
|
|
|
556
584
|
expect(out.m[0]).toBe(2); // (0,0)
|
|
557
585
|
expect(out.m[4]).toBe(3); // (1,1)
|
|
558
586
|
});
|
|
587
|
+
|
|
588
|
+
it('supports out === source', () => {
|
|
589
|
+
const matrix = createMatrix3(1, 2, 3, 4, 5, 6, 7, 8, 9);
|
|
590
|
+
scaleMatrix3(matrix, matrix, 2, 3);
|
|
591
|
+
expect(Array.from(matrix.m)).toEqual([2, 8, 14, 6, 15, 24, 3, 6, 9]);
|
|
592
|
+
});
|
|
559
593
|
});
|
|
560
594
|
|
|
561
595
|
describe('setMatrix3', () => {
|
|
@@ -766,6 +800,12 @@ describe('translateMatrix3', () => {
|
|
|
766
800
|
expect(out.m[6]).toBe(2); // (0,2) tx
|
|
767
801
|
expect(out.m[7]).toBe(3); // (1,2) ty
|
|
768
802
|
});
|
|
803
|
+
|
|
804
|
+
it('supports out === source', () => {
|
|
805
|
+
const matrix = createMatrix3(1, 2, 3, 4, 5, 6, 0, 0, 1);
|
|
806
|
+
translateMatrix3(matrix, matrix, 2, 3);
|
|
807
|
+
expect(Array.from(matrix.m)).toEqual([1, 4, 0, 2, 5, 0, 11, 29, 1]);
|
|
808
|
+
});
|
|
769
809
|
});
|
|
770
810
|
|
|
771
811
|
describe('transposeMatrix3', () => {
|
package/src/matrix3Pool.test.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { attachEntityBinding, getEntityBinding } from '@flighthq/entity/contract';
|
|
1
2
|
import { getMatrix3Element } from '@flighthq/geometry/contract';
|
|
2
3
|
import type { Matrix3 } from '@flighthq/types/contract';
|
|
3
4
|
|
|
@@ -67,6 +68,16 @@ describe('clearMatrix3Pool', () => {
|
|
|
67
68
|
});
|
|
68
69
|
|
|
69
70
|
describe('releaseMatrix3', () => {
|
|
71
|
+
it('detaches the previous owner binding before the matrix is reused', () => {
|
|
72
|
+
const m = acquireMatrix3();
|
|
73
|
+
attachEntityBinding(m, { owner: 'first' });
|
|
74
|
+
releaseMatrix3(m);
|
|
75
|
+
|
|
76
|
+
const reused = acquireMatrix3();
|
|
77
|
+
expect(reused).toBe(m);
|
|
78
|
+
expect(getEntityBinding(reused)).toBeNull();
|
|
79
|
+
});
|
|
80
|
+
|
|
70
81
|
it('handles null safely', () => {
|
|
71
82
|
expect(() => releaseMatrix3(null as unknown as Matrix3)).not.toThrow();
|
|
72
83
|
});
|