@emasoft/svg-matrix 1.0.19 → 1.0.21
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.
- package/README.md +256 -759
- package/bin/svg-matrix.js +171 -2
- package/bin/svglinter.cjs +1162 -0
- package/package.json +9 -3
- package/src/animation-optimization.js +394 -0
- package/src/animation-references.js +440 -0
- package/src/arc-length.js +940 -0
- package/src/bezier-analysis.js +1626 -0
- package/src/bezier-intersections.js +1369 -0
- package/src/clip-path-resolver.js +110 -2
- package/src/convert-path-data.js +583 -0
- package/src/css-specificity.js +443 -0
- package/src/douglas-peucker.js +356 -0
- package/src/flatten-pipeline.js +109 -4
- package/src/geometry-to-path.js +126 -16
- package/src/gjk-collision.js +840 -0
- package/src/index.js +175 -2
- package/src/off-canvas-detection.js +1222 -0
- package/src/path-analysis.js +1241 -0
- package/src/path-data-plugins.js +928 -0
- package/src/path-optimization.js +825 -0
- package/src/path-simplification.js +1140 -0
- package/src/polygon-clip.js +376 -99
- package/src/svg-boolean-ops.js +898 -0
- package/src/svg-collections.js +910 -0
- package/src/svg-parser.js +175 -16
- package/src/svg-rendering-context.js +627 -0
- package/src/svg-toolbox.js +7495 -0
- package/src/svg-validation-data.js +944 -0
- package/src/transform-decomposition.js +810 -0
- package/src/transform-optimization.js +936 -0
- package/src/use-symbol-resolver.js +75 -7
|
@@ -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
|
+
};
|