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
Binary file added demos/pr-392-same-net-trace-cleanup-demo.mp4
Binary file not shown.
27 changes: 21 additions & 6 deletions lib/solvers/TraceCleanupSolver/TraceCleanupSolver.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import type { InputProblem } from "lib/types/InputProblem"
import type { GraphicsObject, Line } from "graphics-debug"
import { minimizeTurnsWithFilteredLabels } from "./minimizeTurnsWithFilteredLabels"
import { balanceZShapes } from "./balanceZShapes"
import { BaseSolver } from "lib/solvers/BaseSolver/BaseSolver"
import type { SolvedTracePath } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceLinesSolver"
import { visualizeInputProblem } from "lib/solvers/SchematicTracePipelineSolver/visualizeInputProblem"
import type { InputProblem } from "lib/types/InputProblem"
import type { NetLabelPlacement } from "../NetLabelPlacementSolver/NetLabelPlacementSolver"
import { balanceZShapes } from "./balanceZShapes"
import { minimizeTurnsWithFilteredLabels } from "./minimizeTurnsWithFilteredLabels"

/**
* Defines the input structure for the TraceCleanupSolver.
Expand All @@ -18,13 +18,15 @@ interface TraceCleanupSolverInput {
paddingBuffer: number
}

import { UntangleTraceSubsolver } from "./sub-solver/UntangleTraceSubsolver"
import { combineCloseSameNetSegments } from "./combineCloseSameNetSegments"
import { is4PointRectangle } from "./is4PointRectangle"
import { UntangleTraceSubsolver } from "./sub-solver/UntangleTraceSubsolver"

/**
* Represents the different stages or steps within the trace cleanup pipeline.
*/
type PipelineStep =
| "combining_same_net_segments"
| "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 = "combining_same_net_segments"
} else if (this.activeSubSolver.failed) {
this.activeSubSolver = null
this.pipelineStep = "minimizing_turns"
this.pipelineStep = "combining_same_net_segments"
}
return
}
Expand All @@ -78,6 +80,9 @@ export class TraceCleanupSolver extends BaseSolver {
case "untangling_traces":
this._runUntangleTracesStep()
break
case "combining_same_net_segments":
this._runCombineCloseSameNetSegmentsStep()
break
case "minimizing_turns":
this._runMinimizeTurnsStep()
break
Expand All @@ -94,6 +99,16 @@ export class TraceCleanupSolver extends BaseSolver {
})
}

private _runCombineCloseSameNetSegmentsStep() {
this.outputTraces = combineCloseSameNetSegments(
Array.from(this.tracesMap.values()),
this.input.paddingBuffer,
)
this.tracesMap = new Map(this.outputTraces.map((t) => [t.mspPairId, t]))
this.traceIdQueue = Array.from(this.outputTraces.map((e) => e.mspPairId))
this.pipelineStep = "minimizing_turns"
}

