@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 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 nestingDepth = /* @__PURE__ */ new Map();
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 && chordContains(other.t1, other.t2, trace.t1, trace.t2, perimeter)) {
1110
- depth++;
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 maxDepth = Math.max(...Array.from(nestingDepth.values()), 1);
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.get(idx) || 0;
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 = 3.5 * (1 - progress) + 0.5;
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 steps = [baseStep, baseStep * 1.5, baseStep * 0.5];
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 = [step, -step, step * 2, -step * 2];
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 > 15) {
1460
- this.optimizationStep = this.maxOptimizationSteps;
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
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@tscircuit/curvy-trace-solver",
3
3
  "main": "dist/index.js",
4
- "version": "0.0.7",
4
+ "version": "0.0.8",
5
5
  "type": "module",
6
6
  "scripts": {
7
7
  "start": "cosmos",