@flighthq/geometry 0.1.0

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 (125) hide show
  1. package/dist/aabb.d.ts +88 -0
  2. package/dist/aabb.d.ts.map +1 -0
  3. package/dist/aabb.js +230 -0
  4. package/dist/aabb.js.map +1 -0
  5. package/dist/boundingSphere.d.ts +62 -0
  6. package/dist/boundingSphere.d.ts.map +1 -0
  7. package/dist/boundingSphere.js +188 -0
  8. package/dist/boundingSphere.js.map +1 -0
  9. package/dist/capsule.d.ts +38 -0
  10. package/dist/capsule.d.ts.map +1 -0
  11. package/dist/capsule.js +215 -0
  12. package/dist/capsule.js.map +1 -0
  13. package/dist/frustum.d.ts +49 -0
  14. package/dist/frustum.d.ts.map +1 -0
  15. package/dist/frustum.js +149 -0
  16. package/dist/frustum.js.map +1 -0
  17. package/dist/index.d.ts +25 -0
  18. package/dist/index.d.ts.map +1 -0
  19. package/dist/index.js +25 -0
  20. package/dist/index.js.map +1 -0
  21. package/dist/matrix.d.ts +152 -0
  22. package/dist/matrix.d.ts.map +1 -0
  23. package/dist/matrix.js +488 -0
  24. package/dist/matrix.js.map +1 -0
  25. package/dist/matrix3.d.ts +63 -0
  26. package/dist/matrix3.d.ts.map +1 -0
  27. package/dist/matrix3.js +408 -0
  28. package/dist/matrix3.js.map +1 -0
  29. package/dist/matrix3Pool.d.ts +6 -0
  30. package/dist/matrix3Pool.d.ts.map +1 -0
  31. package/dist/matrix3Pool.js +26 -0
  32. package/dist/matrix3Pool.js.map +1 -0
  33. package/dist/matrix4.d.ts +238 -0
  34. package/dist/matrix4.d.ts.map +1 -0
  35. package/dist/matrix4.js +931 -0
  36. package/dist/matrix4.js.map +1 -0
  37. package/dist/matrix4Pool.d.ts +6 -0
  38. package/dist/matrix4Pool.d.ts.map +1 -0
  39. package/dist/matrix4Pool.js +26 -0
  40. package/dist/matrix4Pool.js.map +1 -0
  41. package/dist/matrixPool.d.ts +6 -0
  42. package/dist/matrixPool.d.ts.map +1 -0
  43. package/dist/matrixPool.js +26 -0
  44. package/dist/matrixPool.js.map +1 -0
  45. package/dist/obb.d.ts +44 -0
  46. package/dist/obb.d.ts.map +1 -0
  47. package/dist/obb.js +248 -0
  48. package/dist/obb.js.map +1 -0
  49. package/dist/plane.d.ts +66 -0
  50. package/dist/plane.d.ts.map +1 -0
  51. package/dist/plane.js +140 -0
  52. package/dist/plane.js.map +1 -0
  53. package/dist/quaternion.d.ts +128 -0
  54. package/dist/quaternion.d.ts.map +1 -0
  55. package/dist/quaternion.js +508 -0
  56. package/dist/quaternion.js.map +1 -0
  57. package/dist/quaternionPool.d.ts +6 -0
  58. package/dist/quaternionPool.d.ts.map +1 -0
  59. package/dist/quaternionPool.js +29 -0
  60. package/dist/quaternionPool.js.map +1 -0
  61. package/dist/ray3d.d.ts +69 -0
  62. package/dist/ray3d.d.ts.map +1 -0
  63. package/dist/ray3d.js +264 -0
  64. package/dist/ray3d.js.map +1 -0
  65. package/dist/rectangle.d.ts +56 -0
  66. package/dist/rectangle.d.ts.map +1 -0
  67. package/dist/rectangle.js +235 -0
  68. package/dist/rectangle.js.map +1 -0
  69. package/dist/rectanglePool.d.ts +6 -0
  70. package/dist/rectanglePool.d.ts.map +1 -0
  71. package/dist/rectanglePool.js +29 -0
  72. package/dist/rectanglePool.js.map +1 -0
  73. package/dist/typedarray.d.ts +4 -0
  74. package/dist/typedarray.d.ts.map +1 -0
  75. package/dist/typedarray.js +25 -0
  76. package/dist/typedarray.js.map +1 -0
  77. package/dist/vector2.d.ts +130 -0
  78. package/dist/vector2.d.ts.map +1 -0
  79. package/dist/vector2.js +250 -0
  80. package/dist/vector2.js.map +1 -0
  81. package/dist/vector2Pool.d.ts +6 -0
  82. package/dist/vector2Pool.d.ts.map +1 -0
  83. package/dist/vector2Pool.js +20 -0
  84. package/dist/vector2Pool.js.map +1 -0
  85. package/dist/vector3.d.ts +220 -0
  86. package/dist/vector3.d.ts.map +1 -0
  87. package/dist/vector3.js +380 -0
  88. package/dist/vector3.js.map +1 -0
  89. package/dist/vector3Pool.d.ts +6 -0
  90. package/dist/vector3Pool.d.ts.map +1 -0
  91. package/dist/vector3Pool.js +28 -0
  92. package/dist/vector3Pool.js.map +1 -0
  93. package/dist/vector4.d.ts +180 -0
  94. package/dist/vector4.d.ts.map +1 -0
  95. package/dist/vector4.js +333 -0
  96. package/dist/vector4.js.map +1 -0
  97. package/dist/vector4Pool.d.ts +6 -0
  98. package/dist/vector4Pool.d.ts.map +1 -0
  99. package/dist/vector4Pool.js +29 -0
  100. package/dist/vector4Pool.js.map +1 -0
  101. package/package.json +38 -0
  102. package/src/aabb.test.ts +341 -0
  103. package/src/boundingSphere.test.ts +247 -0
  104. package/src/capsule.test.ts +157 -0
  105. package/src/frustum.test.ts +160 -0
  106. package/src/matrix.test.ts +1111 -0
  107. package/src/matrix3.test.ts +752 -0
  108. package/src/matrix3Pool.test.ts +73 -0
  109. package/src/matrix4.test.ts +1627 -0
  110. package/src/matrix4Pool.test.ts +71 -0
  111. package/src/matrixPool.test.ts +69 -0
  112. package/src/obb.test.ts +183 -0
  113. package/src/plane.test.ts +225 -0
  114. package/src/quaternion.test.ts +531 -0
  115. package/src/quaternionPool.test.ts +64 -0
  116. package/src/ray3d.test.ts +297 -0
  117. package/src/rectangle.test.ts +982 -0
  118. package/src/rectanglePool.test.ts +71 -0
  119. package/src/typedarray.test.ts +118 -0
  120. package/src/vector2.test.ts +958 -0
  121. package/src/vector2Pool.test.ts +65 -0
  122. package/src/vector3.test.ts +749 -0
  123. package/src/vector3Pool.test.ts +68 -0
  124. package/src/vector4.test.ts +660 -0
  125. package/src/vector4Pool.test.ts +71 -0
