@tscircuit/curvy-trace-solver 0.0.7 → 0.0.8
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/dist/index.d.ts +3 -0
- package/dist/index.js +251 -14
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -49,6 +49,9 @@ declare class CurvyTraceSolver extends BaseSolver {
|
|
|
49
49
|
private updateCollisionPairs;
|
|
50
50
|
private computeTotalCost;
|
|
51
51
|
private computeCostForTrace;
|
|
52
|
+
private tracesIntersect;
|
|
53
|
+
private findIntersectingPairs;
|
|
54
|
+
private resolveIntersections;
|
|
52
55
|
private optimizeStep;
|
|
53
56
|
private buildOutputTraces;
|
|
54
57
|
_step(): void;
|
package/dist/index.js
CHANGED
|
@@ -1014,6 +1014,13 @@ function segmentDistSq(a1x, a1y, a2x, a2y, b1x, b1y, b2x, b2y) {
|
|
|
1014
1014
|
ptSegDistSq(b2x, b2y, a1x, a1y, a2x, a2y)
|
|
1015
1015
|
);
|
|
1016
1016
|
}
|
|
1017
|
+
function segmentsIntersect(a1x, a1y, a2x, a2y, b1x, b1y, b2x, b2y) {
|
|
1018
|
+
const d1 = (b2x - b1x) * (a1y - b1y) - (b2y - b1y) * (a1x - b1x);
|
|
1019
|
+
const d2 = (b2x - b1x) * (a2y - b1y) - (b2y - b1y) * (a2x - b1x);
|
|
1020
|
+
const d3 = (a2x - a1x) * (b1y - a1y) - (a2y - a1y) * (b1x - a1x);
|
|
1021
|
+
const d4 = (a2x - a1x) * (b2y - a1y) - (a2y - a1y) * (b2x - a1x);
|
|
1022
|
+
return (d1 > 0 && d2 < 0 || d1 < 0 && d2 > 0) && (d3 > 0 && d4 < 0 || d3 < 0 && d4 > 0);
|
|
1023
|
+
}
|
|
1017
1024
|
function chordContains(aT1, aT2, bT1, bT2, perimeter) {
|
|
1018
1025
|
const normalize = (t) => (t % perimeter + perimeter) % perimeter;
|
|
1019
1026
|
const a1 = normalize(aT1), a2 = normalize(aT2);
|
|
@@ -1102,17 +1109,23 @@ var CurvyTraceSolver = class extends BaseSolver {
|
|
|
1102
1109
|
t2: getPerimeterT(pair.end, bounds),
|
|
1103
1110
|
idx
|
|
1104
1111
|
}));
|
|
1105
|
-
const
|
|
1112
|
+
const containedBy = tracesWithT.map(() => []);
|
|
1113
|
+
const contains = tracesWithT.map(() => []);
|
|
1106
1114
|
for (const trace of tracesWithT) {
|
|
1107
|
-
let depth = 0;
|
|
1108
1115
|
for (const other of tracesWithT) {
|
|
1109
|
-
if (trace.idx !== other.idx
|
|
1110
|
-
|
|
1116
|
+
if (trace.idx !== other.idx) {
|
|
1117
|
+
if (trace.pair.networkId && other.pair.networkId && trace.pair.networkId === other.pair.networkId) {
|
|
1118
|
+
continue;
|
|
1119
|
+
}
|
|
1120
|
+
if (chordContains(other.t1, other.t2, trace.t1, trace.t2, perimeter)) {
|
|
1121
|
+
containedBy[trace.idx].push(other.idx);
|
|
1122
|
+
contains[other.idx].push(trace.idx);
|
|
1123
|
+
}
|
|
1111
1124
|
}
|
|
1112
1125
|
}
|
|
1113
|
-
nestingDepth.set(trace.idx, depth);
|
|
1114
1126
|
}
|
|
1115
|
-
const
|
|
1127
|
+
const nestingDepth = containedBy.map((arr) => arr.length);
|
|
1128
|
+
const maxDepth = Math.max(...nestingDepth, 1);
|
|
1116
1129
|
this.traces = tracesWithT.map(({ pair, t1, t2, idx }) => {
|
|
1117
1130
|
const perpDir1 = getInwardPerpendicular(pair.start, bounds);
|
|
1118
1131
|
const perpDir2 = getInwardPerpendicular(pair.end, bounds);
|
|
@@ -1120,7 +1133,7 @@ var CurvyTraceSolver = class extends BaseSolver {
|
|
|
1120
1133
|
pair.end.x - pair.start.x,
|
|
1121
1134
|
pair.end.y - pair.start.y
|
|
1122
1135
|
);
|
|
1123
|
-
const depth = nestingDepth
|
|
1136
|
+
const depth = nestingDepth[idx];
|
|
1124
1137
|
const normalizedDepth = depth / maxDepth;
|
|
1125
1138
|
const midPoint = {
|
|
1126
1139
|
x: (pair.start.x + pair.end.x) / 2,
|
|
@@ -1156,7 +1169,9 @@ var CurvyTraceSolver = class extends BaseSolver {
|
|
|
1156
1169
|
perpDir1,
|
|
1157
1170
|
perpDir2,
|
|
1158
1171
|
d1,
|
|
1159
|
-
d2
|
|
1172
|
+
d2,
|
|
1173
|
+
containedBy: containedBy[idx],
|
|
1174
|
+
contains: contains[idx]
|
|
1160
1175
|
};
|
|
1161
1176
|
});
|
|
1162
1177
|
this.sampledPoints = this.traces.map(
|
|
@@ -1344,12 +1359,211 @@ var CurvyTraceSolver = class extends BaseSolver {
|
|
|
1344
1359
|
}
|
|
1345
1360
|
return cost;
|
|
1346
1361
|
}
|
|
1362
|
+
// Check if two traces intersect or are very close (potential intersection)
|
|
1363
|
+
// Uses higher resolution sampling for accuracy
|
|
1364
|
+
tracesIntersect(i, j) {
|
|
1365
|
+
const trace1 = this.traces[i];
|
|
1366
|
+
const trace2 = this.traces[j];
|
|
1367
|
+
const INTERSECT_SAMPLES = 15;
|
|
1368
|
+
const p1 = new Float64Array((INTERSECT_SAMPLES + 1) * 2);
|
|
1369
|
+
const p2 = new Float64Array((INTERSECT_SAMPLES + 1) * 2);
|
|
1370
|
+
sampleCubicBezierInline(
|
|
1371
|
+
trace1.waypointPair.start.x,
|
|
1372
|
+
trace1.waypointPair.start.y,
|
|
1373
|
+
trace1.ctrl1.x,
|
|
1374
|
+
trace1.ctrl1.y,
|
|
1375
|
+
trace1.ctrl2.x,
|
|
1376
|
+
trace1.ctrl2.y,
|
|
1377
|
+
trace1.waypointPair.end.x,
|
|
1378
|
+
trace1.waypointPair.end.y,
|
|
1379
|
+
p1,
|
|
1380
|
+
INTERSECT_SAMPLES
|
|
1381
|
+
);
|
|
1382
|
+
sampleCubicBezierInline(
|
|
1383
|
+
trace2.waypointPair.start.x,
|
|
1384
|
+
trace2.waypointPair.start.y,
|
|
1385
|
+
trace2.ctrl1.x,
|
|
1386
|
+
trace2.ctrl1.y,
|
|
1387
|
+
trace2.ctrl2.x,
|
|
1388
|
+
trace2.ctrl2.y,
|
|
1389
|
+
trace2.waypointPair.end.x,
|
|
1390
|
+
trace2.waypointPair.end.y,
|
|
1391
|
+
p2,
|
|
1392
|
+
INTERSECT_SAMPLES
|
|
1393
|
+
);
|
|
1394
|
+
for (let a = 0; a < INTERSECT_SAMPLES; a++) {
|
|
1395
|
+
const a1x = p1[a * 2];
|
|
1396
|
+
const a1y = p1[a * 2 + 1];
|
|
1397
|
+
const a2x = p1[(a + 1) * 2];
|
|
1398
|
+
const a2y = p1[(a + 1) * 2 + 1];
|
|
1399
|
+
for (let b = 0; b < INTERSECT_SAMPLES; b++) {
|
|
1400
|
+
const b1x = p2[b * 2];
|
|
1401
|
+
const b1y = p2[b * 2 + 1];
|
|
1402
|
+
const b2x = p2[(b + 1) * 2];
|
|
1403
|
+
const b2y = p2[(b + 1) * 2 + 1];
|
|
1404
|
+
if (segmentsIntersect(a1x, a1y, a2x, a2y, b1x, b1y, b2x, b2y)) {
|
|
1405
|
+
return true;
|
|
1406
|
+
}
|
|
1407
|
+
}
|
|
1408
|
+
}
|
|
1409
|
+
return false;
|
|
1410
|
+
}
|
|
1411
|
+
// Find all pairs of traces that currently intersect
|
|
1412
|
+
findIntersectingPairs() {
|
|
1413
|
+
const intersecting = [];
|
|
1414
|
+
for (let i = 0; i < this.traces.length; i++) {
|
|
1415
|
+
for (let j = i + 1; j < this.traces.length; j++) {
|
|
1416
|
+
const ti = this.traces[i];
|
|
1417
|
+
const tj = this.traces[j];
|
|
1418
|
+
if (ti.networkId && tj.networkId && ti.networkId === tj.networkId) {
|
|
1419
|
+
continue;
|
|
1420
|
+
}
|
|
1421
|
+
const bi = this.traceBounds[i];
|
|
1422
|
+
const bj = this.traceBounds[j];
|
|
1423
|
+
if (bi.maxX < bj.minX || bj.maxX < bi.minX || bi.maxY < bj.minY || bj.maxY < bi.minY) {
|
|
1424
|
+
continue;
|
|
1425
|
+
}
|
|
1426
|
+
if (this.tracesIntersect(i, j)) {
|
|
1427
|
+
intersecting.push([i, j]);
|
|
1428
|
+
}
|
|
1429
|
+
}
|
|
1430
|
+
}
|
|
1431
|
+
return intersecting;
|
|
1432
|
+
}
|
|
1433
|
+
// Resolve intersections by separating traces in depth
|
|
1434
|
+
// Only keeps changes if they improve overall cost
|
|
1435
|
+
resolveIntersections() {
|
|
1436
|
+
const { bounds, preferredTraceToTraceSpacing } = this.problem;
|
|
1437
|
+
const { minX, maxX, minY, maxY } = bounds;
|
|
1438
|
+
const minDim = Math.min(maxX - minX, maxY - minY);
|
|
1439
|
+
const minDist = minDim * 0.02;
|
|
1440
|
+
const maxDist = minDim * 1.5;
|
|
1441
|
+
const intersecting = this.findIntersectingPairs();
|
|
1442
|
+
if (intersecting.length === 0) return 0;
|
|
1443
|
+
let resolved = 0;
|
|
1444
|
+
for (const [i, j] of intersecting) {
|
|
1445
|
+
const ti = this.traces[i];
|
|
1446
|
+
const tj = this.traces[j];
|
|
1447
|
+
if (!this.tracesIntersect(i, j)) continue;
|
|
1448
|
+
let outerIdx;
|
|
1449
|
+
let innerIdx;
|
|
1450
|
+
if (ti.containedBy.includes(j)) {
|
|
1451
|
+
outerIdx = j;
|
|
1452
|
+
innerIdx = i;
|
|
1453
|
+
} else if (tj.containedBy.includes(i)) {
|
|
1454
|
+
outerIdx = i;
|
|
1455
|
+
innerIdx = j;
|
|
1456
|
+
} else {
|
|
1457
|
+
const avgDi = (ti.d1 + ti.d2) / 2;
|
|
1458
|
+
const avgDj = (tj.d1 + tj.d2) / 2;
|
|
1459
|
+
if (avgDi < avgDj) {
|
|
1460
|
+
outerIdx = i;
|
|
1461
|
+
innerIdx = j;
|
|
1462
|
+
} else {
|
|
1463
|
+
outerIdx = j;
|
|
1464
|
+
innerIdx = i;
|
|
1465
|
+
}
|
|
1466
|
+
}
|
|
1467
|
+
const outerTrace = this.traces[outerIdx];
|
|
1468
|
+
const innerTrace = this.traces[innerIdx];
|
|
1469
|
+
const origOuterD1 = outerTrace.d1;
|
|
1470
|
+
const origOuterD2 = outerTrace.d2;
|
|
1471
|
+
const origInnerD1 = innerTrace.d1;
|
|
1472
|
+
const origInnerD2 = innerTrace.d2;
|
|
1473
|
+
const costBeforeThis = this.computeTotalCost();
|
|
1474
|
+
const separation = preferredTraceToTraceSpacing * 2;
|
|
1475
|
+
const strategies = [
|
|
1476
|
+
// Strategy 1: Small increase for inner
|
|
1477
|
+
{ innerMult: 1, outerMult: 0 },
|
|
1478
|
+
// Strategy 2: Small decrease for outer
|
|
1479
|
+
{ innerMult: 0, outerMult: 1 },
|
|
1480
|
+
// Strategy 3: Both directions
|
|
1481
|
+
{ innerMult: 0.5, outerMult: 0.5 },
|
|
1482
|
+
// Strategy 4: Larger increase for inner
|
|
1483
|
+
{ innerMult: 2, outerMult: 0 },
|
|
1484
|
+
// Strategy 5: Both larger
|
|
1485
|
+
{ innerMult: 1, outerMult: 1 },
|
|
1486
|
+
// More aggressive
|
|
1487
|
+
{ innerMult: 3, outerMult: 0 },
|
|
1488
|
+
{ innerMult: 2, outerMult: 1 },
|
|
1489
|
+
{ innerMult: 4, outerMult: 0 }
|
|
1490
|
+
];
|
|
1491
|
+
let bestCost = costBeforeThis;
|
|
1492
|
+
let bestStrategy = null;
|
|
1493
|
+
for (const strategy of strategies) {
|
|
1494
|
+
outerTrace.d1 = origOuterD1;
|
|
1495
|
+
outerTrace.d2 = origOuterD2;
|
|
1496
|
+
innerTrace.d1 = origInnerD1;
|
|
1497
|
+
innerTrace.d2 = origInnerD2;
|
|
1498
|
+
innerTrace.d1 = Math.min(
|
|
1499
|
+
maxDist,
|
|
1500
|
+
origInnerD1 + separation * strategy.innerMult
|
|
1501
|
+
);
|
|
1502
|
+
innerTrace.d2 = Math.min(
|
|
1503
|
+
maxDist,
|
|
1504
|
+
origInnerD2 + separation * strategy.innerMult
|
|
1505
|
+
);
|
|
1506
|
+
outerTrace.d1 = Math.max(
|
|
1507
|
+
minDist,
|
|
1508
|
+
origOuterD1 - separation * strategy.outerMult
|
|
1509
|
+
);
|
|
1510
|
+
outerTrace.d2 = Math.max(
|
|
1511
|
+
minDist,
|
|
1512
|
+
origOuterD2 - separation * strategy.outerMult
|
|
1513
|
+
);
|
|
1514
|
+
this.updateControlPointsFromDistances(innerIdx);
|
|
1515
|
+
this.updateControlPointsFromDistances(outerIdx);
|
|
1516
|
+
this.updateSingleTraceSample(innerIdx);
|
|
1517
|
+
this.updateSingleTraceSample(outerIdx);
|
|
1518
|
+
if (!this.tracesIntersect(outerIdx, innerIdx)) {
|
|
1519
|
+
const newCost = this.computeTotalCost();
|
|
1520
|
+
if (bestStrategy === null || newCost < bestCost) {
|
|
1521
|
+
bestCost = newCost;
|
|
1522
|
+
bestStrategy = strategy;
|
|
1523
|
+
}
|
|
1524
|
+
}
|
|
1525
|
+
}
|
|
1526
|
+
if (bestStrategy) {
|
|
1527
|
+
innerTrace.d1 = Math.min(
|
|
1528
|
+
maxDist,
|
|
1529
|
+
origInnerD1 + separation * bestStrategy.innerMult
|
|
1530
|
+
);
|
|
1531
|
+
innerTrace.d2 = Math.min(
|
|
1532
|
+
maxDist,
|
|
1533
|
+
origInnerD2 + separation * bestStrategy.innerMult
|
|
1534
|
+
);
|
|
1535
|
+
outerTrace.d1 = Math.max(
|
|
1536
|
+
minDist,
|
|
1537
|
+
origOuterD1 - separation * bestStrategy.outerMult
|
|
1538
|
+
);
|
|
1539
|
+
outerTrace.d2 = Math.max(
|
|
1540
|
+
minDist,
|
|
1541
|
+
origOuterD2 - separation * bestStrategy.outerMult
|
|
1542
|
+
);
|
|
1543
|
+
this.updateControlPointsFromDistances(innerIdx);
|
|
1544
|
+
this.updateControlPointsFromDistances(outerIdx);
|
|
1545
|
+
this.updateSingleTraceSample(innerIdx);
|
|
1546
|
+
this.updateSingleTraceSample(outerIdx);
|
|
1547
|
+
resolved++;
|
|
1548
|
+
} else {
|
|
1549
|
+
outerTrace.d1 = origOuterD1;
|
|
1550
|
+
outerTrace.d2 = origOuterD2;
|
|
1551
|
+
innerTrace.d1 = origInnerD1;
|
|
1552
|
+
innerTrace.d2 = origInnerD2;
|
|
1553
|
+
this.updateControlPointsFromDistances(outerIdx);
|
|
1554
|
+
this.updateControlPointsFromDistances(innerIdx);
|
|
1555
|
+
this.updateSingleTraceSample(outerIdx);
|
|
1556
|
+
this.updateSingleTraceSample(innerIdx);
|
|
1557
|
+
}
|
|
1558
|
+
}
|
|
1559
|
+
return resolved;
|
|
1560
|
+
}
|
|
1347
1561
|
optimizeStep() {
|
|
1348
|
-
const { bounds } = this.problem;
|
|
1562
|
+
const { bounds, preferredTraceToTraceSpacing } = this.problem;
|
|
1349
1563
|
const { minX, maxX, minY, maxY } = bounds;
|
|
1350
1564
|
const minDim = Math.min(maxX - minX, maxY - minY);
|
|
1351
1565
|
const progress = this.optimizationStep / this.maxOptimizationSteps;
|
|
1352
|
-
const baseStep =
|
|
1566
|
+
const baseStep = 4 * (1 - progress) + 0.5;
|
|
1353
1567
|
const minDist = minDim * 0.02;
|
|
1354
1568
|
const maxDist = minDim * 1.5;
|
|
1355
1569
|
const traceCosts = [];
|
|
@@ -1360,9 +1574,19 @@ var CurvyTraceSolver = class extends BaseSolver {
|
|
|
1360
1574
|
for (const { idx: i, cost: currentCost } of traceCosts) {
|
|
1361
1575
|
if (currentCost === 0) continue;
|
|
1362
1576
|
const trace = this.traces[i];
|
|
1363
|
-
const
|
|
1577
|
+
const costMultiplier = currentCost > 100 ? 2 : 1;
|
|
1578
|
+
const steps = [baseStep * costMultiplier, baseStep * 1.5 * costMultiplier, baseStep * 0.5];
|
|
1364
1579
|
for (const step of steps) {
|
|
1365
|
-
const deltas =
|
|
1580
|
+
const deltas = currentCost > 100 ? [
|
|
1581
|
+
step,
|
|
1582
|
+
-step,
|
|
1583
|
+
step * 2,
|
|
1584
|
+
-step * 2,
|
|
1585
|
+
step * 3,
|
|
1586
|
+
-step * 3,
|
|
1587
|
+
preferredTraceToTraceSpacing * 2,
|
|
1588
|
+
-preferredTraceToTraceSpacing * 2
|
|
1589
|
+
] : [step, -step, step * 2, -step * 2];
|
|
1366
1590
|
let bestCost = this.computeCostForTrace(i);
|
|
1367
1591
|
let bestD1 = trace.d1;
|
|
1368
1592
|
let bestD2 = trace.d2;
|
|
@@ -1451,13 +1675,25 @@ var CurvyTraceSolver = class extends BaseSolver {
|
|
|
1451
1675
|
if (this.optimizationStep < this.maxOptimizationSteps) {
|
|
1452
1676
|
this.optimizeStep();
|
|
1453
1677
|
this.optimizationStep++;
|
|
1678
|
+
if (this.optimizationStep % 10 === 0) {
|
|
1679
|
+
const resolved = this.resolveIntersections();
|
|
1680
|
+
if (resolved > 0) {
|
|
1681
|
+
this.updateCollisionPairs();
|
|
1682
|
+
}
|
|
1683
|
+
}
|
|
1454
1684
|
const currentCost = this.computeTotalCost();
|
|
1455
1685
|
if (currentCost === 0) {
|
|
1456
1686
|
this.optimizationStep = this.maxOptimizationSteps;
|
|
1457
1687
|
} else if (currentCost >= this.lastCost * 0.99) {
|
|
1458
1688
|
this.stagnantSteps++;
|
|
1459
|
-
if (this.stagnantSteps >
|
|
1460
|
-
|
|
1689
|
+
if (this.stagnantSteps > 10) {
|
|
1690
|
+
const resolved = this.resolveIntersections();
|
|
1691
|
+
if (resolved > 0) {
|
|
1692
|
+
this.updateCollisionPairs();
|
|
1693
|
+
this.stagnantSteps = 0;
|
|
1694
|
+
} else if (this.stagnantSteps > 15) {
|
|
1695
|
+
this.optimizationStep = this.maxOptimizationSteps;
|
|
1696
|
+
}
|
|
1461
1697
|
}
|
|
1462
1698
|
} else {
|
|
1463
1699
|
this.stagnantSteps = 0;
|
|
@@ -1465,6 +1701,7 @@ var CurvyTraceSolver = class extends BaseSolver {
|
|
|
1465
1701
|
this.lastCost = currentCost;
|
|
1466
1702
|
}
|
|
1467
1703
|
if (this.optimizationStep >= this.maxOptimizationSteps) {
|
|
1704
|
+
this.resolveIntersections();
|
|
1468
1705
|
this.buildOutputTraces();
|
|
1469
1706
|
this.solved = true;
|
|
1470
1707
|
}
|