@flighthq/geometry 0.2.0 → 0.2.1-next.410.a23f189

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.
@@ -30,6 +30,11 @@ import {
30
30
  } from '@flighthq/geometry';
31
31
  import type { Matrix3 } from '@flighthq/types';
32
32
 
33
+ // Matrix3 storage is column-major: element (row r, column c) lives at m[3 * c + r], matching
34
+ // Matrix4 and the GL/GLSL/WGSL uniform ABI. A matrix-like {m} built from a raw Float32Array below
35
+ // therefore lists its values column-by-column. getMatrix3Element(r, c) / column / row accessors are
36
+ // layout-agnostic, so those assertions double as storage-independent behavior locks.
37
+
33
38
  describe('cloneMatrix3', () => {
34
39
  it('should clone the matrix correctly', () => {
35
40
  const m1 = createMatrix3(2, 3, 4, 5, 6, 7, 8, 9, 10);
@@ -46,7 +51,8 @@ describe('cloneMatrix3', () => {
46
51
  });
47
52
 
48
53
  it('should also clone matrix-like objects', () => {
49
- const obj = { m: new Float32Array([2, 3, 4, 5, 6, 7, 8, 9, 10]) };
54
+ // Column-major storage of rows [2,3,4],[5,6,7],[8,9,10].
55
+ const obj = { m: new Float32Array([2, 5, 8, 3, 6, 9, 4, 7, 10]) };
50
56
  const m2 = cloneMatrix3(obj);
51
57
  expect(getMatrix3Element(m2, 0, 0)).toBe(2);
52
58
  expect(getMatrix3Element(m2, 0, 1)).toBe(3);
@@ -60,7 +66,7 @@ describe('cloneMatrix3', () => {
60
66
  });
61
67
 
62
68
  it('should return a matrix instance', () => {
63
- const obj = { m: new Float32Array([2, 3, 4, 5, 6, 7, 8, 9, 10]) };
69
+ const obj = { m: new Float32Array([2, 5, 8, 3, 6, 9, 4, 7, 10]) };
64
70
  const m2: Matrix3 = cloneMatrix3(obj);
65
71
  expect(m2).not.toBeNull();
66
72
  });
@@ -153,7 +159,8 @@ describe('copyMatrix3ColumnToVector3', () => {
153
159
  });
154
160
 
155
161
  it('should allow matrix-like and vector-like objects', () => {
156
- const m = { m: new Float32Array([1, 2, 3, 4, 5, 6, 7, 8, 9]) };
162
+ // Column-major storage of rows [1,2,3],[4,5,6],[7,8,9]; column 0 = (1,4,7).
163
+ const m = { m: new Float32Array([1, 4, 7, 2, 5, 8, 3, 6, 9]) };
157
164
  const v = { x: 0, y: 0, z: 0 };
158
165
  copyMatrix3ColumnToVector3(v, 0, m); // column 0
159
166
  expect(v.x).toBe(1);
@@ -190,10 +197,10 @@ describe('copyMatrix3RowFromVector3', () => {
190
197
  it('should allow matrix-like and vector-like objects', () => {
191
198
  const m = { m: new Float32Array([1, 0, 0, 0, 1, 0, 0, 0, 1]) };
192
199
  const v = { x: 1, y: 2, z: 3 };
193
- copyMatrix3RowFromVector3(m, 0, v); // row 0
200
+ copyMatrix3RowFromVector3(m, 0, v); // row 0 -> column-major indices 0, 3, 6
194
201
  expect(m.m[0]).toBe(1);
195
- expect(m.m[1]).toBe(2);
196
- expect(m.m[2]).toBe(3);
202
+ expect(m.m[3]).toBe(2);
203
+ expect(m.m[6]).toBe(3);
197
204
  });
198
205
  });
199
206
 
@@ -226,7 +233,8 @@ describe('copyMatrix3RowToVector3', () => {
226
233
  });
227
234
 
228
235
  it('should allow matrix-like and vector-like objects', () => {
229
- const m = { m: new Float32Array([1, 2, 3, 4, 5, 6, 7, 8, 9]) };
236
+ // Column-major storage of rows [1,2,3],[4,5,6],[7,8,9]; row 0 = (1,2,3).
237
+ const m = { m: new Float32Array([1, 4, 7, 2, 5, 8, 3, 6, 9]) };
230
238
  const v = { x: 0, y: 0, z: 0 };
231
239
  copyMatrix3RowToVector3(v, 0, m); // row 0
232
240
  expect(v.x).toBe(1); // m.a
@@ -249,6 +257,11 @@ describe('createMatrix3', () => {
249
257
  expect(getMatrix3Element(m, 2, 2)).toBe(10);
250
258
  });
251
259
 
260
+ it('stores its (row, column) arguments column-major', () => {
261
+ const m = createMatrix3(1, 2, 3, 4, 5, 6, 7, 8, 9);
262
+ expect(Array.from(m.m)).toEqual([1, 4, 7, 2, 5, 8, 3, 6, 9]);
263
+ });
264
+
252
265
  it('should default to identity matrix when no values are provided', () => {
253
266
  const m = createMatrix3();
254
267
  expect(getMatrix3Element(m, 0, 0)).toBe(1);
@@ -295,7 +308,8 @@ describe('equalsMatrix3', () => {
295
308
 
296
309
  it('should return true if one object is matrix-like and one is not', () => {
297
310
  const mat1 = createMatrix3(1, 2, 3, 4, 5, 6, 7, 8, 9);
298
- const mat2 = { m: new Float32Array([1, 2, 3, 4, 5, 6, 7, 8, 9]) };
311
+ // Column-major storage of the same matrix.
312
+ const mat2 = { m: new Float32Array([1, 4, 7, 2, 5, 8, 3, 6, 9]) };
299
313
  expect(equalsMatrix3(mat1, mat2)).toBe(true);
300
314
  });
301
315
 
@@ -335,24 +349,19 @@ describe('getMatrix3Element', () => {
335
349
 
336
350
  describe('inverseMatrix3', () => {
337
351
  it('should invert the matrix correctly', () => {
338
- // Create a matrix with scaling of 2 and translation of (5, 3)
352
+ // A scale of 2 with translation of (5, 3): rows [2,0,5],[0,2,3],[0,0,1].
339
353
  const m = createMatrix3(2, 0, 5, 0, 2, 3, 0, 0, 1);
340
354
 
341
- // Apply inversion
342
- let out = createMatrix3();
355
+ const out = createMatrix3();
343
356
  inverseMatrix3(out, m);
344
357
 
345
- // Expected inverse matrix:
346
- // Scaling should be 0.5 (inverse of 2)
347
- // Translation should be -2.5 (inverse of 5 scaled by 0.5) and -1.5 (inverse of 3 scaled by 0.5)
348
-
349
- // Assert the inverse matrix values
350
- expect(out.m[0]).toBeCloseTo(0.5); // Inverse scaling on x
351
- expect(out.m[1]).toBeCloseTo(0); // No shear on x
352
- expect(out.m[3]).toBeCloseTo(0); // No shear on y
353
- expect(out.m[4]).toBeCloseTo(0.5); // Inverse scaling on y
354
- expect(out.m[2]).toBeCloseTo(-2.5); // Inverse translation on x
355
- expect(out.m[5]).toBeCloseTo(-1.5); // Inverse translation on y
358
+ // Inverse: scale 0.5, translation (-2.5, -1.5). Column-major storage.
359
+ expect(out.m[0]).toBeCloseTo(0.5); // (0,0) inverse scale x
360
+ expect(out.m[1]).toBeCloseTo(0); // (1,0) no shear
361
+ expect(out.m[3]).toBeCloseTo(0); // (0,1) no shear
362
+ expect(out.m[4]).toBeCloseTo(0.5); // (1,1) inverse scale y
363
+ expect(out.m[6]).toBeCloseTo(-2.5); // (0,2) inverse translation x
364
+ expect(out.m[7]).toBeCloseTo(-1.5); // (1,2) inverse translation y
356
365
  });
357
366
 
358
367
  it('should not depend on initial out matrix values', () => {
@@ -364,22 +373,23 @@ describe('inverseMatrix3', () => {
364
373
  const result = createMatrix3();
365
374
  multiplyMatrix3(result, source, out);
366
375
 
367
- expect(result.m[0]).toBeCloseTo(1);
368
- expect(result.m[1]).toBeCloseTo(0);
369
- expect(result.m[3]).toBeCloseTo(0);
370
- expect(result.m[4]).toBeCloseTo(1);
376
+ expect(getMatrix3Element(result, 0, 0)).toBeCloseTo(1);
377
+ expect(getMatrix3Element(result, 0, 1)).toBeCloseTo(0);
378
+ expect(getMatrix3Element(result, 1, 0)).toBeCloseTo(0);
379
+ expect(getMatrix3Element(result, 1, 1)).toBeCloseTo(1);
371
380
  });
372
381
 
373
382
  it('should should allow matrix-like objects', () => {
374
- const m = { m: new Float32Array([2, 0, 5, 0, 2, 3, 0, 0, 1]) };
375
- let out = { m: new Float32Array([0, 0, 0, 0, 0, 0, 0, 0, 0]) };
383
+ // Column-major storage of the affine scale-2 translate-(5,3).
384
+ const m = { m: new Float32Array([2, 0, 0, 0, 2, 0, 5, 3, 1]) };
385
+ const out = { m: new Float32Array([0, 0, 0, 0, 0, 0, 0, 0, 0]) };
376
386
  inverseMatrix3(out, m);
377
- expect(out.m[0]).toBeCloseTo(0.5); // Inverse scaling on x
378
- expect(out.m[1]).toBeCloseTo(0); // No shear on x
379
- expect(out.m[3]).toBeCloseTo(0); // No shear on y
380
- expect(out.m[4]).toBeCloseTo(0.5); // Inverse scaling on y
381
- expect(out.m[2]).toBeCloseTo(-2.5); // Inverse translation on x
382
- expect(out.m[5]).toBeCloseTo(-1.5); // Inverse translation on y
387
+ expect(out.m[0]).toBeCloseTo(0.5); // (0,0) inverse scale x
388
+ expect(out.m[1]).toBeCloseTo(0); // (1,0) no shear
389
+ expect(out.m[3]).toBeCloseTo(0); // (0,1) no shear
390
+ expect(out.m[4]).toBeCloseTo(0.5); // (1,1) inverse scale y
391
+ expect(out.m[6]).toBeCloseTo(-2.5); // (0,2) inverse translation x
392
+ expect(out.m[7]).toBeCloseTo(-1.5); // (1,2) inverse translation y
383
393
  });
384
394
 
385
395
  it('supports out === source for affine matrices', () => {
@@ -476,12 +486,22 @@ describe('multiplyMatrix3', () => {
476
486
  expect(equalsMatrix3(out, b)).toBe(true);
477
487
  });
478
488
 
489
+ it('composes an affine scale then translate (row/column semantics)', () => {
490
+ // a = scale(2,2), b = translate(5,5). a * b applies b then a to a column vector.
491
+ const a = createMatrix3(2, 0, 0, 0, 2, 0, 0, 0, 1);
492
+ const b = createMatrix3(1, 0, 5, 0, 1, 5, 0, 0, 1);
493
+ multiplyMatrix3(a, a, b);
494
+ expect(getMatrix3Element(a, 0, 2)).toBe(10);
495
+ expect(getMatrix3Element(a, 1, 2)).toBe(10);
496
+ });
497
+
479
498
  it('should allow matrix-like objects', () => {
499
+ // Column-major: a = diag(2,2,1), b = translate(5,5).
480
500
  const a = { m: new Float32Array([2, 0, 0, 0, 2, 0, 0, 0, 1]) };
481
- const b = { m: new Float32Array([1, 0, 5, 0, 1, 5, 0, 0, 1]) };
501
+ const b = { m: new Float32Array([1, 0, 0, 0, 1, 0, 5, 5, 1]) };
482
502
  multiplyMatrix3(a, a, b);
483
- expect(a.m[2]).toBe(10);
484
- expect(a.m[5]).toBe(10);
503
+ expect(a.m[6]).toBe(10); // (0,2)
504
+ expect(a.m[7]).toBe(10); // (1,2)
485
505
  });
486
506
  });
487
507
 
@@ -490,24 +510,24 @@ describe('rotateMatrix3', () => {
490
510
  const m = createMatrix3(1, 0, 0, 0, 1, 0, 0, 0, 1);
491
511
  const out = createMatrix3();
492
512
  rotateMatrix3(out, m, Math.PI / 2); // 90 degrees
493
- expect(out.m[0]).toBeCloseTo(0);
494
- expect(out.m[1]).toBeCloseTo(-1);
495
- expect(out.m[3]).toBeCloseTo(1);
496
- expect(out.m[4]).toBeCloseTo(0);
513
+ // Rotation (0,1)-plane: (0,0)=0, (0,1)=-1, (1,0)=1, (1,1)=0.
514
+ expect(getMatrix3Element(out, 0, 0)).toBeCloseTo(0);
515
+ expect(getMatrix3Element(out, 0, 1)).toBeCloseTo(-1);
516
+ expect(getMatrix3Element(out, 1, 0)).toBeCloseTo(1);
517
+ expect(getMatrix3Element(out, 1, 1)).toBeCloseTo(0);
497
518
  });
498
519
 
499
520
  it('should allow a matrix-like object', () => {
500
521
  const m = { m: new Float32Array([1, 0, 0, 0, 1, 0, 0, 0, 1]) };
501
522
  const out = { m: new Float32Array([1, 0, 0, 0, 1, 0, 0, 0, 1]) };
502
523
 
503
- // Apply 90 degrees rotation (π/2 radians)
504
524
  rotateMatrix3(out, m, Math.PI / 2);
505
525
 
506
- // Check that the resulting matrix corresponds to a 90-degree rotation matrix
507
- expect(out.m[0]).toBeCloseTo(0); // a = cos(π/2) = 0
508
- expect(out.m[1]).toBeCloseTo(-1); // b = -sin(π/2) = -1
509
- expect(out.m[3]).toBeCloseTo(1); // c = sin(π/2) = 1
510
- expect(out.m[4]).toBeCloseTo(0); // d = cos(π/2) = 0
526
+ // 90-degree rotation, column-major storage: (0,0)=0,(0,1)=-1,(1,0)=1,(1,1)=0.
527
+ expect(out.m[0]).toBeCloseTo(0); // (0,0)
528
+ expect(out.m[1]).toBeCloseTo(1); // (1,0)
529
+ expect(out.m[3]).toBeCloseTo(-1); // (0,1)
530
+ expect(out.m[4]).toBeCloseTo(0); // (1,1)
511
531
  });
512
532
 
513
533
  it('supports out === source', () => {
@@ -525,21 +545,21 @@ describe('scaleMatrix3', () => {
525
545
  const m = createMatrix3();
526
546
  const out = createMatrix3();
527
547
  scaleMatrix3(out, m, 2, 3);
528
- expect(out.m[0]).toBe(2);
529
- expect(out.m[4]).toBe(3);
548
+ expect(getMatrix3Element(out, 0, 0)).toBe(2);
549
+ expect(getMatrix3Element(out, 1, 1)).toBe(3);
530
550
  });
531
551
 
532
552
  it('should allow a matrix-like object', () => {
533
553
  const m = { m: new Float32Array([1, 0, 0, 0, 1, 0, 0, 0, 1]) };
534
554
  const out = createMatrix3();
535
555
  scaleMatrix3(out, m, 2, 3);
536
- expect(out.m[0]).toBe(2); // a
537
- expect(out.m[4]).toBe(3); // d
556
+ expect(out.m[0]).toBe(2); // (0,0)
557
+ expect(out.m[4]).toBe(3); // (1,1)
538
558
  });
539
559
  });
540
560
 
541
561
  describe('setMatrix3', () => {
542
- it('sets all 9 elements in row-major order', () => {
562
+ it('sets all 9 elements by (row, column)', () => {
543
563
  const m = createMatrix3();
544
564
  setMatrix3(m, 1, 2, 3, 4, 5, 6, 7, 8, 9);
545
565
  for (let row = 0; row < 3; row++) {
@@ -548,6 +568,12 @@ describe('setMatrix3', () => {
548
568
  }
549
569
  }
550
570
  });
571
+
572
+ it('stores its arguments column-major', () => {
573
+ const m = createMatrix3();
574
+ setMatrix3(m, 1, 2, 3, 4, 5, 6, 7, 8, 9);
575
+ expect(Array.from(m.m)).toEqual([1, 4, 7, 2, 5, 8, 3, 6, 9]);
576
+ });
551
577
  });
552
578
 
553
579
  describe('setMatrix3Element', () => {
@@ -583,36 +609,22 @@ describe('setMatrix3FromFloat32Array', () => {
583
609
 
584
610
  describe('setMatrix3FromMatrix', () => {
585
611
  it('should convert a Matrix3x2 to a Matrix3', () => {
586
- // Define a matrix (6 values, row-major)
587
612
  const mat2D = createMatrix();
588
-
589
- // Define an empty Matrix3 to store the result
590
613
  const mat = createMatrix3();
591
-
592
- // Call the fromMatrix3x2 function
593
614
  setMatrix3FromMatrix(mat, mat2D);
594
615
 
595
- // Expected result: Identity matrix for Matrix3
616
+ // Identity, column-major.
596
617
  const expectedMatrix3 = new Float32Array([1, 0, 0, 0, 1, 0, 0, 0, 1]);
597
-
598
- // Assert that the result matches the expected outcome
599
618
  expect(mat.m).toEqual(expectedMatrix3);
600
619
  });
601
620
 
602
621
  it('should handle scaling and translation', () => {
603
- // Define a matrix with scaling and translation
604
622
  const mat2D = createMatrix(2, 0, 0, 3, 5, 10); // Scaling by 2,3 and translation by (5,10)
605
-
606
- // Define an empty Matrix3 to store the result
607
623
  const mat = createMatrix3();
608
-
609
- // Call the fromMatrix3x2 function
610
624
  setMatrix3FromMatrix(mat, mat2D);
611
625
 
612
- // Expected result: Scaling and translation in Matrix3
613
- const expectedMatrix3 = new Float32Array([2, 0, 5, 0, 3, 10, 0, 0, 1]);
614
-
615
- // Assert that the result matches the expected outcome
626
+ // Column-major storage of the affine scale-(2,3) translate-(5,10).
627
+ const expectedMatrix3 = new Float32Array([2, 0, 0, 0, 3, 0, 5, 10, 1]);
616
628
  expect(mat.m).toEqual(expectedMatrix3);
617
629
  });
618
630
  });
@@ -643,9 +655,7 @@ describe('setMatrix3FromMatrix4', () => {
643
655
  const mat = createMatrix3();
644
656
  setMatrix3FromMatrix4(mat, Matrix4x4);
645
657
 
646
- // Expected result: Identity matrix for Matrix3
647
658
  const expectedMatrix3 = new Float32Array([1, 0, 0, 0, 1, 0, 0, 0, 1]);
648
-
649
659
  expect(mat.m).toEqual(expectedMatrix3);
650
660
  });
651
661
 
@@ -674,11 +684,20 @@ describe('setMatrix3FromMatrix4', () => {
674
684
  const mat = createMatrix3();
675
685
  setMatrix3FromMatrix4(mat, Matrix4x4);
676
686
 
677
- // Expected result: Upper-left 3x3 part of Matrix4x4
678
687
  const expectedMatrix3 = new Float32Array([2, 0, 0, 0, 3, 0, 0, 0, 4]);
679
-
680
688
  expect(mat.m).toEqual(expectedMatrix3);
681
689
  });
690
+
691
+ it('copies the upper-left 3x3 column-major (no transpose)', () => {
692
+ // Matrix4 columns [1,2,3,0],[4,5,6,0],[7,8,9,0],[0,0,0,1]; upper-left 3x3 is column-major
693
+ // [1,2,3, 4,5,6, 7,8,9] and copies straight through.
694
+ const m4 = { m: new Float32Array([1, 2, 3, 0, 4, 5, 6, 0, 7, 8, 9, 0, 0, 0, 0, 1]) };
695
+ const mat = createMatrix3();
696
+ setMatrix3FromMatrix4(mat, m4);
697
+ expect(Array.from(mat.m)).toEqual([1, 2, 3, 4, 5, 6, 7, 8, 9]);
698
+ expect(getMatrix3Element(mat, 0, 1)).toBe(4);
699
+ expect(getMatrix3Element(mat, 1, 0)).toBe(2);
700
+ });
682
701
  });
683
702
 
684
703
  describe('setMatrix3Identity', () => {
@@ -699,13 +718,24 @@ describe('setMatrix3NormalFromMatrix4', () => {
699
718
  const m4 = createMatrix4(0, 1, 0, 0, -1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
700
719
  const out = createMatrix3();
701
720
  setMatrix3NormalFromMatrix4(out, m4);
702
- // For a pure rotation, normal matrix == rotation. Row-major matrix3 of R (+x -> +y):
703
- // [[0,-1,0],[1,0,0],[0,0,1]].
721
+ // For a pure rotation, normal matrix == rotation R (+x -> +y): (0,0)=0,(0,1)=-1,(1,0)=1,(1,1)=0.
722
+ expect(getMatrix3Element(out, 0, 0)).toBeCloseTo(0, 5);
723
+ expect(getMatrix3Element(out, 0, 1)).toBeCloseTo(-1, 5);
724
+ expect(getMatrix3Element(out, 1, 0)).toBeCloseTo(1, 5);
725
+ expect(getMatrix3Element(out, 1, 1)).toBeCloseTo(0, 5);
726
+ expect(getMatrix3Element(out, 2, 2)).toBeCloseTo(1, 5);
727
+ });
728
+
729
+ it('uploads column-major so a GL upload needs no transpose', () => {
730
+ // The whole point of the migration: the normal matrix' storage matches uniformMatrix3fv(false).
731
+ // 90-degree rotation about z, column-major storage: R = [[0,-1,0],[1,0,0],[0,0,1]] -> [0,1,0,-1,0,0,0,0,1].
732
+ const m4 = createMatrix4(0, 1, 0, 0, -1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
733
+ const out = createMatrix3();
734
+ setMatrix3NormalFromMatrix4(out, m4);
704
735
  expect(out.m[0]).toBeCloseTo(0, 5);
705
- expect(out.m[1]).toBeCloseTo(-1, 5);
706
- expect(out.m[3]).toBeCloseTo(1, 5);
736
+ expect(out.m[1]).toBeCloseTo(1, 5);
737
+ expect(out.m[3]).toBeCloseTo(-1, 5);
707
738
  expect(out.m[4]).toBeCloseTo(0, 5);
708
- expect(out.m[8]).toBeCloseTo(1, 5);
709
739
  });
710
740
 
711
741
  it('keeps a normal perpendicular under non-uniform scale', () => {
@@ -714,9 +744,9 @@ describe('setMatrix3NormalFromMatrix4', () => {
714
744
  const normalMatrix = createMatrix3();
715
745
  setMatrix3NormalFromMatrix4(normalMatrix, m4);
716
746
  // Inverse-transpose of diag(2,1,1) = diag(0.5,1,1).
717
- expect(normalMatrix.m[0]).toBeCloseTo(0.5, 5);
718
- expect(normalMatrix.m[4]).toBeCloseTo(1, 5);
719
- expect(normalMatrix.m[8]).toBeCloseTo(1, 5);
747
+ expect(getMatrix3Element(normalMatrix, 0, 0)).toBeCloseTo(0.5, 5);
748
+ expect(getMatrix3Element(normalMatrix, 1, 1)).toBeCloseTo(1, 5);
749
+ expect(getMatrix3Element(normalMatrix, 2, 2)).toBeCloseTo(1, 5);
720
750
  });
721
751
  });
722
752
 
@@ -725,16 +755,16 @@ describe('translateMatrix3', () => {
725
755
  const m = createMatrix3();
726
756
  const out = createMatrix3();
727
757
  translateMatrix3(out, m, 2, 3);
728
- expect(out.m[2]).toBe(2);
729
- expect(out.m[5]).toBe(3);
758
+ expect(getMatrix3Element(out, 0, 2)).toBe(2);
759
+ expect(getMatrix3Element(out, 1, 2)).toBe(3);
730
760
  });
731
761
 
732
762
  it('should allow a matrix-like object', () => {
733
763
  const m = { m: new Float32Array([1, 0, 0, 0, 1, 0, 0, 0, 1]) };
734
764
  const out = createMatrix3();
735
765
  translateMatrix3(out, m, 2, 3);
736
- expect(out.m[2]).toBe(2); // tx
737
- expect(out.m[5]).toBe(3); // ty
766
+ expect(out.m[6]).toBe(2); // (0,2) tx
767
+ expect(out.m[7]).toBe(3); // (1,2) ty
738
768
  });
739
769
  });
740
770
 
@@ -743,13 +773,17 @@ describe('transposeMatrix3', () => {
743
773
  const m = createMatrix3(1, 2, 3, 4, 5, 6, 7, 8, 9);
744
774
  const out = createMatrix3();
745
775
  transposeMatrix3(out, m);
746
- expect(Array.from(out.m)).toEqual([1, 4, 7, 2, 5, 8, 3, 6, 9]);
776
+ // Transpose of rows [1,2,3],[4,5,6],[7,8,9] is rows [1,4,7],[2,5,8],[3,6,9];
777
+ // column-major storage of that is [1,2,3,4,5,6,7,8,9].
778
+ expect(Array.from(out.m)).toEqual([1, 2, 3, 4, 5, 6, 7, 8, 9]);
779
+ expect(getMatrix3Element(out, 0, 1)).toBe(4);
780
+ expect(getMatrix3Element(out, 1, 0)).toBe(2);
747
781
  });
748
782
 
749
783
  it('supports out === source', () => {
750
784
  const m = createMatrix3(1, 2, 3, 4, 5, 6, 7, 8, 9);
751
785
  transposeMatrix3(m, m);
752
- expect(Array.from(m.m)).toEqual([1, 4, 7, 2, 5, 8, 3, 6, 9]);
786
+ expect(Array.from(m.m)).toEqual([1, 2, 3, 4, 5, 6, 7, 8, 9]);
753
787
  });
754
788
 
755
789
  it('transpose of identity is identity', () => {
@@ -765,7 +799,8 @@ describe('writeMatrix3ToFloat32Array', () => {
765
799
  const m = createMatrix3(1, 2, 3, 4, 5, 6, 7, 8, 9);
766
800
  const data = new Float32Array(10);
767
801
  writeMatrix3ToFloat32Array(data, 1, m);
768
- expect(Array.from(data)).toEqual([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]);
802
+ // Column-major storage of the matrix, written verbatim.
803
+ expect(Array.from(data)).toEqual([0, 1, 4, 7, 2, 5, 8, 3, 6, 9]);
769
804
  });
770
805
 
771
806
  it('round-trips through setMatrix3FromFloat32Array', () => {
@@ -1603,6 +1603,19 @@ describe('transposeMatrix4', () => {
1603
1603
  transposeMatrix4(matrix, matrix);
1604
1604
  expect(equalsMatrix4(matrix, expected)).toBe(true);
1605
1605
  });
1606
+
1607
+ it('supports out === source with non-symmetric values', () => {
1608
+ // A deliberately non-symmetric matrix where aliasing would corrupt results
1609
+ // if __swap read from out after a prior __swap already wrote to it.
1610
+ const m = createMatrix4();
1611
+ setMatrix4(m, 2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53);
1612
+ transposeMatrix4(m, m);
1613
+ // Column-major transposed: rows become columns.
1614
+ const expected = [2, 11, 23, 41, 3, 13, 29, 43, 5, 17, 31, 47, 7, 19, 37, 53];
1615
+ for (let i = 0; i < 16; i++) {
1616
+ expect(m.m[i]).toBe(expected[i]);
1617
+ }
1618
+ });
1606
1619
  });
1607
1620
 
1608
1621
  describe('writeMatrix4ToFloat32Array', () => {
@@ -15,6 +15,7 @@ import {
15
15
  normalizeQuaternion,
16
16
  rotateVector3ByQuaternion,
17
17
  setMatrix4FromQuaternion,
18
+ setQuaternion,
18
19
  setQuaternionFromAxisAngle,
19
20
  setQuaternionFromEuler,
20
21
  setQuaternionFromMatrix4,
@@ -359,6 +360,14 @@ describe('setMatrix4FromQuaternion', () => {
359
360
  });
360
361
  });
361
362
 
363
+ describe('setQuaternion', () => {
364
+ it('sets the components directly', () => {
365
+ const out = createQuaternion();
366
+ setQuaternion(out, 0.1, 0.2, 0.3, 0.4);
367
+ expect(out).toMatchObject({ w: 0.4, x: 0.1, y: 0.2, z: 0.3 });
368
+ });
369
+ });
370
+
362
371
  describe('setQuaternionFromAxisAngle', () => {
363
372
  it('builds a half-angle quaternion', () => {
364
373
  const q = createQuaternion();
@@ -0,0 +1,84 @@
1
+ import { describe, expect, it } from 'vitest';
2
+
3
+ import { createMatrix } from './matrix';
4
+ import { createTransform2D, decomposeMatrixToTransform2D } from './transform2d';
5
+
6
+ const DEG_TO_RAD = Math.PI / 180;
7
+
8
+ // Mirrors the display object's forward transform build (recomputeLocalTransform2D) so round-trip
9
+ // tests exercise the exact convention decomposeMatrixToTransform2D inverts.
10
+ function buildMatrix(x: number, y: number, rotation: number, scaleX: number, scaleY: number, skewX = 0, skewY = 0) {
11
+ const radY = (rotation + skewY) * DEG_TO_RAD;
12
+ const radX = (rotation + skewX) * DEG_TO_RAD;
13
+ return createMatrix(
14
+ Math.cos(radY) * scaleX,
15
+ Math.sin(radY) * scaleX,
16
+ -Math.sin(radX) * scaleY,
17
+ Math.cos(radX) * scaleY,
18
+ x,
19
+ y,
20
+ );
21
+ }
22
+
23
+ describe('createTransform2D', () => {
24
+ it('defaults to the identity transform', () => {
25
+ const t = createTransform2D();
26
+ expect(t).toMatchObject({
27
+ pivotX: 0,
28
+ pivotY: 0,
29
+ rotation: 0,
30
+ scaleX: 1,
31
+ scaleY: 1,
32
+ skewX: 0,
33
+ skewY: 0,
34
+ x: 0,
35
+ y: 0,
36
+ });
37
+ });
38
+
39
+ it('accepts positional fields', () => {
40
+ const t = createTransform2D(10, 20, 45, 2, 3);
41
+ expect(t).toMatchObject({ rotation: 45, scaleX: 2, scaleY: 3, x: 10, y: 20 });
42
+ });
43
+ });
44
+
45
+ describe('decomposeMatrixToTransform2D', () => {
46
+ it('decomposes the identity matrix', () => {
47
+ const out = createTransform2D(99, 99, 99, 99, 99, 99, 99);
48
+ decomposeMatrixToTransform2D(out, createMatrix());
49
+ expect(out).toMatchObject({ rotation: 0, scaleX: 1, scaleY: 1, skewX: 0, skewY: 0, x: 0, y: 0 });
50
+ });
51
+
52
+ it('recovers translation and scale', () => {
53
+ const out = createTransform2D();
54
+ decomposeMatrixToTransform2D(out, buildMatrix(5, 7, 0, 2, 3));
55
+ expect(out.x).toBeCloseTo(5);
56
+ expect(out.y).toBeCloseTo(7);
57
+ expect(out.scaleX).toBeCloseTo(2);
58
+ expect(out.scaleY).toBeCloseTo(3);
59
+ });
60
+
61
+ it('recovers a pure rotation with no skew', () => {
62
+ const out = createTransform2D();
63
+ decomposeMatrixToTransform2D(out, buildMatrix(0, 0, 30, 1, 1));
64
+ expect(out.rotation).toBeCloseTo(30);
65
+ expect(out.skewX).toBe(0);
66
+ expect(out.skewY).toBe(0);
67
+ });
68
+
69
+ it('round-trips a skewed transform, folding the angles into skew', () => {
70
+ const out = createTransform2D();
71
+ decomposeMatrixToTransform2D(out, buildMatrix(0, 0, 0, 1, 1, 10, 20));
72
+ expect(out.rotation).toBe(0);
73
+ expect(out.skewX).toBeCloseTo(10);
74
+ expect(out.skewY).toBeCloseTo(20);
75
+ });
76
+
77
+ it('carries a reflection on scaleY', () => {
78
+ const out = createTransform2D();
79
+ // A mirror across X: determinant negative.
80
+ decomposeMatrixToTransform2D(out, createMatrix(1, 0, 0, -1, 0, 0));
81
+ expect(out.scaleX).toBeCloseTo(1);
82
+ expect(out.scaleY).toBeCloseTo(-1);
83
+ });
84
+ });
@@ -0,0 +1,50 @@
1
+ import { describe, expect, it } from 'vitest';
2
+
3
+ import { createMatrix4 } from './matrix4';
4
+ import { createQuaternion } from './quaternion';
5
+ import { composeMatrix4FromTransform3D, createTransform3D, decomposeMatrix4ToTransform3D } from './transform3d';
6
+ import { createVector3 } from './vector3';
7
+
8
+ describe('composeMatrix4FromTransform3D', () => {
9
+ it('composes translation, rotation, and scale into a matrix', () => {
10
+ const t = createTransform3D();
11
+ t.position = createVector3(4, 5, 6);
12
+ t.scale = createVector3(2, 2, 2);
13
+ const out = createMatrix4();
14
+ composeMatrix4FromTransform3D(out, t);
15
+ // Identity rotation + uniform scale 2 + translation (4,5,6): diagonal scale, last column translation.
16
+ expect(out.m[0]).toBeCloseTo(2);
17
+ expect(out.m[5]).toBeCloseTo(2);
18
+ expect(out.m[10]).toBeCloseTo(2);
19
+ expect(out.m[12]).toBeCloseTo(4);
20
+ expect(out.m[13]).toBeCloseTo(5);
21
+ expect(out.m[14]).toBeCloseTo(6);
22
+ });
23
+ });
24
+
25
+ describe('createTransform3D', () => {
26
+ it('defaults to identity: zero translation, identity rotation, unit scale', () => {
27
+ const t = createTransform3D();
28
+ expect(t.position).toMatchObject({ x: 0, y: 0, z: 0 });
29
+ expect(t.rotation).toMatchObject({ x: 0, y: 0, z: 0, w: 1 });
30
+ expect(t.scale).toMatchObject({ x: 1, y: 1, z: 1 });
31
+ });
32
+ });
33
+
34
+ describe('decomposeMatrix4ToTransform3D', () => {
35
+ it('round-trips a shear-free compose', () => {
36
+ const source = createTransform3D();
37
+ source.position = createVector3(1, 2, 3);
38
+ source.rotation = createQuaternion(0, 0, 0, 1);
39
+ source.scale = createVector3(2, 3, 4);
40
+ const m = createMatrix4();
41
+ composeMatrix4FromTransform3D(m, source);
42
+
43
+ const out = createTransform3D();
44
+ decomposeMatrix4ToTransform3D(out, m);
45
+ expect(out.position).toMatchObject({ x: 1, y: 2, z: 3 });
46
+ expect(out.scale.x).toBeCloseTo(2);
47
+ expect(out.scale.y).toBeCloseTo(3);
48
+ expect(out.scale.z).toBeCloseTo(4);
49
+ });
50
+ });