@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.
- package/README.md +256 -759
- package/bin/svg-matrix.js +171 -2
- package/bin/svglinter.cjs +1162 -0
- package/package.json +8 -2
- package/scripts/postinstall.js +6 -9
- 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
package/src/polygon-clip.js
CHANGED
|
@@ -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
|
-
*
|
|
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.
|
|
1232
|
-
* 3.
|
|
1233
|
-
*
|
|
1234
|
-
*
|
|
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
|
-
//
|
|
1274
|
-
|
|
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
|
-
*
|
|
1272
|
+
* Trace the outer boundary for polygon union.
|
|
1287
1273
|
*
|
|
1288
|
-
*
|
|
1289
|
-
*
|
|
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
|
|
1306
|
-
// Find intersection points
|
|
1307
|
-
const
|
|
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
|
|
1311
|
-
const
|
|
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
|
|
1315
|
-
const
|
|
1445
|
+
const c = poly2[j];
|
|
1446
|
+
const d = poly2[(j + 1) % poly2.length];
|
|
1316
1447
|
|
|
1317
|
-
const intersection = segmentIntersection(
|
|
1448
|
+
const intersection = segmentIntersection(a, b, c, d);
|
|
1318
1449
|
if (intersection) {
|
|
1319
|
-
|
|
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
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
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
|
-
//
|
|
1329
|
-
const
|
|
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
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
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
|
-
|
|
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.
|
|
1356
|
-
* 3.
|
|
1357
|
-
* 4.
|
|
1358
|
-
* 5.
|
|
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
|
-
//
|
|
1403
|
-
|
|
1561
|
+
// Use boundary tracing for difference
|
|
1562
|
+
return traceBoundaryDifference(poly1, poly2);
|
|
1563
|
+
}
|
|
1404
1564
|
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
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
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
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
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
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
|
-
//
|
|
1421
|
-
const
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
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
|
-
//
|
|
1427
|
-
|
|
1428
|
-
|
|
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
|
|
1432
|
-
if (
|
|
1638
|
+
// If all poly1 vertices are inside poly2, no difference remains
|
|
1639
|
+
if (startIdx === -1) {
|
|
1433
1640
|
return [];
|
|
1434
1641
|
}
|
|
1435
1642
|
|
|
1436
|
-
//
|
|
1437
|
-
const
|
|
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
|
-
|
|
1440
|
-
|
|
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
|
-
|
|
1444
|
-
|
|
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
|
// ============================================================================
|