private _runMinimizeTurnsStep() {
if (this.traceIdQueue.length === 0) {
this.pipelineStep = "balancing_l_shapes"
Expand Down
194 changes: 194 additions & 0 deletions lib/solvers/TraceCleanupSolver/combineCloseSameNetSegments.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
import type { Point } from "@tscircuit/math-utils"
import type { SolvedTracePath } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceLinesSolver"
import { simplifyPath } from "./simplifyPath"

const EPSILON = 1e-9
const MAX_PASSES = 20

type SegmentOrientation = "horizontal" | "vertical"

interface TraceSegment {
traceIndex: number
pointIndex: number
orientation: SegmentOrientation
fixedCoord: number
minCoord: number
maxCoord: number
length: number
isTerminal: boolean
}

const getTraceNetKey = (trace: SolvedTracePath): string =>
trace.userNetId ?? trace.globalConnNetId ?? trace.dcConnNetId

const getSegmentsForTrace = (
trace: SolvedTracePath,
traceIndex: number,
): TraceSegment[] => {
const segments: TraceSegment[] = []

for (
let pointIndex = 0;
pointIndex < trace.tracePath.length - 1;
pointIndex++
) {
const start = trace.tracePath[pointIndex]!
const end = trace.tracePath[pointIndex + 1]!

if (Math.abs(start.y - end.y) <= EPSILON) {
const minCoord = Math.min(start.x, end.x)
const maxCoord = Math.max(start.x, end.x)
const length = maxCoord - minCoord
if (length <= EPSILON) continue
segments.push({
traceIndex,
pointIndex,
orientation: "horizontal",
fixedCoord: start.y,
minCoord,
maxCoord,
length,
isTerminal:
pointIndex === 0 || pointIndex === trace.tracePath.length - 2,
})
} else if (Math.abs(start.x - end.x) <= EPSILON) {
const minCoord = Math.min(start.y, end.y)
const maxCoord = Math.max(start.y, end.y)
const length = maxCoord - minCoord
if (length <= EPSILON) continue
segments.push({
traceIndex,
pointIndex,
orientation: "vertical",
fixedCoord: start.x,
minCoord,
maxCoord,
length,
isTerminal:
pointIndex === 0 || pointIndex === trace.tracePath.length - 2,
})
}
}

return segments
}

const hasProjectionOverlap = (a: TraceSegment, b: TraceSegment): boolean => {
const overlap =
Math.min(a.maxCoord, b.maxCoord) - Math.max(a.minCoord, b.minCoord)
return overlap > EPSILON
}

const shouldCombineSegments = (
a: TraceSegment,
b: TraceSegment,
maxDistance: number,
): boolean => {
if (a.traceIndex === b.traceIndex) return false
if (a.orientation !== b.orientation) return false
if (a.isTerminal || b.isTerminal) return false
if (!hasProjectionOverlap(a, b)) return false

const distance = Math.abs(a.fixedCoord - b.fixedCoord)
return distance > EPSILON && distance <= maxDistance
}

const wouldOverlapDifferentNetSegment = (
traces: SolvedTracePath[],
target: TraceSegment,
targetNetKey: string,
fixedCoord: number,
): boolean => {
for (let traceIndex = 0; traceIndex < traces.length; traceIndex++) {
const trace = traces[traceIndex]!
if (getTraceNetKey(trace) === targetNetKey) continue

for (const segment of getSegmentsForTrace(trace, traceIndex)) {
if (segment.orientation !== target.orientation) continue
if (Math.abs(segment.fixedCoord - fixedCoord) > EPSILON) continue
if (hasProjectionOverlap(segment, target)) return true
}
}

return false
}

const snapSegmentFixedCoord = (
trace: SolvedTracePath,
segment: TraceSegment,
fixedCoord: number,
): SolvedTracePath => {
const tracePath = trace.tracePath.map((point) => ({ ...point }))
const start = tracePath[segment.pointIndex]!
const end = tracePath[segment.pointIndex + 1]!

if (segment.orientation === "horizontal") {
start.y = fixedCoord
end.y = fixedCoord
} else {
start.x = fixedCoord
end.x = fixedCoord
}

return {
...trace,
tracePath: simplifyPath(tracePath as Point[]),
}
}

export const combineCloseSameNetSegments = (
traces: SolvedTracePath[],
maxDistance = 0.1,
): SolvedTracePath[] => {
const outputTraces = traces.map((trace) => ({
...trace,
tracePath: trace.tracePath.map((point) => ({ ...point })),
}))

for (let pass = 0; pass < MAX_PASSES; pass++) {
let changed = false

const segments = outputTraces.flatMap((trace, traceIndex) =>
getSegmentsForTrace(trace, traceIndex),
)

for (let i = 0; i < segments.length; i++) {
const a = segments[i]!
const traceA = outputTraces[a.traceIndex]!
const netA = getTraceNetKey(traceA)

for (let j = i + 1; j < segments.length; j++) {
const b = segments[j]!
const traceB = outputTraces[b.traceIndex]!

if (netA !== getTraceNetKey(traceB)) continue
if (!shouldCombineSegments(a, b, maxDistance)) continue

const [anchor, target] = a.length >= b.length ? [a, b] : [b, a]
if (
wouldOverlapDifferentNetSegment(
outputTraces,
target,
netA,
anchor.fixedCoord,
)
) {
continue
}
outputTraces[target.traceIndex] = snapSegmentFixedCoord(
outputTraces[target.traceIndex]!,
target,
anchor.fixedCoord,
)
changed = true
break
}

if (changed) break
}

if (!changed) break
}

return outputTraces
}
120 changes: 120 additions & 0 deletions tests/functions/combine-close-same-net-segments.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
import { expect, test } from "bun:test"
import type { SolvedTracePath } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceLinesSolver"
import { combineCloseSameNetSegments } from "lib/solvers/TraceCleanupSolver/combineCloseSameNetSegments"

const baseTrace = (
id: string,
netId: string,
tracePath: Array<{ x: number; y: number }>,
): SolvedTracePath =>
({
mspPairId: id,
dcConnNetId: netId,
globalConnNetId: netId,
pins: [] as any,
pinIds: [],
mspConnectionPairIds: [id],
tracePath,
}) as SolvedTracePath

test("snaps close parallel same-net segments onto the longer segment", () => {
const traces = [
baseTrace("a", "N1", [
{ x: -1, y: -1 },
{ x: 0, y: 0 },
{ x: 10, y: 0 },
{ x: 10, y: 3 },
]),
baseTrace("b", "N1", [
{ x: -1, y: 2 },
{ x: 1, y: 0.05 },
{ x: 9, y: 0.05 },
{ x: 9, y: 2 },
]),
]

const combined = combineCloseSameNetSegments(traces, 0.1)

expect(combined[1]!.tracePath[1]).toEqual({ x: 1, y: 0 })
expect(combined[1]!.tracePath[2]).toEqual({ x: 9, y: 0 })
})

test("does not snap nearby segments from different nets", () => {
const traces = [
baseTrace("a", "N1", [
{ x: 0, y: 0 },
{ x: 10, y: 0 },
]),
baseTrace("b", "N2", [
{ x: 1, y: 0.05 },
{ x: 9, y: 0.05 },
]),
]

const combined = combineCloseSameNetSegments(traces, 0.1)

expect(combined[1]!.tracePath).toEqual(traces[1]!.tracePath)
})

test("does not snap same-net segments outside the distance threshold", () => {
const traces = [
baseTrace("a", "N1", [
{ x: 0, y: 0 },
{ x: 10, y: 0 },
]),
baseTrace("b", "N1", [
{ x: 1, y: 0.5 },
{ x: 9, y: 0.5 },
]),
]

const combined = combineCloseSameNetSegments(traces, 0.1)

expect(combined[1]!.tracePath).toEqual(traces[1]!.tracePath)
})

test("preserves terminal pin legs", () => {
const traces = [
baseTrace("a", "N1", [
{ x: 0, y: 0 },
{ x: 10, y: 0 },
]),
baseTrace("b", "N1", [
{ x: 1, y: 0.05 },
{ x: 9, y: 0.05 },
]),
]

const combined = combineCloseSameNetSegments(traces, 0.1)

expect(combined[0]!.tracePath).toEqual(traces[0]!.tracePath)
expect(combined[1]!.tracePath).toEqual(traces[1]!.tracePath)
})

test("rejects snaps that would overlap a different-net segment", () => {
const traces = [
baseTrace("a", "N1", [
{ x: -1, y: -1 },
{ x: 0, y: 0 },
{ x: 10, y: 0 },
{ x: 11, y: 1 },
]),
baseTrace("b", "N1", [
{ x: -1, y: 2 },
{ x: 1, y: 0.05 },
{ x: 9, y: 0.05 },
{ x: 11, y: 2 },
]),
baseTrace("c", "N2", [
{ x: -1, y: 3 },
{ x: 2, y: 0 },
{ x: 8, y: 0 },
{ x: 11, y: 3 },
]),
]

const combined = combineCloseSameNetSegments(traces, 0.1)

expect(combined[1]!.tracePath[1]).toEqual({ x: 1, y: 0.05 })
expect(combined[1]!.tracePath[2]).toEqual({ x: 9, y: 0.05 })
})
Loading