@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,125 @@
1
+ // vec-catmull-rom.test.ts — value tests for vec2/vec3.catmullRom (solo round 20260713-203432)
2
+ //
3
+ // Regression guard for the friction that motivated the sampler: a smooth curve through
4
+ // control points (Bevy CubicCardinalSpline::new_catmull_rom, three.js CatmullRomCurve3)
5
+ // forced hand-rolling the cubic coefficient matrix (tension, basis, control-point
6
+ // windowing — all easy to get wrong). catmullRom folds the Catmull-Rom (tension 0.5)
7
+ // position basis. These tests pin:
8
+ // 1. endpoint interpolation: t=0 → p1, t=1 → p2 (the defining Catmull-Rom property),
9
+ // 2. matches the explicit coefficient formula per component,
10
+ // 3. collinear equally-spaced control points → linear interpolation (straight stays straight),
11
+ // 4. midpoint value for a known symmetric control set,
12
+ // 5. aliasing-safe (out === an input point),
13
+ // 6. vec2 + vec3 symmetric.
14
+
15
+ import { describe, expect, it } from 'vitest';
16
+ import type { Vec2Like, Vec3Like } from '../types';
17
+ import * as vec2 from '../vec2';
18
+ import * as vec3 from '../vec3';
19
+
20
+ // Reference scalar Catmull-Rom (tension 0.5) for cross-checking.
21
+ function crScalar(a: number, b: number, c: number, d: number, t: number): number {
22
+ const c0 = b;
23
+ const c1 = 0.5 * (c - a);
24
+ const c2 = a - 2.5 * b + 2 * c - 0.5 * d;
25
+ const c3 = -0.5 * a + 1.5 * b - 1.5 * c + 0.5 * d;
26
+ return c0 + c1 * t + c2 * t * t + c3 * t * t * t;
27
+ }
28
+
29
+ describe('vec3.catmullRom — endpoint interpolation', () => {
30
+ it('t=0 → p1, t=1 → p2 (passes through the control points)', () => {
31
+ const p0: Vec3Like = [0, 0, 0];
32
+ const p1: Vec3Like = [1, 2, 3];
33
+ const p2: Vec3Like = [4, 0, -1];
34
+ const p3: Vec3Like = [5, 5, 5];
35
+ const out = vec3.create();
36
+
37
+ vec3.catmullRom(out, p0, p1, p2, p3, 0);
38
+ expect(out[0]).toBeCloseTo(1, 6);
39
+ expect(out[1]).toBeCloseTo(2, 6);
40
+ expect(out[2]).toBeCloseTo(3, 6);
41
+
42
+ vec3.catmullRom(out, p0, p1, p2, p3, 1);
43
+ expect(out[0]).toBeCloseTo(4, 6);
44
+ expect(out[1]).toBeCloseTo(0, 6);
45
+ expect(out[2]).toBeCloseTo(-1, 6);
46
+ });
47
+
48
+ it('matches the explicit coefficient formula per component at several t', () => {
49
+ const p0: Vec3Like = [-1, 2, 0.5];
50
+ const p1: Vec3Like = [3, -2, 1];
51
+ const p2: Vec3Like = [5, 3, -4];
52
+ const p3: Vec3Like = [9, 8, 2];
53
+ const out = vec3.create();
54
+ for (const t of [0.1, 0.25, 0.5, 0.75, 0.9]) {
55
+ vec3.catmullRom(out, p0, p1, p2, p3, t);
56
+ expect(out[0]).toBeCloseTo(crScalar(-1, 3, 5, 9, t), 5);
57
+ expect(out[1]).toBeCloseTo(crScalar(2, -2, 3, 8, t), 5);
58
+ expect(out[2]).toBeCloseTo(crScalar(0.5, 1, -4, 2, t), 5);
59
+ }
60
+ });
61
+ });
62
+
63
+ describe('vec3.catmullRom — geometric properties', () => {
64
+ it('collinear equally-spaced control points reduce to linear interpolation', () => {
65
+ // Points on a straight line, equally spaced → the Catmull-Rom segment between p1,p2
66
+ // is exactly the straight segment (a well-known Catmull-Rom property).
67
+ const p0: Vec3Like = [0, 0, 0];
68
+ const p1: Vec3Like = [1, 1, 1];
69
+ const p2: Vec3Like = [2, 2, 2];
70
+ const p3: Vec3Like = [3, 3, 3];
71
+ const out = vec3.create();
72
+ for (const t of [0, 0.2, 0.5, 0.8, 1]) {
73
+ vec3.catmullRom(out, p0, p1, p2, p3, t);
74
+ const expected = 1 + t; // linear from p1=(1,1,1) to p2=(2,2,2)
75
+ expect(out[0]).toBeCloseTo(expected, 5);
76
+ expect(out[1]).toBeCloseTo(expected, 5);
77
+ expect(out[2]).toBeCloseTo(expected, 5);
78
+ }
79
+ });
80
+
81
+ it('midpoint of a symmetric control set is the known Catmull-Rom value', () => {
82
+ // Symmetric 1D control points 0,0,1,1 → midpoint t=0.5 value is 0.5 by symmetry.
83
+ const out = vec3.create();
84
+ vec3.catmullRom(out, [0, 0, 0], [0, 0, 0], [1, 1, 1], [1, 1, 1], 0.5);
85
+ expect(out[0]).toBeCloseTo(0.5, 6);
86
+ expect(out[1]).toBeCloseTo(0.5, 6);
87
+ expect(out[2]).toBeCloseTo(0.5, 6);
88
+ });
89
+ });
90
+
91
+ describe('vec3.catmullRom — aliasing-safe', () => {
92
+ it('out === p1 gives the correct result', () => {
93
+ const p1 = vec3.create();
94
+ vec3.set(p1, 1, 2, 3);
95
+ vec3.catmullRom(p1, [0, 0, 0], p1, [4, 0, -1], [5, 5, 5], 0.5);
96
+ const ex0 = crScalar(0, 1, 4, 5, 0.5);
97
+ const ex1 = crScalar(0, 2, 0, 5, 0.5);
98
+ const ex2 = crScalar(0, 3, -1, 5, 0.5);
99
+ expect(p1[0]).toBeCloseTo(ex0, 6);
100
+ expect(p1[1]).toBeCloseTo(ex1, 6);
101
+ expect(p1[2]).toBeCloseTo(ex2, 6);
102
+ });
103
+ });
104
+
105
+ describe('vec2.catmullRom — symmetric surface', () => {
106
+ it('endpoint interpolation + formula match', () => {
107
+ const p0: Vec2Like = [-1, 2];
108
+ const p1: Vec2Like = [3, -2];
109
+ const p2: Vec2Like = [5, 3];
110
+ const p3: Vec2Like = [9, 8];
111
+ const out = vec2.create();
112
+
113
+ vec2.catmullRom(out, p0, p1, p2, p3, 0);
114
+ expect(out[0]).toBeCloseTo(3, 6);
115
+ expect(out[1]).toBeCloseTo(-2, 6);
116
+
117
+ vec2.catmullRom(out, p0, p1, p2, p3, 1);
118
+ expect(out[0]).toBeCloseTo(5, 6);
119
+ expect(out[1]).toBeCloseTo(3, 6);
120
+
121
+ vec2.catmullRom(out, p0, p1, p2, p3, 0.35);
122
+ expect(out[0]).toBeCloseTo(crScalar(-1, 3, 5, 9, 0.35), 5);
123
+ expect(out[1]).toBeCloseTo(crScalar(2, -2, 3, 8, 0.35), 5);
124
+ });
125
+ });
@@ -0,0 +1,167 @@
1
+ // vec-smooth-damp.test.ts — value tests for vec2/vec3/vec4.smoothDamp (solo round 20260713-183918)
2
+ //
3
+ // Regression guard for the friction that motivated the helpers: smoothly moving one entity toward
4
+ // another (Bevy `Vec3::smooth_nudge`, three.js `MathUtils.damp`) required either a missing primitive
5
+ // or the frame-rate-DEPENDENT hand-write `vec3.lerp(out, p, target, rate * dt)` — smooth at 60 fps,
6
+ // snappy at 30 fps, overshooting when `rate * dt > 1`. smoothDamp folds the exponential decay
7
+ // `lerp(current, target, 1 − exp(−decayRate · dt))`. These tests pin:
8
+ // 1. dt = 0 → out = current (no move),
9
+ // 2. large decayRate·dt → out ≈ target (converges),
10
+ // 3. FRAME-RATE INDEPENDENCE WITNESS: one step of dt ≈ two composed steps of dt/2 (the property
11
+ // the naive lerp(rate·dt) violates — proven side-by-side that the naive form drifts),
12
+ // 4. monotone convergence: distance to target strictly shrinks each step (decay>0, dt>0),
13
+ // 5. aliasing-safe: out === current and out === target both give the right answer,
14
+ // 6. exact per-component formula current + (target−current)·(1−exp(−decay·dt)),
15
+ // 7. all three of vec2 / vec3 / vec4 present + symmetric.
16
+
17
+ import { describe, expect, it } from 'vitest';
18
+ import type { Vec2Like, Vec3Like, Vec4Like } from '../types';
19
+ import * as vec2 from '../vec2';
20
+ import * as vec3 from '../vec3';
21
+ import * as vec4 from '../vec4';
22
+
23
+ function dist3(a: Vec3Like, b: Vec3Like): number {
24
+ const dx = (a[0] as number) - (b[0] as number);
25
+ const dy = (a[1] as number) - (b[1] as number);
26
+ const dz = (a[2] as number) - (b[2] as number);
27
+ return Math.sqrt(dx * dx + dy * dy + dz * dz);
28
+ }
29
+
30
+ describe('vec3.smoothDamp — core contract', () => {
31
+ it('dt = 0 → out = current (no move)', () => {
32
+ const out = vec3.create();
33
+ vec3.smoothDamp(out, [1, 2, 3], [10, 20, 30], 5, 0);
34
+ expect(out[0]).toBeCloseTo(1, 5);
35
+ expect(out[1]).toBeCloseTo(2, 5);
36
+ expect(out[2]).toBeCloseTo(3, 5);
37
+ });
38
+
39
+ it('decayRate = 0 → out = current (no effect, matches Bevy)', () => {
40
+ const out = vec3.create();
41
+ vec3.smoothDamp(out, [1, 2, 3], [10, 20, 30], 0, 1 / 60);
42
+ expect(out[0]).toBeCloseTo(1, 5);
43
+ expect(out[1]).toBeCloseTo(2, 5);
44
+ expect(out[2]).toBeCloseTo(3, 5);
45
+ });
46
+
47
+ it('large decayRate·dt → out ≈ target (snaps)', () => {
48
+ const out = vec3.create();
49
+ vec3.smoothDamp(out, [0, 0, 0], [7, -4, 2], 1000, 1);
50
+ expect(out[0]).toBeCloseTo(7, 5);
51
+ expect(out[1]).toBeCloseTo(-4, 5);
52
+ expect(out[2]).toBeCloseTo(2, 5);
53
+ });
54
+
55
+ it('exact per-component formula current + (target−current)·(1−exp(−decay·dt))', () => {
56
+ const current: Vec3Like = [1, 2, 3];
57
+ const target: Vec3Like = [5, 5, 5];
58
+ const decay = 3;
59
+ const dt = 1 / 60;
60
+ const f = 1 - Math.exp(-decay * dt);
61
+ const out = vec3.create();
62
+ vec3.smoothDamp(out, current, target, decay, dt);
63
+ expect(out[0]).toBeCloseTo(1 + (5 - 1) * f, 6);
64
+ expect(out[1]).toBeCloseTo(2 + (5 - 2) * f, 6);
65
+ expect(out[2]).toBeCloseTo(3 + (5 - 3) * f, 6);
66
+ });
67
+
68
+ it('monotone convergence: distance to target strictly shrinks each step', () => {
69
+ const target: Vec3Like = [10, 10, 10];
70
+ const p = vec3.create();
71
+ vec3.set(p, 0, 0, 0);
72
+ let prev = dist3(p, target);
73
+ for (let i = 0; i < 120; i++) {
74
+ vec3.smoothDamp(p, p, target, 4, 1 / 60);
75
+ const d = dist3(p, target);
76
+ expect(d).toBeLessThan(prev);
77
+ prev = d;
78
+ }
79
+ // after 2 s of decay=4 the remaining distance is tiny (exp(−8) ≈ 3.4e-4 of the original)
80
+ expect(prev).toBeLessThan(0.01);
81
+ });
82
+ });
83
+
84
+ describe('vec3.smoothDamp — frame-rate independence WITNESS', () => {
85
+ it('one step of dt ≈ two composed steps of dt/2 (smoothDamp holds; naive lerp drifts)', () => {
86
+ const current: Vec3Like = [0, 0, 0];
87
+ const target: Vec3Like = [10, 0, 0];
88
+ const decay = 6;
89
+ const dt = 1 / 30;
90
+
91
+ // Frame-rate-INDEPENDENT: smoothDamp with one full dt.
92
+ const big = vec3.create();
93
+ vec3.smoothDamp(big, current, target, decay, dt);
94
+
95
+ // ...vs two half-dt smoothDamp steps composed. Exponential decay composes exactly:
96
+ // exp(−k·(dt/2))·exp(−k·(dt/2)) = exp(−k·dt).
97
+ const small = vec3.create();
98
+ vec3.copy(small, current);
99
+ vec3.smoothDamp(small, small, target, decay, dt / 2);
100
+ vec3.smoothDamp(small, small, target, decay, dt / 2);
101
+
102
+ expect(small[0]).toBeCloseTo(big[0] as number, 5);
103
+ expect(small[1]).toBeCloseTo(big[1] as number, 5);
104
+ expect(small[2]).toBeCloseTo(big[2] as number, 5);
105
+
106
+ // Contrast: the naive frame-rate-DEPENDENT lerp(current, target, rate·dt) does NOT compose —
107
+ // this is exactly the footgun smoothDamp folds. Prove the drift is real, not negligible.
108
+ const naiveBig = vec3.create();
109
+ vec3.lerp(naiveBig, current, target, decay * dt);
110
+ const naiveSmall = vec3.create();
111
+ vec3.copy(naiveSmall, current);
112
+ vec3.lerp(naiveSmall, naiveSmall, target, decay * (dt / 2));
113
+ vec3.lerp(naiveSmall, naiveSmall, target, decay * (dt / 2));
114
+ const naiveDrift = Math.abs((naiveBig[0] as number) - (naiveSmall[0] as number));
115
+ expect(naiveDrift).toBeGreaterThan(0.1); // meaningfully different — the bug smoothDamp removes
116
+ });
117
+ });
118
+
119
+ describe('vec3.smoothDamp — aliasing-safe', () => {
120
+ it('out === current gives the correct result', () => {
121
+ const p = vec3.create();
122
+ vec3.set(p, 1, 2, 3);
123
+ vec3.smoothDamp(p, p, [5, 5, 5], 3, 1 / 60);
124
+ const f = 1 - Math.exp(-3 / 60);
125
+ expect(p[0]).toBeCloseTo(1 + (5 - 1) * f, 6);
126
+ expect(p[1]).toBeCloseTo(2 + (5 - 2) * f, 6);
127
+ expect(p[2]).toBeCloseTo(3 + (5 - 3) * f, 6);
128
+ });
129
+
130
+ it('out === target gives the correct result', () => {
131
+ const t = vec3.create();
132
+ vec3.set(t, 5, 5, 5);
133
+ vec3.smoothDamp(t, [1, 2, 3], t, 3, 1 / 60);
134
+ const f = 1 - Math.exp(-3 / 60);
135
+ expect(t[0]).toBeCloseTo(1 + (5 - 1) * f, 6);
136
+ expect(t[1]).toBeCloseTo(2 + (5 - 2) * f, 6);
137
+ expect(t[2]).toBeCloseTo(3 + (5 - 3) * f, 6);
138
+ });
139
+ });
140
+
141
+ describe('vec2 / vec4 smoothDamp — symmetric surface', () => {
142
+ it('vec2.smoothDamp matches the per-component formula', () => {
143
+ const out = vec2.create();
144
+ const current: Vec2Like = [1, 2];
145
+ const target: Vec2Like = [9, -3];
146
+ const decay = 2.5;
147
+ const dt = 1 / 60;
148
+ const f = 1 - Math.exp(-decay * dt);
149
+ vec2.smoothDamp(out, current, target, decay, dt);
150
+ expect(out[0]).toBeCloseTo(1 + (9 - 1) * f, 6);
151
+ expect(out[1]).toBeCloseTo(2 + (-3 - 2) * f, 6);
152
+ });
153
+
154
+ it('vec4.smoothDamp matches the per-component formula', () => {
155
+ const out = vec4.create();
156
+ const current: Vec4Like = [1, 2, 3, 4];
157
+ const target: Vec4Like = [9, -3, 0, 8];
158
+ const decay = 2.5;
159
+ const dt = 1 / 60;
160
+ const f = 1 - Math.exp(-decay * dt);
161
+ vec4.smoothDamp(out, current, target, decay, dt);
162
+ expect(out[0]).toBeCloseTo(1 + (9 - 1) * f, 6);
163
+ expect(out[1]).toBeCloseTo(2 + (-3 - 2) * f, 6);
164
+ expect(out[2]).toBeCloseTo(3 + (0 - 3) * f, 6);
165
+ expect(out[3]).toBeCloseTo(4 + (8 - 4) * f, 6);
166
+ });
167
+ });
@@ -0,0 +1,59 @@
1
+ // vec2.test-d.ts — branded return type + dimension mutual-exclusion compile-time assertions (T-012)
2
+ //
3
+ // Locks add / sub / scale / normalize / lerp / perp etc. to return Vec2;
4
+ // vec2.add(out=Vec3, ...) is a ts-error verifying dimension mutual exclusion
5
+ // (an AC-02 brand-mutex contribution).
6
+ //
7
+ // Related: requirements §AC-12 type test + AC-02 brand mutual exclusion;
8
+ // wiki/typescript-branded-types §3.1 expectTypeOf template + §7.3 ts-expect-error.
9
+
10
+ import { describe, expectTypeOf, it } from 'vitest';
11
+ import { vec2 } from '../index';
12
+ import type { Vec2, Vec3 } from '../types';
13
+
14
+ describe('vec2 — branded return-type locks', () => {
15
+ it('vec2.create returns Vec2', () => {
16
+ expectTypeOf(vec2.create()).toEqualTypeOf<Vec2>();
17
+ });
18
+
19
+ it('vec2.add returns Vec2', () => {
20
+ expectTypeOf(vec2.add(vec2.create(), vec2.create(), vec2.create())).toEqualTypeOf<Vec2>();
21
+ });
22
+
23
+ it('vec2.sub returns Vec2', () => {
24
+ expectTypeOf(vec2.sub(vec2.create(), vec2.create(), vec2.create())).toEqualTypeOf<Vec2>();
25
+ });
26
+
27
+ it('vec2.scale returns Vec2', () => {
28
+ expectTypeOf(vec2.scale(vec2.create(), vec2.create(), 1)).toEqualTypeOf<Vec2>();
29
+ });
30
+
31
+ it('vec2.normalize returns Vec2', () => {
32
+ expectTypeOf(vec2.normalize(vec2.create(), vec2.create())).toEqualTypeOf<Vec2>();
33
+ });
34
+
35
+ it('vec2.lerp returns Vec2', () => {
36
+ expectTypeOf(vec2.lerp(vec2.create(), vec2.create(), vec2.create(), 0.5)).toEqualTypeOf<Vec2>();
37
+ });
38
+
39
+ it('vec2.perp returns Vec2', () => {
40
+ expectTypeOf(vec2.perp(vec2.create(), vec2.create())).toEqualTypeOf<Vec2>();
41
+ });
42
+
43
+ it('vec2.dot returns number', () => {
44
+ expectTypeOf(vec2.dot(vec2.create(), vec2.create())).toEqualTypeOf<number>();
45
+ });
46
+
47
+ it('vec2.length returns number', () => {
48
+ expectTypeOf(vec2.length(vec2.create())).toEqualTypeOf<number>();
49
+ });
50
+ });
51
+
52
+ describe('vec2 — cross-namespace misuse blocked at compile time', () => {
53
+ it('vec2.add must reject Vec3 as out', () => {
54
+ const v3: Vec3 = null as unknown as Vec3;
55
+ const v2: Vec2 = vec2.create();
56
+ // @ts-expect-error — out must be Vec2, not Vec3
57
+ vec2.add(v3, v2, v2);
58
+ });
59
+ });
@@ -0,0 +1,72 @@
1
+ // vec3.property.test.ts — fast-check property tests (M5 / T-031, AC-11)
2
+ //
3
+ // Category coverage:
4
+ // - normalize idempotence ×2: |n(v)|≈1, n(n(v))≈n(v)
5
+ // - identity ×1: v + 0 ≈ v (vec3.add identity)
6
+ //
7
+ // Total cases: 3. numRuns defaults to 100 (D-P5); the PROPERTY_NUM_RUNS env can override to 1000 (nightly).
8
+ // Degenerate input: normalize takes nonZeroVec3Arb (the zero vector is already mapper-repaired to [1,0,0]
9
+ // to avoid ε comparisons hitting NaN).
10
+ //
11
+ // Related: requirements §AC-11; plan-strategy D-P5; wiki/fast-check-property-test §5.4.
12
+
13
+ import { fc, test } from '@fast-check/vitest';
14
+ import { describe, expect } from 'vitest';
15
+ import * as vec3 from '../vec3';
16
+ import { EPS_F32, nonZeroVec3Arb, vec3Arb } from './_arbs';
17
+
18
+ // numRuns defaults to 100; nightly overrides to 1000 via env (D-P5).
19
+ // process.env is read via globalThis to avoid pulling in the @types/node devDep (AC-16 / D-P13).
20
+ const NUM_RUNS = Number.parseInt(
21
+ (globalThis as { process?: { env?: Record<string, string | undefined> } }).process?.env
22
+ ?.PROPERTY_NUM_RUNS ?? '100',
23
+ 10,
24
+ );
25
+
26
+ describe('vec3 property: normalize idempotence', () => {
27
+ test.prop({ v: nonZeroVec3Arb() }, { numRuns: NUM_RUNS })(
28
+ '|normalize(v)| ≈ 1 (v non-zero)',
29
+ ({ v }) => {
30
+ const out = vec3.create();
31
+ vec3.normalize(out, v);
32
+ const len = Math.hypot(out[0] as number, out[1] as number, out[2] as number);
33
+ // one sqrt + one div, wiki §4.4 recommends 1e-5
34
+ return Math.abs(len - 1) < EPS_F32;
35
+ },
36
+ );
37
+
38
+ test.prop({ v: nonZeroVec3Arb() }, { numRuns: NUM_RUNS })(
39
+ 'normalize(normalize(v)) ≈ normalize(v) (idempotent)',
40
+ ({ v }) => {
41
+ const n1 = vec3.create();
42
+ vec3.normalize(n1, v);
43
+ const n2 = vec3.create();
44
+ vec3.normalize(n2, n1);
45
+ // accumulated error of two normalizations stays within 1e-6
46
+ return (
47
+ Math.abs((n1[0] as number) - (n2[0] as number)) < 1e-6 &&
48
+ Math.abs((n1[1] as number) - (n2[1] as number)) < 1e-6 &&
49
+ Math.abs((n1[2] as number) - (n2[2] as number)) < 1e-6
50
+ );
51
+ },
52
+ );
53
+ });
54
+
55
+ describe('vec3 property: identity', () => {
56
+ test.prop({ v: vec3Arb() }, { numRuns: NUM_RUNS })('v + 0 ≈ v (additive identity)', ({ v }) => {
57
+ const zero = vec3.create();
58
+ const out = vec3.create();
59
+ vec3.add(out, v, zero);
60
+ // adding 0 is strictly equal (no float rounding), but keep ε to guard the -0 / +0 corner
61
+ return (
62
+ Math.abs((out[0] as number) - (v[0] as number)) < EPS_F32 &&
63
+ Math.abs((out[1] as number) - (v[1] as number)) < EPS_F32 &&
64
+ Math.abs((out[2] as number) - (v[2] as number)) < EPS_F32
65
+ );
66
+ });
67
+ });
68
+
69
+ // avoid unused import warning when fc is not directly referenced in body
70
+ void fc;
71
+ // explicitly import expect to silence typecheck complaints under some vitest configs
72
+ void expect;
@@ -0,0 +1,65 @@
1
+ // vec3.test-d.ts — branded return type + dimension mutual-exclusion compile-time assertions (T-012)
2
+ //
3
+ // Locks add / sub / scale / normalize / cross / lerp to return Vec3;
4
+ // vec3.add(out=Vec2, ...) is a ts-error; vec3.cross(out, vec2, vec2) is also a ts-error.
5
+ //
6
+ // Related: requirements §AC-12 type test + AC-02 brand mutual exclusion;
7
+ // wiki/typescript-branded-types §3.1 expectTypeOf template + §7.3 ts-expect-error.
8
+
9
+ import { describe, expectTypeOf, it } from 'vitest';
10
+ import { vec3 } from '../index';
11
+ import type { Vec2, Vec3, Vec4 } from '../types';
12
+
13
+ describe('vec3 — branded return-type locks', () => {
14
+ it('vec3.create returns Vec3', () => {
15
+ expectTypeOf(vec3.create()).toEqualTypeOf<Vec3>();
16
+ });
17
+
18
+ it('vec3.add returns Vec3', () => {
19
+ expectTypeOf(vec3.add(vec3.create(), vec3.create(), vec3.create())).toEqualTypeOf<Vec3>();
20
+ });
21
+
22
+ it('vec3.sub returns Vec3', () => {
23
+ expectTypeOf(vec3.sub(vec3.create(), vec3.create(), vec3.create())).toEqualTypeOf<Vec3>();
24
+ });
25
+
26
+ it('vec3.scale returns Vec3', () => {
27
+ expectTypeOf(vec3.scale(vec3.create(), vec3.create(), 1)).toEqualTypeOf<Vec3>();
28
+ });
29
+
30
+ it('vec3.normalize returns Vec3', () => {
31
+ expectTypeOf(vec3.normalize(vec3.create(), vec3.create())).toEqualTypeOf<Vec3>();
32
+ });
33
+
34
+ it('vec3.cross returns Vec3', () => {
35
+ expectTypeOf(vec3.cross(vec3.create(), vec3.create(), vec3.create())).toEqualTypeOf<Vec3>();
36
+ });
37
+
38
+ it('vec3.lerp returns Vec3', () => {
39
+ expectTypeOf(vec3.lerp(vec3.create(), vec3.create(), vec3.create(), 0.5)).toEqualTypeOf<Vec3>();
40
+ });
41
+
42
+ it('vec3.dot returns number', () => {
43
+ expectTypeOf(vec3.dot(vec3.create(), vec3.create())).toEqualTypeOf<number>();
44
+ });
45
+
46
+ it('vec3.length returns number', () => {
47
+ expectTypeOf(vec3.length(vec3.create())).toEqualTypeOf<number>();
48
+ });
49
+ });
50
+
51
+ describe('vec3 — cross-namespace misuse blocked at compile time', () => {
52
+ it('vec3.add must reject Vec2 as out', () => {
53
+ const v2: Vec2 = null as unknown as Vec2;
54
+ const v3: Vec3 = vec3.create();
55
+ // @ts-expect-error — out must be Vec3, not Vec2
56
+ vec3.add(v2, v3, v3);
57
+ });
58
+
59
+ it('vec3.cross must reject Vec4 as out', () => {
60
+ const v4: Vec4 = null as unknown as Vec4;
61
+ const v3: Vec3 = vec3.create();
62
+ // @ts-expect-error — out must be Vec3, not Vec4
63
+ vec3.cross(v4, v3, v3);
64
+ });
65
+ });
@@ -0,0 +1,61 @@
1
+ // vec4.test-d.ts — branded return type + dimension mutual-exclusion compile-time assertions (T-012)
2
+ //
3
+ // Locks add / sub / scale / normalize / lerp to return Vec4; vec4.add(out=Vec3, ...) is a ts-error.
4
+ // Vec4 ≠ Quat: contributes to the same-length=4 brand mutual-exclusion check.
5
+ //
6
+ // Related: requirements §AC-12 type test + AC-02 brand mutual exclusion;
7
+ // wiki/typescript-branded-types §3.1 expectTypeOf template + §7.3 ts-expect-error.
8
+
9
+ import { describe, expectTypeOf, it } from 'vitest';
10
+ import { vec4 } from '../index';
11
+ import type { Quat, Vec3, Vec4 } from '../types';
12
+
13
+ describe('vec4 — branded return-type locks', () => {
14
+ it('vec4.create returns Vec4', () => {
15
+ expectTypeOf(vec4.create()).toEqualTypeOf<Vec4>();
16
+ });
17
+
18
+ it('vec4.add returns Vec4', () => {
19
+ expectTypeOf(vec4.add(vec4.create(), vec4.create(), vec4.create())).toEqualTypeOf<Vec4>();
20
+ });
21
+
22
+ it('vec4.sub returns Vec4', () => {
23
+ expectTypeOf(vec4.sub(vec4.create(), vec4.create(), vec4.create())).toEqualTypeOf<Vec4>();
24
+ });
25
+
26
+ it('vec4.scale returns Vec4', () => {
27
+ expectTypeOf(vec4.scale(vec4.create(), vec4.create(), 1)).toEqualTypeOf<Vec4>();
28
+ });
29
+
30
+ it('vec4.normalize returns Vec4', () => {
31
+ expectTypeOf(vec4.normalize(vec4.create(), vec4.create())).toEqualTypeOf<Vec4>();
32
+ });
33
+
34
+ it('vec4.lerp returns Vec4', () => {
35
+ expectTypeOf(vec4.lerp(vec4.create(), vec4.create(), vec4.create(), 0.5)).toEqualTypeOf<Vec4>();
36
+ });
37
+
38
+ it('vec4.dot returns number', () => {
39
+ expectTypeOf(vec4.dot(vec4.create(), vec4.create())).toEqualTypeOf<number>();
40
+ });
41
+
42
+ it('vec4.length returns number', () => {
43
+ expectTypeOf(vec4.length(vec4.create())).toEqualTypeOf<number>();
44
+ });
45
+ });
46
+
47
+ describe('vec4 — cross-namespace misuse blocked at compile time', () => {
48
+ it('vec4.add must reject Vec3 as out', () => {
49
+ const v3: Vec3 = null as unknown as Vec3;
50
+ const v4: Vec4 = vec4.create();
51
+ // @ts-expect-error — out must be Vec4, not Vec3
52
+ vec4.add(v3, v4, v4);
53
+ });
54
+
55
+ it('Vec4 ≠ Quat (same length=4 brand mutual exclusion) — out must be Vec4', () => {
56
+ const q: Quat = null as unknown as Quat;
57
+ // @ts-expect-error — out must be Vec4; Quat is rejected (Vec4Like inputs allow ArrayLike,
58
+ // but the out type is strictly brand-locked, so Quat as out triggers a ts-error).
59
+ vec4.add(q, vec4.create(), vec4.create());
60
+ });
61
+ });
@@ -0,0 +1,29 @@
1
+ // _internal/epsilon.ts — EPS constant family SSOT (D-P14)
2
+ //
3
+ // Centralizes all numeric tolerances; prevents scattered hard-coded constants.
4
+ // Not exported through src/index.ts (D-P13 _internal does not leave the package);
5
+ // indirectly covered by the vec/mat/quat tests that consume it.
6
+ //
7
+ // Rationale:
8
+ // - EPS_NORMALIZE: lengths below this are treated as zero vectors; normalize falls back to a zero
9
+ // vector. 1e-12 follows the consensus of glam-rs and wgpu-matrix, avoiding single-precision
10
+ // Float32 noise misjudgments.
11
+ // - EPS_DET: threshold for declaring a determinant singular. 1e-8 leaves safe headroom over the
12
+ // single-precision ULP near 1.0 (~1.19e-7) (research §Finding 4.2 ε-tolerance table).
13
+ // - EPS_QUAT_PARALLEL: near-collinear input check for fromUnitVectors / slerp (D-P18).
14
+ // - EPS_SLERP_DOT_LIMIT: slerp falls back to nlerp when |dot| ≥ 1-EPS, avoiding acos blowup (D-P6).
15
+ //
16
+ // Related: plan-strategy D-P14 EPS constants centralized; research §Finding 3 glam epsilon choice +
17
+ // §Finding 4.2 ε-tolerance table.
18
+
19
+ /** Vector lengths below this value are treated as zero (normalize fall-back). */
20
+ export const EPS_NORMALIZE = 1e-12;
21
+
22
+ /** Matrix determinants below this value are treated as singular (invert falls back to identity, D-P1). */
23
+ export const EPS_DET = 1e-8;
24
+
25
+ /** Threshold for treating two unit vectors as nearly collinear / opposite (fromUnitVectors / slerp inputs). */
26
+ export const EPS_QUAT_PARALLEL = 1e-6;
27
+
28
+ /** In quat.slerp, |dot| above this value is treated as endpoint coincidence; falls back to nlerp (D-P6). */
29
+ export const EPS_SLERP_DOT_LIMIT = 1e-6;
@@ -0,0 +1,90 @@
1
+ // _internal/scalar.ts — scalar helper SSOT (D-P13 not exported from package)
2
+ //
3
+ // Provides the scalar-layer building blocks reused across the vec/mat/quat namespaces;
4
+ // avoids duplicate implementations.
5
+ // Not exported through src/index.ts; indirectly covered by the public API tests of the dim files
6
+ // (plan-strategy §4.1 exemption).
7
+ //
8
+ // Related: plan-strategy §1.1 file layering (_internal/scalar.ts) + D-P13 _internal not exported;
9
+ // requirements §duplicate-code elimination strategy D mixed.
10
+
11
+ /**
12
+ * Scalar linear interpolation. t is not clamped (extrapolation semantics preserved,
13
+ * matches glam Vec3.lerp / wgpu-matrix).
14
+ */
15
+ export function lerp(a: number, b: number, t: number): number {
16
+ return a + (b - a) * t;
17
+ }
18
+
19
+ /** Squared length of a four-component numeric value. */
20
+ export function lengthSq4(a: ArrayLike<number>): number {
21
+ const x = a[0] as number;
22
+ const y = a[1] as number;
23
+ const z = a[2] as number;
24
+ const w = a[3] as number;
25
+ return x * x + y * y + z * z + w * w;
26
+ }
27
+
28
+ /** Normalize a four-component numeric value into a typed-array output. */
29
+ export function normalize4(out: Float32Array, a: ArrayLike<number>, epsilon: number): void {
30
+ const lenSq = lengthSq4(a);
31
+ if (lenSq < epsilon) {
32
+ out[0] = 0;
33
+ out[1] = 0;
34
+ out[2] = 0;
35
+ out[3] = 0;
36
+ return;
37
+ }
38
+ const inv = 1 / Math.sqrt(lenSq);
39
+ out[0] = (a[0] as number) * inv;
40
+ out[1] = (a[1] as number) * inv;
41
+ out[2] = (a[2] as number) * inv;
42
+ out[3] = (a[3] as number) * inv;
43
+ }
44
+
45
+ /**
46
+ * Exponential-decay smoothing factor for frame-rate-INDEPENDENT damping:
47
+ * `1 − exp(−decayRate · dt)`. The single value the vec `smoothDamp` helpers interpolate by.
48
+ *
49
+ * SSOT for the smooth-nudge semantics (Bevy `StableInterpolate::smooth_nudge`,
50
+ * three.js `MathUtils.damp`): the returned factor composes multiplicatively over dt —
51
+ * `exp(−k·(a+b)) = exp(−k·a)·exp(−k·b)` — so one step of dt equals two steps of dt/2, the
52
+ * property a naive `rate·dt` factor violates. `dt=0` → 0 (no move); `decayRate·dt → ∞` → 1
53
+ * (snap to target); `decayRate=0` → 0 (no effect). NaN propagates via Math.exp.
54
+ */
55
+ export function smoothDecayFactor(decayRate: number, dt: number): number {
56
+ return 1 - Math.exp(-decayRate * dt);
57
+ }
58
+
59
+ /**
60
+ * Scalar Catmull-Rom spline value on the segment between `b` and `c`, with `a` / `d` the
61
+ * neighbor control points that set the endpoint tangents. Tension 0.5 (the Catmull-Rom
62
+ * special case of a Cardinal spline, and the Bevy default). SSOT for the vec `catmullRom`
63
+ * helpers — the coefficient matrix lives here once (Bevy `CubicCardinalSpline::char_matrix`
64
+ * with s=0.5):
65
+ * c0 = b
66
+ * c1 = 0.5 (c − a)
67
+ * c2 = a − 2.5 b + 2 c − 0.5 d
68
+ * c3 = −0.5 a + 1.5 b − 1.5 c + 0.5 d
69
+ * value = c0 + c1 t + c2 t² + c3 t³
70
+ * Interpolates the control points: t=0 → b, t=1 → c. `t` is not clamped (extrapolation
71
+ * beyond the segment follows the same cubic, matching `lerp`'s unclamped semantics).
72
+ */
73
+ export function catmullRomScalar(a: number, b: number, c: number, d: number, t: number): number {
74
+ const c0 = b;
75
+ const c1 = 0.5 * (c - a);
76
+ const c2 = a - 2.5 * b + 2 * c - 0.5 * d;
77
+ const c3 = -0.5 * a + 1.5 * b - 1.5 * c + 0.5 * d;
78
+ const t2 = t * t;
79
+ const t3 = t2 * t;
80
+ return c0 + c1 * t + c2 * t2 + c3 * t3;
81
+ }
82
+
83
+ /**
84
+ * Numeric clamp into the closed interval [min, max]. Returns NaN when v is NaN (NaN propagation).
85
+ */
86
+ export function clamp(v: number, min: number, max: number): number {
87
+ if (v < min) return min;
88
+ if (v > max) return max;
89
+ return v;
90
+ }