diff --git a/lib/solvers/SameNetTraceSegmentMergeSolver/SameNetTraceSegmentMergeSolver.ts b/lib/solvers/SameNetTraceSegmentMergeSolver/SameNetTraceSegmentMergeSolver.ts new file mode 100644 index 000000000..9fc06fff6 --- /dev/null +++ b/lib/solvers/SameNetTraceSegmentMergeSolver/SameNetTraceSegmentMergeSolver.ts @@ -0,0 +1,202 @@ +import type { Point } from "@tscircuit/math-utils" +import { BaseSolver } from "lib/solvers/BaseSolver/BaseSolver" +import type { SolvedTracePath } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceLinesSolver" + +type SegmentOrientation = "horizontal" | "vertical" + +interface SegmentRef { + traceIndex: number + segmentIndex: number + orientation: SegmentOrientation + axis: number + min: number + max: number + length: number + movable: boolean +} + +export interface SameNetTraceSegmentMergeSolverParams { + traces: SolvedTracePath[] + mergeDistance?: number + minOverlap?: number + minOverlapRatio?: number +} + +const EPS = 1e-6 + +const cloneTrace = (trace: SolvedTracePath): SolvedTracePath => ({ + ...trace, + tracePath: trace.tracePath.map((point) => ({ ...point })), +}) + +const pointsEqual = (a: Point, b: Point) => + Math.abs(a.x - b.x) < EPS && Math.abs(a.y - b.y) < EPS + +const isCollinear = (a: Point, b: Point, c: Point) => + (Math.abs(a.x - b.x) < EPS && Math.abs(b.x - c.x) < EPS) || + (Math.abs(a.y - b.y) < EPS && Math.abs(b.y - c.y) < EPS) + +const simplifyPath = (path: Point[]) => { + const withoutDuplicates: Point[] = [] + for (const point of path) { + const last = withoutDuplicates[withoutDuplicates.length - 1] + if (!last || !pointsEqual(last, point)) { + withoutDuplicates.push(point) + } + } + + const simplified: Point[] = [] + for (const point of withoutDuplicates) { + simplified.push(point) + while (simplified.length >= 3) { + const a = simplified[simplified.length - 3]! + const b = simplified[simplified.length - 2]! + const c = simplified[simplified.length - 1]! + if (!isCollinear(a, b, c)) break + simplified.splice(simplified.length - 2, 1) + } + } + return simplified +} + +const getSegment = ( + trace: SolvedTracePath, + traceIndex: number, + segmentIndex: number, +): SegmentRef | null => { + const start = trace.tracePath[segmentIndex]! + const end = trace.tracePath[segmentIndex + 1]! + const isHorizontal = Math.abs(start.y - end.y) < EPS + const isVertical = Math.abs(start.x - end.x) < EPS + + if (!isHorizontal && !isVertical) return null + + const orientation = isHorizontal ? "horizontal" : "vertical" + const min = + orientation === "horizontal" + ? Math.min(start.x, end.x) + : Math.min(start.y, end.y) + const max = + orientation === "horizontal" + ? Math.max(start.x, end.x) + : Math.max(start.y, end.y) + const length = max - min + + if (length < EPS) return null + + return { + traceIndex, + segmentIndex, + orientation, + axis: orientation === "horizontal" ? start.y : start.x, + min, + max, + length, + movable: segmentIndex > 0 && segmentIndex + 1 < trace.tracePath.length - 1, + } +} + +const getOverlap = (a: SegmentRef, b: SegmentRef) => + Math.min(a.max, b.max) - Math.max(a.min, b.min) + +export class SameNetTraceSegmentMergeSolver extends BaseSolver { + private traces: SolvedTracePath[] + private mergeDistance: number + private minOverlap: number + private minOverlapRatio: number + + constructor(params: SameNetTraceSegmentMergeSolverParams) { + super() + this.traces = params.traces.map(cloneTrace) + this.mergeDistance = params.mergeDistance ?? 0.19 + this.minOverlap = params.minOverlap ?? 0.05 + this.minOverlapRatio = params.minOverlapRatio ?? 0.75 + } + + override getConstructorParams(): SameNetTraceSegmentMergeSolverParams { + return { + traces: this.traces, + mergeDistance: this.mergeDistance, + minOverlap: this.minOverlap, + minOverlapRatio: this.minOverlapRatio, + } + } + + override _step() { + let changed = true + while (changed) { + changed = this.mergeNextSegmentPair() + } + this.solved = true + } + + private mergeNextSegmentPair() { + for (let i = 0; i < this.traces.length; i++) { + const traceA = this.traces[i]! + for (let j = i + 1; j < this.traces.length; j++) { + const traceB = this.traces[j]! + if (traceA.globalConnNetId !== traceB.globalConnNetId) continue + + for (let aIdx = 0; aIdx < traceA.tracePath.length - 1; aIdx++) { + const segmentA = getSegment(traceA, i, aIdx) + if (!segmentA) continue + + for (let bIdx = 0; bIdx < traceB.tracePath.length - 1; bIdx++) { + const segmentB = getSegment(traceB, j, bIdx) + if (!segmentB) continue + + if (this.tryMergeSegments(segmentA, segmentB)) { + return true + } + } + } + } + } + return false + } + + private tryMergeSegments(segmentA: SegmentRef, segmentB: SegmentRef) { + if (segmentA.orientation !== segmentB.orientation) return false + + const axisDistance = Math.abs(segmentA.axis - segmentB.axis) + if (axisDistance < EPS || axisDistance > this.mergeDistance) return false + + const overlap = getOverlap(segmentA, segmentB) + const shorterLength = Math.min(segmentA.length, segmentB.length) + if (overlap < this.minOverlap) return false + if (overlap < shorterLength * this.minOverlapRatio) return false + + const segmentToMove = this.getMovableSegment(segmentA, segmentB) + const targetSegment = segmentToMove === segmentA ? segmentB : segmentA + + if (!segmentToMove) return false + + const trace = this.traces[segmentToMove.traceIndex]! + const start = trace.tracePath[segmentToMove.segmentIndex]! + const end = trace.tracePath[segmentToMove.segmentIndex + 1]! + + if (segmentToMove.orientation === "horizontal") { + start.y = targetSegment.axis + end.y = targetSegment.axis + } else { + start.x = targetSegment.axis + end.x = targetSegment.axis + } + + trace.tracePath = simplifyPath(trace.tracePath) + return true + } + + private getMovableSegment(segmentA: SegmentRef, segmentB: SegmentRef) { + if (segmentA.movable && !segmentB.movable) return segmentA + if (segmentB.movable && !segmentA.movable) return segmentB + if (!segmentA.movable && !segmentB.movable) return null + return segmentA.length <= segmentB.length ? segmentA : segmentB + } + + getOutput() { + return { + traces: this.traces, + } + } +} diff --git a/lib/solvers/SchematicTracePipelineSolver/SchematicTracePipelineSolver.ts b/lib/solvers/SchematicTracePipelineSolver/SchematicTracePipelineSolver.ts index 59821f0c1..559d7cdb1 100644 --- a/lib/solvers/SchematicTracePipelineSolver/SchematicTracePipelineSolver.ts +++ b/lib/solvers/SchematicTracePipelineSolver/SchematicTracePipelineSolver.ts @@ -26,6 +26,7 @@ import { AvailableNetOrientationSolver } from "../AvailableNetOrientationSolver/ import { VccNetLabelCornerPlacementSolver } from "../VccNetLabelCornerPlacementSolver/VccNetLabelCornerPlacementSolver" import { TraceAnchoredNetLabelOverlapSolver } from "../TraceAnchoredNetLabelOverlapSolver/TraceAnchoredNetLabelOverlapSolver" import { NetLabelTraceCollisionSolver } from "../NetLabelTraceCollisionSolver/NetLabelTraceCollisionSolver" +import { SameNetTraceSegmentMergeSolver } from "../SameNetTraceSegmentMergeSolver/SameNetTraceSegmentMergeSolver" type PipelineStep BaseSolver> = { solverName: string @@ -75,6 +76,7 @@ export class SchematicTracePipelineSolver extends BaseSolver { labelMergingSolver?: MergedNetLabelObstacleSolver traceLabelOverlapAvoidanceSolver?: TraceLabelOverlapAvoidanceSolver traceCleanupSolver?: TraceCleanupSolver + sameNetTraceSegmentMergeSolver?: SameNetTraceSegmentMergeSolver example28Solver?: Example28Solver availableNetOrientationSolver?: AvailableNetOrientationSolver vccNetLabelCornerPlacementSolver?: VccNetLabelCornerPlacementSolver @@ -217,11 +219,21 @@ export class SchematicTracePipelineSolver extends BaseSolver { }, ] }), + definePipelineStep( + "sameNetTraceSegmentMergeSolver", + SameNetTraceSegmentMergeSolver, + (instance) => [ + { + traces: instance.traceCleanupSolver!.getOutput().traces, + }, + ], + ), definePipelineStep( "netLabelPlacementSolver", NetLabelPlacementSolver, (instance) => { const traces = + instance.sameNetTraceSegmentMergeSolver?.getOutput().traces ?? instance.traceCleanupSolver?.getOutput().traces ?? instance.traceLabelOverlapAvoidanceSolver!.getOutput().traces @@ -237,6 +249,7 @@ export class SchematicTracePipelineSolver extends BaseSolver { ), definePipelineStep("example28Solver", Example28Solver, (instance) => { const traces = + instance.sameNetTraceSegmentMergeSolver?.getOutput().traces ?? instance.traceCleanupSolver?.getOutput().traces ?? instance.traceLabelOverlapAvoidanceSolver!.getOutput().traces diff --git a/tests/solvers/SameNetTraceSegmentMergeSolver/same-net-trace-segment-merge-solver.test.ts b/tests/solvers/SameNetTraceSegmentMergeSolver/same-net-trace-segment-merge-solver.test.ts new file mode 100644 index 000000000..70a556022 --- /dev/null +++ b/tests/solvers/SameNetTraceSegmentMergeSolver/same-net-trace-segment-merge-solver.test.ts @@ -0,0 +1,106 @@ +import { expect, test } from "bun:test" +import { SameNetTraceSegmentMergeSolver } from "lib/solvers/SameNetTraceSegmentMergeSolver/SameNetTraceSegmentMergeSolver" +import type { SolvedTracePath } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceLinesSolver" + +const makeTrace = ( + mspPairId: string, + globalConnNetId: string, + tracePath: Array<{ x: number; y: number }>, +): SolvedTracePath => + ({ + mspPairId, + dcConnNetId: globalConnNetId, + globalConnNetId, + pins: [], + tracePath, + mspConnectionPairIds: [mspPairId], + pinIds: [], + }) as any + +test("snaps close overlapping same-net internal segments onto a shared axis", () => { + const traces = [ + makeTrace("a", "gnd", [ + { x: 0, y: 0 }, + { x: 10, y: 0 }, + ]), + makeTrace("b", "gnd", [ + { x: 1, y: 2 }, + { x: 1, y: 0.08 }, + { x: 9, y: 0.08 }, + { x: 9, y: 2 }, + ]), + ] + + const solver = new SameNetTraceSegmentMergeSolver({ traces }) + solver.solve() + + expect(solver.getOutput().traces[1]!.tracePath).toEqual([ + { x: 1, y: 2 }, + { x: 1, y: 0 }, + { x: 9, y: 0 }, + { x: 9, y: 2 }, + ]) +}) + +test("does not snap segments from different nets", () => { + const traces = [ + makeTrace("a", "vcc", [ + { x: 0, y: 0 }, + { x: 10, y: 0 }, + ]), + makeTrace("b", "gnd", [ + { x: 1, y: 2 }, + { x: 1, y: 0.08 }, + { x: 9, y: 0.08 }, + { x: 9, y: 2 }, + ]), + ] + + const solver = new SameNetTraceSegmentMergeSolver({ traces }) + solver.solve() + + expect(solver.getOutput().traces[1]!.tracePath[1]!.y).toBe(0.08) + expect(solver.getOutput().traces[1]!.tracePath[2]!.y).toBe(0.08) +}) + +test("does not move terminal pin segments", () => { + const traces = [ + makeTrace("a", "gnd", [ + { x: 0, y: 0 }, + { x: 10, y: 0 }, + ]), + makeTrace("b", "gnd", [ + { x: 1, y: 0.08 }, + { x: 9, y: 0.08 }, + ]), + ] + + const solver = new SameNetTraceSegmentMergeSolver({ traces }) + solver.solve() + + expect(solver.getOutput().traces[1]!.tracePath).toEqual([ + { x: 1, y: 0.08 }, + { x: 9, y: 0.08 }, + ]) +}) + +test("requires most of the shorter segment to overlap the target run", () => { + const traces = [ + makeTrace("a", "gnd", [ + { x: 0, y: 0 }, + { x: 4, y: 0 }, + ]), + makeTrace("b", "gnd", [ + { x: 2, y: 2 }, + { x: 2, y: 0.08 }, + { x: 10, y: 0.08 }, + { x: 10, y: 2 }, + ]), + ] + + const solver = new SameNetTraceSegmentMergeSolver({ traces }) + solver.solve() + + expect(solver.getOutput().traces[1]!.tracePath[1]!.y).toBe(0.08) + expect(solver.getOutput().traces[1]!.tracePath[2]!.y).toBe(0.08) +})