Skip to content

Commit f636a9e

Browse files
FranciscoFrancisco
authored andcommitted
feat: add SameNetTraceMergeSolver to combine close same-net trace segments
Adds a new pipeline phase that identifies same-net trace segments running in parallel within a small gap threshold (0.15 units) and merges them by averaging their fixed coordinate. This reduces visual clutter where the MSP tree creates multiple nearby parallel wires for the same electrical net. The solver runs after TraceCleanupSolver in the pipeline, before the final NetLabelPlacementSolver pass. Fixes #29
1 parent e71dc7f commit f636a9e

File tree

3 files changed

+384
-0
lines changed

3 files changed

+384
-0
lines changed
Lines changed: 204 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,204 @@
1+
import { BaseSolver } from "lib/solvers/BaseSolver/BaseSolver"
2+
import type { InputProblem } from "lib/types/InputProblem"
3+
import type { SolvedTracePath } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceLinesSolver"
4+
import type { GraphicsObject, Line } from "graphics-debug"
5+
import { visualizeInputProblem } from "lib/solvers/SchematicTracePipelineSolver/visualizeInputProblem"
6+
import type { Point } from "@tscircuit/math-utils"
7+
8+
/**
9+
* The gap threshold (in schematic units) within which two parallel same-net
10+
* segments are considered "close enough" to merge.
11+
*/
12+
const GAP_THRESHOLD = 0.15
13+
14+
interface SameNetTraceMergeSolverInput {
15+
inputProblem: InputProblem
16+
allTraces: SolvedTracePath[]
17+
}
18+
19+
interface Segment {
20+
traceIndex: number
21+
segIndex: number
22+
p1: Point
23+
p2: Point
24+
orientation: "horizontal" | "vertical"
25+
/** The fixed coordinate (y for horizontal, x for vertical) */
26+
fixedCoord: number
27+
/** min and max of the varying coordinate */
28+
rangeMin: number
29+
rangeMax: number
30+
}
31+
32+
/**
33+
* Pipeline phase that finds same-net trace segments running close together
34+
* (parallel, within GAP_THRESHOLD) and merges them into a single averaged
35+
* segment. This reduces visual clutter where the MSP tree creates multiple
36+
* nearby parallel wires for the same electrical net.
37+
*
38+
* Runs after TraceCleanupSolver in the pipeline.
39+
*/
40+
export class SameNetTraceMergeSolver extends BaseSolver {
41+
private input: SameNetTraceMergeSolverInput
42+
private outputTraces: SolvedTracePath[]
43+
private processed = false
44+
45+
constructor(solverInput: SameNetTraceMergeSolverInput) {
46+
super()
47+
this.input = solverInput
48+
this.outputTraces = solverInput.allTraces.map((t) => ({
49+
...t,
50+
tracePath: [...t.tracePath],
51+
}))
52+
}
53+
54+
override _step() {
55+
if (this.processed) {
56+
this.solved = true
57+
return
58+
}
59+
this.processed = true
60+
61+
// Group traces by globalConnNetId (same electrical net)
62+
const netGroups = new Map<string, number[]>()
63+
for (let i = 0; i < this.outputTraces.length; i++) {
64+
const trace = this.outputTraces[i]
65+
const netId = trace.globalConnNetId
66+
if (!netGroups.has(netId)) {
67+
netGroups.set(netId, [])
68+
}
69+
netGroups.get(netId)!.push(i)
70+
}
71+
72+
// For each net group with more than one trace, try to merge close segments
73+
for (const [_netId, traceIndices] of netGroups) {
74+
if (traceIndices.length < 2) continue
75+
this.mergeCloseSegmentsInGroup(traceIndices)
76+
}
77+
78+
this.solved = true
79+
}
80+
81+
private mergeCloseSegmentsInGroup(traceIndices: number[]) {
82+
// Extract all orthogonal segments from the traces in this group
83+
const segments: Segment[] = []
84+
85+
for (const traceIdx of traceIndices) {
86+
const path = this.outputTraces[traceIdx].tracePath
87+
for (let s = 0; s < path.length - 1; s++) {
88+
const p1 = path[s]
89+
const p2 = path[s + 1]
90+
const dx = Math.abs(p2.x - p1.x)
91+
const dy = Math.abs(p2.y - p1.y)
92+
93+
if (dx < 1e-9 && dy < 1e-9) continue // zero-length segment
94+
95+
if (dy < 1e-9) {
96+
// Horizontal segment
97+
segments.push({
98+
traceIndex: traceIdx,
99+
segIndex: s,
100+
p1,
101+
p2,
102+
orientation: "horizontal",
103+
fixedCoord: p1.y,
104+
rangeMin: Math.min(p1.x, p2.x),
105+
rangeMax: Math.max(p1.x, p2.x),
106+
})
107+
} else if (dx < 1e-9) {
108+
// Vertical segment
109+
segments.push({
110+
traceIndex: traceIdx,
111+
segIndex: s,
112+
p1,
113+
p2,
114+
orientation: "vertical",
115+
fixedCoord: p1.x,
116+
rangeMin: Math.min(p1.y, p2.y),
117+
rangeMax: Math.max(p1.y, p2.y),
118+
})
119+
}
120+
// Diagonal segments are ignored
121+
}
122+
}
123+
124+
// Find pairs of segments that are close and overlapping in range
125+
const merged = new Set<string>() // "traceIndex:segIndex" keys already merged
126+
127+
for (let i = 0; i < segments.length; i++) {
128+
for (let j = i + 1; j < segments.length; j++) {
129+
const a = segments[i]
130+
const b = segments[j]
131+
132+
// Must be same orientation, different traces
133+
if (a.orientation !== b.orientation) continue
134+
if (a.traceIndex === b.traceIndex) continue
135+
136+
const gap = Math.abs(a.fixedCoord - b.fixedCoord)
137+
if (gap > GAP_THRESHOLD) continue
138+
if (gap < 1e-9) continue // already coincident, no merge needed
139+
140+
// Check that their ranges overlap
141+
const overlapMin = Math.max(a.rangeMin, b.rangeMin)
142+
const overlapMax = Math.min(a.rangeMax, b.rangeMax)
143+
if (overlapMax - overlapMin < 1e-9) continue // no meaningful overlap
144+
145+
const keyA = `${a.traceIndex}:${a.segIndex}`
146+
const keyB = `${b.traceIndex}:${b.segIndex}`
147+
if (merged.has(keyA) || merged.has(keyB)) continue
148+
149+
// Merge: average the fixed coordinate on both segments
150+
const avgCoord = (a.fixedCoord + b.fixedCoord) / 2
151+
152+
this.shiftSegmentFixedCoord(a, avgCoord)
153+
this.shiftSegmentFixedCoord(b, avgCoord)
154+
155+
merged.add(keyA)
156+
merged.add(keyB)
157+
}
158+
}
159+
}
160+
161+
/**
162+
* Shifts a segment's fixed coordinate to a new value, updating the trace
163+
* path points in place. Also updates the adjacent points so that the path
164+
* remains connected (perpendicular legs stretch/shrink).
165+
*/
166+
private shiftSegmentFixedCoord(seg: Segment, newFixedCoord: number) {
167+
const path = this.outputTraces[seg.traceIndex].tracePath
168+
const s = seg.segIndex
169+
170+
if (seg.orientation === "horizontal") {
171+
// Shift y of both endpoints
172+
path[s] = { x: path[s].x, y: newFixedCoord }
173+
path[s + 1] = { x: path[s + 1].x, y: newFixedCoord }
174+
} else {
175+
// Shift x of both endpoints
176+
path[s] = { x: newFixedCoord, y: path[s].y }
177+
path[s + 1] = { x: newFixedCoord, y: path[s + 1].y }
178+
}
179+
}
180+
181+
getOutput() {
182+
return {
183+
traces: this.outputTraces,
184+
}
185+
}
186+
187+
override visualize(): GraphicsObject {
188+
const graphics = visualizeInputProblem(this.input.inputProblem, {
189+
chipAlpha: 0.1,
190+
connectionAlpha: 0.1,
191+
})
192+
193+
if (!graphics.lines) graphics.lines = []
194+
195+
for (const trace of this.outputTraces) {
196+
const line: Line = {
197+
points: trace.tracePath.map((p) => ({ x: p.x, y: p.y })),
198+
strokeColor: "blue",
199+
}
200+
graphics.lines.push(line)
201+
}
202+
return graphics
203+
}
204+
}

