Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 15 additions & 2 deletions lib/solvers/TraceCleanupSolver/TraceCleanupSolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type { InputProblem } from "lib/types/InputProblem"
import type { GraphicsObject, Line } from "graphics-debug"
import { minimizeTurnsWithFilteredLabels } from "./minimizeTurnsWithFilteredLabels"
import { balanceZShapes } from "./balanceZShapes"
import { mergeSameNetCloseTraces } from "./mergeSameNetCloseTraces"
import { BaseSolver } from "lib/solvers/BaseSolver/BaseSolver"
import type { SolvedTracePath } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceLinesSolver"
import { visualizeInputProblem } from "lib/solvers/SchematicTracePipelineSolver/visualizeInputProblem"
Expand All @@ -25,6 +26,7 @@ import { is4PointRectangle } from "./is4PointRectangle"
* Represents the different stages or steps within the trace cleanup pipeline.
*/
type PipelineStep =
| "merging_same_net_traces"
| "minimizing_turns"
| "balancing_l_shapes"
| "untangling_traces"
Expand Down Expand Up @@ -66,10 +68,10 @@ export class TraceCleanupSolver extends BaseSolver {
this.outputTraces = output.traces
this.tracesMap = new Map(this.outputTraces.map((t) => [t.mspPairId, t]))
this.activeSubSolver = null
this.pipelineStep = "minimizing_turns"
this.pipelineStep = "merging_same_net_traces"
} else if (this.activeSubSolver.failed) {
this.activeSubSolver = null
this.pipelineStep = "minimizing_turns"
this.pipelineStep = "merging_same_net_traces"
}
return
}
Expand All @@ -78,6 +80,9 @@ export class TraceCleanupSolver extends BaseSolver {
case "untangling_traces":
this._runUntangleTracesStep()
break
case "merging_same_net_traces":
this._runMergeSameNetTracesStep()
break
case "minimizing_turns":
this._runMinimizeTurnsStep()
break
Expand All @@ -87,6 +92,14 @@ export class TraceCleanupSolver extends BaseSolver {
}
}

private _runMergeSameNetTracesStep() {
this.outputTraces = mergeSameNetCloseTraces(
Array.from(this.tracesMap.values()),
)
this.tracesMap = new Map(this.outputTraces.map((t) => [t.mspPairId, t]))
this.pipelineStep = "minimizing_turns"
}

