@tscircuit/schematic-trace-solver 0.0.119 → 0.0.121
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.js +156 -52
- package/lib/solvers/AvailableNetOrientationSolver/AvailableNetOrientationSolver.ts +3 -3
- package/lib/solvers/TraceCleanupSolver/sameNetRailAlignment/evaluateRailGroup.ts +79 -59
- package/lib/solvers/TraceCleanupSolver/sameNetRailAlignment/getRailAlignmentFallbackCoordinates.ts +96 -0
- package/lib/utils/doesPathCoincideWithTraces.ts +71 -1
- package/package.json +1 -1
- package/tests/bug-reports/bug-report-20260707T092615Z/__snapshots__/bug-report-20260707T092615Z.snap.svg +3 -3
- package/tests/bug-reports/bug-report-20260728T225606Z/__snapshots__/bug-report-20260728T225606Z.snap.svg +3 -3
- package/tests/repros/__snapshots__/repro-atmega328p-missing-gnd-netlabel.snap.svg +3 -3
- package/tests/repros/__snapshots__/repro-bq25895-cross-net-trace-overlap.snap.svg +101 -0
- package/tests/repros/__snapshots__/repro-ground-label-on-lower-rail-end.snap.svg +58 -0
- package/tests/repros/assets/repro-bq25895-cross-net-trace-overlap.input.json +498 -0
- package/tests/repros/repro-bq25895-cross-net-trace-overlap.test.ts +75 -0
- package/tests/repros/repro-ground-label-on-lower-rail-end.test.ts +84 -0
- package/tests/solvers/TraceCleanupSolver/alignSameNetRails-trace-clearance.test.ts +68 -0
package/dist/index.js
CHANGED
|
@@ -3922,7 +3922,12 @@ var generateRerouteCandidates = ({
|
|
|
3922
3922
|
};
|
|
3923
3923
|
|
|
3924
3924
|
// lib/utils/doesPathCoincideWithTraces.ts
|
|
3925
|
+
import { boundsIntersection } from "@tscircuit/math-utils";
|
|
3925
3926
|
var COINCIDENT_EPS = 2e-3;
|
|
3927
|
+
var GEOMETRY_EPS = 1e-6;
|
|
3928
|
+
var SCHEMATIC_TRACE_STROKE_WIDTH = 0.02;
|
|
3929
|
+
var SCHEMATIC_TRACE_MIN_CENTERLINE_CLEARANCE = SCHEMATIC_TRACE_STROKE_WIDTH + GEOMETRY_EPS;
|
|
3930
|
+
var SCHEMATIC_TRACE_MIN_VISUAL_CENTERLINE_CLEARANCE = SCHEMATIC_TRACE_STROKE_WIDTH * 3;
|
|
3926
3931
|
var doesPathCoincideWithTraces = (path, traces) => {
|
|
3927
3932
|
const rangesOverlap1D = (a1, a2, b1, b2) => Math.min(Math.max(a1, a2), Math.max(b1, b2)) - Math.max(Math.min(a1, a2), Math.min(b1, b2)) > COINCIDENT_EPS;
|
|
3928
3933
|
for (let i = 0; i < path.length - 1; i++) {
|
|
@@ -3954,6 +3959,44 @@ var doesPathCoincideWithTraces = (path, traces) => {
|
|
|
3954
3959
|
}
|
|
3955
3960
|
return false;
|
|
3956
3961
|
};
|
|
3962
|
+
var doesPathOverlapTraceStrokes = (path, traces) => {
|
|
3963
|
+
const traceStrokeRadius = SCHEMATIC_TRACE_MIN_CENTERLINE_CLEARANCE / 2;
|
|
3964
|
+
const getStrokeBounds = (start, end, isVertical5) => isVertical5 ? {
|
|
3965
|
+
minX: start.x - traceStrokeRadius,
|
|
3966
|
+
maxX: start.x + traceStrokeRadius,
|
|
3967
|
+
minY: Math.min(start.y, end.y),
|
|
3968
|
+
maxY: Math.max(start.y, end.y)
|
|
3969
|
+
} : {
|
|
3970
|
+
minX: Math.min(start.x, end.x),
|
|
3971
|
+
maxX: Math.max(start.x, end.x),
|
|
3972
|
+
minY: start.y - traceStrokeRadius,
|
|
3973
|
+
maxY: start.y + traceStrokeRadius
|
|
3974
|
+
};
|
|
3975
|
+
for (let pathIndex = 0; pathIndex < path.length - 1; pathIndex++) {
|
|
3976
|
+
const pathStart = path[pathIndex];
|
|
3977
|
+
const pathEnd = path[pathIndex + 1];
|
|
3978
|
+
const isVertical5 = Math.abs(pathStart.x - pathEnd.x) < COINCIDENT_EPS;
|
|
3979
|
+
const isHorizontal4 = Math.abs(pathStart.y - pathEnd.y) < COINCIDENT_EPS;
|
|
3980
|
+
if (!isVertical5 && !isHorizontal4) continue;
|
|
3981
|
+
const pathBounds = getStrokeBounds(pathStart, pathEnd, isVertical5);
|
|
3982
|
+
for (const trace of traces) {
|
|
3983
|
+
for (let traceIndex = 0; traceIndex < trace.tracePath.length - 1; traceIndex++) {
|
|
3984
|
+
const traceStart = trace.tracePath[traceIndex];
|
|
3985
|
+
const traceEnd = trace.tracePath[traceIndex + 1];
|
|
3986
|
+
const isParallel = isVertical5 ? Math.abs(traceStart.x - traceEnd.x) < COINCIDENT_EPS : Math.abs(traceStart.y - traceEnd.y) < COINCIDENT_EPS;
|
|
3987
|
+
if (!isParallel) continue;
|
|
3988
|
+
const overlap = boundsIntersection(
|
|
3989
|
+
pathBounds,
|
|
3990
|
+
getStrokeBounds(traceStart, traceEnd, isVertical5)
|
|
3991
|
+
);
|
|
3992
|
+
if (!overlap) continue;
|
|
3993
|
+
const overlapLength = isVertical5 ? overlap.maxY - overlap.minY : overlap.maxX - overlap.minX;
|
|
3994
|
+
if (overlapLength > COINCIDENT_EPS) return true;
|
|
3995
|
+
}
|
|
3996
|
+
}
|
|
3997
|
+
}
|
|
3998
|
+
return false;
|
|
3999
|
+
};
|
|
3957
4000
|
|
|
3958
4001
|
// lib/solvers/TraceLabelOverlapAvoidanceSolver/sub-solvers/SingleOverlapSolver/SingleOverlapSolver.ts
|
|
3959
4002
|
var MAX_TRIES = 5;
|
|
@@ -5307,6 +5350,53 @@ var balanceZShapes = ({
|
|
|
5307
5350
|
};
|
|
5308
5351
|
};
|
|
5309
5352
|
|
|
5353
|
+
// lib/solvers/TraceCleanupSolver/sameNetRailAlignment/getRailAlignmentFallbackCoordinates.ts
|
|
5354
|
+
import { boundsIntersection as boundsIntersection2 } from "@tscircuit/math-utils";
|
|
5355
|
+
var getAlongBounds = (orientation, minAlong, maxAlong) => orientation === "vertical" ? { minX: 0, maxX: 0, minY: minAlong, maxY: maxAlong } : { minX: minAlong, maxX: maxAlong, minY: 0, maxY: 0 };
|
|
5356
|
+
var hasPositiveAlongOverlap = (orientation, first, second) => {
|
|
5357
|
+
const overlap = boundsIntersection2(first, second);
|
|
5358
|
+
if (!overlap) return false;
|
|
5359
|
+
const overlapLength = orientation === "vertical" ? overlap.maxY - overlap.minY : overlap.maxX - overlap.minX;
|
|
5360
|
+
return overlapLength > RAIL_ALIGNMENT_EPSILON;
|
|
5361
|
+
};
|
|
5362
|
+
var getRailAlignmentFallbackCoordinates = ({
|
|
5363
|
+
group,
|
|
5364
|
+
originalCoordinates,
|
|
5365
|
+
otherNetTraces
|
|
5366
|
+
}) => {
|
|
5367
|
+
const orientation = group[0].orientation;
|
|
5368
|
+
const groupAlongBounds = group.map(
|
|
5369
|
+
(segment) => getAlongBounds(orientation, segment.minAlong, segment.maxAlong)
|
|
5370
|
+
);
|
|
5371
|
+
const fallbackClearance = SCHEMATIC_TRACE_MIN_VISUAL_CENTERLINE_CLEARANCE;
|
|
5372
|
+
const coordinates = [];
|
|
5373
|
+
for (const trace of otherNetTraces) {
|
|
5374
|
+
for (let index = 0; index < trace.tracePath.length - 1; index++) {
|
|
5375
|
+
const start = trace.tracePath[index];
|
|
5376
|
+
const end = trace.tracePath[index + 1];
|
|
5377
|
+
if (getRailOrientation(start, end) !== orientation) continue;
|
|
5378
|
+
const minAlong = orientation === "vertical" ? Math.min(start.y, end.y) : Math.min(start.x, end.x);
|
|
5379
|
+
const maxAlong = orientation === "vertical" ? Math.max(start.y, end.y) : Math.max(start.x, end.x);
|
|
5380
|
+
const traceAlongBounds = getAlongBounds(orientation, minAlong, maxAlong);
|
|
5381
|
+
if (!groupAlongBounds.some(
|
|
5382
|
+
(bounds) => hasPositiveAlongOverlap(orientation, bounds, traceAlongBounds)
|
|
5383
|
+
)) {
|
|
5384
|
+
continue;
|
|
5385
|
+
}
|
|
5386
|
+
const coordinate = orientation === "vertical" ? start.x : start.y;
|
|
5387
|
+
const touchesOriginalCandidate = originalCoordinates.some(
|
|
5388
|
+
(originalCoordinate) => Math.abs(originalCoordinate - coordinate) <= SCHEMATIC_TRACE_MIN_CENTERLINE_CLEARANCE
|
|
5389
|
+
);
|
|
5390
|
+
if (!touchesOriginalCandidate) continue;
|
|
5391
|
+
coordinates.push(
|
|
5392
|
+
coordinate - fallbackClearance,
|
|
5393
|
+
coordinate + fallbackClearance
|
|
5394
|
+
);
|
|
5395
|
+
}
|
|
5396
|
+
}
|
|
5397
|
+
return getDistinctCoordinates(coordinates);
|
|
5398
|
+
};
|
|
5399
|
+
|
|
5310
5400
|
// lib/solvers/TraceCleanupSolver/sameNetRailAlignment/moveRailSegments.ts
|
|
5311
5401
|
var moveRailSegments = (trace, segments, coordinate) => {
|
|
5312
5402
|
const pointsToMove = /* @__PURE__ */ new Set();
|
|
@@ -5578,7 +5668,7 @@ var evaluateRailGroup = ({
|
|
|
5578
5668
|
(trace) => groupTraceIds.has(trace.mspPairId)
|
|
5579
5669
|
);
|
|
5580
5670
|
const baseline = getTraceGeometryMetrics(originalGroupTraces, traces);
|
|
5581
|
-
const
|
|
5671
|
+
const originalCoordinates = getDistinctCoordinates(
|
|
5582
5672
|
group.map((segment) => segment.coordinate)
|
|
5583
5673
|
);
|
|
5584
5674
|
const otherNetTraces = traces.filter(
|
|
@@ -5587,58 +5677,72 @@ var evaluateRailGroup = ({
|
|
|
5587
5677
|
const immutableSameNetTraces = traces.filter(
|
|
5588
5678
|
(trace) => trace.globalConnNetId === group[0].globalConnNetId && !eligibleTraceIds.has(trace.mspPairId)
|
|
5589
5679
|
);
|
|
5590
|
-
|
|
5591
|
-
|
|
5592
|
-
const
|
|
5593
|
-
|
|
5594
|
-
const
|
|
5595
|
-
|
|
5596
|
-
|
|
5597
|
-
|
|
5680
|
+
const evaluateCoordinates = (coordinates) => {
|
|
5681
|
+
let best = null;
|
|
5682
|
+
for (const coordinate of coordinates) {
|
|
5683
|
+
const candidateMap = /* @__PURE__ */ new Map();
|
|
5684
|
+
for (const trace of originalGroupTraces) {
|
|
5685
|
+
const candidateTrace = moveRailSegments(
|
|
5686
|
+
trace,
|
|
5687
|
+
group.filter((segment) => segment.traceId === trace.mspPairId),
|
|
5688
|
+
coordinate
|
|
5689
|
+
);
|
|
5690
|
+
candidateMap.set(trace.mspPairId, candidateTrace);
|
|
5691
|
+
}
|
|
5692
|
+
const candidateTraces = [...candidateMap.values()];
|
|
5693
|
+
const allCandidateTraces = traces.map(
|
|
5694
|
+
(trace) => candidateMap.get(trace.mspPairId) ?? trace
|
|
5598
5695
|
);
|
|
5599
|
-
|
|
5600
|
-
|
|
5601
|
-
|
|
5602
|
-
|
|
5603
|
-
|
|
5604
|
-
|
|
5605
|
-
|
|
5606
|
-
|
|
5607
|
-
|
|
5608
|
-
netLabels: netLabelPlacements
|
|
5609
|
-
}).length === 0 && !doesPathCoincideWithTraces(candidate2.tracePath, otherNetTraces) && !doesPathCoincideWithTraces(
|
|
5610
|
-
candidate2.tracePath,
|
|
5611
|
-
immutableSameNetTraces.filter(
|
|
5612
|
-
(trace) => trace.mspPairId !== candidate2.mspPairId
|
|
5696
|
+
const candidatesAreClear = candidateTraces.every(
|
|
5697
|
+
(candidate2) => !isPathCollidingWithObstacles(candidate2.tracePath, obstacles) && detectTraceLabelOverlap({
|
|
5698
|
+
traces: [candidate2],
|
|
5699
|
+
netLabels: netLabelPlacements
|
|
5700
|
+
}).length === 0 && !doesPathOverlapTraceStrokes(candidate2.tracePath, otherNetTraces) && !doesPathCoincideWithTraces(
|
|
5701
|
+
candidate2.tracePath,
|
|
5702
|
+
immutableSameNetTraces.filter(
|
|
5703
|
+
(trace) => trace.mspPairId !== candidate2.mspPairId
|
|
5704
|
+
)
|
|
5613
5705
|
)
|
|
5614
|
-
)
|
|
5615
|
-
);
|
|
5616
|
-
if (!candidatesAreClear) continue;
|
|
5617
|
-
if (!preservesLabelAnchors(netLabelPlacements, traces, allCandidateTraces)) {
|
|
5618
|
-
continue;
|
|
5619
|
-
}
|
|
5620
|
-
const metrics = getTraceGeometryMetrics(candidateTraces, allCandidateTraces);
|
|
5621
|
-
if (metrics.otherNetCrossings > baseline.otherNetCrossings) continue;
|
|
5622
|
-
if (!isReadabilityImprovement(metrics, baseline)) continue;
|
|
5623
|
-
const score = {
|
|
5624
|
-
...metrics,
|
|
5625
|
-
displacement: group.reduce(
|
|
5626
|
-
(sum, segment) => sum + Math.abs(segment.coordinate - coordinate),
|
|
5627
|
-
0
|
|
5628
|
-
),
|
|
5629
|
-
coordinate
|
|
5630
|
-
};
|
|
5631
|
-
const changedTraceIds = candidateTraces.filter((candidate2) => {
|
|
5632
|
-
const original = traces.find(
|
|
5633
|
-
(trace) => trace.mspPairId === candidate2.mspPairId
|
|
5634
5706
|
);
|
|
5635
|
-
|
|
5636
|
-
|
|
5637
|
-
|
|
5638
|
-
|
|
5639
|
-
|
|
5640
|
-
|
|
5641
|
-
|
|
5707
|
+
if (!candidatesAreClear) continue;
|
|
5708
|
+
if (!preservesLabelAnchors(netLabelPlacements, traces, allCandidateTraces)) {
|
|
5709
|
+
continue;
|
|
5710
|
+
}
|
|
5711
|
+
const metrics = getTraceGeometryMetrics(
|
|
5712
|
+
candidateTraces,
|
|
5713
|
+
allCandidateTraces
|
|
5714
|
+
);
|
|
5715
|
+
if (metrics.otherNetCrossings > baseline.otherNetCrossings) continue;
|
|
5716
|
+
if (!isReadabilityImprovement(metrics, baseline)) continue;
|
|
5717
|
+
const score = {
|
|
5718
|
+
...metrics,
|
|
5719
|
+
displacement: group.reduce(
|
|
5720
|
+
(sum, segment) => sum + Math.abs(segment.coordinate - coordinate),
|
|
5721
|
+
0
|
|
5722
|
+
),
|
|
5723
|
+
coordinate
|
|
5724
|
+
};
|
|
5725
|
+
const changedTraceIds = candidateTraces.filter((candidate2) => {
|
|
5726
|
+
const original = traces.find(
|
|
5727
|
+
(trace) => trace.mspPairId === candidate2.mspPairId
|
|
5728
|
+
);
|
|
5729
|
+
return tracePathChanged(original, candidate2);
|
|
5730
|
+
}).map((trace) => trace.mspPairId);
|
|
5731
|
+
if (changedTraceIds.length === 0) continue;
|
|
5732
|
+
const candidate = { traces: allCandidateTraces, changedTraceIds, score };
|
|
5733
|
+
if (!best || scoreIsBetter(candidate.score, best.score)) best = candidate;
|
|
5734
|
+
}
|
|
5735
|
+
return best;
|
|
5736
|
+
};
|
|
5737
|
+
const originalCandidate = evaluateCoordinates(originalCoordinates);
|
|
5738
|
+
if (originalCandidate) return originalCandidate;
|
|
5739
|
+
return evaluateCoordinates(
|
|
5740
|
+
getRailAlignmentFallbackCoordinates({
|
|
5741
|
+
group,
|
|
5742
|
+
originalCoordinates,
|
|
5743
|
+
otherNetTraces
|
|
5744
|
+
})
|
|
5745
|
+
);
|
|
5642
5746
|
};
|
|
5643
5747
|
|
|
5644
5748
|
// lib/solvers/TraceCleanupSolver/sameNetRailAlignment/getComponentSideRailSegments.ts
|
|
@@ -7999,9 +8103,9 @@ var AvailableNetOrientationSolver = class extends BaseSolver {
|
|
|
7999
8103
|
(connection) => connection.netId === label.netId
|
|
8000
8104
|
);
|
|
8001
8105
|
const isTwoPinNet = netConnection?.pinIds.length === 2;
|
|
8002
|
-
const
|
|
8106
|
+
const isDistanceSplitVerticalRail = (netConnection?.pinIds.length ?? 0) > 2 && isYOrientation(requiredOrientation) && this.hasPortOnlyLabelOnSameNet(label);
|
|
8003
8107
|
const isPairedSameSidePowerRail = isYOrientation(requiredOrientation) && this.hasOppositeVerticalRailOnSameChipSide(label, requiredOrientation);
|
|
8004
|
-
if (orientations.length === 1 && isYOrientation(requiredOrientation) && this.isOutwardHorizontalFallback(label) && this.hasTraceContinuingInOrientation(label, requiredOrientation) && (
|
|
8108
|
+
if (orientations.length === 1 && isYOrientation(requiredOrientation) && this.isOutwardHorizontalFallback(label) && this.hasTraceContinuingInOrientation(label, requiredOrientation) && (isDistanceSplitVerticalRail || isPairedSameSidePowerRail)) {
|
|
8005
8109
|
const traceAnchorCandidate2 = this.findValidOutwardTraceAnchorCandidate(
|
|
8006
8110
|
label,
|
|
8007
8111
|
requiredOrientation,
|
|
@@ -258,9 +258,9 @@ export class AvailableNetOrientationSolver extends BaseSolver {
|
|
|
258
258
|
(connection) => connection.netId === label.netId,
|
|
259
259
|
)
|
|
260
260
|
const isTwoPinNet = netConnection?.pinIds.length === 2
|
|
261
|
-
const
|
|
261
|
+
const isDistanceSplitVerticalRail =
|
|
262
262
|
(netConnection?.pinIds.length ?? 0) > 2 &&
|
|
263
|
-
requiredOrientation
|
|
263
|
+
isYOrientation(requiredOrientation) &&
|
|
264
264
|
this.hasPortOnlyLabelOnSameNet(label)
|
|
265
265
|
const isPairedSameSidePowerRail =
|
|
266
266
|
isYOrientation(requiredOrientation) &&
|
|
@@ -270,7 +270,7 @@ export class AvailableNetOrientationSolver extends BaseSolver {
|
|
|
270
270
|
isYOrientation(requiredOrientation) &&
|
|
271
271
|
this.isOutwardHorizontalFallback(label) &&
|
|
272
272
|
this.hasTraceContinuingInOrientation(label, requiredOrientation) &&
|
|
273
|
-
(
|
|
273
|
+
(isDistanceSplitVerticalRail || isPairedSameSidePowerRail)
|
|
274
274
|
) {
|
|
275
275
|
// Keep the established outward column, but attach at the furthest trace
|
|
276
276
|
// point in the required vertical direction. This places y+ labels above
|
|
@@ -3,8 +3,12 @@ import type { SolvedTracePath } from "lib/solvers/SchematicTraceLinesSolver/Sche
|
|
|
3
3
|
import { isPathCollidingWithObstacles } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver2/collisions"
|
|
4
4
|
import type { ObstacleRect } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver2/rect"
|
|
5
5
|
import { detectTraceLabelOverlap } from "lib/solvers/TraceLabelOverlapAvoidanceSolver/detectTraceLabelOverlap"
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
doesPathCoincideWithTraces,
|
|
8
|
+
doesPathOverlapTraceStrokes,
|
|
9
|
+
} from "lib/utils/doesPathCoincideWithTraces"
|
|
7
10
|
import { getDistinctCoordinates, pointsEqual } from "./geometry"
|
|
11
|
+
import { getRailAlignmentFallbackCoordinates } from "./getRailAlignmentFallbackCoordinates"
|
|
8
12
|
import { moveRailSegments } from "./moveRailSegments"
|
|
9
13
|
import { preservesLabelAnchors } from "./preservesLabelAnchors"
|
|
10
14
|
import {
|
|
@@ -43,7 +47,7 @@ export const evaluateRailGroup = ({
|
|
|
43
47
|
groupTraceIds.has(trace.mspPairId),
|
|
44
48
|
)
|
|
45
49
|
const baseline = getTraceGeometryMetrics(originalGroupTraces, traces)
|
|
46
|
-
const
|
|
50
|
+
const originalCoordinates = getDistinctCoordinates(
|
|
47
51
|
group.map((segment) => segment.coordinate),
|
|
48
52
|
)
|
|
49
53
|
const otherNetTraces = traces.filter(
|
|
@@ -55,70 +59,86 @@ export const evaluateRailGroup = ({
|
|
|
55
59
|
!eligibleTraceIds.has(trace.mspPairId),
|
|
56
60
|
)
|
|
57
61
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
const
|
|
62
|
+
const evaluateCoordinates = (coordinates: number[]) => {
|
|
63
|
+
let best: AlignmentCandidate | null = null
|
|
64
|
+
for (const coordinate of coordinates) {
|
|
65
|
+
const candidateMap = new Map<string, SolvedTracePath>()
|
|
61
66
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
67
|
+
for (const trace of originalGroupTraces) {
|
|
68
|
+
const candidateTrace = moveRailSegments(
|
|
69
|
+
trace,
|
|
70
|
+
group.filter((segment) => segment.traceId === trace.mspPairId),
|
|
71
|
+
coordinate,
|
|
72
|
+
)
|
|
73
|
+
candidateMap.set(trace.mspPairId, candidateTrace)
|
|
74
|
+
}
|
|
70
75
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
76
|
+
const candidateTraces = [...candidateMap.values()]
|
|
77
|
+
const allCandidateTraces = traces.map(
|
|
78
|
+
(trace) => candidateMap.get(trace.mspPairId) ?? trace,
|
|
79
|
+
)
|
|
80
|
+
const candidatesAreClear = candidateTraces.every(
|
|
81
|
+
(candidate) =>
|
|
82
|
+
!isPathCollidingWithObstacles(candidate.tracePath, obstacles) &&
|
|
83
|
+
detectTraceLabelOverlap({
|
|
84
|
+
traces: [candidate],
|
|
85
|
+
netLabels: netLabelPlacements,
|
|
86
|
+
}).length === 0 &&
|
|
87
|
+
!doesPathOverlapTraceStrokes(candidate.tracePath, otherNetTraces) &&
|
|
88
|
+
!doesPathCoincideWithTraces(
|
|
89
|
+
candidate.tracePath,
|
|
90
|
+
immutableSameNetTraces.filter(
|
|
91
|
+
(trace) => trace.mspPairId !== candidate.mspPairId,
|
|
92
|
+
),
|
|
87
93
|
),
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
}
|
|
94
|
+
)
|
|
95
|
+
if (!candidatesAreClear) continue
|
|
96
|
+
if (
|
|
97
|
+
!preservesLabelAnchors(netLabelPlacements, traces, allCandidateTraces)
|
|
98
|
+
) {
|
|
99
|
+
continue
|
|
100
|
+
}
|
|
96
101
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
102
|
+
const metrics = getTraceGeometryMetrics(
|
|
103
|
+
candidateTraces,
|
|
104
|
+
allCandidateTraces,
|
|
105
|
+
)
|
|
106
|
+
if (metrics.otherNetCrossings > baseline.otherNetCrossings) continue
|
|
107
|
+
if (!isReadabilityImprovement(metrics, baseline)) continue
|
|
100
108
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
109
|
+
const score: AlignmentScore = {
|
|
110
|
+
...metrics,
|
|
111
|
+
displacement: group.reduce(
|
|
112
|
+
(sum, segment) => sum + Math.abs(segment.coordinate - coordinate),
|
|
113
|
+
0,
|
|
114
|
+
),
|
|
115
|
+
coordinate,
|
|
116
|
+
}
|
|
117
|
+
const changedTraceIds = candidateTraces
|
|
118
|
+
.filter((candidate) => {
|
|
119
|
+
const original = traces.find(
|
|
120
|
+
(trace) => trace.mspPairId === candidate.mspPairId,
|
|
121
|
+
)!
|
|
122
|
+
return tracePathChanged(original, candidate)
|
|
123
|
+
})
|
|
124
|
+
.map((trace) => trace.mspPairId)
|
|
125
|
+
if (changedTraceIds.length === 0) continue
|
|
126
|
+
|
|
127
|
+
const candidate = { traces: allCandidateTraces, changedTraceIds, score }
|
|
128
|
+
if (!best || scoreIsBetter(candidate.score, best.score)) best = candidate
|
|
108
129
|
}
|
|
109
|
-
const changedTraceIds = candidateTraces
|
|
110
|
-
.filter((candidate) => {
|
|
111
|
-
const original = traces.find(
|
|
112
|
-
(trace) => trace.mspPairId === candidate.mspPairId,
|
|
113
|
-
)!
|
|
114
|
-
return tracePathChanged(original, candidate)
|
|
115
|
-
})
|
|
116
|
-
.map((trace) => trace.mspPairId)
|
|
117
|
-
if (changedTraceIds.length === 0) continue
|
|
118
130
|
|
|
119
|
-
|
|
120
|
-
if (!best || scoreIsBetter(candidate.score, best.score)) best = candidate
|
|
131
|
+
return best
|
|
121
132
|
}
|
|
122
133
|
|
|
123
|
-
|
|
134
|
+
const originalCandidate = evaluateCoordinates(originalCoordinates)
|
|
135
|
+
if (originalCandidate) return originalCandidate
|
|
136
|
+
|
|
137
|
+
return evaluateCoordinates(
|
|
138
|
+
getRailAlignmentFallbackCoordinates({
|
|
139
|
+
group,
|
|
140
|
+
originalCoordinates,
|
|
141
|
+
otherNetTraces,
|
|
142
|
+
}),
|
|
143
|
+
)
|
|
124
144
|
}
|
package/lib/solvers/TraceCleanupSolver/sameNetRailAlignment/getRailAlignmentFallbackCoordinates.ts
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { boundsIntersection, type Bounds } from "@tscircuit/math-utils"
|
|
2
|
+
import type { SolvedTracePath } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceLinesSolver"
|
|
3
|
+
import {
|
|
4
|
+
SCHEMATIC_TRACE_MIN_CENTERLINE_CLEARANCE,
|
|
5
|
+
SCHEMATIC_TRACE_MIN_VISUAL_CENTERLINE_CLEARANCE,
|
|
6
|
+
} from "lib/utils/doesPathCoincideWithTraces"
|
|
7
|
+
import {
|
|
8
|
+
getDistinctCoordinates,
|
|
9
|
+
getRailOrientation,
|
|
10
|
+
RAIL_ALIGNMENT_EPSILON,
|
|
11
|
+
} from "./geometry"
|
|
12
|
+
import type { RailOrientation, RailSegment } from "./types"
|
|
13
|
+
|
|
14
|
+
const getAlongBounds = (
|
|
15
|
+
orientation: RailOrientation,
|
|
16
|
+
minAlong: number,
|
|
17
|
+
maxAlong: number,
|
|
18
|
+
): Bounds =>
|
|
19
|
+
orientation === "vertical"
|
|
20
|
+
? { minX: 0, maxX: 0, minY: minAlong, maxY: maxAlong }
|
|
21
|
+
: { minX: minAlong, maxX: maxAlong, minY: 0, maxY: 0 }
|
|
22
|
+
|
|
23
|
+
const hasPositiveAlongOverlap = (
|
|
24
|
+
orientation: RailOrientation,
|
|
25
|
+
first: Bounds,
|
|
26
|
+
second: Bounds,
|
|
27
|
+
) => {
|
|
28
|
+
const overlap = boundsIntersection(first, second)
|
|
29
|
+
if (!overlap) return false
|
|
30
|
+
const overlapLength =
|
|
31
|
+
orientation === "vertical"
|
|
32
|
+
? overlap.maxY - overlap.minY
|
|
33
|
+
: overlap.maxX - overlap.minX
|
|
34
|
+
return overlapLength > RAIL_ALIGNMENT_EPSILON
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Derives alternatives only for original alignment coordinates whose rendered
|
|
39
|
+
* stroke touches a parallel segment from another net.
|
|
40
|
+
*/
|
|
41
|
+
export const getRailAlignmentFallbackCoordinates = ({
|
|
42
|
+
group,
|
|
43
|
+
originalCoordinates,
|
|
44
|
+
otherNetTraces,
|
|
45
|
+
}: {
|
|
46
|
+
group: RailSegment[]
|
|
47
|
+
originalCoordinates: number[]
|
|
48
|
+
otherNetTraces: SolvedTracePath[]
|
|
49
|
+
}) => {
|
|
50
|
+
const orientation = group[0]!.orientation
|
|
51
|
+
const groupAlongBounds = group.map((segment) =>
|
|
52
|
+
getAlongBounds(orientation, segment.minAlong, segment.maxAlong),
|
|
53
|
+
)
|
|
54
|
+
const fallbackClearance = SCHEMATIC_TRACE_MIN_VISUAL_CENTERLINE_CLEARANCE
|
|
55
|
+
const coordinates: number[] = []
|
|
56
|
+
|
|
57
|
+
for (const trace of otherNetTraces) {
|
|
58
|
+
for (let index = 0; index < trace.tracePath.length - 1; index++) {
|
|
59
|
+
const start = trace.tracePath[index]!
|
|
60
|
+
const end = trace.tracePath[index + 1]!
|
|
61
|
+
if (getRailOrientation(start, end) !== orientation) continue
|
|
62
|
+
|
|
63
|
+
const minAlong =
|
|
64
|
+
orientation === "vertical"
|
|
65
|
+
? Math.min(start.y, end.y)
|
|
66
|
+
: Math.min(start.x, end.x)
|
|
67
|
+
const maxAlong =
|
|
68
|
+
orientation === "vertical"
|
|
69
|
+
? Math.max(start.y, end.y)
|
|
70
|
+
: Math.max(start.x, end.x)
|
|
71
|
+
const traceAlongBounds = getAlongBounds(orientation, minAlong, maxAlong)
|
|
72
|
+
if (
|
|
73
|
+
!groupAlongBounds.some((bounds) =>
|
|
74
|
+
hasPositiveAlongOverlap(orientation, bounds, traceAlongBounds),
|
|
75
|
+
)
|
|
76
|
+
) {
|
|
77
|
+
continue
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const coordinate = orientation === "vertical" ? start.x : start.y
|
|
81
|
+
const touchesOriginalCandidate = originalCoordinates.some(
|
|
82
|
+
(originalCoordinate) =>
|
|
83
|
+
Math.abs(originalCoordinate - coordinate) <=
|
|
84
|
+
SCHEMATIC_TRACE_MIN_CENTERLINE_CLEARANCE,
|
|
85
|
+
)
|
|
86
|
+
if (!touchesOriginalCandidate) continue
|
|
87
|
+
|
|
88
|
+
coordinates.push(
|
|
89
|
+
coordinate - fallbackClearance,
|
|
90
|
+
coordinate + fallbackClearance,
|
|
91
|
+
)
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return getDistinctCoordinates(coordinates)
|
|
96
|
+
}
|
|
@@ -1,7 +1,18 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { boundsIntersection, type Point } from "@tscircuit/math-utils"
|
|
2
2
|
import type { SolvedTracePath } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceLinesSolver"
|
|
3
3
|
|
|
4
4
|
const COINCIDENT_EPS = 2e-3
|
|
5
|
+
const GEOMETRY_EPS = 1e-6
|
|
6
|
+
|
|
7
|
+
// Schematic traces are rendered 0.02 schematic units wide.
|
|
8
|
+
export const SCHEMATIC_TRACE_STROKE_WIDTH = 0.02
|
|
9
|
+
export const SCHEMATIC_TRACE_MIN_CENTERLINE_CLEARANCE =
|
|
10
|
+
SCHEMATIC_TRACE_STROKE_WIDTH + GEOMETRY_EPS
|
|
11
|
+
// Rail-alignment fallbacks need a visible gap, not only non-overlapping trace
|
|
12
|
+
// bodies. Three stroke widths of centerline separation leave two stroke widths
|
|
13
|
+
// of whitespace between the rendered traces.
|
|
14
|
+
export const SCHEMATIC_TRACE_MIN_VISUAL_CENTERLINE_CLEARANCE =
|
|
15
|
+
SCHEMATIC_TRACE_STROKE_WIDTH * 3
|
|
5
16
|
|
|
6
17
|
/**
|
|
7
18
|
* Returns true when an orthogonal path shares a positive-length segment with
|
|
@@ -59,3 +70,62 @@ export const doesPathCoincideWithTraces = (
|
|
|
59
70
|
|
|
60
71
|
return false
|
|
61
72
|
}
|
|
73
|
+
|
|
74
|
+
/** Returns true when the rendered strokes of parallel trace runs overlap. */
|
|
75
|
+
export const doesPathOverlapTraceStrokes = (
|
|
76
|
+
path: Point[],
|
|
77
|
+
traces: SolvedTracePath[],
|
|
78
|
+
): boolean => {
|
|
79
|
+
const traceStrokeRadius = SCHEMATIC_TRACE_MIN_CENTERLINE_CLEARANCE / 2
|
|
80
|
+
const getStrokeBounds = (start: Point, end: Point, isVertical: boolean) =>
|
|
81
|
+
isVertical
|
|
82
|
+
? {
|
|
83
|
+
minX: start.x - traceStrokeRadius,
|
|
84
|
+
maxX: start.x + traceStrokeRadius,
|
|
85
|
+
minY: Math.min(start.y, end.y),
|
|
86
|
+
maxY: Math.max(start.y, end.y),
|
|
87
|
+
}
|
|
88
|
+
: {
|
|
89
|
+
minX: Math.min(start.x, end.x),
|
|
90
|
+
maxX: Math.max(start.x, end.x),
|
|
91
|
+
minY: start.y - traceStrokeRadius,
|
|
92
|
+
maxY: start.y + traceStrokeRadius,
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
for (let pathIndex = 0; pathIndex < path.length - 1; pathIndex++) {
|
|
96
|
+
const pathStart = path[pathIndex]!
|
|
97
|
+
const pathEnd = path[pathIndex + 1]!
|
|
98
|
+
const isVertical = Math.abs(pathStart.x - pathEnd.x) < COINCIDENT_EPS
|
|
99
|
+
const isHorizontal = Math.abs(pathStart.y - pathEnd.y) < COINCIDENT_EPS
|
|
100
|
+
if (!isVertical && !isHorizontal) continue
|
|
101
|
+
|
|
102
|
+
const pathBounds = getStrokeBounds(pathStart, pathEnd, isVertical)
|
|
103
|
+
for (const trace of traces) {
|
|
104
|
+
for (
|
|
105
|
+
let traceIndex = 0;
|
|
106
|
+
traceIndex < trace.tracePath.length - 1;
|
|
107
|
+
traceIndex++
|
|
108
|
+
) {
|
|
109
|
+
const traceStart = trace.tracePath[traceIndex]!
|
|
110
|
+
const traceEnd = trace.tracePath[traceIndex + 1]!
|
|
111
|
+
const isParallel = isVertical
|
|
112
|
+
? Math.abs(traceStart.x - traceEnd.x) < COINCIDENT_EPS
|
|
113
|
+
: Math.abs(traceStart.y - traceEnd.y) < COINCIDENT_EPS
|
|
114
|
+
if (!isParallel) continue
|
|
115
|
+
|
|
116
|
+
const overlap = boundsIntersection(
|
|
117
|
+
pathBounds,
|
|
118
|
+
getStrokeBounds(traceStart, traceEnd, isVertical),
|
|
119
|
+
)
|
|
120
|
+
if (!overlap) continue
|
|
121
|
+
|
|
122
|
+
const overlapLength = isVertical
|
|
123
|
+
? overlap.maxY - overlap.minY
|
|
124
|
+
: overlap.maxX - overlap.minX
|
|
125
|
+
if (overlapLength > COINCIDENT_EPS) return true
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return false
|
|
131
|
+
}
|