lib/solvers/SchematicTracePipelineSolver/SchematicTracePipelineSolver.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import { expandChipsToFitPins } from "./expandChipsToFitPins"
2020
import { LongDistancePairSolver } from "../LongDistancePairSolver/LongDistancePairSolver"
2121
import { MergedNetLabelObstacleSolver } from "../TraceLabelOverlapAvoidanceSolver/sub-solvers/LabelMergingSolver/LabelMergingSolver"
2222
import { TraceCleanupSolver } from "../TraceCleanupSolver/TraceCleanupSolver"
23+
import { SameNetTraceMergeSolver } from "../SameNetTraceMergeSolver/SameNetTraceMergeSolver"
2324

2425
type PipelineStep<T extends new (...args: any[]) => BaseSolver> = {
2526
solverName: string
@@ -69,6 +70,7 @@ export class SchematicTracePipelineSolver extends BaseSolver {
6970
labelMergingSolver?: MergedNetLabelObstacleSolver
7071
traceLabelOverlapAvoidanceSolver?: TraceLabelOverlapAvoidanceSolver
7172
traceCleanupSolver?: TraceCleanupSolver
73+
sameNetTraceMergeSolver?: SameNetTraceMergeSolver
7274

7375
startTimeOfPhase: Record<string, number>
7476
endTimeOfPhase: Record<string, number>
@@ -206,11 +208,27 @@ export class SchematicTracePipelineSolver extends BaseSolver {
206208
},
207209
]
208210
}),
211+
definePipelineStep(
212+
"sameNetTraceMergeSolver",
213+
SameNetTraceMergeSolver,
214+
(instance) => {
215+
const traces =
216+
instance.traceCleanupSolver?.getOutput().traces ??
217+
instance.traceLabelOverlapAvoidanceSolver!.getOutput().traces
218+
return [
219+
{
220+
inputProblem: instance.inputProblem,
221+
allTraces: traces,
222+
},
223+
]
224+
},
225+
),
209226
definePipelineStep(
210227
"netLabelPlacementSolver",
211228
NetLabelPlacementSolver,
212229
(instance) => {
213230
const traces =
231+
instance.sameNetTraceMergeSolver?.getOutput().traces ??
214232
instance.traceCleanupSolver?.getOutput().traces ??
215233
instance.traceLabelOverlapAvoidanceSolver!.getOutput().traces
216234

0 commit comments

Comments
 (0)