@@ -0,0 +1,71 @@
1
+ import { getMatrix4Element } from '@flighthq/geometry';
2
+ import type { Matrix4 } from '@flighthq/types';
3
+
4
+ import { acquireIdentityMatrix4, acquireMatrix4, clearMatrix4Pool, releaseMatrix4 } from './matrix4Pool';
5
+
6
+ beforeEach(() => {
7
+ clearMatrix4Pool();
8
+ });
9
+
10
+ describe('acquireIdentityMatrix4', () => {
11
+ it('returns a matrix set to identity', () => {
12
+ const m = acquireIdentityMatrix4();
13
+ expect(getMatrix4Element(m, 0, 0)).toBe(1);
14
+ expect(getMatrix4Element(m, 0, 1)).toBe(0);
15
+ expect(getMatrix4Element(m, 1, 1)).toBe(1);
16
+ expect(getMatrix4Element(m, 2, 2)).toBe(1);
17
+ expect(getMatrix4Element(m, 3, 3)).toBe(1);
18
+ expect(getMatrix4Element(m, 0, 3)).toBe(0);
19
+ expect(getMatrix4Element(m, 3, 0)).toBe(0);
20
+ });
21
+
22
+ it('resets a released matrix to identity', () => {
23
+ const m1 = acquireMatrix4();
24
+ m1.m[0] = 5;
25
+ m1.m[12] = 10;
26
+ releaseMatrix4(m1);
27
+ const m2 = acquireIdentityMatrix4();
28
+ expect(m2).toBe(m1);
29
+ expect(getMatrix4Element(m2, 0, 0)).toBe(1);
30
+ expect(getMatrix4Element(m2, 3, 0)).toBe(0);
31
+ });
32
+ });
33
+
34
+ describe('acquireMatrix4', () => {
35
+ it('returns a new Matrix4 when pool is empty', () => {
36
+ const m: Matrix4 = acquireMatrix4();
37
+ expect(m).not.toBeNull();
38
+ });
39
+
40
+ it('reuses released matrices', () => {
41
+ const m1 = acquireMatrix4();
42
+ releaseMatrix4(m1);
43
+ const m2 = acquireMatrix4();
44
+ expect(m2).toBe(m1);
45
+ });
46
+
47
+ it('reuses released matrices in last-in-first-out order', () => {
48
+ const m1 = acquireMatrix4();
49
+ const m2 = acquireMatrix4();
50
+ releaseMatrix4(m1);
51
+ releaseMatrix4(m2);
52
+ expect(acquireMatrix4()).toBe(m2);
53
+ expect(acquireMatrix4()).toBe(m1);
54
+ });
55
+ });
56
+
57
+ describe('clearMatrix4Pool', () => {
58
+ it('empties the pool so the next get allocates fresh', () => {
59
+ const m = acquireMatrix4();
60
+ releaseMatrix4(m);
61
+ clearMatrix4Pool();
62
+ const m2 = acquireMatrix4();
63
+ expect(m2).not.toBe(m);
64
+ });
65
+ });
66
+
67
+ describe('releaseMatrix4', () => {
68
+ it('handles null safely', () => {
69
+ expect(() => releaseMatrix4(null as unknown as Matrix4)).not.toThrow();
70
+ });
71
+ });
@@ -0,0 +1,69 @@
1
+ import type { Matrix } from '@flighthq/types';
2
+
3
+ import { acquireIdentityMatrix, acquireMatrix, clearMatrixPool, releaseMatrix } from './matrixPool';
4
+
5
+ beforeEach(() => {
6
+ clearMatrixPool();
7
+ });
8
+
9
+ describe('acquireIdentityMatrix', () => {
10
+ it('returns a new Matrix when pool is empty', () => {
11
+ const m: Matrix = acquireMatrix();
12
+ expect(m).not.toBeNull();
13
+ });
14
+
15
+ it('reuses released matrices', () => {
16
+ const m1 = acquireMatrix();
17
+ releaseMatrix(m1);
18
+ const m2 = acquireMatrix();
19
+ expect(m2).toBe(m1);
20
+ });
21
+
22
+ it('reuses released matrices in last-in-first-out order', () => {
23
+ const m1 = acquireMatrix();
24
+ const m2 = acquireMatrix();
25
+ releaseMatrix(m1);
26
+ releaseMatrix(m2);
27
+ expect(acquireMatrix()).toBe(m2);
28
+ expect(acquireMatrix()).toBe(m1);
29
+ });
30
+ });
31
+
32
+ describe('acquireMatrix', () => {
33
+ it('returns a matrix set to identity', () => {
34
+ const m = acquireIdentityMatrix();
35
+ expect(m.a).toBe(1);
36
+ expect(m.b).toBe(0);
37
+ expect(m.c).toBe(0);
38
+ expect(m.d).toBe(1);
39
+ expect(m.tx).toBe(0);
40
+ expect(m.ty).toBe(0);
41
+ });
42
+
43
+ it('resets a released matrix to identity', () => {
44
+ const m1 = acquireMatrix();
45
+ m1.a = 5;
46
+ m1.tx = 10;
47
+ releaseMatrix(m1);
48
+ const m2 = acquireIdentityMatrix();
49
+ expect(m2).toBe(m1);
50
+ expect(m2.a).toBe(1);
51
+ expect(m2.tx).toBe(0);
52
+ });
53
+ });
54
+
55
+ describe('clearMatrixPool', () => {
56
+ it('empties the pool so the next get allocates fresh', () => {
57
+ const m = acquireMatrix();
58
+ releaseMatrix(m);
59
+ clearMatrixPool();
60
+ const m2 = acquireMatrix();
61
+ expect(m2).not.toBe(m);
62
+ });
63
+ });
64
+
65
+ describe('releaseMatrix', () => {
66
+ it('handles null safely', () => {
67
+ expect(() => releaseMatrix(null as unknown as Matrix)).not.toThrow();
68
+ });
69
+ });
@@ -0,0 +1,183 @@
1
+ import {
2
+ createAabb,
3
+ createMatrix4,
4
+ createObb,
5
+ createRay3D,
6
+ createVector3,
7
+ getClosestPointOnObb,
8
+ intersectRay3DObb,
9
+ isObbIntersectingAabb,
10
+ isObbIntersectingObb,
11
+ setMatrix4Position,
12
+ setObb,
13
+ transformObbByMatrix4,
14
+ } from '@flighthq/geometry';
15
+
16
+ describe('createObb', () => {
17
+ it('stores center, half-extents, and orientation', () => {
18
+ const o = createObb(1, 2, 3, 4, 5, 6, 0, 0, 0, 1);
19
+ expect(o.centerX).toBe(1);
20
+ expect(o.centerY).toBe(2);
21
+ expect(o.centerZ).toBe(3);
22
+ expect(o.halfExtentX).toBe(4);
23
+ expect(o.halfExtentY).toBe(5);
24
+ expect(o.halfExtentZ).toBe(6);
25
+ expect(o.orientationX).toBe(0);
26
+ expect(o.orientationY).toBe(0);
27
+ expect(o.orientationZ).toBe(0);
28
+ expect(o.orientationW).toBe(1);
29
+ });
30
+ });
31
+
32
+ describe('getClosestPointOnObb', () => {
33
+ it('clamps an outside point to the nearest face of an axis-aligned OBB', () => {
34
+ const o = createObb(0, 0, 0, 1, 1, 1, 0, 0, 0, 1);
35
+ const out = createVector3();
36
+ getClosestPointOnObb(out, o, createVector3(5, 0, 0));
37
+ expect(out.x).toBeCloseTo(1, 6);
38
+ expect(out.y).toBeCloseTo(0, 6);
39
+ expect(out.z).toBeCloseTo(0, 6);
40
+ });
41
+
42
+ it('returns a point inside the OBB unchanged', () => {
43
+ const o = createObb(0, 0, 0, 2, 2, 2, 0, 0, 0, 1);
44
+ const out = createVector3();
45
+ getClosestPointOnObb(out, o, createVector3(1, 0.5, -0.5));
46
+ expect(out.x).toBeCloseTo(1, 6);
47
+ expect(out.y).toBeCloseTo(0.5, 6);
48
+ expect(out.z).toBeCloseTo(-0.5, 6);
49
+ });
50
+
51
+ it('handles off-center OBB with identity orientation', () => {
52
+ const o = createObb(10, 0, 0, 1, 1, 1, 0, 0, 0, 1);
53
+ const out = createVector3();
54
+ getClosestPointOnObb(out, o, createVector3(0, 0, 0));
55
+ expect(out.x).toBeCloseTo(9, 6);
56
+ expect(out.y).toBeCloseTo(0, 6);
57
+ expect(out.z).toBeCloseTo(0, 6);
58
+ });
59
+
60
+ it('supports out === point', () => {
61
+ const o = createObb(0, 0, 0, 1, 1, 1, 0, 0, 0, 1);
62
+ const p = createVector3(5, 0, 0);
63
+ getClosestPointOnObb(p, o, p);
64
+ expect(p.x).toBeCloseTo(1, 6);
65
+ });
66
+ });
67
+
68
+ describe('intersectRay3DObb', () => {
69
+ it('hits a unit OBB at the expected t from outside', () => {
70
+ const o = createObb(0, 0, 0, 1, 1, 1, 0, 0, 0, 1);
71
+ // Ray from (5,0,0) pointing in -X: should enter at x=1, t=4.
72
+ const ray = createRay3D(5, 0, 0, -1, 0, 0);
73
+ const t = intersectRay3DObb(ray, o);
74
+ expect(t).toBeCloseTo(4, 6);
75
+ });
76
+
77
+ it('returns -1 for a ray pointing away from the OBB', () => {
78
+ const o = createObb(0, 0, 0, 1, 1, 1, 0, 0, 0, 1);
79
+ const ray = createRay3D(5, 0, 0, 1, 0, 0);
80
+ expect(intersectRay3DObb(ray, o)).toBe(-1);
81
+ });
82
+
83
+ it('returns 0 for a ray starting inside the OBB', () => {
84
+ const o = createObb(0, 0, 0, 2, 2, 2, 0, 0, 0, 1);
85
+ const ray = createRay3D(0, 0, 0, 0, 0, 1);
86
+ expect(intersectRay3DObb(ray, o)).toBe(0);
87
+ });
88
+
89
+ it('returns -1 for a ray that misses an off-center OBB', () => {
90
+ const o = createObb(10, 10, 10, 1, 1, 1, 0, 0, 0, 1);
91
+ const ray = createRay3D(0, 0, 0, 1, 0, 0);
92
+ expect(intersectRay3DObb(ray, o)).toBe(-1);
93
+ });
94
+ });
95
+
96
+ describe('isObbIntersectingAabb', () => {
97
+ it('returns true for coincident OBB and AABB', () => {
98
+ const o = createObb(0, 0, 0, 1, 1, 1, 0, 0, 0, 1);
99
+ const a = createAabb(-1, -1, -1, 1, 1, 1);
100
+ expect(isObbIntersectingAabb(o, a)).toBe(true);
101
+ });
102
+
103
+ it('returns true for overlapping OBB and AABB', () => {
104
+ const o = createObb(0.5, 0, 0, 1, 1, 1, 0, 0, 0, 1);
105
+ const a = createAabb(-1, -1, -1, 1, 1, 1);
106
+ expect(isObbIntersectingAabb(o, a)).toBe(true);
107
+ });
108
+
109
+ it('returns false for separated OBB and AABB', () => {
110
+ const o = createObb(10, 0, 0, 1, 1, 1, 0, 0, 0, 1);
111
+ const a = createAabb(-1, -1, -1, 1, 1, 1);
112
+ expect(isObbIntersectingAabb(o, a)).toBe(false);
113
+ });
114
+ });
115
+
116
+ describe('isObbIntersectingObb', () => {
117
+ it('returns true for two coincident OBBs', () => {
118
+ const a = createObb(0, 0, 0, 1, 1, 1, 0, 0, 0, 1);
119
+ const b = createObb(0, 0, 0, 1, 1, 1, 0, 0, 0, 1);
120
+ expect(isObbIntersectingObb(a, b)).toBe(true);
121
+ });
122
+
123
+ it('returns true for two overlapping OBBs', () => {
124
+ const a = createObb(0, 0, 0, 1, 1, 1, 0, 0, 0, 1);
125
+ const b = createObb(1.5, 0, 0, 1, 1, 1, 0, 0, 0, 1);
126
+ expect(isObbIntersectingObb(a, b)).toBe(true);
127
+ });
128
+
129
+ it('returns false for two separated OBBs', () => {
130
+ const a = createObb(0, 0, 0, 1, 1, 1, 0, 0, 0, 1);
131
+ const b = createObb(10, 0, 0, 1, 1, 1, 0, 0, 0, 1);
132
+ expect(isObbIntersectingObb(a, b)).toBe(false);
133
+ });
134
+ });
135
+
136
+ describe('setObb', () => {
137
+ it('updates all fields in place', () => {
138
+ const o = createObb(0, 0, 0, 0, 0, 0, 0, 0, 0, 1);
139
+ setObb(o, 1, 2, 3, 4, 5, 6, 0.1, 0.2, 0.3, 0.9);
140
+ expect(o.centerX).toBe(1);
141
+ expect(o.centerY).toBe(2);
142
+ expect(o.centerZ).toBe(3);
143
+ expect(o.halfExtentX).toBe(4);
144
+ expect(o.halfExtentY).toBe(5);
145
+ expect(o.halfExtentZ).toBe(6);
146
+ expect(o.orientationX).toBe(0.1);
147
+ expect(o.orientationW).toBe(0.9);
148
+ });
149
+ });
150
+
151
+ describe('transformObbByMatrix4', () => {
152
+ it('translates the center by a translation matrix', () => {
153
+ const o = createObb(0, 0, 0, 1, 1, 1, 0, 0, 0, 1);
154
+ const m = createMatrix4();
155
+ setMatrix4Position(m, createVector3(5, 0, 0));
156
+ const out = createObb(0, 0, 0, 0, 0, 0, 0, 0, 0, 1);
157
+ transformObbByMatrix4(out, o, m);
158
+ expect(out.centerX).toBeCloseTo(5, 6);
159
+ expect(out.centerY).toBeCloseTo(0, 6);
160
+ expect(out.halfExtentX).toBeCloseTo(1, 6);
161
+ expect(out.orientationW).toBeCloseTo(1, 5);
162
+ });
163
+
164
+ it('scales half-extents by a uniform scale matrix', () => {
165
+ const o = createObb(0, 0, 0, 1, 1, 1, 0, 0, 0, 1);
166
+ const m = createMatrix4(2, 0, 0, 0, 0, 2, 0, 0, 0, 0, 2, 0, 0, 0, 0, 1);
167
+ const out = createObb(0, 0, 0, 0, 0, 0, 0, 0, 0, 1);
168
+ transformObbByMatrix4(out, o, m);
169
+ expect(out.halfExtentX).toBeCloseTo(2, 6);
170
+ expect(out.halfExtentY).toBeCloseTo(2, 6);
171
+ expect(out.halfExtentZ).toBeCloseTo(2, 6);
172
+ });
173
+
174
+ it('supports out === obb', () => {
175
+ const o = createObb(1, 0, 0, 1, 1, 1, 0, 0, 0, 1);
176
+ const m = createMatrix4();
177
+ setMatrix4Position(m, createVector3(0, 3, 0));
178
+ transformObbByMatrix4(o, o, m);
179
+ expect(o.centerX).toBeCloseTo(1, 6);
180
+ expect(o.centerY).toBeCloseTo(3, 6);
181
+ expect(o.halfExtentX).toBeCloseTo(1, 6);
182
+ });
183
+ });
@@ -0,0 +1,225 @@
1
+ import {
2
+ clonePlane,
3
+ copyPlane,
4
+ createPlane,
5
+ createVector3,
6
+ getClosestPointOnPlane,
7
+ getPlaneCoplanarPoint,
8
+ getPlaneSignedDistanceToPoint,
9
+ normalizePlane,
10
+ projectVector3OntoPlane,
11
+ setPlane,
12
+ setPlaneFromNormalAndPoint,
13
+ setPlaneFromPoints,
14
+ } from '@flighthq/geometry';
15
+
16
+ describe('clonePlane', () => {
17
+ it('creates an independent copy', () => {
18
+ const p = createPlane(1, 0, 0, -5);
19
+ const c = clonePlane(p);
20
+ expect(c).not.toBe(p);
21
+ expect(c.a).toBe(1);
22
+ expect(c.d).toBe(-5);
23
+ });
24
+ });
25
+
26
+ describe('copyPlane', () => {
27
+ it('copies all coefficients', () => {
28
+ const src = createPlane(0, 1, 0, 3);
29
+ const out = createPlane();
30
+ copyPlane(out, src);
31
+ expect(out.a).toBe(0);
32
+ expect(out.b).toBe(1);
33
+ expect(out.c).toBe(0);
34
+ expect(out.d).toBe(3);
35
+ });
36
+
37
+ it('supports out === source', () => {
38
+ const p = createPlane(2, 3, 4, 5);
39
+ copyPlane(p, p);
40
+ expect(p.a).toBe(2);
41
+ expect(p.d).toBe(5);
42
+ });
43
+ });
44
+
45
+ describe('createPlane', () => {
46
+ it('defaults to zeros', () => {
47
+ const p = createPlane();
48
+ expect(p.a).toBe(0);
49
+ expect(p.b).toBe(0);
50
+ expect(p.c).toBe(0);
51
+ expect(p.d).toBe(0);
52
+ });
53
+
54
+ it('uses provided coefficients', () => {
55
+ const p = createPlane(1, 2, 3, 4);
56
+ expect(p.a).toBe(1);
57
+ expect(p.b).toBe(2);
58
+ expect(p.c).toBe(3);
59
+ expect(p.d).toBe(4);
60
+ });
61
+ });
62
+
63
+ describe('getClosestPointOnPlane', () => {
64
+ it('projects a point onto the plane along the normal', () => {
65
+ // Plane z = 0 => normal (0,0,1), d = 0.
66
+ const p = createPlane(0, 0, 1, 0);
67
+ const out = createVector3();
68
+ getClosestPointOnPlane(out, p, createVector3(2, 3, 5));
69
+ expect(out.x).toBeCloseTo(2, 6);
70
+ expect(out.y).toBeCloseTo(3, 6);
71
+ expect(out.z).toBeCloseTo(0, 6);
72
+ });
73
+
74
+ it('leaves a point already on the plane in place', () => {
75
+ const p = createPlane(1, 0, 0, -5);
76
+ const out = createVector3();
77
+ getClosestPointOnPlane(out, p, createVector3(5, 1, 2));
78
+ expect(out.x).toBeCloseTo(5, 6);
79
+ expect(out.y).toBeCloseTo(1, 6);
80
+ expect(out.z).toBeCloseTo(2, 6);
81
+ });
82
+
83
+ it('agrees with projectVector3OntoPlane', () => {
84
+ const p = createPlane(0, 1, 0, -3);
85
+ const closest = createVector3();
86
+ const projected = createVector3();
87
+ getClosestPointOnPlane(closest, p, createVector3(4, 7, -2));
88
+ projectVector3OntoPlane(projected, createVector3(4, 7, -2), p);
89
+ expect(closest.x).toBeCloseTo(projected.x, 6);
90
+ expect(closest.y).toBeCloseTo(projected.y, 6);
91
+ expect(closest.z).toBeCloseTo(projected.z, 6);
92
+ });
93
+
94
+ it('supports out === point', () => {
95
+ const p = createPlane(0, 0, 1, 0);
96
+ const v = createVector3(1, 2, 9);
97
+ getClosestPointOnPlane(v, p, v);
98
+ expect(v.z).toBeCloseTo(0, 6);
99
+ });
100
+ });
101
+
102
+ describe('getPlaneCoplanarPoint', () => {
103
+ it('point on the plane has signed distance zero', () => {
104
+ // Plane x = 5 => normal (+1,0,0), d = -5.
105
+ const p = createPlane(1, 0, 0, -5);
106
+ const out = createVector3();
107
+ getPlaneCoplanarPoint(out, p);
108
+ expect(getPlaneSignedDistanceToPoint(p, out)).toBeCloseTo(0, 6);
109
+ });
110
+ });
111
+
112
+ describe('getPlaneSignedDistanceToPoint', () => {
113
+ it('is positive on the normal side and negative behind', () => {
114
+ // Plane x = 0 with normal +x.
115
+ const p = createPlane(1, 0, 0, 0);
116
+ expect(getPlaneSignedDistanceToPoint(p, createVector3(3, 0, 0))).toBeCloseTo(3, 6);
117
+ expect(getPlaneSignedDistanceToPoint(p, createVector3(-2, 5, 9))).toBeCloseTo(-2, 6);
118
+ });
119
+
120
+ it('accounts for the plane offset', () => {
121
+ // Plane x = 4 with normal +x => a*x + d = x - 4.
122
+ const p = createPlane(1, 0, 0, -4);
123
+ expect(getPlaneSignedDistanceToPoint(p, createVector3(4, 0, 0))).toBeCloseTo(0, 6);
124
+ });
125
+ });
126
+
127
+ describe('normalizePlane', () => {
128
+ it('normalizes a plane with non-unit normal', () => {
129
+ // Normal (0,2,0), d=4 → normalized (0,1,0), d=2.
130
+ const p = createPlane(0, 2, 0, 4);
131
+ const out = createPlane();
132
+ normalizePlane(out, p);
133
+ expect(out.b).toBeCloseTo(1, 6);
134
+ expect(out.d).toBeCloseTo(2, 6);
135
+ });
136
+
137
+ it('supports out === source', () => {
138
+ const p = createPlane(0, 3, 0, 6);
139
+ normalizePlane(p, p);
140
+ expect(p.b).toBeCloseTo(1, 6);
141
+ expect(p.d).toBeCloseTo(2, 6);
142
+ });
143
+
144
+ it('leaves a degenerate plane unchanged', () => {
145
+ const p = createPlane(0, 0, 0, 5);
146
+ const out = createPlane();
147
+ normalizePlane(out, p);
148
+ expect(out.d).toBe(5);
149
+ });
150
+ });
151
+
152
+ describe('projectVector3OntoPlane', () => {
153
+ it('projects a point above the XZ plane down to y=0', () => {
154
+ // Plane y=0: normal (0,1,0), d=0.
155
+ const plane = createPlane(0, 1, 0, 0);
156
+ const out = createVector3();
157
+ projectVector3OntoPlane(out, createVector3(3, 5, 2), plane);
158
+ expect(out.x).toBeCloseTo(3, 6);
159
+ expect(out.y).toBeCloseTo(0, 6);
160
+ expect(out.z).toBeCloseTo(2, 6);
161
+ });
162
+
163
+ it('supports out === point', () => {
164
+ const plane = createPlane(1, 0, 0, 0);
165
+ const pt = createVector3(4, 2, 1);
166
+ projectVector3OntoPlane(pt, pt, plane);
167
+ expect(pt.x).toBeCloseTo(0, 6);
168
+ expect(pt.y).toBeCloseTo(2, 6);
169
+ expect(pt.z).toBeCloseTo(1, 6);
170
+ });
171
+ });
172
+
173
+ describe('setPlane', () => {
174
+ it('sets all coefficients', () => {
175
+ const p = createPlane();
176
+ setPlane(p, 9, 8, 7, 6);
177
+ expect(p.a).toBe(9);
178
+ expect(p.b).toBe(8);
179
+ expect(p.c).toBe(7);
180
+ expect(p.d).toBe(6);
181
+ });
182
+ });
183
+
184
+ describe('setPlaneFromNormalAndPoint', () => {
185
+ it('builds a plane that contains the given point', () => {
186
+ const normal = createVector3(0, 1, 0);
187
+ const point = createVector3(0, 5, 0);
188
+ const p = createPlane();
189
+ setPlaneFromNormalAndPoint(p, normal, point);
190
+ expect(getPlaneSignedDistanceToPoint(p, point)).toBeCloseTo(0, 6);
191
+ });
192
+
193
+ it('stores the normal in (a, b, c)', () => {
194
+ const normal = createVector3(1, 0, 0);
195
+ const point = createVector3(3, 0, 0);
196
+ const p = createPlane();
197
+ setPlaneFromNormalAndPoint(p, normal, point);
198
+ expect(p.a).toBeCloseTo(1, 6);
199
+ expect(p.b).toBeCloseTo(0, 6);
200
+ expect(p.c).toBeCloseTo(0, 6);
201
+ expect(p.d).toBeCloseTo(-3, 6);
202
+ });
203
+ });
204
+
205
+ describe('setPlaneFromPoints', () => {
206
+ it('builds the XZ plane from three points in XZ', () => {
207
+ const p = createPlane();
208
+ setPlaneFromPoints(p, createVector3(0, 0, 0), createVector3(1, 0, 0), createVector3(0, 0, 1));
209
+ // Normal should be +y (right-hand rule CCW from top)
210
+ expect(Math.abs(p.b)).toBeCloseTo(1, 5);
211
+ expect(p.a).toBeCloseTo(0, 5);
212
+ expect(p.c).toBeCloseTo(0, 5);
213
+ });
214
+
215
+ it('all three points lie on the resulting plane', () => {
216
+ const a = createVector3(1, 2, 3);
217
+ const b = createVector3(4, 0, 1);
218
+ const c = createVector3(-1, 3, 5);
219
+ const p = createPlane();
220
+ setPlaneFromPoints(p, a, b, c);
221
+ expect(getPlaneSignedDistanceToPoint(p, a)).toBeCloseTo(0, 5);
222
+ expect(getPlaneSignedDistanceToPoint(p, b)).toBeCloseTo(0, 5);
223
+ expect(getPlaneSignedDistanceToPoint(p, c)).toBeCloseTo(0, 5);
224
+ });
225
+ });