@emasoft/svg-matrix 1.0.18 → 1.0.20

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.
@@ -0,0 +1,840 @@
1
+ /**
2
+ * GJK Collision Detection with Arbitrary Precision and Mathematical Verification
3
+ *
4
+ * Implementation of the Gilbert-Johnson-Keerthi algorithm for detecting
5
+ * intersections between convex polygons using arbitrary precision arithmetic.
6
+ *
7
+ * Guarantees:
8
+ * 1. ARBITRARY PRECISION - All calculations use Decimal.js (50+ digits)
9
+ * 2. MATHEMATICAL VERIFICATION - Intersection results are verified
10
+ *
11
+ * ## Algorithm Overview
12
+ *
13
+ * The GJK algorithm determines if two convex shapes intersect by checking if
14
+ * the origin is contained in their Minkowski difference.
15
+ *
16
+ * For shapes A and B:
17
+ * - Minkowski difference: A ⊖ B = {a - b : a ∈ A, b ∈ B}
18
+ * - A and B intersect iff origin ∈ A ⊖ B
19
+ *
20
+ * Instead of computing the full Minkowski difference (expensive), GJK
21
+ * iteratively builds a simplex (triangle in 2D) that approaches the origin.
22
+ *
23
+ * ## Key Functions
24
+ *
25
+ * - `support(shape, direction)`: Returns the point in shape farthest along direction
26
+ * - `minkowskiSupport(A, B, d)`: Returns support(A, d) - support(B, -d)
27
+ * - `gjkIntersects(A, B)`: Returns true if shapes intersect
28
+ *
29
+ * @module gjk-collision
30
+ */
31
+
32
+ import Decimal from 'decimal.js';
33
+
34
+ // Set high precision for all calculations
35
+ Decimal.set({ precision: 80 });
36
+
37
+ // Helper to convert to Decimal
38
+ const D = x => (x instanceof Decimal ? x : new Decimal(x));
39
+
40
+ // Near-zero threshold for comparisons
41
+ const EPSILON = new Decimal('1e-40');
42
+
43
+ // Maximum iterations to prevent infinite loops
44
+ const MAX_ITERATIONS = 100;
45
+
46
+ // ============================================================================
47
+ // Point and Vector Utilities
48
+ // ============================================================================
49
+
50
+ /**
51
+ * Create a point/vector with Decimal coordinates.
52
+ * @param {number|string|Decimal} x - X coordinate
53
+ * @param {number|string|Decimal} y - Y coordinate
54
+ * @returns {{x: Decimal, y: Decimal}} Point object
55
+ */
56
+ export function point(x, y) {
57
+ return { x: D(x), y: D(y) };
58
+ }
59
+
60
+ /**
61
+ * Add two vectors.
62
+ * @param {{x: Decimal, y: Decimal}} a - First vector
63
+ * @param {{x: Decimal, y: Decimal}} b - Second vector
64
+ * @returns {{x: Decimal, y: Decimal}} Sum vector
65
+ */
66
+ export function vectorAdd(a, b) {
67
+ return { x: a.x.plus(b.x), y: a.y.plus(b.y) };
68
+ }
69
+
70
+ /**
71
+ * Subtract two vectors.
72
+ * @param {{x: Decimal, y: Decimal}} a - First vector
73
+ * @param {{x: Decimal, y: Decimal}} b - Second vector
74
+ * @returns {{x: Decimal, y: Decimal}} Difference vector (a - b)
75
+ */
76
+ export function vectorSub(a, b) {
77
+ return { x: a.x.minus(b.x), y: a.y.minus(b.y) };
78
+ }
79
+
80
+ /**
81
+ * Negate a vector.
82
+ * @param {{x: Decimal, y: Decimal}} v - Vector to negate
83
+ * @returns {{x: Decimal, y: Decimal}} Negated vector
84
+ */
85
+ export function vectorNeg(v) {
86
+ return { x: v.x.neg(), y: v.y.neg() };
87
+ }
88
+
89
+ /**
90
+ * Scale a vector.
91
+ * @param {{x: Decimal, y: Decimal}} v - Vector to scale
92
+ * @param {number|string|Decimal} s - Scale factor
93
+ * @returns {{x: Decimal, y: Decimal}} Scaled vector
94
+ */
95
+ export function vectorScale(v, s) {
96
+ const sd = D(s);
97
+ return { x: v.x.mul(sd), y: v.y.mul(sd) };
98
+ }
99
+
100
+ /**
101
+ * Dot product of two vectors.
102
+ * @param {{x: Decimal, y: Decimal}} a - First vector
103
+ * @param {{x: Decimal, y: Decimal}} b - Second vector
104
+ * @returns {Decimal} Dot product
105
+ */
106
+ export function dot(a, b) {
107
+ return a.x.mul(b.x).plus(a.y.mul(b.y));
108
+ }
109
+
110
+ /**
111
+ * 2D cross product (returns scalar z-component of 3D cross product).
112
+ * @param {{x: Decimal, y: Decimal}} a - First vector
113
+ * @param {{x: Decimal, y: Decimal}} b - Second vector
114
+ * @returns {Decimal} Cross product (a.x * b.y - a.y * b.x)
115
+ */
116
+ export function cross(a, b) {
117
+ return a.x.mul(b.y).minus(a.y.mul(b.x));
118
+ }
119
+
120
+ /**
121
+ * Squared magnitude of a vector.
122
+ * @param {{x: Decimal, y: Decimal}} v - Vector
123
+ * @returns {Decimal} Squared magnitude
124
+ */
125
+ export function magnitudeSquared(v) {
126
+ return v.x.mul(v.x).plus(v.y.mul(v.y));
127
+ }
128
+
129
+ /**
130
+ * Magnitude of a vector.
131
+ * @param {{x: Decimal, y: Decimal}} v - Vector
132
+ * @returns {Decimal} Magnitude
133
+ */
134
+ export function magnitude(v) {
135
+ return magnitudeSquared(v).sqrt();
136
+ }
137
+
138
+ /**
139
+ * Normalize a vector to unit length.
140
+ * @param {{x: Decimal, y: Decimal}} v - Vector to normalize
141
+ * @returns {{x: Decimal, y: Decimal}} Unit vector
142
+ */
143
+ export function normalize(v) {
144
+ const mag = magnitude(v);
145
+ if (mag.lessThan(EPSILON)) {
146
+ return { x: D(0), y: D(0) };
147
+ }
148
+ return { x: v.x.div(mag), y: v.y.div(mag) };
149
+ }
150
+
151
+ /**
152
+ * Get perpendicular vector (90° counter-clockwise rotation).
153
+ * @param {{x: Decimal, y: Decimal}} v - Vector
154
+ * @returns {{x: Decimal, y: Decimal}} Perpendicular vector
155
+ */
156
+ export function perpendicular(v) {
157
+ return { x: v.y.neg(), y: v.x };
158
+ }
159
+
160
+ /**
161
+ * Triple product for 2D: (A × B) × C = B(A·C) - A(B·C)
162
+ * This gives a vector perpendicular to C in the direction away from A.
163
+ * @param {{x: Decimal, y: Decimal}} a - First vector
164
+ * @param {{x: Decimal, y: Decimal}} b - Second vector
165
+ * @param {{x: Decimal, y: Decimal}} c - Third vector
166
+ * @returns {{x: Decimal, y: Decimal}} Triple product result
167
+ */
168
+ export function tripleProduct(a, b, c) {
169
+ // In 2D: (A × B) × C = B(A·C) - A(B·C)
170
+ const ac = dot(a, c);
171
+ const bc = dot(b, c);
172
+ return vectorSub(vectorScale(b, ac), vectorScale(a, bc));
173
+ }
174
+
175
+ // ============================================================================
176
+ // Support Functions
177
+ // ============================================================================
178
+
179
+ /**
180
+ * Find the point in a convex polygon farthest along a direction.
181
+ *
182
+ * This is the support function for a convex polygon - it finds the vertex
183
+ * that is farthest in the given direction.
184
+ *
185
+ * @param {Array<{x: Decimal, y: Decimal}>} polygon - Convex polygon vertices
186
+ * @param {{x: Decimal, y: Decimal}} direction - Direction to search
187
+ * @returns {{x: Decimal, y: Decimal}} Farthest point
188
+ */
189
+ export function supportPoint(polygon, direction) {
190
+ if (polygon.length === 0) {
191
+ return point(0, 0);
192
+ }
193
+
194
+ let maxDot = dot(polygon[0], direction);
195
+ let maxPoint = polygon[0];
196
+
197
+ for (let i = 1; i < polygon.length; i++) {
198
+ const d = dot(polygon[i], direction);
199
+ if (d.greaterThan(maxDot)) {
200
+ maxDot = d;
201
+ maxPoint = polygon[i];
202
+ }
203
+ }
204
+
205
+ return maxPoint;
206
+ }
207
+
208
+ /**
209
+ * Compute support point on the Minkowski difference A ⊖ B.
210
+ *
211
+ * For Minkowski difference, the support in direction d is:
212
+ * support(A ⊖ B, d) = support(A, d) - support(B, -d)
213
+ *
214
+ * @param {Array<{x: Decimal, y: Decimal}>} polygonA - First convex polygon
215
+ * @param {Array<{x: Decimal, y: Decimal}>} polygonB - Second convex polygon
216
+ * @param {{x: Decimal, y: Decimal}} direction - Direction to search
217
+ * @returns {{x: Decimal, y: Decimal}} Support point on Minkowski difference
218
+ */
219
+ export function minkowskiSupport(polygonA, polygonB, direction) {
220
+ const pointA = supportPoint(polygonA, direction);
221
+ const pointB = supportPoint(polygonB, vectorNeg(direction));
222
+ return vectorSub(pointA, pointB);
223
+ }
224
+
225
+ // ============================================================================
226
+ // Simplex Operations
227
+ // ============================================================================
228
+
229
+ /**
230
+ * Process a line simplex (2 points) and determine next direction.
231
+ *
232
+ * Given simplex [A, B] where A is the newest point:
233
+ * - If origin is in region AB, find direction perpendicular to AB toward origin
234
+ * - If origin is beyond A in direction opposite to B, direction is toward origin from A
235
+ *
236
+ * @param {Array<{x: Decimal, y: Decimal}>} simplex - Current simplex (modified in place)
237
+ * @param {{x: Decimal, y: Decimal}} direction - Current search direction (modified)
238
+ * @returns {{contains: boolean, newDirection: {x: Decimal, y: Decimal}}}
239
+ */
240
+ export function processLineSimplex(simplex, direction) {
241
+ const A = simplex[0]; // Newest point
242
+ const B = simplex[1];
243
+
244
+ const AB = vectorSub(B, A);
245
+ const AO = vectorNeg(A); // Vector from A to origin
246
+
247
+ // Check if origin is in the region of the line segment
248
+ const ABperp = tripleProduct(AB, AO, AB);
249
+
250
+ // If ABperp is essentially zero, points are collinear with origin
251
+ if (magnitudeSquared(ABperp).lessThan(EPSILON)) {
252
+ // Origin is on the line - check if between A and B
253
+ const dotAB_AO = dot(AB, AO);
254
+ const dotAB_AB = dot(AB, AB);
255
+
256
+ if (dotAB_AO.greaterThanOrEqualTo(0) && dotAB_AO.lessThanOrEqualTo(dotAB_AB)) {
257
+ // Origin is on the segment - we have intersection!
258
+ return { contains: true, newDirection: direction };
259
+ }
260
+
261
+ // Origin is on the line but outside segment
262
+ // Keep searching in direction perpendicular to the line
263
+ return { contains: false, newDirection: perpendicular(AB) };
264
+ }
265
+
266
+ return { contains: false, newDirection: ABperp };
267
+ }
268
+
269
+ /**
270
+ * Process a triangle simplex (3 points) and determine if origin is inside.
271
+ *
272
+ * Given simplex [A, B, C] where A is the newest point:
273
+ * - Check if origin is inside the triangle
274
+ * - If not, reduce to the edge closest to origin and get new direction
275
+ *
276
+ * @param {Array<{x: Decimal, y: Decimal}>} simplex - Current simplex (modified in place)
277
+ * @param {{x: Decimal, y: Decimal}} direction - Current search direction
278
+ * @returns {{contains: boolean, newDirection: {x: Decimal, y: Decimal}, newSimplex: Array}}
279
+ */
280
+ export function processTriangleSimplex(simplex, direction) {
281
+ const A = simplex[0]; // Newest point
282
+ const B = simplex[1];
283
+ const C = simplex[2];
284
+
285
+ const AB = vectorSub(B, A);
286
+ const AC = vectorSub(C, A);
287
+ const AO = vectorNeg(A); // Vector from A to origin
288
+
289
+ // Get perpendiculars to edges, pointing outward from triangle
290
+ const ABperp = tripleProduct(AC, AB, AB);
291
+ const ACperp = tripleProduct(AB, AC, AC);
292
+
293
+ // Check if origin is outside AB edge
294
+ if (dot(ABperp, AO).greaterThan(EPSILON)) {
295
+ // Origin is outside AB edge
296
+ // Remove C, keep A and B
297
+ return {
298
+ contains: false,
299
+ newDirection: ABperp,
300
+ newSimplex: [A, B]
301
+ };
302
+ }
303
+
304
+ // Check if origin is outside AC edge
305
+ if (dot(ACperp, AO).greaterThan(EPSILON)) {
306
+ // Origin is outside AC edge
307
+ // Remove B, keep A and C
308
+ return {
309
+ contains: false,
310
+ newDirection: ACperp,
311
+ newSimplex: [A, C]
312
+ };
313
+ }
314
+
315
+ // Origin is inside the triangle!
316
+ return {
317
+ contains: true,
318
+ newDirection: direction,
319
+ newSimplex: simplex
320
+ };
321
+ }
322
+
323
+ // ============================================================================
324
+ // GJK Algorithm
325
+ // ============================================================================
326
+
327
+ /**
328
+ * GJK (Gilbert-Johnson-Keerthi) intersection test.
329
+ *
330
+ * Determines if two convex polygons intersect by checking if the origin
331
+ * is contained in their Minkowski difference.
332
+ *
333
+ * VERIFICATION: When intersection is found, we verify by checking that
334
+ * at least one point from each polygon is actually overlapping.
335
+ *
336
+ * @param {Array<{x: Decimal, y: Decimal}>} polygonA - First convex polygon
337
+ * @param {Array<{x: Decimal, y: Decimal}>} polygonB - Second convex polygon
338
+ * @returns {{intersects: boolean, iterations: number, simplex: Array, verified: boolean}}
339
+ */
340
+ export function gjkIntersects(polygonA, polygonB) {
341
+ // Handle empty polygons
342
+ if (polygonA.length === 0 || polygonB.length === 0) {
343
+ return { intersects: false, iterations: 0, simplex: [], verified: true };
344
+ }
345
+
346
+ // Handle single points
347
+ if (polygonA.length === 1 && polygonB.length === 1) {
348
+ const dist = magnitude(vectorSub(polygonA[0], polygonB[0]));
349
+ return {
350
+ intersects: dist.lessThan(EPSILON),
351
+ iterations: 1,
352
+ simplex: [],
353
+ verified: true
354
+ };
355
+ }
356
+
357
+ // Initial direction: from center of A to center of B
358
+ let direction = vectorSub(
359
+ centroid(polygonB),
360
+ centroid(polygonA)
361
+ );
362
+
363
+ // If centers are the same, use arbitrary direction
364
+ if (magnitudeSquared(direction).lessThan(EPSILON)) {
365
+ direction = point(1, 0);
366
+ }
367
+
368
+ // Get first support point
369
+ let simplex = [minkowskiSupport(polygonA, polygonB, direction)];
370
+
371
+ // New direction: toward origin from first point
372
+ direction = vectorNeg(simplex[0]);
373
+
374
+ // If first point is at origin, we have intersection
375
+ if (magnitudeSquared(simplex[0]).lessThan(EPSILON)) {
376
+ return {
377
+ intersects: true,
378
+ iterations: 1,
379
+ simplex,
380
+ verified: verifyIntersection(polygonA, polygonB)
381
+ };
382
+ }
383
+
384
+ for (let iteration = 0; iteration < MAX_ITERATIONS; iteration++) {
385
+ // Get new support point in current direction
386
+ const newPoint = minkowskiSupport(polygonA, polygonB, direction);
387
+
388
+ // Check if we passed the origin
389
+ // If the new point isn't past the origin in the search direction,
390
+ // then the origin is not in the Minkowski difference
391
+ if (dot(newPoint, direction).lessThanOrEqualTo(EPSILON)) {
392
+ return {
393
+ intersects: false,
394
+ iterations: iteration + 1,
395
+ simplex,
396
+ verified: true
397
+ };
398
+ }
399
+
400
+ // Add new point to simplex
401
+ simplex.unshift(newPoint);
402
+
403
+ // Process simplex based on size
404
+ if (simplex.length === 2) {
405
+ // Line case
406
+ const result = processLineSimplex(simplex, direction);
407
+ if (result.contains) {
408
+ return {
409
+ intersects: true,
410
+ iterations: iteration + 1,
411
+ simplex,
412
+ verified: verifyIntersection(polygonA, polygonB)
413
+ };
414
+ }
415
+ direction = result.newDirection;
416
+ } else if (simplex.length === 3) {
417
+ // Triangle case
418
+ const result = processTriangleSimplex(simplex, direction);
419
+ if (result.contains) {
420
+ return {
421
+ intersects: true,
422
+ iterations: iteration + 1,
423
+ simplex: result.newSimplex,
424
+ verified: verifyIntersection(polygonA, polygonB)
425
+ };
426
+ }
427
+ simplex = result.newSimplex;
428
+ direction = result.newDirection;
429
+ }
430
+
431
+ // Normalize direction to prevent numerical issues
432
+ direction = normalize(direction);
433
+
434
+ // Check for zero direction (numerical issues)
435
+ if (magnitudeSquared(direction).lessThan(EPSILON)) {
436
+ // Can't determine - assume no intersection to be safe
437
+ return {
438
+ intersects: false,
439
+ iterations: iteration + 1,
440
+ simplex,
441
+ verified: false
442
+ };
443
+ }
444
+ }
445
+
446
+ // Max iterations reached - assume no intersection
447
+ return {
448
+ intersects: false,
449
+ iterations: MAX_ITERATIONS,
450
+ simplex,
451
+ verified: false
452
+ };
453
+ }
454
+
455
+ // ============================================================================
456
+ // Verification
457
+ // ============================================================================
458
+
459
+ /**
460
+ * Calculate centroid of a polygon.
461
+ * @param {Array<{x: Decimal, y: Decimal}>} polygon - Polygon vertices
462
+ * @returns {{x: Decimal, y: Decimal}} Centroid point
463
+ */
464
+ export function centroid(polygon) {
465
+ if (polygon.length === 0) {
466
+ return point(0, 0);
467
+ }
468
+
469
+ let sumX = D(0);
470
+ let sumY = D(0);
471
+
472
+ for (const p of polygon) {
473
+ sumX = sumX.plus(p.x);
474
+ sumY = sumY.plus(p.y);
475
+ }
476
+
477
+ const n = D(polygon.length);
478
+ return { x: sumX.div(n), y: sumY.div(n) };
479
+ }
480
+
481
+ /**
482
+ * Check if a point is inside a convex polygon.
483
+ *
484
+ * Uses the cross product sign method: a point is inside if it's on the
485
+ * same side of all edges.
486
+ *
487
+ * @param {{x: Decimal, y: Decimal}} pt - Point to test
488
+ * @param {Array<{x: Decimal, y: Decimal}>} polygon - Convex polygon
489
+ * @returns {boolean} True if point is inside (including boundary)
490
+ */
491
+ export function pointInConvexPolygon(pt, polygon) {
492
+ if (polygon.length < 3) {
493
+ return false;
494
+ }
495
+
496
+ let sign = null;
497
+
498
+ for (let i = 0; i < polygon.length; i++) {
499
+ const p1 = polygon[i];
500
+ const p2 = polygon[(i + 1) % polygon.length];
501
+
502
+ const edge = vectorSub(p2, p1);
503
+ const toPoint = vectorSub(pt, p1);
504
+ const crossVal = cross(edge, toPoint);
505
+
506
+ // On the edge is considered inside
507
+ if (crossVal.abs().lessThan(EPSILON)) {
508
+ continue;
509
+ }
510
+
511
+ const currentSign = crossVal.greaterThan(0);
512
+
513
+ if (sign === null) {
514
+ sign = currentSign;
515
+ } else if (sign !== currentSign) {
516
+ return false;
517
+ }
518
+ }
519
+
520
+ return true;
521
+ }
522
+
523
+ /**
524
+ * Verify that two polygons actually intersect.
525
+ *
526
+ * This is a secondary check after GJK to verify the result.
527
+ * We check if any vertex of one polygon is inside the other,
528
+ * or if any edges intersect.
529
+ *
530
+ * @param {Array<{x: Decimal, y: Decimal}>} polygonA - First polygon
531
+ * @param {Array<{x: Decimal, y: Decimal}>} polygonB - Second polygon
532
+ * @returns {boolean} True if intersection is verified
533
+ */
534
+ export function verifyIntersection(polygonA, polygonB) {
535
+ // Check if any vertex of A is inside B
536
+ for (const p of polygonA) {
537
+ if (pointInConvexPolygon(p, polygonB)) {
538
+ return true;
539
+ }
540
+ }
541
+
542
+ // Check if any vertex of B is inside A
543
+ for (const p of polygonB) {
544
+ if (pointInConvexPolygon(p, polygonA)) {
545
+ return true;
546
+ }
547
+ }
548
+
549
+ // Check if any edges intersect
550
+ for (let i = 0; i < polygonA.length; i++) {
551
+ const a1 = polygonA[i];
552
+ const a2 = polygonA[(i + 1) % polygonA.length];
553
+
554
+ for (let j = 0; j < polygonB.length; j++) {
555
+ const b1 = polygonB[j];
556
+ const b2 = polygonB[(j + 1) % polygonB.length];
557
+
558
+ if (segmentsIntersect(a1, a2, b1, b2)) {
559
+ return true;
560
+ }
561
+ }
562
+ }
563
+
564
+ return false;
565
+ }
566
+
567
+ /**
568
+ * Check if two line segments intersect.
569
+ *
570
+ * @param {{x: Decimal, y: Decimal}} a1 - First segment start
571
+ * @param {{x: Decimal, y: Decimal}} a2 - First segment end
572
+ * @param {{x: Decimal, y: Decimal}} b1 - Second segment start
573
+ * @param {{x: Decimal, y: Decimal}} b2 - Second segment end
574
+ * @returns {boolean} True if segments intersect
575
+ */
576
+ export function segmentsIntersect(a1, a2, b1, b2) {
577
+ const d1 = vectorSub(a2, a1);
578
+ const d2 = vectorSub(b2, b1);
579
+
580
+ const crossD = cross(d1, d2);
581
+
582
+ // Parallel segments
583
+ if (crossD.abs().lessThan(EPSILON)) {
584
+ // Check if collinear and overlapping
585
+ const d3 = vectorSub(b1, a1);
586
+ if (cross(d1, d3).abs().lessThan(EPSILON)) {
587
+ // Collinear - check overlap
588
+ const t0 = dot(d3, d1).div(dot(d1, d1));
589
+ const d4 = vectorSub(b2, a1);
590
+ const t1 = dot(d4, d1).div(dot(d1, d1));
591
+
592
+ const tMin = Decimal.min(t0, t1);
593
+ const tMax = Decimal.max(t0, t1);
594
+
595
+ return tMax.greaterThanOrEqualTo(0) && tMin.lessThanOrEqualTo(1);
596
+ }
597
+ return false;
598
+ }
599
+
600
+ // Non-parallel - find intersection parameter
601
+ const d3 = vectorSub(b1, a1);
602
+ const t = cross(d3, d2).div(crossD);
603
+ const u = cross(d3, d1).div(crossD);
604
+
605
+ // Check if intersection is within both segments
606
+ return t.greaterThanOrEqualTo(0) && t.lessThanOrEqualTo(1) &&
607
+ u.greaterThanOrEqualTo(0) && u.lessThanOrEqualTo(1);
608
+ }
609
+
610
+ // ============================================================================
611
+ // Distance Calculation (EPA - Expanding Polytope Algorithm)
612
+ // ============================================================================
613
+
614
+ /**
615
+ * Calculate the minimum distance between two non-intersecting convex polygons.
616
+ *
617
+ * Uses a modified GJK algorithm that returns the closest points
618
+ * when shapes don't intersect.
619
+ *
620
+ * @param {Array<{x: Decimal, y: Decimal}>} polygonA - First convex polygon
621
+ * @param {Array<{x: Decimal, y: Decimal}>} polygonB - Second convex polygon
622
+ * @returns {{distance: Decimal, closestA: {x: Decimal, y: Decimal}, closestB: {x: Decimal, y: Decimal}, verified: boolean}}
623
+ */
624
+ export function gjkDistance(polygonA, polygonB) {
625
+ // First check if they intersect
626
+ const intersection = gjkIntersects(polygonA, polygonB);
627
+
628
+ if (intersection.intersects) {
629
+ return {
630
+ distance: D(0),
631
+ closestA: centroid(polygonA),
632
+ closestB: centroid(polygonB),
633
+ verified: true
634
+ };
635
+ }
636
+
637
+ // Use the final simplex to find closest point to origin on Minkowski difference
638
+ // The closest point corresponds to the minimum distance
639
+
640
+ // For non-intersecting shapes, find the closest points by
641
+ // examining all vertex-edge pairs
642
+
643
+ let minDist = D(Infinity);
644
+ let closestA = polygonA[0];
645
+ let closestB = polygonB[0];
646
+
647
+ // Check all pairs of vertices
648
+ for (const pA of polygonA) {
649
+ for (const pB of polygonB) {
650
+ const dist = magnitude(vectorSub(pA, pB));
651
+ if (dist.lessThan(minDist)) {
652
+ minDist = dist;
653
+ closestA = pA;
654
+ closestB = pB;
655
+ }
656
+ }
657
+ }
658
+
659
+ // Check vertex-to-edge distances
660
+ for (const pA of polygonA) {
661
+ for (let i = 0; i < polygonB.length; i++) {
662
+ const e1 = polygonB[i];
663
+ const e2 = polygonB[(i + 1) % polygonB.length];
664
+ const closest = closestPointOnSegment(pA, e1, e2);
665
+ const dist = magnitude(vectorSub(pA, closest));
666
+ if (dist.lessThan(minDist)) {
667
+ minDist = dist;
668
+ closestA = pA;
669
+ closestB = closest;
670
+ }
671
+ }
672
+ }
673
+
674
+ for (const pB of polygonB) {
675
+ for (let i = 0; i < polygonA.length; i++) {
676
+ const e1 = polygonA[i];
677
+ const e2 = polygonA[(i + 1) % polygonA.length];
678
+ const closest = closestPointOnSegment(pB, e1, e2);
679
+ const dist = magnitude(vectorSub(pB, closest));
680
+ if (dist.lessThan(minDist)) {
681
+ minDist = dist;
682
+ closestA = closest;
683
+ closestB = pB;
684
+ }
685
+ }
686
+ }
687
+
688
+ // VERIFICATION: Check that the distance is correct
689
+ const verifiedDist = magnitude(vectorSub(closestA, closestB));
690
+ const verified = verifiedDist.minus(minDist).abs().lessThan(EPSILON);
691
+
692
+ return {
693
+ distance: minDist,
694
+ closestA,
695
+ closestB,
696
+ verified
697
+ };
698
+ }
699
+
700
+ /**
701
+ * Find the closest point on a line segment to a given point.
702
+ *
703
+ * @param {{x: Decimal, y: Decimal}} pt - Query point
704
+ * @param {{x: Decimal, y: Decimal}} a - Segment start
705
+ * @param {{x: Decimal, y: Decimal}} b - Segment end
706
+ * @returns {{x: Decimal, y: Decimal}} Closest point on segment
707
+ */
708
+ export function closestPointOnSegment(pt, a, b) {
709
+ const ab = vectorSub(b, a);
710
+ const ap = vectorSub(pt, a);
711
+
712
+ const abLengthSq = magnitudeSquared(ab);
713
+
714
+ if (abLengthSq.lessThan(EPSILON)) {
715
+ // Degenerate segment (a == b)
716
+ return a;
717
+ }
718
+
719
+ // Project pt onto line ab, clamped to [0, 1]
720
+ let t = dot(ap, ab).div(abLengthSq);
721
+ t = Decimal.max(D(0), Decimal.min(D(1), t));
722
+
723
+ return vectorAdd(a, vectorScale(ab, t));
724
+ }
725
+
726
+ // ============================================================================
727
+ // Convenience Functions
728
+ // ============================================================================
729
+
730
+ /**
731
+ * High-level function to check if two polygons overlap.
732
+ *
733
+ * Handles input normalization and provides a simple boolean result.
734
+ *
735
+ * @param {Array<{x: number|Decimal, y: number|Decimal}>} polygonA - First polygon
736
+ * @param {Array<{x: number|Decimal, y: number|Decimal}>} polygonB - Second polygon
737
+ * @returns {{overlaps: boolean, verified: boolean}}
738
+ */
739
+ export function polygonsOverlap(polygonA, polygonB) {
740
+ // Normalize input to Decimal
741
+ const normA = polygonA.map(p => point(p.x, p.y));
742
+ const normB = polygonB.map(p => point(p.x, p.y));
743
+
744
+ const result = gjkIntersects(normA, normB);
745
+
746
+ return {
747
+ overlaps: result.intersects,
748
+ verified: result.verified
749
+ };
750
+ }
751
+
752
+ /**
753
+ * Calculate the distance between two polygons.
754
+ *
755
+ * @param {Array<{x: number|Decimal, y: number|Decimal}>} polygonA - First polygon
756
+ * @param {Array<{x: number|Decimal, y: number|Decimal}>} polygonB - Second polygon
757
+ * @returns {{distance: Decimal, verified: boolean}}
758
+ */
759
+ export function polygonsDistance(polygonA, polygonB) {
760
+ // Normalize input to Decimal
761
+ const normA = polygonA.map(p => point(p.x, p.y));
762
+ const normB = polygonB.map(p => point(p.x, p.y));
763
+
764
+ const result = gjkDistance(normA, normB);
765
+
766
+ return {
767
+ distance: result.distance,
768
+ closestA: result.closestA,
769
+ closestB: result.closestB,
770
+ verified: result.verified
771
+ };
772
+ }
773
+
774
+ /**
775
+ * Check if a point is inside a convex polygon (convenience wrapper).
776
+ *
777
+ * @param {{x: number|Decimal, y: number|Decimal}} pt - Point to test
778
+ * @param {Array<{x: number|Decimal, y: number|Decimal}>} polygon - Polygon
779
+ * @returns {boolean} True if inside
780
+ */
781
+ export function isPointInPolygon(pt, polygon) {
782
+ const normPt = point(pt.x, pt.y);
783
+ const normPoly = polygon.map(p => point(p.x, p.y));
784
+
785
+ return pointInConvexPolygon(normPt, normPoly);
786
+ }
787
+
788
+ // ============================================================================
789
+ // Exports
790
+ // ============================================================================
791
+
792
+ export {
793
+ EPSILON,
794
+ MAX_ITERATIONS,
795
+ D
796
+ };
797
+
798
+ export default {
799
+ // Point/vector utilities
800
+ point,
801
+ vectorAdd,
802
+ vectorSub,
803
+ vectorNeg,
804
+ vectorScale,
805
+ dot,
806
+ cross,
807
+ magnitude,
808
+ magnitudeSquared,
809
+ normalize,
810
+ perpendicular,
811
+ tripleProduct,
812
+
813
+ // Support functions
814
+ supportPoint,
815
+ minkowskiSupport,
816
+
817
+ // Simplex operations
818
+ processLineSimplex,
819
+ processTriangleSimplex,
820
+
821
+ // GJK algorithm
822
+ gjkIntersects,
823
+ gjkDistance,
824
+
825
+ // Verification
826
+ centroid,
827
+ pointInConvexPolygon,
828
+ verifyIntersection,
829
+ segmentsIntersect,
830
+ closestPointOnSegment,
831
+
832
+ // Convenience functions
833
+ polygonsOverlap,
834
+ polygonsDistance,
835
+ isPointInPolygon,
836
+
837
+ // Constants
838
+ EPSILON,
839
+ MAX_ITERATIONS
840
+ };