From 16839b810e2633a4840fe6d037579193fdd6d48a Mon Sep 17 00:00:00 2001 From: Mohamed-elgypaly Date: Thu, 21 May 2026 14:15:26 +0300 Subject: [PATCH 1/2] feat: implement TraceSegmentCombinerSolver to combine same-net trace segments (#29) --- .../SchematicTracePipelineSolver.ts | 55 ++++--- .../TraceSegmentCombinerSolver.ts | 151 ++++++++++++++++++ .../trace-segment-combiner-solver.test.ts | 92 +++++++++++ 3 files changed, 277 insertions(+), 21 deletions(-) create mode 100644 lib/solvers/TraceSegmentCombinerSolver/TraceSegmentCombinerSolver.ts create mode 100644 tests/solvers/trace-segment-combiner-solver.test.ts diff --git a/lib/solvers/SchematicTracePipelineSolver/SchematicTracePipelineSolver.ts b/lib/solvers/SchematicTracePipelineSolver/SchematicTracePipelineSolver.ts index 59821f0c1..4b9c3b238 100644 --- a/lib/solvers/SchematicTracePipelineSolver/SchematicTracePipelineSolver.ts +++ b/lib/solvers/SchematicTracePipelineSolver/SchematicTracePipelineSolver.ts @@ -6,26 +6,27 @@ import type { GraphicsObject } from "graphics-debug" import { BaseSolver } from "lib/solvers/BaseSolver/BaseSolver" import type { InputProblem } from "lib/types/InputProblem" +import { AvailableNetOrientationSolver } from "../AvailableNetOrientationSolver/AvailableNetOrientationSolver" +import { Example28Solver } from "../Example28Solver/Example28Solver" +import { LongDistancePairSolver } from "../LongDistancePairSolver/LongDistancePairSolver" import { MspConnectionPairSolver } from "../MspConnectionPairSolver/MspConnectionPairSolver" +import { NetLabelPlacementSolver } from "../NetLabelPlacementSolver/NetLabelPlacementSolver" +import { NetLabelTraceCollisionSolver } from "../NetLabelTraceCollisionSolver/NetLabelTraceCollisionSolver" import { SchematicTraceLinesSolver, type SolvedTracePath, } from "../SchematicTraceLinesSolver/SchematicTraceLinesSolver" +import { TraceAnchoredNetLabelOverlapSolver } from "../TraceAnchoredNetLabelOverlapSolver/TraceAnchoredNetLabelOverlapSolver" +import { TraceCleanupSolver } from "../TraceCleanupSolver/TraceCleanupSolver" +import type { MergedNetLabelObstacleSolver } from "../TraceLabelOverlapAvoidanceSolver/sub-solvers/LabelMergingSolver/LabelMergingSolver" +import { TraceLabelOverlapAvoidanceSolver } from "../TraceLabelOverlapAvoidanceSolver/TraceLabelOverlapAvoidanceSolver" import { TraceOverlapShiftSolver } from "../TraceOverlapShiftSolver/TraceOverlapShiftSolver" -import { NetLabelPlacementSolver } from "../NetLabelPlacementSolver/NetLabelPlacementSolver" +import { TraceSegmentCombinerSolver } from "../TraceSegmentCombinerSolver/TraceSegmentCombinerSolver" +import { VccNetLabelCornerPlacementSolver } from "../VccNetLabelCornerPlacementSolver/VccNetLabelCornerPlacementSolver" import { colorAvailableNetOrientationLabels } from "./colorAvailableNetOrientationLabels" -import { visualizeInputProblem } from "./visualizeInputProblem" -import { TraceLabelOverlapAvoidanceSolver } from "../TraceLabelOverlapAvoidanceSolver/TraceLabelOverlapAvoidanceSolver" import { correctPinsInsideChips } from "./correctPinsInsideChip" import { expandChipsToFitPins } from "./expandChipsToFitPins" -import { LongDistancePairSolver } from "../LongDistancePairSolver/LongDistancePairSolver" -import { MergedNetLabelObstacleSolver } from "../TraceLabelOverlapAvoidanceSolver/sub-solvers/LabelMergingSolver/LabelMergingSolver" -import { TraceCleanupSolver } from "../TraceCleanupSolver/TraceCleanupSolver" -import { Example28Solver } from "../Example28Solver/Example28Solver" -import { AvailableNetOrientationSolver } from "../AvailableNetOrientationSolver/AvailableNetOrientationSolver" -import { VccNetLabelCornerPlacementSolver } from "../VccNetLabelCornerPlacementSolver/VccNetLabelCornerPlacementSolver" -import { TraceAnchoredNetLabelOverlapSolver } from "../TraceAnchoredNetLabelOverlapSolver/TraceAnchoredNetLabelOverlapSolver" -import { NetLabelTraceCollisionSolver } from "../NetLabelTraceCollisionSolver/NetLabelTraceCollisionSolver" +import { visualizeInputProblem } from "./visualizeInputProblem" type PipelineStep BaseSolver> = { solverName: string @@ -70,6 +71,7 @@ export class SchematicTracePipelineSolver extends BaseSolver { // guidelinesSolver?: GuidelinesSolver schematicTraceLinesSolver?: SchematicTraceLinesSolver longDistancePairSolver?: LongDistancePairSolver + traceSegmentCombinerSolver?: TraceSegmentCombinerSolver traceOverlapShiftSolver?: TraceOverlapShiftSolver netLabelPlacementSolver?: NetLabelPlacementSolver labelMergingSolver?: MergedNetLabelObstacleSolver @@ -94,7 +96,7 @@ export class SchematicTracePipelineSolver extends BaseSolver { MspConnectionPairSolver, () => [{ inputProblem: this.inputProblem }], { - onSolved: (mspSolver) => {}, + onSolved: (_mspSolver) => {}, }, ), // definePipelineStep( @@ -136,17 +138,27 @@ export class SchematicTracePipelineSolver extends BaseSolver { }, ], { - onSolved: (schematicTraceLinesSolver) => {}, + onSolved: (_schematicTraceLinesSolver) => {}, }, ), + definePipelineStep( + "traceSegmentCombinerSolver", + TraceSegmentCombinerSolver, + (instance) => [ + { + inputProblem: instance.inputProblem, + allTraces: + instance.longDistancePairSolver!.getOutput().allTracesMerged, + }, + ], + ), definePipelineStep( "traceOverlapShiftSolver", TraceOverlapShiftSolver, () => [ { inputProblem: this.inputProblem, - inputTracePaths: - this.longDistancePairSolver?.getOutput().allTracesMerged!, + inputTracePaths: this.traceSegmentCombinerSolver!.getOutput().traces!, globalConnMap: this.mspConnectionPairSolver!.globalConnMap, }, ], @@ -163,9 +175,10 @@ export class SchematicTracePipelineSolver extends BaseSolver { inputTraceMap: this.traceOverlapShiftSolver?.correctedTraceMap ?? Object.fromEntries( - this.longDistancePairSolver!.getOutput().allTracesMerged.map( - (p) => [p.mspPairId, p], - ), + this.traceSegmentCombinerSolver!.getOutput().traces.map((p) => [ + p.mspPairId, + p, + ]), ), }, ], @@ -183,8 +196,8 @@ export class SchematicTracePipelineSolver extends BaseSolver { instance.traceOverlapShiftSolver?.correctedTraceMap ?? Object.fromEntries( instance - .longDistancePairSolver!.getOutput() - .allTracesMerged.map((p) => [p.mspPairId, p]), + .traceSegmentCombinerSolver!.getOutput() + .traces.map((p: SolvedTracePath) => [p.mspPairId, p]), ) const traces = Object.values(traceMap) const netLabelPlacements = @@ -360,7 +373,7 @@ export class SchematicTracePipelineSolver extends BaseSolver { } const constructorParams = pipelineStepDef.getConstructorParams(this) - // @ts-ignore + // @ts-expect-error this.activeSubSolver = new pipelineStepDef.solverClass(...constructorParams) ;(this as any)[pipelineStepDef.solverName] = this.activeSubSolver this.timeSpentOnPhase[pipelineStepDef.solverName] = 0 diff --git a/lib/solvers/TraceSegmentCombinerSolver/TraceSegmentCombinerSolver.ts b/lib/solvers/TraceSegmentCombinerSolver/TraceSegmentCombinerSolver.ts new file mode 100644 index 000000000..100e05bec --- /dev/null +++ b/lib/solvers/TraceSegmentCombinerSolver/TraceSegmentCombinerSolver.ts @@ -0,0 +1,151 @@ +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" + +export interface TraceSegmentCombinerSolverParams { + inputProblem: InputProblem + allTraces: SolvedTracePath[] +} + +export class TraceSegmentCombinerSolver extends BaseSolver { + inputProblem: InputProblem + allTraces: SolvedTracePath[] + outputTraces: SolvedTracePath[] + + constructor(params: TraceSegmentCombinerSolverParams) { + super() + this.inputProblem = params.inputProblem + this.allTraces = params.allTraces + this.outputTraces = JSON.parse(JSON.stringify(params.allTraces)) + } + + override _step() { + this.combineSameNetTraces() + this.solved = true + } + + private combineSameNetTraces() { + const EPS = 0.1001 // Distance threshold for "close together" + const netGroups: Record = {} + + for (const trace of this.outputTraces) { + const netId = trace.globalConnNetId + if (!netGroups[netId]) netGroups[netId] = [] + netGroups[netId].push(trace) + } + + for (const netId in netGroups) { + const paths = netGroups[netId]! + if (paths.length < 2) continue + + let changed = true + while (changed) { + changed = false + for (let i = 0; i < paths.length; i++) { + for (let j = 0; j < paths.length; j++) { + if (i === j) continue + if (this.tryCombinePaths(paths[i]!, paths[j]!, EPS)) { + changed = true + } + } + } + } + } + + // Final simplification pass + for (const trace of this.outputTraces) { + trace.tracePath = simplifyPath(trace.tracePath) + } + } + + private tryCombinePaths( + pathA: SolvedTracePath, + pathB: SolvedTracePath, + eps: number, + ): boolean { + let changed = false + const ptsA = pathA.tracePath + const ptsB = pathB.tracePath + + for (let i = 0; i < ptsA.length - 1; i++) { + const a1 = ptsA[i]! + const a2 = ptsA[i + 1]! + const aVert = Math.abs(a1.x - a2.x) < 1e-6 + const aHorz = Math.abs(a1.y - a2.y) < 1e-6 + if (!aVert && !aHorz) continue + + for (let j = 0; j < ptsB.length - 1; j++) { + const b1 = ptsB[j]! + const b2 = ptsB[j + 1]! + const bVert = Math.abs(b1.x - b2.x) < 1e-6 + const bHorz = Math.abs(b1.y - b2.y) < 1e-6 + if (!bVert && !bHorz) continue + + if (aVert && bVert) { + // Both vertical, check if they are close in X and overlap in Y + const dx = Math.abs(a1.x - b1.x) + if (dx > 0 && dx < eps) { + if (this.segmentsOverlap1D(a1.y, a2.y, b1.y, b2.y)) { + // Snap B's segments to A's X + this.snapSegmentX(pathB, j, a1.x) + changed = true + } + } + } else if (aHorz && bHorz) { + // Both horizontal, check if they are close in Y and overlap in X + const dy = Math.abs(a1.y - b1.y) + if (dy > 0 && dy < eps) { + if (this.segmentsOverlap1D(a1.x, a2.x, b1.x, b2.x)) { + // Snap B's segments to A's Y + this.snapSegmentY(pathB, j, a1.y) + changed = true + } + } + } + } + } + return changed + } + + private segmentsOverlap1D( + a1: number, + a2: number, + b1: number, + b2: number, + ): boolean { + const minA = Math.min(a1, a2) + const maxA = Math.max(a1, a2) + const minB = Math.min(b1, b2) + const maxB = Math.max(b1, b2) + return Math.max(minA, minB) < Math.min(maxA, maxB) - 1e-6 + } + + private snapSegmentX(path: SolvedTracePath, segIdx: number, newX: number) { + path.tracePath[segIdx]!.x = newX + path.tracePath[segIdx + 1]!.x = newX + } + + private snapSegmentY(path: SolvedTracePath, segIdx: number, newY: number) { + path.tracePath[segIdx]!.y = newY + path.tracePath[segIdx + 1]!.y = newY + } + + getOutput() { + return { + traces: this.outputTraces, + } + } + + override visualize() { + const graphics = visualizeInputProblem(this.inputProblem) + for (const trace of this.outputTraces) { + graphics.lines!.push({ + points: trace.tracePath, + strokeColor: "blue", + }) + } + return graphics + } +} diff --git a/tests/solvers/trace-segment-combiner-solver.test.ts b/tests/solvers/trace-segment-combiner-solver.test.ts new file mode 100644 index 000000000..a16e90aef --- /dev/null +++ b/tests/solvers/trace-segment-combiner-solver.test.ts @@ -0,0 +1,92 @@ +import { expect, test } from "bun:test" +import type { SolvedTracePath } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceLinesSolver" +import { TraceSegmentCombinerSolver } from "lib/solvers/TraceSegmentCombinerSolver/TraceSegmentCombinerSolver" + +test("TraceSegmentCombinerSolver - combines parallel close segments", () => { + const allTraces: SolvedTracePath[] = [ + { + mspPairId: "trace1", + globalConnNetId: "NET1", + tracePath: [ + { x: 0, y: 0 }, + { x: 10, y: 0 }, + ], + pins: [] as any, + dcConnNetId: "NET1", + mspConnectionPairIds: ["trace1"], + pinIds: ["P1", "P3"], + }, + { + mspPairId: "trace2", + globalConnNetId: "NET1", + tracePath: [ + { x: 0, y: 0.05 }, + { x: 10, y: 0.05 }, + ], + pins: [] as any, + dcConnNetId: "NET1", + mspConnectionPairIds: ["trace2"], + pinIds: ["P2", "P4"], + }, + ] + + const solver = new TraceSegmentCombinerSolver({ + inputProblem: { + chips: [], + directConnections: [], + netConnections: [], + availableNetLabelOrientations: {}, + }, + allTraces, + }) + + solver.step() + const output = solver.getOutput().traces + + expect(output[1].tracePath[0].y).toBe(0) + expect(output[1].tracePath[1].y).toBe(0) +}) + +test("TraceSegmentCombinerSolver - does not combine different nets", () => { + const allTraces: SolvedTracePath[] = [ + { + mspPairId: "trace1", + globalConnNetId: "NET1", + tracePath: [ + { x: 0, y: 0 }, + { x: 10, y: 0 }, + ], + pins: [] as any, + dcConnNetId: "NET1", + mspConnectionPairIds: ["trace1"], + pinIds: ["P1", "P3"], + }, + { + mspPairId: "trace2", + globalConnNetId: "NET2", + tracePath: [ + { x: 0, y: 0.05 }, + { x: 10, y: 0.05 }, + ], + pins: [] as any, + dcConnNetId: "NET2", + mspConnectionPairIds: ["trace2"], + pinIds: ["P2", "P4"], + }, + ] + + const solver = new TraceSegmentCombinerSolver({ + inputProblem: { + chips: [], + directConnections: [], + netConnections: [], + availableNetLabelOrientations: {}, + }, + allTraces, + }) + + solver.step() + const output = solver.getOutput().traces + + expect(output[1].tracePath[0].y).toBe(0.05) +}) From 5b84821c82d42c7cfb50802534def5d5311fc497 Mon Sep 17 00:00:00 2001 From: Mohamed-elgypaly Date: Thu, 21 May 2026 15:09:19 +0300 Subject: [PATCH 2/2] chore: update snapshot for example29 after trace combiner implementation --- .../examples/__snapshots__/example29.snap.svg | 150 ++++++------------ 1 file changed, 52 insertions(+), 98 deletions(-) diff --git a/tests/examples/__snapshots__/example29.snap.svg b/tests/examples/__snapshots__/example29.snap.svg index 7f0d46494..5fa82123f 100644 --- a/tests/examples/__snapshots__/example29.snap.svg +++ b/tests/examples/__snapshots__/example29.snap.svg @@ -489,188 +489,142 @@ x+" data-x="-8.4" data-y="-16.6" cx="198.31710258539454" cy="392.52251162760695" x+" data-x="-8.4" data-y="-17" cx="198.31710258539454" cy="399.68032912258366" r="3" fill="hsl(226, 100%, 50%, 0.8)" /> - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + @@ -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" /> +globalConnNetId: connectivity_net11" data-x="-5.875" data-y="-10" x="239.4745531815111" y="272.6290685867457" width="8.052544681848872" height="3.5789087474884127" fill="hsl(80, 100%, 50%, 0.35)" stroke="black" stroke-width="0.05588295598214286" />