@tscircuit/schematic-trace-solver 0.0.101 → 0.0.103
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 +14 -6
- package/dist/index.js +251 -66
- package/lib/solvers/AvailableNetOrientationSolver/AvailableNetOrientationSolver.ts +58 -3
- package/lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver2/SchematicTraceSingleLineSolver2.ts +13 -5
- 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/site/bug-reports/bug-report-20260717T042845Z.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/bug-reports/bug-report-20260717T042845Z/__snapshots__/bug-report-20260717T042845Z.snap.svg +54 -0
- package/tests/bug-reports/bug-report-20260717T042845Z/bug-report-20260717T042845Z.json +113 -0
- package/tests/bug-reports/bug-report-20260717T042845Z/bug-report-20260717T042845Z.test.ts +12 -0
- package/tests/examples/__snapshots__/example11.snap.svg +19 -19
- 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 -19
- package/tests/repros/__snapshots__/trace-overlap-box-resistor.snap.svg +10 -10
- package/tests/repros/repro47-endpoint-obstacle-detour.test.ts +30 -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;
|
|
@@ -753,6 +760,7 @@ declare class AvailableNetOrientationSolver extends BaseSolver {
|
|
|
753
760
|
private finish;
|
|
754
761
|
private setCurrentLabel;
|
|
755
762
|
private findCorrectedCandidate;
|
|
763
|
+
private hasTraceContinuingInOrientation;
|
|
756
764
|
private findValidRotatedCandidate;
|
|
757
765
|
private findValidTraceAnchorCandidate;
|
|
758
766
|
private getTraceAnchorCandidatePoints;
|
package/dist/index.js
CHANGED
|
@@ -1291,6 +1291,7 @@ var SchematicTraceSingleLineSolver2 = class extends BaseSolver {
|
|
|
1291
1291
|
const isEndpointChipObstacle = rect.kind === "chip" && this.pins.some((pin) => pin.chipId === rect.chipId);
|
|
1292
1292
|
const canGenerateEndpointDetour = path.length === 3 || path.length === 4 && this.connectionPair !== void 0 && !isEndpointChipObstacle;
|
|
1293
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);
|
|
1294
1295
|
const detours = generateEndpointCollisionDetours({
|
|
1295
1296
|
path,
|
|
1296
1297
|
collidingSegmentIndex: segIndex,
|
|
@@ -1300,9 +1301,7 @@ var SchematicTraceSingleLineSolver2 = class extends BaseSolver {
|
|
|
1300
1301
|
if (this.visited.has(key)) return false;
|
|
1301
1302
|
this.visited.add(key);
|
|
1302
1303
|
return true;
|
|
1303
|
-
}).sort(
|
|
1304
|
-
(a2, b2) => this.pathLength(a2) - this.pathLength(b2) || this.getPinBandPenalty(a2) - this.getPinBandPenalty(b2)
|
|
1305
|
-
);
|
|
1304
|
+
}).sort(compareDetours);
|
|
1306
1305
|
for (const detour of detours) {
|
|
1307
1306
|
const nextCollisionRects = new Set(collisionRects);
|
|
1308
1307
|
nextCollisionRects.add(rect);
|
|
@@ -1542,6 +1541,9 @@ var SchematicTraceLinesSolver = class extends BaseSolver {
|
|
|
1542
1541
|
}
|
|
1543
1542
|
};
|
|
1544
1543
|
|
|
1544
|
+
// lib/solvers/TraceOverlapShiftSolver/TraceOverlapIssueSolver/TraceOverlapIssueSolver.ts
|
|
1545
|
+
import { doSegmentsIntersect } from "@tscircuit/math-utils";
|
|
1546
|
+
|
|
1545
1547
|
// lib/solvers/TraceOverlapShiftSolver/TraceOverlapIssueSolver/applyJogToTrace.ts
|
|
1546
1548
|
var applyJogToTerminalSegment = ({
|
|
1547
1549
|
pts,
|
|
@@ -1630,6 +1632,7 @@ var applyJogToTerminalSegment = ({
|
|
|
1630
1632
|
var EPS4 = 1e-6;
|
|
1631
1633
|
var TraceOverlapIssueSolver = class extends BaseSolver {
|
|
1632
1634
|
overlappingTraceSegments;
|
|
1635
|
+
interactionKind;
|
|
1633
1636
|
traceNetIslands;
|
|
1634
1637
|
obstacleRects;
|
|
1635
1638
|
SHIFT_DISTANCE = 0.1;
|
|
@@ -1637,6 +1640,7 @@ var TraceOverlapIssueSolver = class extends BaseSolver {
|
|
|
1637
1640
|
constructor(params) {
|
|
1638
1641
|
super();
|
|
1639
1642
|
this.overlappingTraceSegments = params.overlappingTraceSegments;
|
|
1643
|
+
this.interactionKind = params.interactionKind;
|
|
1640
1644
|
this.traceNetIslands = params.traceNetIslands;
|
|
1641
1645
|
this.obstacleRects = getObstacleRects(params.inputProblem);
|
|
1642
1646
|
for (const { connNetId, pathsWithOverlap } of this.overlappingTraceSegments) {
|
|
@@ -1660,19 +1664,64 @@ var TraceOverlapIssueSolver = class extends BaseSolver {
|
|
|
1660
1664
|
const someGroupCanShift = groupShouldStayInPlace.some(
|
|
1661
1665
|
(shouldStay) => !shouldStay
|
|
1662
1666
|
);
|
|
1663
|
-
const
|
|
1664
|
-
if (someGroupCanShift && groupShouldStayInPlace[
|
|
1665
|
-
const n = Math.floor(
|
|
1666
|
-
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;
|
|
1667
1671
|
return this.getObstacleAwareOffset({
|
|
1668
1672
|
group,
|
|
1669
|
-
offset:
|
|
1673
|
+
offset: direction * n * this.SHIFT_DISTANCE
|
|
1670
1674
|
});
|
|
1671
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 };
|
|
1672
1721
|
const eq = (a, b) => Math.abs(a - b) < EPS4;
|
|
1673
1722
|
const samePoint = (p, q) => !!p && !!q && eq(p.x, q.x) && eq(p.y, q.y);
|
|
1674
|
-
this.overlappingTraceSegments.forEach((group,
|
|
1675
|
-
const offset = offsets[
|
|
1723
|
+
this.overlappingTraceSegments.forEach((group, groupIndex) => {
|
|
1724
|
+
const offset = offsets[groupIndex];
|
|
1676
1725
|
if (offset === 0) return;
|
|
1677
1726
|
const byPath = /* @__PURE__ */ new Map();
|
|
1678
1727
|
for (const loc of group.pathsWithOverlap) {
|
|
@@ -1683,9 +1732,22 @@ var TraceOverlapIssueSolver = class extends BaseSolver {
|
|
|
1683
1732
|
}
|
|
1684
1733
|
for (const [pathIdx, segIdxSet] of byPath) {
|
|
1685
1734
|
const original = this.traceNetIslands[group.connNetId][pathIdx];
|
|
1686
|
-
const current =
|
|
1735
|
+
const current = correctedTraceMap[original.mspPairId] ?? original;
|
|
1687
1736
|
const pts = current.tracePath.map((p) => ({ ...p }));
|
|
1688
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();
|
|
1689
1751
|
const JOG_SIZE = this.SHIFT_DISTANCE;
|
|
1690
1752
|
for (const si of segIdxsRev) {
|
|
1691
1753
|
if (si < 0 || si >= pts.length - 1) continue;
|
|
@@ -1700,15 +1762,25 @@ var TraceOverlapIssueSolver = class extends BaseSolver {
|
|
|
1700
1762
|
} else {
|
|
1701
1763
|
const start = pts[si];
|
|
1702
1764
|
const end = pts[si + 1];
|
|
1703
|
-
const
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
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
|
+
}
|
|
1712
1784
|
}
|
|
1713
1785
|
}
|
|
1714
1786
|
}
|
|
@@ -1718,13 +1790,39 @@ var TraceOverlapIssueSolver = class extends BaseSolver {
|
|
|
1718
1790
|
cleaned.push(p);
|
|
1719
1791
|
}
|
|
1720
1792
|
}
|
|
1721
|
-
|
|
1793
|
+
correctedTraceMap[original.mspPairId] = {
|
|
1722
1794
|
...current,
|
|
1723
1795
|
tracePath: cleaned
|
|
1724
1796
|
};
|
|
1725
1797
|
}
|
|
1726
1798
|
});
|
|
1727
|
-
|
|
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;
|
|
1728
1826
|
}
|
|
1729
1827
|
getObstacleAwareOffset({
|
|
1730
1828
|
group,
|
|
@@ -1886,15 +1984,58 @@ var TraceOverlapShiftSolver = class extends BaseSolver {
|
|
|
1886
1984
|
const pathsB = this.traceNetIslands[netB] || [];
|
|
1887
1985
|
const overlapsA = [];
|
|
1888
1986
|
const overlapsB = [];
|
|
1987
|
+
const pointContacts = [];
|
|
1889
1988
|
const seenA = /* @__PURE__ */ new Set();
|
|
1890
1989
|
const seenB = /* @__PURE__ */ new Set();
|
|
1891
|
-
const
|
|
1990
|
+
const getRangeOverlap1D = (a1, a2, b1, b2) => {
|
|
1892
1991
|
const minA = Math.min(a1, a2);
|
|
1893
1992
|
const maxA = Math.max(a1, a2);
|
|
1894
1993
|
const minB = Math.min(b1, b2);
|
|
1895
1994
|
const maxB = Math.max(b1, b2);
|
|
1896
|
-
|
|
1897
|
-
|
|
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
|
+
});
|
|
1898
2039
|
};
|
|
1899
2040
|
for (let pa = 0; pa < pathsA.length; pa++) {
|
|
1900
2041
|
const pathA = pathsA[pa];
|
|
@@ -1916,45 +2057,23 @@ var TraceOverlapShiftSolver = class extends BaseSolver {
|
|
|
1916
2057
|
if (!bVert && !bHorz) continue;
|
|
1917
2058
|
if (aVert && bVert) {
|
|
1918
2059
|
if (Math.abs(a1.x - b1.x) < EPS11) {
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
});
|
|
1927
|
-
seenA.add(keyA);
|
|
1928
|
-
}
|
|
1929
|
-
if (!seenB.has(keyB)) {
|
|
1930
|
-
overlapsB.push({
|
|
1931
|
-
solvedTracePathIndex: pb,
|
|
1932
|
-
traceSegmentIndex: sb
|
|
1933
|
-
});
|
|
1934
|
-
seenB.add(keyB);
|
|
1935
|
-
}
|
|
1936
|
-
}
|
|
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
|
+
});
|
|
1937
2067
|
}
|
|
1938
2068
|
} else if (aHorz && bHorz) {
|
|
1939
2069
|
if (Math.abs(a1.y - b1.y) < EPS11) {
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
});
|
|
1948
|
-
seenA.add(keyA);
|
|
1949
|
-
}
|
|
1950
|
-
if (!seenB.has(keyB)) {
|
|
1951
|
-
overlapsB.push({
|
|
1952
|
-
solvedTracePathIndex: pb,
|
|
1953
|
-
traceSegmentIndex: sb
|
|
1954
|
-
});
|
|
1955
|
-
seenB.add(keyB);
|
|
1956
|
-
}
|
|
1957
|
-
}
|
|
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
|
+
});
|
|
1958
2077
|
}
|
|
1959
2078
|
}
|
|
1960
2079
|
}
|
|
@@ -1963,12 +2082,41 @@ var TraceOverlapShiftSolver = class extends BaseSolver {
|
|
|
1963
2082
|
}
|
|
1964
2083
|
if (overlapsA.length > 0 && overlapsB.length > 0) {
|
|
1965
2084
|
return {
|
|
2085
|
+
interactionKind: "overlap",
|
|
1966
2086
|
overlappingTraceSegments: [
|
|
1967
2087
|
{ connNetId: netA, pathsWithOverlap: overlapsA },
|
|
1968
2088
|
{ connNetId: netB, pathsWithOverlap: overlapsB }
|
|
1969
2089
|
]
|
|
1970
2090
|
};
|
|
1971
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
|
+
}
|
|
1972
2120
|
}
|
|
1973
2121
|
}
|
|
1974
2122
|
return null;
|
|
@@ -2044,9 +2192,10 @@ var TraceOverlapShiftSolver = class extends BaseSolver {
|
|
|
2044
2192
|
this.solved = true;
|
|
2045
2193
|
return;
|
|
2046
2194
|
}
|
|
2047
|
-
const { overlappingTraceSegments } = overlapIssue;
|
|
2195
|
+
const { interactionKind, overlappingTraceSegments } = overlapIssue;
|
|
2048
2196
|
this.activeSubSolver = new TraceOverlapIssueSolver({
|
|
2049
2197
|
inputProblem: this.inputProblem,
|
|
2198
|
+
interactionKind,
|
|
2050
2199
|
overlappingTraceSegments,
|
|
2051
2200
|
traceNetIslands: this.traceNetIslands
|
|
2052
2201
|
});
|
|
@@ -4165,7 +4314,7 @@ var expandChipsToFitPins = (problem) => {
|
|
|
4165
4314
|
};
|
|
4166
4315
|
|
|
4167
4316
|
// lib/utils/does-trace-overlap-with-existing-traces.ts
|
|
4168
|
-
import { doSegmentsIntersect } from "@tscircuit/math-utils";
|
|
4317
|
+
import { doSegmentsIntersect as doSegmentsIntersect2 } from "@tscircuit/math-utils";
|
|
4169
4318
|
function doesTraceOverlapWithExistingTraces(newTracePath, existingTraces) {
|
|
4170
4319
|
for (let i = 0; i < newTracePath.length - 1; i++) {
|
|
4171
4320
|
const newSegmentP1 = newTracePath[i];
|
|
@@ -4174,7 +4323,7 @@ function doesTraceOverlapWithExistingTraces(newTracePath, existingTraces) {
|
|
|
4174
4323
|
for (let j = 0; j < existingTrace.tracePath.length - 1; j++) {
|
|
4175
4324
|
const existingSegmentP1 = existingTrace.tracePath[j];
|
|
4176
4325
|
const existingSegmentP2 = existingTrace.tracePath[j + 1];
|
|
4177
|
-
if (
|
|
4326
|
+
if (doSegmentsIntersect2(
|
|
4178
4327
|
newSegmentP1,
|
|
4179
4328
|
newSegmentP2,
|
|
4180
4329
|
existingSegmentP1,
|
|
@@ -7335,6 +7484,26 @@ var AvailableNetOrientationSolver = class extends BaseSolver {
|
|
|
7335
7484
|
}
|
|
7336
7485
|
findCorrectedCandidate(label, labelIndex) {
|
|
7337
7486
|
const orientations = this.getAvailableOrientations(label);
|
|
7487
|
+
const requiredOrientation = orientations[0];
|
|
7488
|
+
const isTwoPinNet = this.inputProblem.netConnections.some(
|
|
7489
|
+
(connection) => connection.netId === label.netId && connection.pinIds.length === 2
|
|
7490
|
+
);
|
|
7491
|
+
if (isTwoPinNet && orientations.length === 1 && isYOrientation(requiredOrientation) && this.hasTraceContinuingInOrientation(label, requiredOrientation)) {
|
|
7492
|
+
const alignedCandidate = this.findValidCandidateInShiftColumn({
|
|
7493
|
+
label,
|
|
7494
|
+
labelIndex,
|
|
7495
|
+
orientation: requiredOrientation,
|
|
7496
|
+
direction: dir(requiredOrientation),
|
|
7497
|
+
baseAnchor: this.getWickOffsetAnchor(
|
|
7498
|
+
label.anchorPoint,
|
|
7499
|
+
requiredOrientation
|
|
7500
|
+
),
|
|
7501
|
+
maxSearchDistance: this.maxSearchDistance,
|
|
7502
|
+
outwardDistance: 0,
|
|
7503
|
+
stopOnTraceCollision: false
|
|
7504
|
+
});
|
|
7505
|
+
if (alignedCandidate) return alignedCandidate;
|
|
7506
|
+
}
|
|
7338
7507
|
const rotatedCandidate = this.findValidRotatedCandidate(
|
|
7339
7508
|
label,
|
|
7340
7509
|
labelIndex,
|
|
@@ -7359,6 +7528,21 @@ var AvailableNetOrientationSolver = class extends BaseSolver {
|
|
|
7359
7528
|
labelIndex
|
|
7360
7529
|
);
|
|
7361
7530
|
}
|
|
7531
|
+
hasTraceContinuingInOrientation(label, orientation) {
|
|
7532
|
+
const direction = dir(orientation);
|
|
7533
|
+
return Object.values(this.traceMap).some((trace) => {
|
|
7534
|
+
if (trace.globalConnNetId !== label.globalConnNetId) return false;
|
|
7535
|
+
const path = trace.tracePath;
|
|
7536
|
+
return path.some((point, pointIndex) => {
|
|
7537
|
+
if (Math.abs(point.x - label.anchorPoint.x) > EPS9 || Math.abs(point.y - label.anchorPoint.y) > EPS9) {
|
|
7538
|
+
return false;
|
|
7539
|
+
}
|
|
7540
|
+
return [path[pointIndex - 1], path[pointIndex + 1]].some(
|
|
7541
|
+
(neighbor) => neighbor && Math.abs(neighbor.x - point.x) <= EPS9 && (neighbor.y - point.y) * direction.y > EPS9
|
|
7542
|
+
);
|
|
7543
|
+
});
|
|
7544
|
+
});
|
|
7545
|
+
}
|
|
7362
7546
|
findValidRotatedCandidate(label, labelIndex, orientations) {
|
|
7363
7547
|
for (const orientation of orientations) {
|
|
7364
7548
|
const candidate = this.createCandidate(
|
|
@@ -7504,7 +7688,8 @@ var AvailableNetOrientationSolver = class extends BaseSolver {
|
|
|
7504
7688
|
baseAnchor,
|
|
7505
7689
|
maxSearchDistance,
|
|
7506
7690
|
outwardDistance,
|
|
7507
|
-
phase = "shift"
|
|
7691
|
+
phase = "shift",
|
|
7692
|
+
stopOnTraceCollision = true
|
|
7508
7693
|
} = params;
|
|
7509
7694
|
for (let distance4 = LABEL_SEARCH_STEP; distance4 <= maxSearchDistance + EPS9; distance4 += LABEL_SEARCH_STEP) {
|
|
7510
7695
|
const anchorPoint = {
|
|
@@ -7525,7 +7710,7 @@ var AvailableNetOrientationSolver = class extends BaseSolver {
|
|
|
7525
7710
|
result.selected = true;
|
|
7526
7711
|
return result;
|
|
7527
7712
|
}
|
|
7528
|
-
if (result.status === "trace-collision") break;
|
|
7713
|
+
if (stopOnTraceCollision && result.status === "trace-collision") break;
|
|
7529
7714
|
}
|
|
7530
7715
|
return null;
|
|
7531
7716
|
}
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
import type { SolvedTracePath } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceLinesSolver"
|
|
12
12
|
import type { InputPin, InputProblem } from "lib/types/InputProblem"
|
|
13
13
|
import { dir, type FacingDirection } from "lib/utils/dir"
|
|
14
|
+
import { rectIntersectsAnyTextBox } from "lib/utils/textBoxBounds"
|
|
14
15
|
import { EPS, LABEL_SEARCH_STEP, WICK_CLEARANCE } from "./constants"
|
|
15
16
|
import {
|
|
16
17
|
getConnectorTracePath,
|
|
@@ -22,8 +23,8 @@ import {
|
|
|
22
23
|
rectsOverlap,
|
|
23
24
|
simplifyOrthogonalPath,
|
|
24
25
|
traceCrossesBoundsInterior,
|
|
25
|
-
tracePathIntersectsBounds,
|
|
26
26
|
tracePathCrossesAnyBounds,
|
|
27
|
+
tracePathIntersectsBounds,
|
|
27
28
|
} from "./geometry"
|
|
28
29
|
import { getPinMap, getTracePins, toNetLabelPlacementPatch } from "./traces"
|
|
29
30
|
import type {
|
|
@@ -36,7 +37,6 @@ import type {
|
|
|
36
37
|
EvaluatedCandidate,
|
|
37
38
|
} from "./types"
|
|
38
39
|
import { visualizeAvailableNetOrientationSolver } from "./visualize"
|
|
39
|
-
import { rectIntersectsAnyTextBox } from "lib/utils/textBoxBounds"
|
|
40
40
|
|
|
41
41
|
const LABEL_TRACE_CLEARANCE = 0.1
|
|
42
42
|
|
|
@@ -182,6 +182,34 @@ export class AvailableNetOrientationSolver extends BaseSolver {
|
|
|
182
182
|
|
|
183
183
|
private findCorrectedCandidate(label: NetLabelPlacement, labelIndex: number) {
|
|
184
184
|
const orientations = this.getAvailableOrientations(label)
|
|
185
|
+
const requiredOrientation = orientations[0]!
|
|
186
|
+
const isTwoPinNet = this.inputProblem.netConnections.some(
|
|
187
|
+
(connection) =>
|
|
188
|
+
connection.netId === label.netId && connection.pinIds.length === 2,
|
|
189
|
+
)
|
|
190
|
+
if (
|
|
191
|
+
isTwoPinNet &&
|
|
192
|
+
orientations.length === 1 &&
|
|
193
|
+
isYOrientation(requiredOrientation) &&
|
|
194
|
+
this.hasTraceContinuingInOrientation(label, requiredOrientation)
|
|
195
|
+
) {
|
|
196
|
+
// Keep two-pin vertical net labels aligned with the existing trace.
|
|
197
|
+
const alignedCandidate = this.findValidCandidateInShiftColumn({
|
|
198
|
+
label,
|
|
199
|
+
labelIndex,
|
|
200
|
+
orientation: requiredOrientation,
|
|
201
|
+
direction: dir(requiredOrientation),
|
|
202
|
+
baseAnchor: this.getWickOffsetAnchor(
|
|
203
|
+
label.anchorPoint,
|
|
204
|
+
requiredOrientation,
|
|
205
|
+
),
|
|
206
|
+
maxSearchDistance: this.maxSearchDistance,
|
|
207
|
+
outwardDistance: 0,
|
|
208
|
+
stopOnTraceCollision: false,
|
|
209
|
+
})
|
|
210
|
+
if (alignedCandidate) return alignedCandidate
|
|
211
|
+
}
|
|
212
|
+
|
|
185
213
|
const rotatedCandidate = this.findValidRotatedCandidate(
|
|
186
214
|
label,
|
|
187
215
|
labelIndex,
|
|
@@ -210,6 +238,31 @@ export class AvailableNetOrientationSolver extends BaseSolver {
|
|
|
210
238
|
)
|
|
211
239
|
}
|
|
212
240
|
|
|
241
|
+
private hasTraceContinuingInOrientation(
|
|
242
|
+
label: NetLabelPlacement,
|
|
243
|
+
orientation: "y+" | "y-",
|
|
244
|
+
) {
|
|
245
|
+
const direction = dir(orientation)
|
|
246
|
+
return Object.values(this.traceMap).some((trace) => {
|
|
247
|
+
if (trace.globalConnNetId !== label.globalConnNetId) return false
|
|
248
|
+
const path = trace.tracePath
|
|
249
|
+
return path.some((point, pointIndex) => {
|
|
250
|
+
if (
|
|
251
|
+
Math.abs(point.x - label.anchorPoint.x) > EPS ||
|
|
252
|
+
Math.abs(point.y - label.anchorPoint.y) > EPS
|
|
253
|
+
) {
|
|
254
|
+
return false
|
|
255
|
+
}
|
|
256
|
+
return [path[pointIndex - 1], path[pointIndex + 1]].some(
|
|
257
|
+
(neighbor) =>
|
|
258
|
+
neighbor &&
|
|
259
|
+
Math.abs(neighbor.x - point.x) <= EPS &&
|
|
260
|
+
(neighbor.y - point.y) * direction.y > EPS,
|
|
261
|
+
)
|
|
262
|
+
})
|
|
263
|
+
})
|
|
264
|
+
}
|
|
265
|
+
|
|
213
266
|
private findValidRotatedCandidate(
|
|
214
267
|
label: NetLabelPlacement,
|
|
215
268
|
labelIndex: number,
|
|
@@ -401,6 +454,7 @@ export class AvailableNetOrientationSolver extends BaseSolver {
|
|
|
401
454
|
maxSearchDistance: number
|
|
402
455
|
outwardDistance: number
|
|
403
456
|
phase?: CandidatePhase
|
|
457
|
+
stopOnTraceCollision?: boolean
|
|
404
458
|
}) {
|
|
405
459
|
const {
|
|
406
460
|
label,
|
|
@@ -411,6 +465,7 @@ export class AvailableNetOrientationSolver extends BaseSolver {
|
|
|
411
465
|
maxSearchDistance,
|
|
412
466
|
outwardDistance,
|
|
413
467
|
phase = "shift",
|
|
468
|
+
stopOnTraceCollision = true,
|
|
414
469
|
} = params
|
|
415
470
|
|
|
416
471
|
for (
|
|
@@ -437,7 +492,7 @@ export class AvailableNetOrientationSolver extends BaseSolver {
|
|
|
437
492
|
result.selected = true
|
|
438
493
|
return result
|
|
439
494
|
}
|
|
440
|
-
if (result.status === "trace-collision") break
|
|
495
|
+
if (stopOnTraceCollision && result.status === "trace-collision") break
|
|
441
496
|
}
|
|
442
497
|
|
|
443
498
|
return null
|
|
@@ -370,6 +370,18 @@ export class SchematicTraceSingleLineSolver2 extends BaseSolver {
|
|
|
370
370
|
!isEndpointChipObstacle)
|
|
371
371
|
|
|
372
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)
|
|
373
385
|
const detours = generateEndpointCollisionDetours({
|
|
374
386
|
path,
|
|
375
387
|
collidingSegmentIndex: segIndex,
|
|
@@ -381,11 +393,7 @@ export class SchematicTraceSingleLineSolver2 extends BaseSolver {
|
|
|
381
393
|
this.visited.add(key)
|
|
382
394
|
return true
|
|
383
395
|
})
|
|
384
|
-
.sort(
|
|
385
|
-
(a, b) =>
|
|
386
|
-
this.pathLength(a) - this.pathLength(b) ||
|
|
387
|
-
this.getPinBandPenalty(a) - this.getPinBandPenalty(b),
|
|
388
|
-
)
|
|
396
|
+
.sort(compareDetours)
|
|
389
397
|
|
|
390
398
|
for (const detour of detours) {
|
|
391
399
|
const nextCollisionRects = new Set(collisionRects)
|