@tscircuit/schematic-trace-solver 0.0.91 → 0.0.93
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 +7 -1
- package/dist/index.js +150 -21
- package/lib/solvers/Example28Solver/doesPathRunAlongChipBoundary.ts +18 -0
- package/lib/solvers/Example28Solver/reroute.ts +29 -10
- package/lib/solvers/Example28Solver/types.ts +7 -0
- package/lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver2/SchematicTraceSingleLineSolver2.ts +35 -23
- package/lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver2/calculateDirectShortPath.ts +161 -0
- package/package.json +1 -1
- package/tests/bug-reports/bug-report-20260706T220324Z/__snapshots__/bug-report-20260706T220324Z.snap.svg +4 -6
- package/tests/bug-reports/bug-report-20260707T230831Z/__snapshots__/bug-report-20260707T230831Z.snap.svg +46 -52
- package/tests/examples/__snapshots__/example03.snap.svg +44 -44
- package/tests/examples/__snapshots__/example32.snap.svg +3 -3
- package/tests/examples/__snapshots__/example42.snap.svg +1 -1
- package/tests/repros/__snapshots__/repro-missing-trace-netlabel.snap.svg +60 -0
- package/tests/repros/__snapshots__/repro-netlabel-overlap-trace.snap.svg +60 -0
- package/tests/repros/__snapshots__/repro-rectifier-trace-overlap.snap.svg +58 -0
- package/tests/repros/assets/repro-missing-trace-netlabel.input.json +155 -0
- package/tests/repros/assets/repro-netlabel-overlap-trace.input.json +115 -0
- package/tests/repros/assets/repro-rectifier-trace-overlap.input.json +137 -0
- package/tests/repros/repro-missing-trace-netlabel.test.ts +14 -0
- package/tests/repros/repro-netlabel-overlap-trace.test.ts +14 -0
- package/tests/repros/repro-rectifier-trace-overlap.test.ts +14 -0
package/dist/index.d.ts
CHANGED
|
@@ -194,7 +194,6 @@ declare class SchematicTraceSingleLineSolver2 extends BaseSolver {
|
|
|
194
194
|
private axisOfSegment;
|
|
195
195
|
private pathLength;
|
|
196
196
|
private getPinBandPenalty;
|
|
197
|
-
private pathCost;
|
|
198
197
|
private isSegmentOutsidePinBand;
|
|
199
198
|
_step(): void;
|
|
200
199
|
visualize(): GraphicsObject;
|
|
@@ -650,6 +649,13 @@ type TracePathScore = {
|
|
|
650
649
|
labelIntersections: number;
|
|
651
650
|
labelHugDistance: number;
|
|
652
651
|
traceIntersections: number;
|
|
652
|
+
/**
|
|
653
|
+
* Total length the path runs along a chip boundary. A soft quality penalty
|
|
654
|
+
* (traces hugging a chip edge look bad) that is only preferred against once
|
|
655
|
+
* the more important factors above are equal, so a boundary-hugging route is
|
|
656
|
+
* still chosen over one that actually overlaps a label or another trace.
|
|
657
|
+
*/
|
|
658
|
+
chipBoundaryOverlap: number;
|
|
653
659
|
pathLength: number;
|
|
654
660
|
};
|
|
655
661
|
type RerouteCandidateResult = {
|
package/dist/index.js
CHANGED
|
@@ -577,7 +577,7 @@ var MspConnectionPairSolver = class extends BaseSolver {
|
|
|
577
577
|
import "graphics-debug";
|
|
578
578
|
|
|
579
579
|
// lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver2/SchematicTraceSingleLineSolver2.ts
|
|
580
|
-
import { calculateElbow } from "calculate-elbow";
|
|
580
|
+
import { calculateElbow as calculateElbow2 } from "calculate-elbow";
|
|
581
581
|
|
|
582
582
|
// lib/solvers/GuidelinesSolver/getInputChipBounds.ts
|
|
583
583
|
function getInputChipBounds(chip) {
|
|
@@ -846,6 +846,111 @@ function getRectBounds(center, w, h) {
|
|
|
846
846
|
};
|
|
847
847
|
}
|
|
848
848
|
|
|
849
|
+
// lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver2/calculateDirectShortPath.ts
|
|
850
|
+
import { calculateElbow } from "calculate-elbow";
|
|
851
|
+
var MAX_SHORT_TRACE_DISTANCE = 0.15;
|
|
852
|
+
var SHORT_TRACE_OVERSHOOT = MAX_SHORT_TRACE_DISTANCE / 7.5;
|
|
853
|
+
var FALLBACK_ELBOW_MAX_OVERSHOOT = 0.2;
|
|
854
|
+
function segmentDirection(from, to) {
|
|
855
|
+
if (to.x > from.x) return "x+";
|
|
856
|
+
if (to.x < from.x) return "x-";
|
|
857
|
+
if (to.y > from.y) return "y+";
|
|
858
|
+
if (to.y < from.y) return "y-";
|
|
859
|
+
return null;
|
|
860
|
+
}
|
|
861
|
+
function pathMatchesPinDirections({
|
|
862
|
+
path,
|
|
863
|
+
pin1,
|
|
864
|
+
pin2
|
|
865
|
+
}) {
|
|
866
|
+
const firstDirection = segmentDirection(path[0], path[1]);
|
|
867
|
+
const lastDirection = segmentDirection(
|
|
868
|
+
path[path.length - 1],
|
|
869
|
+
path[path.length - 2]
|
|
870
|
+
);
|
|
871
|
+
return firstDirection === pin1._facingDirection && lastDirection === pin2._facingDirection;
|
|
872
|
+
}
|
|
873
|
+
function calculateShortOrthogonalRoute(pin1, pin2) {
|
|
874
|
+
if (pin1.x === pin2.x || pin1.y === pin2.y) return null;
|
|
875
|
+
const firstDir = pin1._facingDirection;
|
|
876
|
+
const lastDir = pin2._facingDirection;
|
|
877
|
+
const start = { x: pin1.x, y: pin1.y };
|
|
878
|
+
const end = { x: pin2.x, y: pin2.y };
|
|
879
|
+
let path = null;
|
|
880
|
+
if (firstDir?.startsWith("y") && lastDir?.startsWith("x")) {
|
|
881
|
+
let yOffset = -SHORT_TRACE_OVERSHOOT;
|
|
882
|
+
if (firstDir === "y+") {
|
|
883
|
+
yOffset = SHORT_TRACE_OVERSHOOT;
|
|
884
|
+
}
|
|
885
|
+
const routeY = start.y + yOffset;
|
|
886
|
+
const routeX = (start.x + end.x) / 2;
|
|
887
|
+
path = [
|
|
888
|
+
start,
|
|
889
|
+
{ x: start.x, y: routeY },
|
|
890
|
+
{ x: routeX, y: routeY },
|
|
891
|
+
{ x: routeX, y: end.y },
|
|
892
|
+
end
|
|
893
|
+
];
|
|
894
|
+
} else if (firstDir?.startsWith("x") && lastDir?.startsWith("y")) {
|
|
895
|
+
let xOffset = -SHORT_TRACE_OVERSHOOT;
|
|
896
|
+
if (firstDir === "x+") {
|
|
897
|
+
xOffset = SHORT_TRACE_OVERSHOOT;
|
|
898
|
+
}
|
|
899
|
+
const routeX = start.x + xOffset;
|
|
900
|
+
const routeY = (start.y + end.y) / 2;
|
|
901
|
+
path = [
|
|
902
|
+
start,
|
|
903
|
+
{ x: routeX, y: start.y },
|
|
904
|
+
{ x: routeX, y: routeY },
|
|
905
|
+
{ x: end.x, y: routeY },
|
|
906
|
+
end
|
|
907
|
+
];
|
|
908
|
+
}
|
|
909
|
+
if (!path) return null;
|
|
910
|
+
if (pathMatchesPinDirections({ path, pin1, pin2 })) {
|
|
911
|
+
return path;
|
|
912
|
+
}
|
|
913
|
+
return null;
|
|
914
|
+
}
|
|
915
|
+
function calculateDirectShortPath(pin1, pin2) {
|
|
916
|
+
const routingDistance = Math.abs(pin1.x - pin2.x) + Math.abs(pin1.y - pin2.y);
|
|
917
|
+
if (routingDistance > MAX_SHORT_TRACE_DISTANCE) return null;
|
|
918
|
+
const start = { x: pin1.x, y: pin1.y };
|
|
919
|
+
const end = { x: pin2.x, y: pin2.y };
|
|
920
|
+
const orthogonalRoute = calculateShortOrthogonalRoute(pin1, pin2);
|
|
921
|
+
if (orthogonalRoute) return orthogonalRoute;
|
|
922
|
+
let candidatePaths = [];
|
|
923
|
+
if (pin1.x !== pin2.x && pin1.y !== pin2.y) {
|
|
924
|
+
candidatePaths = [
|
|
925
|
+
[start, { x: pin2.x, y: pin1.y }, end],
|
|
926
|
+
[start, { x: pin1.x, y: pin2.y }, end]
|
|
927
|
+
];
|
|
928
|
+
} else {
|
|
929
|
+
candidatePaths = [[start, end]];
|
|
930
|
+
}
|
|
931
|
+
for (const path of candidatePaths) {
|
|
932
|
+
if (pathMatchesPinDirections({ path, pin1, pin2 })) return path;
|
|
933
|
+
}
|
|
934
|
+
return calculateElbow(
|
|
935
|
+
{
|
|
936
|
+
x: pin1.x,
|
|
937
|
+
y: pin1.y,
|
|
938
|
+
facingDirection: pin1._facingDirection
|
|
939
|
+
},
|
|
940
|
+
{
|
|
941
|
+
x: pin2.x,
|
|
942
|
+
y: pin2.y,
|
|
943
|
+
facingDirection: pin2._facingDirection
|
|
944
|
+
},
|
|
945
|
+
{
|
|
946
|
+
overshoot: Math.min(
|
|
947
|
+
FALLBACK_ELBOW_MAX_OVERSHOOT,
|
|
948
|
+
Math.max(SHORT_TRACE_OVERSHOOT, routingDistance / 4)
|
|
949
|
+
)
|
|
950
|
+
}
|
|
951
|
+
);
|
|
952
|
+
}
|
|
953
|
+
|
|
849
954
|
// lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver2/SchematicTraceSingleLineSolver2.ts
|
|
850
955
|
var SchematicTraceSingleLineSolver2 = class extends BaseSolver {
|
|
851
956
|
pins;
|
|
@@ -885,7 +990,8 @@ var SchematicTraceSingleLineSolver2 = class extends BaseSolver {
|
|
|
885
990
|
) : []
|
|
886
991
|
);
|
|
887
992
|
const [pin1, pin2] = this.pins;
|
|
888
|
-
|
|
993
|
+
const directShortPath = calculateDirectShortPath(pin1, pin2);
|
|
994
|
+
this.baseElbow = directShortPath ?? calculateElbow2(
|
|
889
995
|
{
|
|
890
996
|
x: pin1.x,
|
|
891
997
|
y: pin1.y,
|
|
@@ -898,6 +1004,7 @@ var SchematicTraceSingleLineSolver2 = class extends BaseSolver {
|
|
|
898
1004
|
},
|
|
899
1005
|
{ overshoot: 0.2 }
|
|
900
1006
|
);
|
|
1007
|
+
this.solvedTracePath = directShortPath;
|
|
901
1008
|
this.aabb = aabbFromPoints(
|
|
902
1009
|
{ x: pin1.x, y: pin1.y },
|
|
903
1010
|
{ x: pin2.x, y: pin2.y }
|
|
@@ -1005,9 +1112,6 @@ var SchematicTraceSingleLineSolver2 = class extends BaseSolver {
|
|
|
1005
1112
|
}
|
|
1006
1113
|
return penalty;
|
|
1007
1114
|
}
|
|
1008
|
-
pathCost(path) {
|
|
1009
|
-
return this.pathLength(path) + this.getPinBandPenalty(path);
|
|
1010
|
-
}
|
|
1011
1115
|
isSegmentOutsidePinBand(a, b) {
|
|
1012
1116
|
if (isHorizontal(a, b)) {
|
|
1013
1117
|
return a.y <= this.aabb.minY || a.y >= this.aabb.maxY;
|
|
@@ -1103,8 +1207,12 @@ var SchematicTraceSingleLineSolver2 = class extends BaseSolver {
|
|
|
1103
1207
|
this.visited.add(key);
|
|
1104
1208
|
const nextSet = new Set(collisionRects);
|
|
1105
1209
|
nextSet.add(rect);
|
|
1106
|
-
|
|
1107
|
-
|
|
1210
|
+
newStates.push({
|
|
1211
|
+
path: newPath,
|
|
1212
|
+
collisionRects: nextSet,
|
|
1213
|
+
length: this.pathLength(newPath),
|
|
1214
|
+
pinBandPenalty: this.getPinBandPenalty(newPath)
|
|
1215
|
+
});
|
|
1108
1216
|
};
|
|
1109
1217
|
for (const coord of candidates) {
|
|
1110
1218
|
addShiftedCandidate(segIndex, axis, coord);
|
|
@@ -1136,7 +1244,9 @@ var SchematicTraceSingleLineSolver2 = class extends BaseSolver {
|
|
|
1136
1244
|
addShiftedCandidate(adjacentSegIndex, adjacentAxis, coord);
|
|
1137
1245
|
}
|
|
1138
1246
|
}
|
|
1139
|
-
newStates.sort(
|
|
1247
|
+
newStates.sort(
|
|
1248
|
+
(a2, b2) => a2.length - b2.length || a2.pinBandPenalty - b2.pinBandPenalty
|
|
1249
|
+
);
|
|
1140
1250
|
for (const st of newStates) {
|
|
1141
1251
|
this.queue.push({ path: st.path, collisionRects: st.collisionRects });
|
|
1142
1252
|
}
|
|
@@ -5520,6 +5630,19 @@ var doesPathRunAlongChipBoundary = (path, chipObstacles) => {
|
|
|
5520
5630
|
}
|
|
5521
5631
|
return false;
|
|
5522
5632
|
};
|
|
5633
|
+
var getChipBoundaryOverlap = (path, chipObstacles) => {
|
|
5634
|
+
let total = 0;
|
|
5635
|
+
for (let i = 0; i < path.length - 1; i++) {
|
|
5636
|
+
const start = path[i];
|
|
5637
|
+
const end = path[i + 1];
|
|
5638
|
+
for (const obstacle of chipObstacles) {
|
|
5639
|
+
if (!segmentRunsAlongRectBoundary(start, end, obstacle)) continue;
|
|
5640
|
+
const overlap = getSegmentOverlapWithRectSpan(start, end, obstacle);
|
|
5641
|
+
if (overlap > EPS7) total += overlap;
|
|
5642
|
+
}
|
|
5643
|
+
}
|
|
5644
|
+
return total;
|
|
5645
|
+
};
|
|
5523
5646
|
var getSegmentOverlapWithRectSpan = (start, end, rect) => {
|
|
5524
5647
|
const isVertical4 = Math.abs(start.x - end.x) < EPS7;
|
|
5525
5648
|
if (isVertical4) {
|
|
@@ -5551,7 +5674,8 @@ var findBestReroutePath = ({
|
|
|
5551
5674
|
obstacleLabel,
|
|
5552
5675
|
outputTraces,
|
|
5553
5676
|
outputNetLabelPlacements,
|
|
5554
|
-
candidateResults
|
|
5677
|
+
candidateResults,
|
|
5678
|
+
chipObstacles
|
|
5555
5679
|
});
|
|
5556
5680
|
markSelectedCandidate(candidateResults, bestPath);
|
|
5557
5681
|
return { bestPath, candidateResults };
|
|
@@ -5638,14 +5762,6 @@ var createCandidateResult = ({
|
|
|
5638
5762
|
selected: false
|
|
5639
5763
|
};
|
|
5640
5764
|
}
|
|
5641
|
-
if (doesPathRunAlongChipBoundary(path, chipObstacles)) {
|
|
5642
|
-
return {
|
|
5643
|
-
path,
|
|
5644
|
-
status: "chip-collision",
|
|
5645
|
-
usesHorizontalSegmentPush,
|
|
5646
|
-
selected: false
|
|
5647
|
-
};
|
|
5648
|
-
}
|
|
5649
5765
|
return {
|
|
5650
5766
|
path,
|
|
5651
5767
|
score: scoreTracePath({
|
|
@@ -5653,7 +5769,8 @@ var createCandidateResult = ({
|
|
|
5653
5769
|
tracePath: path,
|
|
5654
5770
|
obstacleLabel,
|
|
5655
5771
|
outputTraces,
|
|
5656
|
-
outputNetLabelPlacements
|
|
5772
|
+
outputNetLabelPlacements,
|
|
5773
|
+
chipObstacles
|
|
5657
5774
|
}),
|
|
5658
5775
|
status: "valid",
|
|
5659
5776
|
usesHorizontalSegmentPush,
|
|
@@ -5743,7 +5860,8 @@ var selectBestReroutePath = ({
|
|
|
5743
5860
|
obstacleLabel,
|
|
5744
5861
|
outputTraces,
|
|
5745
5862
|
outputNetLabelPlacements,
|
|
5746
|
-
candidateResults
|
|
5863
|
+
candidateResults,
|
|
5864
|
+
chipObstacles
|
|
5747
5865
|
}) => {
|
|
5748
5866
|
for (const candidate of candidateResults) {
|
|
5749
5867
|
if (!candidate.usesHorizontalSegmentPush) continue;
|
|
@@ -5757,7 +5875,8 @@ var selectBestReroutePath = ({
|
|
|
5757
5875
|
tracePath: trace.tracePath,
|
|
5758
5876
|
obstacleLabel,
|
|
5759
5877
|
outputTraces,
|
|
5760
|
-
outputNetLabelPlacements
|
|
5878
|
+
outputNetLabelPlacements,
|
|
5879
|
+
chipObstacles
|
|
5761
5880
|
});
|
|
5762
5881
|
for (const candidate of candidateResults) {
|
|
5763
5882
|
if (candidate.status !== "valid" || !candidate.score) continue;
|
|
@@ -5843,7 +5962,8 @@ var scoreTracePath = ({
|
|
|
5843
5962
|
tracePath,
|
|
5844
5963
|
obstacleLabel,
|
|
5845
5964
|
outputTraces,
|
|
5846
|
-
outputNetLabelPlacements
|
|
5965
|
+
outputNetLabelPlacements,
|
|
5966
|
+
chipObstacles
|
|
5847
5967
|
}) => {
|
|
5848
5968
|
const candidateTrace = { ...trace, tracePath };
|
|
5849
5969
|
return {
|
|
@@ -5853,6 +5973,7 @@ var scoreTracePath = ({
|
|
|
5853
5973
|
}).length,
|
|
5854
5974
|
labelHugDistance: getLabelHugDistance(tracePath, obstacleLabel),
|
|
5855
5975
|
traceIntersections: countTraceIntersections(candidateTrace, outputTraces),
|
|
5976
|
+
chipBoundaryOverlap: getChipBoundaryOverlap(tracePath, chipObstacles),
|
|
5856
5977
|
pathLength: getPathLength(tracePath)
|
|
5857
5978
|
};
|
|
5858
5979
|
};
|
|
@@ -5865,6 +5986,14 @@ var countTraceIntersections = (trace, outputTraces) => {
|
|
|
5865
5986
|
return count;
|
|
5866
5987
|
};
|
|
5867
5988
|
var isBetterScore = (score, bestScore) => {
|
|
5989
|
+
const scoreHasLabelOverlap = score.labelIntersections > 0;
|
|
5990
|
+
const bestHasLabelOverlap = bestScore.labelIntersections > 0;
|
|
5991
|
+
if (scoreHasLabelOverlap !== bestHasLabelOverlap) {
|
|
5992
|
+
return !scoreHasLabelOverlap;
|
|
5993
|
+
}
|
|
5994
|
+
if (score.chipBoundaryOverlap !== bestScore.chipBoundaryOverlap) {
|
|
5995
|
+
return score.chipBoundaryOverlap < bestScore.chipBoundaryOverlap;
|
|
5996
|
+
}
|
|
5868
5997
|
if (score.labelIntersections !== bestScore.labelIntersections) {
|
|
5869
5998
|
return score.labelIntersections < bestScore.labelIntersections;
|
|
5870
5999
|
}
|
|
@@ -20,6 +20,24 @@ export const doesPathRunAlongChipBoundary = (
|
|
|
20
20
|
return false
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
+
/** Total length of the path that runs along (overlapping) a chip boundary. */
|
|
24
|
+
export const getChipBoundaryOverlap = (
|
|
25
|
+
path: Point[],
|
|
26
|
+
chipObstacles: ChipObstacle[],
|
|
27
|
+
) => {
|
|
28
|
+
let total = 0
|
|
29
|
+
for (let i = 0; i < path.length - 1; i++) {
|
|
30
|
+
const start = path[i]!
|
|
31
|
+
const end = path[i + 1]!
|
|
32
|
+
for (const obstacle of chipObstacles) {
|
|
33
|
+
if (!segmentRunsAlongRectBoundary(start, end, obstacle)) continue
|
|
34
|
+
const overlap = getSegmentOverlapWithRectSpan(start, end, obstacle)
|
|
35
|
+
if (overlap > EPS) total += overlap
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return total
|
|
39
|
+
}
|
|
40
|
+
|
|
23
41
|
const getSegmentOverlapWithRectSpan = (
|
|
24
42
|
start: Point,
|
|
25
43
|
end: Point,
|
|
@@ -14,7 +14,7 @@ import {
|
|
|
14
14
|
getPathLength,
|
|
15
15
|
isPathCollidingWithChipInterior,
|
|
16
16
|
} from "./geometry"
|
|
17
|
-
import {
|
|
17
|
+
import { getChipBoundaryOverlap } from "./doesPathRunAlongChipBoundary"
|
|
18
18
|
import type {
|
|
19
19
|
ChipObstacle,
|
|
20
20
|
RerouteCandidateResult,
|
|
@@ -52,6 +52,7 @@ export const findBestReroutePath = ({
|
|
|
52
52
|
outputTraces,
|
|
53
53
|
outputNetLabelPlacements,
|
|
54
54
|
candidateResults,
|
|
55
|
+
chipObstacles,
|
|
55
56
|
})
|
|
56
57
|
|
|
57
58
|
markSelectedCandidate(candidateResults, bestPath)
|
|
@@ -166,15 +167,11 @@ const createCandidateResult = ({
|
|
|
166
167
|
}
|
|
167
168
|
}
|
|
168
169
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
selected: false,
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
|
|
170
|
+
// Note: a path that merely runs along a chip boundary is NOT rejected here.
|
|
171
|
+
// It is a valid route, just a lower-quality one — scoreTracePath penalizes it
|
|
172
|
+
// via chipBoundaryOverlap so it loses only to routes that are otherwise as
|
|
173
|
+
// good. Hard-rejecting it discarded the best available route when every
|
|
174
|
+
// alternative overlapped a label or another trace.
|
|
178
175
|
return {
|
|
179
176
|
path,
|
|
180
177
|
score: scoreTracePath({
|
|
@@ -183,6 +180,7 @@ const createCandidateResult = ({
|
|
|
183
180
|
obstacleLabel,
|
|
184
181
|
outputTraces,
|
|
185
182
|
outputNetLabelPlacements,
|
|
183
|
+
chipObstacles,
|
|
186
184
|
}),
|
|
187
185
|
status: "valid",
|
|
188
186
|
usesHorizontalSegmentPush,
|
|
@@ -293,12 +291,14 @@ const selectBestReroutePath = ({
|
|
|
293
291
|
outputTraces,
|
|
294
292
|
outputNetLabelPlacements,
|
|
295
293
|
candidateResults,
|
|
294
|
+
chipObstacles,
|
|
296
295
|
}: {
|
|
297
296
|
trace: SolvedTracePath
|
|
298
297
|
obstacleLabel: NetLabelPlacement
|
|
299
298
|
outputTraces: SolvedTracePath[]
|
|
300
299
|
outputNetLabelPlacements: NetLabelPlacement[]
|
|
301
300
|
candidateResults: RerouteCandidateResult[]
|
|
301
|
+
chipObstacles: ChipObstacle[]
|
|
302
302
|
}) => {
|
|
303
303
|
for (const candidate of candidateResults) {
|
|
304
304
|
if (!candidate.usesHorizontalSegmentPush) continue
|
|
@@ -314,6 +314,7 @@ const selectBestReroutePath = ({
|
|
|
314
314
|
obstacleLabel,
|
|
315
315
|
outputTraces,
|
|
316
316
|
outputNetLabelPlacements,
|
|
317
|
+
chipObstacles,
|
|
317
318
|
})
|
|
318
319
|
|
|
319
320
|
for (const candidate of candidateResults) {
|
|
@@ -437,12 +438,14 @@ const scoreTracePath = ({
|
|
|
437
438
|
obstacleLabel,
|
|
438
439
|
outputTraces,
|
|
439
440
|
outputNetLabelPlacements,
|
|
441
|
+
chipObstacles,
|
|
440
442
|
}: {
|
|
441
443
|
trace: SolvedTracePath
|
|
442
444
|
tracePath: Point[]
|
|
443
445
|
obstacleLabel: NetLabelPlacement
|
|
444
446
|
outputTraces: SolvedTracePath[]
|
|
445
447
|
outputNetLabelPlacements: NetLabelPlacement[]
|
|
448
|
+
chipObstacles: ChipObstacle[]
|
|
446
449
|
}): TracePathScore => {
|
|
447
450
|
const candidateTrace = { ...trace, tracePath }
|
|
448
451
|
return {
|
|
@@ -452,6 +455,7 @@ const scoreTracePath = ({
|
|
|
452
455
|
}).length,
|
|
453
456
|
labelHugDistance: getLabelHugDistance(tracePath, obstacleLabel),
|
|
454
457
|
traceIntersections: countTraceIntersections(candidateTrace, outputTraces),
|
|
458
|
+
chipBoundaryOverlap: getChipBoundaryOverlap(tracePath, chipObstacles),
|
|
455
459
|
pathLength: getPathLength(tracePath),
|
|
456
460
|
}
|
|
457
461
|
}
|
|
@@ -469,6 +473,21 @@ const countTraceIntersections = (
|
|
|
469
473
|
}
|
|
470
474
|
|
|
471
475
|
const isBetterScore = (score: TracePathScore, bestScore: TracePathScore) => {
|
|
476
|
+
// A route with no label overlap is always preferred over one with any. This
|
|
477
|
+
// is separate from the full overlap count below so that a *clean* route wins
|
|
478
|
+
// even if it hugs a chip boundary, while a route that must overlap a label is
|
|
479
|
+
// still steered away from also hugging a boundary.
|
|
480
|
+
const scoreHasLabelOverlap = score.labelIntersections > 0
|
|
481
|
+
const bestHasLabelOverlap = bestScore.labelIntersections > 0
|
|
482
|
+
if (scoreHasLabelOverlap !== bestHasLabelOverlap) {
|
|
483
|
+
// Reached only when exactly one route overlaps a label; the clean one wins.
|
|
484
|
+
return !scoreHasLabelOverlap
|
|
485
|
+
}
|
|
486
|
+
// Boundary hugging is only accepted as the price of a clean route; once a
|
|
487
|
+
// label overlap is unavoidable, avoid hugging a boundary on top of it.
|
|
488
|
+
if (score.chipBoundaryOverlap !== bestScore.chipBoundaryOverlap) {
|
|
489
|
+
return score.chipBoundaryOverlap < bestScore.chipBoundaryOverlap
|
|
490
|
+
}
|
|
472
491
|
if (score.labelIntersections !== bestScore.labelIntersections) {
|
|
473
492
|
return score.labelIntersections < bestScore.labelIntersections
|
|
474
493
|
}
|
|
@@ -15,6 +15,13 @@ export type TracePathScore = {
|
|
|
15
15
|
labelIntersections: number
|
|
16
16
|
labelHugDistance: number
|
|
17
17
|
traceIntersections: number
|
|
18
|
+
/**
|
|
19
|
+
* Total length the path runs along a chip boundary. A soft quality penalty
|
|
20
|
+
* (traces hugging a chip edge look bad) that is only preferred against once
|
|
21
|
+
* the more important factors above are equal, so a boundary-hugging route is
|
|
22
|
+
* still chosen over one that actually overlaps a label or another trace.
|
|
23
|
+
*/
|
|
24
|
+
chipBoundaryOverlap: number
|
|
18
25
|
pathLength: number
|
|
19
26
|
}
|
|
20
27
|
|
|
@@ -18,6 +18,7 @@ import { pathKey, shiftSegmentOrth } from "./pathOps"
|
|
|
18
18
|
import type { FacingDirection } from "lib/utils/dir"
|
|
19
19
|
import { getDimsForOrientation } from "lib/solvers/NetLabelPlacementSolver/SingleNetLabelPlacementSolver/geometry"
|
|
20
20
|
import type { RectPadding } from "lib/utils/textBoxBounds"
|
|
21
|
+
import { calculateDirectShortPath } from "./calculateDirectShortPath"
|
|
21
22
|
|
|
22
23
|
type PathKey = string
|
|
23
24
|
|
|
@@ -80,21 +81,26 @@ export class SchematicTraceSingleLineSolver2 extends BaseSolver {
|
|
|
80
81
|
: [],
|
|
81
82
|
)
|
|
82
83
|
|
|
83
|
-
// Build initial elbow path
|
|
84
84
|
const [pin1, pin2] = this.pins
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
85
|
+
const directShortPath = calculateDirectShortPath(pin1, pin2)
|
|
86
|
+
|
|
87
|
+
// Build initial elbow path
|
|
88
|
+
this.baseElbow =
|
|
89
|
+
directShortPath ??
|
|
90
|
+
calculateElbow(
|
|
91
|
+
{
|
|
92
|
+
x: pin1.x,
|
|
93
|
+
y: pin1.y,
|
|
94
|
+
facingDirection: pin1._facingDirection!,
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
x: pin2.x,
|
|
98
|
+
y: pin2.y,
|
|
99
|
+
facingDirection: pin2._facingDirection!,
|
|
100
|
+
},
|
|
101
|
+
{ overshoot: 0.2 },
|
|
102
|
+
)
|
|
103
|
+
this.solvedTracePath = directShortPath
|
|
98
104
|
|
|
99
105
|
// Bounds defined by PA and PB
|
|
100
106
|
this.aabb = aabbFromPoints(
|
|
@@ -233,10 +239,6 @@ export class SchematicTraceSingleLineSolver2 extends BaseSolver {
|
|
|
233
239
|
return penalty
|
|
234
240
|
}
|
|
235
241
|
|
|
236
|
-
private pathCost(path: Point[]): number {
|
|
237
|
-
return this.pathLength(path) + this.getPinBandPenalty(path)
|
|
238
|
-
}
|
|
239
|
-
|
|
240
242
|
private isSegmentOutsidePinBand(a: Point, b: Point): boolean {
|
|
241
243
|
if (isHorizontal(a, b)) {
|
|
242
244
|
return a.y <= this.aabb.minY || a.y >= this.aabb.maxY
|
|
@@ -356,11 +358,15 @@ export class SchematicTraceSingleLineSolver2 extends BaseSolver {
|
|
|
356
358
|
candidates.push(...mids)
|
|
357
359
|
}
|
|
358
360
|
|
|
359
|
-
// Generate new shifted paths
|
|
361
|
+
// Generate new shifted paths. Order by path length first, then prefer paths
|
|
362
|
+
// that stay out of the pin band. The pin-band preference is a tiebreaker
|
|
363
|
+
// only: it must never make the solver pick a longer route (a flat additive
|
|
364
|
+
// penalty would, discarding a shorter valid route in favor of a detour).
|
|
360
365
|
const newStates: Array<{
|
|
361
366
|
path: Point[]
|
|
362
367
|
collisionRects: Set<ObstacleRect>
|
|
363
|
-
|
|
368
|
+
length: number
|
|
369
|
+
pinBandPenalty: number
|
|
364
370
|
}> = []
|
|
365
371
|
|
|
366
372
|
const addShiftedCandidate = (
|
|
@@ -380,8 +386,12 @@ export class SchematicTraceSingleLineSolver2 extends BaseSolver {
|
|
|
380
386
|
this.visited.add(key)
|
|
381
387
|
const nextSet = new Set(collisionRects)
|
|
382
388
|
nextSet.add(rect)
|
|
383
|
-
|
|
384
|
-
|
|
389
|
+
newStates.push({
|
|
390
|
+
path: newPath,
|
|
391
|
+
collisionRects: nextSet,
|
|
392
|
+
length: this.pathLength(newPath),
|
|
393
|
+
pinBandPenalty: this.getPinBandPenalty(newPath),
|
|
394
|
+
})
|
|
385
395
|
}
|
|
386
396
|
|
|
387
397
|
for (const coord of candidates) {
|
|
@@ -424,7 +434,9 @@ export class SchematicTraceSingleLineSolver2 extends BaseSolver {
|
|
|
424
434
|
}
|
|
425
435
|
}
|
|
426
436
|
|
|
427
|
-
newStates.sort(
|
|
437
|
+
newStates.sort(
|
|
438
|
+
(a, b) => a.length - b.length || a.pinBandPenalty - b.pinBandPenalty,
|
|
439
|
+
)
|
|
428
440
|
for (const st of newStates) {
|
|
429
441
|
this.queue.push({ path: st.path, collisionRects: st.collisionRects })
|
|
430
442
|
}
|