From 83f5dbed6ed53af2fcf1121f802fff9978d4bbb6 Mon Sep 17 00:00:00 2001 From: lezebomb <2498179128@qq.com> Date: Sat, 16 May 2026 16:00:32 +0800 Subject: [PATCH 1/5] feat: merge close same-net trace segments --- .../SameNetTraceSegmentMergeSolver.ts | 325 ++++++++++++++++++ .../SchematicTracePipelineSolver.ts | 16 + .../SameNetTraceSegmentMergeSolver.test.ts | 83 +++++ 3 files changed, 424 insertions(+) create mode 100644 lib/solvers/SameNetTraceSegmentMergeSolver/SameNetTraceSegmentMergeSolver.ts create mode 100644 tests/solvers/SameNetTraceSegmentMergeSolver/SameNetTraceSegmentMergeSolver.test.ts diff --git a/lib/solvers/SameNetTraceSegmentMergeSolver/SameNetTraceSegmentMergeSolver.ts b/lib/solvers/SameNetTraceSegmentMergeSolver/SameNetTraceSegmentMergeSolver.ts new file mode 100644 index 00000000..5d5dbf3f --- /dev/null +++ b/lib/solvers/SameNetTraceSegmentMergeSolver/SameNetTraceSegmentMergeSolver.ts @@ -0,0 +1,325 @@ +import { doSegmentsIntersect, type Point } from "@tscircuit/math-utils" +import { BaseSolver } from "lib/solvers/BaseSolver/BaseSolver" +import type { InputProblem } from "lib/types/InputProblem" +import type { SolvedTracePath } from "../SchematicTraceLinesSolver/SchematicTraceLinesSolver" +import { visualizeInputProblem } from "../SchematicTracePipelineSolver/visualizeInputProblem" +import { simplifyPath } from "../TraceCleanupSolver/simplifyPath" + +const EPS = 1e-6 +const DEFAULT_MERGE_DISTANCE = 0.25 + +type SegmentOrientation = "horizontal" | "vertical" + +interface SegmentLocator { + traceIndex: number + segmentIndex: number + orientation: SegmentOrientation + p1: Point + p2: Point +} + +export interface SameNetTraceSegmentMergeSolverParams { + inputProblem: InputProblem + inputTracePaths: SolvedTracePath[] + mergeDistance?: number +} + +export class SameNetTraceSegmentMergeSolver extends BaseSolver { + inputProblem: InputProblem + mergeDistance: number + outputTraces: SolvedTracePath[] + + constructor(private params: SameNetTraceSegmentMergeSolverParams) { + super() + this.inputProblem = params.inputProblem + this.mergeDistance = params.mergeDistance ?? DEFAULT_MERGE_DISTANCE + this.outputTraces = params.inputTracePaths.map((trace) => ({ + ...trace, + tracePath: trace.tracePath.map((point) => ({ ...point })), + })) + } + + override getConstructorParams(): ConstructorParameters< + typeof SameNetTraceSegmentMergeSolver + >[0] { + return this.params + } + + override _step() { + const merged = this.mergeNextCloseSameNetSegment() + if (!merged) { + this.solved = true + } + } + + private mergeNextCloseSameNetSegment(): boolean { + for (let i = 0; i < this.outputTraces.length; i++) { + const traceA = this.outputTraces[i]! + for (let j = i + 1; j < this.outputTraces.length; j++) { + const traceB = this.outputTraces[j]! + if (traceA.globalConnNetId !== traceB.globalConnNetId) continue + + const segmentAList = getOrthogonalSegments(traceA.tracePath, i) + const segmentBList = getOrthogonalSegments(traceB.tracePath, j) + + for (const segmentA of segmentAList) { + for (const segmentB of segmentBList) { + if (segmentA.orientation !== segmentB.orientation) continue + + const candidate = getMergeCandidate( + segmentA, + segmentB, + this.mergeDistance, + ) + if (!candidate) continue + + const targetSegment = + candidate.moveTraceIndex === i ? segmentA : segmentB + const originalTrace = this.outputTraces[candidate.moveTraceIndex]! + const candidatePath = moveSegmentOntoCoordinate( + originalTrace.tracePath, + targetSegment.segmentIndex, + targetSegment.orientation, + candidate.targetCoordinate, + ) + + if ( + doesIntroduceDifferentNetIntersection({ + originalPath: originalTrace.tracePath, + candidatePath, + sourceTraceIndex: candidate.moveTraceIndex, + sourceNetId: originalTrace.globalConnNetId, + allTraces: this.outputTraces, + }) + ) { + continue + } + + this.outputTraces[candidate.moveTraceIndex] = { + ...originalTrace, + tracePath: candidatePath, + } + return true + } + } + } + } + + return false + } + + getOutput() { + return { + traces: this.outputTraces, + traceMap: Object.fromEntries( + this.outputTraces.map((trace) => [trace.mspPairId, trace]), + ) as Record, + } + } + + override visualize() { + const graphics = visualizeInputProblem(this.inputProblem, { + chipAlpha: 0.1, + connectionAlpha: 0.1, + }) + + for (const trace of this.outputTraces) { + graphics.lines!.push({ + points: trace.tracePath, + strokeColor: "purple", + }) + } + + return graphics + } +} + +const getOrthogonalSegments = ( + tracePath: Point[], + traceIndex: number, +): SegmentLocator[] => { + const segments: SegmentLocator[] = [] + for ( + let segmentIndex = 0; + segmentIndex < tracePath.length - 1; + segmentIndex++ + ) { + const p1 = tracePath[segmentIndex]! + const p2 = tracePath[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 (Math.abs(p1.x - p2.x) < EPS && Math.abs(p1.y - p2.y) < EPS) continue + + segments.push({ + traceIndex, + segmentIndex, + orientation: isHorizontal ? "horizontal" : "vertical", + p1, + p2, + }) + } + return segments +} + +const getMergeCandidate = ( + segmentA: SegmentLocator, + segmentB: SegmentLocator, + mergeDistance: number, +): { moveTraceIndex: number; targetCoordinate: number } | null => { + const gap = + segmentA.orientation === "horizontal" + ? Math.abs(segmentA.p1.y - segmentB.p1.y) + : Math.abs(segmentA.p1.x - segmentB.p1.x) + + if (gap < EPS || gap > mergeDistance) return null + if (!projectedRangesOverlap(segmentA, segmentB)) return null + + const lengthA = getSegmentLength(segmentA) + const lengthB = getSegmentLength(segmentB) + + return lengthA >= lengthB + ? { + moveTraceIndex: segmentB.traceIndex, + targetCoordinate: + segmentA.orientation === "horizontal" ? segmentA.p1.y : segmentA.p1.x, + } + : { + moveTraceIndex: segmentA.traceIndex, + targetCoordinate: + segmentB.orientation === "horizontal" ? segmentB.p1.y : segmentB.p1.x, + } +} + +const projectedRangesOverlap = ( + segmentA: SegmentLocator, + segmentB: SegmentLocator, +) => { + const [aMin, aMax] = getProjectedRange(segmentA) + const [bMin, bMax] = getProjectedRange(segmentB) + return Math.min(aMax, bMax) - Math.max(aMin, bMin) > EPS +} + +const getProjectedRange = (segment: SegmentLocator): [number, number] => { + if (segment.orientation === "horizontal") { + return [ + Math.min(segment.p1.x, segment.p2.x), + Math.max(segment.p1.x, segment.p2.x), + ] + } + return [ + Math.min(segment.p1.y, segment.p2.y), + Math.max(segment.p1.y, segment.p2.y), + ] +} + +const getSegmentLength = (segment: SegmentLocator) => { + if (segment.orientation === "horizontal") { + return Math.abs(segment.p1.x - segment.p2.x) + } + return Math.abs(segment.p1.y - segment.p2.y) +} + +const moveSegmentOntoCoordinate = ( + tracePath: Point[], + segmentIndex: number, + orientation: SegmentOrientation, + targetCoordinate: number, +): Point[] => { + const start = tracePath[segmentIndex]! + const end = tracePath[segmentIndex + 1]! + const movedStart = + orientation === "horizontal" + ? { x: start.x, y: targetCoordinate } + : { x: targetCoordinate, y: start.y } + const movedEnd = + orientation === "horizontal" + ? { x: end.x, y: targetCoordinate } + : { x: targetCoordinate, y: end.y } + + return simplifyPath( + removeConsecutiveDuplicatePoints([ + ...tracePath.slice(0, segmentIndex + 1), + movedStart, + movedEnd, + ...tracePath.slice(segmentIndex + 1), + ]), + ) +} + +const removeConsecutiveDuplicatePoints = (path: Point[]) => + path.filter((point, index) => { + if (index === 0) return true + const previous = path[index - 1]! + return ( + Math.abs(point.x - previous.x) > EPS || + Math.abs(point.y - previous.y) > EPS + ) + }) + +const doesIntroduceDifferentNetIntersection = ({ + originalPath, + candidatePath, + sourceTraceIndex, + sourceNetId, + allTraces, +}: { + originalPath: Point[] + candidatePath: Point[] + sourceTraceIndex: number + sourceNetId: string + allTraces: SolvedTracePath[] +}) => { + const originalIntersections = countDifferentNetIntersections({ + path: originalPath, + sourceTraceIndex, + sourceNetId, + allTraces, + }) + const candidateIntersections = countDifferentNetIntersections({ + path: candidatePath, + sourceTraceIndex, + sourceNetId, + allTraces, + }) + + return candidateIntersections > originalIntersections +} + +const countDifferentNetIntersections = ({ + path, + sourceTraceIndex, + sourceNetId, + allTraces, +}: { + path: Point[] + sourceTraceIndex: number + sourceNetId: string + allTraces: SolvedTracePath[] +}) => { + let count = 0 + for (let i = 0; i < path.length - 1; i++) { + const p1 = path[i]! + const p2 = path[i + 1]! + for (let traceIndex = 0; traceIndex < allTraces.length; traceIndex++) { + if (traceIndex === sourceTraceIndex) continue + const otherTrace = allTraces[traceIndex]! + if (otherTrace.globalConnNetId === sourceNetId) continue + + for (let j = 0; j < otherTrace.tracePath.length - 1; j++) { + if ( + doSegmentsIntersect( + p1, + p2, + otherTrace.tracePath[j]!, + otherTrace.tracePath[j + 1]!, + ) + ) { + count++ + } + } + } + } + return count +} diff --git a/lib/solvers/SchematicTracePipelineSolver/SchematicTracePipelineSolver.ts b/lib/solvers/SchematicTracePipelineSolver/SchematicTracePipelineSolver.ts index 59821f0c..e358ecd2 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 @@ -71,6 +72,7 @@ export class SchematicTracePipelineSolver extends BaseSolver { schematicTraceLinesSolver?: SchematicTraceLinesSolver longDistancePairSolver?: LongDistancePairSolver traceOverlapShiftSolver?: TraceOverlapShiftSolver + sameNetTraceSegmentMergeSolver?: SameNetTraceSegmentMergeSolver netLabelPlacementSolver?: NetLabelPlacementSolver labelMergingSolver?: MergedNetLabelObstacleSolver traceLabelOverlapAvoidanceSolver?: TraceLabelOverlapAvoidanceSolver @@ -154,6 +156,18 @@ export class SchematicTracePipelineSolver extends BaseSolver { onSolved: (_solver) => {}, }, ), + definePipelineStep( + "sameNetTraceSegmentMergeSolver", + SameNetTraceSegmentMergeSolver, + (instance) => [ + { + inputProblem: instance.inputProblem, + inputTracePaths: Object.values( + instance.traceOverlapShiftSolver!.correctedTraceMap, + ), + }, + ], + ), definePipelineStep( "netLabelPlacementSolver", NetLabelPlacementSolver, @@ -161,6 +175,7 @@ export class SchematicTracePipelineSolver extends BaseSolver { { inputProblem: this.inputProblem, inputTraceMap: + this.sameNetTraceSegmentMergeSolver?.getOutput().traceMap ?? this.traceOverlapShiftSolver?.correctedTraceMap ?? Object.fromEntries( this.longDistancePairSolver!.getOutput().allTracesMerged.map( @@ -180,6 +195,7 @@ export class SchematicTracePipelineSolver extends BaseSolver { TraceLabelOverlapAvoidanceSolver, (instance) => { const traceMap = + instance.sameNetTraceSegmentMergeSolver?.getOutput().traceMap ?? instance.traceOverlapShiftSolver?.correctedTraceMap ?? Object.fromEntries( instance diff --git a/tests/solvers/SameNetTraceSegmentMergeSolver/SameNetTraceSegmentMergeSolver.test.ts b/tests/solvers/SameNetTraceSegmentMergeSolver/SameNetTraceSegmentMergeSolver.test.ts new file mode 100644 index 00000000..f1d43d24 --- /dev/null +++ b/tests/solvers/SameNetTraceSegmentMergeSolver/SameNetTraceSegmentMergeSolver.test.ts @@ -0,0 +1,83 @@ +import { expect, test } from "bun:test" +import { SameNetTraceSegmentMergeSolver } from "lib/solvers/SameNetTraceSegmentMergeSolver/SameNetTraceSegmentMergeSolver" +import type { SolvedTracePath } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceLinesSolver" +import type { InputProblem } from "lib/types/InputProblem" + +const inputProblem: InputProblem = { + chips: [], + directConnections: [], + netConnections: [], + availableNetLabelOrientations: {}, +} + +const createTrace = ( + mspPairId: string, + globalConnNetId: string, + tracePath: SolvedTracePath["tracePath"], +): SolvedTracePath => ({ + mspPairId, + dcConnNetId: globalConnNetId, + globalConnNetId, + pins: [] as any, + tracePath, + mspConnectionPairIds: [mspPairId], + pinIds: [], +}) + +test("merges close parallel same-net segments onto the longer segment", () => { + const solver = new SameNetTraceSegmentMergeSolver({ + inputProblem, + inputTracePaths: [ + createTrace("main", "GND", [ + { x: 0, y: 0 }, + { x: 4, y: 0 }, + ]), + createTrace("branch", "GND", [ + { x: 1, y: 0.4 }, + { x: 1, y: 0.12 }, + { x: 3, y: 0.12 }, + { x: 3, y: 0.4 }, + ]), + ], + }) + + solver.solve() + + expect(solver.getOutput().traceMap.branch.tracePath).toEqual([ + { x: 1, y: 0.4 }, + { x: 1, y: 0 }, + { x: 3, y: 0 }, + { x: 3, y: 0.4 }, + ]) +}) + +test("rejects a same-net merge that would introduce a different-net intersection", () => { + const solver = new SameNetTraceSegmentMergeSolver({ + inputProblem, + inputTracePaths: [ + createTrace("main", "GND", [ + { x: 0, y: 0 }, + { x: 4, y: 0 }, + ]), + createTrace("branch", "GND", [ + { x: 1, y: 0.4 }, + { x: 1, y: 0.12 }, + { x: 3, y: 0.12 }, + { x: 3, y: 0.4 }, + ]), + createTrace("blocker", "SIG", [ + { x: 2, y: -0.05 }, + { x: 2, y: 0.05 }, + ]), + ], + }) + + solver.solve() + + expect(solver.getOutput().traceMap.branch.tracePath).toEqual([ + { x: 1, y: 0.4 }, + { x: 1, y: 0.12 }, + { x: 3, y: 0.12 }, + { x: 3, y: 0.4 }, + ]) +}) From a4ad263bec88314e5b3d5ed67572d83c74da9667 Mon Sep 17 00:00:00 2001 From: lezebomb <2498179128@qq.com> Date: Sat, 16 May 2026 16:24:34 +0800 Subject: [PATCH 2/5] test: update snapshots for same-net trace merge --- .../examples/__snapshots__/example14.snap.svg | 50 +- .../examples/__snapshots__/example15.snap.svg | 599 ++++++++---------- .../examples/__snapshots__/example18.snap.svg | 139 ++-- 3 files changed, 323 insertions(+), 465 deletions(-) diff --git a/tests/examples/__snapshots__/example14.snap.svg b/tests/examples/__snapshots__/example14.snap.svg index b91ea230..39785b8e 100644 --- a/tests/examples/__snapshots__/example14.snap.svg +++ b/tests/examples/__snapshots__/example14.snap.svg @@ -73,36 +73,28 @@ y-" data-x="1.2000000000000002" data-y="1.1500000000000001" cx="419.076923076923 y+" data-x="1.2000000000000002" data-y="2.25" cx="419.07692307692315" cy="98.15384615384613" r="3" fill="hsl(349, 100%, 50%, 0.8)" /> - + - + - + - + - + - + - + - + @@ -150,7 +142,7 @@ orientation: y+" data-x="1.4755000000000003" data-y="-1.2944553500000002" cx="44 - + @@ -196,43 +188,35 @@ orientation: y+" data-x="1.4755000000000003" data-y="-1.2944553500000002" cx="44 +globalConnNetId: connectivity_net4" data-x="1.3510000000000002" data-y="-0.5760000000000001" x="423.4707692307693" y="322.24" width="17.230769230769226" height="38.769230769230774" fill="#00000066" stroke="#000000" stroke-width="0.011607142857142856" /> +globalConnNetId: connectivity_net4" data-x="-1.2000000000000002" data-y="-2.6750000000000003" x="203.6923076923077" y="503.0769230769231" width="17.230769230769255" height="38.76923076923083" fill="#00000066" stroke="#000000" stroke-width="0.011607142857142856" /> +globalConnNetId: connectivity_net1" data-x="-1.6250000000000002" data-y="-0.30000000000000004" x="156.30769230769232" y="309.2307692307692" width="38.769230769230774" height="17.230769230769226" fill="hsl(40, 100%, 50%, 0.35)" stroke="black" stroke-width="0.011607142857142856" /> +globalConnNetId: connectivity_net3" data-x="1.6250000000000002" data-y="0.09999999999999998" x="436.3076923076924" y="274.7692307692308" width="38.769230769230774" height="17.230769230769226" fill="hsl(40, 100%, 50%, 0.35)" stroke="black" stroke-width="0.011607142857142856" /> +globalConnNetId: connectivity_net5" data-x="-1.3010000000000002" data-y="0.7260000000000001" x="194.99076923076925" y="210.0676923076923" width="17.230769230769255" height="38.769230769230745" fill="#ef444466" stroke="#ef4444" stroke-width="0.011607142857142856" /> +globalConnNetId: connectivity_net5" data-x="3.2" data-y="0.525" x="582.7692307692308" y="227.3846153846154" width="17.230769230769283" height="38.769230769230745" fill="#ef444466" stroke="#ef4444" stroke-width="0.011607142857142856" /> +globalConnNetId: connectivity_net0" data-x="-1.5500000000000003" data-y="0.32500000000000007" x="173.53846153846155" y="244.6153846153846" width="17.230769230769226" height="38.769230769230745" fill="hsl(40, 100%, 50%, 0.35)" stroke="black" stroke-width="0.011607142857142856" /> +globalConnNetId: connectivity_net2" data-x="1.4755000000000003" data-y="-1.0694553500000001" x="434.19692307692316" y="364.7530763076923" width="17.230769230769226" height="38.769230769230774" fill="hsl(40, 100%, 50%, 0.35)" stroke="black" stroke-width="0.011607142857142856" />