From b2d89cce3d8eed574d1d69bc99b2db2df61df557 Mon Sep 17 00:00:00 2001 From: "tho.nguyen" <91511523+haki203@users.noreply.github.com> Date: Sat, 23 May 2026 10:57:16 +0700 Subject: [PATCH] Add same-net segment alignment cleanup --- .../TraceCleanupSolver/TraceCleanupSolver.ts | 16 +- .../alignNearbySameNetSegments.ts | 254 ++++++++++++++++++ .../alignNearbySameNetSegments.test.ts | 156 +++++++++++ 3 files changed, 425 insertions(+), 1 deletion(-) create mode 100644 lib/solvers/TraceCleanupSolver/alignNearbySameNetSegments.ts create mode 100644 tests/solvers/TraceCleanupSolver/alignNearbySameNetSegments.test.ts diff --git a/lib/solvers/TraceCleanupSolver/TraceCleanupSolver.ts b/lib/solvers/TraceCleanupSolver/TraceCleanupSolver.ts index e9bac7ca3..3f7658898 100644 --- a/lib/solvers/TraceCleanupSolver/TraceCleanupSolver.ts +++ b/lib/solvers/TraceCleanupSolver/TraceCleanupSolver.ts @@ -20,6 +20,7 @@ interface TraceCleanupSolverInput { import { UntangleTraceSubsolver } from "./sub-solver/UntangleTraceSubsolver" import { is4PointRectangle } from "./is4PointRectangle" +import { alignNearbySameNetSegments } from "./alignNearbySameNetSegments" /** * Represents the different stages or steps within the trace cleanup pipeline. @@ -27,6 +28,7 @@ import { is4PointRectangle } from "./is4PointRectangle" type PipelineStep = | "minimizing_turns" | "balancing_l_shapes" + | "aligning_same_net_segments" | "untangling_traces" /** @@ -84,6 +86,9 @@ export class TraceCleanupSolver extends BaseSolver { case "balancing_l_shapes": this._runBalanceLShapesStep() break + case "aligning_same_net_segments": + this._runAlignSameNetSegmentsStep() + break } } @@ -108,13 +113,22 @@ export class TraceCleanupSolver extends BaseSolver { private _runBalanceLShapesStep() { if (this.traceIdQueue.length === 0) { - this.solved = true + this.pipelineStep = "aligning_same_net_segments" return } this._processTrace("balancing_l_shapes") } + private _runAlignSameNetSegmentsStep() { + this.outputTraces = alignNearbySameNetSegments({ + traces: this.outputTraces, + inputProblem: this.input.inputProblem, + }) + this.tracesMap = new Map(this.outputTraces.map((t) => [t.mspPairId, t])) + this.solved = true + } + private _processTrace(step: "minimizing_turns" | "balancing_l_shapes") { const targetMspConnectionPairId = this.traceIdQueue.shift()! this.activeTraceId = targetMspConnectionPairId diff --git a/lib/solvers/TraceCleanupSolver/alignNearbySameNetSegments.ts b/lib/solvers/TraceCleanupSolver/alignNearbySameNetSegments.ts new file mode 100644 index 000000000..a237052c6 --- /dev/null +++ b/lib/solvers/TraceCleanupSolver/alignNearbySameNetSegments.ts @@ -0,0 +1,254 @@ +import type { Point } from "@tscircuit/math-utils" +import type { SolvedTracePath } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceLinesSolver" +import { + isHorizontal, + isVertical, + segmentIntersectsRect, +} from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver2/collisions" +import { getObstacleRects } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver2/rect" +import type { InputProblem } from "lib/types/InputProblem" +import { simplifyPath } from "./simplifyPath" + +const EPS = 1e-6 +const DEFAULT_MAX_DISTANCE = 0.15 +const MIN_OVERLAP = 0.05 +const MAX_PASSES = 20 + +type SegmentInfo = { + traceIndex: number + pointIndex: number + orientation: "h" | "v" + fixedCoord: number + start: number + end: number + length: number +} + +const cloneTrace = (trace: SolvedTracePath): SolvedTracePath => ({ + ...trace, + tracePath: trace.tracePath.map((p) => ({ ...p })), +}) + +const getSegmentInfo = ( + trace: SolvedTracePath, + traceIndex: number, +): SegmentInfo[] => { + const segments: SegmentInfo[] = [] + + for (let i = 1; i < trace.tracePath.length - 2; i++) { + const a = trace.tracePath[i]! + const b = trace.tracePath[i + 1]! + + if (isHorizontal(a, b)) { + const start = Math.min(a.x, b.x) + const end = Math.max(a.x, b.x) + segments.push({ + traceIndex, + pointIndex: i, + orientation: "h", + fixedCoord: a.y, + start, + end, + length: end - start, + }) + } else if (isVertical(a, b)) { + const start = Math.min(a.y, b.y) + const end = Math.max(a.y, b.y) + segments.push({ + traceIndex, + pointIndex: i, + orientation: "v", + fixedCoord: a.x, + start, + end, + length: end - start, + }) + } + } + + return segments +} + +const projectionOverlap = (a: SegmentInfo, b: SegmentInfo) => + Math.min(a.end, b.end) - Math.max(a.start, b.start) + +const moveSegmentToFixedCoord = ( + trace: SolvedTracePath, + segment: SegmentInfo, + fixedCoord: number, +) => { + const nextPath = trace.tracePath.map((p) => ({ ...p })) + const a = nextPath[segment.pointIndex]! + const b = nextPath[segment.pointIndex + 1]! + + if (segment.orientation === "h") { + a.y = fixedCoord + b.y = fixedCoord + } else { + a.x = fixedCoord + b.x = fixedCoord + } + + return { + ...trace, + tracePath: simplifyPath(nextPath), + } +} + +const segmentPairs = (path: Point[]) => + path.slice(0, -1).map((point, index) => [point, path[index + 1]!] as const) + +const rangesTouch = ( + aStart: number, + aEnd: number, + bStart: number, + bEnd: number, +) => Math.min(aEnd, bEnd) >= Math.max(aStart, bStart) - EPS + +const inRange = (value: number, start: number, end: number) => + value >= Math.min(start, end) - EPS && value <= Math.max(start, end) + EPS + +const axisAlignedSegmentsTouch = ( + a1: Point, + a2: Point, + b1: Point, + b2: Point, +) => { + if (isHorizontal(a1, a2) && isHorizontal(b1, b2)) { + return ( + Math.abs(a1.y - b1.y) <= EPS && + rangesTouch( + Math.min(a1.x, a2.x), + Math.max(a1.x, a2.x), + Math.min(b1.x, b2.x), + Math.max(b1.x, b2.x), + ) + ) + } + + if (isVertical(a1, a2) && isVertical(b1, b2)) { + return ( + Math.abs(a1.x - b1.x) <= EPS && + rangesTouch( + Math.min(a1.y, a2.y), + Math.max(a1.y, a2.y), + Math.min(b1.y, b2.y), + Math.max(b1.y, b2.y), + ) + ) + } + + const h1 = isHorizontal(a1, a2) + ? [a1, a2] + : isHorizontal(b1, b2) + ? [b1, b2] + : null + const v1 = isVertical(a1, a2) + ? [a1, a2] + : isVertical(b1, b2) + ? [b1, b2] + : null + + if (!h1 || !v1) return false + + const [hA, hB] = h1 + const [vA, vB] = v1 + + return inRange(vA.x, hA.x, hB.x) && inRange(hA.y, vA.y, vB.y) +} + +const isMoveSafe = ( + traces: SolvedTracePath[], + movedTrace: SolvedTracePath, + movedTraceIndex: number, + inputProblem: InputProblem, +) => { + const obstacles = getObstacleRects(inputProblem) + + for (const [a, b] of segmentPairs(movedTrace.tracePath)) { + if (obstacles.some((rect) => segmentIntersectsRect(a, b, rect))) { + return false + } + } + + for (let traceIndex = 0; traceIndex < traces.length; traceIndex++) { + if (traceIndex === movedTraceIndex) continue + const otherTrace = traces[traceIndex]! + if (otherTrace.globalConnNetId === movedTrace.globalConnNetId) continue + + for (const [a1, a2] of segmentPairs(movedTrace.tracePath)) { + for (const [b1, b2] of segmentPairs(otherTrace.tracePath)) { + if (axisAlignedSegmentsTouch(a1, a2, b1, b2)) { + return false + } + } + } + } + + return true +} + +export const alignNearbySameNetSegments = ({ + traces, + inputProblem, + maxDistance = DEFAULT_MAX_DISTANCE, +}: { + traces: SolvedTracePath[] + inputProblem: InputProblem + maxDistance?: number +}): SolvedTracePath[] => { + let outputTraces = traces.map(cloneTrace) + + for (let pass = 0; pass < MAX_PASSES; pass++) { + let changed = false + + for (let i = 0; i < outputTraces.length; i++) { + const trace = outputTraces[i]! + const sameNetTraces = outputTraces + .map((candidate, traceIndex) => ({ candidate, traceIndex })) + .filter( + ({ candidate, traceIndex }) => + traceIndex > i && + candidate.globalConnNetId === trace.globalConnNetId, + ) + + const traceSegments = getSegmentInfo(trace, i) + + for (const { candidate, traceIndex } of sameNetTraces) { + const candidateSegments = getSegmentInfo(candidate, traceIndex) + + for (const a of traceSegments) { + for (const b of candidateSegments) { + if (a.orientation !== b.orientation) continue + if (Math.abs(a.fixedCoord - b.fixedCoord) > maxDistance) continue + if (projectionOverlap(a, b) < MIN_OVERLAP) continue + + const target = a.length >= b.length ? a : b + const moving = target === a ? b : a + const movedTrace = moveSegmentToFixedCoord( + outputTraces[moving.traceIndex]!, + moving, + target.fixedCoord, + ) + + if ( + isMoveSafe( + outputTraces, + movedTrace, + moving.traceIndex, + inputProblem, + ) + ) { + outputTraces[moving.traceIndex] = movedTrace + changed = true + } + } + } + } + } + + if (!changed) break + } + + return outputTraces +} diff --git a/tests/solvers/TraceCleanupSolver/alignNearbySameNetSegments.test.ts b/tests/solvers/TraceCleanupSolver/alignNearbySameNetSegments.test.ts new file mode 100644 index 000000000..3eea051a1 --- /dev/null +++ b/tests/solvers/TraceCleanupSolver/alignNearbySameNetSegments.test.ts @@ -0,0 +1,156 @@ +import { expect, test } from "bun:test" +import { TraceCleanupSolver } from "lib/solvers/TraceCleanupSolver/TraceCleanupSolver" +import { alignNearbySameNetSegments } from "lib/solvers/TraceCleanupSolver/alignNearbySameNetSegments" +import type { SolvedTracePath } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceLinesSolver" +import type { InputProblem } from "lib/types/InputProblem" + +const inputProblem: InputProblem = { + chips: [ + { + chipId: "obs-a", + center: { x: 3, y: 0 }, + width: 2, + height: 0.6, + pins: [], + }, + { + chipId: "obs-b", + center: { x: 3, y: 2 }, + width: 2, + height: 0.6, + pins: [], + }, + ], + directConnections: [], + netConnections: [], + availableNetLabelOrientations: {}, +} + +const emptyInputProblem: InputProblem = { + chips: [], + directConnections: [], + netConnections: [], + availableNetLabelOrientations: {}, +} + +const makeTrace = ( + mspPairId: string, + globalConnNetId: string, + tracePath: SolvedTracePath["tracePath"], +): SolvedTracePath => ({ + mspPairId, + dcConnNetId: globalConnNetId, + globalConnNetId, + pins: [ + { + pinId: `${mspPairId}-a`, + chipId: "chip", + x: tracePath[0]!.x, + y: tracePath[0]!.y, + }, + { + pinId: `${mspPairId}-b`, + chipId: "chip", + x: tracePath.at(-1)!.x, + y: tracePath.at(-1)!.y, + }, + ], + mspConnectionPairIds: [mspPairId], + pinIds: [`${mspPairId}-a`, `${mspPairId}-b`], + tracePath, +}) + +const solveCleanup = (traces: SolvedTracePath[]) => { + const solver = new TraceCleanupSolver({ + inputProblem, + allTraces: traces, + allLabelPlacements: [], + mergedLabelNetIdMap: {}, + paddingBuffer: 0.1, + }) + solver.solve() + return solver.getOutput().traces +} + +test("aligns close overlapping internal same-net horizontal segments while preserving endpoints", () => { + const [lowerTrace, upperTrace] = solveCleanup([ + makeTrace("a", "net-1", [ + { x: 0, y: 0 }, + { x: 1, y: 0 }, + { x: 1, y: 1 }, + { x: 5, y: 1 }, + { x: 5, y: 0 }, + { x: 6, y: 0 }, + ]), + makeTrace("b", "net-1", [ + { x: 0, y: 2 }, + { x: 1, y: 2 }, + { x: 1, y: 1.1 }, + { x: 5, y: 1.1 }, + { x: 5, y: 2 }, + { x: 6, y: 2 }, + ]), + ]) + + expect(lowerTrace!.tracePath[2]!.y).toBe(1) + expect(lowerTrace!.tracePath[3]!.y).toBe(1) + expect(upperTrace!.tracePath[0]).toEqual({ x: 0, y: 2 }) + expect(upperTrace!.tracePath.at(-1)).toEqual({ x: 6, y: 2 }) + expect(upperTrace!.tracePath[2]!.y).toBe(1) + expect(upperTrace!.tracePath[3]!.y).toBe(1) +}) + +test("does not align close segments from different nets", () => { + const [, otherNetTrace] = solveCleanup([ + makeTrace("a", "net-1", [ + { x: 0, y: 0 }, + { x: 1, y: 0 }, + { x: 1, y: 1 }, + { x: 5, y: 1 }, + { x: 5, y: 0 }, + { x: 6, y: 0 }, + ]), + makeTrace("b", "net-2", [ + { x: 0, y: 2 }, + { x: 1, y: 2 }, + { x: 1, y: 1.1 }, + { x: 5, y: 1.1 }, + { x: 5, y: 2 }, + { x: 6, y: 2 }, + ]), + ]) + + expect(otherNetTrace!.tracePath[2]!.y).toBe(1.1) + expect(otherNetTrace!.tracePath[3]!.y).toBe(1.1) +}) + +test("does not align when the move would touch a different-net endpoint", () => { + const [, blockedTrace] = alignNearbySameNetSegments({ + inputProblem: emptyInputProblem, + traces: [ + makeTrace("a", "net-1", [ + { x: 0, y: 0 }, + { x: 1, y: 0 }, + { x: 1, y: 1 }, + { x: 4, y: 1 }, + { x: 4, y: 0 }, + { x: 5, y: 0 }, + ]), + makeTrace("b", "net-1", [ + { x: 2, y: 2 }, + { x: 3, y: 2 }, + { x: 3, y: 1.1 }, + { x: 5, y: 1.1 }, + { x: 5, y: 2 }, + { x: 6, y: 2 }, + ]), + makeTrace("c", "net-2", [ + { x: 4.5, y: 0.5 }, + { x: 4.5, y: 1 }, + ]), + ], + }) + + expect(blockedTrace!.tracePath[2]!.y).toBe(1.1) + expect(blockedTrace!.tracePath[3]!.y).toBe(1.1) +})