private _runUntangleTracesStep() {
this.activeSubSolver = new UntangleTraceSubsolver({
...this.input,
Expand Down
117 changes: 117 additions & 0 deletions lib/solvers/TraceCleanupSolver/mergeSameNetCloseTraces.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
import type { SolvedTracePath } from "../SchematicTraceLinesSolver/SchematicTraceLinesSolver"

const EPS = 2e-3

/**
* Default maximum perpendicular distance for two parallel same-net segments
* to be considered "close together" and therefore mergeable.
*/
const DEFAULT_CLOSE_THRESHOLD = 0.15

interface InternalSegment {
traceIndex: number
pointIndex: number
isHorizontal: boolean
/** Y for horizontal segments, X for vertical segments */
coord: number
/** Lower bound on the parallel axis */
parallelStart: number
/** Upper bound on the parallel axis */
parallelEnd: number
}

/**
* Aligns parallel segments from same-net traces that are close together but
* not perfectly co-linear so they share a common Y (horizontal) or X
* (vertical) coordinate. Only segments that are strictly internal (neither
* endpoint is a pin endpoint of the trace path) are considered so that
* adjustments cannot detach a trace from its pin.
*/
export function mergeSameNetCloseTraces(
traces: SolvedTracePath[],
closeThreshold: number = DEFAULT_CLOSE_THRESHOLD,
): SolvedTracePath[] {
const newTraces = traces.map((t) => ({
...t,
tracePath: t.tracePath.map((p) => ({ ...p })),
}))

const groupsByNet: Record<string, number[]> = {}
newTraces.forEach((t, i) => {
if (!groupsByNet[t.globalConnNetId]) groupsByNet[t.globalConnNetId] = []
groupsByNet[t.globalConnNetId]!.push(i)
})

for (const traceIndices of Object.values(groupsByNet)) {
if (traceIndices.length < 2) continue

const collectInternalSegments = (): InternalSegment[] => {
const segments: InternalSegment[] = []
for (const ti of traceIndices) {
const path = newTraces[ti]!.tracePath
// Segments touching path endpoints connect to pins; skip them so we
// never alter pin connections.
for (let pi = 1; pi < path.length - 2; pi++) {
const p1 = path[pi]!
const p2 = path[pi + 1]!
const isHorz =
Math.abs(p1.y - p2.y) < EPS && Math.abs(p1.x - p2.x) >= EPS
const isVert =
Math.abs(p1.x - p2.x) < EPS && Math.abs(p1.y - p2.y) >= EPS
if (!isHorz && !isVert) continue
segments.push({
traceIndex: ti,
pointIndex: pi,
isHorizontal: isHorz,
coord: isHorz ? (p1.y + p2.y) / 2 : (p1.x + p2.x) / 2,
parallelStart: isHorz ? Math.min(p1.x, p2.x) : Math.min(p1.y, p2.y),
parallelEnd: isHorz ? Math.max(p1.x, p2.x) : Math.max(p1.y, p2.y),
})
}
}
return segments
}

let changed = true
let safetyIterations = 0
while (changed && safetyIterations < 32) {
changed = false
safetyIterations++
const segments = collectInternalSegments()

for (let i = 0; i < segments.length; i++) {
for (let j = i + 1; j < segments.length; j++) {
const a = segments[i]!
const b = segments[j]!
if (a.traceIndex === b.traceIndex) continue
if (a.isHorizontal !== b.isHorizontal) continue

const parallelOverlap =
Math.min(a.parallelEnd, b.parallelEnd) -
Math.max(a.parallelStart, b.parallelStart)
if (parallelOverlap <= EPS) continue

const perpDist = Math.abs(a.coord - b.coord)
if (perpDist < EPS) continue
if (perpDist > closeThreshold) continue

const newCoord = (a.coord + b.coord) / 2

const apply = (s: InternalSegment) => {
const path = newTraces[s.traceIndex]!.tracePath
const key: "x" | "y" = s.isHorizontal ? "y" : "x"
path[s.pointIndex]![key] = newCoord
path[s.pointIndex + 1]![key] = newCoord
s.coord = newCoord
}

apply(a)
apply(b)
changed = true
}
}
}
}

return newTraces
}
151 changes: 151 additions & 0 deletions tests/solvers/TraceCleanupSolver/mergeSameNetCloseTraces.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
import { test, expect } from "bun:test"
import { mergeSameNetCloseTraces } from "lib/solvers/TraceCleanupSolver/mergeSameNetCloseTraces"
import type { SolvedTracePath } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceLinesSolver"

const makeTrace = (
mspPairId: string,
globalConnNetId: string,
tracePath: { x: number; y: number }[],
): SolvedTracePath =>
({
mspPairId,
globalConnNetId,
dcConnNetId: globalConnNetId,
pins: [],
tracePath,
mspConnectionPairIds: [mspPairId],
pinIds: [],
}) as unknown as SolvedTracePath

test("merges two parallel same-net horizontal segments that are close", () => {
// Two same-net traces with nearly aligned horizontal middle segments.
// path A goes (0,0) -> (0,1) -> (5,1) -> (5,2)
// path B goes (0,0) -> (0,1.05) -> (5,1.05) -> (5,2)
const traces = [
makeTrace("a", "net1", [
{ x: 0, y: 0 },
{ x: 0, y: 1 },
{ x: 5, y: 1 },
{ x: 5, y: 2 },
]),
makeTrace("b", "net1", [
{ x: 0, y: 0 },
{ x: 0, y: 1.05 },
{ x: 5, y: 1.05 },
{ x: 5, y: 2 },
]),
]

const merged = mergeSameNetCloseTraces(traces)

// The middle horizontal segments should now share the same Y value.
const aMidY = merged[0]!.tracePath[1]!.y
const bMidY = merged[1]!.tracePath[1]!.y
expect(Math.abs(aMidY - bMidY)).toBeLessThan(1e-6)
expect(merged[0]!.tracePath[2]!.y).toBeCloseTo(aMidY)
expect(merged[1]!.tracePath[2]!.y).toBeCloseTo(bMidY)
})

test("merges two parallel same-net vertical segments that are close", () => {
const traces = [
makeTrace("a", "net1", [
{ x: 0, y: 0 },
{ x: 1, y: 0 },
{ x: 1, y: 5 },
{ x: 2, y: 5 },
]),
makeTrace("b", "net1", [
{ x: 0, y: 0 },
{ x: 1.05, y: 0 },
{ x: 1.05, y: 5 },
{ x: 2, y: 5 },
]),
]

const merged = mergeSameNetCloseTraces(traces)
expect(
Math.abs(merged[0]!.tracePath[1]!.x - merged[1]!.tracePath[1]!.x),
).toBeLessThan(1e-6)
})

test("does not merge segments from different nets", () => {
const traces = [
makeTrace("a", "net1", [
{ x: 0, y: 0 },
{ x: 0, y: 1 },
{ x: 5, y: 1 },
{ x: 5, y: 2 },
]),
makeTrace("b", "net2", [
{ x: 0, y: 0 },
{ x: 0, y: 1.05 },
{ x: 5, y: 1.05 },
{ x: 5, y: 2 },
]),
]

const merged = mergeSameNetCloseTraces(traces)
expect(merged[0]!.tracePath[1]!.y).toBe(1)
expect(merged[1]!.tracePath[1]!.y).toBe(1.05)
})

test("does not merge segments that are too far apart", () => {
const traces = [
makeTrace("a", "net1", [
{ x: 0, y: 0 },
{ x: 0, y: 1 },
{ x: 5, y: 1 },
{ x: 5, y: 2 },
]),
makeTrace("b", "net1", [
{ x: 0, y: 0 },
{ x: 0, y: 3 },
{ x: 5, y: 3 },
{ x: 5, y: 4 },
]),
]

const merged = mergeSameNetCloseTraces(traces)
expect(merged[0]!.tracePath[1]!.y).toBe(1)
expect(merged[1]!.tracePath[1]!.y).toBe(3)
})

test("does not move endpoint segments connected to pins", () => {
// Both traces only have endpoint segments — should not be modified.
const traces = [
makeTrace("a", "net1", [
{ x: 0, y: 1 },
{ x: 5, y: 1 },
]),
makeTrace("b", "net1", [
{ x: 0, y: 1.05 },
{ x: 5, y: 1.05 },
]),
]

const merged = mergeSameNetCloseTraces(traces)
expect(merged[0]!.tracePath[0]!.y).toBe(1)
expect(merged[1]!.tracePath[0]!.y).toBe(1.05)
})

test("does not merge non-overlapping parallel segments", () => {
// Same-net, parallel, close Y, but x-ranges do not overlap.
const traces = [
makeTrace("a", "net1", [
{ x: 0, y: 0 },
{ x: 0, y: 1 },
{ x: 2, y: 1 },
{ x: 2, y: 2 },
]),
makeTrace("b", "net1", [
{ x: 3, y: 0 },
{ x: 3, y: 1.05 },
{ x: 5, y: 1.05 },
{ x: 5, y: 2 },
]),
]

const merged = mergeSameNetCloseTraces(traces)
expect(merged[0]!.tracePath[1]!.y).toBe(1)
expect(merged[1]!.tracePath[1]!.y).toBe(1.05)
})
Loading