@tscircuit/schematic-trace-solver 0.0.125 → 0.0.127
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/README.md +28 -0
- package/dist/index.d.ts +159 -2
- package/dist/index.js +739 -36
- package/lib/index.ts +2 -0
- package/lib/solvers/InlineNetLabelSolver/InlineNetLabelSolver.ts +424 -0
- package/lib/solvers/InlineNetLabelSolver/getAxisAlignedSegments.ts +73 -0
- package/lib/solvers/NetLabelTraceCollisionSolver/NetLabelTraceCollisionSolver.ts +15 -0
- package/lib/solvers/SchematicTracePipelineSolver/SchematicTracePipelineSolver.ts +86 -11
- package/lib/solvers/TraceElbowTransitionSimplificationSolver/TraceElbowTransitionSimplificationSolver.ts +216 -0
- package/lib/solvers/TraceElbowTransitionSimplificationSolver/generateElbowTransitionSimplificationCandidates.ts +186 -0
- package/lib/solvers/TraceElbowTransitionSimplificationSolver/types.ts +10 -0
- package/lib/solvers/TraceLabelOverlapAvoidanceSolver/TraceLabelOverlapAvoidanceSolver.ts +3 -0
- package/lib/solvers/TraceLabelOverlapAvoidanceSolver/sub-solvers/OverlapAvoidanceStepSolver/OverlapAvoidanceStepSolver.ts +16 -0
- package/lib/solvers/TraceLabelOverlapAvoidanceSolver/sub-solvers/SingleOverlapSolver/SingleOverlapSolver.ts +56 -13
- package/lib/types/InputProblem.ts +24 -0
- package/package.json +1 -1
- package/site/bug-reports/bug-report-20260806T093501Z.page.tsx +4 -0
- package/site/examples/inline-net-label01.page.tsx +6 -0
- package/tests/assets/inline-net-label01.json +47 -0
- package/tests/bug-reports/bug-report-20260706T213649Z/__snapshots__/bug-report-20260706T213649Z.snap.svg +3 -3
- package/tests/bug-reports/bug-report-20260706T220324Z/__snapshots__/bug-report-20260706T220324Z.snap.svg +6 -6
- package/tests/bug-reports/bug-report-20260806T093501Z/__snapshots__/bug-report-20260806T093501Z.snap.svg +135 -0
- package/tests/bug-reports/bug-report-20260806T093501Z/bug-report-20260806T093501Z.json +505 -0
- package/tests/bug-reports/bug-report-20260806T093501Z/bug-report-20260806T093501Z.test.ts +66 -0
- package/tests/examples/__snapshots__/example33.snap.svg +1 -1
- package/tests/functions/getAxisAlignedSegments.test.ts +56 -0
- package/tests/solvers/InlineNetLabelSolver/__snapshots__/inline-net-label01.snap.svg +54 -0
- package/tests/solvers/InlineNetLabelSolver/inline-net-label01.test.ts +44 -0
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
import type { Point } from "@tscircuit/math-utils"
|
|
2
|
+
import type { GraphicsObject } from "graphics-debug"
|
|
3
|
+
import { BaseSolver } from "lib/solvers/BaseSolver/BaseSolver"
|
|
4
|
+
import type { NetLabelPlacement } from "lib/solvers/NetLabelPlacementSolver/NetLabelPlacementSolver"
|
|
5
|
+
import type { SolvedTracePath } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceLinesSolver"
|
|
6
|
+
import { isPathCollidingWithObstacles } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver2/collisions"
|
|
7
|
+
import { getObstacleRects } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver2/rect"
|
|
8
|
+
import { visualizeInputProblem } from "lib/solvers/SchematicTracePipelineSolver/visualizeInputProblem"
|
|
9
|
+
import { preservesLabelAnchors } from "lib/solvers/TraceCleanupSolver/sameNetRailAlignment/preservesLabelAnchors"
|
|
10
|
+
import { simplifyPath } from "lib/solvers/TraceCleanupSolver/simplifyPath"
|
|
11
|
+
import { detectTraceLabelOverlap } from "lib/solvers/TraceLabelOverlapAvoidanceSolver/detectTraceLabelOverlap"
|
|
12
|
+
import type { InputProblem } from "lib/types/InputProblem"
|
|
13
|
+
import { doesPathCoincideWithTraces } from "lib/utils/doesPathCoincideWithTraces"
|
|
14
|
+
import type { CompletedTraceReroute } from "./types"
|
|
15
|
+
import { generateElbowTransitionSimplificationCandidates } from "./generateElbowTransitionSimplificationCandidates"
|
|
16
|
+
|
|
17
|
+
interface TraceElbowTransitionSimplificationSolverInput {
|
|
18
|
+
inputProblem: InputProblem
|
|
19
|
+
traces: SolvedTracePath[]
|
|
20
|
+
completedReroutes: CompletedTraceReroute[]
|
|
21
|
+
netLabelPlacements: NetLabelPlacement[]
|
|
22
|
+
paddingBuffer: number
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const PATH_LENGTH_EPSILON = 1e-9
|
|
26
|
+
|
|
27
|
+
const getPathLength = (points: Point[]) =>
|
|
28
|
+
points.slice(1).reduce((length, point, pointIndex) => {
|
|
29
|
+
const previousPoint = points[pointIndex]!
|
|
30
|
+
return (
|
|
31
|
+
length +
|
|
32
|
+
Math.abs(point.x - previousPoint.x) +
|
|
33
|
+
Math.abs(point.y - previousPoint.y)
|
|
34
|
+
)
|
|
35
|
+
}, 0)
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Post-processes traces after trace/label overlap avoidance. It removes
|
|
39
|
+
* redundant elbow transitions and, when needed, shifts a simplified elbow
|
|
40
|
+
* around a rendered label. Every replacement is revalidated against labels,
|
|
41
|
+
* components, other-net traces, and label anchors.
|
|
42
|
+
*/
|
|
43
|
+
export class TraceElbowTransitionSimplificationSolver extends BaseSolver {
|
|
44
|
+
private input: TraceElbowTransitionSimplificationSolverInput
|
|
45
|
+
private outputTraces: SolvedTracePath[]
|
|
46
|
+
private traceIdQueue: string[]
|
|
47
|
+
private obstacles: ReturnType<typeof getObstacleRects>
|
|
48
|
+
|
|
49
|
+
constructor(input: TraceElbowTransitionSimplificationSolverInput) {
|
|
50
|
+
super()
|
|
51
|
+
this.input = input
|
|
52
|
+
this.outputTraces = [...input.traces]
|
|
53
|
+
const reroutedTraceIds = new Set(
|
|
54
|
+
input.completedReroutes.map(
|
|
55
|
+
(completedReroute) => completedReroute.initialTrace.mspPairId,
|
|
56
|
+
),
|
|
57
|
+
)
|
|
58
|
+
this.traceIdQueue = input.traces
|
|
59
|
+
.map((trace) => trace.mspPairId)
|
|
60
|
+
.filter((traceId) => reroutedTraceIds.has(traceId))
|
|
61
|
+
this.obstacles = getObstacleRects(input.inputProblem)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
override _step() {
|
|
65
|
+
const traceId = this.traceIdQueue.shift()
|
|
66
|
+
if (!traceId) {
|
|
67
|
+
this.solved = true
|
|
68
|
+
return
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const traceIndex = this.outputTraces.findIndex(
|
|
72
|
+
(trace) => trace.mspPairId === traceId,
|
|
73
|
+
)
|
|
74
|
+
const trace = this.outputTraces[traceIndex]!
|
|
75
|
+
const tracePath = simplifyPath(trace.tracePath)
|
|
76
|
+
const initialOverlaps = detectTraceLabelOverlap({
|
|
77
|
+
traces: [{ ...trace, tracePath }],
|
|
78
|
+
netLabels: this.input.netLabelPlacements,
|
|
79
|
+
})
|
|
80
|
+
const otherNetTraces = this.outputTraces.filter(
|
|
81
|
+
(otherTrace) =>
|
|
82
|
+
otherTrace.mspPairId !== trace.mspPairId &&
|
|
83
|
+
otherTrace.globalConnNetId !== trace.globalConnNetId,
|
|
84
|
+
)
|
|
85
|
+
const candidateByPath = new Map<string, Point[]>()
|
|
86
|
+
const completedReroutes = this.input.completedReroutes.filter(
|
|
87
|
+
(completedReroute) =>
|
|
88
|
+
completedReroute.initialTrace.mspPairId === trace.mspPairId,
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
for (const completedReroute of completedReroutes) {
|
|
92
|
+
const initialReroutePath = simplifyPath(
|
|
93
|
+
completedReroute.initialTrace.tracePath,
|
|
94
|
+
)
|
|
95
|
+
const reroutedPath = simplifyPath(completedReroute.reroutedTracePath)
|
|
96
|
+
const initialRerouteOverlapCount = detectTraceLabelOverlap({
|
|
97
|
+
traces: [
|
|
98
|
+
{ ...completedReroute.initialTrace, tracePath: initialReroutePath },
|
|
99
|
+
],
|
|
100
|
+
netLabels: this.input.netLabelPlacements,
|
|
101
|
+
}).length
|
|
102
|
+
|
|
103
|
+
const candidates = generateElbowTransitionSimplificationCandidates({
|
|
104
|
+
trace: completedReroute.initialTrace,
|
|
105
|
+
label: completedReroute.label,
|
|
106
|
+
netLabelPlacements: this.input.netLabelPlacements,
|
|
107
|
+
paddingBuffer: this.input.paddingBuffer,
|
|
108
|
+
detourCount: completedReroute.detourCount,
|
|
109
|
+
})
|
|
110
|
+
for (const candidate of candidates) {
|
|
111
|
+
const simplifiedCandidate = simplifyPath(candidate)
|
|
112
|
+
const candidateTrace = {
|
|
113
|
+
...completedReroute.initialTrace,
|
|
114
|
+
tracePath: simplifiedCandidate,
|
|
115
|
+
}
|
|
116
|
+
const candidateOverlapCount = detectTraceLabelOverlap({
|
|
117
|
+
traces: [candidateTrace],
|
|
118
|
+
netLabels: this.input.netLabelPlacements,
|
|
119
|
+
}).length
|
|
120
|
+
const isSimplerEquivalentReroute =
|
|
121
|
+
candidateOverlapCount < initialRerouteOverlapCount &&
|
|
122
|
+
Math.abs(
|
|
123
|
+
getPathLength(simplifiedCandidate) - getPathLength(reroutedPath),
|
|
124
|
+
) < PATH_LENGTH_EPSILON &&
|
|
125
|
+
simplifiedCandidate.length < reroutedPath.length &&
|
|
126
|
+
preservesLabelAnchors(
|
|
127
|
+
this.input.netLabelPlacements,
|
|
128
|
+
[completedReroute.initialTrace],
|
|
129
|
+
[candidateTrace],
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
if (isSimplerEquivalentReroute) {
|
|
133
|
+
candidateByPath.set(
|
|
134
|
+
simplifiedCandidate
|
|
135
|
+
.map((point) => `${point.x},${point.y}`)
|
|
136
|
+
.join(";"),
|
|
137
|
+
simplifiedCandidate,
|
|
138
|
+
)
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const initialOverlapIds = new Set(
|
|
144
|
+
initialOverlaps.map(
|
|
145
|
+
({ label }) => `${label.globalConnNetId}:${label.netId}`,
|
|
146
|
+
),
|
|
147
|
+
)
|
|
148
|
+
const initialPathLength = getPathLength(tracePath)
|
|
149
|
+
|
|
150
|
+
const validCandidates = [...candidateByPath.values()].filter(
|
|
151
|
+
(candidatePath) => {
|
|
152
|
+
const candidateTrace = { ...trace, tracePath: candidatePath }
|
|
153
|
+
const candidateOverlaps = detectTraceLabelOverlap({
|
|
154
|
+
traces: [candidateTrace],
|
|
155
|
+
netLabels: this.input.netLabelPlacements,
|
|
156
|
+
})
|
|
157
|
+
const candidateOnlyKeepsExistingOverlaps = candidateOverlaps.every(
|
|
158
|
+
({ label }) =>
|
|
159
|
+
initialOverlapIds.has(`${label.globalConnNetId}:${label.netId}`),
|
|
160
|
+
)
|
|
161
|
+
const reducesCollisions =
|
|
162
|
+
candidateOverlaps.length < initialOverlaps.length
|
|
163
|
+
const simplifiesGeometry =
|
|
164
|
+
candidateOverlaps.length === initialOverlaps.length &&
|
|
165
|
+
getPathLength(candidatePath) <=
|
|
166
|
+
initialPathLength + PATH_LENGTH_EPSILON &&
|
|
167
|
+
candidatePath.length < tracePath.length
|
|
168
|
+
|
|
169
|
+
return (
|
|
170
|
+
candidateOnlyKeepsExistingOverlaps &&
|
|
171
|
+
(reducesCollisions || simplifiesGeometry) &&
|
|
172
|
+
preservesLabelAnchors(
|
|
173
|
+
this.input.netLabelPlacements,
|
|
174
|
+
[trace],
|
|
175
|
+
[candidateTrace],
|
|
176
|
+
) &&
|
|
177
|
+
!isPathCollidingWithObstacles(candidatePath, this.obstacles) &&
|
|
178
|
+
!doesPathCoincideWithTraces(candidatePath, otherNetTraces)
|
|
179
|
+
)
|
|
180
|
+
},
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
const getOverlapCount = (candidatePath: Point[]) =>
|
|
184
|
+
detectTraceLabelOverlap({
|
|
185
|
+
traces: [{ ...trace, tracePath: candidatePath }],
|
|
186
|
+
netLabels: this.input.netLabelPlacements,
|
|
187
|
+
}).length
|
|
188
|
+
validCandidates.sort((a, b) => {
|
|
189
|
+
const overlapDifference = getOverlapCount(a) - getOverlapCount(b)
|
|
190
|
+
if (overlapDifference !== 0) return overlapDifference
|
|
191
|
+
return getPathLength(a) - getPathLength(b) || a.length - b.length
|
|
192
|
+
})
|
|
193
|
+
|
|
194
|
+
const bestCandidate = validCandidates[0]
|
|
195
|
+
if (!bestCandidate) return
|
|
196
|
+
|
|
197
|
+
this.outputTraces[traceIndex] = { ...trace, tracePath: bestCandidate }
|
|
198
|
+
this.stats.simplifiedTraceCount = (this.stats.simplifiedTraceCount ?? 0) + 1
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
getOutput() {
|
|
202
|
+
return {
|
|
203
|
+
traces: this.outputTraces,
|
|
204
|
+
netLabelPlacements: this.input.netLabelPlacements,
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
override visualize(): GraphicsObject {
|
|
209
|
+
const graphics = visualizeInputProblem(this.input.inputProblem)
|
|
210
|
+
graphics.lines ??= []
|
|
211
|
+
for (const trace of this.outputTraces) {
|
|
212
|
+
graphics.lines.push({ points: trace.tracePath, strokeColor: "purple" })
|
|
213
|
+
}
|
|
214
|
+
return graphics
|
|
215
|
+
}
|
|
216
|
+
}
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import type { Point } from "@tscircuit/math-utils"
|
|
2
|
+
import type { NetLabelPlacement } from "lib/solvers/NetLabelPlacementSolver/NetLabelPlacementSolver"
|
|
3
|
+
import { getRectBounds } from "lib/solvers/NetLabelPlacementSolver/SingleNetLabelPlacementSolver/geometry"
|
|
4
|
+
import type { SolvedTracePath } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceLinesSolver"
|
|
5
|
+
import {
|
|
6
|
+
isHorizontal,
|
|
7
|
+
isVertical,
|
|
8
|
+
segmentIntersectsRect,
|
|
9
|
+
} from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver2/collisions"
|
|
10
|
+
import { shiftSegmentOrth } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver2/pathOps"
|
|
11
|
+
import { simplifyPath } from "lib/solvers/TraceCleanupSolver/simplifyPath"
|
|
12
|
+
import { detectTraceLabelOverlap } from "lib/solvers/TraceLabelOverlapAvoidanceSolver/detectTraceLabelOverlap"
|
|
13
|
+
|
|
14
|
+
export const isSimpleFiveSegmentElbow = (path: Point[]): boolean => {
|
|
15
|
+
const simplifiedPath = simplifyPath(path)
|
|
16
|
+
if (simplifiedPath.length !== 6) return false
|
|
17
|
+
|
|
18
|
+
const segmentIsHorizontal = simplifiedPath
|
|
19
|
+
.slice(0, -1)
|
|
20
|
+
.map((point, index) => isHorizontal(point, simplifiedPath[index + 1]!))
|
|
21
|
+
|
|
22
|
+
return segmentIsHorizontal.every(
|
|
23
|
+
(isHorizontalSegment, index) =>
|
|
24
|
+
index === 0 || isHorizontalSegment !== segmentIsHorizontal[index - 1],
|
|
25
|
+
)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const generateSegmentShiftCandidates = ({
|
|
29
|
+
trace,
|
|
30
|
+
label,
|
|
31
|
+
paddingBuffer,
|
|
32
|
+
detourCount,
|
|
33
|
+
}: {
|
|
34
|
+
trace: SolvedTracePath
|
|
35
|
+
label: NetLabelPlacement
|
|
36
|
+
paddingBuffer: number
|
|
37
|
+
detourCount: number
|
|
38
|
+
}): Point[][] => {
|
|
39
|
+
if (trace.globalConnNetId === label.globalConnNetId) return []
|
|
40
|
+
|
|
41
|
+
const path = simplifyPath(trace.tracePath)
|
|
42
|
+
if (!isSimpleFiveSegmentElbow(path)) return []
|
|
43
|
+
|
|
44
|
+
const labelBounds = getRectBounds(label.center, label.width, label.height)
|
|
45
|
+
const effectivePadding = paddingBuffer + detourCount * paddingBuffer
|
|
46
|
+
const paddedLabelBounds = {
|
|
47
|
+
minX: labelBounds.minX - effectivePadding,
|
|
48
|
+
maxX: labelBounds.maxX + effectivePadding,
|
|
49
|
+
minY: labelBounds.minY - effectivePadding,
|
|
50
|
+
maxY: labelBounds.maxY + effectivePadding,
|
|
51
|
+
}
|
|
52
|
+
const candidates: Point[][] = []
|
|
53
|
+
|
|
54
|
+
for (let segmentIndex = 1; segmentIndex < path.length - 2; segmentIndex++) {
|
|
55
|
+
const segmentStart = path[segmentIndex]!
|
|
56
|
+
const segmentEnd = path[segmentIndex + 1]!
|
|
57
|
+
if (!segmentIntersectsRect(segmentStart, segmentEnd, labelBounds)) continue
|
|
58
|
+
|
|
59
|
+
const isHorizontalSegment = isHorizontal(segmentStart, segmentEnd)
|
|
60
|
+
const isVerticalSegment = isVertical(segmentStart, segmentEnd)
|
|
61
|
+
if (!isHorizontalSegment && !isVerticalSegment) continue
|
|
62
|
+
|
|
63
|
+
const axis = isHorizontalSegment ? "y" : "x"
|
|
64
|
+
const coordinates = isHorizontalSegment
|
|
65
|
+
? [paddedLabelBounds.minY, paddedLabelBounds.maxY]
|
|
66
|
+
: [paddedLabelBounds.minX, paddedLabelBounds.maxX]
|
|
67
|
+
|
|
68
|
+
for (const coordinate of coordinates) {
|
|
69
|
+
const shiftedPath = shiftSegmentOrth(path, segmentIndex, axis, coordinate)
|
|
70
|
+
if (shiftedPath) candidates.push(shiftedPath)
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return candidates
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const generateTransitionShiftCandidates = ({
|
|
78
|
+
trace,
|
|
79
|
+
label,
|
|
80
|
+
paddingBuffer,
|
|
81
|
+
detourCount,
|
|
82
|
+
}: {
|
|
83
|
+
trace: SolvedTracePath
|
|
84
|
+
label: NetLabelPlacement
|
|
85
|
+
paddingBuffer: number
|
|
86
|
+
detourCount: number
|
|
87
|
+
}): Point[][] => {
|
|
88
|
+
const path = simplifyPath(trace.tracePath)
|
|
89
|
+
if (!isSimpleFiveSegmentElbow(path)) return []
|
|
90
|
+
|
|
91
|
+
const labelBounds = getRectBounds(label.center, label.width, label.height)
|
|
92
|
+
const effectivePadding = paddingBuffer + detourCount * paddingBuffer
|
|
93
|
+
const paddedLabelBounds = {
|
|
94
|
+
minX: labelBounds.minX - effectivePadding,
|
|
95
|
+
maxX: labelBounds.maxX + effectivePadding,
|
|
96
|
+
minY: labelBounds.minY - effectivePadding,
|
|
97
|
+
maxY: labelBounds.maxY + effectivePadding,
|
|
98
|
+
}
|
|
99
|
+
const start = path[0]!
|
|
100
|
+
const end = path[path.length - 1]!
|
|
101
|
+
const middleSegmentIndex = 2
|
|
102
|
+
const candidates: Point[][] = []
|
|
103
|
+
|
|
104
|
+
for (let segmentIndex = 0; segmentIndex < path.length - 1; segmentIndex++) {
|
|
105
|
+
const segmentStart = path[segmentIndex]!
|
|
106
|
+
const segmentEnd = path[segmentIndex + 1]!
|
|
107
|
+
if (!segmentIntersectsRect(segmentStart, segmentEnd, labelBounds)) continue
|
|
108
|
+
if (Math.abs(segmentIndex - middleSegmentIndex) !== 1) continue
|
|
109
|
+
|
|
110
|
+
const isHorizontalSegment = isHorizontal(segmentStart, segmentEnd)
|
|
111
|
+
const isVerticalSegment = isVertical(segmentStart, segmentEnd)
|
|
112
|
+
if (!isHorizontalSegment && !isVerticalSegment) continue
|
|
113
|
+
|
|
114
|
+
const axis = isHorizontalSegment ? "x" : "y"
|
|
115
|
+
const min = isHorizontalSegment
|
|
116
|
+
? paddedLabelBounds.minX
|
|
117
|
+
: paddedLabelBounds.minY
|
|
118
|
+
const max = isHorizontalSegment
|
|
119
|
+
? paddedLabelBounds.maxX
|
|
120
|
+
: paddedLabelBounds.maxY
|
|
121
|
+
const startCoordinate = isHorizontalSegment ? start.x : start.y
|
|
122
|
+
const endCoordinate = isHorizontalSegment ? end.x : end.y
|
|
123
|
+
const corridorCoordinates: number[] = []
|
|
124
|
+
|
|
125
|
+
if (startCoordinate < min) {
|
|
126
|
+
corridorCoordinates.push((startCoordinate + min) / 2)
|
|
127
|
+
} else if (startCoordinate > max) {
|
|
128
|
+
corridorCoordinates.push((startCoordinate + max) / 2)
|
|
129
|
+
}
|
|
130
|
+
if (endCoordinate < min) {
|
|
131
|
+
corridorCoordinates.push((endCoordinate + min) / 2)
|
|
132
|
+
} else if (endCoordinate > max) {
|
|
133
|
+
corridorCoordinates.push((endCoordinate + max) / 2)
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
for (const coordinate of new Set(corridorCoordinates)) {
|
|
137
|
+
const shiftedPath = shiftSegmentOrth(
|
|
138
|
+
path,
|
|
139
|
+
middleSegmentIndex,
|
|
140
|
+
axis,
|
|
141
|
+
coordinate,
|
|
142
|
+
)
|
|
143
|
+
if (shiftedPath) candidates.push(shiftedPath)
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return candidates
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export const generateElbowTransitionSimplificationCandidates = ({
|
|
151
|
+
trace,
|
|
152
|
+
label,
|
|
153
|
+
netLabelPlacements,
|
|
154
|
+
paddingBuffer,
|
|
155
|
+
detourCount,
|
|
156
|
+
}: {
|
|
157
|
+
trace: SolvedTracePath
|
|
158
|
+
label: NetLabelPlacement
|
|
159
|
+
netLabelPlacements: NetLabelPlacement[]
|
|
160
|
+
paddingBuffer: number
|
|
161
|
+
detourCount: number
|
|
162
|
+
}): Point[][] => {
|
|
163
|
+
const transitionCandidates = generateTransitionShiftCandidates({
|
|
164
|
+
trace,
|
|
165
|
+
label,
|
|
166
|
+
paddingBuffer,
|
|
167
|
+
detourCount,
|
|
168
|
+
})
|
|
169
|
+
|
|
170
|
+
return transitionCandidates.flatMap((tracePath) => {
|
|
171
|
+
const shiftedTrace = { ...trace, tracePath }
|
|
172
|
+
const shiftedOverlaps = detectTraceLabelOverlap({
|
|
173
|
+
traces: [shiftedTrace],
|
|
174
|
+
netLabels: netLabelPlacements,
|
|
175
|
+
})
|
|
176
|
+
|
|
177
|
+
return shiftedOverlaps.flatMap(({ label: shiftedLabel }) =>
|
|
178
|
+
generateSegmentShiftCandidates({
|
|
179
|
+
trace: shiftedTrace,
|
|
180
|
+
label: shiftedLabel,
|
|
181
|
+
paddingBuffer,
|
|
182
|
+
detourCount,
|
|
183
|
+
}),
|
|
184
|
+
)
|
|
185
|
+
})
|
|
186
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { Point } from "@tscircuit/math-utils"
|
|
2
|
+
import type { NetLabelPlacement } from "lib/solvers/NetLabelPlacementSolver/NetLabelPlacementSolver"
|
|
3
|
+
import type { SolvedTracePath } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceLinesSolver"
|
|
4
|
+
|
|
5
|
+
export interface CompletedTraceReroute {
|
|
6
|
+
initialTrace: SolvedTracePath
|
|
7
|
+
reroutedTracePath: Point[]
|
|
8
|
+
label: NetLabelPlacement
|
|
9
|
+
detourCount: number
|
|
10
|
+
}
|
|
@@ -122,6 +122,9 @@ export class TraceLabelOverlapAvoidanceSolver extends BaseSolver {
|
|
|
122
122
|
const solvedTraces = this.subSolvers.flatMap((s) => s.getOutput().allTraces)
|
|
123
123
|
return {
|
|
124
124
|
traces: [...this.cleanTraces, ...solvedTraces],
|
|
125
|
+
completedReroutes: this.subSolvers.flatMap(
|
|
126
|
+
(solver) => solver.completedReroutes,
|
|
127
|
+
),
|
|
125
128
|
netLabelPlacements:
|
|
126
129
|
this.labelMergingSolver?.getOutput().netLabelPlacements ??
|
|
127
130
|
this.netLabelPlacements,
|
|
@@ -8,6 +8,7 @@ import { detectTraceLabelOverlap } from "../../detectTraceLabelOverlap"
|
|
|
8
8
|
import { SingleOverlapSolver } from "../SingleOverlapSolver/SingleOverlapSolver"
|
|
9
9
|
import { doesTraceStartOrEndInLabel } from "./doesTraceStartOrEndInLabel"
|
|
10
10
|
import { visualizeDecomposition } from "./visualizeDecomposition"
|
|
11
|
+
import type { CompletedTraceReroute } from "lib/solvers/TraceElbowTransitionSimplificationSolver/types"
|
|
11
12
|
|
|
12
13
|
type Overlap = ReturnType<typeof detectTraceLabelOverlap>[0]
|
|
13
14
|
|
|
@@ -35,6 +36,7 @@ export class OverlapAvoidanceStepSolver extends BaseSolver {
|
|
|
35
36
|
allTraces: SolvedTracePath[]
|
|
36
37
|
tracesToAvoidOverlapping: SolvedTracePath[]
|
|
37
38
|
modifiedTraces: SolvedTracePath[] = []
|
|
39
|
+
completedReroutes: CompletedTraceReroute[] = []
|
|
38
40
|
|
|
39
41
|
private readonly PADDING_BUFFER = 0.1
|
|
40
42
|
private detourCounts: Map<string, number> = new Map()
|
|
@@ -66,6 +68,17 @@ export class OverlapAvoidanceStepSolver extends BaseSolver {
|
|
|
66
68
|
if (this.activeSubSolver.solved) {
|
|
67
69
|
const solvedPath = this.activeSubSolver.solvedTracePath
|
|
68
70
|
if (solvedPath) {
|
|
71
|
+
this.completedReroutes.push({
|
|
72
|
+
initialTrace: {
|
|
73
|
+
...this.activeSubSolver.initialTrace,
|
|
74
|
+
tracePath: this.activeSubSolver.initialTrace.tracePath.map(
|
|
75
|
+
(point) => ({ ...point }),
|
|
76
|
+
),
|
|
77
|
+
},
|
|
78
|
+
reroutedTracePath: solvedPath.map((point) => ({ ...point })),
|
|
79
|
+
label: this.activeSubSolver.label,
|
|
80
|
+
detourCount: this.activeSubSolver.detourCount,
|
|
81
|
+
})
|
|
69
82
|
const traceIndex = this.allTraces.findIndex(
|
|
70
83
|
(t) => t.mspPairId === this.activeSubSolver!.initialTrace.mspPairId,
|
|
71
84
|
)
|
|
@@ -154,6 +167,7 @@ export class OverlapAvoidanceStepSolver extends BaseSolver {
|
|
|
154
167
|
paddingBuffer: this.PADDING_BUFFER,
|
|
155
168
|
detourCount: detourCount,
|
|
156
169
|
tracesToAvoidOverlapping: this.tracesToAvoidOverlapping,
|
|
170
|
+
netLabelPlacements: this.initialNetLabelPlacements,
|
|
157
171
|
})
|
|
158
172
|
} else {
|
|
159
173
|
const overlapId = `${traceToFix.mspPairId}-${labelToAvoid.globalConnNetId}`
|
|
@@ -197,6 +211,7 @@ export class OverlapAvoidanceStepSolver extends BaseSolver {
|
|
|
197
211
|
paddingBuffer: this.PADDING_BUFFER,
|
|
198
212
|
detourCount: detourCount,
|
|
199
213
|
tracesToAvoidOverlapping: this.tracesToAvoidOverlapping,
|
|
214
|
+
netLabelPlacements: this.initialNetLabelPlacements,
|
|
200
215
|
})
|
|
201
216
|
} else {
|
|
202
217
|
const overlapId = `${traceToFix.mspPairId}-${labelToAvoid.globalConnNetId}`
|
|
@@ -217,6 +232,7 @@ export class OverlapAvoidanceStepSolver extends BaseSolver {
|
|
|
217
232
|
paddingBuffer: this.PADDING_BUFFER,
|
|
218
233
|
detourCount: detourCount,
|
|
219
234
|
tracesToAvoidOverlapping: this.tracesToAvoidOverlapping,
|
|
235
|
+
netLabelPlacements: this.initialNetLabelPlacements,
|
|
220
236
|
})
|
|
221
237
|
}
|
|
222
238
|
}
|
|
@@ -19,9 +19,22 @@ interface SingleOverlapSolverInput {
|
|
|
19
19
|
paddingBuffer: number
|
|
20
20
|
detourCount: number
|
|
21
21
|
tracesToAvoidOverlapping?: SolvedTracePath[]
|
|
22
|
+
netLabelPlacements?: NetLabelPlacement[]
|
|
22
23
|
}
|
|
23
24
|
|
|
24
25
|
const MAX_TRIES = 5
|
|
26
|
+
const PATH_LENGTH_EPSILON = 1e-9
|
|
27
|
+
|
|
28
|
+
const getPathLength = (points: Point[]) => {
|
|
29
|
+
let length = 0
|
|
30
|
+
for (let pointIndex = 0; pointIndex < points.length - 1; pointIndex++) {
|
|
31
|
+
const point = points[pointIndex]!
|
|
32
|
+
const nextPoint = points[pointIndex + 1]!
|
|
33
|
+
length += Math.abs(nextPoint.x - point.x) + Math.abs(nextPoint.y - point.y)
|
|
34
|
+
}
|
|
35
|
+
return length
|
|
36
|
+
}
|
|
37
|
+
|
|
25
38
|
/**
|
|
26
39
|
* This solver attempts to find a valid rerouting for a single trace that is
|
|
27
40
|
* overlapping with a net label. It tries various candidate paths until it
|
|
@@ -35,6 +48,8 @@ export class SingleOverlapSolver extends BaseSolver {
|
|
|
35
48
|
obstacles: ReturnType<typeof getObstacleRects>
|
|
36
49
|
label: NetLabelPlacement
|
|
37
50
|
tracesToAvoidOverlapping: SolvedTracePath[]
|
|
51
|
+
netLabelPlacements: NetLabelPlacement[]
|
|
52
|
+
detourCount: number
|
|
38
53
|
_tried: number = 0
|
|
39
54
|
|
|
40
55
|
constructor(solverInput: SingleOverlapSolverInput) {
|
|
@@ -42,9 +57,14 @@ export class SingleOverlapSolver extends BaseSolver {
|
|
|
42
57
|
this.initialTrace = solverInput.trace
|
|
43
58
|
this.problem = solverInput.problem
|
|
44
59
|
this.label = solverInput.label
|
|
60
|
+
this.detourCount = solverInput.detourCount
|
|
45
61
|
this.tracesToAvoidOverlapping = (
|
|
46
62
|
solverInput.tracesToAvoidOverlapping ?? []
|
|
47
63
|
).filter((t) => t.globalConnNetId !== solverInput.trace.globalConnNetId)
|
|
64
|
+
this.netLabelPlacements = solverInput.netLabelPlacements ?? [
|
|
65
|
+
solverInput.label,
|
|
66
|
+
]
|
|
67
|
+
this.obstacles = getObstacleRects(this.problem)
|
|
48
68
|
|
|
49
69
|
// Calculate an effective padding for this specific run based on the detourCount.
|
|
50
70
|
const effectivePadding =
|
|
@@ -56,20 +76,33 @@ export class SingleOverlapSolver extends BaseSolver {
|
|
|
56
76
|
paddingBuffer: effectivePadding, // Use the calculated, larger padding
|
|
57
77
|
})
|
|
58
78
|
|
|
59
|
-
const
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
79
|
+
const getLabelOverlapCount = (path: Point[]) =>
|
|
80
|
+
detectTraceLabelOverlap({
|
|
81
|
+
traces: [{ ...this.initialTrace, tracePath: path }],
|
|
82
|
+
netLabels: this.netLabelPlacements,
|
|
83
|
+
}).length
|
|
84
|
+
|
|
85
|
+
const candidateByPath = new Map<string, Point[]>()
|
|
86
|
+
for (const candidate of candidates) {
|
|
87
|
+
const simplifiedCandidate = simplifyPath(candidate)
|
|
88
|
+
candidateByPath.set(
|
|
89
|
+
simplifiedCandidate.map((point) => `${point.x},${point.y}`).join(";"),
|
|
90
|
+
simplifiedCandidate,
|
|
91
|
+
)
|
|
67
92
|
}
|
|
68
93
|
|
|
69
|
-
this.queuedCandidatePaths =
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
94
|
+
this.queuedCandidatePaths = [...candidateByPath.values()].sort((a, b) => {
|
|
95
|
+
const pathLengthDifference = getPathLength(a) - getPathLength(b)
|
|
96
|
+
if (Math.abs(pathLengthDifference) >= PATH_LENGTH_EPSILON) {
|
|
97
|
+
return pathLengthDifference
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const overlapCountDifference =
|
|
101
|
+
getLabelOverlapCount(a) - getLabelOverlapCount(b)
|
|
102
|
+
if (overlapCountDifference !== 0) return overlapCountDifference
|
|
103
|
+
|
|
104
|
+
return a.length - b.length
|
|
105
|
+
})
|
|
73
106
|
}
|
|
74
107
|
|
|
75
108
|
override _step() {
|
|
@@ -91,9 +124,19 @@ export class SingleOverlapSolver extends BaseSolver {
|
|
|
91
124
|
traces: [{ ...this.initialTrace, tracePath: simplifiedPath }],
|
|
92
125
|
netLabels: [this.label],
|
|
93
126
|
}).length > 0
|
|
127
|
+
const initialPath = simplifyPath(this.initialTrace.tracePath)
|
|
128
|
+
const initialLabelOverlaps = detectTraceLabelOverlap({
|
|
129
|
+
traces: [{ ...this.initialTrace, tracePath: initialPath }],
|
|
130
|
+
netLabels: this.netLabelPlacements,
|
|
131
|
+
})
|
|
132
|
+
const candidateLabelOverlaps = detectTraceLabelOverlap({
|
|
133
|
+
traces: [{ ...this.initialTrace, tracePath: simplifiedPath }],
|
|
134
|
+
netLabels: this.netLabelPlacements,
|
|
135
|
+
})
|
|
94
136
|
|
|
95
137
|
if (
|
|
96
138
|
!stillOverlapsLabel &&
|
|
139
|
+
candidateLabelOverlaps.length <= initialLabelOverlaps.length &&
|
|
97
140
|
!isPathCollidingWithObstacles(simplifiedPath, this.obstacles) &&
|
|
98
141
|
!doesPathCoincideWithTraces(simplifiedPath, this.tracesToAvoidOverlapping)
|
|
99
142
|
) {
|
|
@@ -128,7 +171,7 @@ export class SingleOverlapSolver extends BaseSolver {
|
|
|
128
171
|
})
|
|
129
172
|
|
|
130
173
|
// Draw next candidate
|
|
131
|
-
if (this.queuedCandidatePaths.length > 0) {
|
|
174
|
+
if (!this.solvedTracePath && this.queuedCandidatePaths.length > 0) {
|
|
132
175
|
graphics.lines.push({
|
|
133
176
|
points: this.queuedCandidatePaths[0],
|
|
134
177
|
strokeColor: "orange",
|
|
@@ -34,6 +34,30 @@ export interface InputDirectConnection {
|
|
|
34
34
|
pinIds: [PinId, PinId]
|
|
35
35
|
netId?: string
|
|
36
36
|
netLabelWidth?: number
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* When true, this point-to-point connection may be labeled with an "inline
|
|
40
|
+
* net label": the net name is drawn parallel to (and offset from) the routed
|
|
41
|
+
* trace instead of being placed as a separate anchored net label at the end
|
|
42
|
+
* of the trace.
|
|
43
|
+
*
|
|
44
|
+
* Only set this for connections whose net name is worth showing on the wire -
|
|
45
|
+
* the solver trusts the caller (e.g. @tscircuit/core) to make that decision.
|
|
46
|
+
* An inline label is only emitted when the connection actually got routed.
|
|
47
|
+
*/
|
|
48
|
+
allowInlineNetLabel?: boolean
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Extent of the inline net label along the trace. Falls back to
|
|
52
|
+
* `netLabelWidth`, then to an estimate from the netId text.
|
|
53
|
+
*/
|
|
54
|
+
inlineNetLabelWidth?: number
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Height of the inline net label text. Defaults to
|
|
58
|
+
* DEFAULT_INLINE_NET_LABEL_HEIGHT.
|
|
59
|
+
*/
|
|
60
|
+
inlineNetLabelHeight?: number
|
|
37
61
|
}
|
|
38
62
|
|
|
39
63
|
export interface InputNetConnection {
|
package/package.json
CHANGED