@tscircuit/schematic-trace-solver 0.0.154 → 0.0.155
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 +1 -1
- package/dist/index.js +297 -110
- package/lib/solvers/AvailableNetOrientationSolver/AvailableNetOrientationSolver.ts +60 -16
- package/lib/solvers/TraceCleanupSolver/alignSameNetRails.ts +17 -0
- package/lib/solvers/TraceCleanupSolver/sameNetRailAlignment/evaluateRailGroup.ts +32 -4
- package/lib/solvers/TraceCleanupSolver/sameNetRailAlignment/getComponentSideRailSegments.ts +28 -5
- package/lib/solvers/TraceCleanupSolver/sameNetRailAlignment/getFixedLabelCoordinate.ts +114 -0
- package/lib/solvers/TraceCleanupSolver/sameNetRailAlignment/getRailGroups.ts +98 -32
- package/lib/solvers/TraceCleanupSolver/sameNetRailAlignment/moveRailSegments.ts +2 -0
- package/package.json +1 -1
- package/tests/bug-reports/bug-report-20260707T134549Z/__snapshots__/bug-report-20260707T134549Z.snap.svg +2 -2
- package/tests/bug-reports/bug-report-20260730T061837Z/__snapshots__/bug-report-20260730T061837Z.snap.svg +2 -2
- package/tests/bug-reports/bug-report-20260825T103621Z/__snapshots__/bug-report-20260825T103621Z.snap.svg +2 -2
- package/tests/examples/__snapshots__/example51.snap.svg +2 -2
- package/tests/repros/__snapshots__/repro-ti-power-output-section.snap.svg +45 -45
|
@@ -653,10 +653,8 @@ export class AvailableNetOrientationSolver extends BaseSolver {
|
|
|
653
653
|
labelIndex: number,
|
|
654
654
|
) {
|
|
655
655
|
const direction = dir(orientation)
|
|
656
|
-
const
|
|
657
|
-
|
|
658
|
-
orientation,
|
|
659
|
-
).sort((a, b) => {
|
|
656
|
+
const { points } = this.getTraceAnchorCandidates(label, orientation)
|
|
657
|
+
const candidatePoints = points.sort((a, b) => {
|
|
660
658
|
const aAlongDirection = a.x * direction.x + a.y * direction.y
|
|
661
659
|
const bAlongDirection = b.x * direction.x + b.y * direction.y
|
|
662
660
|
return bAlongDirection - aAlongDirection
|
|
@@ -687,20 +685,29 @@ export class AvailableNetOrientationSolver extends BaseSolver {
|
|
|
687
685
|
labelIndex: number,
|
|
688
686
|
) {
|
|
689
687
|
const direction = dir(orientation)
|
|
690
|
-
const
|
|
691
|
-
|
|
692
|
-
orientation
|
|
693
|
-
|
|
688
|
+
const preservedColumnAnchor = this.getSearchStartAnchor(label, orientation)
|
|
689
|
+
const { points, preferNearestBendOnOutwardRow } =
|
|
690
|
+
this.getTraceAnchorCandidates(label, orientation)
|
|
691
|
+
const candidatePoints = points.sort((a, b) => {
|
|
694
692
|
const aAlongDirection = a.x * direction.x + a.y * direction.y
|
|
695
693
|
const bAlongDirection = b.x * direction.x + b.y * direction.y
|
|
696
|
-
|
|
694
|
+
if (!preferNearestBendOnOutwardRow) {
|
|
695
|
+
return bAlongDirection - aAlongDirection
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
const aPerpendicularDistance = isYOrientation(orientation)
|
|
699
|
+
? Math.abs(a.x - preservedColumnAnchor.x)
|
|
700
|
+
: Math.abs(a.y - preservedColumnAnchor.y)
|
|
701
|
+
const bPerpendicularDistance = isYOrientation(orientation)
|
|
702
|
+
? Math.abs(b.x - preservedColumnAnchor.x)
|
|
703
|
+
: Math.abs(b.y - preservedColumnAnchor.y)
|
|
704
|
+
return (
|
|
705
|
+
bAlongDirection - aAlongDirection ||
|
|
706
|
+
aPerpendicularDistance - bPerpendicularDistance
|
|
707
|
+
)
|
|
697
708
|
})
|
|
698
709
|
|
|
699
710
|
for (const connectorSource of candidatePoints) {
|
|
700
|
-
const preservedColumnAnchor = this.getSearchStartAnchor(
|
|
701
|
-
label,
|
|
702
|
-
orientation,
|
|
703
|
-
)
|
|
704
711
|
const preservedColumnCandidate = this.createCandidate(
|
|
705
712
|
label,
|
|
706
713
|
{
|
|
@@ -762,13 +769,14 @@ export class AvailableNetOrientationSolver extends BaseSolver {
|
|
|
762
769
|
return null
|
|
763
770
|
}
|
|
764
771
|
|
|
765
|
-
private
|
|
772
|
+
private getTraceAnchorCandidates(
|
|
766
773
|
label: NetLabelPlacement,
|
|
767
774
|
orientation: FacingDirection,
|
|
768
775
|
) {
|
|
769
776
|
const seen = new Set<string>()
|
|
770
777
|
const points: Point[] = []
|
|
771
|
-
const
|
|
778
|
+
const directHostTraceIds = new Set(label.mspConnectionPairIds ?? [])
|
|
779
|
+
const connectedTraceIds = new Set(directHostTraceIds)
|
|
772
780
|
|
|
773
781
|
if (isYOrientation(orientation)) {
|
|
774
782
|
const connectedPinIds = new Set<string>()
|
|
@@ -818,7 +826,43 @@ export class AvailableNetOrientationSolver extends BaseSolver {
|
|
|
818
826
|
}
|
|
819
827
|
}
|
|
820
828
|
|
|
821
|
-
|
|
829
|
+
const direction = dir(orientation)
|
|
830
|
+
const furthestAlong = Math.max(
|
|
831
|
+
...points.map((point) => point.x * direction.x + point.y * direction.y),
|
|
832
|
+
)
|
|
833
|
+
const furthestRowPoints = points.filter(
|
|
834
|
+
(point) =>
|
|
835
|
+
Math.abs(
|
|
836
|
+
point.x * direction.x + point.y * direction.y - furthestAlong,
|
|
837
|
+
) <= EPS,
|
|
838
|
+
)
|
|
839
|
+
const directHostPinIds = new Set(
|
|
840
|
+
[...directHostTraceIds].flatMap(
|
|
841
|
+
(traceId) => this.traceMap[traceId]?.pinIds ?? [],
|
|
842
|
+
),
|
|
843
|
+
)
|
|
844
|
+
const adjacentTraces = [...connectedTraceIds].flatMap((traceId) => {
|
|
845
|
+
if (directHostTraceIds.has(traceId)) return []
|
|
846
|
+
const trace = this.traceMap[traceId]
|
|
847
|
+
if (!trace?.pinIds.some((pinId) => directHostPinIds.has(pinId))) return []
|
|
848
|
+
return [trace]
|
|
849
|
+
})
|
|
850
|
+
|
|
851
|
+
return {
|
|
852
|
+
points,
|
|
853
|
+
// Prefer the nearest bend when the outward row is on a trace directly
|
|
854
|
+
// adjacent to the label's host. Deeper transitive chains keep their
|
|
855
|
+
// stable trace ordering.
|
|
856
|
+
preferNearestBendOnOutwardRow: adjacentTraces.some((trace) =>
|
|
857
|
+
furthestRowPoints.some((point) =>
|
|
858
|
+
trace.tracePath.some(
|
|
859
|
+
(tracePoint) =>
|
|
860
|
+
Math.abs(tracePoint.x - point.x) <= EPS &&
|
|
861
|
+
Math.abs(tracePoint.y - point.y) <= EPS,
|
|
862
|
+
),
|
|
863
|
+
),
|
|
864
|
+
),
|
|
865
|
+
}
|
|
822
866
|
}
|
|
823
867
|
|
|
824
868
|
private sharesVerticalRailWithAny(
|
|
@@ -13,6 +13,14 @@ interface AlignSameNetRailsInput {
|
|
|
13
13
|
eligibleTraceIds: ReadonlySet<string>
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
+
const getTraceStateKey = (traces: SolvedTracePath[]) =>
|
|
17
|
+
JSON.stringify(
|
|
18
|
+
traces.map((trace) => ({
|
|
19
|
+
mspPairId: trace.mspPairId,
|
|
20
|
+
tracePath: trace.tracePath,
|
|
21
|
+
})),
|
|
22
|
+
)
|
|
23
|
+
|
|
16
24
|
export const alignSameNetRails = ({
|
|
17
25
|
inputProblem,
|
|
18
26
|
traces,
|
|
@@ -24,6 +32,7 @@ export const alignSameNetRails = ({
|
|
|
24
32
|
alignedTraceCount: number
|
|
25
33
|
} => {
|
|
26
34
|
let outputTraces = [...traces]
|
|
35
|
+
const seenTraceStates = new Set([getTraceStateKey(outputTraces)])
|
|
27
36
|
const obstacles = getObstacleRects(inputProblem)
|
|
28
37
|
const alignedTraceIds = new Set<string>()
|
|
29
38
|
let alignedRailGroupCount = 0
|
|
@@ -38,6 +47,7 @@ export const alignSameNetRails = ({
|
|
|
38
47
|
eligibleTraceIds,
|
|
39
48
|
inputProblem,
|
|
40
49
|
obstacles,
|
|
50
|
+
netLabelPlacements,
|
|
41
51
|
)
|
|
42
52
|
let applied: AlignmentCandidate | null = null
|
|
43
53
|
|
|
@@ -53,9 +63,16 @@ export const alignSameNetRails = ({
|
|
|
53
63
|
}
|
|
54
64
|
if (!applied) break
|
|
55
65
|
|
|
66
|
+
const candidateStateKey = getTraceStateKey(applied.traces)
|
|
67
|
+
// A rail can be eligible from both endpoint components. When that creates
|
|
68
|
+
// a cycle, restore the first-seen state instead of making the final layout
|
|
69
|
+
// depend on the maximum-pass parity.
|
|
70
|
+
const repeatsSeenState = seenTraceStates.has(candidateStateKey)
|
|
71
|
+
if (!repeatsSeenState) seenTraceStates.add(candidateStateKey)
|
|
56
72
|
outputTraces = applied.traces
|
|
57
73
|
alignedRailGroupCount++
|
|
58
74
|
for (const traceId of applied.changedTraceIds) alignedTraceIds.add(traceId)
|
|
75
|
+
if (repeatsSeenState) break
|
|
59
76
|
}
|
|
60
77
|
|
|
61
78
|
return {
|
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
} from "lib/utils/doesPathCoincideWithTraces"
|
|
10
10
|
import { getDistinctCoordinates, pointsEqual } from "./geometry"
|
|
11
11
|
import { getRailAlignmentFallbackCoordinates } from "./getRailAlignmentFallbackCoordinates"
|
|
12
|
+
import { getFixedLabelCoordinate } from "./getFixedLabelCoordinate"
|
|
12
13
|
import { moveRailSegments } from "./moveRailSegments"
|
|
13
14
|
import { preservesLabelAnchors } from "./preservesLabelAnchors"
|
|
14
15
|
import {
|
|
@@ -50,6 +51,11 @@ export const evaluateRailGroup = ({
|
|
|
50
51
|
const originalCoordinates = getDistinctCoordinates(
|
|
51
52
|
group.map((segment) => segment.coordinate),
|
|
52
53
|
)
|
|
54
|
+
const fixedLabelCoordinate = getFixedLabelCoordinate(
|
|
55
|
+
group,
|
|
56
|
+
netLabelPlacements,
|
|
57
|
+
traces,
|
|
58
|
+
)
|
|
53
59
|
const otherNetTraces = traces.filter(
|
|
54
60
|
(trace) => trace.globalConnNetId !== group[0]!.globalConnNetId,
|
|
55
61
|
)
|
|
@@ -59,7 +65,10 @@ export const evaluateRailGroup = ({
|
|
|
59
65
|
!eligibleTraceIds.has(trace.mspPairId),
|
|
60
66
|
)
|
|
61
67
|
|
|
62
|
-
const evaluateCoordinates = (
|
|
68
|
+
const evaluateCoordinates = (
|
|
69
|
+
coordinates: number[],
|
|
70
|
+
options?: { coordinateIsFixedByLabel?: boolean },
|
|
71
|
+
) => {
|
|
63
72
|
let best: AlignmentCandidate | null = null
|
|
64
73
|
for (const coordinate of coordinates) {
|
|
65
74
|
const candidateMap = new Map<string, SolvedTracePath>()
|
|
@@ -104,7 +113,15 @@ export const evaluateRailGroup = ({
|
|
|
104
113
|
allCandidateTraces,
|
|
105
114
|
)
|
|
106
115
|
if (metrics.otherNetCrossings > baseline.otherNetCrossings) continue
|
|
107
|
-
|
|
116
|
+
// A fixed label anchor determines the rail coordinate. It may lengthen
|
|
117
|
+
// endpoint legs, but it must still preserve turns and every safety gate.
|
|
118
|
+
if (
|
|
119
|
+
options?.coordinateIsFixedByLabel
|
|
120
|
+
? metrics.turnCount > baseline.turnCount
|
|
121
|
+
: !isReadabilityImprovement(metrics, baseline)
|
|
122
|
+
) {
|
|
123
|
+
continue
|
|
124
|
+
}
|
|
108
125
|
|
|
109
126
|
const score: AlignmentScore = {
|
|
110
127
|
...metrics,
|
|
@@ -124,16 +141,27 @@ export const evaluateRailGroup = ({
|
|
|
124
141
|
.map((trace) => trace.mspPairId)
|
|
125
142
|
if (changedTraceIds.length === 0) continue
|
|
126
143
|
|
|
127
|
-
const candidate = {
|
|
144
|
+
const candidate = {
|
|
145
|
+
traces: allCandidateTraces,
|
|
146
|
+
changedTraceIds,
|
|
147
|
+
score,
|
|
148
|
+
}
|
|
128
149
|
if (!best || scoreIsBetter(candidate.score, best.score)) best = candidate
|
|
129
150
|
}
|
|
130
151
|
|
|
131
152
|
return best
|
|
132
153
|
}
|
|
133
154
|
|
|
134
|
-
const originalCandidate = evaluateCoordinates(
|
|
155
|
+
const originalCandidate = evaluateCoordinates(
|
|
156
|
+
fixedLabelCoordinate === null
|
|
157
|
+
? originalCoordinates
|
|
158
|
+
: [fixedLabelCoordinate],
|
|
159
|
+
{ coordinateIsFixedByLabel: fixedLabelCoordinate !== null },
|
|
160
|
+
)
|
|
135
161
|
if (originalCandidate) return originalCandidate
|
|
136
162
|
|
|
163
|
+
if (fixedLabelCoordinate !== null) return null
|
|
164
|
+
|
|
137
165
|
return evaluateCoordinates(
|
|
138
166
|
getRailAlignmentFallbackCoordinates({
|
|
139
167
|
group,
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { distance } from "@tscircuit/math-utils"
|
|
2
|
+
import { DEFAULT_MAX_MSP_PAIR_DISTANCE } from "lib/solvers/MspConnectionPairSolver/MspConnectionPairSolver"
|
|
1
3
|
import type { SolvedTracePath } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceLinesSolver"
|
|
2
4
|
import { getPinDirection } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver/getPinDirection"
|
|
3
5
|
import type { InputChip } from "lib/types/InputProblem"
|
|
@@ -80,12 +82,22 @@ const railIsOutsideComponent = (
|
|
|
80
82
|
}
|
|
81
83
|
}
|
|
82
84
|
|
|
83
|
-
/**
|
|
85
|
+
/**
|
|
86
|
+
* Associates each movable rail with its nearest endpoint. Label-anchored group
|
|
87
|
+
* discovery may opt into equally-near endpoints for local connections.
|
|
88
|
+
*/
|
|
84
89
|
export const getComponentSideRailSegments = (
|
|
85
90
|
trace: SolvedTracePath,
|
|
86
91
|
chipMap: Map<string, InputChip>,
|
|
92
|
+
options?: {
|
|
93
|
+
includeTiedEndpointAssociations?: boolean
|
|
94
|
+
maxMspPairDistance?: number
|
|
95
|
+
},
|
|
87
96
|
): RailSegment[] => {
|
|
88
97
|
const segments: RailSegment[] = []
|
|
98
|
+
const isLocalConnection =
|
|
99
|
+
distance(trace.pins[0]!, trace.pins[1]!) <=
|
|
100
|
+
(options?.maxMspPairDistance ?? DEFAULT_MAX_MSP_PAIR_DISTANCE)
|
|
89
101
|
|
|
90
102
|
for (const segment of getMovableRailSegments(trace)) {
|
|
91
103
|
const associations = trace.pins.flatMap((pin, pinIndex) => {
|
|
@@ -111,10 +123,21 @@ export const getComponentSideRailSegments = (
|
|
|
111
123
|
})
|
|
112
124
|
|
|
113
125
|
associations.sort((a, b) => a.distanceFromEndpoint - b.distanceFromEndpoint)
|
|
114
|
-
const
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
126
|
+
const minimumDistance = associations[0]?.distanceFromEndpoint
|
|
127
|
+
const nearestAssociationKeys = new Set<string>()
|
|
128
|
+
for (const association of associations) {
|
|
129
|
+
if (
|
|
130
|
+
nearestAssociationKeys.size > 0 &&
|
|
131
|
+
(!options?.includeTiedEndpointAssociations || !isLocalConnection)
|
|
132
|
+
) {
|
|
133
|
+
break
|
|
134
|
+
}
|
|
135
|
+
if (association.distanceFromEndpoint !== minimumDistance) continue
|
|
136
|
+
const associationKey = `${association.componentId}:${association.componentFacingDirection}`
|
|
137
|
+
if (nearestAssociationKeys.has(associationKey)) continue
|
|
138
|
+
nearestAssociationKeys.add(associationKey)
|
|
139
|
+
segments.push({ ...segment, ...association })
|
|
140
|
+
}
|
|
118
141
|
}
|
|
119
142
|
|
|
120
143
|
return segments
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { distance } from "@tscircuit/math-utils"
|
|
2
|
+
import type { NetLabelPlacement } from "lib/solvers/NetLabelPlacementSolver/NetLabelPlacementSolver"
|
|
3
|
+
import { tracePathContainsPoint } from "lib/solvers/RailNetLabelCornerPlacementSolver/geometry"
|
|
4
|
+
import type { SolvedTracePath } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceLinesSolver"
|
|
5
|
+
import { getDistinctCoordinates, RAIL_ALIGNMENT_EPSILON } from "./geometry"
|
|
6
|
+
import type { RailSegment } from "./types"
|
|
7
|
+
|
|
8
|
+
const getTransitivelyConnectedTraceIds = (
|
|
9
|
+
group: RailSegment[],
|
|
10
|
+
traces: SolvedTracePath[],
|
|
11
|
+
) => {
|
|
12
|
+
const groupNetId = group[0]!.globalConnNetId
|
|
13
|
+
const sameNetTraces = traces.filter(
|
|
14
|
+
(trace) => trace.globalConnNetId === groupNetId,
|
|
15
|
+
)
|
|
16
|
+
const connectedTraceIds = new Set(group.map((segment) => segment.traceId))
|
|
17
|
+
const connectedPinIds = new Set(
|
|
18
|
+
sameNetTraces
|
|
19
|
+
.filter((trace) => connectedTraceIds.has(trace.mspPairId))
|
|
20
|
+
.flatMap((trace) => trace.pinIds),
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
for (let changed = true; changed; ) {
|
|
24
|
+
changed = false
|
|
25
|
+
for (const trace of sameNetTraces) {
|
|
26
|
+
if (connectedTraceIds.has(trace.mspPairId)) continue
|
|
27
|
+
if (!trace.pinIds.some((pinId) => connectedPinIds.has(pinId))) continue
|
|
28
|
+
|
|
29
|
+
connectedTraceIds.add(trace.mspPairId)
|
|
30
|
+
for (const pinId of trace.pinIds) connectedPinIds.add(pinId)
|
|
31
|
+
changed = true
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return connectedTraceIds
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const getLabelsLinkedToRailGroup = (
|
|
39
|
+
group: RailSegment[],
|
|
40
|
+
netLabelPlacements: NetLabelPlacement[],
|
|
41
|
+
traces: SolvedTracePath[],
|
|
42
|
+
) => {
|
|
43
|
+
const traceIds = getTransitivelyConnectedTraceIds(group, traces)
|
|
44
|
+
return netLabelPlacements.filter((label) =>
|
|
45
|
+
label.mspConnectionPairIds.some((traceId) => traceIds.has(traceId)),
|
|
46
|
+
)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export const getFixedLabelCoordinate = (
|
|
50
|
+
group: RailSegment[],
|
|
51
|
+
netLabelPlacements: NetLabelPlacement[],
|
|
52
|
+
traces: SolvedTracePath[],
|
|
53
|
+
) => {
|
|
54
|
+
const traceMap = new Map(traces.map((trace) => [trace.mspPairId, trace]))
|
|
55
|
+
const groupTraceIds = new Set(group.map((segment) => segment.traceId))
|
|
56
|
+
const groupHasOnlySingleRailTraces = [...groupTraceIds].every(
|
|
57
|
+
(traceId) => traceMap.get(traceId)?.tracePath.length === 4,
|
|
58
|
+
)
|
|
59
|
+
// A fixed label may lengthen the endpoint legs around one movable rail. A
|
|
60
|
+
// trace with more elbows still has to pass the normal readability scoring.
|
|
61
|
+
if (!groupHasOnlySingleRailTraces) return null
|
|
62
|
+
|
|
63
|
+
const orientation = group[0]!.orientation
|
|
64
|
+
const linkedLabels = getLabelsLinkedToRailGroup(
|
|
65
|
+
group,
|
|
66
|
+
netLabelPlacements,
|
|
67
|
+
traces,
|
|
68
|
+
)
|
|
69
|
+
const anchoringLabels = linkedLabels.filter(
|
|
70
|
+
(label) =>
|
|
71
|
+
label.orientation === group[0]!.componentFacingDirection &&
|
|
72
|
+
label.mspConnectionPairIds.some((traceId) => {
|
|
73
|
+
const trace = traceMap.get(traceId)
|
|
74
|
+
return (
|
|
75
|
+
trace && tracePathContainsPoint(trace.tracePath, label.anchorPoint)
|
|
76
|
+
)
|
|
77
|
+
}),
|
|
78
|
+
)
|
|
79
|
+
const coordinates = getDistinctCoordinates(
|
|
80
|
+
anchoringLabels.map((label) =>
|
|
81
|
+
orientation === "vertical" ? label.anchorPoint.x : label.anchorPoint.y,
|
|
82
|
+
),
|
|
83
|
+
)
|
|
84
|
+
if (coordinates.length !== 1) return null
|
|
85
|
+
|
|
86
|
+
const labelCoordinate = coordinates[0]!
|
|
87
|
+
const coordinate =
|
|
88
|
+
group.find(
|
|
89
|
+
(segment) =>
|
|
90
|
+
Math.abs(segment.coordinate - labelCoordinate) <=
|
|
91
|
+
RAIL_ALIGNMENT_EPSILON,
|
|
92
|
+
)?.coordinate ?? labelCoordinate
|
|
93
|
+
const labelIsAnchoredToRoutedBackbone = anchoringLabels.some((label) =>
|
|
94
|
+
label.mspConnectionPairIds.some(
|
|
95
|
+
(traceId) => (traceMap.get(traceId)?.tracePath.length ?? 0) > 4,
|
|
96
|
+
),
|
|
97
|
+
)
|
|
98
|
+
// A multi-turn label trace is an intentional routed backbone, so its rail
|
|
99
|
+
// coordinate may propagate through the local component chain. Labels riding
|
|
100
|
+
// a single movable rail remain subject to the per-connection locality gate.
|
|
101
|
+
if (labelIsAnchoredToRoutedBackbone) return coordinate
|
|
102
|
+
|
|
103
|
+
// Rail cleanup is local: a label cannot pull a connection farther than the
|
|
104
|
+
// span of the pins that connection already joins.
|
|
105
|
+
const coordinateIsLocalToEveryTrace = group.every((segment) => {
|
|
106
|
+
const trace = traceMap.get(segment.traceId)
|
|
107
|
+
if (!trace) return false
|
|
108
|
+
return (
|
|
109
|
+
Math.abs(segment.coordinate - coordinate) <=
|
|
110
|
+
distance(trace.pins[0]!, trace.pins[1]!) + RAIL_ALIGNMENT_EPSILON
|
|
111
|
+
)
|
|
112
|
+
})
|
|
113
|
+
return coordinateIsLocalToEveryTrace ? coordinate : null
|
|
114
|
+
}
|
|
@@ -3,7 +3,9 @@ import type { SolvedTracePath } from "lib/solvers/SchematicTraceLinesSolver/Sche
|
|
|
3
3
|
import { segmentIntersectsRect } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver2/collisions"
|
|
4
4
|
import type { ObstacleRect } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver2/rect"
|
|
5
5
|
import type { InputProblem } from "lib/types/InputProblem"
|
|
6
|
+
import type { NetLabelPlacement } from "lib/solvers/NetLabelPlacementSolver/NetLabelPlacementSolver"
|
|
6
7
|
import { getComponentSideRailSegments } from "./getComponentSideRailSegments"
|
|
8
|
+
import { getFixedLabelCoordinate } from "./getFixedLabelCoordinate"
|
|
7
9
|
import { nearlyEqual, rangesTouchOrOverlap } from "./geometry"
|
|
8
10
|
import type { RailSegment } from "./types"
|
|
9
11
|
|
|
@@ -66,49 +68,113 @@ export const getRailGroups = (
|
|
|
66
68
|
eligibleTraceIds: ReadonlySet<string>,
|
|
67
69
|
inputProblem: InputProblem,
|
|
68
70
|
obstacles: ObstacleRect[],
|
|
71
|
+
netLabelPlacements: NetLabelPlacement[],
|
|
69
72
|
): RailSegment[][] => {
|
|
70
73
|
const chipMap = new Map(inputProblem.chips.map((chip) => [chip.chipId, chip]))
|
|
71
|
-
const segments = traces
|
|
72
|
-
.filter((trace) => eligibleTraceIds.has(trace.mspPairId))
|
|
73
|
-
.flatMap((trace) => getComponentSideRailSegments(trace, chipMap))
|
|
74
74
|
const traceMap = new Map(traces.map((trace) => [trace.mspPairId, trace]))
|
|
75
|
-
const
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
const
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
const
|
|
88
|
-
group
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
) {
|
|
95
|
-
if (visited.has(candidateIndex)) continue
|
|
96
|
-
const candidate = segments[candidateIndex]!
|
|
97
|
-
if (!canJoinRailGroup(start, current, candidate, traceMap, obstacles)) {
|
|
98
|
-
continue
|
|
99
|
-
}
|
|
75
|
+
const eligibleTraces = traces.filter((trace) =>
|
|
76
|
+
eligibleTraceIds.has(trace.mspPairId),
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
const collectConnectedGroups = (segments: RailSegment[]) => {
|
|
80
|
+
const visited = new Set<number>()
|
|
81
|
+
const connectedGroups: RailSegment[][] = []
|
|
82
|
+
|
|
83
|
+
for (let startIndex = 0; startIndex < segments.length; startIndex++) {
|
|
84
|
+
if (visited.has(startIndex)) continue
|
|
85
|
+
|
|
86
|
+
const start = segments[startIndex]!
|
|
87
|
+
const queue = [startIndex]
|
|
88
|
+
const group: RailSegment[] = []
|
|
89
|
+
visited.add(startIndex)
|
|
90
|
+
|
|
91
|
+
for (let queueIndex = 0; queueIndex < queue.length; queueIndex++) {
|
|
92
|
+
const current = segments[queue[queueIndex]!]!
|
|
93
|
+
group.push(current)
|
|
100
94
|
|
|
101
|
-
|
|
102
|
-
|
|
95
|
+
for (
|
|
96
|
+
let candidateIndex = 0;
|
|
97
|
+
candidateIndex < segments.length;
|
|
98
|
+
candidateIndex++
|
|
99
|
+
) {
|
|
100
|
+
if (visited.has(candidateIndex)) continue
|
|
101
|
+
const candidate = segments[candidateIndex]!
|
|
102
|
+
if (
|
|
103
|
+
!canJoinRailGroup(start, current, candidate, traceMap, obstacles)
|
|
104
|
+
) {
|
|
105
|
+
continue
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
visited.add(candidateIndex)
|
|
109
|
+
queue.push(candidateIndex)
|
|
110
|
+
}
|
|
103
111
|
}
|
|
112
|
+
|
|
113
|
+
connectedGroups.push(group)
|
|
104
114
|
}
|
|
105
115
|
|
|
116
|
+
return connectedGroups
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const groupKey = (group: RailSegment[]) =>
|
|
120
|
+
[
|
|
121
|
+
group[0]!.componentId,
|
|
122
|
+
group[0]!.componentFacingDirection,
|
|
123
|
+
group[0]!.orientation,
|
|
124
|
+
...group
|
|
125
|
+
.map((segment) => `${segment.traceId}:${segment.segmentIndex}`)
|
|
126
|
+
.sort(),
|
|
127
|
+
].join("|")
|
|
128
|
+
|
|
129
|
+
const selectedGroups: RailSegment[][] = []
|
|
130
|
+
const selectedGroupKeys = new Set<string>()
|
|
131
|
+
const addEligibleGroup = (
|
|
132
|
+
group: RailSegment[],
|
|
133
|
+
options?: { requireFixedLabel?: boolean },
|
|
134
|
+
) => {
|
|
106
135
|
const traceCount = new Set(group.map((segment) => segment.traceId)).size
|
|
136
|
+
if (traceCount < 2) return
|
|
137
|
+
|
|
138
|
+
const fixedLabelCoordinate = getFixedLabelCoordinate(
|
|
139
|
+
group,
|
|
140
|
+
netLabelPlacements,
|
|
141
|
+
traces,
|
|
142
|
+
)
|
|
143
|
+
if (options?.requireFixedLabel && fixedLabelCoordinate === null) return
|
|
144
|
+
|
|
107
145
|
const hasDifferentCoordinates = group.some(
|
|
108
146
|
(segment) => !nearlyEqual(segment.coordinate, group[0]!.coordinate),
|
|
109
147
|
)
|
|
110
|
-
|
|
148
|
+
const hasDifferentFixedLabelCoordinate =
|
|
149
|
+
fixedLabelCoordinate !== null &&
|
|
150
|
+
!nearlyEqual(fixedLabelCoordinate, group[0]!.coordinate)
|
|
151
|
+
if (!hasDifferentCoordinates && !hasDifferentFixedLabelCoordinate) return
|
|
152
|
+
|
|
153
|
+
const key = groupKey(group)
|
|
154
|
+
if (selectedGroupKeys.has(key)) return
|
|
155
|
+
selectedGroupKeys.add(key)
|
|
156
|
+
selectedGroups.push(group)
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const primarySegments = eligibleTraces.flatMap((trace) =>
|
|
160
|
+
getComponentSideRailSegments(trace, chipMap),
|
|
161
|
+
)
|
|
162
|
+
// Preserve the original nearest-endpoint grouping and its ordering.
|
|
163
|
+
for (const group of collectConnectedGroups(primarySegments)) {
|
|
164
|
+
addEligibleGroup(group)
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Equal-distance endpoint associations can bridge a component chain, but
|
|
168
|
+
// only a fixed label is allowed to opt that broader group into alignment.
|
|
169
|
+
const tiedEndpointSegments = eligibleTraces.flatMap((trace) =>
|
|
170
|
+
getComponentSideRailSegments(trace, chipMap, {
|
|
171
|
+
includeTiedEndpointAssociations: true,
|
|
172
|
+
maxMspPairDistance: inputProblem.maxMspPairDistance,
|
|
173
|
+
}),
|
|
174
|
+
)
|
|
175
|
+
for (const group of collectConnectedGroups(tiedEndpointSegments)) {
|
|
176
|
+
addEligibleGroup(group, { requireFixedLabel: true })
|
|
111
177
|
}
|
|
112
178
|
|
|
113
|
-
return
|
|
179
|
+
return selectedGroups
|
|
114
180
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { SolvedTracePath } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceLinesSolver"
|
|
2
2
|
import { simplifyPath } from "../simplifyPath"
|
|
3
|
+
import { nearlyEqual } from "./geometry"
|
|
3
4
|
import type { RailSegment } from "./types"
|
|
4
5
|
|
|
5
6
|
export const moveRailSegments = (
|
|
@@ -9,6 +10,7 @@ export const moveRailSegments = (
|
|
|
9
10
|
): SolvedTracePath => {
|
|
10
11
|
const pointsToMove = new Set<number>()
|
|
11
12
|
for (const segment of segments) {
|
|
13
|
+
if (nearlyEqual(segment.coordinate, coordinate)) continue
|
|
12
14
|
pointsToMove.add(segment.segmentIndex)
|
|
13
15
|
pointsToMove.add(segment.segmentIndex + 1)
|
|
14
16
|
}
|