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
15 changes: 11 additions & 4 deletions lib/solvers/TraceCleanupSolver/TraceCleanupSolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ interface TraceCleanupSolverInput {

import { UntangleTraceSubsolver } from "./sub-solver/UntangleTraceSubsolver"
import { is4PointRectangle } from "./is4PointRectangle"
import { removeNetSegmentDuplicates } from "./removeNetSegmentDuplicates"
import { removeDuplicateConsecutivePoints } from "./simplifyPath"

/**
* Represents the different stages or steps within the trace cleanup pipeline.
Expand Down Expand Up @@ -49,11 +51,16 @@ export class TraceCleanupSolver extends BaseSolver {
constructor(solverInput: TraceCleanupSolverInput) {
super()
this.input = solverInput
this.outputTraces = [...solverInput.allTraces]

// Preprocessing: remove duplicate consecutive points (zero-length segments)
const cleanedTraces = solverInput.allTraces.map((trace) => ({
...trace,
tracePath: removeDuplicateConsecutivePoints(trace.tracePath),
}))

this.outputTraces = [...cleanedTraces]
this.tracesMap = new Map(this.outputTraces.map((t) => [t.mspPairId, t]))
this.traceIdQueue = Array.from(
solverInput.allTraces.map((e) => e.mspPairId),
)
this.traceIdQueue = Array.from(cleanedTraces.map((e) => e.mspPairId))
}

override _step() {
Expand Down
114 changes: 114 additions & 0 deletions lib/solvers/TraceCleanupSolver/removeNetSegmentDuplicates.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
import type { SolvedTracePath } from "../SchematicTraceLinesSolver/SchematicTraceLinesSolver"

const EPSILON = 1e-9

/**
* Creates a canonical (direction-independent) key for a segment defined by two points.
* Sorts the endpoints so that the key is the same regardless of traversal direction.
*/
function getSegmentKey(ax: number, ay: number, bx: number, by: number): string {
// Sort endpoints: first by x, then by y
if (ax < bx - EPSILON || (Math.abs(ax - bx) < EPSILON && ay < by - EPSILON)) {
return `${ax.toFixed(9)},${ay.toFixed(9)}-${bx.toFixed(9)},${by.toFixed(9)}`
}
return `${bx.toFixed(9)},${by.toFixed(9)}-${ax.toFixed(9)},${ay.toFixed(9)}`
}

/**
* Removes duplicate trace segments that appear when multiple MSP connection pairs
* in the same net share a pin endpoint and independently route through the same
* physical segment near that shared pin.
*
* For each net, the first trace that uses a segment keeps it; subsequent traces
* that start or end with the same segment have those endpoint segments trimmed.
*
* Only endpoint segments (first or last) are candidates for trimming, since
* those are the ones that overlap near shared pin endpoints.
*/
export function removeNetSegmentDuplicates(
traces: SolvedTracePath[],
): SolvedTracePath[] {
// Group traces by globalConnNetId
const tracesByNet = new Map<string, SolvedTracePath[]>()
for (const trace of traces) {
const netId = trace.globalConnNetId
if (!tracesByNet.has(netId)) {
tracesByNet.set(netId, [])
}
tracesByNet.get(netId)!.push(trace)
}

const result: SolvedTracePath[] = []

for (const [_netId, netTraces] of tracesByNet) {
if (netTraces.length <= 1) {
result.push(...netTraces)
continue
}

// Collect all segments from all traces in this net, tracking which trace
// index first claimed each segment
const claimedSegments = new Set<string>()

// First pass: register all segments from the first trace (it keeps everything)
const firstTrace = netTraces[0]
for (let i = 0; i < firstTrace.tracePath.length - 1; i++) {
const p1 = firstTrace.tracePath[i]
const p2 = firstTrace.tracePath[i + 1]
claimedSegments.add(getSegmentKey(p1.x, p1.y, p2.x, p2.y))
}
result.push(firstTrace)

// Second pass: for subsequent traces, trim duplicate endpoint segments
for (let t = 1; t < netTraces.length; t++) {
const trace = netTraces[t]
const path = [...trace.tracePath]

// Trim from the start: remove leading segments that are duplicates
while (path.length >= 2) {
const key = getSegmentKey(path[0].x, path[0].y, path[1].x, path[1].y)
if (claimedSegments.has(key)) {
path.shift()
} else {
break
}
}

// Trim from the end: remove trailing segments that are duplicates
while (path.length >= 2) {
const key = getSegmentKey(
path[path.length - 2].x,
path[path.length - 2].y,
path[path.length - 1].x,
path[path.length - 1].y,
)
if (claimedSegments.has(key)) {
path.pop()
} else {
break
}
}

// Register this trace's remaining segments as claimed
for (let i = 0; i < path.length - 1; i++) {
const p1 = path[i]
const p2 = path[i + 1]
claimedSegments.add(getSegmentKey(p1.x, p1.y, p2.x, p2.y))
}

// Only keep traces that still have at least 2 points (a valid segment)
if (path.length >= 2) {
result.push({
...trace,
tracePath: path,
})
} else {
// Trace was entirely duplicated; still include it but with original path
// to avoid breaking downstream references
result.push(trace)
}
}
}

return result
}
22 changes: 22 additions & 0 deletions lib/solvers/TraceCleanupSolver/simplifyPath.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,28 @@ import {
isVertical,
} from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver2/collisions"

const EPSILON = 1e-9

/**
* Removes consecutive duplicate points (zero-length segments) from a path.
* These can appear after path concatenation in the UntangleTraceSubsolver.
*/
export const removeDuplicateConsecutivePoints = (path: Point[]): Point[] => {
if (path.length <= 1) return path
const result: Point[] = [path[0]]
for (let i = 1; i < path.length; i++) {
const prev = result[result.length - 1]
const curr = path[i]
if (
Math.abs(prev.x - curr.x) > EPSILON ||
Math.abs(prev.y - curr.y) > EPSILON
) {
result.push(curr)
}
}
return result
}

export const simplifyPath = (path: Point[]): Point[] => {
if (path.length < 3) return path
const newPath: Point[] = [path[0]]
Expand Down
134 changes: 134 additions & 0 deletions tests/solvers/TraceCleanupSolver/removeNetSegmentDuplicates.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
import { test, expect } from "bun:test"
import { removeNetSegmentDuplicates } from "lib/solvers/TraceCleanupSolver/removeNetSegmentDuplicates"
import type { SolvedTracePath } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceLinesSolver"

function makeTrace(
id: string,
netId: string,
points: Array<{ x: number; y: number }>,
): SolvedTracePath {
return {
mspPairId: id,
dcConnNetId: netId,
globalConnNetId: netId,
pins: [
{ pinId: "p1", x: points[0].x, y: points[0].y, chipId: "c1" },
{
pinId: "p2",
x: points[points.length - 1].x,
y: points[points.length - 1].y,
chipId: "c2",
},
] as any,
tracePath: points,
mspConnectionPairIds: [id],
pinIds: ["p1", "p2"],
}
}

test("removeNetSegmentDuplicates: single trace unchanged", () => {
const traces = [
makeTrace("t1", "net1", [
{ x: 0, y: 0 },
{ x: 1, y: 0 },
{ x: 1, y: 1 },
]),
]
const result = removeNetSegmentDuplicates(traces)
expect(result).toHaveLength(1)
expect(result[0].tracePath).toHaveLength(3)
})

test("removeNetSegmentDuplicates: different nets are not deduplicated", () => {
const traces = [
makeTrace("t1", "net1", [
{ x: 0, y: 0 },
{ x: 1, y: 0 },
]),
makeTrace("t2", "net2", [
{ x: 0, y: 0 },
{ x: 1, y: 0 },
]),
]
const result = removeNetSegmentDuplicates(traces)
expect(result).toHaveLength(2)
expect(result[0].tracePath).toHaveLength(2)
expect(result[1].tracePath).toHaveLength(2)
})

test("removeNetSegmentDuplicates: shared leading segment is trimmed", () => {
// Two traces in the same net share a starting segment (0,0)-(1,0)
const traces = [
makeTrace("t1", "net1", [
{ x: 0, y: 0 },
{ x: 1, y: 0 },
{ x: 1, y: 1 },
]),
makeTrace("t2", "net1", [
{ x: 0, y: 0 },
{ x: 1, y: 0 },
{ x: 1, y: -1 },
]),
]
const result = removeNetSegmentDuplicates(traces)
expect(result).toHaveLength(2)
// First trace keeps all segments
expect(result[0].tracePath).toHaveLength(3)
// Second trace has the shared leading segment trimmed
expect(result[1].tracePath).toHaveLength(2)
// Second trace should start at (1,0) instead of (0,0)
expect(result[1].tracePath[0]).toEqual({ x: 1, y: 0 })
})

test("removeNetSegmentDuplicates: shared trailing segment is trimmed", () => {
// Two traces in the same net share an ending segment (1,0)-(2,0)
const traces = [
makeTrace("t1", "net1", [
{ x: 0, y: 1 },
{ x: 1, y: 0 },
{ x: 2, y: 0 },
]),
makeTrace("t2", "net1", [
{ x: 0, y: -1 },
{ x: 1, y: 0 },
{ x: 2, y: 0 },
]),
]
const result = removeNetSegmentDuplicates(traces)
expect(result).toHaveLength(2)
// First trace keeps all segments
expect(result[0].tracePath).toHaveLength(3)
// Second trace has the shared trailing segment trimmed
expect(result[1].tracePath).toHaveLength(2)
// Second trace should end at (1,0) instead of (2,0)
expect(result[1].tracePath[result[1].tracePath.length - 1]).toEqual({
x: 1,
y: 0,
})
})

test("removeNetSegmentDuplicates: reverse direction segment is detected", () => {
// Two traces share a segment but traversed in opposite directions
const traces = [
makeTrace("t1", "net1", [
{ x: 0, y: 0 },
{ x: 1, y: 0 },
{ x: 1, y: 1 },
]),
makeTrace("t2", "net1", [
{ x: 1, y: -1 },
{ x: 1, y: 0 },
{ x: 0, y: 0 },
]),
]
const result = removeNetSegmentDuplicates(traces)
expect(result).toHaveLength(2)
// First trace unchanged
expect(result[0].tracePath).toHaveLength(3)
// Second trace should have the shared trailing segment (1,0)-(0,0) trimmed
expect(result[1].tracePath).toHaveLength(2)
expect(result[1].tracePath[result[1].tracePath.length - 1]).toEqual({
x: 1,
y: 0,
})
})
Loading