@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.
@@ -1221,23 +1221,17 @@ function removeDuplicatePoints(points) {
1221
1221
  // ============================================================================
1222
1222
 
1223
1223
  /**
1224
- * Compute the union of two simple polygons.
1224
+ * Compute the union of two simple polygons using boundary tracing.
1225
1225
  *
1226
1226
  * Returns the combined region covered by either or both polygons.
1227
- * This implementation provides simplified results for complex cases.
1227
+ * Properly traces the outer boundary to produce correct non-convex results.
1228
1228
  *
1229
- * Algorithm:
1229
+ * Algorithm (Boundary Tracing):
1230
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.
1231
+ * 2. Insert all edge-edge intersection points into both polygon vertex lists
1232
+ * 3. Start from a vertex guaranteed to be on the outer boundary
1233
+ * 4. Trace the boundary, always following the "outermost" path at intersections
1234
+ * 5. Return the traced polygon
1241
1235
  *
1242
1236
  * @param {Array} polygon1 - First polygon vertices [{x, y}, ...]
1243
1237
  * @param {Array} polygon2 - Second polygon vertices [{x, y}, ...]
@@ -1250,7 +1244,7 @@ function removeDuplicatePoints(points) {
1250
1244
  * const square1 = [point(0,0), point(2,0), point(2,2), point(0,2)];
1251
1245
  * const square2 = [point(1,1), point(3,1), point(3,3), point(1,3)];
1252
1246
  * const result = polygonUnion(square1, square2);
1253
- * // Returns combined region covering both squares
1247
+ * // Returns L-shaped combined region covering both squares
1254
1248
  *
1255
1249
  * @example
1256
1250
  * // Non-overlapping polygons
@@ -1270,74 +1264,246 @@ export function polygonUnion(polygon1, polygon2) {
1270
1264
  return [poly1, poly2];
1271
1265
  }
1272
1266
 
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);
1267
+ // Use boundary tracing for union
1268
+ return traceBoundaryUnion(poly1, poly2);
1283
1269
  }
1284
1270
 
1285
1271
  /**
1286
- * General polygon union using point collection method.
1272
+ * Trace the outer boundary for polygon union.
1287
1273
  *
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.
1274
+ * Uses a simpler approach: for union of two overlapping polygons,
1275
+ * trace the boundary staying on the "outside" of the combined shape.
1276
+ * At each intersection, switch to the other polygon.
1299
1277
  *
1300
1278
  * @private
1301
1279
  * @param {Array} poly1 - First polygon vertices
1302
1280
  * @param {Array} poly2 - Second polygon vertices
1303
1281
  * @returns {Array} Array containing result polygon(s)
1304
1282
  */
1305
- function generalPolygonUnion(poly1, poly2) {
1306
- // Find intersection points
1307
- const intersectionPoints = [];
1283
+ function traceBoundaryUnion(poly1, poly2) {
1284
+ // Find all intersection points with edge indices
1285
+ const intersections = findAllIntersections(poly1, poly2);
1286
+
1287
+ // If no intersections, check containment
1288
+ if (intersections.length === 0) {
1289
+ // Check if one contains the other
1290
+ const p1Inside = pointInPolygon(poly1[0], poly2);
1291
+ const p2Inside = pointInPolygon(poly2[0], poly1);
1292
+
1293
+ if (p1Inside > 0) {
1294
+ // poly1 is inside poly2
1295
+ return [poly2];
1296
+ } else if (p2Inside > 0) {
1297
+ // poly2 is inside poly1
1298
+ return [poly1];
1299
+ } else {
1300
+ // Disjoint - this shouldn't happen if bbox intersects
1301
+ // But they might just touch at a point
1302
+ return [poly1, poly2];
1303
+ }
1304
+ }
1305
+
1306
+ // Build augmented polygons with intersection points inserted
1307
+ const aug1 = augmentPolygon(poly1, intersections.map(i => ({
1308
+ edgeIndex: i.edge1,
1309
+ t: i.t1,
1310
+ point: i.point,
1311
+ intersectionId: i.id
1312
+ })));
1313
+
1314
+ const aug2 = augmentPolygon(poly2, intersections.map(i => ({
1315
+ edgeIndex: i.edge2,
1316
+ t: i.t2,
1317
+ point: i.point,
1318
+ intersectionId: i.id
1319
+ })));
1320
+
1321
+ // Build lookup from intersection ID to indices in both polygons
1322
+ const intersectionMap = new Map();
1323
+ for (let i = 0; i < aug1.length; i++) {
1324
+ if (aug1[i].intersectionId !== undefined) {
1325
+ if (!intersectionMap.has(aug1[i].intersectionId)) {
1326
+ intersectionMap.set(aug1[i].intersectionId, {});
1327
+ }
1328
+ intersectionMap.get(aug1[i].intersectionId).idx1 = i;
1329
+ }
1330
+ }
1331
+ for (let i = 0; i < aug2.length; i++) {
1332
+ if (aug2[i].intersectionId !== undefined) {
1333
+ if (!intersectionMap.has(aug2[i].intersectionId)) {
1334
+ intersectionMap.set(aug2[i].intersectionId, {});
1335
+ }
1336
+ intersectionMap.get(aug2[i].intersectionId).idx2 = i;
1337
+ }
1338
+ }
1339
+
1340
+ // Find starting point: a vertex that's outside the other polygon (guaranteed to be on outer boundary)
1341
+ let startPoly = 1;
1342
+ let startIdx = -1;
1343
+
1344
+ for (let i = 0; i < aug1.length; i++) {
1345
+ const p = aug1[i];
1346
+ if (p.intersectionId === undefined && pointInPolygon(p, poly2) < 0) {
1347
+ startIdx = i;
1348
+ break;
1349
+ }
1350
+ }
1351
+
1352
+ if (startIdx === -1) {
1353
+ // All poly1 vertices are inside poly2, start from poly2
1354
+ startPoly = 2;
1355
+ for (let i = 0; i < aug2.length; i++) {
1356
+ const p = aug2[i];
1357
+ if (p.intersectionId === undefined && pointInPolygon(p, poly1) < 0) {
1358
+ startIdx = i;
1359
+ break;
1360
+ }
1361
+ }
1362
+ }
1363
+
1364
+ if (startIdx === -1) {
1365
+ // Edge case: one polygon contains the other entirely
1366
+ const area1 = polygonArea(poly1).abs();
1367
+ const area2 = polygonArea(poly2).abs();
1368
+ return area1.gt(area2) ? [poly1] : [poly2];
1369
+ }
1370
+
1371
+ // Trace the outer boundary
1372
+ // For union: at each intersection, switch to the other polygon
1373
+ // This works because the outer boundary alternates between the two polygons
1374
+ const result = [];
1375
+ let onPoly1 = startPoly === 1;
1376
+ let currentIdx = startIdx;
1377
+ const usedIntersections = new Set();
1378
+ const maxIterations = aug1.length + aug2.length + 10;
1379
+ let iterations = 0;
1380
+ const startKey = `${startPoly}-${startIdx}`;
1381
+
1382
+ while (iterations < maxIterations) {
1383
+ iterations++;
1384
+
1385
+ const aug = onPoly1 ? aug1 : aug2;
1386
+ const otherAug = onPoly1 ? aug2 : aug1;
1387
+ const vertex = aug[currentIdx];
1388
+
1389
+ // Add vertex to result (avoid duplicates)
1390
+ if (result.length === 0 || !pointsEqual(result[result.length - 1], vertex)) {
1391
+ result.push(point(vertex.x, vertex.y));
1392
+ }
1393
+
1394
+ // Move to next vertex
1395
+ const nextIdx = (currentIdx + 1) % aug.length;
1396
+ const nextVertex = aug[nextIdx];
1397
+
1398
+ // If next vertex is an intersection we haven't used, switch polygons
1399
+ if (nextVertex.intersectionId !== undefined && !usedIntersections.has(nextVertex.intersectionId)) {
1400
+ // Add the intersection point
1401
+ result.push(point(nextVertex.x, nextVertex.y));
1402
+ usedIntersections.add(nextVertex.intersectionId);
1403
+
1404
+ // Switch to other polygon
1405
+ const mapping = intersectionMap.get(nextVertex.intersectionId);
1406
+ const otherIdx = onPoly1 ? mapping.idx2 : mapping.idx1;
1407
+ onPoly1 = !onPoly1;
1408
+ currentIdx = (otherIdx + 1) % otherAug.length;
1409
+ } else {
1410
+ currentIdx = nextIdx;
1411
+ }
1412
+
1413
+ // Check if we're back at start
1414
+ const currentKey = `${onPoly1 ? 1 : 2}-${currentIdx}`;
1415
+ if (currentKey === startKey) {
1416
+ break;
1417
+ }
1418
+ }
1419
+
1420
+ // Remove the last point if it's the same as the first (closed polygon)
1421
+ if (result.length > 1 && pointsEqual(result[0], result[result.length - 1])) {
1422
+ result.pop();
1423
+ }
1424
+
1425
+ return result.length >= 3 ? [result] : [];
1426
+ }
1427
+
1428
+ /**
1429
+ * Find all intersection points between two polygons.
1430
+ *
1431
+ * @private
1432
+ * @param {Array} poly1 - First polygon
1433
+ * @param {Array} poly2 - Second polygon
1434
+ * @returns {Array} Array of intersection objects with edge indices and parameters
1435
+ */
1436
+ function findAllIntersections(poly1, poly2) {
1437
+ const intersections = [];
1438
+ let id = 0;
1308
1439
 
1309
1440
  for (let i = 0; i < poly1.length; i++) {
1310
- const s1 = poly1[i];
1311
- const s2 = poly1[(i + 1) % poly1.length];
1441
+ const a = poly1[i];
1442
+ const b = poly1[(i + 1) % poly1.length];
1312
1443
 
1313
1444
  for (let j = 0; j < poly2.length; j++) {
1314
- const c1 = poly2[j];
1315
- const c2 = poly2[(j + 1) % poly2.length];
1445
+ const c = poly2[j];
1446
+ const d = poly2[(j + 1) % poly2.length];
1316
1447
 
1317
- const intersection = segmentIntersection(s1, s2, c1, c2);
1448
+ const intersection = segmentIntersection(a, b, c, d);
1318
1449
  if (intersection) {
1319
- intersectionPoints.push(point(intersection.x, intersection.y));
1450
+ intersections.push({
1451
+ id: id++,
1452
+ point: point(intersection.x, intersection.y),
1453
+ edge1: i,
1454
+ edge2: j,
1455
+ t1: intersection.t,
1456
+ t2: intersection.s
1457
+ });
1320
1458
  }
1321
1459
  }
1322
1460
  }
1323
1461
 
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);
1462
+ return intersections;
1463
+ }
1464
+
1465
+ /**
1466
+ * Insert intersection points into polygon vertex list.
1467
+ *
1468
+ * @private
1469
+ * @param {Array} polygon - Original polygon vertices
1470
+ * @param {Array} insertions - Points to insert with edge index and t parameter
1471
+ * @returns {Array} Augmented polygon with intersection points inserted
1472
+ */
1473
+ function augmentPolygon(polygon, insertions) {
1474
+ // Group insertions by edge
1475
+ const byEdge = new Map();
1476
+ for (const ins of insertions) {
1477
+ if (!byEdge.has(ins.edgeIndex)) {
1478
+ byEdge.set(ins.edgeIndex, []);
1479
+ }
1480
+ byEdge.get(ins.edgeIndex).push(ins);
1481
+ }
1327
1482
 
1328
- // All boundary points
1329
- const allPoints = [...intersectionPoints, ...poly1Outside, ...poly2Outside];
1483
+ // Sort each edge's insertions by t parameter
1484
+ for (const edgeInsertions of byEdge.values()) {
1485
+ edgeInsertions.sort((a, b) => a.t.minus(b.t).toNumber());
1486
+ }
1330
1487
 
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];
1488
+ // Build augmented polygon
1489
+ const result = [];
1490
+ for (let i = 0; i < polygon.length; i++) {
1491
+ // Add original vertex
1492
+ result.push({ ...polygon[i] });
1493
+
1494
+ // Add intersection points for this edge
1495
+ if (byEdge.has(i)) {
1496
+ for (const ins of byEdge.get(i)) {
1497
+ result.push({
1498
+ x: ins.point.x,
1499
+ y: ins.point.y,
1500
+ intersectionId: ins.intersectionId
1501
+ });
1502
+ }
1503
+ }
1336
1504
  }
1337
1505
 
1338
- // Compute convex hull (simplified union)
1339
- const hull = convexHull(allPoints);
1340
- return hull.length >= 3 ? [hull] : [];
1506
+ return result;
1341
1507
  }
