@tscircuit/schematic-trace-solver 0.0.100 → 0.0.102
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 +13 -6
- package/dist/index.js +219 -67
- package/lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver2/SchematicTraceSingleLineSolver2.ts +26 -7
- package/lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver2/generateEndpointCollisionDetours.ts +5 -2
- package/lib/solvers/TraceOverlapShiftSolver/TraceOverlapIssueSolver/TraceOverlapIssueSolver.ts +168 -24
- package/lib/solvers/TraceOverlapShiftSolver/TraceOverlapShiftSolver.ts +128 -48
- package/package.json +1 -1
- package/site/bug-reports/bug-report-20260716T144856Z.page.tsx +4 -0
- package/site/bug-reports/bug-report-20260717T022934Z.page.tsx +4 -0
- package/site/bug-reports/bug-report-20260717T031704Z.page.tsx +4 -0
- package/tests/bug-reports/bug-report-20260706T213649Z/__snapshots__/bug-report-20260706T213649Z.snap.svg +3 -3
- package/tests/bug-reports/bug-report-20260706T220324Z/__snapshots__/bug-report-20260706T220324Z.snap.svg +3 -3
- package/tests/bug-reports/bug-report-20260707T092615Z/__snapshots__/bug-report-20260707T092615Z.snap.svg +5 -5
- package/tests/bug-reports/bug-report-20260707T140410Z/__snapshots__/bug-report-20260707T140410Z.snap.svg +3 -3
- package/tests/bug-reports/bug-report-20260716T144856Z/__snapshots__/bug-report-20260716T144856Z.snap.svg +149 -0
- package/tests/bug-reports/bug-report-20260716T144856Z/bug-report-20260716T144856Z.json +693 -0
- package/tests/bug-reports/bug-report-20260716T144856Z/bug-report-20260716T144856Z.test.ts +12 -0
- package/tests/bug-reports/bug-report-20260717T022934Z/__snapshots__/bug-report-20260717T022934Z.snap.svg +130 -0
- package/tests/bug-reports/bug-report-20260717T022934Z/bug-report-20260717T022934Z.json +543 -0
- package/tests/bug-reports/bug-report-20260717T022934Z/bug-report-20260717T022934Z.test.ts +12 -0
- package/tests/bug-reports/bug-report-20260717T031704Z/__snapshots__/bug-report-20260717T031704Z.snap.svg +48 -0
- package/tests/bug-reports/bug-report-20260717T031704Z/bug-report-20260717T031704Z.json +77 -0
- package/tests/bug-reports/bug-report-20260717T031704Z/bug-report-20260717T031704Z.test.ts +12 -0
- package/tests/examples/__snapshots__/example11.snap.svg +19 -23
- package/tests/examples/__snapshots__/example19.snap.svg +3 -5
- package/tests/examples/__snapshots__/example29.snap.svg +13 -13
- package/tests/examples/__snapshots__/example37.snap.svg +4 -2
- package/tests/examples/example16.test.ts +22 -0
- package/tests/repros/__snapshots__/repro-rp2040-gamepad-trace-alignment.snap.svg +2 -8
- package/tests/repros/__snapshots__/repro47-endpoint-obstacle-detour.snap.svg +19 -23
- package/tests/repros/__snapshots__/trace-overlap-box-resistor.snap.svg +10 -10
- package/tests/repros/repro47-endpoint-obstacle-detour.test.ts +32 -2
- package/tests/solvers/SchematicTraceSingleLineSolver2/generate-endpoint-collision-detours.test.ts +37 -0
- package/tests/solvers/TraceOverlapShiftSolver/TraceOverlapShiftSolver.test.ts +174 -0
package/dist/index.d.ts
CHANGED
|
@@ -239,8 +239,10 @@ interface OverlappingTraceSegmentLocator {
|
|
|
239
239
|
traceSegmentIndex: number;
|
|
240
240
|
}>;
|
|
241
241
|
}
|
|
242
|
+
type TraceInteractionKind = "overlap" | "point_contact";
|
|
242
243
|
declare class TraceOverlapIssueSolver extends BaseSolver {
|
|
243
244
|
overlappingTraceSegments: OverlappingTraceSegmentLocator[];
|
|
245
|
+
interactionKind: TraceInteractionKind;
|
|
244
246
|
traceNetIslands: Record<ConnNetId$1, Array<SolvedTracePath>>;
|
|
245
247
|
obstacleRects: ReturnType<typeof getObstacleRects>;
|
|
246
248
|
SHIFT_DISTANCE: number;
|
|
@@ -248,9 +250,12 @@ declare class TraceOverlapIssueSolver extends BaseSolver {
|
|
|
248
250
|
constructor(params: {
|
|
249
251
|
inputProblem: InputProblem;
|
|
250
252
|
overlappingTraceSegments: OverlappingTraceSegmentLocator[];
|
|
253
|
+
interactionKind: TraceInteractionKind;
|
|
251
254
|
traceNetIslands: Record<ConnNetId$1, Array<SolvedTracePath>>;
|
|
252
255
|
});
|
|
253
256
|
_step(): void;
|
|
257
|
+
private applyOffsets;
|
|
258
|
+
private countDifferentNetIntersections;
|
|
254
259
|
private getObstacleAwareOffset;
|
|
255
260
|
private getBlockingCollisions;
|
|
256
261
|
visualize(): GraphicsObject;
|
|
@@ -258,14 +263,15 @@ declare class TraceOverlapIssueSolver extends BaseSolver {
|
|
|
258
263
|
|
|
259
264
|
type ConnNetId = string;
|
|
260
265
|
/**
|
|
261
|
-
* This solver finds traces that overlap
|
|
262
|
-
* connected via the globalConnMap, then shifts them
|
|
263
|
-
*
|
|
266
|
+
* This solver finds traces that overlap or meet collinearly and aren't
|
|
267
|
+
* connected via the globalConnMap, then shifts them apart in the direction
|
|
268
|
+
* that minimizes the resulting intersections.
|
|
264
269
|
*
|
|
265
270
|
* All traces are orthogonal, so for traces to be considered overlapping, they
|
|
266
|
-
* need to each have a segment where both are horizontal or both are vertical
|
|
267
|
-
*
|
|
268
|
-
*
|
|
271
|
+
* need to each have a segment where both are horizontal or both are vertical,
|
|
272
|
+
* the segments must be on the same axis, and their ranges must overlap. A
|
|
273
|
+
* point contact is also handled when both segments are internal rails, where
|
|
274
|
+
* it can be separated without moving an anchored terminal segment.
|
|
269
275
|
*
|
|
270
276
|
* Each iteration, we find overlapping traces that aren't part of the same net.
|
|
271
277
|
* This is the same as finding two "trace net islands" that have an overlap.
|
|
@@ -294,6 +300,7 @@ declare class TraceOverlapShiftSolver extends BaseSolver {
|
|
|
294
300
|
computeTraceNetIslands(): Record<ConnNetId, Array<SolvedTracePath>>;
|
|
295
301
|
findNextOverlapIssue(): {
|
|
296
302
|
overlappingTraceSegments: Array<OverlappingTraceSegmentLocator>;
|
|
303
|
+
interactionKind: TraceInteractionKind;
|
|
297
304
|
} | null;
|
|
298
305
|
private findNextDiagonalSegment;
|
|
299
306
|
private findAndFixNextDiagonalSegment;
|
package/dist/index.js
CHANGED
|
@@ -894,7 +894,7 @@ var generateEndpointCollisionDetours = ({
|
|
|
894
894
|
collidingSegmentIndex,
|
|
895
895
|
obstacle
|
|
896
896
|
}) => {
|
|
897
|
-
if (path.length
|
|
897
|
+
if (path.length < 3 || path.length > 4) return [];
|
|
898
898
|
const lastSegmentIndex = path.length - 2;
|
|
899
899
|
if (collidingSegmentIndex !== 0 && collidingSegmentIndex !== lastSegmentIndex) {
|
|
900
900
|
return [];
|
|
@@ -917,7 +917,7 @@ var generateEndpointCollisionDetours = ({
|
|
|
917
917
|
const detours = [];
|
|
918
918
|
for (const escapeCoordinate of [...new Set(escapeCoordinates)]) {
|
|
919
919
|
for (const detourCoordinate of [...new Set(detourCoordinates)]) {
|
|
920
|
-
const
|
|
920
|
+
const orderedDetourPrefix = firstSegmentAxis === "y" ? [
|
|
921
921
|
start,
|
|
922
922
|
{ x: escapeCoordinate, y: start.y },
|
|
923
923
|
{ x: escapeCoordinate, y: detourCoordinate },
|
|
@@ -930,6 +930,7 @@ var generateEndpointCollisionDetours = ({
|
|
|
930
930
|
{ x: detourCoordinate, y: end.y },
|
|
931
931
|
end
|
|
932
932
|
];
|
|
933
|
+
const orderedDetour = [...orderedDetourPrefix, ...orderedPath.slice(3)];
|
|
933
934
|
const detour = shouldReverse ? orderedDetour.reverse() : orderedDetour;
|
|
934
935
|
if (hasOnlyNonzeroOrthogonalSegments(detour)) detours.push(detour);
|
|
935
936
|
}
|
|
@@ -1287,7 +1288,10 @@ var SchematicTraceSingleLineSolver2 = class extends BaseSolver {
|
|
|
1287
1288
|
let { segIndex, rect } = collision;
|
|
1288
1289
|
const isFirstSegment = segIndex === 0;
|
|
1289
1290
|
const isLastSegment = segIndex === path.length - 2;
|
|
1290
|
-
|
|
1291
|
+
const isEndpointChipObstacle = rect.kind === "chip" && this.pins.some((pin) => pin.chipId === rect.chipId);
|
|
1292
|
+
const canGenerateEndpointDetour = path.length === 3 || path.length === 4 && this.connectionPair !== void 0 && !isEndpointChipObstacle;
|
|
1293
|
+
if (canGenerateEndpointDetour && (isFirstSegment || isLastSegment)) {
|
|
1294
|
+
const compareDetours = path.length === 4 ? (a2, b2) => this.getPinBandPenalty(a2) - this.getPinBandPenalty(b2) || this.pathLength(a2) - this.pathLength(b2) : (a2, b2) => this.pathLength(a2) - this.pathLength(b2) || this.getPinBandPenalty(a2) - this.getPinBandPenalty(b2);
|
|
1291
1295
|
const detours = generateEndpointCollisionDetours({
|
|
1292
1296
|
path,
|
|
1293
1297
|
collidingSegmentIndex: segIndex,
|
|
@@ -1297,9 +1301,7 @@ var SchematicTraceSingleLineSolver2 = class extends BaseSolver {
|
|
|
1297
1301
|
if (this.visited.has(key)) return false;
|
|
1298
1302
|
this.visited.add(key);
|
|
1299
1303
|
return true;
|
|
1300
|
-
}).sort(
|
|
1301
|
-
(a2, b2) => this.pathLength(a2) - this.pathLength(b2) || this.getPinBandPenalty(a2) - this.getPinBandPenalty(b2)
|
|
1302
|
-
);
|
|
1304
|
+
}).sort(compareDetours);
|
|
1303
1305
|
for (const detour of detours) {
|
|
1304
1306
|
const nextCollisionRects = new Set(collisionRects);
|
|
1305
1307
|
nextCollisionRects.add(rect);
|
|
@@ -1539,6 +1541,9 @@ var SchematicTraceLinesSolver = class extends BaseSolver {
|
|
|
1539
1541
|
}
|
|
1540
1542
|
};
|
|
1541
1543
|
|
|
1544
|
+
// lib/solvers/TraceOverlapShiftSolver/TraceOverlapIssueSolver/TraceOverlapIssueSolver.ts
|
|
1545
|
+
import { doSegmentsIntersect } from "@tscircuit/math-utils";
|
|
1546
|
+
|
|
1542
1547
|
// lib/solvers/TraceOverlapShiftSolver/TraceOverlapIssueSolver/applyJogToTrace.ts
|
|
1543
1548
|
var applyJogToTerminalSegment = ({
|
|
1544
1549
|
pts,
|
|
@@ -1627,6 +1632,7 @@ var applyJogToTerminalSegment = ({
|
|
|
1627
1632
|
var EPS4 = 1e-6;
|
|
1628
1633
|
var TraceOverlapIssueSolver = class extends BaseSolver {
|
|
1629
1634
|
overlappingTraceSegments;
|
|
1635
|
+
interactionKind;
|
|
1630
1636
|
traceNetIslands;
|
|
1631
1637
|
obstacleRects;
|
|
1632
1638
|
SHIFT_DISTANCE = 0.1;
|
|
@@ -1634,6 +1640,7 @@ var TraceOverlapIssueSolver = class extends BaseSolver {
|
|
|
1634
1640
|
constructor(params) {
|
|
1635
1641
|
super();
|
|
1636
1642
|
this.overlappingTraceSegments = params.overlappingTraceSegments;
|
|
1643
|
+
this.interactionKind = params.interactionKind;
|
|
1637
1644
|
this.traceNetIslands = params.traceNetIslands;
|
|
1638
1645
|
this.obstacleRects = getObstacleRects(params.inputProblem);
|
|
1639
1646
|
for (const { connNetId, pathsWithOverlap } of this.overlappingTraceSegments) {
|
|
@@ -1657,19 +1664,64 @@ var TraceOverlapIssueSolver = class extends BaseSolver {
|
|
|
1657
1664
|
const someGroupCanShift = groupShouldStayInPlace.some(
|
|
1658
1665
|
(shouldStay) => !shouldStay
|
|
1659
1666
|
);
|
|
1660
|
-
const
|
|
1661
|
-
if (someGroupCanShift && groupShouldStayInPlace[
|
|
1662
|
-
const n = Math.floor(
|
|
1663
|
-
const
|
|
1667
|
+
const getSeparationOffsets = (firstDirection) => this.overlappingTraceSegments.map((group, groupIndex) => {
|
|
1668
|
+
if (someGroupCanShift && groupShouldStayInPlace[groupIndex]) return 0;
|
|
1669
|
+
const n = Math.floor(groupIndex / 2) + 1;
|
|
1670
|
+
const direction = groupIndex % 2 === 0 ? firstDirection : -firstDirection;
|
|
1664
1671
|
return this.getObstacleAwareOffset({
|
|
1665
1672
|
group,
|
|
1666
|
-
offset:
|
|
1673
|
+
offset: direction * n * this.SHIFT_DISTANCE
|
|
1667
1674
|
});
|
|
1668
1675
|
});
|
|
1676
|
+
const establishedOffsets = getSeparationOffsets(-1);
|
|
1677
|
+
const isCompoundOverlap = this.interactionKind === "overlap" && this.overlappingTraceSegments.some(
|
|
1678
|
+
(group) => group.pathsWithOverlap.length > 1
|
|
1679
|
+
);
|
|
1680
|
+
if (isCompoundOverlap) {
|
|
1681
|
+
this.correctedTraceMap = this.applyOffsets(establishedOffsets);
|
|
1682
|
+
this.solved = true;
|
|
1683
|
+
return;
|
|
1684
|
+
}
|
|
1685
|
+
const offsetCandidates = this.interactionKind === "overlap" ? [establishedOffsets, getSeparationOffsets(1)] : [];
|
|
1686
|
+
if (this.interactionKind === "point_contact") {
|
|
1687
|
+
const movableGroupIndexes = this.overlappingTraceSegments.map((_, groupIndex) => groupIndex).filter(
|
|
1688
|
+
(groupIndex) => !someGroupCanShift || !groupShouldStayInPlace[groupIndex]
|
|
1689
|
+
).reverse();
|
|
1690
|
+
for (const groupIndex of movableGroupIndexes) {
|
|
1691
|
+
const group = this.overlappingTraceSegments[groupIndex];
|
|
1692
|
+
for (const direction of [-1, 1]) {
|
|
1693
|
+
const offsets = this.overlappingTraceSegments.map(() => 0);
|
|
1694
|
+
offsets[groupIndex] = this.getObstacleAwareOffset({
|
|
1695
|
+
group,
|
|
1696
|
+
offset: direction * this.SHIFT_DISTANCE
|
|
1697
|
+
});
|
|
1698
|
+
offsetCandidates.push(offsets);
|
|
1699
|
+
}
|
|
1700
|
+
}
|
|
1701
|
+
}
|
|
1702
|
+
const candidates = offsetCandidates.map((offsets) => {
|
|
1703
|
+
const correctedTraceMap = this.applyOffsets(offsets);
|
|
1704
|
+
return {
|
|
1705
|
+
correctedTraceMap,
|
|
1706
|
+
intersectionCount: this.countDifferentNetIntersections(correctedTraceMap),
|
|
1707
|
+
totalDisplacement: offsets.reduce(
|
|
1708
|
+
(total, offset) => total + Math.abs(offset),
|
|
1709
|
+
0
|
|
1710
|
+
)
|
|
1711
|
+
};
|
|
1712
|
+
});
|
|
1713
|
+
candidates.sort(
|
|
1714
|
+
(a, b) => a.intersectionCount - b.intersectionCount || a.totalDisplacement - b.totalDisplacement
|
|
1715
|
+
);
|
|
1716
|
+
this.correctedTraceMap = candidates[0].correctedTraceMap;
|
|
1717
|
+
this.solved = true;
|
|
1718
|
+
}
|
|
1719
|
+
applyOffsets(offsets) {
|
|
1720
|
+
const correctedTraceMap = { ...this.correctedTraceMap };
|
|
1669
1721
|
const eq = (a, b) => Math.abs(a - b) < EPS4;
|
|
1670
1722
|
const samePoint = (p, q) => !!p && !!q && eq(p.x, q.x) && eq(p.y, q.y);
|
|
1671
|
-
this.overlappingTraceSegments.forEach((group,
|
|
1672
|
-
const offset = offsets[
|
|
1723
|
+
this.overlappingTraceSegments.forEach((group, groupIndex) => {
|
|
1724
|
+
const offset = offsets[groupIndex];
|
|
1673
1725
|
if (offset === 0) return;
|
|
1674
1726
|
const byPath = /* @__PURE__ */ new Map();
|
|
1675
1727
|
for (const loc of group.pathsWithOverlap) {
|
|
@@ -1680,9 +1732,22 @@ var TraceOverlapIssueSolver = class extends BaseSolver {
|
|
|
1680
1732
|
}
|
|
1681
1733
|
for (const [pathIdx, segIdxSet] of byPath) {
|
|
1682
1734
|
const original = this.traceNetIslands[group.connNetId][pathIdx];
|
|
1683
|
-
const current =
|
|
1735
|
+
const current = correctedTraceMap[original.mspPairId] ?? original;
|
|
1684
1736
|
const pts = current.tracePath.map((p) => ({ ...p }));
|
|
1685
1737
|
const segIdxsRev = Array.from(segIdxSet).sort((a, b) => a - b).reverse();
|
|
1738
|
+
const segmentAxes = /* @__PURE__ */ new Map();
|
|
1739
|
+
for (const si of segIdxsRev) {
|
|
1740
|
+
const start = pts[si];
|
|
1741
|
+
const end = pts[si + 1];
|
|
1742
|
+
if (!start || !end) continue;
|
|
1743
|
+
if (Math.abs(start.x - end.x) < EPS4) {
|
|
1744
|
+
segmentAxes.set(si, "vertical");
|
|
1745
|
+
} else if (Math.abs(start.y - end.y) < EPS4) {
|
|
1746
|
+
segmentAxes.set(si, "horizontal");
|
|
1747
|
+
}
|
|
1748
|
+
}
|
|
1749
|
+
const shiftedXPointIndexes = /* @__PURE__ */ new Set();
|
|
1750
|
+
const shiftedYPointIndexes = /* @__PURE__ */ new Set();
|
|
1686
1751
|
const JOG_SIZE = this.SHIFT_DISTANCE;
|
|
1687
1752
|
for (const si of segIdxsRev) {
|
|
1688
1753
|
if (si < 0 || si >= pts.length - 1) continue;
|
|
@@ -1697,15 +1762,25 @@ var TraceOverlapIssueSolver = class extends BaseSolver {
|
|
|
1697
1762
|
} else {
|
|
1698
1763
|
const start = pts[si];
|
|
1699
1764
|
const end = pts[si + 1];
|
|
1700
|
-
const
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1765
|
+
const axis = segmentAxes.get(si);
|
|
1766
|
+
if (axis === "vertical") {
|
|
1767
|
+
if (!shiftedXPointIndexes.has(si)) {
|
|
1768
|
+
start.x += offset;
|
|
1769
|
+
shiftedXPointIndexes.add(si);
|
|
1770
|
+
}
|
|
1771
|
+
if (!shiftedXPointIndexes.has(si + 1)) {
|
|
1772
|
+
end.x += offset;
|
|
1773
|
+
shiftedXPointIndexes.add(si + 1);
|
|
1774
|
+
}
|
|
1775
|
+
} else if (axis === "horizontal") {
|
|
1776
|
+
if (!shiftedYPointIndexes.has(si)) {
|
|
1777
|
+
start.y += offset;
|
|
1778
|
+
shiftedYPointIndexes.add(si);
|
|
1779
|
+
}
|
|
1780
|
+
if (!shiftedYPointIndexes.has(si + 1)) {
|
|
1781
|
+
end.y += offset;
|
|
1782
|
+
shiftedYPointIndexes.add(si + 1);
|
|
1783
|
+
}
|
|
1709
1784
|
}
|
|
1710
1785
|
}
|
|
1711
1786
|
}
|
|
@@ -1715,13 +1790,39 @@ var TraceOverlapIssueSolver = class extends BaseSolver {
|
|
|
1715
1790
|
cleaned.push(p);
|
|
1716
1791
|
}
|
|
1717
1792
|
}
|
|
1718
|
-
|
|
1793
|
+
correctedTraceMap[original.mspPairId] = {
|
|
1719
1794
|
...current,
|
|
1720
1795
|
tracePath: cleaned
|
|
1721
1796
|
};
|
|
1722
1797
|
}
|
|
1723
1798
|
});
|
|
1724
|
-
|
|
1799
|
+
return correctedTraceMap;
|
|
1800
|
+
}
|
|
1801
|
+
countDifferentNetIntersections(correctedTraceMap) {
|
|
1802
|
+
const traces = Object.values(this.traceNetIslands).flatMap(
|
|
1803
|
+
(island) => island.map((trace) => correctedTraceMap[trace.mspPairId] ?? trace)
|
|
1804
|
+
);
|
|
1805
|
+
let intersectionCount = 0;
|
|
1806
|
+
for (let traceIndex = 0; traceIndex < traces.length; traceIndex++) {
|
|
1807
|
+
const trace = traces[traceIndex];
|
|
1808
|
+
for (let otherTraceIndex = traceIndex + 1; otherTraceIndex < traces.length; otherTraceIndex++) {
|
|
1809
|
+
const otherTrace = traces[otherTraceIndex];
|
|
1810
|
+
if (trace.globalConnNetId === otherTrace.globalConnNetId) continue;
|
|
1811
|
+
for (let segmentIndex = 0; segmentIndex < trace.tracePath.length - 1; segmentIndex++) {
|
|
1812
|
+
for (let otherSegmentIndex = 0; otherSegmentIndex < otherTrace.tracePath.length - 1; otherSegmentIndex++) {
|
|
1813
|
+
if (doSegmentsIntersect(
|
|
1814
|
+
trace.tracePath[segmentIndex],
|
|
1815
|
+
trace.tracePath[segmentIndex + 1],
|
|
1816
|
+
otherTrace.tracePath[otherSegmentIndex],
|
|
1817
|
+
otherTrace.tracePath[otherSegmentIndex + 1]
|
|
1818
|
+
)) {
|
|
1819
|
+
intersectionCount++;
|
|
1820
|
+
}
|
|
1821
|
+
}
|
|
1822
|
+
}
|
|
1823
|
+
}
|
|
1824
|
+
}
|
|
1825
|
+
return intersectionCount;
|
|
1725
1826
|
}
|
|
1726
1827
|
getObstacleAwareOffset({
|
|
1727
1828
|
group,
|
|
@@ -1883,15 +1984,58 @@ var TraceOverlapShiftSolver = class extends BaseSolver {
|
|
|
1883
1984
|
const pathsB = this.traceNetIslands[netB] || [];
|
|
1884
1985
|
const overlapsA = [];
|
|
1885
1986
|
const overlapsB = [];
|
|
1987
|
+
const pointContacts = [];
|
|
1886
1988
|
const seenA = /* @__PURE__ */ new Set();
|
|
1887
1989
|
const seenB = /* @__PURE__ */ new Set();
|
|
1888
|
-
const
|
|
1990
|
+
const getRangeOverlap1D = (a1, a2, b1, b2) => {
|
|
1889
1991
|
const minA = Math.min(a1, a2);
|
|
1890
1992
|
const maxA = Math.max(a1, a2);
|
|
1891
1993
|
const minB = Math.min(b1, b2);
|
|
1892
1994
|
const maxB = Math.max(b1, b2);
|
|
1893
|
-
|
|
1894
|
-
|
|
1995
|
+
return Math.min(maxA, maxB) - Math.max(minA, minB);
|
|
1996
|
+
};
|
|
1997
|
+
const recordCollinearInteraction = ({
|
|
1998
|
+
pathAIndex,
|
|
1999
|
+
segmentAIndex,
|
|
2000
|
+
pathBIndex,
|
|
2001
|
+
segmentBIndex,
|
|
2002
|
+
rangeOverlap
|
|
2003
|
+
}) => {
|
|
2004
|
+
if (rangeOverlap > EPS11) {
|
|
2005
|
+
const keyA = `${pathAIndex}:${segmentAIndex}`;
|
|
2006
|
+
const keyB = `${pathBIndex}:${segmentBIndex}`;
|
|
2007
|
+
if (!seenA.has(keyA)) {
|
|
2008
|
+
overlapsA.push({
|
|
2009
|
+
solvedTracePathIndex: pathAIndex,
|
|
2010
|
+
traceSegmentIndex: segmentAIndex
|
|
2011
|
+
});
|
|
2012
|
+
seenA.add(keyA);
|
|
2013
|
+
}
|
|
2014
|
+
if (!seenB.has(keyB)) {
|
|
2015
|
+
overlapsB.push({
|
|
2016
|
+
solvedTracePathIndex: pathBIndex,
|
|
2017
|
+
traceSegmentIndex: segmentBIndex
|
|
2018
|
+
});
|
|
2019
|
+
seenB.add(keyB);
|
|
2020
|
+
}
|
|
2021
|
+
return;
|
|
2022
|
+
}
|
|
2023
|
+
if (rangeOverlap < -EPS11) return;
|
|
2024
|
+
const pathA = pathsA[pathAIndex].tracePath;
|
|
2025
|
+
const pathB = pathsB[pathBIndex].tracePath;
|
|
2026
|
+
const segmentAStart = pathA[segmentAIndex];
|
|
2027
|
+
const segmentAEnd = pathA[segmentAIndex + 1];
|
|
2028
|
+
const segmentBStart = pathB[segmentBIndex];
|
|
2029
|
+
const segmentBEnd = pathB[segmentBIndex + 1];
|
|
2030
|
+
const hasTerminalSegment = segmentAIndex === 0 || segmentAIndex === pathA.length - 2 || segmentBIndex === 0 || segmentBIndex === pathB.length - 2;
|
|
2031
|
+
if (hasTerminalSegment) return;
|
|
2032
|
+
pointContacts.push({
|
|
2033
|
+
pathAIndex,
|
|
2034
|
+
segmentAIndex,
|
|
2035
|
+
pathBIndex,
|
|
2036
|
+
segmentBIndex,
|
|
2037
|
+
combinedLength: Math.abs(segmentAStart.x - segmentAEnd.x) + Math.abs(segmentAStart.y - segmentAEnd.y) + Math.abs(segmentBStart.x - segmentBEnd.x) + Math.abs(segmentBStart.y - segmentBEnd.y)
|
|
2038
|
+
});
|
|
1895
2039
|
};
|
|
1896
2040
|
for (let pa = 0; pa < pathsA.length; pa++) {
|
|
1897
2041
|
const pathA = pathsA[pa];
|
|
@@ -1913,45 +2057,23 @@ var TraceOverlapShiftSolver = class extends BaseSolver {
|
|
|
1913
2057
|
if (!bVert && !bHorz) continue;
|
|
1914
2058
|
if (aVert && bVert) {
|
|
1915
2059
|
if (Math.abs(a1.x - b1.x) < EPS11) {
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
});
|
|
1924
|
-
seenA.add(keyA);
|
|
1925
|
-
}
|
|
1926
|
-
if (!seenB.has(keyB)) {
|
|
1927
|
-
overlapsB.push({
|
|
1928
|
-
solvedTracePathIndex: pb,
|
|
1929
|
-
traceSegmentIndex: sb
|
|
1930
|
-
});
|
|
1931
|
-
seenB.add(keyB);
|
|
1932
|
-
}
|
|
1933
|
-
}
|
|
2060
|
+
recordCollinearInteraction({
|
|
2061
|
+
pathAIndex: pa,
|
|
2062
|
+
segmentAIndex: sa,
|
|
2063
|
+
pathBIndex: pb,
|
|
2064
|
+
segmentBIndex: sb,
|
|
2065
|
+
rangeOverlap: getRangeOverlap1D(a1.y, a2.y, b1.y, b2.y)
|
|
2066
|
+
});
|
|
1934
2067
|
}
|
|
1935
2068
|
} else if (aHorz && bHorz) {
|
|
1936
2069
|
if (Math.abs(a1.y - b1.y) < EPS11) {
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
});
|
|
1945
|
-
seenA.add(keyA);
|
|
1946
|
-
}
|
|
1947
|
-
if (!seenB.has(keyB)) {
|
|
1948
|
-
overlapsB.push({
|
|
1949
|
-
solvedTracePathIndex: pb,
|
|
1950
|
-
traceSegmentIndex: sb
|
|
1951
|
-
});
|
|
1952
|
-
seenB.add(keyB);
|
|
1953
|
-
}
|
|
1954
|
-
}
|
|
2070
|
+
recordCollinearInteraction({
|
|
2071
|
+
pathAIndex: pa,
|
|
2072
|
+
segmentAIndex: sa,
|
|
2073
|
+
pathBIndex: pb,
|
|
2074
|
+
segmentBIndex: sb,
|
|
2075
|
+
rangeOverlap: getRangeOverlap1D(a1.x, a2.x, b1.x, b2.x)
|
|
2076
|
+
});
|
|
1955
2077
|
}
|
|
1956
2078
|
}
|
|
1957
2079
|
}
|
|
@@ -1960,12 +2082,41 @@ var TraceOverlapShiftSolver = class extends BaseSolver {
|
|
|
1960
2082
|
}
|
|
1961
2083
|
if (overlapsA.length > 0 && overlapsB.length > 0) {
|
|
1962
2084
|
return {
|
|
2085
|
+
interactionKind: "overlap",
|
|
1963
2086
|
overlappingTraceSegments: [
|
|
1964
2087
|
{ connNetId: netA, pathsWithOverlap: overlapsA },
|
|
1965
2088
|
{ connNetId: netB, pathsWithOverlap: overlapsB }
|
|
1966
2089
|
]
|
|
1967
2090
|
};
|
|
1968
2091
|
}
|
|
2092
|
+
const bestPointContact = pointContacts.sort(
|
|
2093
|
+
(a, b) => b.combinedLength - a.combinedLength
|
|
2094
|
+
)[0];
|
|
2095
|
+
if (bestPointContact) {
|
|
2096
|
+
return {
|
|
2097
|
+
interactionKind: "point_contact",
|
|
2098
|
+
overlappingTraceSegments: [
|
|
2099
|
+
{
|
|
2100
|
+
connNetId: netA,
|
|
2101
|
+
pathsWithOverlap: [
|
|
2102
|
+
{
|
|
2103
|
+
solvedTracePathIndex: bestPointContact.pathAIndex,
|
|
2104
|
+
traceSegmentIndex: bestPointContact.segmentAIndex
|
|
2105
|
+
}
|
|
2106
|
+
]
|
|
2107
|
+
},
|
|
2108
|
+
{
|
|
2109
|
+
connNetId: netB,
|
|
2110
|
+
pathsWithOverlap: [
|
|
2111
|
+
{
|
|
2112
|
+
solvedTracePathIndex: bestPointContact.pathBIndex,
|
|
2113
|
+
traceSegmentIndex: bestPointContact.segmentBIndex
|
|
2114
|
+
}
|
|
2115
|
+
]
|
|
2116
|
+
}
|
|
2117
|
+
]
|
|
2118
|
+
};
|
|
2119
|
+
}
|
|
1969
2120
|
}
|
|
1970
2121
|
}
|
|
1971
2122
|
return null;
|
|
@@ -2041,9 +2192,10 @@ var TraceOverlapShiftSolver = class extends BaseSolver {
|
|
|
2041
2192
|
this.solved = true;
|
|
2042
2193
|
return;
|
|
2043
2194
|
}
|
|
2044
|
-
const { overlappingTraceSegments } = overlapIssue;
|
|
2195
|
+
const { interactionKind, overlappingTraceSegments } = overlapIssue;
|
|
2045
2196
|
this.activeSubSolver = new TraceOverlapIssueSolver({
|
|
2046
2197
|
inputProblem: this.inputProblem,
|
|
2198
|
+
interactionKind,
|
|
2047
2199
|
overlappingTraceSegments,
|
|
2048
2200
|
traceNetIslands: this.traceNetIslands
|
|
2049
2201
|
});
|
|
@@ -4162,7 +4314,7 @@ var expandChipsToFitPins = (problem) => {
|
|
|
4162
4314
|
};
|
|
4163
4315
|
|
|
4164
4316
|
// lib/utils/does-trace-overlap-with-existing-traces.ts
|
|
4165
|
-
import { doSegmentsIntersect } from "@tscircuit/math-utils";
|
|
4317
|
+
import { doSegmentsIntersect as doSegmentsIntersect2 } from "@tscircuit/math-utils";
|
|
4166
4318
|
function doesTraceOverlapWithExistingTraces(newTracePath, existingTraces) {
|
|
4167
4319
|
for (let i = 0; i < newTracePath.length - 1; i++) {
|
|
4168
4320
|
const newSegmentP1 = newTracePath[i];
|
|
@@ -4171,7 +4323,7 @@ function doesTraceOverlapWithExistingTraces(newTracePath, existingTraces) {
|
|
|
4171
4323
|
for (let j = 0; j < existingTrace.tracePath.length - 1; j++) {
|
|
4172
4324
|
const existingSegmentP1 = existingTrace.tracePath[j];
|
|
4173
4325
|
const existingSegmentP2 = existingTrace.tracePath[j + 1];
|
|
4174
|
-
if (
|
|
4326
|
+
if (doSegmentsIntersect2(
|
|
4175
4327
|
newSegmentP1,
|
|
4176
4328
|
newSegmentP2,
|
|
4177
4329
|
existingSegmentP1,
|
|
@@ -357,8 +357,31 @@ export class SchematicTraceSingleLineSolver2 extends BaseSolver {
|
|
|
357
357
|
// Never move the first or last segments - move adjacent segment instead
|
|
358
358
|
const isFirstSegment = segIndex === 0
|
|
359
359
|
const isLastSegment = segIndex === path.length - 2
|
|
360
|
-
|
|
361
|
-
|
|
360
|
+
const isEndpointChipObstacle =
|
|
361
|
+
rect.kind === "chip" &&
|
|
362
|
+
this.pins.some((pin) => pin.chipId === rect.chipId)
|
|
363
|
+
// U-shaped detours are local repairs for fixed MSP pairs obstructed by an
|
|
364
|
+
// intermediate obstacle. Endpoint-chip and optional long-distance routing
|
|
365
|
+
// require multi-trace or net-level selection instead.
|
|
366
|
+
const canGenerateEndpointDetour =
|
|
367
|
+
path.length === 3 ||
|
|
368
|
+
(path.length === 4 &&
|
|
369
|
+
this.connectionPair !== undefined &&
|
|
370
|
+
!isEndpointChipObstacle)
|
|
371
|
+
|
|
372
|
+
if (canGenerateEndpointDetour && (isFirstSegment || isLastSegment)) {
|
|
373
|
+
// A three-point detour replaces an L elbow, so the shortest valid route
|
|
374
|
+
// remains the best choice. A four-point detour preserves the far side of
|
|
375
|
+
// a U elbow; prefer the candidate that keeps its new internal rails out
|
|
376
|
+
// of the pin band, then use length to choose between equivalent routes.
|
|
377
|
+
const compareDetours =
|
|
378
|
+
path.length === 4
|
|
379
|
+
? (a: Point[], b: Point[]) =>
|
|
380
|
+
this.getPinBandPenalty(a) - this.getPinBandPenalty(b) ||
|
|
381
|
+
this.pathLength(a) - this.pathLength(b)
|
|
382
|
+
: (a: Point[], b: Point[]) =>
|
|
383
|
+
this.pathLength(a) - this.pathLength(b) ||
|
|
384
|
+
this.getPinBandPenalty(a) - this.getPinBandPenalty(b)
|
|
362
385
|
const detours = generateEndpointCollisionDetours({
|
|
363
386
|
path,
|
|
364
387
|
collidingSegmentIndex: segIndex,
|
|
@@ -370,11 +393,7 @@ export class SchematicTraceSingleLineSolver2 extends BaseSolver {
|
|
|
370
393
|
this.visited.add(key)
|
|
371
394
|
return true
|
|
372
395
|
})
|
|
373
|
-
.sort(
|
|
374
|
-
(a, b) =>
|
|
375
|
-
this.pathLength(a) - this.pathLength(b) ||
|
|
376
|
-
this.getPinBandPenalty(a) - this.getPinBandPenalty(b),
|
|
377
|
-
)
|
|
396
|
+
.sort(compareDetours)
|
|
378
397
|
|
|
379
398
|
for (const detour of detours) {
|
|
380
399
|
const nextCollisionRects = new Set(collisionRects)
|
|
@@ -28,7 +28,9 @@ export const generateEndpointCollisionDetours = ({
|
|
|
28
28
|
collidingSegmentIndex: number
|
|
29
29
|
obstacle: ObstacleRect
|
|
30
30
|
}): Point[][] => {
|
|
31
|
-
|
|
31
|
+
// Endpoint detours expand the initial L- and U-shaped elbow candidates.
|
|
32
|
+
// Longer paths have already been expanded and are handled by segment shifts.
|
|
33
|
+
if (path.length < 3 || path.length > 4) return []
|
|
32
34
|
|
|
33
35
|
const lastSegmentIndex = path.length - 2
|
|
34
36
|
if (
|
|
@@ -58,7 +60,7 @@ export const generateEndpointCollisionDetours = ({
|
|
|
58
60
|
const detours: Point[][] = []
|
|
59
61
|
for (const escapeCoordinate of [...new Set(escapeCoordinates)]) {
|
|
60
62
|
for (const detourCoordinate of [...new Set(detourCoordinates)]) {
|
|
61
|
-
const
|
|
63
|
+
const orderedDetourPrefix =
|
|
62
64
|
firstSegmentAxis === "y"
|
|
63
65
|
? [
|
|
64
66
|
start!,
|
|
@@ -75,6 +77,7 @@ export const generateEndpointCollisionDetours = ({
|
|
|
75
77
|
end!,
|
|
76
78
|
]
|
|
77
79
|
|
|
80
|
+
const orderedDetour = [...orderedDetourPrefix, ...orderedPath.slice(3)]
|
|
78
81
|
const detour = shouldReverse ? orderedDetour.reverse() : orderedDetour
|
|
79
82
|
if (hasOnlyNonzeroOrthogonalSegments(detour)) detours.push(detour)
|
|
80
83
|
}
|