@tscircuit/schematic-trace-solver 0.0.92 → 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 +41 -19
- 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 +15 -9
- package/package.json +1 -1
- package/tests/bug-reports/bug-report-20260706T220324Z/__snapshots__/bug-report-20260706T220324Z.snap.svg +4 -6
- 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 +18 -20
- package/tests/repros/__snapshots__/repro-netlabel-overlap-trace.snap.svg +3 -3
- package/tests/repros/__snapshots__/repro-rectifier-trace-overlap.snap.svg +3 -3
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
|
@@ -1112,9 +1112,6 @@ var SchematicTraceSingleLineSolver2 = class extends BaseSolver {
|
|
|
1112
1112
|
}
|
|
1113
1113
|
return penalty;
|
|
1114
1114
|
}
|
|
1115
|
-
pathCost(path) {
|
|
1116
|
-
return this.pathLength(path) + this.getPinBandPenalty(path);
|
|
1117
|
-
}
|
|
1118
1115
|
isSegmentOutsidePinBand(a, b) {
|
|
1119
1116
|
if (isHorizontal(a, b)) {
|
|
1120
1117
|
return a.y <= this.aabb.minY || a.y >= this.aabb.maxY;
|
|
@@ -1210,8 +1207,12 @@ var SchematicTraceSingleLineSolver2 = class extends BaseSolver {
|
|
|
1210
1207
|
this.visited.add(key);
|
|
1211
1208
|
const nextSet = new Set(collisionRects);
|
|
1212
1209
|
nextSet.add(rect);
|
|
1213
|
-
|
|
1214
|
-
|
|
1210
|
+
newStates.push({
|
|
1211
|
+
path: newPath,
|
|
1212
|
+
collisionRects: nextSet,
|
|
1213
|
+
length: this.pathLength(newPath),
|
|
1214
|
+
pinBandPenalty: this.getPinBandPenalty(newPath)
|
|
1215
|
+
});
|
|
1215
1216
|
};
|
|
1216
1217
|
for (const coord of candidates) {
|
|
1217
1218
|
addShiftedCandidate(segIndex, axis, coord);
|
|
@@ -1243,7 +1244,9 @@ var SchematicTraceSingleLineSolver2 = class extends BaseSolver {
|
|
|
1243
1244
|
addShiftedCandidate(adjacentSegIndex, adjacentAxis, coord);
|
|
1244
1245
|
}
|
|
1245
1246
|
}
|
|
1246
|
-
newStates.sort(
|
|
1247
|
+
newStates.sort(
|
|
1248
|
+
(a2, b2) => a2.length - b2.length || a2.pinBandPenalty - b2.pinBandPenalty
|
|
1249
|
+
);
|
|
1247
1250
|
for (const st of newStates) {
|
|
1248
1251
|
this.queue.push({ path: st.path, collisionRects: st.collisionRects });
|
|
1249
1252
|
}
|
|
@@ -5627,6 +5630,19 @@ var doesPathRunAlongChipBoundary = (path, chipObstacles) => {
|
|
|
5627
5630
|
}
|
|
5628
5631
|
return false;
|
|
5629
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
|
+
};
|
|
5630
5646
|
var getSegmentOverlapWithRectSpan = (start, end, rect) => {
|
|
5631
5647
|
const isVertical4 = Math.abs(start.x - end.x) < EPS7;
|
|
5632
5648
|
if (isVertical4) {
|
|
@@ -5658,7 +5674,8 @@ var findBestReroutePath = ({
|
|
|
5658
5674
|
obstacleLabel,
|
|
5659
5675
|
outputTraces,
|
|
5660
5676
|
outputNetLabelPlacements,
|
|
5661
|
-
candidateResults
|
|
5677
|
+
candidateResults,
|
|
5678
|
+
chipObstacles
|
|
5662
5679
|
});
|
|
5663
5680
|
markSelectedCandidate(candidateResults, bestPath);
|
|
5664
5681
|
return { bestPath, candidateResults };
|
|
@@ -5745,14 +5762,6 @@ var createCandidateResult = ({
|
|
|
5745
5762
|
selected: false
|
|
5746
5763
|
};
|
|
5747
5764
|
}
|
|
5748
|
-
if (doesPathRunAlongChipBoundary(path, chipObstacles)) {
|
|
5749
|
-
return {
|
|
5750
|
-
path,
|
|
5751
|
-
status: "chip-collision",
|
|
5752
|
-
usesHorizontalSegmentPush,
|
|
5753
|
-
selected: false
|
|
5754
|
-
};
|
|
5755
|
-
}
|
|
5756
5765
|
return {
|
|
5757
5766
|
path,
|
|
5758
5767
|
score: scoreTracePath({
|
|
@@ -5760,7 +5769,8 @@ var createCandidateResult = ({
|
|
|
5760
5769
|
tracePath: path,
|
|
5761
5770
|
obstacleLabel,
|
|
5762
5771
|
outputTraces,
|
|
5763
|
-
outputNetLabelPlacements
|
|
5772
|
+
outputNetLabelPlacements,
|
|
5773
|
+
chipObstacles
|
|
5764
5774
|
}),
|
|
5765
5775
|
status: "valid",
|
|
5766
5776
|
usesHorizontalSegmentPush,
|
|
@@ -5850,7 +5860,8 @@ var selectBestReroutePath = ({
|
|
|
5850
5860
|
obstacleLabel,
|
|
5851
5861
|
outputTraces,
|
|
5852
5862
|
outputNetLabelPlacements,
|
|
5853
|
-
candidateResults
|
|
5863
|
+
candidateResults,
|
|
5864
|
+
chipObstacles
|
|
5854
5865
|
}) => {
|
|
5855
5866
|
for (const candidate of candidateResults) {
|
|
5856
5867
|
if (!candidate.usesHorizontalSegmentPush) continue;
|
|
@@ -5864,7 +5875,8 @@ var selectBestReroutePath = ({
|
|
|
5864
5875
|
tracePath: trace.tracePath,
|
|
5865
5876
|
obstacleLabel,
|
|
5866
5877
|
outputTraces,
|
|
5867
|
-
outputNetLabelPlacements
|
|
5878
|
+
outputNetLabelPlacements,
|
|
5879
|
+
chipObstacles
|
|
5868
5880
|
});
|
|
5869
5881
|
for (const candidate of candidateResults) {
|
|
5870
5882
|
if (candidate.status !== "valid" || !candidate.score) continue;
|
|
@@ -5950,7 +5962,8 @@ var scoreTracePath = ({
|
|
|
5950
5962
|
tracePath,
|
|
5951
5963
|
obstacleLabel,
|
|
5952
5964
|
outputTraces,
|
|
5953
|
-
outputNetLabelPlacements
|
|
5965
|
+
outputNetLabelPlacements,
|
|
5966
|
+
chipObstacles
|
|
5954
5967
|
}) => {
|
|
5955
5968
|
const candidateTrace = { ...trace, tracePath };
|
|
5956
5969
|
return {
|
|
@@ -5960,6 +5973,7 @@ var scoreTracePath = ({
|
|
|
5960
5973
|
}).length,
|
|
5961
5974
|
labelHugDistance: getLabelHugDistance(tracePath, obstacleLabel),
|
|
5962
5975
|
traceIntersections: countTraceIntersections(candidateTrace, outputTraces),
|
|
5976
|
+
chipBoundaryOverlap: getChipBoundaryOverlap(tracePath, chipObstacles),
|
|
5963
5977
|
pathLength: getPathLength(tracePath)
|
|
5964
5978
|
};
|
|
5965
5979
|
};
|
|
@@ -5972,6 +5986,14 @@ var countTraceIntersections = (trace, outputTraces) => {
|
|
|
5972
5986
|
return count;
|
|
5973
5987
|
};
|
|
5974
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
|
+
}
|
|
5975
5997
|
if (score.labelIntersections !== bestScore.labelIntersections) {
|
|
5976
5998
|
return score.labelIntersections < bestScore.labelIntersections;
|
|
5977
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
|
|
|
@@ -239,10 +239,6 @@ export class SchematicTraceSingleLineSolver2 extends BaseSolver {
|
|
|
239
239
|
return penalty
|
|
240
240
|
}
|
|
241
241
|
|
|
242
|
-
private pathCost(path: Point[]): number {
|
|
243
|
-
return this.pathLength(path) + this.getPinBandPenalty(path)
|
|
244
|
-
}
|
|
245
|
-
|
|
246
242
|
private isSegmentOutsidePinBand(a: Point, b: Point): boolean {
|
|
247
243
|
if (isHorizontal(a, b)) {
|
|
248
244
|
return a.y <= this.aabb.minY || a.y >= this.aabb.maxY
|
|
@@ -362,11 +358,15 @@ export class SchematicTraceSingleLineSolver2 extends BaseSolver {
|
|
|
362
358
|
candidates.push(...mids)
|
|
363
359
|
}
|
|
364
360
|
|
|
365
|
-
// 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).
|
|
366
365
|
const newStates: Array<{
|
|
367
366
|
path: Point[]
|
|
368
367
|
collisionRects: Set<ObstacleRect>
|
|
369
|
-
|
|
368
|
+
length: number
|
|
369
|
+
pinBandPenalty: number
|
|
370
370
|
}> = []
|
|
371
371
|
|
|
372
372
|
const addShiftedCandidate = (
|
|
@@ -386,8 +386,12 @@ export class SchematicTraceSingleLineSolver2 extends BaseSolver {
|
|
|
386
386
|
this.visited.add(key)
|
|
387
387
|
const nextSet = new Set(collisionRects)
|
|
388
388
|
nextSet.add(rect)
|
|
389
|
-
|
|
390
|
-
|
|
389
|
+
newStates.push({
|
|
390
|
+
path: newPath,
|
|
391
|
+
collisionRects: nextSet,
|
|
392
|
+
length: this.pathLength(newPath),
|
|
393
|
+
pinBandPenalty: this.getPinBandPenalty(newPath),
|
|
394
|
+
})
|
|
391
395
|
}
|
|
392
396
|
|
|
393
397
|
for (const coord of candidates) {
|
|
@@ -430,7 +434,9 @@ export class SchematicTraceSingleLineSolver2 extends BaseSolver {
|
|
|
430
434
|
}
|
|
431
435
|
}
|
|
432
436
|
|
|
433
|
-
newStates.sort(
|
|
437
|
+
newStates.sort(
|
|
438
|
+
(a, b) => a.length - b.length || a.pinBandPenalty - b.pinBandPenalty,
|
|
439
|
+
)
|
|
434
440
|
for (const st of newStates) {
|
|
435
441
|
this.queue.push({ path: st.path, collisionRects: st.collisionRects })
|
|
436
442
|
}
|