1342
1508
 
1343
1509
  // ============================================================================
@@ -1350,19 +1516,12 @@ function generalPolygonUnion(poly1, poly2) {
1350
1516
  * Returns the region(s) in polygon1 that are NOT covered by polygon2.
1351
1517
  * This is the "subtraction" operation in polygon boolean algebra.
1352
1518
  *
1353
- * Algorithm:
1519
+ * Algorithm (Boundary Tracing):
1354
1520
  * 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.
1521
+ * 2. Insert all edge-edge intersection points into both polygon vertex lists
1522
+ * 3. Start from a polygon1 vertex that's outside polygon2
1523
+ * 4. Trace polygon1's boundary, switching to polygon2 (reversed) at intersections
1524
+ * 5. Return the traced polygon
1366
1525
  *
1367
1526
  * @param {Array} polygon1 - First polygon (subject) [{x, y}, ...]
1368
1527
  * @param {Array} polygon2 - Second polygon (to subtract) [{x, y}, ...]
@@ -1373,7 +1532,7 @@ function generalPolygonUnion(poly1, poly2) {
1373
1532
  * const square1 = [point(0,0), point(3,0), point(3,3), point(0,3)];
1374
1533
  * const square2 = [point(1,1), point(4,1), point(4,4), point(1,4)];
1375
1534
  * const result = polygonDifference(square1, square2);
1376
- * // Returns portion of square1 not covered by square2
1535
+ * // Returns L-shaped portion of square1 not covered by square2
1377
1536
  *
1378
1537
  * @example
1379
1538
  * // No overlap - return original
@@ -1399,49 +1558,167 @@ export function polygonDifference(polygon1, polygon2) {
1399
1558
  return [poly1];
1400
1559
  }
1401
1560
 
1402
- // Find intersection points
1403
- const intersectionPoints = [];
1561
+ // Use boundary tracing for difference
1562
+ return traceBoundaryDifference(poly1, poly2);
1563
+ }
1404
1564
 
1405
- for (let i = 0; i < poly1.length; i++) {
1406
- const s1 = poly1[i];
1407
- const s2 = poly1[(i + 1) % poly1.length];
1565
+ /**
1566
+ * Trace the boundary for polygon difference (poly1 - poly2).
1567
+ *
1568
+ * Traces poly1's boundary, but when entering poly2, follows poly2's boundary
1569
+ * (in reverse) until exiting back to poly1's exterior.
1570
+ *
1571
+ * @private
1572
+ * @param {Array} poly1 - Subject polygon vertices
1573
+ * @param {Array} poly2 - Clipping polygon vertices (to subtract)
1574
+ * @returns {Array} Array containing result polygon(s)
1575
+ */
1576
+ function traceBoundaryDifference(poly1, poly2) {
1577
+ // Find all intersection points with edge indices
1578
+ const intersections = findAllIntersections(poly1, poly2);
1408
1579
 
1409
- for (let j = 0; j < poly2.length; j++) {
1410
- const c1 = poly2[j];
1411
- const c2 = poly2[(j + 1) % poly2.length];
1580
+ // If no intersections, check containment
1581
+ if (intersections.length === 0) {
1582
+ // Check if poly1 is entirely inside poly2
1583
+ const p1Inside = pointInPolygon(poly1[0], poly2);
1412
1584
 
1413
- const intersection = segmentIntersection(s1, s2, c1, c2);
1414
- if (intersection) {
1415
- intersectionPoints.push(point(intersection.x, intersection.y));
1416
- }
1585
+ if (p1Inside > 0) {
1586
+ // poly1 is completely inside poly2 - nothing remains
1587
+ return [];
1588
+ } else {
1589
+ // poly2 doesn't overlap poly1 - return original
1590
+ return [poly1];
1417
1591
  }
1418
1592
  }
1419
1593
 
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);
1594
+ // Build augmented polygons with intersection points inserted
1595
+ const aug1 = augmentPolygon(poly1, intersections.map(i => ({
1596
+ edgeIndex: i.edge1,
1597
+ t: i.t1,
1598
+ point: i.point,
1599
+ intersectionId: i.id
1600
+ })));
1601
+
1602
+ const aug2 = augmentPolygon(poly2, intersections.map(i => ({
1603
+ edgeIndex: i.edge2,
1604
+ t: i.t2,
1605
+ point: i.point,
1606
+ intersectionId: i.id
1607
+ })));
1608
+
1609
+ // Build lookup from intersection ID to indices in both polygons
1610
+ const intersectionMap = new Map();
1611
+ for (let i = 0; i < aug1.length; i++) {
1612
+ if (aug1[i].intersectionId !== undefined) {
1613
+ if (!intersectionMap.has(aug1[i].intersectionId)) {
1614
+ intersectionMap.set(aug1[i].intersectionId, {});
1615
+ }
1616
+ intersectionMap.get(aug1[i].intersectionId).idx1 = i;
1617
+ }
1618
+ }
1619
+ for (let i = 0; i < aug2.length; i++) {
1620
+ if (aug2[i].intersectionId !== undefined) {
1621
+ if (!intersectionMap.has(aug2[i].intersectionId)) {
1622
+ intersectionMap.set(aug2[i].intersectionId, {});
1623
+ }
1624
+ intersectionMap.get(aug2[i].intersectionId).idx2 = i;
1625
+ }
1626
+ }
1425
1627
 
1426
- // If poly2 is entirely outside poly1, return poly1
1427
- if (poly2Inside.length === 0 && intersectionPoints.length === 0) {
1428
- return [poly1];
1628
+ // Find starting point: a poly1 vertex that's outside poly2
1629
+ let startIdx = -1;
1630
+ for (let i = 0; i < aug1.length; i++) {
1631
+ const p = aug1[i];
1632
+ if (p.intersectionId === undefined && pointInPolygon(p, poly2) < 0) {
1633
+ startIdx = i;
1634
+ break;
1635
+ }
1429
1636
  }
1430
1637
 
1431
- // If poly1 is entirely inside poly2, return empty
1432
- if (poly1Outside.length === 0) {
1638
+ // If all poly1 vertices are inside poly2, no difference remains
1639
+ if (startIdx === -1) {
1433
1640
  return [];
1434
1641
  }
1435
1642
 
1436
- // Simplified: return poly1 vertices outside poly2 + intersection points
1437
- const allPoints = [...intersectionPoints, ...poly1Outside];
1643
+ // Trace the difference boundary
1644
+ const result = [];
1645
+ let onPoly1 = true; // We start on poly1
1646
+ let currentIdx = startIdx;
1647
+ const visited = new Set();
1648
+ const maxIterations = aug1.length + aug2.length + 10;
1649
+ let iterations = 0;
1438
1650
 
1439
- if (allPoints.length < 3) {
1440
- return [];
1651
+ while (iterations < maxIterations) {
1652
+ iterations++;
1653
+
1654
+ const poly = onPoly1 ? aug1 : aug2;
1655
+ const vertex = poly[currentIdx];
1656
+
1657
+ // Add vertex to result (avoid duplicates)
1658
+ if (result.length === 0 || !pointsEqual(result[result.length - 1], vertex)) {
1659
+ result.push(point(vertex.x, vertex.y));
1660
+ }
1661
+
1662
+ // Check if we've completed the loop
1663
+ const key = `${onPoly1 ? 1 : 2}-${currentIdx}`;
1664
+ if (visited.has(key)) {
1665
+ break;
1666
+ }
1667
+ visited.add(key);
1668
+
1669
+ if (onPoly1) {
1670
+ // On poly1, moving forward (CCW)
1671
+ const nextIdx = (currentIdx + 1) % aug1.length;
1672
+ const nextVertex = aug1[nextIdx];
1673
+
1674
+ if (nextVertex.intersectionId !== undefined) {
1675
+ // Hit an intersection - check if we're entering or leaving poly2
1676
+ const afterNext = aug1[(nextIdx + 1) % aug1.length];
1677
+ const isEntering = pointInPolygon(afterNext, poly2) > 0;
1678
+
1679
+ if (isEntering) {
1680
+ // Entering poly2 - switch to poly2 and go backwards (CW)
1681
+ const mapping = intersectionMap.get(nextVertex.intersectionId);
1682
+ result.push(point(nextVertex.x, nextVertex.y));
1683
+ onPoly1 = false;
1684
+ // Go backwards on poly2
1685
+ currentIdx = (mapping.idx2 - 1 + aug2.length) % aug2.length;
1686
+ } else {
1687
+ // Leaving poly2 (or just touching) - continue on poly1
1688
+ currentIdx = nextIdx;
1689
+ }
1690
+ } else {
1691
+ currentIdx = nextIdx;
1692
+ }
1693
+ } else {
1694
+ // On poly2, moving backward (CW) - this traces the "inside" boundary of the hole
1695
+ const nextIdx = (currentIdx - 1 + aug2.length) % aug2.length;
1696
+ const nextVertex = aug2[nextIdx];
1697
+
1698
+ if (nextVertex.intersectionId !== undefined) {
1699
+ // Hit an intersection - switch back to poly1
1700
+ const mapping = intersectionMap.get(nextVertex.intersectionId);
1701
+ result.push(point(nextVertex.x, nextVertex.y));
1702
+ onPoly1 = true;
1703
+ // Continue forward on poly1 from the intersection
1704
+ currentIdx = (mapping.idx1 + 1) % aug1.length;
1705
+ } else {
1706
+ currentIdx = nextIdx;
1707
+ }
1708
+ }
1709
+
1710
+ // Safety: check if we're back at start
1711
+ if (onPoly1 && currentIdx === startIdx) {
1712
+ break;
1713
+ }
1441
1714
  }
1442
1715
 
1443
- const hull = convexHull(allPoints);
1444
- return hull.length >= 3 ? [hull] : [];
1716
+ // Remove the last point if it's the same as the first (closed polygon)
1717
+ if (result.length > 1 && pointsEqual(result[0], result[result.length - 1])) {
1718
+ result.pop();
1719
+ }
1720
+
1721
+ return result.length >= 3 ? [result] : [];
1445
1722
  }
1446
1723
 
1447
1724
  // ============================================================================