@flighthq/geometry 0.5.1-next.903.f434bc2 → 0.5.1-next.905.5fbf787

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/README.md CHANGED
@@ -13,5 +13,5 @@ Import the supported application-facing API from `@flighthq/geometry`. The `@fli
13
13
  This package is part of the locked-version Flight SDK graph. Applications may instead install and import `@flighthq/sdk` when package-level tree shaking is sufficient.
14
14
 
15
15
  - [Flight project](https://github.com/flighthq/flight)
16
- - [Source for @flighthq/geometry@0.5.1-next.903.f434bc2](https://github.com/flighthq/flight/tree/f434bc233778d75aeea61995df3a8cfaec459a85/packages/geometry)
17
- - [License](https://github.com/flighthq/flight/blob/f434bc233778d75aeea61995df3a8cfaec459a85/LICENSE.md)
16
+ - [Source for @flighthq/geometry@0.5.1-next.905.5fbf787](https://github.com/flighthq/flight/tree/5fbf7874064c384307542d68b4dcb6a895ff0dcf/packages/geometry)
17
+ - [License](https://github.com/flighthq/flight/blob/5fbf7874064c384307542d68b4dcb6a895ff0dcf/LICENSE.md)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flighthq/geometry",
3
- "version": "0.5.1-next.903.f434bc2",
3
+ "version": "0.5.1-next.905.5fbf787",
4
4
  "author": "Joshua Granick and other contributors",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -38,10 +38,10 @@
38
38
  "clean:dist": "tsx ../../scripts/clean-package-dist.ts"
39
39
  },
40
40
  "dependencies": {
41
- "@flighthq/entity": "0.5.1-next.903.f434bc2",
42
- "@flighthq/log": "0.5.1-next.903.f434bc2",
43
- "@flighthq/math": "0.5.1-next.903.f434bc2",
44
- "@flighthq/types": "0.5.1-next.903.f434bc2"
41
+ "@flighthq/entity": "0.5.1-next.905.5fbf787",
42
+ "@flighthq/log": "0.5.1-next.905.5fbf787",
43
+ "@flighthq/math": "0.5.1-next.905.5fbf787",
44
+ "@flighthq/types": "0.5.1-next.905.5fbf787"
45
45
  },
46
46
  "devDependencies": {
47
47
  "typescript": "^5.3.0"
package/src/aabb.test.ts CHANGED
@@ -22,6 +22,8 @@ import {
22
22
  unionAabb,
23
23
  } from '@flighthq/geometry/contract';
24
24
 
25
+ import { initializeAabb } from './aabb';
26
+
25
27
  describe('cloneAabb', () => {
26
28
  it('creates an independent copy with independent corner vectors', () => {
27
29
  const a = createAabb(-1, -2, -3, 4, 5, 6);
@@ -198,6 +200,12 @@ describe('getClosestPointOnAabb', () => {
198
200
  });
199
201
  });
200
202
 
203
+ describe('initializeAabb', () => {
204
+ it('is the construction initializer of createAabb', () => {
205
+ expect(typeof initializeAabb).toBe('function');
206
+ });
207
+ });
208
+
201
209
  describe('intersectAabb', () => {
202
210
  it('produces the overlapping sub-box for two overlapping boxes', () => {
203
211
  const a = createAabb(0, 0, 0, 4, 4, 4);
@@ -361,7 +369,6 @@ describe('transformAabbByMatrix4', () => {
361
369
  expect(a.max.z).toBeCloseTo(6, 6);
362
370
  });
363
371
  });
364
-
365
372
  describe('unionAabb', () => {
366
373
  it('encloses both boxes', () => {
367
374
  const a = createAabb(0, 0, 0, 1, 1, 1);
@@ -15,6 +15,8 @@ import {
15
15
  transformBoundingSphereByMatrix4,
16
16
  } from '@flighthq/geometry/contract';
17
17
 
18
+ import { initializeBoundingSphere } from './boundingSphere';
19
+
18
20
  describe('cloneBoundingSphere', () => {
19
21
  it('creates an independent copy with an independent center', () => {
20
22
  const s = createBoundingSphere(1, 2, 3, 4);
@@ -122,6 +124,12 @@ describe('getClosestPointOnBoundingSphere', () => {
122
124
  });
123
125
  });
124
126
 
127
+ describe('initializeBoundingSphere', () => {
128
+ it('is the construction initializer of createBoundingSphere', () => {
129
+ expect(typeof initializeBoundingSphere).toBe('function');
130
+ });
131
+ });
132
+
125
133
  describe('isBoundingSphereIntersectingBoundingSphere', () => {
126
134
  it('returns true for overlapping spheres', () => {
127
135
  const a = createBoundingSphere(0, 0, 0, 2);
@@ -242,7 +250,6 @@ describe('setBoundingSphereFromAabb', () => {
242
250
  expect(s.radius).toBe(-1);
243
251
  });
244
252
  });
245
-
246
253
  describe('transformBoundingSphereByMatrix4', () => {
247
254
  it('translates the center and preserves the radius', () => {
248
255
  const s = createBoundingSphere(0, 0, 0, 2);
@@ -12,6 +12,8 @@ import {
12
12
  setCapsule,
13
13
  } from '@flighthq/geometry/contract';
14
14
 
15
+ import { initializeCapsule } from './capsule';
16
+
15
17
  describe('createCapsule', () => {
16
18
  it('stores all fields', () => {
17
19
  const c = createCapsule(1, 2, 3, 4, 5, 6, 0.5);
@@ -109,6 +111,12 @@ describe('getClosestPointOnCapsule', () => {
109
111
  });
110
112
  });
111
113
 
114
+ describe('initializeCapsule', () => {
115
+ it('is the construction initializer of createCapsule', () => {
116
+ expect(typeof initializeCapsule).toBe('function');
117
+ });
118
+ });
119
+
112
120
  describe('intersectRay3DCapsule', () => {
113
121
  it('hits a unit capsule from the side', () => {
114
122
  // Capsule from (0,-1,0) to (0,1,0) with radius 1.
@@ -308,20 +316,6 @@ describe('isCapsuleIntersectingSphere', () => {
308
316
  });
309
317
  });
310
318
 
311
- describe('setCapsule', () => {
312
- it('updates all fields in place', () => {
313
- const c = createCapsule(0, 0, 0, 0, 0, 0, 0);
314
- setCapsule(c, 1, 2, 3, 4, 5, 6, 7);
315
- expect(c.startX).toBe(1);
316
- expect(c.startY).toBe(2);
317
- expect(c.startZ).toBe(3);
318
- expect(c.endX).toBe(4);
319
- expect(c.endY).toBe(5);
320
- expect(c.endZ).toBe(6);
321
- expect(c.radius).toBe(7);
322
- });
323
- });
324
-
325
319
  // The six coordinates of a capsule axis: start x, y, z then end x, y, z.
326
320
  type SegmentArguments = [number, number, number, number, number, number];
327
321
 
@@ -347,3 +341,16 @@ function measureSegmentDistance(first: readonly number[], second: readonly numbe
347
341
  }
348
342
  return smallest;
349
343
  }
344
+ describe('setCapsule', () => {
345
+ it('updates all fields in place', () => {
346
+ const c = createCapsule(0, 0, 0, 0, 0, 0, 0);
347
+ setCapsule(c, 1, 2, 3, 4, 5, 6, 7);
348
+ expect(c.startX).toBe(1);
349
+ expect(c.startY).toBe(2);
350
+ expect(c.startZ).toBe(3);
351
+ expect(c.endX).toBe(4);
352
+ expect(c.endY).toBe(5);
353
+ expect(c.endZ).toBe(6);
354
+ expect(c.radius).toBe(7);
355
+ });
356
+ });
@@ -18,6 +18,8 @@ import {
18
18
  } from '@flighthq/geometry/contract';
19
19
  import type { Matrix4 } from '@flighthq/types/contract';
20
20
 
21
+ import { initializeFrustum } from './frustum';
22
+
21
23
  // View-projection for a camera at (0,0,5) looking toward the origin down -z.
22
24
  function createTestViewProjection(): Matrix4 {
23
25
  const projection = createPerspectiveMatrix4(0.5, 1, 0.1, 100);
@@ -96,6 +98,12 @@ describe('getFrustumCorners', () => {
96
98
  });
97
99
  });
98
100
 
101
+ describe('initializeFrustum', () => {
102
+ it('is the construction initializer of createFrustum', () => {
103
+ expect(typeof initializeFrustum).toBe('function');
104
+ });
105
+ });
106
+
99
107
  describe('isFrustumContainingPoint', () => {
100
108
  it('contains a point in front of the camera', () => {
101
109
  const f = createFrustum();
@@ -196,7 +204,6 @@ describe('isFrustumIntersectingSphere', () => {
196
204
  expect(isFrustumIntersectingSphere(f, createBoundingSphere(0, 0, 5.5, 2))).toBe(true);
197
205
  });
198
206
  });
199
-
200
207
  describe('setFrustumFromMatrix4', () => {
201
208
  it('produces unit-length plane normals', () => {
202
209
  const f = createFrustum();
@@ -43,6 +43,8 @@ import {
43
43
  } from '@flighthq/geometry/contract';
44
44
  import type { Matrix, Matrix3Like, Matrix4Like } from '@flighthq/types/contract';
45
45
 
46
+ import { initializeMatrix } from './matrix';
47
+
46
48
  describe('cloneMatrix', () => {
47
49
  it('should clone the matrix3x2 correctly', () => {
48
50
  const m1 = createMatrix(2, 3, 4, 5, 6, 7);
@@ -305,6 +307,12 @@ describe('equalsMatrix', () => {
305
307
  });
306
308
  });
307
309
 
310
+ describe('initializeMatrix', () => {
311
+ it('is the construction initializer of createMatrix', () => {
312
+ expect(typeof initializeMatrix).toBe('function');
313
+ });
314
+ });
315
+
308
316
  describe('inverseMatrix', () => {
309
317
  it('should invert the matrix3x2 correctly', () => {
310
318
  // Create a matrix3x2 with scaling of 2 and translation of (5, 3)
@@ -1180,7 +1188,6 @@ describe('translateMatrixByVectorXY', () => {
1180
1188
  expect(matrix).toMatchObject({ a: 2, b: 3, c: 4, d: 5, tx: 28, ty: 36 });
1181
1189
  });
1182
1190
  });
1183
-
1184
1191
  describe('writeMatrixToFloat32Array', () => {
1185
1192
  it('writes 6 values at the offset', () => {
1186
1193
  const array = new Float32Array(6);
@@ -30,6 +30,8 @@ import {
30
30
  } from '@flighthq/geometry/contract';
31
31
  import type { Matrix3 } from '@flighthq/types/contract';
32
32
 
33
+ import { initializeMatrix3 } from './matrix3';
34
+
33
35
  // Matrix3 storage is column-major: element (row r, column c) lives at m[3 * c + r], matching
34
36
  // Matrix4 and the GL/GLSL/WGSL uniform ABI. A matrix-like {m} built from a raw Float32Array below
35
37
  // therefore lists its values column-by-column. getMatrix3Element(r, c) / column / row accessors are
@@ -363,6 +365,12 @@ describe('getMatrix3Element', () => {
363
365
  });
364
366
  });
365
367
 
368
+ describe('initializeMatrix3', () => {
369
+ it('is the construction initializer of createMatrix3', () => {
370
+ expect(typeof initializeMatrix3).toBe('function');
371
+ });
372
+ });
373
+
366
374
  describe('inverseMatrix3', () => {
367
375
  it('should invert the matrix correctly', () => {
368
376
  // A scale of 2 with translation of (5, 3): rows [2,0,5],[0,2,3],[0,0,1].
@@ -833,7 +841,6 @@ describe('transposeMatrix3', () => {
833
841
  expect(Array.from(out.m)).toEqual([1, 0, 0, 0, 1, 0, 0, 0, 1]);
834
842
  });
835
843
  });
836
-
837
844
  describe('writeMatrix3ToFloat32Array', () => {
838
845
  it('writes 9 values starting at the given offset', () => {
839
846
  const m = createMatrix3(1, 2, 3, 4, 5, 6, 7, 8, 9);
@@ -58,6 +58,8 @@ import {
58
58
  } from '@flighthq/geometry/contract';
59
59
  import type { Matrix, Matrix4 } from '@flighthq/types/contract';
60
60
 
61
+ import { initializeMatrix4 } from './matrix4';
62
+
61
63
  const X_AXIS = { x: 1, y: 0, z: 0, w: 0 };
62
64
  const Y_AXIS = { x: 0, y: 1, z: 0, w: 0 };
63
65
  const Z_AXIS = { x: 0, y: 0, z: 1, w: 0 };
@@ -837,6 +839,12 @@ describe('getMatrix4Position', () => {
837
839
  });
838
840
  });
839
841
 
842
+ describe('initializeMatrix4', () => {
843
+ it('is the construction initializer of createMatrix4', () => {
844
+ expect(typeof initializeMatrix4).toBe('function');
845
+ });
846
+ });
847
+
840
848
  describe('interpolateMatrix4', () => {
841
849
  it('returns a at t=0', () => {
842
850
  const a = createMatrix4();
@@ -2044,6 +2052,11 @@ describe('transposeMatrix4', () => {
2044
2052
  });
2045
2053
  });
2046
2054
 
2055
+ function expectMatrix4Close(actual: Matrix4, expected: Matrix4): void {
2056
+ for (let i = 0; i < 16; i++) {
2057
+ expect(actual.m[i]).toBeCloseTo(expected.m[i]);
2058
+ }
2059
+ }
2047
2060
  describe('writeMatrix4ToFloat32Array', () => {
2048
2061
  it('writes 16 elements at offset 0', () => {
2049
2062
  const m = createMatrix4();
@@ -2068,9 +2081,3 @@ describe('writeMatrix4ToFloat32Array', () => {
2068
2081
  expect(arr[3]).toBe(0);
2069
2082
  });
2070
2083
  });
2071
-
2072
- function expectMatrix4Close(actual: Matrix4, expected: Matrix4): void {
2073
- for (let i = 0; i < 16; i++) {
2074
- expect(actual.m[i]).toBeCloseTo(expected.m[i]);
2075
- }
2076
- }
package/src/obb.test.ts CHANGED
@@ -20,6 +20,8 @@ import {
20
20
  } from '@flighthq/geometry/contract';
21
21
  import type { Matrix4, Obb, Vector3 } from '@flighthq/types/contract';
22
22
 
23
+ import { initializeObb } from './obb';
24
+
23
25
  describe('createObb', () => {
24
26
  it('stores center, half-extents, and orientation', () => {
25
27
  const o = createObb(1, 2, 3, 4, 5, 6, 0, 0, 0, 1);
@@ -72,6 +74,12 @@ describe('getClosestPointOnObb', () => {
72
74
  });
73
75
  });
74
76
 
77
+ describe('initializeObb', () => {
78
+ it('is the construction initializer of createObb', () => {
79
+ expect(typeof initializeObb).toBe('function');
80
+ });
81
+ });
82
+
75
83
  describe('intersectRay3DObb', () => {
76
84
  it('hits a unit OBB at the expected t from outside', () => {
77
85
  const o = createObb(0, 0, 0, 1, 1, 1, 0, 0, 0, 1);
@@ -293,91 +301,6 @@ describe('setObb', () => {
293
301
  });
294
302
  });
295
303
 
296
- describe('transformObbByMatrix4', () => {
297
- it('translates the center by a translation matrix', () => {
298
- const o = createObb(0, 0, 0, 1, 1, 1, 0, 0, 0, 1);
299
- const m = createMatrix4();
300
- setMatrix4Position(m, createVector3(5, 0, 0));
301
- const out = createObb(0, 0, 0, 0, 0, 0, 0, 0, 0, 1);
302
- transformObbByMatrix4(out, o, m);
303
- expect(out.centerX).toBeCloseTo(5, 6);
304
- expect(out.centerY).toBeCloseTo(0, 6);
305
- expect(out.halfExtentX).toBeCloseTo(1, 6);
306
- expect(out.orientationW).toBeCloseTo(1, 5);
307
- });
308
-
309
- it('scales half-extents by a uniform scale matrix', () => {
310
- const o = createObb(0, 0, 0, 1, 1, 1, 0, 0, 0, 1);
311
- const m = createMatrix4(2, 0, 0, 0, 0, 2, 0, 0, 0, 0, 2, 0, 0, 0, 0, 1);
312
- const out = createObb(0, 0, 0, 0, 0, 0, 0, 0, 0, 1);
313
- transformObbByMatrix4(out, o, m);
314
- expect(out.halfExtentX).toBeCloseTo(2, 6);
315
- expect(out.halfExtentY).toBeCloseTo(2, 6);
316
- expect(out.halfExtentZ).toBeCloseTo(2, 6);
317
- });
318
-
319
- it('supports out === obb', () => {
320
- const o = createObb(1, 0, 0, 1, 1, 1, 0, 0, 0, 1);
321
- const m = createMatrix4();
322
- setMatrix4Position(m, createVector3(0, 3, 0));
323
- transformObbByMatrix4(o, o, m);
324
- expect(o.centerX).toBeCloseTo(1, 6);
325
- expect(o.centerY).toBeCloseTo(3, 6);
326
- expect(o.halfExtentX).toBeCloseTo(1, 6);
327
- });
328
-
329
- it('rotates the box the same way the matrix rotates points', () => {
330
- // The orientation extracted from the matrix must turn the box in the matrix's own direction.
331
- // A conjugated extraction turns it by the same angle about the opposite axis, which is
332
- // invisible for axis-aligned boxes and half turns but wrong for every general rotation.
333
- expectTransformedObbMatchesRotatedPoints(rotationMatrix4(1, 0, 0, 0.7), createTiltedObb());
334
- expectTransformedObbMatchesRotatedPoints(rotationMatrix4(1, 2, 3, 0.9), createTiltedObb());
335
- });
336
-
337
- it('rotates the box correctly near a half turn about each axis', () => {
338
- // A half turn drives the three `trace <= 0` branches of the matrix-to-quaternion extraction,
339
- // one per dominant diagonal term. Just short of pi so the conjugate is a different rotation:
340
- // at exactly pi the quaternion equals its own conjugate up to sign and hides an error.
341
- const nearHalfTurn = Math.PI - 0.01;
342
- expectTransformedObbMatchesRotatedPoints(rotationMatrix4(1, 0, 0, nearHalfTurn), createTiltedObb());
343
- expectTransformedObbMatchesRotatedPoints(rotationMatrix4(0, 1, 0, nearHalfTurn), createTiltedObb());
344
- expectTransformedObbMatchesRotatedPoints(rotationMatrix4(0, 0, 1, nearHalfTurn), createTiltedObb());
345
- });
346
-
347
- it('flattens the box along a collapsed matrix column and keeps the other extents', () => {
348
- // A zero-length column has no direction to read, so that axis falls back to its identity
349
- // direction rather than dividing by zero, and the half-extent along it goes to zero.
350
- const o = createObb(0, 0, 0, 2, 3, 4, 0, 0, 0, 1);
351
- const m = createMatrix4(0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
352
- const out = createObb(0, 0, 0, 0, 0, 0, 0, 0, 0, 1);
353
- transformObbByMatrix4(out, o, m);
354
- expect(out.halfExtentX).toBe(0);
355
- expect(out.halfExtentY).toBeCloseTo(3, 6);
356
- expect(out.halfExtentZ).toBeCloseTo(4, 6);
357
- expect(out.orientationW).toBeCloseTo(1, 6);
358
- });
359
-
360
- it('collapses the box to a point at the matrix position when every column is zero', () => {
361
- // All three columns collapse, so the fallback rotation is the identity and the box keeps the
362
- // orientation it came in with — the transform contributes nothing but the translation.
363
- const tilt = orientationFromAxisAngle(1, 1, 1, 0.6);
364
- const o = createObb(1, 2, 3, 2, 3, 4, ...tilt);
365
- const m = createMatrix4(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 6, 7, 1);
366
- const out = createObb(0, 0, 0, 0, 0, 0, 0, 0, 0, 1);
367
- transformObbByMatrix4(out, o, m);
368
- expect(out.centerX).toBeCloseTo(5, 6);
369
- expect(out.centerY).toBeCloseTo(6, 6);
370
- expect(out.centerZ).toBeCloseTo(7, 6);
371
- expect(out.halfExtentX).toBe(0);
372
- expect(out.halfExtentY).toBe(0);
373
- expect(out.halfExtentZ).toBe(0);
374
- expect(out.orientationX).toBeCloseTo(tilt[0], 6);
375
- expect(out.orientationY).toBeCloseTo(tilt[1], 6);
376
- expect(out.orientationZ).toBeCloseTo(tilt[2], 6);
377
- expect(out.orientationW).toBeCloseTo(tilt[3], 6);
378
- });
379
- });
380
-
381
304
  // A thin rod: long along one of its own local axes, barely thick along the other two, so the
382
305
  // dominant feature of the box is a single edge direction.
383
306
  function createRodObb(
@@ -486,3 +409,87 @@ const FACE_AXIS_SEPARATIONS: ReadonlyArray<{
486
409
  { axis: 1, clear: 4.7, extents: [0.5, 1.5, 2.5], ofTurnedBox: true },
487
410
  { axis: 2, clear: 3.7, extents: [2.5, 1.5, 0.5], ofTurnedBox: true },
488
411
  ];
412
+ describe('transformObbByMatrix4', () => {
413
+ it('translates the center by a translation matrix', () => {
414
+ const o = createObb(0, 0, 0, 1, 1, 1, 0, 0, 0, 1);
415
+ const m = createMatrix4();
416
+ setMatrix4Position(m, createVector3(5, 0, 0));
417
+ const out = createObb(0, 0, 0, 0, 0, 0, 0, 0, 0, 1);
418
+ transformObbByMatrix4(out, o, m);
419
+ expect(out.centerX).toBeCloseTo(5, 6);
420
+ expect(out.centerY).toBeCloseTo(0, 6);
421
+ expect(out.halfExtentX).toBeCloseTo(1, 6);
422
+ expect(out.orientationW).toBeCloseTo(1, 5);
423
+ });
424
+
425
+ it('scales half-extents by a uniform scale matrix', () => {
426
+ const o = createObb(0, 0, 0, 1, 1, 1, 0, 0, 0, 1);
427
+ const m = createMatrix4(2, 0, 0, 0, 0, 2, 0, 0, 0, 0, 2, 0, 0, 0, 0, 1);
428
+ const out = createObb(0, 0, 0, 0, 0, 0, 0, 0, 0, 1);
429
+ transformObbByMatrix4(out, o, m);
430
+ expect(out.halfExtentX).toBeCloseTo(2, 6);
431
+ expect(out.halfExtentY).toBeCloseTo(2, 6);
432
+ expect(out.halfExtentZ).toBeCloseTo(2, 6);
433
+ });
434
+
435
+ it('supports out === obb', () => {
436
+ const o = createObb(1, 0, 0, 1, 1, 1, 0, 0, 0, 1);
437
+ const m = createMatrix4();
438
+ setMatrix4Position(m, createVector3(0, 3, 0));
439
+ transformObbByMatrix4(o, o, m);
440
+ expect(o.centerX).toBeCloseTo(1, 6);
441
+ expect(o.centerY).toBeCloseTo(3, 6);
442
+ expect(o.halfExtentX).toBeCloseTo(1, 6);
443
+ });
444
+
445
+ it('rotates the box the same way the matrix rotates points', () => {
446
+ // The orientation extracted from the matrix must turn the box in the matrix's own direction.
447
+ // A conjugated extraction turns it by the same angle about the opposite axis, which is
448
+ // invisible for axis-aligned boxes and half turns but wrong for every general rotation.
449
+ expectTransformedObbMatchesRotatedPoints(rotationMatrix4(1, 0, 0, 0.7), createTiltedObb());
450
+ expectTransformedObbMatchesRotatedPoints(rotationMatrix4(1, 2, 3, 0.9), createTiltedObb());
451
+ });
452
+
453
+ it('rotates the box correctly near a half turn about each axis', () => {
454
+ // A half turn drives the three `trace <= 0` branches of the matrix-to-quaternion extraction,
455
+ // one per dominant diagonal term. Just short of pi so the conjugate is a different rotation:
456
+ // at exactly pi the quaternion equals its own conjugate up to sign and hides an error.
457
+ const nearHalfTurn = Math.PI - 0.01;
458
+ expectTransformedObbMatchesRotatedPoints(rotationMatrix4(1, 0, 0, nearHalfTurn), createTiltedObb());
459
+ expectTransformedObbMatchesRotatedPoints(rotationMatrix4(0, 1, 0, nearHalfTurn), createTiltedObb());
460
+ expectTransformedObbMatchesRotatedPoints(rotationMatrix4(0, 0, 1, nearHalfTurn), createTiltedObb());
461
+ });
462
+
463
+ it('flattens the box along a collapsed matrix column and keeps the other extents', () => {
464
+ // A zero-length column has no direction to read, so that axis falls back to its identity
465
+ // direction rather than dividing by zero, and the half-extent along it goes to zero.
466
+ const o = createObb(0, 0, 0, 2, 3, 4, 0, 0, 0, 1);
467
+ const m = createMatrix4(0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
468
+ const out = createObb(0, 0, 0, 0, 0, 0, 0, 0, 0, 1);
469
+ transformObbByMatrix4(out, o, m);
470
+ expect(out.halfExtentX).toBe(0);
471
+ expect(out.halfExtentY).toBeCloseTo(3, 6);
472
+ expect(out.halfExtentZ).toBeCloseTo(4, 6);
473
+ expect(out.orientationW).toBeCloseTo(1, 6);
474
+ });
475
+
476
+ it('collapses the box to a point at the matrix position when every column is zero', () => {
477
+ // All three columns collapse, so the fallback rotation is the identity and the box keeps the
478
+ // orientation it came in with — the transform contributes nothing but the translation.
479
+ const tilt = orientationFromAxisAngle(1, 1, 1, 0.6);
480
+ const o = createObb(1, 2, 3, 2, 3, 4, ...tilt);
481
+ const m = createMatrix4(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 6, 7, 1);
482
+ const out = createObb(0, 0, 0, 0, 0, 0, 0, 0, 0, 1);
483
+ transformObbByMatrix4(out, o, m);
484
+ expect(out.centerX).toBeCloseTo(5, 6);
485
+ expect(out.centerY).toBeCloseTo(6, 6);
486
+ expect(out.centerZ).toBeCloseTo(7, 6);
487
+ expect(out.halfExtentX).toBe(0);
488
+ expect(out.halfExtentY).toBe(0);
489
+ expect(out.halfExtentZ).toBe(0);
490
+ expect(out.orientationX).toBeCloseTo(tilt[0], 6);
491
+ expect(out.orientationY).toBeCloseTo(tilt[1], 6);
492
+ expect(out.orientationZ).toBeCloseTo(tilt[2], 6);
493
+ expect(out.orientationW).toBeCloseTo(tilt[3], 6);
494
+ });
495
+ });
package/src/plane.test.ts CHANGED
@@ -13,6 +13,8 @@ import {
13
13
  setPlaneFromPoints,
14
14
  } from '@flighthq/geometry/contract';
15
15
 
16
+ import { initializePlane } from './plane';
17
+
16
18
  describe('clonePlane', () => {
17
19
  it('creates an independent copy', () => {
18
20
  const p = createPlane(1, 0, 0, -5);
@@ -124,6 +126,12 @@ describe('getPlaneSignedDistanceToPoint', () => {
124
126
  });
125
127
  });
126
128
 
129
+ describe('initializePlane', () => {
130
+ it('is the construction initializer of createPlane', () => {
131
+ expect(typeof initializePlane).toBe('function');
132
+ });
133
+ });
134
+
127
135
  describe('normalizePlane', () => {
128
136
  it('normalizes a plane with non-unit normal', () => {
129
137
  // Normal (0,2,0), d=4 → normalized (0,1,0), d=2.
@@ -201,7 +209,6 @@ describe('setPlaneFromNormalAndPoint', () => {
201
209
  expect(p.d).toBeCloseTo(-3, 6);
202
210
  });
203
211
  });
204
-
205
212
  describe('setPlaneFromPoints', () => {
206
213
  it('builds the XZ plane from three points in XZ', () => {
207
214
  const p = createPlane();
@@ -28,6 +28,8 @@ import {
28
28
  } from '@flighthq/geometry/contract';
29
29
  import type { Quaternion } from '@flighthq/types/contract';
30
30
 
31
+ import { initializeQuaternion } from './quaternion';
32
+
31
33
  function expectQuaternionClose(q: Readonly<Quaternion>, x: number, y: number, z: number, w: number): void {
32
34
  expect(q.x).toBeCloseTo(x, 6);
33
35
  expect(q.y).toBeCloseTo(y, 6);
@@ -277,6 +279,12 @@ describe('getQuaternionEuler', () => {
277
279
  });
278
280
  });
279
281
 
282
+ describe('initializeQuaternion', () => {
283
+ it('is the construction initializer of createQuaternion', () => {
284
+ expect(typeof initializeQuaternion).toBe('function');
285
+ });
286
+ });
287
+
280
288
  describe('inverseQuaternion', () => {
281
289
  it('q * inverse(q) ≈ identity for a unit quaternion', () => {
282
290
  const q = createQuaternion();
@@ -679,7 +687,6 @@ describe('setQuaternionLookRotation', () => {
679
687
  expectQuaternionClose(aliasedUp, distinct.x, distinct.y, distinct.z, distinct.w);
680
688
  });
681
689
  });
682
-
683
690
  describe('slerpQuaternion', () => {
684
691
  it('returns a at t=0 and b at t=1', () => {
685
692
  const a = createQuaternion();
package/src/ray3d.test.ts CHANGED
@@ -9,6 +9,7 @@ import {
9
9
  getClosestPointBetweenRay3Ds,
10
10
  getClosestPointOnRay3D,
11
11
  getRay3DPointAt,
12
+ initializeRay3D,
12
13
  intersectRay3DAabb,
13
14
  intersectRay3DPlane,
14
15
  intersectRay3DSphere,
@@ -204,6 +205,12 @@ describe('getRay3DPointAt', () => {
204
205
  });
205
206
  });
206
207
 
208
+ describe('initializeRay3D', () => {
209
+ it('is the construction initializer of createRay3D', () => {
210
+ expect(typeof initializeRay3D).toBe('function');
211
+ });
212
+ });
213
+
207
214
  describe('intersectRay3DAabb', () => {
208
215
  it('returns t=0 when the ray origin is inside the box', () => {
209
216
  const ray = createRay3D(0, 0, 0, 0, 0, 1);
@@ -489,6 +496,18 @@ describe('setRay3D', () => {
489
496
  });
490
497
  });
491
498
 
499
+ // Six rays, one per side of the box spanning (-1, -4, -6) to (2, 3, 5), each starting 10 units out
500
+ // along an axis and aimed at it. The last number is where that ray meets the box: all six differ,
501
+ // so a slab reading another axis' bound gives a different answer rather than the same one.
502
+ // Reversing the direction of each turns the set into six rays aimed away from the box.
503
+ const RAYS_INTO_LOPSIDED_BOX: ReadonlyArray<readonly [number, number, number, number, number, number, number]> = [
504
+ [-10, 0, 0, 1, 0, 0, 9],
505
+ [10, 0, 0, -1, 0, 0, 8],
506
+ [0, -10, 0, 0, 1, 0, 6],
507
+ [0, 10, 0, 0, -1, 0, 7],
508
+ [0, 0, -10, 0, 0, 1, 4],
509
+ [0, 0, 10, 0, 0, -1, 5],
510
+ ];
492
511
  describe('transformRay3DByMatrix4', () => {
493
512
  it('leaves the ray unchanged under the identity matrix', () => {
494
513
  const ray = createRay3D(1, 2, 3, 0, 0, 1);
@@ -530,16 +549,3 @@ describe('transformRay3DByMatrix4', () => {
530
549
  expect(ray.direction.z).toBeCloseTo(1);
531
550
  });
532
551
  });
533
-
534
- // Six rays, one per side of the box spanning (-1, -4, -6) to (2, 3, 5), each starting 10 units out
535
- // along an axis and aimed at it. The last number is where that ray meets the box: all six differ,
536
- // so a slab reading another axis' bound gives a different answer rather than the same one.
537
- // Reversing the direction of each turns the set into six rays aimed away from the box.
538
- const RAYS_INTO_LOPSIDED_BOX: ReadonlyArray<readonly [number, number, number, number, number, number, number]> = [
539
- [-10, 0, 0, 1, 0, 0, 9],
540
- [10, 0, 0, -1, 0, 0, 8],
541
- [0, -10, 0, 0, 1, 0, 6],
542
- [0, 10, 0, 0, -1, 0, 7],
543
- [0, 0, -10, 0, 0, 1, 4],
544
- [0, 0, 10, 0, 0, -1, 5],
545
- ];
@@ -43,6 +43,8 @@ import {
43
43
  } from '@flighthq/geometry/contract';
44
44
  import type { Rectangle } from '@flighthq/types/contract';
45
45
 
46
+ import { initializeRectangle } from './rectangle';
47
+
46
48
  let r: Rectangle;
47
49
  let r2: Rectangle;
48
50
 
@@ -601,6 +603,12 @@ describe('inflateRectangle', () => {
601
603
  });
602
604
  });
603
605
 
606
+ describe('initializeRectangle', () => {
607
+ it('is the construction initializer of createRectangle', () => {
608
+ expect(typeof initializeRectangle).toBe('function');
609
+ });
610
+ });
611
+
604
612
  describe('intersectsRectangle', () => {
605
613
  it('returns true if rectangles overlap', () => {
606
614
  const r3 = createRectangle(5, 10, 10, 10);
@@ -1004,7 +1012,6 @@ describe('setRectangleTopLeft', () => {
1004
1012
  expect(r.y).toBe(10);
1005
1013
  });
1006
1014
  });
1007
-
1008
1015
  describe('setTo', () => {
1009
1016
  it('assigns all four properties', () => {
1010
1017
  const rect = createRectangle();
@@ -1,7 +1,7 @@
1
1
  import { describe, expect, it } from 'vitest';
2
2
 
3
3
  import { createMatrix } from './matrix';
4
- import { createTransform2D, decomposeMatrixToTransform2D } from './transform2d';
4
+ import { createTransform2D, decomposeMatrixToTransform2D, initializeTransform2D } from './transform2d';
5
5
 
6
6
  const DEG_TO_RAD = Math.PI / 180;
7
7
 
@@ -120,3 +120,8 @@ function expectRoundTrip(source: Readonly<ReturnType<typeof createMatrix>>) {
120
120
  expect(rebuilt.c).toBeCloseTo(source.c);
121
121
  expect(rebuilt.d).toBeCloseTo(source.d);
122
122
  }
123
+ describe('initializeTransform2D', () => {
124
+ it('is the construction initializer of createTransform2D', () => {
125
+ expect(typeof initializeTransform2D).toBe('function');
126
+ });
127
+ });
@@ -2,7 +2,12 @@ import { describe, expect, it } from 'vitest';
2
2
 
3
3
  import { createMatrix4 } from './matrix4';
4
4
  import { createQuaternion } from './quaternion';
5
- import { composeMatrix4FromTransform3D, createTransform3D, decomposeMatrix4ToTransform3D } from './transform3d';
5
+ import {
6
+ composeMatrix4FromTransform3D,
7
+ createTransform3D,
8
+ decomposeMatrix4ToTransform3D,
9
+ initializeTransform3D,
10
+ } from './transform3d';
6
11
  import { createVector3 } from './vector3';
7
12
 
8
13
  describe('composeMatrix4FromTransform3D', () => {
@@ -48,3 +53,8 @@ describe('decomposeMatrix4ToTransform3D', () => {
48
53
  expect(out.scale.z).toBeCloseTo(4);
49
54
  });
50
55
  });
56
+ describe('initializeTransform3D', () => {
57
+ it('is the construction initializer of createTransform3D', () => {
58
+ expect(typeof initializeTransform3D).toBe('function');
59
+ });
60
+ });
@@ -36,6 +36,8 @@ import {
36
36
  } from '@flighthq/geometry/contract';
37
37
  import type { Vector2 } from '@flighthq/types/contract';
38
38
 
39
+ import { initializeVector2 } from './vector2';
40
+
39
41
  let pt: Vector2;
40
42
  let pt2: Vector2;
41
43
 
@@ -511,6 +513,12 @@ describe('getVector2LengthSquared', () => {
511
513
  });
512
514
  });
513
515
 
516
+ describe('initializeVector2', () => {
517
+ it('is the construction initializer of createVector2', () => {
518
+ expect(typeof initializeVector2).toBe('function');
519
+ });
520
+ });
521
+
514
522
  describe('interpolateVector2', () => {
515
523
  const cases = [
516
524
  { t: 0, expected: (a: number, _: number) => a },
@@ -1181,7 +1189,6 @@ describe('subtractVector2', () => {
1181
1189
  expect(result).not.toBe(pt2);
1182
1190
  });
1183
1191
  });
1184
-
1185
1192
  describe('writeVector2ToFloat32Array', () => {
1186
1193
  it('writes 2 values at the offset', () => {
1187
1194
  const array = new Float32Array(6);
@@ -39,6 +39,8 @@ import {
39
39
  } from '@flighthq/geometry/contract';
40
40
  import type { Vector3 } from '@flighthq/types/contract';
41
41
 
42
+ import { initializeVector3 } from './vector3';
43
+
42
44
  describe('addVector3', () => {
43
45
  it('returns a new vector when no target is passed', () => {
44
46
  const a = createVector3(1, 2, 3);
@@ -461,6 +463,12 @@ describe('getVector3Spherical', () => {
461
463
  });
462
464
  });
463
465
 
466
+ describe('initializeVector3', () => {
467
+ it('is the construction initializer of createVector3', () => {
468
+ expect(typeof initializeVector3).toBe('function');
469
+ });
470
+ });
471
+
464
472
  describe('interpolateVector3', () => {
465
473
  it('returns a at t=0 and b at t=1', () => {
466
474
  const out = createVector3();
@@ -947,7 +955,6 @@ describe('Y_AXIS', () => {
947
955
  expect(yAxis.z).toBe(0);
948
956
  });
949
957
  });
950
-
951
958
  describe('Z_AXIS', () => {
952
959
  it('returns the unit vector along the Z-axis', () => {
953
960
  const zAxis: Vector3 = VECTOR3_Z_AXIS;
@@ -36,6 +36,8 @@ import {
36
36
  } from '@flighthq/geometry/contract';
37
37
  import type { Vector4 } from '@flighthq/types/contract';
38
38
 
39
+ import { initializeVector4 } from './vector4';
40
+
39
41
  describe('addVector4', () => {
40
42
  it('returns a new vector when no target is passed', () => {
41
43
  const a = createVector4(1, 2, 3, 10);
@@ -398,6 +400,12 @@ describe('getVector4LengthSquared', () => {
398
400
  });
399
401
  });
400
402
 
403
+ describe('initializeVector4', () => {
404
+ it('is the construction initializer of createVector4', () => {
405
+ expect(typeof initializeVector4).toBe('function');
406
+ });
407
+ });
408
+
401
409
  describe('interpolateVector4', () => {
402
410
  it('returns a at t=0 and b at t=1', () => {
403
411
  const out = createVector4();
@@ -583,6 +591,8 @@ describe('nearEqualsVector4', () => {
583
591
  });
584
592
  });
585
593
 
594
+ // Properties
595
+
586
596
  describe('negateVector4', () => {
587
597
  it('inverts the values of the vector components', () => {
588
598
  const v = createVector4(1, -2, 3, -4);
@@ -604,8 +614,6 @@ describe('negateVector4', () => {
604
614
  });
605
615
  });
606
616
 
607
- // Properties
608
-
609
617
  describe('normalizeVector4', () => {
610
618
  it('normalizes the vector', () => {
611
619
  const v = createVector4(3, 4, 0, 0);
@@ -878,7 +886,6 @@ describe('Y_AXIS', () => {
878
886
  expect(yAxis.w).toBe(0);
879
887
  });
880
888
  });
881
-
882
889
  describe('Z_AXIS', () => {
883
890
  it('returns the unit vector along the Z-axis', () => {
884
891
  const zAxis = VECTOR4_Z_AXIS;