From 26ecad6af97db9277c4c23ee14af36feee8f7b78 Mon Sep 17 00:00:00 2001 From: Jarno de Vries Date: Sun, 24 May 2026 21:03:49 +0200 Subject: [PATCH] feat: combine close same-net trace segments --- lib/index.ts | 3 +- .../SameNetTraceSegmentCombinationSolver.ts | 268 ++++++++++++++++++ .../SchematicTracePipelineSolver.ts | 36 ++- ...t-trace-segment-combination-solver.test.ts | 118 ++++++++ 4 files changed, 411 insertions(+), 14 deletions(-) create mode 100644 lib/solvers/SameNetTraceSegmentCombinationSolver/SameNetTraceSegmentCombinationSolver.ts create mode 100644 tests/solvers/SameNetTraceSegmentCombinationSolver/same-net-trace-segment-combination-solver.test.ts diff --git a/lib/index.ts b/lib/index.ts index 3985b32ac..702563300 100644 --- a/lib/index.ts +++ b/lib/index.ts @@ -1,3 +1,4 @@ +export * from "./solvers/SameNetTraceSegmentCombinationSolver/SameNetTraceSegmentCombinationSolver" +export { SchematicTraceSingleLineSolver2 } from "./solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver2/SchematicTraceSingleLineSolver2" export * from "./solvers/SchematicTracePipelineSolver/SchematicTracePipelineSolver" export * from "./types/InputProblem" -export { SchematicTraceSingleLineSolver2 } from "./solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver2/SchematicTraceSingleLineSolver2" diff --git a/lib/solvers/SameNetTraceSegmentCombinationSolver/SameNetTraceSegmentCombinationSolver.ts b/lib/solvers/SameNetTraceSegmentCombinationSolver/SameNetTraceSegmentCombinationSolver.ts new file mode 100644 index 000000000..aaf13e488 --- /dev/null +++ b/lib/solvers/SameNetTraceSegmentCombinationSolver/SameNetTraceSegmentCombinationSolver.ts @@ -0,0 +1,268 @@ +import type { Point } from "@tscircuit/math-utils" +import { getSegmentIntersection } from "@tscircuit/math-utils/line-intersections" +import type { GraphicsObject } from "graphics-debug" +import { BaseSolver } from "lib/solvers/BaseSolver/BaseSolver" +import type { SolvedTracePath } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceLinesSolver" +import type { InputProblem } from "lib/types/InputProblem" +import { visualizeInputProblem } from "../SchematicTracePipelineSolver/visualizeInputProblem" + +const EPS = 1e-6 +const DEFAULT_MAX_COMBINE_DISTANCE = 0.12 + +type SegmentOrientation = "horizontal" | "vertical" + +type SegmentLocator = { + trace: SolvedTracePath + segmentIndex: number + orientation: SegmentOrientation + coord: number + min: number + max: number + length: number +} + +export class SameNetTraceSegmentCombinationSolver extends BaseSolver { + inputProblem: InputProblem + inputTraces: SolvedTracePath[] + outputTraces: SolvedTracePath[] + maxCombineDistance: number + + constructor(params: { + inputProblem: InputProblem + inputTraces: SolvedTracePath[] + maxCombineDistance?: number + }) { + super() + this.inputProblem = params.inputProblem + this.inputTraces = params.inputTraces + this.outputTraces = params.inputTraces.map((trace) => ({ + ...trace, + tracePath: trace.tracePath.map((point) => ({ ...point })), + mspConnectionPairIds: [...trace.mspConnectionPairIds], + pinIds: [...trace.pinIds], + pins: [{ ...trace.pins[0] }, { ...trace.pins[1] }], + })) + this.maxCombineDistance = + params.maxCombineDistance ?? DEFAULT_MAX_COMBINE_DISTANCE + } + + override getConstructorParams(): ConstructorParameters< + typeof SameNetTraceSegmentCombinationSolver + >[0] { + return { + inputProblem: this.inputProblem, + inputTraces: this.inputTraces, + maxCombineDistance: this.maxCombineDistance, + } + } + + override _step() { + const didCombine = this.combineNextSegment() + if (!didCombine) { + this.solved = true + } + } + + getOutput() { + return { + traces: this.outputTraces, + } + } + + private combineNextSegment(): boolean { + const segments = this.getSegments() + + 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.trace.mspPairId === b.trace.mspPairId) continue + if (a.trace.globalConnNetId !== b.trace.globalConnNetId) continue + if (a.orientation !== b.orientation) continue + + const distance = Math.abs(a.coord - b.coord) + if (distance <= EPS || distance > this.maxCombineDistance) continue + + const target = this.getContainedCandidate(a, b) + if (!target) continue + + if (this.tryMoveSegment(target.moving, target.anchor.coord)) { + return true + } + } + } + + return false + } + + private getSegments(): SegmentLocator[] { + const segments: SegmentLocator[] = [] + + for (const trace of this.outputTraces) { + const points = trace.tracePath + for ( + let segmentIndex = 0; + segmentIndex < points.length - 1; + segmentIndex++ + ) { + const p1 = points[segmentIndex]! + const p2 = points[segmentIndex + 1]! + const isHorizontal = Math.abs(p1.y - p2.y) <= EPS + const isVertical = Math.abs(p1.x - p2.x) <= EPS + if (!isHorizontal && !isVertical) continue + + if (isHorizontal) { + const min = Math.min(p1.x, p2.x) + const max = Math.max(p1.x, p2.x) + segments.push({ + trace, + segmentIndex, + orientation: "horizontal", + coord: p1.y, + min, + max, + length: max - min, + }) + } else { + const min = Math.min(p1.y, p2.y) + const max = Math.max(p1.y, p2.y) + segments.push({ + trace, + segmentIndex, + orientation: "vertical", + coord: p1.x, + min, + max, + length: max - min, + }) + } + } + } + + return segments + } + + private getContainedCandidate( + a: SegmentLocator, + b: SegmentLocator, + ): { moving: SegmentLocator; anchor: SegmentLocator } | null { + const aInB = a.min >= b.min - EPS && a.max <= b.max + EPS + const bInA = b.min >= a.min - EPS && b.max <= a.max + EPS + + if (aInB && bInA) { + return a.length <= b.length + ? { moving: a, anchor: b } + : { moving: b, anchor: a } + } + if (aInB) return { moving: a, anchor: b } + if (bInA) return { moving: b, anchor: a } + return null + } + + private tryMoveSegment(segment: SegmentLocator, newCoord: number): boolean { + const trace = segment.trace + const pointCount = trace.tracePath.length + + if (segment.segmentIndex === 0) return false + if (segment.segmentIndex + 2 >= pointCount) return false + + const candidatePath = trace.tracePath.map((point) => ({ ...point })) + const p1 = candidatePath[segment.segmentIndex]! + const p2 = candidatePath[segment.segmentIndex + 1]! + + if (segment.orientation === "horizontal") { + p1.y = newCoord + p2.y = newCoord + } else { + p1.x = newCoord + p2.x = newCoord + } + + const simplifiedPath = simplifyPath(candidatePath) + + if (!isOrthogonalPath(simplifiedPath)) return false + if (this.collidesWithDifferentNetTrace(trace, simplifiedPath)) return false + + trace.tracePath = simplifiedPath + return true + } + + private collidesWithDifferentNetTrace( + movingTrace: SolvedTracePath, + candidatePath: Point[], + ): boolean { + for (let i = 0; i < candidatePath.length - 1; i++) { + const a1 = candidatePath[i]! + const a2 = candidatePath[i + 1]! + + for (const otherTrace of this.outputTraces) { + if (otherTrace.mspPairId === movingTrace.mspPairId) continue + if (otherTrace.globalConnNetId === movingTrace.globalConnNetId) continue + + for (let j = 0; j < otherTrace.tracePath.length - 1; j++) { + const b1 = otherTrace.tracePath[j]! + const b2 = otherTrace.tracePath[j + 1]! + if (getSegmentIntersection(a1, a2, b1, b2)) { + return true + } + } + } + } + + return false + } + + override visualize(): GraphicsObject { + const graphics = visualizeInputProblem(this.inputProblem) + for (const trace of this.outputTraces) { + graphics.lines!.push({ + points: trace.tracePath, + strokeColor: "purple", + }) + } + return graphics + } +} + +const isOrthogonalPath = (path: Point[]) => { + for (let i = 0; i < path.length - 1; i++) { + const p1 = path[i]! + const p2 = path[i + 1]! + if (Math.abs(p1.x - p2.x) > EPS && Math.abs(p1.y - p2.y) > EPS) { + return false + } + } + return true +} + +const simplifyPath = (path: Point[]) => { + const withoutDuplicates: Point[] = [] + for (const point of path) { + const prev = withoutDuplicates[withoutDuplicates.length - 1] + if ( + !prev || + Math.abs(prev.x - point.x) > EPS || + Math.abs(prev.y - point.y) > EPS + ) { + withoutDuplicates.push(point) + } + } + + const simplified: Point[] = [] + for (const point of withoutDuplicates) { + simplified.push(point) + while (simplified.length >= 3) { + const p1 = simplified[simplified.length - 3]! + const p2 = simplified[simplified.length - 2]! + const p3 = simplified[simplified.length - 1]! + const allHorizontal = + Math.abs(p1.y - p2.y) <= EPS && Math.abs(p2.y - p3.y) <= EPS + const allVertical = + Math.abs(p1.x - p2.x) <= EPS && Math.abs(p2.x - p3.x) <= EPS + if (!allHorizontal && !allVertical) break + simplified.splice(simplified.length - 2, 1) + } + } + + return simplified +} diff --git a/lib/solvers/SchematicTracePipelineSolver/SchematicTracePipelineSolver.ts b/lib/solvers/SchematicTracePipelineSolver/SchematicTracePipelineSolver.ts index 59821f0c1..818be0050 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 { SameNetTraceSegmentCombinationSolver } from "../SameNetTraceSegmentCombinationSolver/SameNetTraceSegmentCombinationSolver" type PipelineStep BaseSolver> = { solverName: string @@ -71,6 +72,7 @@ export class SchematicTracePipelineSolver extends BaseSolver { schematicTraceLinesSolver?: SchematicTraceLinesSolver longDistancePairSolver?: LongDistancePairSolver traceOverlapShiftSolver?: TraceOverlapShiftSolver + sameNetTraceSegmentCombinationSolver?: SameNetTraceSegmentCombinationSolver netLabelPlacementSolver?: NetLabelPlacementSolver labelMergingSolver?: MergedNetLabelObstacleSolver traceLabelOverlapAvoidanceSolver?: TraceLabelOverlapAvoidanceSolver @@ -154,19 +156,29 @@ export class SchematicTracePipelineSolver extends BaseSolver { onSolved: (_solver) => {}, }, ), + definePipelineStep( + "sameNetTraceSegmentCombinationSolver", + SameNetTraceSegmentCombinationSolver, + (instance) => [ + { + inputProblem: instance.inputProblem, + inputTraces: Object.values( + instance.traceOverlapShiftSolver!.correctedTraceMap, + ), + }, + ], + ), definePipelineStep( "netLabelPlacementSolver", NetLabelPlacementSolver, () => [ { inputProblem: this.inputProblem, - inputTraceMap: - this.traceOverlapShiftSolver?.correctedTraceMap ?? - Object.fromEntries( - this.longDistancePairSolver!.getOutput().allTracesMerged.map( - (p) => [p.mspPairId, p], - ), + inputTraceMap: Object.fromEntries( + this.sameNetTraceSegmentCombinationSolver!.getOutput().traces.map( + (p) => [p.mspPairId, p], ), + ), }, ], { @@ -179,13 +191,11 @@ export class SchematicTracePipelineSolver extends BaseSolver { "traceLabelOverlapAvoidanceSolver", TraceLabelOverlapAvoidanceSolver, (instance) => { - const traceMap = - instance.traceOverlapShiftSolver?.correctedTraceMap ?? - Object.fromEntries( - instance - .longDistancePairSolver!.getOutput() - .allTracesMerged.map((p) => [p.mspPairId, p]), - ) + const traceMap = Object.fromEntries( + instance + .sameNetTraceSegmentCombinationSolver!.getOutput() + .traces.map((p) => [p.mspPairId, p]), + ) const traces = Object.values(traceMap) const netLabelPlacements = instance.netLabelPlacementSolver!.netLabelPlacements diff --git a/tests/solvers/SameNetTraceSegmentCombinationSolver/same-net-trace-segment-combination-solver.test.ts b/tests/solvers/SameNetTraceSegmentCombinationSolver/same-net-trace-segment-combination-solver.test.ts new file mode 100644 index 000000000..d29e55c96 --- /dev/null +++ b/tests/solvers/SameNetTraceSegmentCombinationSolver/same-net-trace-segment-combination-solver.test.ts @@ -0,0 +1,118 @@ +import { expect, test } from "bun:test" +import type { Point } from "@tscircuit/math-utils" +import { SameNetTraceSegmentCombinationSolver } from "lib/solvers/SameNetTraceSegmentCombinationSolver/SameNetTraceSegmentCombinationSolver" +import type { SolvedTracePath } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceLinesSolver" +import type { InputProblem } from "lib/types/InputProblem" + +const inputProblem: InputProblem = { + chips: [], + directConnections: [], + netConnections: [], + availableNetLabelOrientations: {}, +} + +const makeTrace = ( + mspPairId: string, + globalConnNetId: string, + tracePath: Point[], +): SolvedTracePath => ({ + mspPairId, + dcConnNetId: globalConnNetId, + globalConnNetId, + pins: [ + { pinId: `${mspPairId}.a`, chipId: "chip", ...tracePath[0]! }, + { + pinId: `${mspPairId}.b`, + chipId: "chip", + ...tracePath[tracePath.length - 1]!, + }, + ], + tracePath, + mspConnectionPairIds: [mspPairId], + pinIds: [`${mspPairId}.a`, `${mspPairId}.b`], +}) + +test("combines an internal same-net segment onto a nearby containing segment", () => { + const solver = new SameNetTraceSegmentCombinationSolver({ + inputProblem, + inputTraces: [ + makeTrace("a", "net1", [ + { x: 0, y: 1 }, + { x: 0, y: 0.08 }, + { x: 1, y: 0.08 }, + { x: 1, y: 1 }, + ]), + makeTrace("b", "net1", [ + { x: -0.25, y: 0 }, + { x: 1.25, y: 0 }, + ]), + ], + }) + + solver.solve() + + expect(solver.getOutput().traces[0]!.tracePath).toEqual([ + { x: 0, y: 1 }, + { x: 0, y: 0 }, + { x: 1, y: 0 }, + { x: 1, y: 1 }, + ]) +}) + +test("does not move segments that are not contained by the nearby segment", () => { + const solver = new SameNetTraceSegmentCombinationSolver({ + inputProblem, + inputTraces: [ + makeTrace("a", "net1", [ + { x: 0, y: 1 }, + { x: 0, y: 0.08 }, + { x: 1, y: 0.08 }, + { x: 1, y: 1 }, + ]), + makeTrace("b", "net1", [ + { x: 0.5, y: 0 }, + { x: 1.5, y: 0 }, + ]), + ], + }) + + solver.solve() + + expect(solver.getOutput().traces[0]!.tracePath).toEqual([ + { x: 0, y: 1 }, + { x: 0, y: 0.08 }, + { x: 1, y: 0.08 }, + { x: 1, y: 1 }, + ]) +}) + +test("does not introduce intersections with different-net traces", () => { + const solver = new SameNetTraceSegmentCombinationSolver({ + inputProblem, + inputTraces: [ + makeTrace("a", "net1", [ + { x: 0, y: 1 }, + { x: 0, y: 0.08 }, + { x: 1, y: 0.08 }, + { x: 1, y: 1 }, + ]), + makeTrace("b", "net1", [ + { x: -0.25, y: 0 }, + { x: 1.25, y: 0 }, + ]), + makeTrace("c", "net2", [ + { x: -0.1, y: 0.04 }, + { x: 0.1, y: 0.04 }, + ]), + ], + }) + + solver.solve() + + expect(solver.getOutput().traces[0]!.tracePath).toEqual([ + { x: 0, y: 1 }, + { x: 0, y: 0.08 }, + { x: 1, y: 0.08 }, + { x: 1, y: 1 }, + ]) +})