@forgeax/engine-math 0.0.0-dev.8d955ade1c79

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.
Files changed (150) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +294 -0
  3. package/dist/.tsbuildinfo +1 -0
  4. package/dist/__tests__/_arbs.d.ts +36 -0
  5. package/dist/__tests__/_arbs.d.ts.map +1 -0
  6. package/dist/__tests__/_fixtures.d.ts +61 -0
  7. package/dist/__tests__/_fixtures.d.ts.map +1 -0
  8. package/dist/__tests__/bounds2.test.d.ts +2 -0
  9. package/dist/__tests__/bounds2.test.d.ts.map +1 -0
  10. package/dist/__tests__/box3.test.d.ts +2 -0
  11. package/dist/__tests__/box3.test.d.ts.map +1 -0
  12. package/dist/__tests__/easing.test.d.ts +2 -0
  13. package/dist/__tests__/easing.test.d.ts.map +1 -0
  14. package/dist/__tests__/euler.test-d.d.ts +2 -0
  15. package/dist/__tests__/euler.test-d.d.ts.map +1 -0
  16. package/dist/__tests__/mat3.test-d.d.ts +2 -0
  17. package/dist/__tests__/mat3.test-d.d.ts.map +1 -0
  18. package/dist/__tests__/mat4.property.test.d.ts +2 -0
  19. package/dist/__tests__/mat4.property.test.d.ts.map +1 -0
  20. package/dist/__tests__/mat4.test-d.d.ts +2 -0
  21. package/dist/__tests__/mat4.test-d.d.ts.map +1 -0
  22. package/dist/__tests__/mat4.test.d.ts +2 -0
  23. package/dist/__tests__/mat4.test.d.ts.map +1 -0
  24. package/dist/__tests__/noise.test.d.ts +2 -0
  25. package/dist/__tests__/noise.test.d.ts.map +1 -0
  26. package/dist/__tests__/quat.basis.test.d.ts +2 -0
  27. package/dist/__tests__/quat.basis.test.d.ts.map +1 -0
  28. package/dist/__tests__/quat.interpolation.test.d.ts +2 -0
  29. package/dist/__tests__/quat.interpolation.test.d.ts.map +1 -0
  30. package/dist/__tests__/quat.lookat.test.d.ts +2 -0
  31. package/dist/__tests__/quat.lookat.test.d.ts.map +1 -0
  32. package/dist/__tests__/quat.property.test.d.ts +2 -0
  33. package/dist/__tests__/quat.property.test.d.ts.map +1 -0
  34. package/dist/__tests__/quat.rotateaxis.test.d.ts +2 -0
  35. package/dist/__tests__/quat.rotateaxis.test.d.ts.map +1 -0
  36. package/dist/__tests__/quat.test-d.d.ts +2 -0
  37. package/dist/__tests__/quat.test-d.d.ts.map +1 -0
  38. package/dist/__tests__/ray.property.test.d.ts +2 -0
  39. package/dist/__tests__/ray.property.test.d.ts.map +1 -0
  40. package/dist/__tests__/ray.test.d.ts +2 -0
  41. package/dist/__tests__/ray.test.d.ts.map +1 -0
  42. package/dist/__tests__/types.test-d.d.ts +2 -0
  43. package/dist/__tests__/types.test-d.d.ts.map +1 -0
  44. package/dist/__tests__/vec-catmull-rom.test.d.ts +2 -0
  45. package/dist/__tests__/vec-catmull-rom.test.d.ts.map +1 -0
  46. package/dist/__tests__/vec-smooth-damp.test.d.ts +2 -0
  47. package/dist/__tests__/vec-smooth-damp.test.d.ts.map +1 -0
  48. package/dist/__tests__/vec2.test-d.d.ts +2 -0
  49. package/dist/__tests__/vec2.test-d.d.ts.map +1 -0
  50. package/dist/__tests__/vec3.property.test.d.ts +2 -0
  51. package/dist/__tests__/vec3.property.test.d.ts.map +1 -0
  52. package/dist/__tests__/vec3.test-d.d.ts +2 -0
  53. package/dist/__tests__/vec3.test-d.d.ts.map +1 -0
  54. package/dist/__tests__/vec4.test-d.d.ts +2 -0
  55. package/dist/__tests__/vec4.test-d.d.ts.map +1 -0
  56. package/dist/_internal/epsilon.d.ts +9 -0
  57. package/dist/_internal/epsilon.d.ts.map +1 -0
  58. package/dist/_internal/scalar.d.ts +40 -0
  59. package/dist/_internal/scalar.d.ts.map +1 -0
  60. package/dist/box2.d.ts +35 -0
  61. package/dist/box2.d.ts.map +1 -0
  62. package/dist/box3.d.ts +76 -0
  63. package/dist/box3.d.ts.map +1 -0
  64. package/dist/circle2.d.ts +29 -0
  65. package/dist/circle2.d.ts.map +1 -0
  66. package/dist/color.d.ts +64 -0
  67. package/dist/color.d.ts.map +1 -0
  68. package/dist/easing.d.ts +18 -0
  69. package/dist/easing.d.ts.map +1 -0
  70. package/dist/euler.d.ts +51 -0
  71. package/dist/euler.d.ts.map +1 -0
  72. package/dist/f32-to-f16-bytes.d.ts +10 -0
  73. package/dist/f32-to-f16-bytes.d.ts.map +1 -0
  74. package/dist/frustum.d.ts +49 -0
  75. package/dist/frustum.d.ts.map +1 -0
  76. package/dist/index.d.ts +20 -0
  77. package/dist/index.d.ts.map +1 -0
  78. package/dist/index.mjs +3688 -0
  79. package/dist/index.mjs.map +1 -0
  80. package/dist/mat3.d.ts +70 -0
  81. package/dist/mat3.d.ts.map +1 -0
  82. package/dist/mat4.d.ts +329 -0
  83. package/dist/mat4.d.ts.map +1 -0
  84. package/dist/noise.d.ts +12 -0
  85. package/dist/noise.d.ts.map +1 -0
  86. package/dist/quat.d.ts +312 -0
  87. package/dist/quat.d.ts.map +1 -0
  88. package/dist/ray.d.ts +144 -0
  89. package/dist/ray.d.ts.map +1 -0
  90. package/dist/ray2.d.ts +38 -0
  91. package/dist/ray2.d.ts.map +1 -0
  92. package/dist/sphere.d.ts +41 -0
  93. package/dist/sphere.d.ts.map +1 -0
  94. package/dist/types.d.ts +57 -0
  95. package/dist/types.d.ts.map +1 -0
  96. package/dist/vec2.d.ts +76 -0
  97. package/dist/vec2.d.ts.map +1 -0
  98. package/dist/vec3.d.ts +82 -0
  99. package/dist/vec3.d.ts.map +1 -0
  100. package/dist/vec4.d.ts +61 -0
  101. package/dist/vec4.d.ts.map +1 -0
  102. package/package.json +57 -0
  103. package/src/__tests__/_arbs.ts +149 -0
  104. package/src/__tests__/_fixtures.ts +118 -0
  105. package/src/__tests__/bounds2.test.ts +146 -0
  106. package/src/__tests__/box3.test.ts +277 -0
  107. package/src/__tests__/easing.test.ts +109 -0
  108. package/src/__tests__/euler.test-d.ts +63 -0
  109. package/src/__tests__/mat3.test-d.ts +47 -0
  110. package/src/__tests__/mat4.property.test.ts +256 -0
  111. package/src/__tests__/mat4.test-d.ts +130 -0
  112. package/src/__tests__/mat4.test.ts +162 -0
  113. package/src/__tests__/noise.test.ts +75 -0
  114. package/src/__tests__/quat.basis.test.ts +134 -0
  115. package/src/__tests__/quat.interpolation.test.ts +45 -0
  116. package/src/__tests__/quat.lookat.test.ts +103 -0
  117. package/src/__tests__/quat.property.test.ts +150 -0
  118. package/src/__tests__/quat.rotateaxis.test.ts +149 -0
  119. package/src/__tests__/quat.test-d.ts +138 -0
  120. package/src/__tests__/ray.property.test.ts +105 -0
  121. package/src/__tests__/ray.test.ts +539 -0
  122. package/src/__tests__/types.test-d.ts +64 -0
  123. package/src/__tests__/vec-catmull-rom.test.ts +125 -0
  124. package/src/__tests__/vec-smooth-damp.test.ts +167 -0
  125. package/src/__tests__/vec2.test-d.ts +59 -0
  126. package/src/__tests__/vec3.property.test.ts +72 -0
  127. package/src/__tests__/vec3.test-d.ts +65 -0
  128. package/src/__tests__/vec4.test-d.ts +61 -0
  129. package/src/_internal/epsilon.ts +29 -0
  130. package/src/_internal/scalar.ts +90 -0
  131. package/src/box2.ts +168 -0
  132. package/src/box3.ts +372 -0
  133. package/src/circle2.ts +134 -0
  134. package/src/color.ts +177 -0
  135. package/src/easing.ts +49 -0
  136. package/src/euler.ts +239 -0
  137. package/src/f32-to-f16-bytes.ts +71 -0
  138. package/src/frustum.ts +240 -0
  139. package/src/index.ts +65 -0
  140. package/src/mat3.ts +286 -0
  141. package/src/mat4.ts +1334 -0
  142. package/src/noise.ts +78 -0
  143. package/src/quat.ts +847 -0
  144. package/src/ray.ts +575 -0
  145. package/src/ray2.ts +198 -0
  146. package/src/sphere.ts +138 -0
  147. package/src/types.ts +78 -0
  148. package/src/vec2.ts +229 -0
  149. package/src/vec3.ts +294 -0
  150. package/src/vec4.ts +235 -0
