From 16d347bf6e9a539a45de7394aba2d4e3be9aaacc Mon Sep 17 00:00:00 2001 From: dekacchi <138255935+dekacchi@users.noreply.github.com> Date: Fri, 22 May 2026 21:01:40 +0200 Subject: [PATCH] Add same-net trace axis cleanup --- .../TraceCleanupSolver/TraceCleanupSolver.ts | 19 +- .../alignNearbySameNetTraceSegments.ts | 314 ++++++++++++++++++ .../examples/__snapshots__/example18.snap.svg | 32 +- .../examples/__snapshots__/example19.snap.svg | 14 +- .../alignNearbySameNetTraceSegments.test.ts | 130 ++++++++ 5 files changed, 478 insertions(+), 31 deletions(-) create mode 100644 lib/solvers/TraceCleanupSolver/alignNearbySameNetTraceSegments.ts create mode 100644 tests/solvers/TraceCleanupSolver/alignNearbySameNetTraceSegments.test.ts diff --git a/lib/solvers/TraceCleanupSolver/TraceCleanupSolver.ts b/lib/solvers/TraceCleanupSolver/TraceCleanupSolver.ts index e9bac7ca3..efbb5e37a 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 { alignNearbySameNetTraceSegments } from "./alignNearbySameNetTraceSegments" /** * 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,25 @@ 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 = alignNearbySameNetTraceSegments({ + traces: this.outputTraces, + inputProblem: this.input.inputProblem, + allLabelPlacements: this.input.allLabelPlacements, + mergedLabelNetIdMap: this.input.mergedLabelNetIdMap, + paddingBuffer: this.input.paddingBuffer, + }) + 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/alignNearbySameNetTraceSegments.ts b/lib/solvers/TraceCleanupSolver/alignNearbySameNetTraceSegments.ts new file mode 100644 index 000000000..e9998f3a0 --- /dev/null +++ b/lib/solvers/TraceCleanupSolver/alignNearbySameNetTraceSegments.ts @@ -0,0 +1,314 @@ +import type { Point } from "@tscircuit/math-utils" +import { getObstacleRects } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver2/rect" +import { segmentIntersectsRect } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver2/collisions" +import type { SolvedTracePath } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceLinesSolver" +import type { InputProblem } from "lib/types/InputProblem" +import type { NetLabelPlacement } from "../NetLabelPlacementSolver/NetLabelPlacementSolver" +import { simplifyPath } from "./simplifyPath" + +type Orientation = "horizontal" | "vertical" + +interface TraceSegment { + traceIndex: number + segmentIndex: number + orientation: Orientation + axis: number + min: number + max: number + length: number + globalConnNetId: string +} + +interface BlockingRect { + chipId: string + minX: number + minY: number + maxX: number + maxY: number +} + +const EPS = 1e-9 + +const getOrientation = (a: Point, b: Point): Orientation | null => { + if (Math.abs(a.y - b.y) < EPS && Math.abs(a.x - b.x) > EPS) { + return "horizontal" + } + if (Math.abs(a.x - b.x) < EPS && Math.abs(a.y - b.y) > EPS) { + return "vertical" + } + return null +} + +const getInternalSegments = (traces: SolvedTracePath[]): TraceSegment[] => { + const segments: TraceSegment[] = [] + + for (const [traceIndex, trace] of traces.entries()) { + for ( + let segmentIndex = 1; + segmentIndex < trace.tracePath.length - 2; + segmentIndex++ + ) { + const a = trace.tracePath[segmentIndex]! + const b = trace.tracePath[segmentIndex + 1]! + const orientation = getOrientation(a, b) + if (!orientation) continue + + const min = + orientation === "horizontal" ? Math.min(a.x, b.x) : Math.min(a.y, b.y) + const max = + orientation === "horizontal" ? Math.max(a.x, b.x) : Math.max(a.y, b.y) + const length = max - min + if (length < 0.02) continue + + segments.push({ + traceIndex, + segmentIndex, + orientation, + axis: orientation === "horizontal" ? a.y : a.x, + min, + max, + length, + globalConnNetId: trace.globalConnNetId, + }) + } + } + + return segments +} + +const projectionGap = (a: TraceSegment, b: TraceSegment) => + Math.max(0, Math.max(a.min, b.min) - Math.min(a.max, b.max)) + +const projectionOverlap = (a: TraceSegment, b: TraceSegment) => + Math.min(a.max, b.max) - Math.max(a.min, b.min) + +const cloneTraces = (traces: SolvedTracePath[]): SolvedTracePath[] => + traces.map((trace) => ({ + ...trace, + tracePath: trace.tracePath.map((point) => ({ ...point })), + })) + +const moveSegmentAxis = ( + trace: SolvedTracePath, + segmentIndex: number, + orientation: Orientation, + axis: number, +): SolvedTracePath => { + const nextPath = trace.tracePath.map((point) => ({ ...point })) + const a = nextPath[segmentIndex]! + const b = nextPath[segmentIndex + 1]! + + if (orientation === "horizontal") { + a.y = axis + b.y = axis + } else { + a.x = axis + b.x = axis + } + + return { + ...trace, + tracePath: simplifyPath(nextPath), + } +} + +const segmentsIntersect = ( + a1: Point, + a2: Point, + b1: Point, + b2: Point, +): boolean => { + const aOrientation = getOrientation(a1, a2) + const bOrientation = getOrientation(b1, b2) + if (!aOrientation || !bOrientation) return false + + if (aOrientation === bOrientation) { + if (aOrientation === "horizontal") { + if (Math.abs(a1.y - b1.y) > EPS) return false + return ( + Math.min(a2.x, a1.x) < Math.max(b1.x, b2.x) - EPS && + Math.min(b2.x, b1.x) < Math.max(a1.x, a2.x) - EPS + ) + } + + if (Math.abs(a1.x - b1.x) > EPS) return false + return ( + Math.min(a2.y, a1.y) < Math.max(b1.y, b2.y) - EPS && + Math.min(b2.y, b1.y) < Math.max(a1.y, a2.y) - EPS + ) + } + + const horizontal = + aOrientation === "horizontal" ? { a: a1, b: a2 } : { a: b1, b: b2 } + const vertical = + aOrientation === "vertical" ? { a: a1, b: a2 } : { a: b1, b: b2 } + + const minX = Math.min(horizontal.a.x, horizontal.b.x) + const maxX = Math.max(horizontal.a.x, horizontal.b.x) + const minY = Math.min(vertical.a.y, vertical.b.y) + const maxY = Math.max(vertical.a.y, vertical.b.y) + + return ( + vertical.a.x > minX + EPS && + vertical.a.x < maxX - EPS && + horizontal.a.y > minY + EPS && + horizontal.a.y < maxY - EPS + ) +} + +const pathHasDifferentNetCollision = ( + candidateTrace: SolvedTracePath, + traces: SolvedTracePath[], +): boolean => { + for (const otherTrace of traces) { + if (otherTrace.mspPairId === candidateTrace.mspPairId) continue + if (otherTrace.globalConnNetId === candidateTrace.globalConnNetId) continue + + for (let i = 0; i < candidateTrace.tracePath.length - 1; i++) { + const a1 = candidateTrace.tracePath[i]! + const a2 = candidateTrace.tracePath[i + 1]! + for (let j = 0; j < otherTrace.tracePath.length - 1; j++) { + if ( + segmentsIntersect( + a1, + a2, + otherTrace.tracePath[j]!, + otherTrace.tracePath[j + 1]!, + ) + ) { + return true + } + } + } + } + + return false +} + +const pathIntersectsBlockingRects = ( + trace: SolvedTracePath, + rects: BlockingRect[], +): boolean => { + for (let i = 0; i < trace.tracePath.length - 1; i++) { + const a = trace.tracePath[i]! + const b = trace.tracePath[i + 1]! + for (const rect of rects) { + if (segmentIntersectsRect(a, b, rect)) return true + } + } + + return false +} + +const getBlockingRects = ({ + inputProblem, + allLabelPlacements = [], + mergedLabelNetIdMap = {}, + targetNetId, + paddingBuffer = 0, +}: { + inputProblem?: InputProblem + allLabelPlacements?: NetLabelPlacement[] + mergedLabelNetIdMap?: Record> + targetNetId: string + paddingBuffer?: number +}): BlockingRect[] => { + const staticRects = inputProblem ? getObstacleRects(inputProblem) : [] + + const labelRects = allLabelPlacements + .filter((label) => { + const originalNetIds = mergedLabelNetIdMap[label.globalConnNetId] + if (originalNetIds) return !originalNetIds.has(targetNetId) + return label.globalConnNetId !== targetNetId + }) + .map( + (label): BlockingRect => ({ + chipId: `net-label-${label.globalConnNetId}`, + minX: label.center.x - label.width / 2 - paddingBuffer, + maxX: label.center.x + label.width / 2 + paddingBuffer, + minY: label.center.y - label.height / 2 - paddingBuffer, + maxY: label.center.y + label.height / 2 + paddingBuffer, + }), + ) + + return [...staticRects, ...labelRects] +} + +export const alignNearbySameNetTraceSegments = ({ + traces, + inputProblem, + allLabelPlacements, + mergedLabelNetIdMap, + paddingBuffer, + maxAxisDistance = 0.12, + maxProjectionGap = 0.12, +}: { + traces: SolvedTracePath[] + inputProblem?: InputProblem + allLabelPlacements?: NetLabelPlacement[] + mergedLabelNetIdMap?: Record> + paddingBuffer?: number + maxAxisDistance?: number + maxProjectionGap?: number +}): SolvedTracePath[] => { + const alignedTraces = cloneTraces(traces) + + for (let pass = 0; pass < 20; pass++) { + const segments = getInternalSegments(alignedTraces) + let changed = false + + for (let i = 0; i < segments.length; i++) { + const a = segments[i]! + for (let j = i + 1; j < segments.length; j++) { + const b = segments[j]! + if (a.traceIndex === b.traceIndex) continue + if (a.globalConnNetId !== b.globalConnNetId) continue + if (a.orientation !== b.orientation) continue + if (Math.abs(a.axis - b.axis) > maxAxisDistance) continue + if ( + projectionOverlap(a, b) <= EPS && + projectionGap(a, b) > maxProjectionGap + ) { + continue + } + + const anchor = a.length >= b.length ? a : b + const moving = anchor === a ? b : a + if (Math.abs(anchor.axis - moving.axis) < EPS) continue + + const candidateTrace = moveSegmentAxis( + alignedTraces[moving.traceIndex]!, + moving.segmentIndex, + moving.orientation, + anchor.axis, + ) + const candidateTraces = [...alignedTraces] + candidateTraces[moving.traceIndex] = candidateTrace + + if (pathHasDifferentNetCollision(candidateTrace, candidateTraces)) { + continue + } + + const blockingRects = getBlockingRects({ + inputProblem, + allLabelPlacements, + mergedLabelNetIdMap, + targetNetId: moving.globalConnNetId, + paddingBuffer, + }) + if (pathIntersectsBlockingRects(candidateTrace, blockingRects)) { + continue + } + + alignedTraces[moving.traceIndex] = candidateTrace + changed = true + break + } + if (changed) break + } + + if (!changed) break + } + + return alignedTraces +} diff --git a/tests/examples/__snapshots__/example18.snap.svg b/tests/examples/__snapshots__/example18.snap.svg index 8c7f05fc8..bc8149cf2 100644 --- a/tests/examples/__snapshots__/example18.snap.svg +++ b/tests/examples/__snapshots__/example18.snap.svg @@ -65,24 +65,19 @@ y-" data-x="1.7580660749999977" data-y="-3.3025814000000002" cx="494.02875093834 y+" data-x="1.757519574999999" data-y="-2.2" cx="493.97982495355666" cy="501.29024555523955" r="3" fill="hsl(248, 100%, 50%, 0.8)" /> - + - + - + - + - + @@ -139,7 +134,7 @@ orientation: x+" data-x="1.757519574999999" data-y="-2" cx="493.97982495355666" - + @@ -167,28 +162,23 @@ orientation: x+" data-x="1.757519574999999" data-y="-2" cx="493.97982495355666" +globalConnNetId: connectivity_net0" data-x="-1.8574283249999997" data-y="0.9762093000000004" x="161.39522395803996" y="196.79342126794842" width="17.905209437554532" height="40.28672123449769" fill="#ef444466" stroke="#ef4444" stroke-width="0.011169933571428573" /> +globalConnNetId: connectivity_net0" data-x="1.5790330374999988" data-y="2.7275814000000005" x="469.0480260561722" y="40" width="17.90520943755456" height="40.28672123449769" fill="#ef444466" stroke="#ef4444" stroke-width="0.011169933571428573" /> +globalConnNetId: connectivity_net1" data-x="-2.31430995" data-y="-0.9762093000000004" x="120.4924180390637" y="371.58574098183345" width="17.905209437554532" height="40.28672123449769" fill="#00000066" stroke="#000000" stroke-width="0.011169933571428573" /> +globalConnNetId: connectivity_net2" data-x="1.982519574999999" data-y="0.85" x="493.97982495355666" y="219.2831969137558" width="40.28672123449769" height="17.905209437554532" fill="hsl(40, 100%, 50%, 0.35)" stroke="black" stroke-width="0.011169933571428573" /> +globalConnNetId: connectivity_net3" data-x="1.982519574999999" data-y="-2" x="493.97982495355666" y="474.43243139890774" width="40.28672123449769" height="17.90520943755456" fill="hsl(40, 100%, 50%, 0.35)" stroke="black" stroke-width="0.011169933571428573" /> - + - + - + - + @@ -101,7 +97,7 @@ orientation: y+" data-x="3.3884680250000008" data-y="1.2997267500000007" cx="438 - + diff --git a/tests/solvers/TraceCleanupSolver/alignNearbySameNetTraceSegments.test.ts b/tests/solvers/TraceCleanupSolver/alignNearbySameNetTraceSegments.test.ts new file mode 100644 index 000000000..f0aa4f2a0 --- /dev/null +++ b/tests/solvers/TraceCleanupSolver/alignNearbySameNetTraceSegments.test.ts @@ -0,0 +1,130 @@ +import { describe, expect, test } from "bun:test" +import { alignNearbySameNetTraceSegments } from "lib/solvers/TraceCleanupSolver/alignNearbySameNetTraceSegments" +import type { SolvedTracePath } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceLinesSolver" + +const makeTrace = ( + id: string, + globalConnNetId: string, + tracePath: Array<{ x: number; y: number }>, +): SolvedTracePath => + ({ + mspPairId: id, + dcConnNetId: globalConnNetId, + globalConnNetId, + userNetId: globalConnNetId, + tracePath, + mspConnectionPairIds: [id], + pinIds: [`${id}-a`, `${id}-b`], + pins: [ + { pinId: `${id}-a`, chipId: `${id}-chip-a`, ...tracePath[0]! }, + { + pinId: `${id}-b`, + chipId: `${id}-chip-b`, + ...tracePath[tracePath.length - 1]!, + }, + ], + }) as SolvedTracePath + +describe("alignNearbySameNetTraceSegments", () => { + test("aligns nearby overlapping horizontal internal segments on the same net", () => { + const traces = alignNearbySameNetTraceSegments({ + traces: [ + makeTrace("a", "net1", [ + { 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", "net1", [ + { x: 0, y: 0.1 }, + { x: 1, y: 0.1 }, + { x: 1, y: 1.08 }, + { x: 5, y: 1.08 }, + { x: 5, y: 0.1 }, + { x: 6, y: 0.1 }, + ]), + ], + }) + + expect(traces[1]!.tracePath[2]!.y).toBe(1) + expect(traces[1]!.tracePath[3]!.y).toBe(1) + expect(traces[1]!.tracePath[0]!.y).toBe(0.1) + expect(traces[1]!.tracePath.at(-1)!.y).toBe(0.1) + }) + + test("aligns nearby overlapping vertical internal segments on the same net", () => { + const traces = alignNearbySameNetTraceSegments({ + traces: [ + makeTrace("a", "net1", [ + { x: 0, y: 0 }, + { x: 0, y: 1 }, + { x: 1, y: 1 }, + { x: 1, y: 5 }, + { x: 0, y: 5 }, + { x: 0, y: 6 }, + ]), + makeTrace("b", "net1", [ + { x: 0.1, y: 0 }, + { x: 0.1, y: 1 }, + { x: 1.07, y: 1 }, + { x: 1.07, y: 5 }, + { x: 0.1, y: 5 }, + { x: 0.1, y: 6 }, + ]), + ], + }) + + expect(traces[1]!.tracePath[2]!.x).toBe(1) + expect(traces[1]!.tracePath[3]!.x).toBe(1) + expect(traces[1]!.tracePath[0]!.x).toBe(0.1) + expect(traces[1]!.tracePath.at(-1)!.x).toBe(0.1) + }) + + test("does not align segments from different nets", () => { + const original = makeTrace("b", "net2", [ + { x: 0, y: 0.1 }, + { x: 1, y: 0.1 }, + { x: 1, y: 1.08 }, + { x: 5, y: 1.08 }, + { x: 5, y: 0.1 }, + { x: 6, y: 0.1 }, + ]) + const traces = alignNearbySameNetTraceSegments({ + traces: [ + makeTrace("a", "net1", [ + { x: 0, y: 0 }, + { x: 1, y: 0 }, + { x: 1, y: 1 }, + { x: 5, y: 1 }, + { x: 5, y: 0 }, + { x: 6, y: 0 }, + ]), + original, + ], + }) + + expect(traces[1]!.tracePath).toEqual(original.tracePath) + }) + + test("keeps endpoint-only segments anchored", () => { + const original = makeTrace("b", "net1", [ + { x: 0, y: 1.08 }, + { x: 4, y: 1.08 }, + { x: 4, y: 0 }, + ]) + const traces = alignNearbySameNetTraceSegments({ + traces: [ + makeTrace("a", "net1", [ + { x: 0, y: 1 }, + { x: 4, y: 1 }, + { x: 4, y: 0 }, + ]), + original, + ], + }) + + expect(traces[1]!.tracePath).toEqual(original.tracePath) + }) +})