diff --git a/lib/solvers/TraceCleanupSolver/TraceCleanupSolver.ts b/lib/solvers/TraceCleanupSolver/TraceCleanupSolver.ts index e9bac7ca3..5540f2d6f 100644 --- a/lib/solvers/TraceCleanupSolver/TraceCleanupSolver.ts +++ b/lib/solvers/TraceCleanupSolver/TraceCleanupSolver.ts @@ -6,6 +6,7 @@ import { BaseSolver } from "lib/solvers/BaseSolver/BaseSolver" import type { SolvedTracePath } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceLinesSolver" import { visualizeInputProblem } from "lib/solvers/SchematicTracePipelineSolver/visualizeInputProblem" import type { NetLabelPlacement } from "../NetLabelPlacementSolver/NetLabelPlacementSolver" +import { mergeSameNetTraceSegments } from "./mergeSameNetTraceSegments" /** * Defines the input structure for the TraceCleanupSolver. @@ -49,11 +50,12 @@ export class TraceCleanupSolver extends BaseSolver { constructor(solverInput: TraceCleanupSolverInput) { super() this.input = solverInput - this.outputTraces = [...solverInput.allTraces] + this.outputTraces = mergeSameNetTraceSegments({ + traces: solverInput.allTraces, + tolerance: solverInput.paddingBuffer, + }) 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(this.outputTraces.map((e) => e.mspPairId)) } override _step() { @@ -97,9 +99,7 @@ export class TraceCleanupSolver extends BaseSolver { private _runMinimizeTurnsStep() { if (this.traceIdQueue.length === 0) { this.pipelineStep = "balancing_l_shapes" - this.traceIdQueue = Array.from( - this.input.allTraces.map((e) => e.mspPairId), - ) + this.traceIdQueue = Array.from(this.outputTraces.map((e) => e.mspPairId)) return } diff --git a/lib/solvers/TraceCleanupSolver/mergeSameNetTraceSegments.ts b/lib/solvers/TraceCleanupSolver/mergeSameNetTraceSegments.ts new file mode 100644 index 000000000..16f8407bc --- /dev/null +++ b/lib/solvers/TraceCleanupSolver/mergeSameNetTraceSegments.ts @@ -0,0 +1,149 @@ +import type { Point } from "@tscircuit/math-utils" +import type { SolvedTracePath } from "../SchematicTraceLinesSolver/SchematicTraceLinesSolver" +import { simplifyPath } from "./simplifyPath" + +const EPSILON = 1e-9 + +type SegmentOrientation = "horizontal" | "vertical" + +interface SegmentRef { + traceIndex: number + startIndex: number + endIndex: number + netId: string + orientation: SegmentOrientation + coordinate: number + minProjection: number + maxProjection: number +} + +const getSegmentRef = ( + trace: SolvedTracePath, + traceIndex: number, + startIndex: number, +): SegmentRef | null => { + const p1 = trace.tracePath[startIndex] + const p2 = trace.tracePath[startIndex + 1] + + if (!p1 || !p2 || !trace.globalConnNetId) { + return null + } + + if (Math.abs(p1.y - p2.y) <= EPSILON) { + return { + traceIndex, + startIndex, + endIndex: startIndex + 1, + netId: trace.globalConnNetId, + orientation: "horizontal", + coordinate: p1.y, + minProjection: Math.min(p1.x, p2.x), + maxProjection: Math.max(p1.x, p2.x), + } + } + + if (Math.abs(p1.x - p2.x) <= EPSILON) { + return { + traceIndex, + startIndex, + endIndex: startIndex + 1, + netId: trace.globalConnNetId, + orientation: "vertical", + coordinate: p1.x, + minProjection: Math.min(p1.y, p2.y), + maxProjection: Math.max(p1.y, p2.y), + } + } + + return null +} + +const getProjectionGap = (a: SegmentRef, b: SegmentRef): number => { + if (a.maxProjection < b.minProjection) { + return b.minProjection - a.maxProjection + } + if (b.maxProjection < a.minProjection) { + return a.minProjection - b.maxProjection + } + return 0 +} + +const alignSegment = ( + path: Point[], + segment: SegmentRef, + coordinate: number, +) => { + const start = path[segment.startIndex]! + const end = path[segment.endIndex]! + + if (segment.orientation === "horizontal") { + path[segment.startIndex] = { ...start, y: coordinate } + path[segment.endIndex] = { ...end, y: coordinate } + } else { + path[segment.startIndex] = { ...start, x: coordinate } + path[segment.endIndex] = { ...end, x: coordinate } + } +} + +const getSegments = (traces: SolvedTracePath[]): SegmentRef[] => + traces.flatMap((trace, traceIndex) => + trace.tracePath + .slice(0, -1) + .map((_, startIndex) => getSegmentRef(trace, traceIndex, startIndex)) + .filter((segment): segment is SegmentRef => segment !== null), + ) + +export const mergeSameNetTraceSegments = ({ + traces, + tolerance, +}: { + traces: SolvedTracePath[] + tolerance: number +}): SolvedTracePath[] => { + const output = traces.map((trace) => ({ + ...trace, + tracePath: trace.tracePath.map((point) => ({ ...point })), + })) + + let changed = true + let passes = 0 + + while (changed && passes < 4) { + changed = false + passes++ + const segments = getSegments(output) + + 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.netId !== b.netId || + a.orientation !== b.orientation || + a.traceIndex === b.traceIndex + ) { + continue + } + + if (Math.abs(a.coordinate - b.coordinate) > tolerance) { + continue + } + + if (getProjectionGap(a, b) > tolerance) { + continue + } + + const mergedCoordinate = (a.coordinate + b.coordinate) / 2 + alignSegment(output[a.traceIndex]!.tracePath, a, mergedCoordinate) + alignSegment(output[b.traceIndex]!.tracePath, b, mergedCoordinate) + changed = true + } + } + } + + return output.map((trace) => ({ + ...trace, + tracePath: simplifyPath(trace.tracePath), + })) +} diff --git a/tests/examples/__snapshots__/example18.snap.svg b/tests/examples/__snapshots__/example18.snap.svg index 8c7f05fc8..419052cdf 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)" /> - + - + - + - + - + @@ -136,10 +131,10 @@ 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.5895165187499993" data-y="2.7275814000000005" x="469.9865706932519" y="40" width="17.905209437554504" 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" /> - + - + - + - + @@ -95,16 +91,16 @@ orientation: y+" data-x="3.3884680250000008" data-y="1.2997267500000007" cx="438 - + - + - + - + @@ -141,7 +137,7 @@ globalConnNetId: connectivity_net1" data-x="0.7999316625000001" data-y="0.425000 +globalConnNetId: connectivity_net2" data-x="3.407056687500001" data-y="1.5247267500000008" x="430.7056687500001" y="164.26525749999996" width="20" height="45.00000000000003" fill="hsl(40, 100%, 50%, 0.35)" stroke="black" stroke-width="0.01" /> - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + @@ -859,7 +813,7 @@ orientation: x+" data-x="-8.4" data-y="-16.2" cx="198.31710258539454" cy="385.36 - + @@ -877,7 +831,7 @@ orientation: x+" data-x="-8.4" data-y="-16.2" cx="198.31710258539454" cy="385.36 - + @@ -895,7 +849,7 @@ orientation: x+" data-x="-8.4" data-y="-16.2" cx="198.31710258539454" cy="385.36 - + @@ -1070,7 +1024,7 @@ globalConnNetId: connectivity_net2" data-x="1.9500000000000006" data-y="1.475000 +globalConnNetId: connectivity_net3" data-x="-6.2125" data-y="-1.7749999999999997" x="235.67196263730466" y="123.2096283791052" width="3.5789087474883843" height="8.052544681848872" fill="hsl(40, 100%, 50%, 0.35)" stroke="black" stroke-width="0.05588295598214286" /> +globalConnNetId: connectivity_net7" data-x="-6.112500000000001" data-y="-5.775" x="237.46141701104887" y="194.7878033288731" width="3.578908747488356" height="8.052544681848872" fill="hsl(40, 100%, 50%, 0.35)" stroke="black" stroke-width="0.05588295598214286" /> ): SolvedTracePath => + ({ + mspPairId, + globalConnNetId, + tracePath, + mspConnectionPairIds: [mspPairId], + pinIds: [], + pins: [], + }) as any + +test("mergeSameNetTraceSegments aligns close horizontal same-net segments", () => { + const traces = mergeSameNetTraceSegments({ + tolerance: 0.1, + traces: [ + makeTrace({ + mspPairId: "a", + globalConnNetId: "net1", + tracePath: [ + { x: 0, y: 0 }, + { x: 2, y: 0 }, + ], + }), + makeTrace({ + mspPairId: "b", + globalConnNetId: "net1", + tracePath: [ + { x: 1, y: 0.08 }, + { x: 3, y: 0.08 }, + ], + }), + ], + }) + + expect(traces[0]!.tracePath).toEqual([ + { x: 0, y: 0.04 }, + { x: 2, y: 0.04 }, + ]) + expect(traces[1]!.tracePath).toEqual([ + { x: 1, y: 0.04 }, + { x: 3, y: 0.04 }, + ]) +}) + +test("mergeSameNetTraceSegments leaves different-net segments alone", () => { + const traces = mergeSameNetTraceSegments({ + tolerance: 0.1, + traces: [ + makeTrace({ + mspPairId: "a", + globalConnNetId: "net1", + tracePath: [ + { x: 0, y: 0 }, + { x: 2, y: 0 }, + ], + }), + makeTrace({ + mspPairId: "b", + globalConnNetId: "net2", + tracePath: [ + { x: 1, y: 0.08 }, + { x: 3, y: 0.08 }, + ], + }), + ], + }) + + expect(traces[0]!.tracePath[0]!.y).toBe(0) + expect(traces[1]!.tracePath[0]!.y).toBe(0.08) +}) + +test("mergeSameNetTraceSegments aligns close vertical same-net segments", () => { + const traces = mergeSameNetTraceSegments({ + tolerance: 0.1, + traces: [ + makeTrace({ + mspPairId: "a", + globalConnNetId: "net1", + tracePath: [ + { x: 0, y: 0 }, + { x: 0, y: 2 }, + ], + }), + makeTrace({ + mspPairId: "b", + globalConnNetId: "net1", + tracePath: [ + { x: 0.06, y: 1 }, + { x: 0.06, y: 3 }, + ], + }), + ], + }) + + expect(traces[0]!.tracePath).toEqual([ + { x: 0.03, y: 0 }, + { x: 0.03, y: 2 }, + ]) + expect(traces[1]!.tracePath).toEqual([ + { x: 0.03, y: 1 }, + { x: 0.03, y: 3 }, + ]) +})