@tscircuit/schematic-trace-solver 0.0.41 → 0.0.42
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 +118 -7
- package/dist/index.js +760 -432
- package/lib/solvers/SchematicTracePipelineSolver/SchematicTracePipelineSolver.ts +30 -17
- package/lib/solvers/TraceLabelOverlapAvoidanceSolver/TraceLabelOverlapAvoidanceSolver.ts +94 -192
- package/lib/solvers/TraceLabelOverlapAvoidanceSolver/rerouteCollidingTrace.ts +13 -24
- package/lib/solvers/TraceLabelOverlapAvoidanceSolver/sub-solvers/LabelMergingSolver/LabelMergingSolver.ts +205 -0
- package/lib/solvers/TraceLabelOverlapAvoidanceSolver/sub-solvers/OverlapAvoidanceStepSolver/OverlapAvoidanceStepSolver.ts +144 -0
- package/lib/solvers/TraceLabelOverlapAvoidanceSolver/sub-solvers/SingleOverlapSolver/SingleOverlapSolver.ts +117 -0
- package/lib/solvers/TraceLabelOverlapAvoidanceSolver/sub-solvers/TraceCleanupSolver/TraceCleanupSolver.ts +132 -0
- package/lib/solvers/TraceLabelOverlapAvoidanceSolver/{balanceLShapes.ts → sub-solvers/TraceCleanupSolver/balanceLShapes.ts} +12 -10
- package/lib/solvers/TraceLabelOverlapAvoidanceSolver/{hasCollisions.ts → sub-solvers/TraceCleanupSolver/hasCollisions.ts} +1 -1
- package/lib/solvers/TraceLabelOverlapAvoidanceSolver/{hasCollisionsWithLabels.ts → sub-solvers/TraceCleanupSolver/hasCollisionsWithLabels.ts} +1 -1
- package/lib/solvers/TraceLabelOverlapAvoidanceSolver/{minimizeTurnsWithFilteredLabels.ts → sub-solvers/TraceCleanupSolver/minimizeTurnsWithFilteredLabels.ts} +6 -6
- package/lib/solvers/TraceLabelOverlapAvoidanceSolver/{simplifyPath.ts → sub-solvers/TraceCleanupSolver/simplifyPath.ts} +1 -1
- package/lib/solvers/TraceLabelOverlapAvoidanceSolver/{turnMinimization.ts → sub-solvers/TraceCleanupSolver/turnMinimization.ts} +0 -2
- package/lib/solvers/TraceLabelOverlapAvoidanceSolver/tryFourPointDetour.ts +104 -0
- package/lib/solvers/TraceLabelOverlapAvoidanceSolver/trySnipAndReconnect.ts +24 -154
- package/package.json +1 -1
- package/tests/assets/1.input.json +379 -0
- package/tests/assets/2.input.json +330 -0
- package/tests/assets/3.input.json +388 -0
- package/tests/assets/MergedNetLabelObstacles.test.input.json +154 -0
- package/tests/assets/OverlapAvoidanceStepSolver.test.input.json +682 -0
- package/tests/assets/SingleOverlapSolver.test.input.json +170 -0
- package/tests/assets/TraceCleanupSolver.test.input.json +216 -0
- package/tests/assets/TraceLabelOverlapAvoidanceSolver.test.input.json +424 -0
- package/tests/examples/__snapshots__/example25.snap.svg +3 -3
- package/tests/examples/__snapshots__/example26.snap.svg +3 -3
- package/tests/solvers/TraceLabelOverlapAvoidanceSolver/TraceLabelOverlapAvoidanceSolver.test.ts +18 -0
- package/tests/solvers/TraceLabelOverlapAvoidanceSolver/__snapshots__/TraceLabelOverlapAvoidanceSolver.snap.svg +243 -0
- package/tests/solvers/TraceLabelOverlapAvoidanceSolver/renderComparisonView/__snapshots__/renderComparisonView01.snap.svg +223 -0
- package/tests/solvers/TraceLabelOverlapAvoidanceSolver/renderComparisonView/__snapshots__/renderComparisonView02.snap.svg +205 -0
- package/tests/solvers/TraceLabelOverlapAvoidanceSolver/renderComparisonView/__snapshots__/renderComparisonView03.snap.svg +184 -0
- package/tests/solvers/TraceLabelOverlapAvoidanceSolver/renderComparisonView/renderComparisonView01.test.ts +29 -0
- package/tests/solvers/TraceLabelOverlapAvoidanceSolver/renderComparisonView/renderComparisonView02.test.ts +27 -0
- package/tests/solvers/TraceLabelOverlapAvoidanceSolver/renderComparisonView/renderComparisonView03.test.ts +39 -0
- package/tests/solvers/TraceLabelOverlapAvoidanceSolver/sub-solver/MergedNetLabelObstacles.test.ts +14 -0
- package/tests/solvers/TraceLabelOverlapAvoidanceSolver/sub-solver/OverlapAvoidanceStepSolver.test.ts +20 -0
- package/tests/solvers/TraceLabelOverlapAvoidanceSolver/sub-solver/SingleOverlapSolver.test.ts +10 -0
- package/tests/solvers/TraceLabelOverlapAvoidanceSolver/sub-solver/TraceCleanupSolver.test.ts +19 -0
- package/tests/solvers/TraceLabelOverlapAvoidanceSolver/sub-solver/__snapshots__/MergedNetLabelObstacles.snap.svg +140 -0
- package/tests/solvers/TraceLabelOverlapAvoidanceSolver/sub-solver/__snapshots__/OverlapAvoidanceStepSolver.snap.svg +216 -0
- package/tests/solvers/TraceLabelOverlapAvoidanceSolver/sub-solver/__snapshots__/SingleOverlapSolver.snap.svg +125 -0
- package/tests/solvers/TraceLabelOverlapAvoidanceSolver/sub-solver/__snapshots__/TraceCleanupSolver.snap.svg +119 -0
- /package/lib/solvers/TraceLabelOverlapAvoidanceSolver/{countTurns.ts → sub-solvers/TraceCleanupSolver/countTurns.ts} +0 -0
- /package/lib/solvers/TraceLabelOverlapAvoidanceSolver/{tryConnectPoints.ts → sub-solvers/TraceCleanupSolver/tryConnectPoints.ts} +0 -0
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import type { SolvedTracePath } from "../../../SchematicTraceLinesSolver/SchematicTraceLinesSolver"
|
|
2
|
+
import { BaseSolver } from "lib/solvers/BaseSolver/BaseSolver"
|
|
3
|
+
import type { NetLabelPlacement } from "../../../NetLabelPlacementSolver/NetLabelPlacementSolver"
|
|
4
|
+
import { getRectBounds } from "../../../NetLabelPlacementSolver/SingleNetLabelPlacementSolver/geometry"
|
|
5
|
+
import type { GraphicsObject, Line } from "graphics-debug"
|
|
6
|
+
import { getColorFromString } from "lib/utils/getColorFromString"
|
|
7
|
+
import { visualizeInputProblem } from "lib/solvers/SchematicTracePipelineSolver/visualizeInputProblem"
|
|
8
|
+
import type { InputProblem } from "lib/types/InputProblem"
|
|
9
|
+
|
|
10
|
+
interface LabelMergingSolverInput {
|
|
11
|
+
netLabelPlacements: NetLabelPlacement[]
|
|
12
|
+
inputProblem: InputProblem
|
|
13
|
+
traces: SolvedTracePath[]
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
interface LabelMergingSolverOutput {
|
|
17
|
+
netLabelPlacements: NetLabelPlacement[]
|
|
18
|
+
mergedLabelNetIdMap: Record<string, Set<string>>
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Merges multiple net labels into a single, larger label if they are on the
|
|
23
|
+
* same side of the same chip, reducing schematic clutter.
|
|
24
|
+
*/
|
|
25
|
+
export class MergedNetLabelObstacleSolver extends BaseSolver {
|
|
26
|
+
private input: LabelMergingSolverInput
|
|
27
|
+
private output!: LabelMergingSolverOutput
|
|
28
|
+
private inputProblem: InputProblem
|
|
29
|
+
private traces: SolvedTracePath[]
|
|
30
|
+
|
|
31
|
+
constructor(solverInput: LabelMergingSolverInput) {
|
|
32
|
+
// console.log(JSON.stringify(solverInput));
|
|
33
|
+
|
|
34
|
+
super()
|
|
35
|
+
this.input = solverInput
|
|
36
|
+
this.inputProblem = solverInput.inputProblem
|
|
37
|
+
this.traces = solverInput.traces
|
|
38
|
+
|
|
39
|
+
// Initialize output to a default state to allow visualization before the first step
|
|
40
|
+
this.output = {
|
|
41
|
+
netLabelPlacements: solverInput.netLabelPlacements,
|
|
42
|
+
mergedLabelNetIdMap: {},
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
override _step() {
|
|
47
|
+
const originalLabels = this.input.netLabelPlacements
|
|
48
|
+
const mergedLabelNetIdMap: Record<string, Set<string>> = {}
|
|
49
|
+
|
|
50
|
+
if (!originalLabels || originalLabels.length === 0) {
|
|
51
|
+
this.output = {
|
|
52
|
+
netLabelPlacements: [],
|
|
53
|
+
mergedLabelNetIdMap: {},
|
|
54
|
+
}
|
|
55
|
+
this.solved = true
|
|
56
|
+
return
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const labelGroups: Record<string, NetLabelPlacement[]> = {}
|
|
60
|
+
|
|
61
|
+
for (const p of originalLabels) {
|
|
62
|
+
if (p.pinIds.length === 0) continue
|
|
63
|
+
const chipId = p.pinIds[0].split(".")[0]
|
|
64
|
+
if (!chipId) continue
|
|
65
|
+
const key = `${chipId}-${p.orientation}`
|
|
66
|
+
if (!(key in labelGroups)) {
|
|
67
|
+
labelGroups[key] = []
|
|
68
|
+
}
|
|
69
|
+
labelGroups[key]!.push(p)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const finalPlacements: NetLabelPlacement[] = []
|
|
73
|
+
for (const [key, group] of Object.entries(labelGroups)) {
|
|
74
|
+
if (group.length <= 1) {
|
|
75
|
+
finalPlacements.push(...group)
|
|
76
|
+
continue
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
let minX = Infinity
|
|
80
|
+
let minY = Infinity
|
|
81
|
+
let maxX = -Infinity
|
|
82
|
+
let maxY = -Infinity
|
|
83
|
+
for (const p of group) {
|
|
84
|
+
const bounds = getRectBounds(p.center, p.width, p.height)
|
|
85
|
+
minX = Math.min(minX, bounds.minX)
|
|
86
|
+
minY = Math.min(minY, bounds.minY)
|
|
87
|
+
maxX = Math.max(maxX, bounds.maxX)
|
|
88
|
+
maxY = Math.max(maxY, bounds.maxY)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const newWidth = maxX - minX
|
|
92
|
+
const newHeight = maxY - minY
|
|
93
|
+
const template = group[0]!
|
|
94
|
+
const syntheticId = `merged-group-${key}`
|
|
95
|
+
const originalNetIds = new Set(group.map((p) => p.globalConnNetId))
|
|
96
|
+
mergedLabelNetIdMap[syntheticId] = originalNetIds
|
|
97
|
+
|
|
98
|
+
finalPlacements.push({
|
|
99
|
+
...template,
|
|
100
|
+
globalConnNetId: syntheticId,
|
|
101
|
+
width: newWidth,
|
|
102
|
+
height: newHeight,
|
|
103
|
+
center: { x: minX + newWidth / 2, y: minY + newHeight / 2 },
|
|
104
|
+
pinIds: [...new Set(group.flatMap((p) => p.pinIds))],
|
|
105
|
+
mspConnectionPairIds: [
|
|
106
|
+
...new Set(group.flatMap((p) => p.mspConnectionPairIds)),
|
|
107
|
+
],
|
|
108
|
+
})
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
this.output = {
|
|
112
|
+
netLabelPlacements: finalPlacements,
|
|
113
|
+
mergedLabelNetIdMap,
|
|
114
|
+
}
|
|
115
|
+
this.solved = true
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
getOutput(): LabelMergingSolverOutput {
|
|
119
|
+
return this.output
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
override visualize(): GraphicsObject {
|
|
123
|
+
const graphics = visualizeInputProblem(this.inputProblem, {
|
|
124
|
+
chipAlpha: 0.1,
|
|
125
|
+
connectionAlpha: 0.1,
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
if (!graphics.rects) graphics.rects = []
|
|
129
|
+
if (!graphics.lines) graphics.lines = []
|
|
130
|
+
if (!graphics.points) graphics.points = []
|
|
131
|
+
if (!graphics.texts) graphics.texts = []
|
|
132
|
+
|
|
133
|
+
const originalLabelsById = new Map<string, NetLabelPlacement>()
|
|
134
|
+
for (const label of this.input.netLabelPlacements) {
|
|
135
|
+
originalLabelsById.set(label.globalConnNetId, label)
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
for (const trace of this.traces) {
|
|
139
|
+
const line: Line = {
|
|
140
|
+
points: trace.tracePath.map((p) => ({ x: p.x, y: p.y })),
|
|
141
|
+
strokeColor: "blue",
|
|
142
|
+
}
|
|
143
|
+
graphics.lines!.push(line)
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
for (const finalLabel of this.output.netLabelPlacements) {
|
|
147
|
+
const isMerged = finalLabel.globalConnNetId.startsWith("merged-group-")
|
|
148
|
+
const color = getColorFromString(finalLabel.globalConnNetId)
|
|
149
|
+
|
|
150
|
+
if (isMerged) {
|
|
151
|
+
// Draw the new merged label
|
|
152
|
+
graphics.rects.push({
|
|
153
|
+
center: finalLabel.center,
|
|
154
|
+
width: finalLabel.width,
|
|
155
|
+
height: finalLabel.height,
|
|
156
|
+
fill: color.replace(/, 1\)/, ", 0.2)"), // semi-transparent
|
|
157
|
+
stroke: color,
|
|
158
|
+
label: finalLabel.globalConnNetId,
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
const originalNetIds =
|
|
162
|
+
this.output.mergedLabelNetIdMap[finalLabel.globalConnNetId]
|
|
163
|
+
if (originalNetIds) {
|
|
164
|
+
for (const originalNetId of originalNetIds) {
|
|
165
|
+
const originalLabel = originalLabelsById.get(originalNetId)
|
|
166
|
+
if (originalLabel) {
|
|
167
|
+
// Draw the original label as a dashed box
|
|
168
|
+
const bounds = getRectBounds(
|
|
169
|
+
originalLabel.center,
|
|
170
|
+
originalLabel.width,
|
|
171
|
+
originalLabel.height,
|
|
172
|
+
)
|
|
173
|
+
const p1 = { x: bounds.minX, y: bounds.minY }
|
|
174
|
+
const p2 = { x: bounds.maxX, y: bounds.minY }
|
|
175
|
+
const p3 = { x: bounds.maxX, y: bounds.maxY }
|
|
176
|
+
const p4 = { x: bounds.minX, y: bounds.maxY }
|
|
177
|
+
graphics.lines.push({
|
|
178
|
+
points: [p1, p2, p3, p4, p1],
|
|
179
|
+
strokeColor: color,
|
|
180
|
+
strokeDash: "4 4",
|
|
181
|
+
})
|
|
182
|
+
// Draw line from original to new center
|
|
183
|
+
graphics.lines.push({
|
|
184
|
+
points: [originalLabel.center, finalLabel.center],
|
|
185
|
+
strokeColor: color,
|
|
186
|
+
strokeDash: "2 2",
|
|
187
|
+
})
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
} else {
|
|
192
|
+
// Draw un-merged labels
|
|
193
|
+
graphics.rects.push({
|
|
194
|
+
center: finalLabel.center,
|
|
195
|
+
width: finalLabel.width,
|
|
196
|
+
height: finalLabel.height,
|
|
197
|
+
stroke: color,
|
|
198
|
+
label: finalLabel.globalConnNetId,
|
|
199
|
+
})
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
return graphics
|
|
204
|
+
}
|
|
205
|
+
}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import type { GraphicsObject } from "graphics-debug"
|
|
2
|
+
import { BaseSolver } from "lib/solvers/BaseSolver/BaseSolver"
|
|
3
|
+
import type { NetLabelPlacement } from "lib/solvers/NetLabelPlacementSolver/NetLabelPlacementSolver"
|
|
4
|
+
import type { SolvedTracePath } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceLinesSolver"
|
|
5
|
+
import { visualizeInputProblem } from "lib/solvers/SchematicTracePipelineSolver/visualizeInputProblem"
|
|
6
|
+
import type { InputProblem } from "lib/types/InputProblem"
|
|
7
|
+
import { detectTraceLabelOverlap } from "../../detectTraceLabelOverlap"
|
|
8
|
+
import { SingleOverlapSolver } from "../SingleOverlapSolver/SingleOverlapSolver"
|
|
9
|
+
|
|
10
|
+
type Overlap = ReturnType<typeof detectTraceLabelOverlap>[0]
|
|
11
|
+
|
|
12
|
+
// Define a type for the input of the internal overlap solver to avoid conflicts
|
|
13
|
+
interface OverlapCollectionSolverInput {
|
|
14
|
+
inputProblem: InputProblem
|
|
15
|
+
traces: SolvedTracePath[]
|
|
16
|
+
netLabelPlacements: NetLabelPlacement[]
|
|
17
|
+
mergedLabelNetIdMap: Record<string, Set<string>>
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* This is an internal solver that manages the step-by-step process of avoiding
|
|
22
|
+
* multiple overlaps. It follows the pattern of SchematicTraceLinesSolver.
|
|
23
|
+
*/
|
|
24
|
+
export class OverlapAvoidanceStepSolver extends BaseSolver {
|
|
25
|
+
inputProblem: InputProblem
|
|
26
|
+
netLabelPlacements: NetLabelPlacement[]
|
|
27
|
+
mergedLabelNetIdMap: Record<string, Set<string>>
|
|
28
|
+
|
|
29
|
+
allTraces: SolvedTracePath[]
|
|
30
|
+
modifiedTraces: SolvedTracePath[] = []
|
|
31
|
+
|
|
32
|
+
private detourCountByLabel: Record<string, number> = {}
|
|
33
|
+
private readonly PADDING_BUFFER = 0.1
|
|
34
|
+
|
|
35
|
+
public override activeSubSolver: SingleOverlapSolver | null = null
|
|
36
|
+
private overlapQueue: Overlap[] = []
|
|
37
|
+
private recentlyFailed: Set<string> = new Set()
|
|
38
|
+
|
|
39
|
+
constructor(solverInput: OverlapCollectionSolverInput) {
|
|
40
|
+
super()
|
|
41
|
+
this.inputProblem = solverInput.inputProblem
|
|
42
|
+
this.netLabelPlacements = solverInput.netLabelPlacements
|
|
43
|
+
this.mergedLabelNetIdMap = solverInput.mergedLabelNetIdMap
|
|
44
|
+
this.allTraces = [...solverInput.traces]
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
override _step() {
|
|
48
|
+
if (this.activeSubSolver) {
|
|
49
|
+
this.activeSubSolver.step()
|
|
50
|
+
|
|
51
|
+
if (this.activeSubSolver.solved) {
|
|
52
|
+
const solvedPath = this.activeSubSolver.solvedTracePath
|
|
53
|
+
if (solvedPath) {
|
|
54
|
+
const traceIndex = this.allTraces.findIndex(
|
|
55
|
+
(t) => t.mspPairId === this.activeSubSolver!.initialTrace.mspPairId,
|
|
56
|
+
)
|
|
57
|
+
if (traceIndex !== -1) {
|
|
58
|
+
this.allTraces[traceIndex].tracePath = solvedPath
|
|
59
|
+
this.modifiedTraces.push(this.allTraces[traceIndex])
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
this.activeSubSolver = null
|
|
63
|
+
this.recentlyFailed.clear()
|
|
64
|
+
} else if (this.activeSubSolver.failed) {
|
|
65
|
+
const overlapId = `${this.activeSubSolver.initialTrace.mspPairId}-${this.activeSubSolver.label.globalConnNetId}`
|
|
66
|
+
this.recentlyFailed.add(overlapId)
|
|
67
|
+
this.activeSubSolver = null
|
|
68
|
+
}
|
|
69
|
+
return
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const overlaps = detectTraceLabelOverlap(
|
|
73
|
+
this.allTraces,
|
|
74
|
+
this.netLabelPlacements,
|
|
75
|
+
).filter((o) => {
|
|
76
|
+
const originalNetIds = this.mergedLabelNetIdMap[o.label.globalConnNetId]
|
|
77
|
+
if (originalNetIds) {
|
|
78
|
+
return !originalNetIds.has(o.trace.globalConnNetId)
|
|
79
|
+
}
|
|
80
|
+
return o.trace.globalConnNetId !== o.label.globalConnNetId
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
if (overlaps.length === 0) {
|
|
84
|
+
this.solved = true
|
|
85
|
+
return
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const nonFailedOverlaps = overlaps.filter((o) => {
|
|
89
|
+
const overlapId = `${o.trace.mspPairId}-${o.label.globalConnNetId}`
|
|
90
|
+
return !this.recentlyFailed.has(overlapId)
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
if (nonFailedOverlaps.length === 0) {
|
|
94
|
+
this.solved = true // No more progress can be made
|
|
95
|
+
return
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
this.overlapQueue = nonFailedOverlaps
|
|
99
|
+
|
|
100
|
+
const nextOverlap = this.overlapQueue.shift()
|
|
101
|
+
|
|
102
|
+
if (nextOverlap) {
|
|
103
|
+
const traceToFix = this.allTraces.find(
|
|
104
|
+
(t) => t.mspPairId === nextOverlap.trace.mspPairId,
|
|
105
|
+
)
|
|
106
|
+
if (traceToFix) {
|
|
107
|
+
const labelId = nextOverlap.label.globalConnNetId
|
|
108
|
+
const detourCount = this.detourCountByLabel[labelId] || 0
|
|
109
|
+
this.detourCountByLabel[labelId] = detourCount + 1
|
|
110
|
+
|
|
111
|
+
this.activeSubSolver = new SingleOverlapSolver({
|
|
112
|
+
trace: traceToFix,
|
|
113
|
+
label: nextOverlap.label,
|
|
114
|
+
problem: this.inputProblem,
|
|
115
|
+
paddingBuffer: this.PADDING_BUFFER,
|
|
116
|
+
detourCount,
|
|
117
|
+
})
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
getOutput() {
|
|
123
|
+
return {
|
|
124
|
+
allTraces: this.allTraces,
|
|
125
|
+
modifiedTraces: this.modifiedTraces,
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
override visualize(): GraphicsObject {
|
|
130
|
+
if (this.activeSubSolver) {
|
|
131
|
+
return this.activeSubSolver.visualize()
|
|
132
|
+
}
|
|
133
|
+
// When idle, show all the traces
|
|
134
|
+
const graphics = visualizeInputProblem(this.inputProblem)
|
|
135
|
+
if (!graphics.lines) graphics.lines = []
|
|
136
|
+
for (const trace of this.allTraces) {
|
|
137
|
+
graphics.lines!.push({
|
|
138
|
+
points: trace.tracePath,
|
|
139
|
+
strokeColor: "purple",
|
|
140
|
+
})
|
|
141
|
+
}
|
|
142
|
+
return graphics
|
|
143
|
+
}
|
|
144
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import type { Point } from "@tscircuit/math-utils"
|
|
2
|
+
import { BaseSolver } from "lib/solvers/BaseSolver/BaseSolver"
|
|
3
|
+
import type { NetLabelPlacement } from "../../../NetLabelPlacementSolver/NetLabelPlacementSolver"
|
|
4
|
+
import type { InputProblem } from "lib/types/InputProblem"
|
|
5
|
+
import type { GraphicsObject } from "graphics-debug"
|
|
6
|
+
import type { SolvedTracePath } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceLinesSolver"
|
|
7
|
+
import { isPathCollidingWithObstacles } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver2/collisions"
|
|
8
|
+
import { getObstacleRects } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver2/rect"
|
|
9
|
+
import { visualizeInputProblem } from "lib/solvers/SchematicTracePipelineSolver/visualizeInputProblem"
|
|
10
|
+
import { generateRerouteCandidates } from "../../rerouteCollidingTrace"
|
|
11
|
+
import { simplifyPath } from "../TraceCleanupSolver/simplifyPath"
|
|
12
|
+
|
|
13
|
+
interface SingleOverlapSolverInput {
|
|
14
|
+
trace: SolvedTracePath
|
|
15
|
+
label: NetLabelPlacement
|
|
16
|
+
problem: InputProblem
|
|
17
|
+
paddingBuffer: number
|
|
18
|
+
detourCount: number
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* This solver attempts to find a valid rerouting for a single trace that is
|
|
23
|
+
* overlapping with a net label. It tries various candidate paths until it
|
|
24
|
+
* finds one that does not introduce new collisions.
|
|
25
|
+
*/
|
|
26
|
+
export class SingleOverlapSolver extends BaseSolver {
|
|
27
|
+
queuedCandidatePaths: Point[][]
|
|
28
|
+
solvedTracePath: Point[] | null = null
|
|
29
|
+
initialTrace: SolvedTracePath
|
|
30
|
+
problem: InputProblem
|
|
31
|
+
obstacles: ReturnType<typeof getObstacleRects>
|
|
32
|
+
label: NetLabelPlacement
|
|
33
|
+
|
|
34
|
+
constructor(solverInput: SingleOverlapSolverInput) {
|
|
35
|
+
super()
|
|
36
|
+
this.initialTrace = solverInput.trace
|
|
37
|
+
this.problem = solverInput.problem
|
|
38
|
+
this.label = solverInput.label
|
|
39
|
+
|
|
40
|
+
const candidates = generateRerouteCandidates({
|
|
41
|
+
...solverInput,
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
const getPathLength = (pts: Point[]) => {
|
|
45
|
+
let len = 0
|
|
46
|
+
for (let i = 0; i < pts.length - 1; i++) {
|
|
47
|
+
const dx = pts[i + 1].x - pts[i].x
|
|
48
|
+
const dy = pts[i + 1].y - pts[i].y
|
|
49
|
+
len += Math.sqrt(dx * dx + dy * dy)
|
|
50
|
+
}
|
|
51
|
+
return len
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
this.queuedCandidatePaths = candidates.sort(
|
|
55
|
+
(a, b) => getPathLength(a) - getPathLength(b),
|
|
56
|
+
)
|
|
57
|
+
this.obstacles = getObstacleRects(this.problem)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
override _step() {
|
|
61
|
+
if (this.queuedCandidatePaths.length === 0) {
|
|
62
|
+
this.failed = true
|
|
63
|
+
return
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const nextCandidatePath = this.queuedCandidatePaths.shift()!
|
|
67
|
+
const simplifiedPath = simplifyPath(nextCandidatePath)
|
|
68
|
+
|
|
69
|
+
if (!isPathCollidingWithObstacles(simplifiedPath, this.obstacles)) {
|
|
70
|
+
this.solvedTracePath = simplifiedPath
|
|
71
|
+
this.solved = true
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
override visualize(): GraphicsObject {
|
|
76
|
+
const graphics = visualizeInputProblem(this.problem, {
|
|
77
|
+
chipAlpha: 0.1,
|
|
78
|
+
connectionAlpha: 0.1,
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
if (!graphics.lines) graphics.lines = []
|
|
82
|
+
if (!graphics.rects) graphics.rects = []
|
|
83
|
+
|
|
84
|
+
// Draw initial trace
|
|
85
|
+
graphics.lines.push({
|
|
86
|
+
points: this.initialTrace.tracePath,
|
|
87
|
+
strokeColor: "red",
|
|
88
|
+
strokeDash: "4 4",
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
// Draw label
|
|
92
|
+
graphics.rects.push({
|
|
93
|
+
center: this.label.center,
|
|
94
|
+
width: this.label.width,
|
|
95
|
+
height: this.label.height,
|
|
96
|
+
fill: "rgba(255, 0, 0, 0.2)",
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
// Draw next candidate
|
|
100
|
+
if (this.queuedCandidatePaths.length > 0) {
|
|
101
|
+
graphics.lines.push({
|
|
102
|
+
points: this.queuedCandidatePaths[0],
|
|
103
|
+
strokeColor: "orange",
|
|
104
|
+
})
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Draw solved path
|
|
108
|
+
if (this.solvedTracePath) {
|
|
109
|
+
graphics.lines.push({
|
|
110
|
+
points: this.solvedTracePath,
|
|
111
|
+
strokeColor: "green",
|
|
112
|
+
})
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return graphics
|
|
116
|
+
}
|
|
117
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import type { InputProblem } from "lib/types/InputProblem"
|
|
2
|
+
import type { GraphicsObject, Line } from "graphics-debug"
|
|
3
|
+
import type { NetLabelPlacement } from "../../../NetLabelPlacementSolver/NetLabelPlacementSolver"
|
|
4
|
+
import { minimizeTurnsWithFilteredLabels } from "./minimizeTurnsWithFilteredLabels"
|
|
5
|
+
import { balanceLShapes } from "./balanceLShapes"
|
|
6
|
+
import { BaseSolver } from "lib/solvers/BaseSolver/BaseSolver"
|
|
7
|
+
import type { SolvedTracePath } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceLinesSolver"
|
|
8
|
+
import { visualizeInputProblem } from "lib/solvers/SchematicTracePipelineSolver/visualizeInputProblem"
|
|
9
|
+
|
|
10
|
+
interface TraceCleanupSolverInput {
|
|
11
|
+
inputProblem: InputProblem
|
|
12
|
+
allTraces: SolvedTracePath[]
|
|
13
|
+
targetTraceIds: Set<string>
|
|
14
|
+
allLabelPlacements: NetLabelPlacement[]
|
|
15
|
+
mergedLabelNetIdMap: Record<string, Set<string>>
|
|
16
|
+
paddingBuffer: number
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Cleans up traces by minimizing turns and balancing L-shapes to improve
|
|
21
|
+
* the overall aesthetics and readability of the schematic.
|
|
22
|
+
*/
|
|
23
|
+
export class TraceCleanupSolver extends BaseSolver {
|
|
24
|
+
private input: TraceCleanupSolverInput
|
|
25
|
+
private outputTraces: SolvedTracePath[]
|
|
26
|
+
private pipelineStepIndex = 0
|
|
27
|
+
private tracesToProcess: SolvedTracePath[]
|
|
28
|
+
private minimizedTraces: SolvedTracePath[] | null = null
|
|
29
|
+
private balancedTraces: SolvedTracePath[] | null = null
|
|
30
|
+
|
|
31
|
+
constructor(solverInput: TraceCleanupSolverInput) {
|
|
32
|
+
super()
|
|
33
|
+
this.input = solverInput
|
|
34
|
+
this.outputTraces = [...solverInput.allTraces]
|
|
35
|
+
this.tracesToProcess = this.outputTraces.filter((t) =>
|
|
36
|
+
this.input.targetTraceIds.has(t.mspPairId),
|
|
37
|
+
)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
override _step() {
|
|
41
|
+
const {
|
|
42
|
+
targetTraceIds,
|
|
43
|
+
inputProblem,
|
|
44
|
+
allLabelPlacements,
|
|
45
|
+
mergedLabelNetIdMap,
|
|
46
|
+
paddingBuffer,
|
|
47
|
+
} = this.input
|
|
48
|
+
|
|
49
|
+
if (targetTraceIds.size === 0) {
|
|
50
|
+
this.solved = true
|
|
51
|
+
return
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
switch (this.pipelineStepIndex) {
|
|
55
|
+
case 0: {
|
|
56
|
+
// Step 0: Minimize turns
|
|
57
|
+
const minimizedTracesResult = minimizeTurnsWithFilteredLabels({
|
|
58
|
+
traces: this.tracesToProcess,
|
|
59
|
+
inputProblem,
|
|
60
|
+
allLabelPlacements,
|
|
61
|
+
mergedLabelNetIdMap,
|
|
62
|
+
paddingBuffer,
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
this.minimizedTraces = minimizedTracesResult ?? this.tracesToProcess
|
|
66
|
+
|
|
67
|
+
const tracesMap = new Map(
|
|
68
|
+
this.input.allTraces.map((t) => [t.mspPairId, t]),
|
|
69
|
+
)
|
|
70
|
+
for (const trace of this.minimizedTraces) {
|
|
71
|
+
tracesMap.set(trace.mspPairId, trace)
|
|
72
|
+
}
|
|
73
|
+
this.outputTraces = Array.from(tracesMap.values())
|
|
74
|
+
this.pipelineStepIndex++
|
|
75
|
+
break
|
|
76
|
+
}
|
|
77
|
+
case 1: {
|
|
78
|
+
// Step 1: Balance L-shapes
|
|
79
|
+
const balancedTracesResult = balanceLShapes({
|
|
80
|
+
traces: this.minimizedTraces!,
|
|
81
|
+
inputProblem,
|
|
82
|
+
allLabelPlacements,
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
this.balancedTraces = balancedTracesResult ?? this.minimizedTraces
|
|
86
|
+
|
|
87
|
+
const tracesMap = new Map(
|
|
88
|
+
this.input.allTraces.map((t) => [t.mspPairId, t]),
|
|
89
|
+
)
|
|
90
|
+
for (const trace of this.balancedTraces!) {
|
|
91
|
+
tracesMap.set(trace.mspPairId, trace)
|
|
92
|
+
}
|
|
93
|
+
this.outputTraces = Array.from(tracesMap.values())
|
|
94
|
+
this.pipelineStepIndex++
|
|
95
|
+
break
|
|
96
|
+
}
|
|
97
|
+
case 2: {
|
|
98
|
+
// Step 2: Done
|
|
99
|
+
this.solved = true
|
|
100
|
+
break
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
getOutput() {
|
|
106
|
+
return {
|
|
107
|
+
traces: this.outputTraces,
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
override visualize(): GraphicsObject {
|
|
112
|
+
const graphics = visualizeInputProblem(this.input.inputProblem, {
|
|
113
|
+
chipAlpha: 0.1,
|
|
114
|
+
connectionAlpha: 0.1,
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
if (!graphics.lines) graphics.lines = []
|
|
118
|
+
if (!graphics.points) graphics.points = []
|
|
119
|
+
if (!graphics.rects) graphics.rects = []
|
|
120
|
+
if (!graphics.circles) graphics.circles = []
|
|
121
|
+
if (!graphics.texts) graphics.texts = []
|
|
122
|
+
|
|
123
|
+
for (const trace of this.outputTraces) {
|
|
124
|
+
const line: Line = {
|
|
125
|
+
points: trace.tracePath.map((p) => ({ x: p.x, y: p.y })),
|
|
126
|
+
strokeColor: "blue",
|
|
127
|
+
}
|
|
128
|
+
graphics.lines!.push(line)
|
|
129
|
+
}
|
|
130
|
+
return graphics
|
|
131
|
+
}
|
|
132
|
+
}
|
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
import type { Point } from "graphics-debug"
|
|
2
2
|
import type { InputProblem } from "lib/types/InputProblem"
|
|
3
|
-
import type { NetLabelPlacement } from "
|
|
4
|
-
import { getObstacleRects } from "../SchematicTraceLinesSolver/SchematicTraceSingleLineSolver2/rect"
|
|
5
|
-
import type { SolvedTracePath } from "../SchematicTraceLinesSolver/SchematicTraceLinesSolver"
|
|
6
|
-
import { segmentIntersectsRect } from "../SchematicTraceLinesSolver/SchematicTraceSingleLineSolver2/collisions"
|
|
3
|
+
import type { NetLabelPlacement } from "../../../NetLabelPlacementSolver/NetLabelPlacementSolver"
|
|
7
4
|
import { simplifyPath } from "./simplifyPath"
|
|
5
|
+
import type { SolvedTracePath } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceLinesSolver"
|
|
6
|
+
import { segmentIntersectsRect } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver2/collisions"
|
|
7
|
+
import { getObstacleRects } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver2/rect"
|
|
8
8
|
|
|
9
9
|
export const balanceLShapes = ({
|
|
10
10
|
traces,
|
|
11
|
-
|
|
11
|
+
inputProblem,
|
|
12
12
|
allLabelPlacements,
|
|
13
13
|
}: {
|
|
14
14
|
traces: SolvedTracePath[]
|
|
15
|
-
|
|
15
|
+
inputProblem: InputProblem
|
|
16
16
|
allLabelPlacements: NetLabelPlacement[]
|
|
17
17
|
}): SolvedTracePath[] | null => {
|
|
18
18
|
const TOLERANCE = 1e-5
|
|
@@ -46,7 +46,7 @@ export const balanceLShapes = ({
|
|
|
46
46
|
}
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
-
const obstacles = getObstacleRects(
|
|
49
|
+
const obstacles = getObstacleRects(inputProblem).map((obs) => ({
|
|
50
50
|
...obs,
|
|
51
51
|
minX: obs.minX + TOLERANCE,
|
|
52
52
|
maxX: obs.maxX - TOLERANCE,
|
|
@@ -81,7 +81,7 @@ export const balanceLShapes = ({
|
|
|
81
81
|
}
|
|
82
82
|
|
|
83
83
|
const newTraces = traces.map((trace) => {
|
|
84
|
-
|
|
84
|
+
const newPath = [...trace.tracePath]
|
|
85
85
|
|
|
86
86
|
if (newPath.length < 4) {
|
|
87
87
|
return { ...trace }
|
|
@@ -94,7 +94,8 @@ export const balanceLShapes = ({
|
|
|
94
94
|
|
|
95
95
|
if (newPath.length === 4) {
|
|
96
96
|
const [p0, p1, p2, p3] = newPath
|
|
97
|
-
let p1New: Point
|
|
97
|
+
let p1New: Point
|
|
98
|
+
let p2New: Point
|
|
98
99
|
|
|
99
100
|
const isHVHShape = p0.y === p1.y && p1.x === p2.x && p2.y === p3.y
|
|
100
101
|
|
|
@@ -154,7 +155,8 @@ export const balanceLShapes = ({
|
|
|
154
155
|
continue
|
|
155
156
|
}
|
|
156
157
|
|
|
157
|
-
let p2New: Point
|
|
158
|
+
let p2New: Point
|
|
159
|
+
let p3New: Point
|
|
158
160
|
const len1Original = isHVHZShape
|
|
159
161
|
? Math.abs(p1.x - p2.x)
|
|
160
162
|
: Math.abs(p1.y - p2.y)
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Point } from "graphics-debug"
|
|
2
|
-
import { segmentIntersectsRect } from "
|
|
2
|
+
import { segmentIntersectsRect } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver2/collisions"
|
|
3
3
|
|
|
4
4
|
export const hasCollisions = (
|
|
5
5
|
pathSegments: Point[],
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Point } from "@tscircuit/math-utils"
|
|
2
|
-
import { segmentIntersectsRect } from "
|
|
2
|
+
import { segmentIntersectsRect } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver2/collisions"
|
|
3
3
|
|
|
4
4
|
export const hasCollisionsWithLabels = (
|
|
5
5
|
pathSegments: Point[],
|