@emasoft/svg-matrix 1.0.4 → 1.0.6

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,1491 @@
1
+ /**
2
+ * Polygon Boolean Operations with Arbitrary Precision
3
+ *
4
+ * A comprehensive library for 2D polygon operations using arbitrary-precision
5
+ * arithmetic via Decimal.js. Eliminates floating-point errors in geometric
6
+ * computations, making it suitable for CAD, GIS, and other applications
7
+ * requiring exact geometric calculations.
8
+ *
9
+ * ## Key Algorithms Implemented
10
+ *
11
+ * ### Sutherland-Hodgman Clipping
12
+ * Classic O(n) algorithm for clipping a polygon against a convex clipping
13
+ * window. Processes the subject polygon against each edge of the clipping
14
+ * polygon sequentially, outputting vertices that lie inside the half-plane
15
+ * defined by each edge.
16
+ *
17
+ * Reference: Sutherland, I. E., & Hodgman, G. W. (1974). "Reentrant polygon
18
+ * clipping." Communications of the ACM, 17(1), 32-42.
19
+ *
20
+ * ### Graham Scan (Convex Hull)
21
+ * Efficient O(n log n) algorithm for computing the convex hull of a point set.
22
+ * Sorts points by polar angle from a pivot (lowest point), then uses a stack
23
+ * to maintain hull vertices, removing points that create right turns.
24
+ *
25
+ * Reference: Graham, R. L. (1972). "An efficient algorithm for determining
26
+ * the convex hull of a finite planar set." Information Processing Letters, 1(4).
27
+ *
28
+ * ### Winding Number (Point in Polygon)
29
+ * Robust point-in-polygon test using winding number algorithm. Counts how many
30
+ * times the polygon winds around the test point by tracking upward and downward
31
+ * edge crossings of a horizontal ray cast from the point.
32
+ *
33
+ * ### Shoelace Formula (Polygon Area)
34
+ * Also known as surveyor's formula or Gauss's area formula. Computes signed
35
+ * polygon area in O(n) time using only vertex coordinates. Derived from
36
+ * Green's theorem applied to the polygon boundary.
37
+ *
38
+ * ### Boolean Operations
39
+ * Simplified polygon intersection, union, and difference operations. Uses
40
+ * point collection and convex hull for general polygons. For exact results
41
+ * with complex concave polygons, consider full implementations like:
42
+ * - Greiner-Hormann clipping
43
+ * - Martinez-Rueda-Feito algorithm
44
+ * - Vatti clipping algorithm
45
+ *
46
+ * ## Usage Examples
47
+ *
48
+ * @example
49
+ * // Create points with arbitrary precision
50
+ * import { point, polygonArea, polygonIntersection } from './polygon-clip.js';
51
+ *
52
+ * const square = [
53
+ * point('0', '0'),
54
+ * point('10', '0'),
55
+ * point('10', '10'),
56
+ * point('0', '10')
57
+ * ];
58
+ *
59
+ * // Compute area (returns Decimal with value 100)
60
+ * const area = polygonArea(square);
61
+ *
62
+ * @example
63
+ * // Boolean intersection
64
+ * const square1 = [point(0,0), point(2,0), point(2,2), point(0,2)];
65
+ * const square2 = [point(1,1), point(3,1), point(3,3), point(1,3)];
66
+ * const intersection = polygonIntersection(square1, square2);
67
+ * // Returns [point(1,1), point(2,1), point(2,2), point(1,2)]
68
+ *
69
+ * @example
70
+ * // Convex hull of point cloud
71
+ * const points = [point(0,0), point(2,1), point(1,2), point(1,1)];
72
+ * const hull = convexHull(points);
73
+ * // Returns vertices of minimal convex polygon containing all points
74
+ *
75
+ * ## Precision Configuration
76
+ *
77
+ * This module sets Decimal.js precision to 80 decimal places and uses an
78
+ * EPSILON threshold of 1e-40 for near-zero comparisons. These can be
79
+ * adjusted based on application requirements.
80
+ *
81
+ * ## Coordinate System
82
+ *
83
+ * - Counter-clockwise (CCW) vertex order is considered positive orientation
84
+ * - Cross product > 0 indicates left turn (CCW)
85
+ * - Cross product < 0 indicates right turn (CW)
86
+ * - Y-axis points upward (standard mathematical convention)
87
+ *
88
+ * @module polygon-clip
89
+ */
90
+
91
+ import Decimal from 'decimal.js';
92
+
93
+ // Set high precision for all calculations
94
+ Decimal.set({ precision: 80 });
95
+
96
+ // Helper to convert to Decimal
97
+ const D = x => (x instanceof Decimal ? x : new Decimal(x));
98
+
99
+ // Near-zero threshold for comparisons
100
+ const EPSILON = new Decimal('1e-40');
101
+
102
+ // ============================================================================
103
+ // Point and Vector Primitives
104
+ // ============================================================================
105
+
106
+ /**
107
+ * Create a point with arbitrary-precision Decimal coordinates.
108
+ *
109
+ * This is the fundamental building block for all polygon operations in this library.
110
+ * Accepts numbers, strings, or Decimal instances and converts them to high-precision
111
+ * Decimal coordinates to avoid floating-point errors in geometric calculations.
112
+ *
113
+ * @param {number|string|Decimal} x - The x-coordinate (will be converted to Decimal)
114
+ * @param {number|string|Decimal} y - The y-coordinate (will be converted to Decimal)
115
+ * @returns {{x: Decimal, y: Decimal}} Point object with Decimal coordinates
116
+ *
117
+ * @example
118
+ * // Create a point from numbers
119
+ * const p1 = point(3, 4);
120
+ *
121
+ * @example
122
+ * // Create a point from strings for exact precision
123
+ * const p2 = point('0.1', '0.2'); // Avoids 0.1 + 0.2 != 0.3 issues
124
+ *
125
+ * @example
126
+ * // Create a point from existing Decimals
127
+ * const p3 = point(new Decimal('1.5'), new Decimal('2.5'));
128
+ */
129
+ export function point(x, y) {
130
+ return { x: D(x), y: D(y) };
131
+ }
132
+
133
+ /**
134
+ * Check if two points are equal within a specified tolerance.
135
+ *
136
+ * Uses the absolute difference between coordinates to determine equality.
137
+ * This is essential for geometric algorithms where exact equality is rarely
138
+ * achievable due to numerical computation.
139
+ *
140
+ * @param {Object} p1 - First point with {x: Decimal, y: Decimal}
141
+ * @param {Object} p2 - Second point with {x: Decimal, y: Decimal}
142
+ * @param {Decimal} [tolerance=EPSILON] - Maximum allowed difference (default: 1e-40)
143
+ * @returns {boolean} True if points are within tolerance of each other
144
+ *
145
+ * @example
146
+ * const p1 = point(1, 2);
147
+ * const p2 = point(1.0000000001, 2.0000000001);
148
+ * pointsEqual(p1, p2); // true (within default tolerance)
149
+ *
150
+ * @example
151
+ * const p1 = point(0, 0);
152
+ * const p2 = point(1, 1);
153
+ * pointsEqual(p1, p2); // false
154
+ */
155
+ export function pointsEqual(p1, p2, tolerance = EPSILON) {
156
+ return p1.x.minus(p2.x).abs().lt(tolerance) &&
157
+ p1.y.minus(p2.y).abs().lt(tolerance);
158
+ }
159
+
160
+ /**
161
+ * Compute the 2D cross product (z-component of 3D cross product).
162
+ *
163
+ * The cross product is fundamental for determining orientation and turns in 2D geometry.
164
+ * It computes the signed area of the parallelogram formed by vectors (o→a) and (o→b).
165
+ *
166
+ * Algorithm:
167
+ * - Forms vectors v1 = a - o and v2 = b - o
168
+ * - Returns v1.x * v2.y - v1.y * v2.x
169
+ * - This is equivalent to |v1| * |v2| * sin(θ) where θ is the angle from v1 to v2
170
+ *
171
+ * @param {Object} o - Origin point with {x: Decimal, y: Decimal}
172
+ * @param {Object} a - First point with {x: Decimal, y: Decimal}
173
+ * @param {Object} b - Second point with {x: Decimal, y: Decimal}
174
+ * @returns {Decimal} Positive if b is left of o→a (counter-clockwise turn),
175
+ * negative if right (clockwise turn),
176
+ * zero if points are collinear
177
+ *
178
+ * @example
179
+ * // Counter-clockwise turn (left turn)
180
+ * const o = point(0, 0);
181
+ * const a = point(1, 0);
182
+ * const b = point(0, 1);
183
+ * cross(o, a, b); // > 0 (CCW)
184
+ *
185
+ * @example
186
+ * // Clockwise turn (right turn)
187
+ * const o = point(0, 0);
188
+ * const a = point(0, 1);
189
+ * const b = point(1, 0);
190
+ * cross(o, a, b); // < 0 (CW)
191
+ *
192
+ * @example
193
+ * // Collinear points
194
+ * const o = point(0, 0);
195
+ * const a = point(1, 1);
196
+ * const b = point(2, 2);
197
+ * cross(o, a, b); // ≈ 0
198
+ */
199
+ export function cross(o, a, b) {
200
+ const ax = a.x.minus(o.x);
201
+ const ay = a.y.minus(o.y);
202
+ const bx = b.x.minus(o.x);
203
+ const by = b.y.minus(o.y);
204
+ return ax.mul(by).minus(ay.mul(bx));
205
+ }
206
+
207
+ /**
208
+ * Compute the dot product of vectors (o→a) and (o→b).
209
+ *
210
+ * The dot product measures how much two vectors point in the same direction.
211
+ * It's used for angle calculations and projections.
212
+ *
213
+ * Algorithm:
214
+ * - Forms vectors v1 = a - o and v2 = b - o
215
+ * - Returns v1.x * v2.x + v1.y * v2.y
216
+ * - This is equivalent to |v1| * |v2| * cos(θ) where θ is the angle between vectors
217
+ *
218
+ * @param {Object} o - Origin point with {x: Decimal, y: Decimal}
219
+ * @param {Object} a - First point with {x: Decimal, y: Decimal}
220
+ * @param {Object} b - Second point with {x: Decimal, y: Decimal}
221
+ * @returns {Decimal} Dot product value
222
+ * Positive if angle < 90°,
223
+ * zero if perpendicular,
224
+ * negative if angle > 90°
225
+ *
226
+ * @example
227
+ * // Parallel vectors (same direction)
228
+ * const o = point(0, 0);
229
+ * const a = point(1, 0);
230
+ * const b = point(2, 0);
231
+ * dot(o, a, b); // > 0
232
+ *
233
+ * @example
234
+ * // Perpendicular vectors
235
+ * const o = point(0, 0);
236
+ * const a = point(1, 0);
237
+ * const b = point(0, 1);
238
+ * dot(o, a, b); // = 0
239
+ *
240
+ * @example
241
+ * // Opposite direction vectors
242
+ * const o = point(0, 0);
243
+ * const a = point(1, 0);
244
+ * const b = point(-1, 0);
245
+ * dot(o, a, b); // < 0
246
+ */
247
+ export function dot(o, a, b) {
248
+ const ax = a.x.minus(o.x);
249
+ const ay = a.y.minus(o.y);
250
+ const bx = b.x.minus(o.x);
251
+ const by = b.y.minus(o.y);
252
+ return ax.mul(bx).plus(ay.mul(by));
253
+ }
254
+
255
+ /**
256
+ * Determine the sign of a value with tolerance.
257
+ *
258
+ * Returns -1, 0, or 1 to indicate negative, near-zero, or positive values.
259
+ * Uses EPSILON threshold to treat very small values as zero, avoiding
260
+ * numerical precision issues in geometric tests.
261
+ *
262
+ * @param {Decimal} val - Value to test
263
+ * @returns {number} -1 if val < -EPSILON,
264
+ * 0 if |val| <= EPSILON,
265
+ * 1 if val > EPSILON
266
+ *
267
+ * @example
268
+ * sign(new Decimal('-5')); // -1
269
+ * sign(new Decimal('1e-50')); // 0 (within tolerance)
270
+ * sign(new Decimal('5')); // 1
271
+ */
272
+ export function sign(val) {
273
+ if (val.abs().lt(EPSILON)) return 0;
274
+ return val.lt(0) ? -1 : 1;
275
+ }
276
+
277
+ // ============================================================================
278
+ // Segment Intersection
279
+ // ============================================================================
280
+
281
+ /**
282
+ * Compute the intersection point of two line segments using parametric form.
283
+ *
284
+ * Algorithm:
285
+ * - Represents segments parametrically:
286
+ * Segment 1: P(t) = a + t(b - a) for t ∈ [0, 1]
287
+ * Segment 2: Q(s) = c + s(d - c) for s ∈ [0, 1]
288
+ * - Solves P(t) = Q(s) for parameters t and s
289
+ * - Uses Cramer's rule with determinant denom = (b-a) × (d-c)
290
+ * - Returns null if segments are parallel (denom ≈ 0)
291
+ * - Returns null if t or s are outside [0, 1] (intersection beyond segment endpoints)
292
+ *
293
+ * @param {Object} a - Start point of first segment with {x: Decimal, y: Decimal}
294
+ * @param {Object} b - End point of first segment with {x: Decimal, y: Decimal}
295
+ * @param {Object} c - Start point of second segment with {x: Decimal, y: Decimal}
296
+ * @param {Object} d - End point of second segment with {x: Decimal, y: Decimal}
297
+ * @returns {Object|null} Intersection point {x: Decimal, y: Decimal, t: Decimal, s: Decimal}
298
+ * where t is parameter on first segment, s on second segment,
299
+ * or null if segments don't intersect
300
+ *
301
+ * @example
302
+ * // Intersecting segments
303
+ * const a = point(0, 0);
304
+ * const b = point(2, 2);
305
+ * const c = point(0, 2);
306
+ * const d = point(2, 0);
307
+ * const intersection = segmentIntersection(a, b, c, d);
308
+ * // Returns {x: 1, y: 1, t: 0.5, s: 0.5}
309
+ *
310
+ * @example
311
+ * // Non-intersecting segments
312
+ * const a = point(0, 0);
313
+ * const b = point(1, 0);
314
+ * const c = point(0, 1);
315
+ * const d = point(1, 1);
316
+ * segmentIntersection(a, b, c, d); // null
317
+ *
318
+ * @example
319
+ * // Parallel segments
320
+ * const a = point(0, 0);
321
+ * const b = point(1, 0);
322
+ * const c = point(0, 1);
323
+ * const d = point(1, 1);
324
+ * segmentIntersection(a, b, c, d); // null
325
+ */
326
+ export function segmentIntersection(a, b, c, d) {
327
+ // Direction vectors
328
+ const dx1 = b.x.minus(a.x);
329
+ const dy1 = b.y.minus(a.y);
330
+ const dx2 = d.x.minus(c.x);
331
+ const dy2 = d.y.minus(c.y);
332
+
333
+ // Cross product of directions (determinant)
334
+ const denom = dx1.mul(dy2).minus(dy1.mul(dx2));
335
+
336
+ // Check if lines are parallel
337
+ if (denom.abs().lt(EPSILON)) {
338
+ return null; // Parallel or collinear
339
+ }
340
+
341
+ // Vector from a to c
342
+ const dx3 = c.x.minus(a.x);
343
+ const dy3 = c.y.minus(a.y);
344
+
345
+ // Parametric values
346
+ const t = dx3.mul(dy2).minus(dy3.mul(dx2)).div(denom);
347
+ const s = dx3.mul(dy1).minus(dy3.mul(dx1)).div(denom);
348
+
349
+ // Check if intersection is within both segments [0, 1]
350
+ const zero = D(0);
351
+ const one = D(1);
352
+
353
+ if (t.gte(zero) && t.lte(one) && s.gte(zero) && s.lte(one)) {
354
+ return {
355
+ x: a.x.plus(dx1.mul(t)),
356
+ y: a.y.plus(dy1.mul(t)),
357
+ t: t,
358
+ s: s
359
+ };
360
+ }
361
+
362
+ return null;
363
+ }
364
+
365
+ /**
366
+ * Compute intersection of an infinite line with a finite segment.
367
+ *
368
+ * Unlike segmentIntersection, this treats the first input as an infinite line
369
+ * (extending beyond lineA and lineB) while the second input is a bounded segment.
370
+ * Used primarily in clipping algorithms where edges define infinite half-planes.
371
+ *
372
+ * Algorithm:
373
+ * - Line defined by two points: extends infinitely through lineA and lineB
374
+ * - Segment bounded: only between segA and segB
375
+ * - Computes parameter s ∈ [0, 1] for the segment
376
+ * - Returns intersection if s is valid, null otherwise
377
+ *
378
+ * @param {Object} lineA - First point defining the infinite line
379
+ * @param {Object} lineB - Second point defining the infinite line
380
+ * @param {Object} segA - Start point of the bounded segment
381
+ * @param {Object} segB - End point of the bounded segment
382
+ * @returns {Object|null} Intersection point {x: Decimal, y: Decimal, s: Decimal}
383
+ * where s is the parameter on the segment,
384
+ * or null if no intersection exists
385
+ *
386
+ * @example
387
+ * // Line intersects segment
388
+ * const lineA = point(0, 0);
389
+ * const lineB = point(1, 1);
390
+ * const segA = point(0, 1);
391
+ * const segB = point(1, 0);
392
+ * lineSegmentIntersection(lineA, lineB, segA, segB);
393
+ * // Returns intersection point
394
+ *
395
+ * @example
396
+ * // Line parallel to segment
397
+ * const lineA = point(0, 0);
398
+ * const lineB = point(1, 0);
399
+ * const segA = point(0, 1);
400
+ * const segB = point(1, 1);
401
+ * lineSegmentIntersection(lineA, lineB, segA, segB); // null
402
+ */
403
+ export function lineSegmentIntersection(lineA, lineB, segA, segB) {
404
+ const dx1 = lineB.x.minus(lineA.x);
405
+ const dy1 = lineB.y.minus(lineA.y);
406
+ const dx2 = segB.x.minus(segA.x);
407
+ const dy2 = segB.y.minus(segA.y);
408
+
409
+ const denom = dx1.mul(dy2).minus(dy1.mul(dx2));
410
+
411
+ if (denom.abs().lt(EPSILON)) {
412
+ return null;
413
+ }
414
+
415
+ const dx3 = segA.x.minus(lineA.x);
416
+ const dy3 = segA.y.minus(lineA.y);
417
+
418
+ const s = dx3.mul(dy1).minus(dy3.mul(dx1)).div(denom);
419
+
420
+ if (s.gte(0) && s.lte(1)) {
421
+ return {
422
+ x: segA.x.plus(dx2.mul(s)),
423
+ y: segA.y.plus(dy2.mul(s)),
424
+ s: s
425
+ };
426
+ }
427
+
428
+ return null;
429
+ }
430
+
431
+ // ============================================================================
432
+ // Point in Polygon (Ray Casting)
433
+ // ============================================================================
434
+
435
+ /**
436
+ * Test if a point is inside a polygon using the winding number algorithm.
437
+ *
438
+ * This implementation uses a modified ray casting approach that computes the
439
+ * winding number - the number of times the polygon winds around the point.
440
+ * For simple (non-self-intersecting) polygons:
441
+ * - winding = 0 means outside
442
+ * - winding ≠ 0 means inside
443
+ *
444
+ * Algorithm (Winding Number):
445
+ * 1. Cast a horizontal ray from the test point to the right (+x direction)
446
+ * 2. Count upward edge crossings as +1
447
+ * 3. Count downward edge crossings as -1
448
+ * 4. Use cross product to determine if crossing is left of the point
449
+ * 5. Return winding number (non-zero = inside, zero = outside)
450
+ *
451
+ * This is more robust than even-odd ray casting for complex polygons.
452
+ *
453
+ * @param {Object} pt - Point to test with {x: Decimal, y: Decimal}
454
+ * @param {Array} polygon - Array of polygon vertices [{x, y}, ...]
455
+ * @returns {number} 1 if point is strictly inside,
456
+ * 0 if point is on the boundary,
457
+ * -1 if point is strictly outside
458
+ *
459
+ * @example
460
+ * // Point inside square
461
+ * const square = [point(0,0), point(2,0), point(2,2), point(0,2)];
462
+ * pointInPolygon(point(1, 1), square); // 1 (inside)
463
+ *
464
+ * @example
465
+ * // Point outside square
466
+ * pointInPolygon(point(3, 3), square); // -1 (outside)
467
+ *
468
+ * @example
469
+ * // Point on edge
470
+ * pointInPolygon(point(1, 0), square); // 0 (on boundary)
471
+ *
472
+ * @example
473
+ * // Concave polygon
474
+ * const concave = [point(0,0), point(4,0), point(4,4), point(2,2), point(0,4)];
475
+ * pointInPolygon(point(3, 2), concave); // 1 (inside concave region)
476
+ */
477
+ export function pointInPolygon(pt, polygon) {
478
+ const n = polygon.length;
479
+ if (n < 3) return -1;
480
+
481
+ let winding = 0;
482
+
483
+ for (let i = 0; i < n; i++) {
484
+ const p1 = polygon[i];
485
+ const p2 = polygon[(i + 1) % n];
486
+
487
+ // Check if point is on the edge
488
+ if (pointOnSegment(pt, p1, p2)) {
489
+ return 0; // On boundary
490
+ }
491
+
492
+ // Ray casting from pt going right (+x direction)
493
+ if (p1.y.lte(pt.y)) {
494
+ if (p2.y.gt(pt.y)) {
495
+ // Upward crossing
496
+ if (cross(p1, p2, pt).gt(0)) {
497
+ winding++;
498
+ }
499
+ }
500
+ } else {
501
+ if (p2.y.lte(pt.y)) {
502
+ // Downward crossing
503
+ if (cross(p1, p2, pt).lt(0)) {
504
+ winding--;
505
+ }
506
+ }
507
+ }
508
+ }
509
+
510
+ return winding !== 0 ? 1 : -1;
511
+ }
512
+
513
+ /**
514
+ * Check if a point lies on a line segment.
515
+ *
516
+ * Uses two tests:
517
+ * 1. Collinearity: cross product must be near zero
518
+ * 2. Bounding box: point must be within segment's axis-aligned bounding box
519
+ *
520
+ * Algorithm:
521
+ * - Compute cross product to test collinearity
522
+ * - If collinear, check if point is between segment endpoints
523
+ * - Uses bounding box test for efficiency
524
+ *
525
+ * @param {Object} pt - Point to test with {x: Decimal, y: Decimal}
526
+ * @param {Object} a - Segment start point with {x: Decimal, y: Decimal}
527
+ * @param {Object} b - Segment end point with {x: Decimal, y: Decimal}
528
+ * @returns {boolean} True if point lies on the segment (within EPSILON tolerance)
529
+ *
530
+ * @example
531
+ * // Point on segment
532
+ * const a = point(0, 0);
533
+ * const b = point(2, 2);
534
+ * const pt = point(1, 1);
535
+ * pointOnSegment(pt, a, b); // true
536
+ *
537
+ * @example
538
+ * // Point on line but not segment
539
+ * const pt2 = point(3, 3);
540
+ * pointOnSegment(pt2, a, b); // false (beyond endpoint)
541
+ *
542
+ * @example
543
+ * // Point not on line
544
+ * const pt3 = point(1, 2);
545
+ * pointOnSegment(pt3, a, b); // false (not collinear)
546
+ */
547
+ export function pointOnSegment(pt, a, b) {
548
+ // Check collinearity
549
+ const crossVal = cross(a, b, pt);
550
+ if (crossVal.abs().gt(EPSILON)) {
551
+ return false;
552
+ }
553
+
554
+ // Check if pt is between a and b
555
+ const minX = Decimal.min(a.x, b.x);
556
+ const maxX = Decimal.max(a.x, b.x);
557
+ const minY = Decimal.min(a.y, b.y);
558
+ const maxY = Decimal.max(a.y, b.y);
559
+
560
+ return pt.x.gte(minX.minus(EPSILON)) && pt.x.lte(maxX.plus(EPSILON)) &&
561
+ pt.y.gte(minY.minus(EPSILON)) && pt.y.lte(maxY.plus(EPSILON));
562
+ }
563
+
564
+ // ============================================================================
565
+ // Sutherland-Hodgman Algorithm (Convex Polygon Clipping)
566
+ // ============================================================================
567
+
568
+ /**
569
+ * Clip a polygon against a convex clipping polygon using the Sutherland-Hodgman algorithm.
570
+ *
571
+ * The Sutherland-Hodgman algorithm is a classic polygon clipping technique that works
572
+ * efficiently for convex clipping windows. It processes the subject polygon against
573
+ * each edge of the clipping polygon sequentially.
574
+ *
575
+ * Algorithm (Sutherland-Hodgman):
576
+ * 1. Initialize output with subject polygon vertices
577
+ * 2. For each edge of the clipping polygon:
578
+ * a. Create a temporary input list from current output
579
+ * b. Clear output list
580
+ * c. For each edge in the input polygon:
581
+ * - If current vertex is inside: output it (and intersection if entering)
582
+ * - If current vertex is outside but previous was inside: output intersection
583
+ * d. Replace output with new list
584
+ * 3. Return final output polygon
585
+ *
586
+ * "Inside" is defined as being on the left side (or on) the directed clipping edge
587
+ * for counter-clockwise oriented polygons.
588
+ *
589
+ * Limitations:
590
+ * - Clipping polygon MUST be convex
591
+ * - Clipping polygon vertices MUST be in counter-clockwise order
592
+ * - Does not handle holes or self-intersecting polygons
593
+ *
594
+ * @param {Array} subject - Subject polygon vertices [{x, y}, ...] (can be convex or concave)
595
+ * @param {Array} clip - Clipping polygon vertices (MUST be convex, CCW order)
596
+ * @returns {Array} Clipped polygon vertices in CCW order, or empty array if no intersection
597
+ *
598
+ * @example
599
+ * // Clip a square against a triangle
600
+ * const subject = [point(0,0), point(4,0), point(4,4), point(0,4)];
601
+ * const clip = [point(1,1), point(5,1), point(1,5)];
602
+ * const result = clipPolygonSH(subject, clip);
603
+ * // Returns clipped polygon vertices
604
+ *
605
+ * @example
606
+ * // No intersection case
607
+ * const subject = [point(0,0), point(1,0), point(1,1), point(0,1)];
608
+ * const clip = [point(10,10), point(11,10), point(11,11), point(10,11)];
609
+ * clipPolygonSH(subject, clip); // []
610
+ */
611
+ export function clipPolygonSH(subject, clip) {
612
+ if (subject.length < 3 || clip.length < 3) {
613
+ return [];
614
+ }
615
+
616
+ // Convert all points to Decimal
617
+ let output = subject.map(p => point(p.x, p.y));
618
+ const clipPoly = clip.map(p => point(p.x, p.y));
619
+
620
+ // Clip against each edge of the clipping polygon
621
+ for (let i = 0; i < clipPoly.length; i++) {
622
+ if (output.length === 0) {
623
+ return [];
624
+ }
625
+
626
+ const clipEdgeStart = clipPoly[i];
627
+ const clipEdgeEnd = clipPoly[(i + 1) % clipPoly.length];
628
+
629
+ const input = output;
630
+ output = [];
631
+
632
+ for (let j = 0; j < input.length; j++) {
633
+ const current = input[j];
634
+ const prev = input[(j + input.length - 1) % input.length];
635
+
636
+ const currentInside = isInsideEdge(current, clipEdgeStart, clipEdgeEnd);
637
+ const prevInside = isInsideEdge(prev, clipEdgeStart, clipEdgeEnd);
638
+
639
+ if (currentInside) {
640
+ if (!prevInside) {
641
+ // Entering: add intersection point
642
+ const intersection = lineIntersection(prev, current, clipEdgeStart, clipEdgeEnd);
643
+ if (intersection) {
644
+ output.push(intersection);
645
+ }
646
+ }
647
+ output.push(current);
648
+ } else if (prevInside) {
649
+ // Leaving: add intersection point
650
+ const intersection = lineIntersection(prev, current, clipEdgeStart, clipEdgeEnd);
651
+ if (intersection) {
652
+ output.push(intersection);
653
+ }
654
+ }
655
+ }
656
+ }
657
+
658
+ return output;
659
+ }
660
+
661
+ /**
662
+ * Check if a point is inside (left of) a directed edge.
663
+ *
664
+ * Used by Sutherland-Hodgman algorithm. For counter-clockwise oriented
665
+ * polygons, "inside" means on the left side of the directed edge.
666
+ *
667
+ * @private
668
+ * @param {Object} pt - Point to test
669
+ * @param {Object} edgeStart - Edge start point
670
+ * @param {Object} edgeEnd - Edge end point
671
+ * @returns {boolean} True if point is on left side or on the edge
672
+ */
673
+ function isInsideEdge(pt, edgeStart, edgeEnd) {
674
+ // Point is "inside" if it's on the left side of the edge (CCW polygon)
675
+ return cross(edgeStart, edgeEnd, pt).gte(0);
676
+ }
677
+
678
+ /**
679
+ * Compute intersection of two infinite lines (not segments).
680
+ *
681
+ * Unlike segment intersection, this doesn't check parameter bounds.
682
+ * Used by Sutherland-Hodgman when clipping against infinite half-planes.
683
+ *
684
+ * @private
685
+ * @param {Object} a - First point on first line
686
+ * @param {Object} b - Second point on first line
687
+ * @param {Object} c - First point on second line
688
+ * @param {Object} d - Second point on second line
689
+ * @returns {Object|null} Intersection point {x, y} or null if parallel
690
+ */
691
+ function lineIntersection(a, b, c, d) {
692
+ const dx1 = b.x.minus(a.x);
693
+ const dy1 = b.y.minus(a.y);
694
+ const dx2 = d.x.minus(c.x);
695
+ const dy2 = d.y.minus(c.y);
696
+
697
+ const denom = dx1.mul(dy2).minus(dy1.mul(dx2));
698
+
699
+ if (denom.abs().lt(EPSILON)) {
700
+ return null;
701
+ }
702
+
703
+ const dx3 = c.x.minus(a.x);
704
+ const dy3 = c.y.minus(a.y);
705
+
706
+ const t = dx3.mul(dy2).minus(dy3.mul(dx2)).div(denom);
707
+
708
+ return {
709
+ x: a.x.plus(dx1.mul(t)),
710
+ y: a.y.plus(dy1.mul(t))
711
+ };
712
+ }
713
+
714
+ // ============================================================================
715
+ // Polygon Area and Orientation
716
+ // ============================================================================
717
+
718
+ /**
719
+ * Compute the signed area of a polygon using the Shoelace formula.
720
+ *
721
+ * The Shoelace formula (also known as surveyor's formula or Gauss's area formula)
722
+ * efficiently computes polygon area using only vertex coordinates. The sign of
723
+ * the area indicates polygon orientation.
724
+ *
725
+ * Algorithm (Shoelace Formula):
726
+ * 1. For each edge (i, i+1), compute: x[i] * y[i+1] - x[i+1] * y[i]
727
+ * 2. Sum all edge contributions
728
+ * 3. Divide by 2 to get signed area
729
+ *
730
+ * The formula comes from Green's theorem applied to the region enclosed by the polygon.
731
+ *
732
+ * @param {Array} polygon - Array of polygon vertices [{x, y}, ...]
733
+ * @returns {Decimal} Signed area of the polygon
734
+ * Positive if vertices are in counter-clockwise order
735
+ * Negative if vertices are in clockwise order
736
+ * Zero if polygon is degenerate (< 3 vertices or collinear)
737
+ *
738
+ * @example
739
+ * // Counter-clockwise square (area = 4)
740
+ * const square = [point(0,0), point(2,0), point(2,2), point(0,2)];
741
+ * polygonArea(square); // 4
742
+ *
743
+ * @example
744
+ * // Clockwise square (area = -4)
745
+ * const squareCW = [point(0,0), point(0,2), point(2,2), point(2,0)];
746
+ * polygonArea(squareCW); // -4
747
+ *
748
+ * @example
749
+ * // Triangle
750
+ * const triangle = [point(0,0), point(3,0), point(0,4)];
751
+ * polygonArea(triangle); // 6
752
+ */
753
+ export function polygonArea(polygon) {
754
+ const n = polygon.length;
755
+ if (n < 3) return D(0);
756
+
757
+ let area = D(0);
758
+ for (let i = 0; i < n; i++) {
759
+ const p1 = polygon[i];
760
+ const p2 = polygon[(i + 1) % n];
761
+ area = area.plus(p1.x.mul(p2.y).minus(p2.x.mul(p1.y)));
762
+ }
763
+
764
+ return area.div(2);
765
+ }
766
+
767
+ /**
768
+ * Check if polygon vertices are in counter-clockwise order.
769
+ *
770
+ * Uses the signed area to determine orientation. A positive area indicates
771
+ * counter-clockwise (CCW) orientation, which is the standard for many
772
+ * geometric algorithms.
773
+ *
774
+ * @param {Array} polygon - Array of polygon vertices [{x, y}, ...]
775
+ * @returns {boolean} True if vertices are in counter-clockwise order
776
+ *
777
+ * @example
778
+ * const ccw = [point(0,0), point(1,0), point(0,1)];
779
+ * isCounterClockwise(ccw); // true
780
+ *
781
+ * @example
782
+ * const cw = [point(0,0), point(0,1), point(1,0)];
783
+ * isCounterClockwise(cw); // false
784
+ */
785
+ export function isCounterClockwise(polygon) {
786
+ return polygonArea(polygon).gt(0);
787
+ }
788
+
789
+ /**
790
+ * Reverse the order of polygon vertices.
791
+ *
792
+ * This effectively flips the orientation from CCW to CW or vice versa.
793
+ * Useful for ensuring consistent winding order in boolean operations.
794
+ *
795
+ * @param {Array} polygon - Array of polygon vertices [{x, y}, ...]
796
+ * @returns {Array} New array with vertices in reverse order
797
+ *
798
+ * @example
799
+ * const poly = [point(0,0), point(1,0), point(1,1)];
800
+ * const reversed = reversePolygon(poly);
801
+ * // reversed = [point(1,1), point(1,0), point(0,0)]
802
+ */
803
+ export function reversePolygon(polygon) {
804
+ return [...polygon].reverse();
805
+ }
806
+
807
+ /**
808
+ * Ensure polygon vertices are in counter-clockwise order.
809
+ *
810
+ * Many geometric algorithms require CCW orientation. This function
811
+ * checks the current orientation and reverses if necessary.
812
+ *
813
+ * @param {Array} polygon - Array of polygon vertices [{x, y}, ...]
814
+ * @returns {Array} Polygon with vertices in CCW order (may be a new array or the original)
815
+ *
816
+ * @example
817
+ * const cw = [point(0,0), point(0,1), point(1,0)];
818
+ * const ccw = ensureCCW(cw);
819
+ * isCounterClockwise(ccw); // true
820
+ */
821
+ export function ensureCCW(polygon) {
822
+ if (!isCounterClockwise(polygon)) {
823
+ return reversePolygon(polygon);
824
+ }
825
+ return polygon;
826
+ }
827
+
828
+ // ============================================================================
829
+ // Convex Hull (Graham Scan)
830
+ // ============================================================================
831
+
832
+ /**
833
+ * Compute the convex hull of a set of points using the Graham scan algorithm.
834
+ *
835
+ * The Graham scan is an efficient O(n log n) algorithm for computing the convex hull
836
+ * of a point set. The convex hull is the smallest convex polygon containing all points.
837
+ *
838
+ * Algorithm (Graham Scan):
839
+ * 1. Find the lowest point (leftmost if tie) - this is the pivot
840
+ * 2. Sort all other points by polar angle with respect to the pivot
841
+ * 3. For ties in angle, sort by distance (closer points first)
842
+ * 4. Initialize stack with pivot point
843
+ * 5. For each sorted point:
844
+ * a. While stack has 2+ points and last 3 points make a right turn (CW):
845
+ * - Pop from stack (this point is interior, not on hull)
846
+ * b. Push current point onto stack
847
+ * 6. Stack contains convex hull vertices in CCW order
848
+ *
849
+ * The cross product test determines turns:
850
+ * - cross > 0: left turn (CCW) - keep the point
851
+ * - cross ≤ 0: right turn (CW) or collinear - remove previous point
852
+ *
853
+ * @param {Array} points - Array of input points [{x, y}, ...]
854
+ * @returns {Array} Convex hull vertices in counter-clockwise order
855
+ *
856
+ * @example
857
+ * // Square with interior point
858
+ * const points = [
859
+ * point(0,0), point(2,0), point(2,2), point(0,2),
860
+ * point(1,1) // interior point
861
+ * ];
862
+ * const hull = convexHull(points);
863
+ * // Returns [point(0,0), point(2,0), point(2,2), point(0,2)]
864
+ *
865
+ * @example
866
+ * // Collinear points
867
+ * const collinear = [point(0,0), point(1,1), point(2,2), point(3,3)];
868
+ * const hull = convexHull(collinear);
869
+ * // Returns [point(0,0), point(3,3)] (or similar minimal hull)
870
+ *
871
+ * @example
872
+ * // Random point cloud
873
+ * const cloud = [point(1,1), point(3,2), point(0,0), point(2,4), point(4,0)];
874
+ * const hull = convexHull(cloud);
875
+ * // Returns vertices of convex boundary in CCW order
876
+ */
877
+ export function convexHull(points) {
878
+ if (points.length < 3) {
879
+ return points.map(p => point(p.x, p.y));
880
+ }
881
+
882
+ // Convert to Decimal points
883
+ const pts = points.map(p => point(p.x, p.y));
884
+
885
+ // Find the lowest point (and leftmost if tie)
886
+ let lowest = 0;
887
+ for (let i = 1; i < pts.length; i++) {
888
+ if (pts[i].y.lt(pts[lowest].y) ||
889
+ (pts[i].y.eq(pts[lowest].y) && pts[i].x.lt(pts[lowest].x))) {
890
+ lowest = i;
891
+ }
892
+ }
893
+
894
+ // Swap lowest to front
895
+ [pts[0], pts[lowest]] = [pts[lowest], pts[0]];
896
+ const pivot = pts[0];
897
+
898
+ // Sort by polar angle with pivot
899
+ const sorted = pts.slice(1).sort((a, b) => {
900
+ const crossVal = cross(pivot, a, b);
901
+ if (crossVal.abs().lt(EPSILON)) {
902
+ // Collinear: sort by distance
903
+ const distA = a.x.minus(pivot.x).pow(2).plus(a.y.minus(pivot.y).pow(2));
904
+ const distB = b.x.minus(pivot.x).pow(2).plus(b.y.minus(pivot.y).pow(2));
905
+ return distA.minus(distB).toNumber();
906
+ }
907
+ return -crossVal.toNumber(); // CCW order
908
+ });
909
+
910
+ // Build hull
911
+ const hull = [pivot];
912
+
913
+ for (const pt of sorted) {
914
+ while (hull.length > 1 && cross(hull[hull.length - 2], hull[hull.length - 1], pt).lte(0)) {
915
+ hull.pop();
916
+ }
917
+ hull.push(pt);
918
+ }
919
+
920
+ return hull;
921
+ }
922
+
923
+ // ============================================================================
924
+ // Bounding Box Operations
925
+ // ============================================================================
926
+
927
+ /**
928
+ * Compute the axis-aligned bounding box (AABB) of a polygon.
929
+ *
930
+ * The bounding box is the smallest axis-aligned rectangle that contains
931
+ * all vertices of the polygon. Used for quick intersection tests and
932
+ * spatial queries.
933
+ *
934
+ * @param {Array} polygon - Array of polygon vertices [{x, y}, ...]
935
+ * @returns {{minX: Decimal, minY: Decimal, maxX: Decimal, maxY: Decimal}|null}
936
+ * Bounding box with min/max coordinates, or null if polygon is empty
937
+ *
938
+ * @example
939
+ * const triangle = [point(1,1), point(4,2), point(2,5)];
940
+ * const bbox = boundingBox(triangle);
941
+ * // {minX: 1, minY: 1, maxX: 4, maxY: 5}
942
+ *
943
+ * @example
944
+ * const empty = [];
945
+ * boundingBox(empty); // null
946
+ */
947
+ export function boundingBox(polygon) {
948
+ if (polygon.length === 0) {
949
+ return null;
950
+ }
951
+
952
+ let minX = D(polygon[0].x);
953
+ let minY = D(polygon[0].y);
954
+ let maxX = D(polygon[0].x);
955
+ let maxY = D(polygon[0].y);
956
+
957
+ for (const p of polygon) {
958
+ const x = D(p.x);
959
+ const y = D(p.y);
960
+ if (x.lt(minX)) minX = x;
961
+ if (y.lt(minY)) minY = y;
962
+ if (x.gt(maxX)) maxX = x;
963
+ if (y.gt(maxY)) maxY = y;
964
+ }
965
+
966
+ return { minX, minY, maxX, maxY };
967
+ }
968
+
969
+ /**
970
+ * Check if two axis-aligned bounding boxes intersect.
971
+ *
972
+ * Two AABBs intersect if they overlap in both x and y dimensions.
973
+ * This is a fast O(1) test used to quickly reject non-intersecting polygons
974
+ * before performing more expensive exact intersection tests.
975
+ *
976
+ * Algorithm:
977
+ * - Boxes DO NOT intersect if:
978
+ * - bb1's right edge is left of bb2's left edge, OR
979
+ * - bb2's right edge is left of bb1's left edge, OR
980
+ * - bb1's top edge is below bb2's bottom edge, OR
981
+ * - bb2's top edge is below bb1's bottom edge
982
+ * - Otherwise they intersect (or touch)
983
+ *
984
+ * @param {Object} bb1 - First bounding box {minX, minY, maxX, maxY}
985
+ * @param {Object} bb2 - Second bounding box {minX, minY, maxX, maxY}
986
+ * @returns {boolean} True if bounding boxes overlap or touch
987
+ *
988
+ * @example
989
+ * const bb1 = {minX: D(0), minY: D(0), maxX: D(2), maxY: D(2)};
990
+ * const bb2 = {minX: D(1), minY: D(1), maxX: D(3), maxY: D(3)};
991
+ * bboxIntersects(bb1, bb2); // true (overlapping)
992
+ *
993
+ * @example
994
+ * const bb3 = {minX: D(5), minY: D(5), maxX: D(7), maxY: D(7)};
995
+ * bboxIntersects(bb1, bb3); // false (separate)
996
+ */
997
+ export function bboxIntersects(bb1, bb2) {
998
+ if (!bb1 || !bb2) return false;
999
+ return !(bb1.maxX.lt(bb2.minX) || bb2.maxX.lt(bb1.minX) ||
1000
+ bb1.maxY.lt(bb2.minY) || bb2.maxY.lt(bb1.minY));
1001
+ }
1002
+
1003
+ // ============================================================================
1004
+ // General Polygon Intersection (Weiler-Atherton based)
1005
+ // ============================================================================
1006
+
1007
+ /**
1008
+ * Compute the intersection of two simple polygons.
1009
+ *
1010
+ * Returns the region(s) where both polygons overlap. This implementation
1011
+ * handles both convex and concave polygons, though complex cases may produce
1012
+ * simplified results (convex hull of intersection points).
1013
+ *
1014
+ * Algorithm:
1015
+ * 1. Quick rejection: check if bounding boxes intersect
1016
+ * 2. If clip polygon is convex: use efficient Sutherland-Hodgman algorithm
1017
+ * 3. Otherwise: use general point collection method:
1018
+ * a. Find all edge-edge intersection points
1019
+ * b. Find subject vertices inside clip polygon
1020
+ * c. Find clip vertices inside subject polygon
1021
+ * d. Compute convex hull of all collected points
1022
+ *
1023
+ * Note: For complex concave polygons with multiple intersection regions,
1024
+ * this returns a simplified convex hull. Full Weiler-Atherton would be
1025
+ * needed for exact results with holes and multiple components.
1026
+ *
1027
+ * @param {Array} subject - Subject polygon vertices [{x, y}, ...]
1028
+ * @param {Array} clip - Clipping polygon vertices [{x, y}, ...]
1029
+ * @returns {Array<Array>} Array of result polygons (usually one polygon,
1030
+ * or empty array if no intersection)
1031
+ *
1032
+ * @example
1033
+ * // Two overlapping squares
1034
+ * const square1 = [point(0,0), point(2,0), point(2,2), point(0,2)];
1035
+ * const square2 = [point(1,1), point(3,1), point(3,3), point(1,3)];
1036
+ * const result = polygonIntersection(square1, square2);
1037
+ * // Returns intersection region: [point(1,1), point(2,1), point(2,2), point(1,2)]
1038
+ *
1039
+ * @example
1040
+ * // No intersection
1041
+ * const square1 = [point(0,0), point(1,0), point(1,1), point(0,1)];
1042
+ * const square2 = [point(5,5), point(6,5), point(6,6), point(5,6)];
1043
+ * polygonIntersection(square1, square2); // []
1044
+ */
1045
+ export function polygonIntersection(subject, clip) {
1046
+ // Convert to Decimal points
1047
+ const subjectPoly = subject.map(p => point(p.x, p.y));
1048
+ const clipPoly = clip.map(p => point(p.x, p.y));
1049
+
1050
+ // Quick bounding box check
1051
+ const bb1 = boundingBox(subjectPoly);
1052
+ const bb2 = boundingBox(clipPoly);
1053
+
1054
+ if (!bboxIntersects(bb1, bb2)) {
1055
+ return [];
1056
+ }
1057
+
1058
+ // For simple cases, use Sutherland-Hodgman if clip is convex
1059
+ if (isConvex(clipPoly)) {
1060
+ const result = clipPolygonSH(subjectPoly, ensureCCW(clipPoly));
1061
+ return result.length >= 3 ? [result] : [];
1062
+ }
1063
+
1064
+ // For general case, use point collection approach
1065
+ return generalPolygonIntersection(subjectPoly, clipPoly);
1066
+ }
1067
+
1068
+ /**
1069
+ * Check if a polygon is convex.
1070
+ *
1071
+ * A polygon is convex if all interior angles are less than 180°, or equivalently,
1072
+ * if all turns along the boundary are in the same direction (all left or all right).
1073
+ *
1074
+ * Algorithm:
1075
+ * 1. For each triple of consecutive vertices (p0, p1, p2):
1076
+ * - Compute cross product at p1
1077
+ * - Determine turn direction (left/right)
1078
+ * 2. If all non-zero turns are in the same direction: convex
1079
+ * 3. If turns change direction: concave
1080
+ *
1081
+ * @param {Array} polygon - Array of polygon vertices [{x, y}, ...]
1082
+ * @returns {boolean} True if polygon is convex, false if concave or degenerate
1083
+ *
1084
+ * @example
1085
+ * // Convex square
1086
+ * const square = [point(0,0), point(1,0), point(1,1), point(0,1)];
1087
+ * isConvex(square); // true
1088
+ *
1089
+ * @example
1090
+ * // Concave polygon (L-shape)
1091
+ * const lshape = [point(0,0), point(2,0), point(2,1), point(1,1), point(1,2), point(0,2)];
1092
+ * isConvex(lshape); // false
1093
+ *
1094
+ * @example
1095
+ * // Triangle (always convex)
1096
+ * const triangle = [point(0,0), point(2,0), point(1,2)];
1097
+ * isConvex(triangle); // true
1098
+ */
1099
+ export function isConvex(polygon) {
1100
+ const n = polygon.length;
1101
+ if (n < 3) return false;
1102
+
1103
+ let sign = 0;
1104
+
1105
+ for (let i = 0; i < n; i++) {
1106
+ const p0 = polygon[i];
1107
+ const p1 = polygon[(i + 1) % n];
1108
+ const p2 = polygon[(i + 2) % n];
1109
+
1110
+ const crossVal = cross(p0, p1, p2);
1111
+ const currentSign = crossVal.gt(0) ? 1 : (crossVal.lt(0) ? -1 : 0);
1112
+
1113
+ if (currentSign !== 0) {
1114
+ if (sign === 0) {
1115
+ sign = currentSign;
1116
+ } else if (sign !== currentSign) {
1117
+ return false;
1118
+ }
1119
+ }
1120
+ }
1121
+
1122
+ return true;
1123
+ }
1124
+
1125
+ /**
1126
+ * General polygon intersection using point collection method.
1127
+ *
1128
+ * A simplified approach for general (possibly concave) polygon intersection.
1129
+ * Collects all relevant points and computes their convex hull.
1130
+ *
1131
+ * Algorithm:
1132
+ * 1. Find all edge-edge intersection points
1133
+ * 2. Find subject vertices inside or on clip polygon
1134
+ * 3. Find clip vertices inside or on subject polygon
1135
+ * 4. Remove duplicate points
1136
+ * 5. Compute convex hull of all collected points
1137
+ *
1138
+ * Note: Returns convex hull approximation. Not exact for concave results.
1139
+ *
1140
+ * @private
1141
+ * @param {Array} subject - Subject polygon vertices
1142
+ * @param {Array} clip - Clipping polygon vertices
1143
+ * @returns {Array} Single result polygon or empty array
1144
+ */
1145
+ function generalPolygonIntersection(subject, clip) {
1146
+ const intersectionPoints = [];
1147
+
1148
+ // Find all edge intersection points
1149
+ for (let i = 0; i < subject.length; i++) {
1150
+ const s1 = subject[i];
1151
+ const s2 = subject[(i + 1) % subject.length];
1152
+
1153
+ for (let j = 0; j < clip.length; j++) {
1154
+ const c1 = clip[j];
1155
+ const c2 = clip[(j + 1) % clip.length];
1156
+
1157
+ const intersection = segmentIntersection(s1, s2, c1, c2);
1158
+ if (intersection) {
1159
+ intersectionPoints.push(point(intersection.x, intersection.y));
1160
+ }
1161
+ }
1162
+ }
1163
+
1164
+ // Find subject vertices inside clip
1165
+ const subjectInside = subject.filter(p => pointInPolygon(p, clip) >= 0);
1166
+
1167
+ // Find clip vertices inside subject
1168
+ const clipInside = clip.filter(p => pointInPolygon(p, subject) >= 0);
1169
+
1170
+ // Collect all points
1171
+ const allPoints = [...intersectionPoints, ...subjectInside, ...clipInside];
1172
+
1173
+ if (allPoints.length < 3) {
1174
+ return [];
1175
+ }
1176
+
1177
+ // Remove duplicates
1178
+ const unique = removeDuplicatePoints(allPoints);
1179
+
1180
+ if (unique.length < 3) {
1181
+ return [];
1182
+ }
1183
+
1184
+ // Sort points to form a valid polygon (convex hull of intersection)
1185
+ const hull = convexHull(unique);
1186
+
1187
+ return hull.length >= 3 ? [hull] : [];
1188
+ }
1189
+
1190
+ /**
1191
+ * Remove duplicate points from an array using tolerance-based equality.
1192
+ *
1193
+ * Compares each point against already-accepted points using the
1194
+ * pointsEqual function with EPSILON tolerance.
1195
+ *
1196
+ * @private
1197
+ * @param {Array} points - Array of points to deduplicate
1198
+ * @returns {Array} Array with duplicates removed
1199
+ */
1200
+ function removeDuplicatePoints(points) {
1201
+ const result = [];
1202
+
1203
+ for (const p of points) {
1204
+ let isDuplicate = false;
1205
+ for (const r of result) {
1206
+ if (pointsEqual(p, r)) {
1207
+ isDuplicate = true;
1208
+ break;
1209
+ }
1210
+ }
1211
+ if (!isDuplicate) {
1212
+ result.push(p);
1213
+ }
1214
+ }
1215
+
1216
+ return result;
1217
+ }
1218
+
1219
+ // ============================================================================
1220
+ // Polygon Union
1221
+ // ============================================================================
1222
+
1223
+ /**
1224
+ * Compute the union of two simple polygons.
1225
+ *
1226
+ * Returns the combined region covered by either or both polygons.
1227
+ * This implementation provides simplified results for complex cases.
1228
+ *
1229
+ * Algorithm:
1230
+ * 1. Quick optimization: if bounding boxes don't intersect, return both polygons
1231
+ * 2. If both polygons are convex: compute convex hull of all vertices
1232
+ * 3. Otherwise: use point collection method:
1233
+ * a. Find all edge-edge intersection points
1234
+ * b. Find vertices of each polygon outside the other
1235
+ * c. Compute convex hull of boundary points
1236
+ *
1237
+ * Note: This is a simplified union that works well for convex polygons
1238
+ * and gives approximate results for concave polygons. For exact results
1239
+ * with concave polygons, a full polygon clipping algorithm (like
1240
+ * Greiner-Hormann or Martinez-Rueda) would be needed.
1241
+ *
1242
+ * @param {Array} polygon1 - First polygon vertices [{x, y}, ...]
1243
+ * @param {Array} polygon2 - Second polygon vertices [{x, y}, ...]
1244
+ * @returns {Array<Array>} Array of result polygons
1245
+ * (one polygon if they overlap or are merged,
1246
+ * two polygons if separate)
1247
+ *
1248
+ * @example
1249
+ * // Two overlapping squares
1250
+ * const square1 = [point(0,0), point(2,0), point(2,2), point(0,2)];
1251
+ * const square2 = [point(1,1), point(3,1), point(3,3), point(1,3)];
1252
+ * const result = polygonUnion(square1, square2);
1253
+ * // Returns combined region covering both squares
1254
+ *
1255
+ * @example
1256
+ * // Non-overlapping polygons
1257
+ * const square1 = [point(0,0), point(1,0), point(1,1), point(0,1)];
1258
+ * const square2 = [point(5,5), point(6,5), point(6,6), point(5,6)];
1259
+ * polygonUnion(square1, square2); // [square1, square2]
1260
+ */
1261
+ export function polygonUnion(polygon1, polygon2) {
1262
+ const poly1 = polygon1.map(p => point(p.x, p.y));
1263
+ const poly2 = polygon2.map(p => point(p.x, p.y));
1264
+
1265
+ const bb1 = boundingBox(poly1);
1266
+ const bb2 = boundingBox(poly2);
1267
+
1268
+ // If no overlap, return both polygons
1269
+ if (!bboxIntersects(bb1, bb2)) {
1270
+ return [poly1, poly2];
1271
+ }
1272
+
1273
+ // For convex polygons, compute the convex hull of all vertices
1274
+ // This is a simplification - full union requires more complex algorithms
1275
+ if (isConvex(poly1) && isConvex(poly2)) {
1276
+ const allPoints = [...poly1, ...poly2];
1277
+ const hull = convexHull(allPoints);
1278
+ return [hull];
1279
+ }
1280
+
1281
+ // For general case, use point collection approach
1282
+ return generalPolygonUnion(poly1, poly2);
1283
+ }
1284
+
1285
+ /**
1286
+ * General polygon union using point collection method.
1287
+ *
1288
+ * Simplified union for general (possibly concave) polygons.
1289
+ * Collects boundary points and computes their convex hull.
1290
+ *
1291
+ * Algorithm:
1292
+ * 1. Find all edge-edge intersection points
1293
+ * 2. Find poly1 vertices outside poly2
1294
+ * 3. Find poly2 vertices outside poly1
1295
+ * 4. If no boundary points found, one polygon contains the other
1296
+ * 5. Otherwise, compute convex hull of all boundary points
1297
+ *
1298
+ * Note: Returns convex hull approximation. Not exact for concave results.
1299
+ *
1300
+ * @private
1301
+ * @param {Array} poly1 - First polygon vertices
1302
+ * @param {Array} poly2 - Second polygon vertices
1303
+ * @returns {Array} Array containing result polygon(s)
1304
+ */
1305
+ function generalPolygonUnion(poly1, poly2) {
1306
+ // Find intersection points
1307
+ const intersectionPoints = [];
1308
+
1309
+ for (let i = 0; i < poly1.length; i++) {
1310
+ const s1 = poly1[i];
1311
+ const s2 = poly1[(i + 1) % poly1.length];
1312
+
1313
+ for (let j = 0; j < poly2.length; j++) {
1314
+ const c1 = poly2[j];
1315
+ const c2 = poly2[(j + 1) % poly2.length];
1316
+
1317
+ const intersection = segmentIntersection(s1, s2, c1, c2);
1318
+ if (intersection) {
1319
+ intersectionPoints.push(point(intersection.x, intersection.y));
1320
+ }
1321
+ }
1322
+ }
1323
+
1324
+ // Find vertices outside the other polygon
1325
+ const poly1Outside = poly1.filter(p => pointInPolygon(p, poly2) < 0);
1326
+ const poly2Outside = poly2.filter(p => pointInPolygon(p, poly1) < 0);
1327
+
1328
+ // All boundary points
1329
+ const allPoints = [...intersectionPoints, ...poly1Outside, ...poly2Outside];
1330
+
1331
+ if (allPoints.length < 3) {
1332
+ // One contains the other or identical
1333
+ const area1 = polygonArea(poly1).abs();
1334
+ const area2 = polygonArea(poly2).abs();
1335
+ return area1.gt(area2) ? [poly1] : [poly2];
1336
+ }
1337
+
1338
+ // Compute convex hull (simplified union)
1339
+ const hull = convexHull(allPoints);
1340
+ return hull.length >= 3 ? [hull] : [];
1341
+ }
1342
+
1343
+ // ============================================================================
1344
+ // Polygon Difference
1345
+ // ============================================================================
1346
+
1347
+ /**
1348
+ * Compute the difference of two polygons (polygon1 - polygon2).
1349
+ *
1350
+ * Returns the region(s) in polygon1 that are NOT covered by polygon2.
1351
+ * This is the "subtraction" operation in polygon boolean algebra.
1352
+ *
1353
+ * Algorithm:
1354
+ * 1. Quick optimization: if bounding boxes don't intersect, return polygon1
1355
+ * 2. Find all edge-edge intersection points
1356
+ * 3. Find polygon1 vertices outside polygon2
1357
+ * 4. Find polygon2 vertices inside polygon1 (these define the "hole" boundary)
1358
+ * 5. Handle special cases:
1359
+ * - polygon2 entirely outside polygon1: return polygon1
1360
+ * - polygon1 entirely inside polygon2: return empty
1361
+ * 6. Compute convex hull of remaining points (simplified result)
1362
+ *
1363
+ * Note: This is a simplified difference operation that works well for
1364
+ * convex cases. For complex concave polygons with holes, a full
1365
+ * polygon clipping algorithm would be needed.
1366
+ *
1367
+ * @param {Array} polygon1 - First polygon (subject) [{x, y}, ...]
1368
+ * @param {Array} polygon2 - Second polygon (to subtract) [{x, y}, ...]
1369
+ * @returns {Array<Array>} Array of result polygons (possibly empty)
1370
+ *
1371
+ * @example
1372
+ * // Subtract overlapping region
1373
+ * const square1 = [point(0,0), point(3,0), point(3,3), point(0,3)];
1374
+ * const square2 = [point(1,1), point(4,1), point(4,4), point(1,4)];
1375
+ * const result = polygonDifference(square1, square2);
1376
+ * // Returns portion of square1 not covered by square2
1377
+ *
1378
+ * @example
1379
+ * // No overlap - return original
1380
+ * const square1 = [point(0,0), point(1,0), point(1,1), point(0,1)];
1381
+ * const square2 = [point(5,5), point(6,5), point(6,6), point(5,6)];
1382
+ * polygonDifference(square1, square2); // [square1]
1383
+ *
1384
+ * @example
1385
+ * // Complete coverage - return empty
1386
+ * const small = [point(1,1), point(2,1), point(2,2), point(1,2)];
1387
+ * const large = [point(0,0), point(3,0), point(3,3), point(0,3)];
1388
+ * polygonDifference(small, large); // []
1389
+ */
1390
+ export function polygonDifference(polygon1, polygon2) {
1391
+ const poly1 = polygon1.map(p => point(p.x, p.y));
1392
+ const poly2 = polygon2.map(p => point(p.x, p.y));
1393
+
1394
+ const bb1 = boundingBox(poly1);
1395
+ const bb2 = boundingBox(poly2);
1396
+
1397
+ // If no overlap, return original
1398
+ if (!bboxIntersects(bb1, bb2)) {
1399
+ return [poly1];
1400
+ }
1401
+
1402
+ // Find intersection points
1403
+ const intersectionPoints = [];
1404
+
1405
+ for (let i = 0; i < poly1.length; i++) {
1406
+ const s1 = poly1[i];
1407
+ const s2 = poly1[(i + 1) % poly1.length];
1408
+
1409
+ for (let j = 0; j < poly2.length; j++) {
1410
+ const c1 = poly2[j];
1411
+ const c2 = poly2[(j + 1) % poly2.length];
1412
+
1413
+ const intersection = segmentIntersection(s1, s2, c1, c2);
1414
+ if (intersection) {
1415
+ intersectionPoints.push(point(intersection.x, intersection.y));
1416
+ }
1417
+ }
1418
+ }
1419
+
1420
+ // Find poly1 vertices outside poly2
1421
+ const poly1Outside = poly1.filter(p => pointInPolygon(p, poly2) < 0);
1422
+
1423
+ // Find poly2 vertices inside poly1 (these form the "hole" boundary)
1424
+ const poly2Inside = poly2.filter(p => pointInPolygon(p, poly1) > 0);
1425
+
1426
+ // If poly2 is entirely outside poly1, return poly1
1427
+ if (poly2Inside.length === 0 && intersectionPoints.length === 0) {
1428
+ return [poly1];
1429
+ }
1430
+
1431
+ // If poly1 is entirely inside poly2, return empty
1432
+ if (poly1Outside.length === 0) {
1433
+ return [];
1434
+ }
1435
+
1436
+ // Simplified: return poly1 vertices outside poly2 + intersection points
1437
+ const allPoints = [...intersectionPoints, ...poly1Outside];
1438
+
1439
+ if (allPoints.length < 3) {
1440
+ return [];
1441
+ }
1442
+
1443
+ const hull = convexHull(allPoints);
1444
+ return hull.length >= 3 ? [hull] : [];
1445
+ }
1446
+
1447
+ // ============================================================================
1448
+ // Exports
1449
+ // ============================================================================
1450
+
1451
+ export default {
1452
+ // Primitives
1453
+ point,
1454
+ pointsEqual,
1455
+ cross,
1456
+ dot,
1457
+ sign,
1458
+
1459
+ // Segment operations
1460
+ segmentIntersection,
1461
+ lineSegmentIntersection,
1462
+
1463
+ // Point in polygon
1464
+ pointInPolygon,
1465
+ pointOnSegment,
1466
+
1467
+ // Sutherland-Hodgman clipping
1468
+ clipPolygonSH,
1469
+
1470
+ // Polygon properties
1471
+ polygonArea,
1472
+ isCounterClockwise,
1473
+ reversePolygon,
1474
+ ensureCCW,
1475
+ isConvex,
1476
+
1477
+ // Convex hull
1478
+ convexHull,
1479
+
1480
+ // Bounding box
1481
+ boundingBox,
1482
+ bboxIntersects,
1483
+
1484
+ // Boolean operations
1485
+ polygonIntersection,
1486
+ polygonUnion,
1487
+ polygonDifference,
1488
+
1489
+ // Constants
1490
+ EPSILON
1491
+ };