@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
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { attachEntityBinding, getEntityBinding } from '@flighthq/entity/contract';
|
|
1
2
|
import {
|
|
2
3
|
acquireIdentityQuaternion,
|
|
3
4
|
acquireQuaternion,
|
|
@@ -55,6 +56,17 @@ describe('clearQuaternionPool', () => {
|
|
|
55
56
|
});
|
|
56
57
|
|
|
57
58
|
describe('releaseQuaternion', () => {
|
|
59
|
+
it('detaches the previous owner binding before the quaternion is reused', () => {
|
|
60
|
+
clearQuaternionPool();
|
|
61
|
+
const q = acquireQuaternion();
|
|
62
|
+
attachEntityBinding(q, { owner: 'first' });
|
|
63
|
+
releaseQuaternion(q);
|
|
64
|
+
|
|
65
|
+
const reused = acquireQuaternion();
|
|
66
|
+
expect(reused).toBe(q);
|
|
67
|
+
expect(getEntityBinding(reused)).toBeNull();
|
|
68
|
+
});
|
|
69
|
+
|
|
58
70
|
it('ignores a falsy value', () => {
|
|
59
71
|
clearQuaternionPool();
|
|
60
72
|
|
package/src/ray3d.test.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { createAabb } from './aabb';
|
|
2
2
|
import { createBoundingSphere } from './boundingSphere';
|
|
3
|
+
import { createCapsule, intersectRay3DCapsule } from './capsule';
|
|
4
|
+
import { createObb, intersectRay3DObb } from './obb';
|
|
3
5
|
import { createPlane } from './plane';
|
|
4
6
|
import {
|
|
5
7
|
createRay3D,
|
|
@@ -73,6 +75,55 @@ describe('getClosestPointBetweenRay3Ds', () => {
|
|
|
73
75
|
// Gap between the parallel rays is 2 along Y.
|
|
74
76
|
expect(Math.hypot(pa.x - pb.x, pa.y - pb.y, pa.z - pb.z)).toBeCloseTo(2, 6);
|
|
75
77
|
});
|
|
78
|
+
|
|
79
|
+
it('clamps to the origin of the first ray when its nearest approach is behind it', () => {
|
|
80
|
+
// Ray a runs +X from the origin; ray b is a vertical line well behind a's start. The nearest
|
|
81
|
+
// point on a's infinite line is at x = -5, which is not on the ray, so a's origin is the answer.
|
|
82
|
+
const a = createRay3D(0, 0, 0, 1, 0, 0);
|
|
83
|
+
const b = createRay3D(-5, 1, 0, 0, 0, 1);
|
|
84
|
+
const pa = createVector3();
|
|
85
|
+
const pb = createVector3();
|
|
86
|
+
getClosestPointBetweenRay3Ds(pa, pb, a, b);
|
|
87
|
+
expect(pa.x).toBeCloseTo(0, 6);
|
|
88
|
+
expect(pa.y).toBeCloseTo(0, 6);
|
|
89
|
+
expect(pa.z).toBeCloseTo(0, 6);
|
|
90
|
+
expect(pb.x).toBeCloseTo(-5, 6);
|
|
91
|
+
expect(pb.y).toBeCloseTo(1, 6);
|
|
92
|
+
expect(pb.z).toBeCloseTo(0, 6);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('clamps both rays to their origins when each nearest approach is behind the other', () => {
|
|
96
|
+
// Both rays head away, so both clamps fire: the second to its origin, and then the first,
|
|
97
|
+
// re-derived from that, to its own.
|
|
98
|
+
const a = createRay3D(5, 0, 0, 1, 0, 0);
|
|
99
|
+
const b = createRay3D(0, 3, 0, 0, 1, 0);
|
|
100
|
+
const pa = createVector3();
|
|
101
|
+
const pb = createVector3();
|
|
102
|
+
getClosestPointBetweenRay3Ds(pa, pb, a, b);
|
|
103
|
+
expect(pa.x).toBeCloseTo(5, 6);
|
|
104
|
+
expect(pa.y).toBeCloseTo(0, 6);
|
|
105
|
+
expect(pb.x).toBeCloseTo(0, 6);
|
|
106
|
+
expect(pb.y).toBeCloseTo(3, 6);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('writes finite points when either direction has no length', () => {
|
|
110
|
+
// A direction of zero length gives the solver a zero denominator on both the second and the
|
|
111
|
+
// first ray in turn. Without its guards this writes NaN into the caller's vectors, which is
|
|
112
|
+
// worse than a poor answer because it spreads silently through whatever consumes it.
|
|
113
|
+
const pa = createVector3();
|
|
114
|
+
const pb = createVector3();
|
|
115
|
+
|
|
116
|
+
getClosestPointBetweenRay3Ds(pa, pb, createRay3D(0, 0, 0, 1, 0, 0), createRay3D(5, 1, 0, 0, 0, 0));
|
|
117
|
+
expect(Number.isFinite(pa.x) && Number.isFinite(pa.y) && Number.isFinite(pa.z)).toBe(true);
|
|
118
|
+
expect(pb.x).toBe(5);
|
|
119
|
+
expect(pb.y).toBe(1);
|
|
120
|
+
|
|
121
|
+
getClosestPointBetweenRay3Ds(pa, pb, createRay3D(0, 0, 0, 0, 0, 0), createRay3D(0, 0, 5, 0, 0, 1));
|
|
122
|
+
expect(pa.x).toBe(0);
|
|
123
|
+
expect(pa.y).toBe(0);
|
|
124
|
+
expect(pa.z).toBe(0);
|
|
125
|
+
expect(Number.isFinite(pb.x) && Number.isFinite(pb.y) && Number.isFinite(pb.z)).toBe(true);
|
|
126
|
+
});
|
|
76
127
|
});
|
|
77
128
|
|
|
78
129
|
describe('getClosestPointOnRay3D', () => {
|
|
@@ -110,6 +161,17 @@ describe('getClosestPointOnRay3D', () => {
|
|
|
110
161
|
expect(p.y).toBeCloseTo(4, 6);
|
|
111
162
|
expect(p.z).toBeCloseTo(0, 6);
|
|
112
163
|
});
|
|
164
|
+
|
|
165
|
+
it('writes the origin for a ray with no direction', () => {
|
|
166
|
+
// A ray with no direction reaches nowhere, so its origin is the only point it has. Dividing
|
|
167
|
+
// by the zero length instead would put NaN in the caller's vector.
|
|
168
|
+
const ray = createRay3D(1, 2, 3, 0, 0, 0);
|
|
169
|
+
const out = createVector3();
|
|
170
|
+
getClosestPointOnRay3D(out, ray, createVector3(9, 9, 9));
|
|
171
|
+
expect(out.x).toBe(1);
|
|
172
|
+
expect(out.y).toBe(2);
|
|
173
|
+
expect(out.z).toBe(3);
|
|
174
|
+
});
|
|
113
175
|
});
|
|
114
176
|
|
|
115
177
|
describe('getRay3DPointAt', () => {
|
|
@@ -172,6 +234,52 @@ describe('intersectRay3DAabb', () => {
|
|
|
172
234
|
const t = intersectRay3DAabb(ray, aabb);
|
|
173
235
|
expect(t).toBeCloseTo(4);
|
|
174
236
|
});
|
|
237
|
+
|
|
238
|
+
it.each([
|
|
239
|
+
['x', createRay3D(5, 1.5, 1.5, -1, 0, 0), createAabb(2, 1, 1, 1, 2, 2)],
|
|
240
|
+
['y', createRay3D(1.5, 5, 1.5, 0, -1, 0), createAabb(1, 2, 1, 2, 1, 2)],
|
|
241
|
+
['z', createRay3D(1.5, 1.5, 5, 0, 0, -1), createAabb(1, 1, 2, 2, 2, 1)],
|
|
242
|
+
])('returns -1 for an AABB empty on %s', (_axis, ray, empty) => {
|
|
243
|
+
expect(intersectRay3DAabb(ray, empty)).toBe(-1);
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
it('returns -1 for a ray with no direction, inside the box or out', () => {
|
|
247
|
+
// A zero-length direction is not a ray, so it never hits anything — the same answer the
|
|
248
|
+
// sphere, capsule, plane and triangle tests give, and not a containment test in disguise.
|
|
249
|
+
const aabb = createAabb(-1, -1, -1, 1, 1, 1);
|
|
250
|
+
expect(intersectRay3DAabb(createRay3D(0, 0, 0, 0, 0, 0), aabb)).toBe(-1);
|
|
251
|
+
expect(intersectRay3DAabb(createRay3D(5, 0, 0, 0, 0, 0), aabb)).toBe(-1);
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
it('enters each of the six sides at that side’s own distance', () => {
|
|
255
|
+
// One slab per axis, written out three times in the source rather than looped, and each one
|
|
256
|
+
// orders its own pair of crossings. A bound picked up from the block above only shows up on
|
|
257
|
+
// the axis it was mistyped on, and only in one direction along it — so the box has six
|
|
258
|
+
// different distances to its sides and each ray is checked against the one it should hit.
|
|
259
|
+
// A cube would hide exactly the mistake this is here to catch.
|
|
260
|
+
const lopsided = createAabb(-1, -4, -6, 2, 3, 5);
|
|
261
|
+
for (const [ox, oy, oz, dx, dy, dz, entry] of RAYS_INTO_LOPSIDED_BOX) {
|
|
262
|
+
expect(intersectRay3DAabb(createRay3D(ox, oy, oz, dx, dy, dz), lopsided)).toBeCloseTo(entry, 6);
|
|
263
|
+
}
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
it('returns -1 for a ray aimed away from the box along any axis', () => {
|
|
267
|
+
// Same six positions, turned around. The box is behind the origin, so both crossings are
|
|
268
|
+
// behind it and the slab that owns that axis is the one that has to reject.
|
|
269
|
+
const lopsided = createAabb(-1, -4, -6, 2, 3, 5);
|
|
270
|
+
for (const [ox, oy, oz, dx, dy, dz] of RAYS_INTO_LOPSIDED_BOX) {
|
|
271
|
+
expect(intersectRay3DAabb(createRay3D(ox, oy, oz, -dx, -dy, -dz), lopsided)).toBe(-1);
|
|
272
|
+
}
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
it('returns -1 for a ray running alongside the box on any axis', () => {
|
|
276
|
+
// No motion on one axis and outside the box on it: the ray stays level with the box forever
|
|
277
|
+
// and never enters, which each slab has to catch without a crossing to compute.
|
|
278
|
+
const lopsided = createAabb(-1, -4, -6, 2, 3, 5);
|
|
279
|
+
expect(intersectRay3DAabb(createRay3D(10, 0, 0, 0, 1, 0), lopsided)).toBe(-1);
|
|
280
|
+
expect(intersectRay3DAabb(createRay3D(0, 10, 0, 0, 0, 1), lopsided)).toBe(-1);
|
|
281
|
+
expect(intersectRay3DAabb(createRay3D(0, 0, 10, 1, 0, 0), lopsided)).toBe(-1);
|
|
282
|
+
});
|
|
175
283
|
});
|
|
176
284
|
|
|
177
285
|
describe('intersectRay3DPlane', () => {
|
|
@@ -193,6 +301,17 @@ describe('intersectRay3DPlane', () => {
|
|
|
193
301
|
const ray = createRay3D(0, 0, 3, 0, 0, 1); // pointing away from z=0
|
|
194
302
|
expect(intersectRay3DPlane(ray, plane)).toBe(-1);
|
|
195
303
|
});
|
|
304
|
+
|
|
305
|
+
it.each([1, 1e-6, 1e-12])('is invariant when the plane equation is scaled by %s', (scale) => {
|
|
306
|
+
const plane = createPlane(0, 0, scale, 3 * scale);
|
|
307
|
+
const ray = createRay3D(0, 0, -6, 0, 0, 2);
|
|
308
|
+
expect(intersectRay3DPlane(ray, plane)).toBeCloseTo(1.5, 10);
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
it('returns -1 for a zero normal or zero direction', () => {
|
|
312
|
+
expect(intersectRay3DPlane(createRay3D(0, 0, -3, 0, 0, 1), createPlane())).toBe(-1);
|
|
313
|
+
expect(intersectRay3DPlane(createRay3D(0, 0, -3, 0, 0, 0), createPlane(0, 0, 1, 0))).toBe(-1);
|
|
314
|
+
});
|
|
196
315
|
});
|
|
197
316
|
|
|
198
317
|
describe('intersectRay3DSphere', () => {
|
|
@@ -220,6 +339,18 @@ describe('intersectRay3DSphere', () => {
|
|
|
220
339
|
const ray = createRay3D(0, 0, -5, 0, 0, 1);
|
|
221
340
|
expect(intersectRay3DSphere(ray, sphere)).toBe(-1);
|
|
222
341
|
});
|
|
342
|
+
|
|
343
|
+
it('returns -1 when the sphere is squarely behind the ray', () => {
|
|
344
|
+
// Aimed away rather than off to one side: the line through the ray does cross the sphere, so
|
|
345
|
+
// this is not the miss the other test covers — both crossings are simply behind the origin.
|
|
346
|
+
const sphere = createBoundingSphere(0, 0, 0, 1);
|
|
347
|
+
expect(intersectRay3DSphere(createRay3D(5, 0, 0, 1, 0, 0), sphere)).toBe(-1);
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
it('returns -1 for a ray with no direction', () => {
|
|
351
|
+
const sphere = createBoundingSphere(0, 0, 0, 1);
|
|
352
|
+
expect(intersectRay3DSphere(createRay3D(0, 0, 0, 0, 0, 0), sphere)).toBe(-1);
|
|
353
|
+
});
|
|
223
354
|
});
|
|
224
355
|
|
|
225
356
|
describe('intersectRay3DTriangle', () => {
|
|
@@ -255,6 +386,66 @@ describe('intersectRay3DTriangle', () => {
|
|
|
255
386
|
const ray = createRay3D(0, 0, -1, 0, 0, 1);
|
|
256
387
|
expect(intersectRay3DTriangle(ray, p, p, p)).toBe(-1);
|
|
257
388
|
});
|
|
389
|
+
|
|
390
|
+
it.each([1, 1e-6, 1e-12])('accepts a non-normalized direction scaled by %s', (scale) => {
|
|
391
|
+
const ray = createRay3D(0, 0.3, -3, 0, 0, scale);
|
|
392
|
+
expect(intersectRay3DTriangle(ray, a, b, c) * scale).toBeCloseTo(3, 10);
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
it.each([1, 1e-3, 1e-6])('does not treat a triangle scaled by %s as degenerate', (scale) => {
|
|
396
|
+
const scaledA = { x: -scale, y: 0, z: 0 };
|
|
397
|
+
const scaledB = { x: scale, y: 0, z: 0 };
|
|
398
|
+
const scaledC = { x: 0, y: scale, z: 0 };
|
|
399
|
+
const ray = createRay3D(0, 0.3 * scale, -3, 0, 0, 1);
|
|
400
|
+
expect(intersectRay3DTriangle(ray, scaledA, scaledB, scaledC)).toBeCloseTo(3, 10);
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
it('returns -1 for a zero direction', () => {
|
|
404
|
+
expect(intersectRay3DTriangle(createRay3D(0, 0.3, -3, 0, 0, 0), a, b, c)).toBe(-1);
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
it('returns -1 for a nonzero direction nearly parallel to the triangle', () => {
|
|
408
|
+
const nearlyParallel = createRay3D(-1, 0.3, -1e-12, 1, 0, 1e-12);
|
|
409
|
+
expect(intersectRay3DTriangle(nearlyParallel, a, b, c)).toBe(-1);
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
it('returns -1 for points inside the triangle’s span but past its far edge', () => {
|
|
413
|
+
// The triangle covers x in [-1, 1] along the base and rises to (0, 1). A ray through
|
|
414
|
+
// (0.9, 0.9) lands in the corner the hypotenuse cuts off: within the reach of both edges
|
|
415
|
+
// measured from the first corner, but outside the triangle itself.
|
|
416
|
+
const ray = createRay3D(0.9, 0.9, -3, 0, 0, 1);
|
|
417
|
+
expect(intersectRay3DTriangle(ray, a, b, c)).toBe(-1);
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
it('returns -1 for points on the far side of the base', () => {
|
|
421
|
+
const ray = createRay3D(0, -0.5, -3, 0, 0, 1);
|
|
422
|
+
expect(intersectRay3DTriangle(ray, a, b, c)).toBe(-1);
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
it('returns -1 for points beyond either end of the first edge', () => {
|
|
426
|
+
// Off the end of the base in each direction. These are rejected on the first barycentric
|
|
427
|
+
// coordinate, before the second is worked out at all — a different rejection from the two
|
|
428
|
+
// above, and the one that stops a point way off the triangle being measured against its far
|
|
429
|
+
// edge as though it were nearby.
|
|
430
|
+
expect(intersectRay3DTriangle(createRay3D(3, 0, -3, 0, 0, 1), a, b, c)).toBe(-1);
|
|
431
|
+
expect(intersectRay3DTriangle(createRay3D(0, 3, -3, 0, 0, 1), a, b, c)).toBe(-1);
|
|
432
|
+
});
|
|
433
|
+
});
|
|
434
|
+
|
|
435
|
+
describe('ray-shape direction scale parity', () => {
|
|
436
|
+
it.each([1, 1e-6, 1e-12])('preserves the hit point across all six shapes at scale %s', (scale) => {
|
|
437
|
+
const ray = createRay3D(5, 0, 0, -scale, 0, 0);
|
|
438
|
+
const hits = [
|
|
439
|
+
intersectRay3DAabb(ray, createAabb(-1, -1, -1, 1, 1, 1)),
|
|
440
|
+
intersectRay3DSphere(ray, createBoundingSphere(0, 0, 0, 1)),
|
|
441
|
+
intersectRay3DCapsule(ray, createCapsule(0, -1, 0, 0, 1, 0, 1)),
|
|
442
|
+
intersectRay3DObb(ray, createObb(0, 0, 0, 1, 1, 1, 0, 0, 0, 1)),
|
|
443
|
+
intersectRay3DPlane(ray, createPlane(1, 0, 0, -1)),
|
|
444
|
+
intersectRay3DTriangle(ray, { x: 1, y: -1, z: -1 }, { x: 1, y: 1, z: -1 }, { x: 1, y: 0, z: 1 }),
|
|
445
|
+
];
|
|
446
|
+
|
|
447
|
+
for (const t of hits) expect(t * scale).toBeCloseTo(4, 10);
|
|
448
|
+
});
|
|
258
449
|
});
|
|
259
450
|
|
|
260
451
|
describe('setRay3D', () => {
|
|
@@ -295,3 +486,16 @@ describe('setRay3D', () => {
|
|
|
295
486
|
expect(ray.direction.z).toBe(0);
|
|
296
487
|
});
|
|
297
488
|
});
|
|
489
|
+
|
|
490
|
+
// Six rays, one per side of the box spanning (-1, -4, -6) to (2, 3, 5), each starting 10 units out
|
|
491
|
+
// along an axis and aimed at it. The last number is where that ray meets the box: all six differ,
|
|
492
|
+
// so a slab reading another axis' bound gives a different answer rather than the same one.
|
|
493
|
+
// Reversing the direction of each turns the set into six rays aimed away from the box.
|
|
494
|
+
const RAYS_INTO_LOPSIDED_BOX: ReadonlyArray<readonly [number, number, number, number, number, number, number]> = [
|
|
495
|
+
[-10, 0, 0, 1, 0, 0, 9],
|
|
496
|
+
[10, 0, 0, -1, 0, 0, 8],
|
|
497
|
+
[0, -10, 0, 0, 1, 0, 6],
|
|
498
|
+
[0, 10, 0, 0, -1, 0, 7],
|
|
499
|
+
[0, 0, -10, 0, 0, 1, 4],
|
|
500
|
+
[0, 0, 10, 0, 0, -1, 5],
|
|
501
|
+
];
|
package/src/rectangle.test.ts
CHANGED
|
@@ -291,6 +291,11 @@ describe('enclosesRectangle', () => {
|
|
|
291
291
|
});
|
|
292
292
|
|
|
293
293
|
describe('equalsRectangle', () => {
|
|
294
|
+
it('returns false when exactly one rectangle is absent', () => {
|
|
295
|
+
expect(equalsRectangle(r, null)).toBe(false);
|
|
296
|
+
expect(equalsRectangle(undefined, r)).toBe(false);
|
|
297
|
+
});
|
|
298
|
+
|
|
294
299
|
it('returns true for identical rectangles', () => {
|
|
295
300
|
expect(equalsRectangle(r, r)).toBe(true);
|
|
296
301
|
expect(equalsRectangle(r, cloneRectangle(r))).toBe(true);
|
|
@@ -307,28 +312,61 @@ describe('equalsRectangle', () => {
|
|
|
307
312
|
expect(equalsRectangle(r, r)).toBe(true);
|
|
308
313
|
expect(equalsRectangle(r, r2)).toBe(true);
|
|
309
314
|
});
|
|
315
|
+
|
|
316
|
+
it('compares every field, not just the position', () => {
|
|
317
|
+
// Four hand-written comparisons in a row: one left reading the wrong field agrees with the
|
|
318
|
+
// right answer on every pair that differs in more than one place, so each differs alone here.
|
|
319
|
+
const base = createRectangle(1, 2, 3, 4);
|
|
320
|
+
expect(equalsRectangle(base, createRectangle(9, 2, 3, 4))).toBe(false);
|
|
321
|
+
expect(equalsRectangle(base, createRectangle(1, 9, 3, 4))).toBe(false);
|
|
322
|
+
expect(equalsRectangle(base, createRectangle(1, 2, 9, 4))).toBe(false);
|
|
323
|
+
expect(equalsRectangle(base, createRectangle(1, 2, 3, 9))).toBe(false);
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
it('returns false when either side is missing', () => {
|
|
327
|
+
// The signature accepts null and undefined, so absence is an answerable input rather than
|
|
328
|
+
// misuse: nothing is equal to a rectangle, including another absence. Two absences are two
|
|
329
|
+
// unknowns, not one shared value — the same answer the three equalsVector* functions give.
|
|
330
|
+
const rect = createRectangle(1, 2, 3, 4);
|
|
331
|
+
expect(equalsRectangle(rect, null)).toBe(false);
|
|
332
|
+
expect(equalsRectangle(null, rect)).toBe(false);
|
|
333
|
+
expect(equalsRectangle(rect, undefined)).toBe(false);
|
|
334
|
+
expect(equalsRectangle(undefined, rect)).toBe(false);
|
|
335
|
+
expect(equalsRectangle(null, null)).toBe(false);
|
|
336
|
+
expect(equalsRectangle(undefined, undefined)).toBe(false);
|
|
337
|
+
expect(equalsRectangle(null, undefined)).toBe(false);
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
it('holds a rectangle equal to itself even with a NaN field', () => {
|
|
341
|
+
// Comparing field by field would say a rectangle carrying NaN differs from itself, and a
|
|
342
|
+
// caller watching for changes would then see one every time it looked.
|
|
343
|
+
const notANumber = createRectangle(NaN, 2, 3, 4);
|
|
344
|
+
expect(equalsRectangle(notANumber, notANumber)).toBe(true);
|
|
345
|
+
expect(equalsRectangle(notANumber, createRectangle(NaN, 2, 3, 4))).toBe(false);
|
|
346
|
+
});
|
|
310
347
|
});
|
|
311
348
|
|
|
312
349
|
describe('expandRectangleToPoint', () => {
|
|
313
|
-
it('
|
|
350
|
+
it('expands a rectangle to contain an outside point', () => {
|
|
351
|
+
const source = createRectangle(10, 10, 5, 5);
|
|
314
352
|
const result = createRectangle();
|
|
315
|
-
expandRectangleToPoint(result,
|
|
316
|
-
expect(result).not.toBe(
|
|
317
|
-
expect(result
|
|
318
|
-
expect(result.y).toBe(-2);
|
|
319
|
-
expect(result.width).toBe(12);
|
|
320
|
-
expect(result.height).toBe(24);
|
|
353
|
+
expandRectangleToPoint(result, source, createVector2(20, 12));
|
|
354
|
+
expect(result).not.toBe(source);
|
|
355
|
+
expect(result).toMatchObject({ x: 10, y: 10, width: 10, height: 5 });
|
|
321
356
|
});
|
|
322
357
|
|
|
323
|
-
it('
|
|
324
|
-
const r = { x:
|
|
325
|
-
const p = { x:
|
|
358
|
+
it('normalizes flipped bounds while expanding', () => {
|
|
359
|
+
const r = { x: 10, y: 10, width: -5, height: -5 };
|
|
360
|
+
const p = { x: 12, y: 4 };
|
|
326
361
|
const result = { x: 0, y: 0, width: 0, height: 0 };
|
|
327
362
|
expandRectangleToPoint(result, r, p);
|
|
328
|
-
expect(result
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
363
|
+
expect(result).toEqual({ x: 5, y: 4, width: 7, height: 6 });
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
it('supports out === sourceRect', () => {
|
|
367
|
+
const source = createRectangle(10, 10, 5, 5);
|
|
368
|
+
expandRectangleToPoint(source, source, createVector2(20, 12));
|
|
369
|
+
expect(source).toMatchObject({ x: 10, y: 10, width: 10, height: 5 });
|
|
332
370
|
});
|
|
333
371
|
});
|
|
334
372
|
|
|
@@ -835,6 +873,12 @@ describe('offsetRectangleByPoint', () => {
|
|
|
835
873
|
expect(out.x).toBe(3);
|
|
836
874
|
expect(out.y).toBe(4);
|
|
837
875
|
});
|
|
876
|
+
|
|
877
|
+
it('supports out === source', () => {
|
|
878
|
+
const source = createRectangle(1, 2, 10, 20);
|
|
879
|
+
offsetRectangleByPoint(source, source, createVector2(3, 4));
|
|
880
|
+
expect(source).toMatchObject({ x: 4, y: 6, width: 10, height: 20 });
|
|
881
|
+
});
|
|
838
882
|
});
|
|
839
883
|
|
|
840
884
|
describe('setEmptyRectangle', () => {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { attachEntityBinding, getEntityBinding } from '@flighthq/entity/contract';
|
|
1
2
|
import type { Rectangle } from '@flighthq/types/contract';
|
|
2
3
|
|
|
3
4
|
import { acquireEmptyRectangle, acquireRectangle, clearRectanglePool, releaseRectangle } from './rectanglePool';
|
|
@@ -65,6 +66,16 @@ describe('clearRectanglePool', () => {
|
|
|
65
66
|
});
|
|
66
67
|
|
|
67
68
|
describe('releaseRectangle', () => {
|
|
69
|
+
it('detaches the previous owner binding before the rectangle is reused', () => {
|
|
70
|
+
const r = acquireRectangle();
|
|
71
|
+
attachEntityBinding(r, { owner: 'first' });
|
|
72
|
+
releaseRectangle(r);
|
|
73
|
+
|
|
74
|
+
const reused = acquireRectangle();
|
|
75
|
+
expect(reused).toBe(r);
|
|
76
|
+
expect(getEntityBinding(reused)).toBeNull();
|
|
77
|
+
});
|
|
78
|
+
|
|
68
79
|
it('handles null safely', () => {
|
|
69
80
|
expect(() => releaseRectangle(null as unknown as Rectangle)).not.toThrow();
|
|
70
81
|
});
|
package/src/transform2d.test.ts
CHANGED
|
@@ -81,4 +81,42 @@ describe('decomposeMatrixToTransform2D', () => {
|
|
|
81
81
|
expect(out.scaleX).toBeCloseTo(1);
|
|
82
82
|
expect(out.scaleY).toBeCloseTo(-1);
|
|
83
83
|
});
|
|
84
|
+
|
|
85
|
+
it('round-trips a mirrored matrix through the forward build', () => {
|
|
86
|
+
expectRoundTrip(createMatrix(1, 0, 0, -1, 0, 0));
|
|
87
|
+
expectRoundTrip(createMatrix(-1, 0, 0, 1, 0, 0));
|
|
88
|
+
expectRoundTrip(createMatrix(0.5, 0.8, 0.8, -0.5, 3, 4));
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
// The failure set is the negative-determinant set on any input, not a mirror-specific shape, so the
|
|
92
|
+
// property is asserted over a grid spanning both determinant signs rather than at hand-picked cases.
|
|
93
|
+
it('round-trips every non-degenerate matrix in a grid spanning both determinant signs', () => {
|
|
94
|
+
const values = [-2, -1, -0.5, 0.5, 1, 2];
|
|
95
|
+
let negativeDeterminantCount = 0;
|
|
96
|
+
for (const a of values) {
|
|
97
|
+
for (const b of values) {
|
|
98
|
+
for (const c of values) {
|
|
99
|
+
for (const d of values) {
|
|
100
|
+
if (Math.abs(a * d - b * c) < 1e-9) continue;
|
|
101
|
+
if (a * d - b * c < 0) negativeDeterminantCount++;
|
|
102
|
+
expectRoundTrip(createMatrix(a, b, c, d, 0, 0));
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
// Guards the guard: a grid that stopped covering reflections would still pass every assertion above.
|
|
108
|
+
expect(negativeDeterminantCount).toBe(572);
|
|
109
|
+
});
|
|
84
110
|
});
|
|
111
|
+
|
|
112
|
+
// Decomposing and rebuilding through the forward convention must reproduce the original cells. This is
|
|
113
|
+
// the losslessness `setNodeLocalMatrix` promises its callers, and it holds for both determinant signs.
|
|
114
|
+
function expectRoundTrip(source: Readonly<ReturnType<typeof createMatrix>>) {
|
|
115
|
+
const out = createTransform2D();
|
|
116
|
+
decomposeMatrixToTransform2D(out, source);
|
|
117
|
+
const rebuilt = buildMatrix(out.x, out.y, out.rotation, out.scaleX, out.scaleY, out.skewX, out.skewY);
|
|
118
|
+
expect(rebuilt.a).toBeCloseTo(source.a);
|
|
119
|
+
expect(rebuilt.b).toBeCloseTo(source.b);
|
|
120
|
+
expect(rebuilt.c).toBeCloseTo(source.c);
|
|
121
|
+
expect(rebuilt.d).toBeCloseTo(source.d);
|
|
122
|
+
}
|