@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
package/src/box2.ts ADDED
@@ -0,0 +1,168 @@
1
+ // box2.ts — 2D axis-aligned bounding box namespace (P12 / bounding_2d).
2
+ //
3
+ // Storage layout: Float32Array length 4 [minX, minY, maxX, maxY]. The surface
4
+ // mirrors box3 so an AI can transfer broad-phase code between dimensions without
5
+ // learning a second object model.
6
+
7
+ import type { Circle2Like } from './circle2';
8
+ import type { Vec2, Vec2Like } from './types';
9
+
10
+ /** 2D AABB storage: [minX, minY, maxX, maxY]. */
11
+ export type Box2 = Float32Array & { readonly __box2: void };
12
+
13
+ /** Readable 2D AABB input. */
14
+ export type Box2Like = ArrayLike<number>;
15
+
16
+ /** Create a 2D AABB. The default is the inverted-infinity empty box. */
17
+ export function create(
18
+ minX = Number.POSITIVE_INFINITY,
19
+ minY = Number.POSITIVE_INFINITY,
20
+ maxX = Number.NEGATIVE_INFINITY,
21
+ maxY = Number.NEGATIVE_INFINITY,
22
+ ): Box2 {
23
+ return Float32Array.of(minX, minY, maxX, maxY) as Box2;
24
+ }
25
+
26
+ /** Create an AABB from its center and non-negative half-size. */
27
+ export function fromCenter(out: Box2, center: Vec2Like, halfSize: Vec2Like): Box2 {
28
+ const cx = center[0] as number;
29
+ const cy = center[1] as number;
30
+ const hx = Math.abs(halfSize[0] as number);
31
+ const hy = Math.abs(halfSize[1] as number);
32
+ out[0] = cx - hx;
33
+ out[1] = cy - hy;
34
+ out[2] = cx + hx;
35
+ out[3] = cy + hy;
36
+ return out;
37
+ }
38
+
39
+ /** Build the tightest AABB containing the given points. */
40
+ export function fromPoints(out: Box2, points: readonly Vec2Like[]): Box2 {
41
+ out[0] = Number.POSITIVE_INFINITY;
42
+ out[1] = Number.POSITIVE_INFINITY;
43
+ out[2] = Number.NEGATIVE_INFINITY;
44
+ out[3] = Number.NEGATIVE_INFINITY;
45
+ for (let i = 0; i < points.length; i++) {
46
+ const point = points[i] as Vec2Like;
47
+ const x = point[0] as number;
48
+ const y = point[1] as number;
49
+ if (x < (out[0] as number)) out[0] = x;
50
+ if (y < (out[1] as number)) out[1] = y;
51
+ if (x > (out[2] as number)) out[2] = x;
52
+ if (y > (out[3] as number)) out[3] = y;
53
+ }
54
+ return out;
55
+ }
56
+
57
+ /** Write the box center to out. Empty boxes produce non-finite values. */
58
+ export function center(out: Vec2, box: Box2Like): Vec2 {
59
+ out[0] = ((box[0] as number) + (box[2] as number)) * 0.5;
60
+ out[1] = ((box[1] as number) + (box[3] as number)) * 0.5;
61
+ return out;
62
+ }
63
+
64
+ /** Write the box half-size to out. */
65
+ export function halfSize(out: Vec2, box: Box2Like): Vec2 {
66
+ out[0] = ((box[2] as number) - (box[0] as number)) * 0.5;
67
+ out[1] = ((box[3] as number) - (box[1] as number)) * 0.5;
68
+ return out;
69
+ }
70
+
71
+ /** Write the closest point in the box to point. */
72
+ export function closestPoint(out: Vec2, box: Box2Like, point: Vec2Like): Vec2 {
73
+ const x = point[0] as number;
74
+ const y = point[1] as number;
75
+ const minX = box[0] as number;
76
+ const minY = box[1] as number;
77
+ const maxX = box[2] as number;
78
+ const maxY = box[3] as number;
79
+ out[0] = x < minX ? minX : x > maxX ? maxX : x;
80
+ out[1] = y < minY ? minY : y > maxY ? maxY : y;
81
+ return out;
82
+ }
83
+
84
+ /** True when point is inside the box, including its boundary. */
85
+ export function containsPoint(box: Box2Like, point: Vec2Like): boolean {
86
+ const x = point[0] as number;
87
+ const y = point[1] as number;
88
+ return (
89
+ x >= (box[0] as number) &&
90
+ x <= (box[2] as number) &&
91
+ y >= (box[1] as number) &&
92
+ y <= (box[3] as number)
93
+ );
94
+ }
95
+
96
+ /** True when the box completely contains other, including touching edges. */
97
+ export function containsBox(box: Box2Like, other: Box2Like): boolean {
98
+ return (
99
+ (other[0] as number) >= (box[0] as number) &&
100
+ (other[1] as number) >= (box[1] as number) &&
101
+ (other[2] as number) <= (box[2] as number) &&
102
+ (other[3] as number) <= (box[3] as number)
103
+ );
104
+ }
105
+
106
+ /** True when two boxes overlap, including touching edges. */
107
+ export function intersectsBox(a: Box2Like, b: Box2Like): boolean {
108
+ return (
109
+ (a[2] as number) >= (b[0] as number) &&
110
+ (a[0] as number) <= (b[2] as number) &&
111
+ (a[3] as number) >= (b[1] as number) &&
112
+ (a[1] as number) <= (b[3] as number)
113
+ );
114
+ }
115
+
116
+ /** True when a box and a circle overlap, including tangent contact. */
117
+ export function intersectsCircle(box: Box2Like, circle: Circle2Like): boolean {
118
+ const cx = circle[0] as number;
119
+ const cy = circle[1] as number;
120
+ const radius = circle[2] as number;
121
+ if (radius < 0) return false;
122
+ const closestX =
123
+ cx < (box[0] as number)
124
+ ? (box[0] as number)
125
+ : cx > (box[2] as number)
126
+ ? (box[2] as number)
127
+ : cx;
128
+ const closestY =
129
+ cy < (box[1] as number)
130
+ ? (box[1] as number)
131
+ : cy > (box[3] as number)
132
+ ? (box[3] as number)
133
+ : cy;
134
+ const dx = cx - closestX;
135
+ const dy = cy - closestY;
136
+ return dx * dx + dy * dy <= radius * radius;
137
+ }
138
+
139
+ /** Return the smallest box containing a and b. */
140
+ export function merge(out: Box2, a: Box2Like, b: Box2Like): Box2 {
141
+ out[0] = Math.min(a[0] as number, b[0] as number);
142
+ out[1] = Math.min(a[1] as number, b[1] as number);
143
+ out[2] = Math.max(a[2] as number, b[2] as number);
144
+ out[3] = Math.max(a[3] as number, b[3] as number);
145
+ return out;
146
+ }
147
+
148
+ /** Grow the box by a non-negative amount on each side. */
149
+ export function grow(out: Box2, box: Box2Like, amount: Vec2Like): Box2 {
150
+ const x = Math.abs(amount[0] as number);
151
+ const y = Math.abs(amount[1] as number);
152
+ out[0] = (box[0] as number) - x;
153
+ out[1] = (box[1] as number) - y;
154
+ out[2] = (box[2] as number) + x;
155
+ out[3] = (box[3] as number) + y;
156
+ return out;
157
+ }
158
+
159
+ /** Shrink the box by a non-negative amount on each side. */
160
+ export function shrink(out: Box2, box: Box2Like, amount: Vec2Like): Box2 {
161
+ const x = Math.abs(amount[0] as number);
162
+ const y = Math.abs(amount[1] as number);
163
+ out[0] = (box[0] as number) + x;
164
+ out[1] = (box[1] as number) + y;
165
+ out[2] = (box[2] as number) - x;
166
+ out[3] = (box[3] as number) - y;
167
+ return out;
168
+ }
package/src/box3.ts ADDED
@@ -0,0 +1,372 @@
1
+ // box3.ts — axis-aligned bounding box namespace (M3 / w7).
2
+ //
3
+ // 5-function surface (plan-tasks.json w7): create / expandByPoint / containsPoint / intersectsBox / fromPoints.
4
+ //
5
+ // Storage layout: Float32Array length 6 [minX, minY, minZ, maxX, maxY, maxZ].
6
+ // Default state is "empty box" using the standard inverted-infinity convention
7
+ // (min = +Infinity, max = -Infinity) so `expandByPoint` on any finite point
8
+ // collapses to a zero-volume box containing exactly that point; this mirrors
9
+ // three.js Box3.makeEmpty + glam/wgpu-matrix conventions (research knowledge-base
10
+ // gl-matrix-overview §degenerate-anchor equivalent for AABB).
11
+ //
12
+ // Design locks:
13
+ // - branded `Box3` Float32Array local to this module; cast funneled through `create`
14
+ // - pure-function / out-param style consistent with vec3 / mat4;
15
+ // - `expandByPoint` / `fromPoints` write to `out` in place and return it (aliasing-safe);
16
+ // - `containsPoint` / `intersectsBox` are boundary-inclusive (standard AABB semantics).
17
+ //
18
+ // Related: requirements §AC-16 (Box3 / Sphere pure functions);
19
+ // plan-strategy M3 range + D-P5 procedural geometry (Box3 backs future frustum culling);
20
+ // plan-tasks.json w7 acceptanceCheck.
21
+
22
+ import type { Mat4Like, Vec3Like } from './types';
23
+
24
+ /**
25
+ * Box3 storage: Float32Array length 6 [minX, minY, minZ, maxX, maxY, maxZ].
26
+ * Local brand (not part of the seven-piece SSOT because bounding volumes are
27
+ * neither linear-algebra primitives nor ECS POD carriers; types.ts SSOT is
28
+ * reserved for vec/mat/quat/color).
29
+ */
30
+ export type Box3 = Float32Array & { readonly __box3: void };
31
+
32
+ /**
33
+ * Box3 readable input: ArrayLike<number> of length 6 (ordering identical to Box3).
34
+ */
35
+ export type Box3Like = ArrayLike<number>;
36
+
37
+ /**
38
+ * Create a Box3. Defaults to the "empty" box (min=+Inf, max=-Inf) so the first
39
+ * `expandByPoint` call collapses to a zero-volume box at the input point.
40
+ */
41
+ export function create(
42
+ minX: number = Number.POSITIVE_INFINITY,
43
+ minY: number = Number.POSITIVE_INFINITY,
44
+ minZ: number = Number.POSITIVE_INFINITY,
45
+ maxX: number = Number.NEGATIVE_INFINITY,
46
+ maxY: number = Number.NEGATIVE_INFINITY,
47
+ maxZ: number = Number.NEGATIVE_INFINITY,
48
+ ): Box3 {
49
+ return Float32Array.of(minX, minY, minZ, maxX, maxY, maxZ) as Box3;
50
+ }
51
+
52
+ /**
53
+ * out ∪= point: grow min / max so `point` lies inside (boundary inclusive). Returns out.
54
+ * Works correctly from the default empty box (inverted-infinity) — the first call
55
+ * collapses both min and max to `point`.
56
+ */
57
+ export function expandByPoint(out: Box3, point: Vec3Like): Box3 {
58
+ const px = point[0] as number;
59
+ const py = point[1] as number;
60
+ const pz = point[2] as number;
61
+ if (px < (out[0] as number)) out[0] = px;
62
+ if (py < (out[1] as number)) out[1] = py;
63
+ if (pz < (out[2] as number)) out[2] = pz;
64
+ if (px > (out[3] as number)) out[3] = px;
65
+ if (py > (out[4] as number)) out[4] = py;
66
+ if (pz > (out[5] as number)) out[5] = pz;
67
+ return out;
68
+ }
69
+
70
+ /**
71
+ * True when `point` is inside `box` (boundary inclusive). An empty box
72
+ * (min > max on any axis) contains no points.
73
+ */
74
+ export function containsPoint(box: Box3Like, point: Vec3Like): boolean {
75
+ const px = point[0] as number;
76
+ const py = point[1] as number;
77
+ const pz = point[2] as number;
78
+ return (
79
+ px >= (box[0] as number) &&
80
+ px <= (box[3] as number) &&
81
+ py >= (box[1] as number) &&
82
+ py <= (box[4] as number) &&
83
+ pz >= (box[2] as number) &&
84
+ pz <= (box[5] as number)
85
+ );
86
+ }
87
+
88
+ /**
89
+ * True when `a` and `b` overlap on every axis (boundary inclusive — touching
90
+ * faces intersect). Two empty boxes do not intersect.
91
+ */
92
+ export function intersectsBox(a: Box3Like, b: Box3Like): boolean {
93
+ return (
94
+ (a[3] as number) >= (b[0] as number) &&
95
+ (a[0] as number) <= (b[3] as number) &&
96
+ (a[4] as number) >= (b[1] as number) &&
97
+ (a[1] as number) <= (b[4] as number) &&
98
+ (a[5] as number) >= (b[2] as number) &&
99
+ (a[2] as number) <= (b[5] as number)
100
+ );
101
+ }
102
+
103
+ /**
104
+ * Build the tightest AABB enclosing `points`. Writes to `out` in place and
105
+ * returns it. Empty `points` leaves `out` as the inverted-infinity empty box.
106
+ */
107
+ export function fromPoints(out: Box3, points: readonly Vec3Like[]): Box3 {
108
+ out[0] = Number.POSITIVE_INFINITY;
109
+ out[1] = Number.POSITIVE_INFINITY;
110
+ out[2] = Number.POSITIVE_INFINITY;
111
+ out[3] = Number.NEGATIVE_INFINITY;
112
+ out[4] = Number.NEGATIVE_INFINITY;
113
+ out[5] = Number.NEGATIVE_INFINITY;
114
+ for (let i = 0; i < points.length; i++) {
115
+ const p = points[i] as Vec3Like;
116
+ const px = p[0] as number;
117
+ const py = p[1] as number;
118
+ const pz = p[2] as number;
119
+ if (px < (out[0] as number)) out[0] = px;
120
+ if (py < (out[1] as number)) out[1] = py;
121
+ if (pz < (out[2] as number)) out[2] = pz;
122
+ if (px > (out[3] as number)) out[3] = px;
123
+ if (py > (out[4] as number)) out[4] = py;
124
+ if (pz > (out[5] as number)) out[5] = pz;
125
+ }
126
+ return out;
127
+ }
128
+
129
+ /**
130
+ * Build the tightest AABB enclosing flattened xyz `positions` (tight-packed,
131
+ * 3 floats per vertex). Writes to `out` in place and returns it.
132
+ *
133
+ * `positions` must have at least 3 elements to produce a non-empty box.
134
+ * Fewer than 3 elements leaves `out` as the inverted-infinity empty box
135
+ * (identical to `create()` default).
136
+ *
137
+ * Trailing elements beyond the last complete triplet are read verbatim
138
+ * (undefined → NaN), matching the legacy `aabbFromPositions` behaviour
139
+ * this function replaces.
140
+ *
141
+ * Related: `fromPoints` for `Vec3Like[]` input.
142
+ */
143
+ export function fromPositions(out: Box3, positions: ArrayLike<number>): Box3 {
144
+ out[0] = Number.POSITIVE_INFINITY;
145
+ out[1] = Number.POSITIVE_INFINITY;
146
+ out[2] = Number.POSITIVE_INFINITY;
147
+ out[3] = Number.NEGATIVE_INFINITY;
148
+ out[4] = Number.NEGATIVE_INFINITY;
149
+ out[5] = Number.NEGATIVE_INFINITY;
150
+ if (positions.length < 3) return out;
151
+ let minX = positions[0] as number;
152
+ let minY = positions[1] as number;
153
+ let minZ = positions[2] as number;
154
+ let maxX = minX;
155
+ let maxY = minY;
156
+ let maxZ = minZ;
157
+ for (let i = 3; i < positions.length; i += 3) {
158
+ const x = positions[i] as number;
159
+ const y = positions[i + 1] as number;
160
+ const z = positions[i + 2] as number;
161
+ if (x < minX) minX = x;
162
+ if (y < minY) minY = y;
163
+ if (z < minZ) minZ = z;
164
+ if (x > maxX) maxX = x;
165
+ if (y > maxY) maxY = y;
166
+ if (z > maxZ) maxZ = z;
167
+ }
168
+ out[0] = minX;
169
+ out[1] = minY;
170
+ out[2] = minZ;
171
+ out[3] = maxX;
172
+ out[4] = maxY;
173
+ out[5] = maxZ;
174
+ return out;
175
+ }
176
+
177
+ /**
178
+ * Transform an AABB by a 4x4 matrix using the conservative 8-corner method.
179
+ *
180
+ * Each of the 8 corners (all min/max combinations) is transformed by `m` as a
181
+ * homogeneous point (w=1), then a new AABB is computed that tightly encloses
182
+ * all 8 transformed points. This is always conservative — the output AABB is
183
+ * guaranteed to contain the true transformed volume (no false culling).
184
+ *
185
+ * Returns `out`. Aliasing-safe: `box` and `out` may be the same reference.
186
+ *
187
+ * @example
188
+ * ```ts
189
+ * import { box3, mat4 } from '@forgeax/engine-math';
190
+ * const box = box3.create(-1, -1, -1, 1, 1, 1);
191
+ * const xform = mat4.create();
192
+ * mat4.fromTranslation(xform, [5, 0, 0]);
193
+ * box3.transformBox3(box, box, xform);
194
+ * ```
195
+ */
196
+ export function transformBox3(out: Box3, box: Box3Like, m: Mat4Like): Box3 {
197
+ const bx = box[0] as number;
198
+ const by = box[1] as number;
199
+ const bz = box[2] as number;
200
+ const bX = box[3] as number;
201
+ const bY = box[4] as number;
202
+ const bZ = box[5] as number;
203
+
204
+ const m00 = m[0] as number;
205
+ const m01 = m[1] as number;
206
+ const m02 = m[2] as number;
207
+ const m03 = m[3] as number;
208
+ const m10 = m[4] as number;
209
+ const m11 = m[5] as number;
210
+ const m12 = m[6] as number;
211
+ const m13 = m[7] as number;
212
+ const m20 = m[8] as number;
213
+ const m21 = m[9] as number;
214
+ const m22 = m[10] as number;
215
+ const m23 = m[11] as number;
216
+ const m30 = m[12] as number;
217
+ const m31 = m[13] as number;
218
+ const m32 = m[14] as number;
219
+ const m33 = m[15] as number;
220
+
221
+ // Corner 0: (bx, by, bz)
222
+ let x = m00 * bx + m10 * by + m20 * bz + m30;
223
+ let y = m01 * bx + m11 * by + m21 * bz + m31;
224
+ let z = m02 * bx + m12 * by + m22 * bz + m32;
225
+ let w = m03 * bx + m13 * by + m23 * bz + m33;
226
+ if (w !== 0) {
227
+ const iw = 1 / w;
228
+ x *= iw;
229
+ y *= iw;
230
+ z *= iw;
231
+ }
232
+ let minX = x,
233
+ maxX = x,
234
+ minY = y,
235
+ maxY = y,
236
+ minZ = z,
237
+ maxZ = z;
238
+
239
+ // Corner 1: (bX, by, bz)
240
+ x = m00 * bX + m10 * by + m20 * bz + m30;
241
+ y = m01 * bX + m11 * by + m21 * bz + m31;
242
+ z = m02 * bX + m12 * by + m22 * bz + m32;
243
+ w = m03 * bX + m13 * by + m23 * bz + m33;
244
+ if (w !== 0) {
245
+ const iw = 1 / w;
246
+ x *= iw;
247
+ y *= iw;
248
+ z *= iw;
249
+ }
250
+ if (x < minX) minX = x;
251
+ if (x > maxX) maxX = x;
252
+ if (y < minY) minY = y;
253
+ if (y > maxY) maxY = y;
254
+ if (z < minZ) minZ = z;
255
+ if (z > maxZ) maxZ = z;
256
+
257
+ // Corner 2: (bx, bY, bz)
258
+ x = m00 * bx + m10 * bY + m20 * bz + m30;
259
+ y = m01 * bx + m11 * bY + m21 * bz + m31;
260
+ z = m02 * bx + m12 * bY + m22 * bz + m32;
261
+ w = m03 * bx + m13 * bY + m23 * bz + m33;
262
+ if (w !== 0) {
263
+ const iw = 1 / w;
264
+ x *= iw;
265
+ y *= iw;
266
+ z *= iw;
267
+ }
268
+ if (x < minX) minX = x;
269
+ if (x > maxX) maxX = x;
270
+ if (y < minY) minY = y;
271
+ if (y > maxY) maxY = y;
272
+ if (z < minZ) minZ = z;
273
+ if (z > maxZ) maxZ = z;
274
+
275
+ // Corner 3: (bX, bY, bz)
276
+ x = m00 * bX + m10 * bY + m20 * bz + m30;
277
+ y = m01 * bX + m11 * bY + m21 * bz + m31;
278
+ z = m02 * bX + m12 * bY + m22 * bz + m32;
279
+ w = m03 * bX + m13 * bY + m23 * bz + m33;
280
+ if (w !== 0) {
281
+ const iw = 1 / w;
282
+ x *= iw;
283
+ y *= iw;
284
+ z *= iw;
285
+ }
286
+ if (x < minX) minX = x;
287
+ if (x > maxX) maxX = x;
288
+ if (y < minY) minY = y;
289
+ if (y > maxY) maxY = y;
290
+ if (z < minZ) minZ = z;
291
+ if (z > maxZ) maxZ = z;
292
+
293
+ // Corner 4: (bx, by, bZ)
294
+ x = m00 * bx + m10 * by + m20 * bZ + m30;
295
+ y = m01 * bx + m11 * by + m21 * bZ + m31;
296
+ z = m02 * bx + m12 * by + m22 * bZ + m32;
297
+ w = m03 * bx + m13 * by + m23 * bZ + m33;
298
+ if (w !== 0) {
299
+ const iw = 1 / w;
300
+ x *= iw;
301
+ y *= iw;
302
+ z *= iw;
303
+ }
304
+ if (x < minX) minX = x;
305
+ if (x > maxX) maxX = x;
306
+ if (y < minY) minY = y;
307
+ if (y > maxY) maxY = y;
308
+ if (z < minZ) minZ = z;
309
+ if (z > maxZ) maxZ = z;
310
+
311
+ // Corner 5: (bX, by, bZ)
312
+ x = m00 * bX + m10 * by + m20 * bZ + m30;
313
+ y = m01 * bX + m11 * by + m21 * bZ + m31;
314
+ z = m02 * bX + m12 * by + m22 * bZ + m32;
315
+ w = m03 * bX + m13 * by + m23 * bZ + m33;
316
+ if (w !== 0) {
317
+ const iw = 1 / w;
318
+ x *= iw;
319
+ y *= iw;
320
+ z *= iw;
321
+ }
322
+ if (x < minX) minX = x;
323
+ if (x > maxX) maxX = x;
324
+ if (y < minY) minY = y;
325
+ if (y > maxY) maxY = y;
326
+ if (z < minZ) minZ = z;
327
+ if (z > maxZ) maxZ = z;
328
+
329
+ // Corner 6: (bx, bY, bZ)
330
+ x = m00 * bx + m10 * bY + m20 * bZ + m30;
331
+ y = m01 * bx + m11 * bY + m21 * bZ + m31;
332
+ z = m02 * bx + m12 * bY + m22 * bZ + m32;
333
+ w = m03 * bx + m13 * bY + m23 * bZ + m33;
334
+ if (w !== 0) {
335
+ const iw = 1 / w;
336
+ x *= iw;
337
+ y *= iw;
338
+ z *= iw;
339
+ }
340
+ if (x < minX) minX = x;
341
+ if (x > maxX) maxX = x;
342
+ if (y < minY) minY = y;
343
+ if (y > maxY) maxY = y;
344
+ if (z < minZ) minZ = z;
345
+ if (z > maxZ) maxZ = z;
346
+
347
+ // Corner 7: (bX, bY, bZ)
348
+ x = m00 * bX + m10 * bY + m20 * bZ + m30;
349
+ y = m01 * bX + m11 * bY + m21 * bZ + m31;
350
+ z = m02 * bX + m12 * bY + m22 * bZ + m32;
351
+ w = m03 * bX + m13 * bY + m23 * bZ + m33;
352
+ if (w !== 0) {
353
+ const iw = 1 / w;
354
+ x *= iw;
355
+ y *= iw;
356
+ z *= iw;
357
+ }
358
+ if (x < minX) minX = x;
359
+ if (x > maxX) maxX = x;
360
+ if (y < minY) minY = y;
361
+ if (y > maxY) maxY = y;
362
+ if (z < minZ) minZ = z;
363
+ if (z > maxZ) maxZ = z;
364
+
365
+ out[0] = minX;
366
+ out[1] = minY;
367
+ out[2] = minZ;
368
+ out[3] = maxX;
369
+ out[4] = maxY;
370
+ out[5] = maxZ;
371
+ return out;
372
+ }
package/src/circle2.ts ADDED
@@ -0,0 +1,134 @@
1
+ // circle2.ts — 2D bounding-circle namespace (P12 / bounding_2d).
2
+
3
+ import * as box2 from './box2';
4
+ import type { Vec2, Vec2Like } from './types';
5
+
6
+ /** 2D bounding circle storage: Float32Array [centerX, centerY, radius]. */
7
+ export type Circle2 = Float32Array & { readonly __circle2: void };
8
+
9
+ /** Readable 2D bounding-circle input. */
10
+ export type Circle2Like = ArrayLike<number>;
11
+
12
+ /** Create a bounding circle. Negative radius is clamped to zero. */
13
+ export function create(cx = 0, cy = 0, radius = 0): Circle2 {
14
+ return Float32Array.of(cx, cy, radius < 0 ? 0 : radius) as Circle2;
15
+ }
16
+
17
+ /** Build a circle centered at the point-cloud AABB center and enclosing all points. */
18
+ export function fromPoints(out: Circle2, points: readonly Vec2Like[]): Circle2 {
19
+ if (points.length === 0) {
20
+ out[0] = 0;
21
+ out[1] = 0;
22
+ out[2] = -1;
23
+ return out;
24
+ }
25
+ const bounds = box2.create();
26
+ box2.fromPoints(bounds, points);
27
+ const cx = ((bounds[0] as number) + (bounds[2] as number)) * 0.5;
28
+ const cy = ((bounds[1] as number) + (bounds[3] as number)) * 0.5;
29
+ let radiusSq = 0;
30
+ for (let i = 0; i < points.length; i++) {
31
+ const point = points[i] as Vec2Like;
32
+ const dx = (point[0] as number) - cx;
33
+ const dy = (point[1] as number) - cy;
34
+ const distanceSq = dx * dx + dy * dy;
35
+ if (distanceSq > radiusSq) radiusSq = distanceSq;
36
+ }
37
+ out[0] = cx;
38
+ out[1] = cy;
39
+ out[2] = Math.sqrt(radiusSq);
40
+ return out;
41
+ }
42
+
43
+ /** Build the smallest AABB containing the circle. */
44
+ export function toBox(out: box2.Box2, circle: Circle2Like): box2.Box2 {
45
+ const cx = circle[0] as number;
46
+ const cy = circle[1] as number;
47
+ const radius = circle[2] as number;
48
+ out[0] = cx - radius;
49
+ out[1] = cy - radius;
50
+ out[2] = cx + radius;
51
+ out[3] = cy + radius;
52
+ return out;
53
+ }
54
+
55
+ /** Write the circle center to out. */
56
+ export function center(out: Vec2, circle: Circle2Like): Vec2 {
57
+ out[0] = circle[0] as number;
58
+ out[1] = circle[1] as number;
59
+ return out;
60
+ }
61
+
62
+ /** Return the circle radius. */
63
+ export function radius(circle: Circle2Like): number {
64
+ return circle[2] as number;
65
+ }
66
+
67
+ /** Write the closest point in the circle to point. */
68
+ export function closestPoint(out: Vec2, circle: Circle2Like, point: Vec2Like): Vec2 {
69
+ const dx = (point[0] as number) - (circle[0] as number);
70
+ const dy = (point[1] as number) - (circle[1] as number);
71
+ const distanceSq = dx * dx + dy * dy;
72
+ const r = circle[2] as number;
73
+ if (r < 0) {
74
+ out[0] = circle[0] as number;
75
+ out[1] = circle[1] as number;
76
+ return out;
77
+ }
78
+ if (distanceSq <= r * r || distanceSq < 1e-12) {
79
+ out[0] = point[0] as number;
80
+ out[1] = point[1] as number;
81
+ return out;
82
+ }
83
+ const scale = r / Math.sqrt(distanceSq);
84
+ out[0] = (circle[0] as number) + dx * scale;
85
+ out[1] = (circle[1] as number) + dy * scale;
86
+ return out;
87
+ }
88
+
89
+ /** True when point is inside the circle, including its boundary. */
90
+ export function containsPoint(circle: Circle2Like, point: Vec2Like): boolean {
91
+ const dx = (point[0] as number) - (circle[0] as number);
92
+ const dy = (point[1] as number) - (circle[1] as number);
93
+ const r = circle[2] as number;
94
+ return r >= 0 && dx * dx + dy * dy <= r * r;
95
+ }
96
+
97
+ /** True when two circles overlap, including tangent contact. */
98
+ export function intersectsCircle(a: Circle2Like, b: Circle2Like): boolean {
99
+ const dx = (a[0] as number) - (b[0] as number);
100
+ const dy = (a[1] as number) - (b[1] as number);
101
+ const radius = (a[2] as number) + (b[2] as number);
102
+ return (a[2] as number) >= 0 && (b[2] as number) >= 0 && dx * dx + dy * dy <= radius * radius;
103
+ }
104
+
105
+ /** True when the circle overlaps the box, including tangent contact. */
106
+ export function intersectsBox(circle: Circle2Like, box: box2.Box2Like): boolean {
107
+ return box2.intersectsCircle(box, circle);
108
+ }
109
+
110
+ /** Return the smallest circle containing both circles. */
111
+ export function merge(out: Circle2, a: Circle2Like, b: Circle2Like): Circle2 {
112
+ const ax = a[0] as number;
113
+ const ay = a[1] as number;
114
+ const ar = a[2] as number;
115
+ const bx = b[0] as number;
116
+ const by = b[1] as number;
117
+ const br = b[2] as number;
118
+ const dx = bx - ax;
119
+ const dy = by - ay;
120
+ const distance = Math.sqrt(dx * dx + dy * dy);
121
+ if (ar >= distance + br) return createInto(out, ax, ay, ar);
122
+ if (br >= distance + ar) return createInto(out, bx, by, br);
123
+ if (distance < 1e-12) return createInto(out, ax, ay, Math.max(ar, br));
124
+ const nextRadius = (distance + ar + br) * 0.5;
125
+ const shift = (nextRadius - ar) / distance;
126
+ return createInto(out, ax + dx * shift, ay + dy * shift, nextRadius);
127
+ }
128
+
129
+ function createInto(out: Circle2, cx: number, cy: number, r: number): Circle2 {
130
+ out[0] = cx;
131
+ out[1] = cy;
132
+ out[2] = r;
133
+ return out;
134
+ }