@@ -0,0 +1,146 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import * as box2 from '../box2';
3
+ import * as circle2 from '../circle2';
4
+ import * as ray2 from '../ray2';
5
+ import * as vec2 from '../vec2';
6
+
7
+ function expectBox(
8
+ box: box2.Box2Like,
9
+ minX: number,
10
+ minY: number,
11
+ maxX: number,
12
+ maxY: number,
13
+ ): void {
14
+ expect(box[0]).toBe(minX);
15
+ expect(box[1]).toBe(minY);
16
+ expect(box[2]).toBe(maxX);
17
+ expect(box[3]).toBe(maxY);
18
+ }
19
+
20
+ describe('box2', () => {
21
+ it('builds center/half-size and point-cloud bounds', () => {
22
+ const out = box2.create();
23
+ expect(box2.fromCenter(out, [2, -1], [3, 4])).toBe(out);
24
+ expectBox(out, -1, -5, 5, 3);
25
+
26
+ box2.fromPoints(out, [
27
+ [-2, 4],
28
+ [5, 0],
29
+ [1, -3],
30
+ ]);
31
+ expectBox(out, -2, -3, 5, 4);
32
+ });
33
+
34
+ it('supports closest point, containment, merge, grow, and shrink', () => {
35
+ const box = box2.create(-2, -1, 2, 3);
36
+ const point = vec2.create();
37
+ box2.closestPoint(point, box, [5, -4]);
38
+ expect(point).toEqual(new Float32Array([2, -1]));
39
+ expect(box2.containsPoint(box, [2, 3])).toBe(true);
40
+ expect(box2.containsBox(box, box2.create(-1, 0, 1, 2))).toBe(true);
41
+
42
+ const merged = box2.create();
43
+ box2.merge(merged, box, box2.create(0, -4, 5, 1));
44
+ expectBox(merged, -2, -4, 5, 3);
45
+ box2.grow(merged, box, [1, 2]);
46
+ expectBox(merged, -3, -3, 3, 5);
47
+ box2.shrink(merged, box, [1, 1]);
48
+ expectBox(merged, -1, 0, 1, 2);
49
+ });
50
+
51
+ it('intersects boxes and circles at the boundary', () => {
52
+ const box = box2.create(-1, -1, 1, 1);
53
+ expect(box2.intersectsBox(box, box2.create(1, 0, 2, 1))).toBe(true);
54
+ expect(box2.intersectsBox(box, box2.create(1.01, 0, 2, 1))).toBe(false);
55
+ expect(box2.intersectsCircle(box, circle2.create(2, 0, 1))).toBe(true);
56
+ expect(box2.intersectsCircle(box, circle2.create(2.01, 0, 1))).toBe(false);
57
+ expect(box2.intersectsCircle(box, Float32Array.of(0, 0, -1))).toBe(false);
58
+ });
59
+ });
60
+
61
+ describe('circle2', () => {
62
+ it('builds an enclosing circle from points and converts to an AABB', () => {
63
+ const circle = circle2.create();
64
+ circle2.fromPoints(circle, [
65
+ [-2, 0],
66
+ [2, 0],
67
+ [0, 1],
68
+ ]);
69
+ expect(circle[0]).toBe(0);
70
+ expect(circle[1]).toBe(0.5);
71
+ expect(circle[2]).toBeCloseTo(2.06155, 4);
72
+
73
+ const box = box2.create();
74
+ circle2.toBox(box, circle);
75
+ const radius = circle[2] as number;
76
+ expectBox(box, -radius, 0.5 - radius, radius, 0.5 + radius);
77
+ });
78
+
79
+ it('handles closest point, intersections, and merge', () => {
80
+ const circle = circle2.create(0, 0, 2);
81
+ const point = vec2.create();
82
+ circle2.closestPoint(point, circle, [4, 0]);
83
+ expect(point).toEqual(new Float32Array([2, 0]));
84
+ expect(circle2.containsPoint(circle, [2, 0])).toBe(true);
85
+ expect(circle2.intersectsCircle(circle, circle2.create(3, 0, 1))).toBe(true);
86
+ expect(circle2.intersectsCircle(circle, circle2.create(3.01, 0, 1))).toBe(false);
87
+ expect(circle2.intersectsBox(circle, box2.create(-1, -1, 1, 1))).toBe(true);
88
+ circle2.closestPoint(point, Float32Array.of(0, 0, -1), [4, 0]);
89
+ expect(point).toEqual(new Float32Array([0, 0]));
90
+
91
+ const merged = circle2.create();
92
+ circle2.merge(merged, circle, circle2.create(5, 0, 1));
93
+ expect(merged[0]).toBe(2);
94
+ expect(merged[1]).toBe(0);
95
+ expect(merged[2]).toBe(4);
96
+ });
97
+ });
98
+
99
+ describe('ray2', () => {
100
+ it('intersects AABBs from outside, from inside, and with parallel axes', () => {
101
+ const ray = ray2.create(undefined, [-5, 0], [2, 0], 20);
102
+ const box = box2.create(-1, -1, 1, 1);
103
+ expect(ray2.rayAabbIntersects(ray, box)).toEqual({ hit: true, t: 4 });
104
+ expect(ray2.rayAabbIntersects(ray2.create(undefined, [0, 0], [1, 0], 0), box)).toEqual({
105
+ hit: true,
106
+ t: 0,
107
+ });
108
+ expect(ray2.rayAabbIntersects(ray2.create(undefined, [-5, 2], [1, 0], 20), box).hit).toBe(
109
+ false,
110
+ );
111
+ expect(ray2.rayAabbIntersects(ray2.create(undefined, [0, 0], [0, 1], 20), box)).toEqual({
112
+ hit: true,
113
+ t: 0,
114
+ });
115
+ });
116
+
117
+ it('intersects circles and respects the finite maximum distance', () => {
118
+ const hit = ray2.rayCircleIntersects(
119
+ ray2.create(undefined, [-5, 0], [2, 0], 20),
120
+ circle2.create(0, 0, 1),
121
+ );
122
+ expect(hit.hit).toBe(true);
123
+ expect(hit.t).toBeCloseTo(4, 6);
124
+ expect(
125
+ ray2.rayCircleIntersects(ray2.create(undefined, [-5, 0], [1, 0], 3), circle2.create(0, 0, 1))
126
+ .hit,
127
+ ).toBe(false);
128
+ expect(
129
+ ray2.rayCircleIntersects(ray2.create(undefined, [0, 0], [1, 0], 0), circle2.create(0, 0, 1)),
130
+ ).toEqual({ hit: true, t: 0 });
131
+ });
132
+
133
+ it('sweeps AABBs and circles through matching target volumes', () => {
134
+ const ray = ray2.create(undefined, [0, 0], [0, 2], 20);
135
+ const movingBox = box2.create(-1, -1, 1, 1);
136
+ const targetBox = box2.create(-1, 4, 1, 6);
137
+ expect(ray2.aabbCastIntersects(ray, movingBox, targetBox)).toEqual({ hit: true, t: 3 });
138
+
139
+ const movingCircle = circle2.create(0, 0, 1);
140
+ const targetCircle = circle2.create(0, 5, 1);
141
+ const circleHit = ray2.circleCastIntersects(ray, movingCircle, targetCircle);
142
+ expect(circleHit.hit).toBe(true);
143
+ expect(circleHit.t).toBeCloseTo(3, 6);
144
+ expect(ray2.circleCastIntersects(ray, movingCircle, circle2.create(3.1, 5, 1)).hit).toBe(false);
145
+ });
146
+ });
@@ -0,0 +1,277 @@
1
+ // box3.test.ts — box3.fromPositions TDD RED spec (m1-1)
2
+ //
3
+ // RED phase: box3.fromPositions is not yet implemented (M2).
4
+ // All tests assert the numerical semantics that fromPositions must fulfill.
5
+ //
6
+ // Coverage: multi-point tight enclosure, single point (min==max), empty input
7
+ // (inverted-infinity default), degenerate plane (zero-thickness axis),
8
+ // out-param return-reference, migration equivalence with geometry
9
+ // aabbFromPositions (fixed expected values), non-3-multiple trailing
10
+ // behaviour.
11
+ //
12
+ // Related: requirements AC-06/AC-07; plan-strategy D-1/D-2/D-5;
13
+ // plan-tasks.json m1-1 acceptanceCheck.
14
+
15
+ import { describe, expect, it } from 'vitest';
16
+ import * as box3 from '../box3';
17
+
18
+ // ============================================================
19
+ // Helpers
20
+ // ============================================================
21
+
22
+ /** Assert a Box3 has [minX, minY, minZ, maxX, maxY, maxZ] exactly. */
23
+ function expectBox3(
24
+ actual: Float32Array,
25
+ minX: number,
26
+ minY: number,
27
+ minZ: number,
28
+ maxX: number,
29
+ maxY: number,
30
+ maxZ: number,
31
+ ): void {
32
+ expect(actual[0]).toBe(minX);
33
+ expect(actual[1]).toBe(minY);
34
+ expect(actual[2]).toBe(minZ);
35
+ expect(actual[3]).toBe(maxX);
36
+ expect(actual[4]).toBe(maxY);
37
+ expect(actual[5]).toBe(maxZ);
38
+ }
39
+
40
+ /** Assert min == max on every axis (single-point or degenerate). */
41
+ function expectBox3Point(actual: Float32Array, x: number, y: number, z: number): void {
42
+ expectBox3(actual, x, y, z, x, y, z);
43
+ }
44
+
45
+ // ============================================================
46
+ // Multi-point tight enclosure
47
+ // ============================================================
48
+
49
+ describe('box3.fromPositions — multi-point', () => {
50
+ it('3 points: min/max across all xyz components', () => {
51
+ const positions = [1, 10, 100, -5, 0, 50, 3, 20, 0];
52
+ // Points: (1,10,100), (-5,0,50), (3,20,0)
53
+ const out = box3.create();
54
+ const result = box3.fromPositions(out, positions);
55
+ expect(result).toBe(out); // out-param returns same reference
56
+ expectBox3(out, -5, 0, 0, 3, 20, 100);
57
+ });
58
+
59
+ it('5 points with negative coords: correct min/max envelope', () => {
60
+ const positions = [-1, -2, -3, 4, 5, 6, -7, 8, -9, 0, 0, 0, 10, -10, 1];
61
+ // Points: (-1,-2,-3), (4,5,6), (-7,8,-9), (0,0,0), (10,-10,1)
62
+ const out = box3.create();
63
+ box3.fromPositions(out, positions);
64
+ expectBox3(out, -7, -10, -9, 10, 8, 6);
65
+ });
66
+ });
67
+
68
+ // ============================================================
69
+ // Single point (min == max on every axis)
70
+ // ============================================================
71
+
72
+ describe('box3.fromPositions — single point', () => {
73
+ it('one point: min == max on all axes', () => {
74
+ const out = box3.create();
75
+ box3.fromPositions(out, [7, 8, 9]);
76
+ expectBox3Point(out, 7, 8, 9);
77
+ });
78
+
79
+ it('origin point', () => {
80
+ const out = box3.create();
81
+ box3.fromPositions(out, [0, 0, 0]);
82
+ expectBox3Point(out, 0, 0, 0);
83
+ });
84
+ });
85
+
86
+ // ============================================================
87
+ // Empty / insufficient input → inverted-infinity empty box
88
+ // ============================================================
89
+
90
+ describe('box3.fromPositions — empty input', () => {
91
+ it('0 vertices → inverted-infinity empty box (box3.create() default)', () => {
92
+ const out = box3.create();
93
+ box3.fromPositions(out, []);
94
+ expectBox3(
95
+ out,
96
+ Number.POSITIVE_INFINITY,
97
+ Number.POSITIVE_INFINITY,
98
+ Number.POSITIVE_INFINITY,
99
+ Number.NEGATIVE_INFINITY,
100
+ Number.NEGATIVE_INFINITY,
101
+ Number.NEGATIVE_INFINITY,
102
+ );
103
+ });
104
+
105
+ it('1 element (incomplete point) → inverted-infinity empty box', () => {
106
+ const out = box3.create();
107
+ box3.fromPositions(out, [42]);
108
+ expectBox3(
109
+ out,
110
+ Number.POSITIVE_INFINITY,
111
+ Number.POSITIVE_INFINITY,
112
+ Number.POSITIVE_INFINITY,
113
+ Number.NEGATIVE_INFINITY,
114
+ Number.NEGATIVE_INFINITY,
115
+ Number.NEGATIVE_INFINITY,
116
+ );
117
+ });
118
+
119
+ it('2 elements (incomplete point) → inverted-infinity empty box', () => {
120
+ const out = box3.create();
121
+ box3.fromPositions(out, [1, 2]);
122
+ expectBox3(
123
+ out,
124
+ Number.POSITIVE_INFINITY,
125
+ Number.POSITIVE_INFINITY,
126
+ Number.POSITIVE_INFINITY,
127
+ Number.NEGATIVE_INFINITY,
128
+ Number.NEGATIVE_INFINITY,
129
+ Number.NEGATIVE_INFINITY,
130
+ );
131
+ });
132
+
133
+ it('inverted-empty box is not pickable per pick.ts convention (min > max)', () => {
134
+ const out = box3.create();
135
+ box3.fromPositions(out, []);
136
+ // pick.ts checks: localAabb[0] > localAabb[3] → skip
137
+ // inverted-infinity: +Inf > -Inf → true → correctly skipped
138
+ expect(out[0] as number).toBeGreaterThan(out[3] as number);
139
+ });
140
+ });
141
+
142
+ // ============================================================
143
+ // Degenerate plane (one axis zero thickness)
144
+ // ============================================================
145
+
146
+ describe('box3.fromPositions — degenerate plane', () => {
147
+ it('all z=0 → minZ == maxZ == 0 (zero-thickness on Z axis)', () => {
148
+ const positions = [1, 2, 0, 3, 4, 0, 5, 1, 0, 0, 6, 0];
149
+ // Points: (1,2,0), (3,4,0), (5,1,0), (0,6,0)
150
+ const out = box3.create();
151
+ box3.fromPositions(out, positions);
152
+ expectBox3(out, 0, 1, 0, 5, 6, 0);
153
+ expect(out[2]).toBe(out[5]); // minZ == maxZ == 0
154
+ });
155
+
156
+ it('all x=0 → minX == maxX == 0 (zero-thickness on X axis)', () => {
157
+ const positions = [0, 1, 2, 0, 3, 4, 0, 5, 6];
158
+ const out = box3.create();
159
+ box3.fromPositions(out, positions);
160
+ expectBox3(out, 0, 1, 2, 0, 5, 6);
161
+ expect(out[0]).toBe(out[3]); // minX == maxX == 0
162
+ });
163
+
164
+ it('two points same z → degenerate XY plane strip', () => {
165
+ const positions = [-2, 0, 5, 3, 0, 5];
166
+ const out = box3.create();
167
+ box3.fromPositions(out, positions);
168
+ expectBox3(out, -2, 0, 5, 3, 0, 5);
169
+ expect(out[1]).toBe(out[4]); // minY == maxY == 0
170
+ expect(out[2]).toBe(out[5]); // minZ == maxZ == 5
171
+ });
172
+ });
173
+
174
+ // ============================================================
175
+ // Migration equivalence: fixed expected values matching
176
+ // geometry aabbFromPositions output for the same inputs
177
+ // (no dynamic dependency on geometry package — values are
178
+ // hand-computed from the algorithm in box.ts:162-191)
179
+ // ============================================================
180
+
181
+ describe('box3.fromPositions — aabbFromPositions equivalence', () => {
182
+ it('matches aabbFromPositions: 3-point general case', () => {
183
+ // geometry aabbFromPositions would output [1, 1, 0, 4, 8, 6]
184
+ // for positions [1,2,3, 4,1,6, 2,8,0]
185
+ const positions = [1, 2, 3, 4, 1, 6, 2, 8, 0];
186
+ const out = box3.create();
187
+ box3.fromPositions(out, positions);
188
+ expectBox3(out, 1, 1, 0, 4, 8, 6);
189
+ });
190
+
191
+ it('matches aabbFromPositions: 6-point all-positive', () => {
192
+ // Positions: (10,20,30), (5,100,5), (1,1,1), (50,25,0), (30,30,30), (0,0,99)
193
+ // min: (0,0,0), max: (50,100,99)
194
+ const positions = [10, 20, 30, 5, 100, 5, 1, 1, 1, 50, 25, 0, 30, 30, 30, 0, 0, 99];
195
+ const out = box3.create();
196
+ box3.fromPositions(out, positions);
197
+ expectBox3(out, 0, 0, 0, 50, 100, 99);
198
+ });
199
+
200
+ it('matches aabbFromPositions: negative-and-positive mix', () => {
201
+ // Positions: (-5,-10,-15), (3,7,-2)
202
+ // min: (-5,-10,-15), max: (3,7,-2)
203
+ const positions = [-5, -10, -15, 3, 7, -2];
204
+ const out = box3.create();
205
+ box3.fromPositions(out, positions);
206
+ expectBox3(out, -5, -10, -15, 3, 7, -2);
207
+ });
208
+
209
+ it('matches aabbFromPositions: single point case', () => {
210
+ // aabbFromPositions seeds from positions[0..2] and never enters the loop
211
+ const positions = [42, 17, 99];
212
+ const out = box3.create();
213
+ box3.fromPositions(out, positions);
214
+ expectBox3Point(out, 42, 17, 99);
215
+ });
216
+
217
+ it('matches aabbFromPositions: empty input → inverted-infinity', () => {
218
+ // aabbFromPositions with Float32Array(0): returns inverted-infinity
219
+ const out = box3.create();
220
+ box3.fromPositions(out, []);
221
+ expectBox3(
222
+ out,
223
+ Number.POSITIVE_INFINITY,
224
+ Number.POSITIVE_INFINITY,
225
+ Number.POSITIVE_INFINITY,
226
+ Number.NEGATIVE_INFINITY,
227
+ Number.NEGATIVE_INFINITY,
228
+ Number.NEGATIVE_INFINITY,
229
+ );
230
+ });
231
+ });
232
+
233
+ // ============================================================
234
+ // Non-3-multiple trailing: behaviour matches aabbFromPositions
235
+ // (plan-strategy S5.3: trailing elements of an incomplete last point behave identically to old implementation)
236
+ // ============================================================
237
+
238
+ describe('box3.fromPositions — non-3-multiple trailing', () => {
239
+ it('5 elements (last point has 2 values): matches aabbFromPositions', () => {
240
+ // aabbFromPositions: seeds from positions[0..2]=(1,2,3), then i=3:
241
+ // x=4, y=5, z=positions[5]=undefined → NaN; NaN<3 and NaN>3 both false
242
+ // → minZ/maxZ stay at 3 (unchanged from seed).
243
+ const positions = [1, 2, 3, 4, 5];
244
+ const out = box3.create();
245
+ box3.fromPositions(out, positions);
246
+ expectBox3(out, 1, 2, 3, 4, 5, 3);
247
+ });
248
+
249
+ it('4 elements (last point has 1 value): matches aabbFromPositions', () => {
250
+ // Seeds from (1,2,3), i=3: x=4, y=positions[4]=undefined,
251
+ // z=positions[5]=undefined → NaN comparisons all false → y,z unchanged.
252
+ const positions = [1, 2, 3, 4];
253
+ const out = box3.create();
254
+ box3.fromPositions(out, positions);
255
+ expectBox3(out, 1, 2, 3, 4, 2, 3);
256
+ });
257
+ });
258
+
259
+ // ============================================================
260
+ // Out-param identity
261
+ // ============================================================
262
+
263
+ describe('box3.fromPositions — out-param contract', () => {
264
+ it('returns the same Box3 reference passed as out', () => {
265
+ const out = box3.create();
266
+ const result = box3.fromPositions(out, [1, 2, 3]);
267
+ expect(result).toBe(out);
268
+ });
269
+
270
+ it('writes to pre-existing non-default out, overwriting previous state', () => {
271
+ const out = box3.create(100, 100, 100, 200, 200, 200);
272
+ box3.fromPositions(out, [5, 5, 5, -5, -5, -5]);
273
+ expectBox3(out, -5, -5, -5, 5, 5, 5);
274
+ });
275
+ });
276
+
277
+ void expect;
@@ -0,0 +1,109 @@
1
+ // easing.test.ts — value tests for the easing namespace (solo round 20260713-233409)
2
+ //
3
+ // Regression guard for the friction that motivated the namespace: forgeax's math had no
4
+ // easing functions, so non-linear motion forced hand-rolling curves + the [0,1] clamp.
5
+ // These tests pin the polynomial curves and the ElasticInOut endpoint contract:
6
+ // 1. endpoints f(0)=0, f(1)=1,
7
+ // 2. input clamp: t<0 → 0, t>1 → 1 (saturate, not extrapolate),
8
+ // 3. symmetric midpoint f(0.5)=0.5,
9
+ // 4. exact polynomial match at sampled t,
10
+ // 5. monotonic non-decreasing on [0,1],
11
+ // 6. S-shape: below the diagonal early (f(0.25)<0.25), above it late (f(0.75)>0.75).
12
+
13
+ import { describe, expect, it } from 'vitest';
14
+ import { easing } from '../index';
15
+
16
+ const { cubicInOut, elasticInOut, smoothstep, smootherstep } = easing;
17
+
18
+ describe('easing.cubicInOut', () => {
19
+ it('matches Bevy cubic in/out and clamps input', () => {
20
+ expect(cubicInOut(-1)).toBe(0);
21
+ expect(cubicInOut(0)).toBe(0);
22
+ expect(cubicInOut(0.25)).toBeCloseTo(0.0625, 12);
23
+ expect(cubicInOut(0.5)).toBeCloseTo(0.5, 12);
24
+ expect(cubicInOut(0.75)).toBeCloseTo(0.9375, 12);
25
+ expect(cubicInOut(1)).toBe(1);
26
+ expect(cubicInOut(2)).toBe(1);
27
+ });
28
+ });
29
+
30
+ describe('easing.smoothstep', () => {
31
+ it('endpoints: f(0)=0, f(1)=1', () => {
32
+ expect(smoothstep(0)).toBeCloseTo(0, 12);
33
+ expect(smoothstep(1)).toBeCloseTo(1, 12);
34
+ });
35
+
36
+ it('clamps input to [0,1] (saturate, not extrapolate)', () => {
37
+ expect(smoothstep(-2)).toBe(0);
38
+ expect(smoothstep(5)).toBe(1);
39
+ });
40
+
41
+ it('symmetric midpoint f(0.5)=0.5', () => {
42
+ expect(smoothstep(0.5)).toBeCloseTo(0.5, 12);
43
+ });
44
+
45
+ it('matches 3t² − 2t³ at sampled t', () => {
46
+ for (const t of [0.1, 0.25, 0.4, 0.6, 0.75, 0.9]) {
47
+ expect(smoothstep(t)).toBeCloseTo(t * t * (3 - 2 * t), 12);
48
+ }
49
+ });
50
+
51
+ it('monotonic non-decreasing on [0,1]', () => {
52
+ let prev = -1;
53
+ for (let i = 0; i <= 20; i++) {
54
+ const v = smoothstep(i / 20);
55
+ expect(v).toBeGreaterThanOrEqual(prev);
56
+ prev = v;
57
+ }
58
+ });
59
+
60
+ it('S-shape: slow-in below diagonal early, slow-out above late', () => {
61
+ expect(smoothstep(0.25)).toBeLessThan(0.25);
62
+ expect(smoothstep(0.75)).toBeGreaterThan(0.75);
63
+ });
64
+ });
65
+
66
+ describe('easing.smootherstep', () => {
67
+ it('endpoints: f(0)=0, f(1)=1', () => {
68
+ expect(smootherstep(0)).toBeCloseTo(0, 12);
69
+ expect(smootherstep(1)).toBeCloseTo(1, 12);
70
+ });
71
+
72
+ it('clamps input to [0,1]', () => {
73
+ expect(smootherstep(-1)).toBe(0);
74
+ expect(smootherstep(2)).toBe(1);
75
+ });
76
+
77
+ it('symmetric midpoint f(0.5)=0.5', () => {
78
+ expect(smootherstep(0.5)).toBeCloseTo(0.5, 12);
79
+ });
80
+
81
+ it('matches 6t⁵ − 15t⁴ + 10t³ at sampled t', () => {
82
+ for (const t of [0.1, 0.25, 0.4, 0.6, 0.75, 0.9]) {
83
+ expect(smootherstep(t)).toBeCloseTo(6 * t ** 5 - 15 * t ** 4 + 10 * t ** 3, 12);
84
+ }
85
+ });
86
+
87
+ it('S-shape: even flatter endpoints than smoothstep near 0', () => {
88
+ // smootherstep has a flatter start (zero 2nd derivative), so at a small t it eases
89
+ // in LESS than smoothstep (stays closer to 0 longer).
90
+ expect(smootherstep(0.15)).toBeLessThan(smoothstep(0.15));
91
+ expect(smootherstep(0.25)).toBeLessThan(0.25);
92
+ expect(smootherstep(0.75)).toBeGreaterThan(0.75);
93
+ });
94
+ });
95
+
96
+ describe('easing.elasticInOut', () => {
97
+ it('clamps and preserves exact endpoints', () => {
98
+ expect(elasticInOut(-1)).toBe(0);
99
+ expect(elasticInOut(0)).toBe(0);
100
+ expect(elasticInOut(0.5)).toBeCloseTo(0.5, 12);
101
+ expect(elasticInOut(1)).toBe(1);
102
+ expect(elasticInOut(2)).toBe(1);
103
+ });
104
+
105
+ it('has the expected overshoot on both halves', () => {
106
+ expect(elasticInOut(0.35)).toBeLessThan(0);
107
+ expect(elasticInOut(0.65)).toBeGreaterThan(1);
108
+ });
109
+ });
@@ -0,0 +1,63 @@
1
+ // euler.test-d.ts — branded return type + EulerOrder union compile-time assertions (T-026)
2
+ //
3
+ // Locks the return types of euler.create / set / clone / toQuat / fromQuat / fromRotationMatrix;
4
+ // EulerOrder union has 6 exhaustive members; euler.fromQuat(q, 'INVALID') is ts-error.
5
+ //
6
+ // Related: requirements §AC-12 type test; plan-strategy §4.2 type test;
7
+ // wiki/typescript-branded-types §3.1 expectTypeOf template.
8
+
9
+ import { describe, expectTypeOf, it } from 'vitest';
10
+ import { euler, quat } from '../index';
11
+ import type { Euler, EulerOrder, Mat3, Quat } from '../types';
12
+
13
+ describe('euler — return-type locks', () => {
14
+ it('euler.create returns Euler', () => {
15
+ expectTypeOf(euler.create()).toEqualTypeOf<Euler>();
16
+ });
17
+
18
+ it('euler.clone returns Euler', () => {
19
+ expectTypeOf(euler.clone(euler.create())).toEqualTypeOf<Euler>();
20
+ });
21
+
22
+ it('euler.set returns Euler', () => {
23
+ expectTypeOf(euler.set(euler.create(), 0, 0, 0, 'XYZ')).toEqualTypeOf<Euler>();
24
+ });
25
+
26
+ it('euler.toQuat returns Quat', () => {
27
+ expectTypeOf(euler.toQuat(quat.create(), euler.create())).toEqualTypeOf<Quat>();
28
+ });
29
+
30
+ it('euler.fromQuat returns Euler', () => {
31
+ expectTypeOf(euler.fromQuat(euler.create(), quat.create(), 'XYZ')).toEqualTypeOf<Euler>();
32
+ });
33
+
34
+ it('euler.fromRotationMatrix returns Euler', () => {
35
+ const m: Mat3 = null as unknown as Mat3;
36
+ expectTypeOf(euler.fromRotationMatrix(euler.create(), m, 'XYZ')).toEqualTypeOf<Euler>();
37
+ });
38
+ });
39
+
40
+ describe('euler — EulerOrder union lock + invalid-order rejection', () => {
41
+ it('EulerOrder union exhaustive over 6 members', () => {
42
+ expectTypeOf<EulerOrder>().toEqualTypeOf<'XYZ' | 'YXZ' | 'ZXY' | 'ZYX' | 'YZX' | 'XZY'>();
43
+ });
44
+
45
+ it('euler.set accepts every one of the 6 EulerOrder values', () => {
46
+ euler.set(euler.create(), 0, 0, 0, 'XYZ');
47
+ euler.set(euler.create(), 0, 0, 0, 'YXZ');
48
+ euler.set(euler.create(), 0, 0, 0, 'ZXY');
49
+ euler.set(euler.create(), 0, 0, 0, 'ZYX');
50
+ euler.set(euler.create(), 0, 0, 0, 'YZX');
51
+ euler.set(euler.create(), 0, 0, 0, 'XZY');
52
+ });
53
+
54
+ it('euler.fromQuat rejects the INVALID literal', () => {
55
+ // @ts-expect-error — 'INVALID' is not in the EulerOrder union
56
+ euler.fromQuat(euler.create(), quat.create(), 'INVALID');
57
+ });
58
+
59
+ it('euler.set rejects the INVALID literal', () => {
60
+ // @ts-expect-error — 'INVALID' is not in the EulerOrder union
61
+ euler.set(euler.create(), 0, 0, 0, 'INVALID');
62
+ });
63
+ });
@@ -0,0 +1,47 @@
1
+ // mat3.test-d.ts — branded return type + dimension-mix-up compile-time assertions (T-018)
2
+ //
3
+ // Locks multiply / invert / transpose to return Mat3; dimension mix-ups are ts-error.
4
+ //
5
+ // Related: requirements §AC-12 type test + AC-02 brand mutual exclusion;
6
+ // wiki/typescript-branded-types §3.1 expectTypeOf template + §7.3 ts-expect-error.
7
+
8
+ import { describe, expectTypeOf, it } from 'vitest';
9
+ import { mat3 } from '../index';
10
+ import type { Mat3, Mat4, Vec3 } from '../types';
11
+
12
+ describe('mat3 — branded return-type locks', () => {
13
+ it('mat3.create returns Mat3', () => {
14
+ expectTypeOf(mat3.create()).toEqualTypeOf<Mat3>();
15
+ });
16
+
17
+ it('mat3.identity returns Mat3', () => {
18
+ expectTypeOf(mat3.identity(mat3.create())).toEqualTypeOf<Mat3>();
19
+ });
20
+
21
+ it('mat3.multiply returns Mat3', () => {
22
+ expectTypeOf(mat3.multiply(mat3.create(), mat3.create(), mat3.create())).toEqualTypeOf<Mat3>();
23
+ });
24
+
25
+ it('mat3.transpose returns Mat3', () => {
26
+ expectTypeOf(mat3.transpose(mat3.create(), mat3.create())).toEqualTypeOf<Mat3>();
27
+ });
28
+
29
+ it('mat3.invert returns Mat3 (does not return null, D-P1)', () => {
30
+ expectTypeOf(mat3.invert(mat3.create(), mat3.create())).toEqualTypeOf<Mat3>();
31
+ });
32
+ });
33
+
34
+ describe('mat3 — cross-namespace misuse blocked at compile time', () => {
35
+ it('mat3.multiply must reject Mat4 as out', () => {
36
+ const m4: Mat4 = null as unknown as Mat4;
37
+ const m3: Mat3 = mat3.create();
38
+ // @ts-expect-error — out must be Mat3, not Mat4
39
+ mat3.multiply(m4, m3, m3);
40
+ });
41
+
42
+ it('mat3.identity must reject Vec3 as out', () => {
43
+ const v3: Vec3 = null as unknown as Vec3;
44
+ // @ts-expect-error — out must be Mat3, not Vec3
45
+ mat3.identity(v3);
46
+ });
47
+ });