From 9da5d6075aa67d80d1cbd130d679a71bcaf10936 Mon Sep 17 00:00:00 2001 From: kuaaq <130150518+kuaaq@users.noreply.github.com> Date: Mon, 25 May 2026 20:03:09 +0800 Subject: [PATCH 01/19] Create SameNetTraceMergerSolver.ts --- lib/solvers/SameNetTraceMergerSolver/SameNetTraceMergerSolver.ts | 1 + 1 file changed, 1 insertion(+) create mode 100644 lib/solvers/SameNetTraceMergerSolver/SameNetTraceMergerSolver.ts diff --git a/lib/solvers/SameNetTraceMergerSolver/SameNetTraceMergerSolver.ts b/lib/solvers/SameNetTraceMergerSolver/SameNetTraceMergerSolver.ts new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/lib/solvers/SameNetTraceMergerSolver/SameNetTraceMergerSolver.ts @@ -0,0 +1 @@ + From 43b2f32f7131510d58fefcad399d78e8b99da584 Mon Sep 17 00:00:00 2001 From: kuaaq <130150518+kuaaq@users.noreply.github.com> Date: Mon, 25 May 2026 20:04:33 +0800 Subject: [PATCH 02/19] Update SameNetTraceMergerSolver.ts --- .../SameNetTraceMergerSolver.ts | 124 ++++++++++++++++++ 1 file changed, 124 insertions(+) diff --git a/lib/solvers/SameNetTraceMergerSolver/SameNetTraceMergerSolver.ts b/lib/solvers/SameNetTraceMergerSolver/SameNetTraceMergerSolver.ts index 8b1378917..9e14d76ff 100644 --- a/lib/solvers/SameNetTraceMergerSolver/SameNetTraceMergerSolver.ts +++ b/lib/solvers/SameNetTraceMergerSolver/SameNetTraceMergerSolver.ts @@ -1 +1,125 @@ +import type { InputProblem } from "lib/types/InputProblem" +import type { GraphicsObject, Line } from "graphics-debug" +import { BaseSolver } from "lib/solvers/BaseSolver/BaseSolver" +import type { SolvedTracePath } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceLinesSolver" +import { visualizeInputProblem } from "lib/solvers/SchematicTracePipelineSolver/visualizeInputProblem" +interface SameNetTraceMergerSolverInput { + inputProblem: InputProblem + traces: SolvedTracePath[] + threshold?: number +} + +/** + * SameNetTraceMergerSolver snaps nearly-collinear same-net trace segments + * to the same coordinate, eliminating slight offsets introduced by routing. + * Horizontal segments close in Y and vertical segments close in X are merged. + * Runs after traceCleanupSolver in the pipeline. + */ +export class SameNetTraceMergerSolver extends BaseSolver { + private input: SameNetTraceMergerSolverInput + private outputTraces: SolvedTracePath[] + readonly threshold: number + + constructor(input: SameNetTraceMergerSolverInput) { + super() + this.input = input + this.threshold = input.threshold ?? 0.15 + this.outputTraces = input.traces.map((t) => ({ + ...t, + tracePath: t.tracePath.map((p) => ({ x: p.x, y: p.y })), + })) + } + + override _step() { + this._mergeCollinearSegments() + this.solved = true + } + + private _mergeCollinearSegments() { + const netGroups = new Map() + for (let i = 0; i < this.outputTraces.length; i++) { + const netId = this.outputTraces[i]!.globalConnNetId + if (!netGroups.has(netId)) netGroups.set(netId, []) + netGroups.get(netId)!.push(i) + } + for (const traceIndices of netGroups.values()) { + if (traceIndices.length < 2) continue + for (let a = 0; a < traceIndices.length; a++) { + for (let b = a + 1; b < traceIndices.length; b++) { + this._trySnapTraces(traceIndices[a]!, traceIndices[b]!) + } + } + } + } + + /** + * Compare every segment of trace A against every segment of trace B. + * Snap collinear overlapping segments to their average coordinate. + */ + private _trySnapTraces(idxA: number, idxB: number) { + const pathA = this.outputTraces[idxA]!.tracePath + const pathB = this.outputTraces[idxB]!.tracePath + for (let i = 0; i < pathA.length - 1; i++) { + const a1 = pathA[i]! + const a2 = pathA[i + 1]! + const isHorizA = Math.abs(a1.y - a2.y) < 1e-6 + const isVertA = Math.abs(a1.x - a2.x) < 1e-6 + if (!isHorizA && !isVertA) continue + for (let j = 0; j < pathB.length - 1; j++) { + const b1 = pathB[j]! + const b2 = pathB[j + 1]! + if (isHorizA && Math.abs(b1.y - b2.y) < 1e-6) { + const dy = Math.abs(a1.y - b1.y) + if (dy > 1e-6 && dy < this.threshold) { + const xMinA = Math.min(a1.x, a2.x) + const xMaxA = Math.max(a1.x, a2.x) + const xMinB = Math.min(b1.x, b2.x) + const xMaxB = Math.max(b1.x, b2.x) + if (xMaxA > xMinB && xMaxB > xMinA) { + const avgY = (a1.y + b1.y) / 2 + pathA[i]!.y = avgY + pathA[i + 1]!.y = avgY + pathB[j]!.y = avgY + pathB[j + 1]!.y = avgY + } + } + } else if (isVertA && Math.abs(b1.x - b2.x) < 1e-6) { + const dx = Math.abs(a1.x - b1.x) + if (dx > 1e-6 && dx < this.threshold) { + const yMinA = Math.min(a1.y, a2.y) + const yMaxA = Math.max(a1.y, a2.y) + const yMinB = Math.min(b1.y, b2.y) + const yMaxB = Math.max(b1.y, b2.y) + if (yMaxA > yMinB && yMaxB > yMinA) { + const avgX = (a1.x + b1.x) / 2 + pathA[i]!.x = avgX + pathA[i + 1]!.x = avgX + pathB[j]!.x = avgX + pathB[j + 1]!.x = avgX + } + } + } + } + } + } + + getOutput() { + return { traces: this.outputTraces } + } + + override visualize(): GraphicsObject { + const graphics = visualizeInputProblem(this.input.inputProblem, { + chipAlpha: 0.1, + connectionAlpha: 0.1, + }) + if (!graphics.lines) graphics.lines = [] + for (const trace of this.outputTraces) { + graphics.lines!.push({ + points: trace.tracePath.map((p) => ({ x: p.x, y: p.y })), + strokeColor: "blue", + } as Line) + } + return graphics + } +} From 95860bac46f852172e7c5497154634db23431495 Mon Sep 17 00:00:00 2001 From: kuaaq <130150518+kuaaq@users.noreply.github.com> Date: Mon, 25 May 2026 20:10:07 +0800 Subject: [PATCH 03/19] Update SchematicTracePipelineSolver.ts --- .../SchematicTracePipelineSolver.ts | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/lib/solvers/SchematicTracePipelineSolver/SchematicTracePipelineSolver.ts b/lib/solvers/SchematicTracePipelineSolver/SchematicTracePipelineSolver.ts index 59821f0c1..f5cbe8884 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 { SameNetTraceMergerSolver } from "../SameNetTraceMergerSolver/SameNetTraceMergerSolver" type PipelineStep BaseSolver> = { solverName: string @@ -75,6 +76,7 @@ export class SchematicTracePipelineSolver extends BaseSolver { labelMergingSolver?: MergedNetLabelObstacleSolver traceLabelOverlapAvoidanceSolver?: TraceLabelOverlapAvoidanceSolver traceCleanupSolver?: TraceCleanupSolver + sameNetTraceMergerSolver?: SameNetTraceMergerSolver example28Solver?: Example28Solver availableNetOrientationSolver?: AvailableNetOrientationSolver vccNetLabelCornerPlacementSolver?: VccNetLabelCornerPlacementSolver @@ -217,11 +219,22 @@ export class SchematicTracePipelineSolver extends BaseSolver { }, ] }), + definePipelineStep( + "sameNetTraceMergerSolver", + SameNetTraceMergerSolver, + (instance) => { + const traces = + instance.traceCleanupSolver?.getOutput().traces ?? + instance.traceLabelOverlapAvoidanceSolver!.getOutput().traces + return [{ inputProblem: instance.inputProblem, traces }] + }, + ), definePipelineStep( "netLabelPlacementSolver", NetLabelPlacementSolver, (instance) => { const traces = + instance.sameNetTraceMergerSolver?.getOutput().traces ?? instance.traceCleanupSolver?.getOutput().traces ?? instance.traceLabelOverlapAvoidanceSolver!.getOutput().traces @@ -237,6 +250,7 @@ export class SchematicTracePipelineSolver extends BaseSolver { ), definePipelineStep("example28Solver", Example28Solver, (instance) => { const traces = + instance.sameNetTraceMergerSolver?.getOutput().traces ?? instance.traceCleanupSolver?.getOutput().traces ?? instance.traceLabelOverlapAvoidanceSolver!.getOutput().traces @@ -411,7 +425,6 @@ export class SchematicTracePipelineSolver extends BaseSolver { return visualizations[0]! } - // Simple combination of visualizations const finalGraphics = { points: visualizations.flatMap((v) => v.points || []), rects: visualizations.flatMap((v) => v.rects || []), From 1a04c086ae0176fdc42a0295b4396e283d7e0502 Mon Sep 17 00:00:00 2001 From: kuaaq <130150518+kuaaq@users.noreply.github.com> Date: Mon, 25 May 2026 20:11:04 +0800 Subject: [PATCH 04/19] Create same-net-trace-merger.test.ts --- tests/same-net-trace-merger.test.ts | 113 ++++++++++++++++++++++++++++ 1 file changed, 113 insertions(+) create mode 100644 tests/same-net-trace-merger.test.ts diff --git a/tests/same-net-trace-merger.test.ts b/tests/same-net-trace-merger.test.ts new file mode 100644 index 000000000..0d67c34cf --- /dev/null +++ b/tests/same-net-trace-merger.test.ts @@ -0,0 +1,113 @@ +import { describe, test, expect } from "bun:test" +import { SameNetTraceMergerSolver } from "lib/solvers/SameNetTraceMergerSolver/SameNetTraceMergerSolver" +import type { SolvedTracePath } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceLinesSolver" + +const makeTrace = ( + id: string, + netId: string, + points: { x: number; y: number }[], +): SolvedTracePath => + ({ + mspPairId: id, + globalConnNetId: netId, + dcConnNetId: netId, + mspConnectionPairIds: [id], + pinIds: [], + tracePath: points, + }) as any + +describe("SameNetTraceMergerSolver", () => { + test("snaps two nearly-horizontal same-net segments to the same Y", () => { + const traces = [ + makeTrace("t1", "net1", [ + { x: 0, y: 0 }, + { x: 2, y: 0 }, + ]), + makeTrace("t2", "net1", [ + { x: 0, y: 0.1 }, + { x: 2, y: 0.1 }, + ]), + ] + + const solver = new SameNetTraceMergerSolver({ + inputProblem: { chips: [], connections: [] } as any, + traces, + threshold: 0.15, + }) + solver.solve() + + const out = solver.getOutput().traces + expect(out[0]!.tracePath[0]!.y).toBeCloseTo(out[1]!.tracePath[0]!.y, 5) + }) + + test("does NOT snap segments from different nets", () => { + const traces = [ + makeTrace("t1", "net1", [ + { x: 0, y: 0 }, + { x: 2, y: 0 }, + ]), + makeTrace("t2", "net2", [ + { x: 0, y: 0.1 }, + { x: 2, y: 0.1 }, + ]), + ] + + const solver = new SameNetTraceMergerSolver({ + inputProblem: { chips: [], connections: [] } as any, + traces, + threshold: 0.15, + }) + solver.solve() + + const out = solver.getOutput().traces + expect(out[0]!.tracePath[0]!.y).toBeCloseTo(0, 5) + expect(out[1]!.tracePath[0]!.y).toBeCloseTo(0.1, 5) + }) + + test("does NOT snap segments farther apart than the threshold", () => { + const traces = [ + makeTrace("t1", "net1", [ + { x: 0, y: 0 }, + { x: 2, y: 0 }, + ]), + makeTrace("t2", "net1", [ + { x: 0, y: 0.5 }, + { x: 2, y: 0.5 }, + ]), + ] + + const solver = new SameNetTraceMergerSolver({ + inputProblem: { chips: [], connections: [] } as any, + traces, + threshold: 0.15, + }) + solver.solve() + + const out = solver.getOutput().traces + expect(out[0]!.tracePath[0]!.y).toBeCloseTo(0, 5) + expect(out[1]!.tracePath[0]!.y).toBeCloseTo(0.5, 5) + }) + + test("snaps two nearly-vertical same-net segments to the same X", () => { + const traces = [ + makeTrace("t1", "net1", [ + { x: 0, y: 0 }, + { x: 0, y: 2 }, + ]), + makeTrace("t2", "net1", [ + { x: 0.1, y: 0 }, + { x: 0.1, y: 2 }, + ]), + ] + + const solver = new SameNetTraceMergerSolver({ + inputProblem: { chips: [], connections: [] } as any, + traces, + threshold: 0.15, + }) + solver.solve() + + const out = solver.getOutput().traces + expect(out[0]!.tracePath[0]!.x).toBeCloseTo(out[1]!.tracePath[0]!.x, 5) + }) +}) From 64489ba1986e209afd11d23cc12087b28ce619c2 Mon Sep 17 00:00:00 2001 From: kuaaq <130150518+kuaaq@users.noreply.github.com> Date: Tue, 26 May 2026 03:05:39 +0800 Subject: [PATCH 05/19] feat: add visualize() and getOutput() to SameNetTraceMergerSolver --- .../SameNetTraceMergerSolver.ts | 44 ++++++++++++------- 1 file changed, 27 insertions(+), 17 deletions(-) diff --git a/lib/solvers/SameNetTraceMergerSolver/SameNetTraceMergerSolver.ts b/lib/solvers/SameNetTraceMergerSolver/SameNetTraceMergerSolver.ts index 9e14d76ff..5cfadb8cc 100644 --- a/lib/solvers/SameNetTraceMergerSolver/SameNetTraceMergerSolver.ts +++ b/lib/solvers/SameNetTraceMergerSolver/SameNetTraceMergerSolver.ts @@ -18,7 +18,7 @@ interface SameNetTraceMergerSolverInput { */ export class SameNetTraceMergerSolver extends BaseSolver { private input: SameNetTraceMergerSolverInput - private outputTraces: SolvedTracePath[] + outputTraces: SolvedTracePath[] readonly threshold: number constructor(input: SameNetTraceMergerSolverInput) { @@ -36,6 +36,12 @@ export class SameNetTraceMergerSolver extends BaseSolver { this.solved = true } + getOutput() { + return { + traces: this.outputTraces, + } + } + private _mergeCollinearSegments() { const netGroups = new Map() for (let i = 0; i < this.outputTraces.length; i++) { @@ -76,12 +82,13 @@ export class SameNetTraceMergerSolver extends BaseSolver { const xMaxA = Math.max(a1.x, a2.x) const xMinB = Math.min(b1.x, b2.x) const xMaxB = Math.max(b1.x, b2.x) - if (xMaxA > xMinB && xMaxB > xMinA) { + // Check X-axis overlap + if (xMinA < xMaxB && xMinB < xMaxA) { const avgY = (a1.y + b1.y) / 2 - pathA[i]!.y = avgY - pathA[i + 1]!.y = avgY - pathB[j]!.y = avgY - pathB[j + 1]!.y = avgY + a1.y = avgY + a2.y = avgY + b1.y = avgY + b2.y = avgY } } } else if (isVertA && Math.abs(b1.x - b2.x) < 1e-6) { @@ -91,12 +98,13 @@ export class SameNetTraceMergerSolver extends BaseSolver { const yMaxA = Math.max(a1.y, a2.y) const yMinB = Math.min(b1.y, b2.y) const yMaxB = Math.max(b1.y, b2.y) - if (yMaxA > yMinB && yMaxB > yMinA) { + // Check Y-axis overlap + if (yMinA < yMaxB && yMinB < yMaxA) { const avgX = (a1.x + b1.x) / 2 - pathA[i]!.x = avgX - pathA[i + 1]!.x = avgX - pathB[j]!.x = avgX - pathB[j + 1]!.x = avgX + a1.x = avgX + a2.x = avgX + b1.x = avgX + b2.x = avgX } } } @@ -104,21 +112,23 @@ export class SameNetTraceMergerSolver extends BaseSolver { } } - getOutput() { - return { traces: this.outputTraces } - } - override visualize(): GraphicsObject { const graphics = visualizeInputProblem(this.input.inputProblem, { chipAlpha: 0.1, connectionAlpha: 0.1, }) if (!graphics.lines) graphics.lines = [] + if (!graphics.points) graphics.points = [] + if (!graphics.rects) graphics.rects = [] + if (!graphics.circles) graphics.circles = [] + if (!graphics.texts) graphics.texts = [] + for (const trace of this.outputTraces) { - graphics.lines!.push({ + const line: Line = { points: trace.tracePath.map((p) => ({ x: p.x, y: p.y })), strokeColor: "blue", - } as Line) + } + graphics.lines!.push(line) } return graphics } From 695f98e92ebfa03640920b4db2e43585d02ac6e1 Mon Sep 17 00:00:00 2001 From: kuaaq <130150518+kuaaq@users.noreply.github.com> Date: Tue, 26 May 2026 03:05:41 +0800 Subject: [PATCH 06/19] test: add snapshot test and getOutput() usage for SameNetTraceMergerSolver --- tests/same-net-trace-merger.test.ts | 188 ++++++++++++++-------------- 1 file changed, 92 insertions(+), 96 deletions(-) diff --git a/tests/same-net-trace-merger.test.ts b/tests/same-net-trace-merger.test.ts index 0d67c34cf..ff40445e7 100644 --- a/tests/same-net-trace-merger.test.ts +++ b/tests/same-net-trace-merger.test.ts @@ -1,113 +1,109 @@ -import { describe, test, expect } from "bun:test" +import { expect, test } from "bun:test" import { SameNetTraceMergerSolver } from "lib/solvers/SameNetTraceMergerSolver/SameNetTraceMergerSolver" import type { SolvedTracePath } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceLinesSolver" +import inputData from "./assets/SameNetTraceMerger.test.input.json" +// Helper to build a minimal SolvedTracePath const makeTrace = ( - id: string, - netId: string, - points: { x: number; y: number }[], + mspPairId: string, + globalConnNetId: string, + points: Array<{ x: number; y: number }>, ): SolvedTracePath => ({ - mspPairId: id, - globalConnNetId: netId, - dcConnNetId: netId, - mspConnectionPairIds: [id], + mspPairId, + globalConnNetId, + dcConnNetId: globalConnNetId, + pins: [] as any, + mspConnectionPairIds: [], pinIds: [], tracePath: points, - }) as any + }) as SolvedTracePath -describe("SameNetTraceMergerSolver", () => { - test("snaps two nearly-horizontal same-net segments to the same Y", () => { - const traces = [ - makeTrace("t1", "net1", [ - { x: 0, y: 0 }, - { x: 2, y: 0 }, - ]), - makeTrace("t2", "net1", [ - { x: 0, y: 0.1 }, - { x: 2, y: 0.1 }, - ]), - ] - - const solver = new SameNetTraceMergerSolver({ - inputProblem: { chips: [], connections: [] } as any, - traces, - threshold: 0.15, - }) - solver.solve() - - const out = solver.getOutput().traces - expect(out[0]!.tracePath[0]!.y).toBeCloseTo(out[1]!.tracePath[0]!.y, 5) +test("snaps two nearly-horizontal same-net segments to same Y", () => { + const traces = [ + makeTrace("t1", "net.GND", [ + { x: 0, y: 0 }, + { x: 1, y: 0 }, + ]), + makeTrace("t2", "net.GND", [ + { x: 0, y: 0.1 }, + { x: 1, y: 0.1 }, + ]), + ] + const solver = new SameNetTraceMergerSolver({ + inputProblem: { chips: [], directConnections: [], netConnections: [], availableNetLabelOrientations: {} }, + traces, }) + solver.solve() + const out = solver.getOutput().traces + expect(out[0]!.tracePath[0]!.y).toBe(out[1]!.tracePath[0]!.y) +}) - test("does NOT snap segments from different nets", () => { - const traces = [ - makeTrace("t1", "net1", [ - { x: 0, y: 0 }, - { x: 2, y: 0 }, - ]), - makeTrace("t2", "net2", [ - { x: 0, y: 0.1 }, - { x: 2, y: 0.1 }, - ]), - ] - - const solver = new SameNetTraceMergerSolver({ - inputProblem: { chips: [], connections: [] } as any, - traces, - threshold: 0.15, - }) - solver.solve() - - const out = solver.getOutput().traces - expect(out[0]!.tracePath[0]!.y).toBeCloseTo(0, 5) - expect(out[1]!.tracePath[0]!.y).toBeCloseTo(0.1, 5) +test("does NOT snap segments from different nets", () => { + const traces = [ + makeTrace("t1", "net.VCC", [ + { x: 0, y: 0 }, + { x: 1, y: 0 }, + ]), + makeTrace("t2", "net.GND", [ + { x: 0, y: 0.1 }, + { x: 1, y: 0.1 }, + ]), + ] + const solver = new SameNetTraceMergerSolver({ + inputProblem: { chips: [], directConnections: [], netConnections: [], availableNetLabelOrientations: {} }, + traces, }) + solver.solve() + const out = solver.getOutput().traces + expect(out[0]!.tracePath[0]!.y).toBe(0) + expect(out[1]!.tracePath[0]!.y).toBe(0.1) +}) - test("does NOT snap segments farther apart than the threshold", () => { - const traces = [ - makeTrace("t1", "net1", [ - { x: 0, y: 0 }, - { x: 2, y: 0 }, - ]), - makeTrace("t2", "net1", [ - { x: 0, y: 0.5 }, - { x: 2, y: 0.5 }, - ]), - ] - - const solver = new SameNetTraceMergerSolver({ - inputProblem: { chips: [], connections: [] } as any, - traces, - threshold: 0.15, - }) - solver.solve() - - const out = solver.getOutput().traces - expect(out[0]!.tracePath[0]!.y).toBeCloseTo(0, 5) - expect(out[1]!.tracePath[0]!.y).toBeCloseTo(0.5, 5) +test("does NOT snap segments farther apart than threshold", () => { + const traces = [ + makeTrace("t1", "net.GND", [ + { x: 0, y: 0 }, + { x: 1, y: 0 }, + ]), + makeTrace("t2", "net.GND", [ + { x: 0, y: 0.5 }, + { x: 1, y: 0.5 }, + ]), + ] + const solver = new SameNetTraceMergerSolver({ + inputProblem: { chips: [], directConnections: [], netConnections: [], availableNetLabelOrientations: {} }, + traces, + threshold: 0.15, }) + solver.solve() + const out = solver.getOutput().traces + expect(out[0]!.tracePath[0]!.y).toBe(0) + expect(out[1]!.tracePath[0]!.y).toBe(0.5) +}) - test("snaps two nearly-vertical same-net segments to the same X", () => { - const traces = [ - makeTrace("t1", "net1", [ - { x: 0, y: 0 }, - { x: 0, y: 2 }, - ]), - makeTrace("t2", "net1", [ - { x: 0.1, y: 0 }, - { x: 0.1, y: 2 }, - ]), - ] - - const solver = new SameNetTraceMergerSolver({ - inputProblem: { chips: [], connections: [] } as any, - traces, - threshold: 0.15, - }) - solver.solve() - - const out = solver.getOutput().traces - expect(out[0]!.tracePath[0]!.x).toBeCloseTo(out[1]!.tracePath[0]!.x, 5) +test("snaps two nearly-vertical same-net segments to same X", () => { + const traces = [ + makeTrace("t1", "net.GND", [ + { x: 0, y: 0 }, + { x: 0, y: 1 }, + ]), + makeTrace("t2", "net.GND", [ + { x: 0.08, y: 0 }, + { x: 0.08, y: 1 }, + ]), + ] + const solver = new SameNetTraceMergerSolver({ + inputProblem: { chips: [], directConnections: [], netConnections: [], availableNetLabelOrientations: {} }, + traces, }) + solver.solve() + const out = solver.getOutput().traces + expect(out[0]!.tracePath[0]!.x).toBe(out[1]!.tracePath[0]!.x) +}) + +test("SameNetTraceMergerSolver snapshot", async () => { + const solver = new SameNetTraceMergerSolver(inputData as any) + solver.solve() + await expect(solver).toMatchSolverSnapshot(import.meta.path) }) From 891aef988fdfbfb437d30b323b2053b38b6b81dd Mon Sep 17 00:00:00 2001 From: kuaaq <130150518+kuaaq@users.noreply.github.com> Date: Tue, 26 May 2026 03:05:42 +0800 Subject: [PATCH 07/19] test: add SameNetTraceMerger snapshot test input fixture --- .../assets/SameNetTraceMerger.test.input.json | 81 +++++++++++++++++++ 1 file changed, 81 insertions(+) create mode 100644 tests/assets/SameNetTraceMerger.test.input.json diff --git a/tests/assets/SameNetTraceMerger.test.input.json b/tests/assets/SameNetTraceMerger.test.input.json new file mode 100644 index 000000000..2324c5646 --- /dev/null +++ b/tests/assets/SameNetTraceMerger.test.input.json @@ -0,0 +1,81 @@ +{ + "inputProblem": { + "chips": [ + { + "chipId": "U1", + "center": { "x": 0, "y": 0 }, + "width": 1.6, + "height": 1.0, + "pins": [ + { "pinId": "U1.1", "x": -0.8, "y": 0.3 }, + { "pinId": "U1.2", "x": -0.8, "y": 0.0 }, + { "pinId": "U1.3", "x": -0.8, "y": -0.3 } + ] + }, + { + "chipId": "C1", + "center": { "x": -2.5, "y": 0.3 }, + "width": 0.5, + "height": 0.5, + "pins": [ + { "pinId": "C1.1", "x": -2.5, "y": 0.55 }, + { "pinId": "C1.2", "x": -2.5, "y": 0.05 } + ] + }, + { + "chipId": "C2", + "center": { "x": -2.5, "y": -0.3 }, + "width": 0.5, + "height": 0.5, + "pins": [ + { "pinId": "C2.1", "x": -2.5, "y": -0.05 }, + { "pinId": "C2.2", "x": -2.5, "y": -0.55 } + ] + } + ], + "directConnections": [ + { "pinIds": ["U1.1", "C1.1"], "netId": "VCC" }, + { "pinIds": ["U1.2", "C2.1"], "netId": "EN" } + ], + "netConnections": [ + { "netId": "GND", "pinIds": ["U1.3", "C1.2", "C2.2"] } + ], + "availableNetLabelOrientations": { + "VCC": ["y+"], + "EN": ["x-"], + "GND": ["y-"] + } + }, + "traces": [ + { + "mspPairId": "U1.3-C1.2", + "dcConnNetId": "GND", + "globalConnNetId": "GND", + "userNetId": "GND", + "pins": [], + "mspConnectionPairIds": [], + "pinIds": ["U1.3", "C1.2"], + "tracePath": [ + { "x": -0.8, "y": -0.3 }, + { "x": -1.8, "y": -0.3 }, + { "x": -1.8, "y": 0.05 }, + { "x": -2.5, "y": 0.05 } + ] + }, + { + "mspPairId": "U1.3-C2.2", + "dcConnNetId": "GND", + "globalConnNetId": "GND", + "userNetId": "GND", + "pins": [], + "mspConnectionPairIds": [], + "pinIds": ["U1.3", "C2.2"], + "tracePath": [ + { "x": -0.8, "y": -0.3 }, + { "x": -1.82, "y": -0.3 }, + { "x": -1.82, "y": -0.55 }, + { "x": -2.5, "y": -0.55 } + ] + } + ] +} From fda5f6295f77b1354a10a65509e7fd555f5a3176 Mon Sep 17 00:00:00 2001 From: kuaaq <130150518+kuaaq@users.noreply.github.com> Date: Tue, 26 May 2026 03:14:30 +0800 Subject: [PATCH 08/19] fix: expand inputProblem to multiline to pass Biome format check --- tests/same-net-trace-merger.test.ts | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/tests/same-net-trace-merger.test.ts b/tests/same-net-trace-merger.test.ts index ff40445e7..16351e877 100644 --- a/tests/same-net-trace-merger.test.ts +++ b/tests/same-net-trace-merger.test.ts @@ -3,6 +3,13 @@ import { SameNetTraceMergerSolver } from "lib/solvers/SameNetTraceMergerSolver/S import type { SolvedTracePath } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceLinesSolver" import inputData from "./assets/SameNetTraceMerger.test.input.json" +const emptyProblem = { + chips: [], + directConnections: [], + netConnections: [], + availableNetLabelOrientations: {}, +} + // Helper to build a minimal SolvedTracePath const makeTrace = ( mspPairId: string, @@ -31,7 +38,7 @@ test("snaps two nearly-horizontal same-net segments to same Y", () => { ]), ] const solver = new SameNetTraceMergerSolver({ - inputProblem: { chips: [], directConnections: [], netConnections: [], availableNetLabelOrientations: {} }, + inputProblem: emptyProblem, traces, }) solver.solve() @@ -51,7 +58,7 @@ test("does NOT snap segments from different nets", () => { ]), ] const solver = new SameNetTraceMergerSolver({ - inputProblem: { chips: [], directConnections: [], netConnections: [], availableNetLabelOrientations: {} }, + inputProblem: emptyProblem, traces, }) solver.solve() @@ -72,7 +79,7 @@ test("does NOT snap segments farther apart than threshold", () => { ]), ] const solver = new SameNetTraceMergerSolver({ - inputProblem: { chips: [], directConnections: [], netConnections: [], availableNetLabelOrientations: {} }, + inputProblem: emptyProblem, traces, threshold: 0.15, }) @@ -94,7 +101,7 @@ test("snaps two nearly-vertical same-net segments to same X", () => { ]), ] const solver = new SameNetTraceMergerSolver({ - inputProblem: { chips: [], directConnections: [], netConnections: [], availableNetLabelOrientations: {} }, + inputProblem: emptyProblem, traces, }) solver.solve() From 153f7fe9f71f22eea34259e399477e5c446827f3 Mon Sep 17 00:00:00 2001 From: kuaaq <130150518+kuaaq@users.noreply.github.com> Date: Tue, 26 May 2026 03:18:12 +0800 Subject: [PATCH 09/19] fix: push properly formatted test file with newlines From 77784a96719483749ad37e05d7aa639d3ffad132 Mon Sep 17 00:00:00 2001 From: kuaaq <130150518+kuaaq@users.noreply.github.com> Date: Tue, 26 May 2026 03:18:51 +0800 Subject: [PATCH 10/19] fix: push solver with proper newlines (Biome format fix) From 1e13c3472c5e09fc5e3ab4d3af912ca5cbc404e6 Mon Sep 17 00:00:00 2001 From: kuaaq <130150518+kuaaq@users.noreply.github.com> Date: Tue, 26 May 2026 03:21:47 +0800 Subject: [PATCH 11/19] style: apply biome format fixes --- .../SameNetTraceMergerSolver/SameNetTraceMergerSolver.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/lib/solvers/SameNetTraceMergerSolver/SameNetTraceMergerSolver.ts b/lib/solvers/SameNetTraceMergerSolver/SameNetTraceMergerSolver.ts index 5cfadb8cc..4ef5f4d6d 100644 --- a/lib/solvers/SameNetTraceMergerSolver/SameNetTraceMergerSolver.ts +++ b/lib/solvers/SameNetTraceMergerSolver/SameNetTraceMergerSolver.ts @@ -82,7 +82,6 @@ export class SameNetTraceMergerSolver extends BaseSolver { const xMaxA = Math.max(a1.x, a2.x) const xMinB = Math.min(b1.x, b2.x) const xMaxB = Math.max(b1.x, b2.x) - // Check X-axis overlap if (xMinA < xMaxB && xMinB < xMaxA) { const avgY = (a1.y + b1.y) / 2 a1.y = avgY @@ -98,7 +97,6 @@ export class SameNetTraceMergerSolver extends BaseSolver { const yMaxA = Math.max(a1.y, a2.y) const yMinB = Math.min(b1.y, b2.y) const yMaxB = Math.max(b1.y, b2.y) - // Check Y-axis overlap if (yMinA < yMaxB && yMinB < yMaxA) { const avgX = (a1.x + b1.x) / 2 a1.x = avgX @@ -122,7 +120,6 @@ export class SameNetTraceMergerSolver extends BaseSolver { if (!graphics.rects) graphics.rects = [] if (!graphics.circles) graphics.circles = [] if (!graphics.texts) graphics.texts = [] - for (const trace of this.outputTraces) { const line: Line = { points: trace.tracePath.map((p) => ({ x: p.x, y: p.y })), From 551ed6f8bd0c628e18c20f659712c36bc7dc22ee Mon Sep 17 00:00:00 2001 From: kuaaq <130150518+kuaaq@users.noreply.github.com> Date: Tue, 26 May 2026 03:21:55 +0800 Subject: [PATCH 12/19] fix: collapse single-element netConnections array to pass Biome format check --- tests/assets/SameNetTraceMerger.test.input.json | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/tests/assets/SameNetTraceMerger.test.input.json b/tests/assets/SameNetTraceMerger.test.input.json index 2324c5646..981aefbbd 100644 --- a/tests/assets/SameNetTraceMerger.test.input.json +++ b/tests/assets/SameNetTraceMerger.test.input.json @@ -37,9 +37,7 @@ { "pinIds": ["U1.1", "C1.1"], "netId": "VCC" }, { "pinIds": ["U1.2", "C2.1"], "netId": "EN" } ], - "netConnections": [ - { "netId": "GND", "pinIds": ["U1.3", "C1.2", "C2.2"] } - ], + "netConnections": [{ "netId": "GND", "pinIds": ["U1.3", "C1.2", "C2.2"] }], "availableNetLabelOrientations": { "VCC": ["y+"], "EN": ["x-"], From eee537338dcbebcfe2a050b36e6f729ce341c916 Mon Sep 17 00:00:00 2001 From: kuaaq <130150518+kuaaq@users.noreply.github.com> Date: Tue, 26 May 2026 03:22:22 +0800 Subject: [PATCH 13/19] feat: export SameNetTraceMergerSolver from index --- lib/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/index.ts b/lib/index.ts index 3985b32ac..7899d3530 100644 --- a/lib/index.ts +++ b/lib/index.ts @@ -1,3 +1,4 @@ export * from "./solvers/SchematicTracePipelineSolver/SchematicTracePipelineSolver" export * from "./types/InputProblem" export { SchematicTraceSingleLineSolver2 } from "./solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver2/SchematicTraceSingleLineSolver2" +export * from "./solvers/SameNetTraceMergerSolver/SameNetTraceMergerSolver" From 9e44bb8c23a81321c07b1954f07626334fdf9527 Mon Sep 17 00:00:00 2001 From: kuaaq <130150518+kuaaq@users.noreply.github.com> Date: Tue, 26 May 2026 03:26:14 +0800 Subject: [PATCH 14/19] style: apply biome format to test input JSON --- .../assets/SameNetTraceMerger.test.input.json | 146 ++++++++++++++---- 1 file changed, 120 insertions(+), 26 deletions(-) diff --git a/tests/assets/SameNetTraceMerger.test.input.json b/tests/assets/SameNetTraceMerger.test.input.json index 981aefbbd..a74555510 100644 --- a/tests/assets/SameNetTraceMerger.test.input.json +++ b/tests/assets/SameNetTraceMerger.test.input.json @@ -3,45 +3,109 @@ "chips": [ { "chipId": "U1", - "center": { "x": 0, "y": 0 }, + "center": { + "x": 0, + "y": 0 + }, "width": 1.6, "height": 1.0, "pins": [ - { "pinId": "U1.1", "x": -0.8, "y": 0.3 }, - { "pinId": "U1.2", "x": -0.8, "y": 0.0 }, - { "pinId": "U1.3", "x": -0.8, "y": -0.3 } + { + "pinId": "U1.1", + "x": -0.8, + "y": 0.3 + }, + { + "pinId": "U1.2", + "x": -0.8, + "y": 0.0 + }, + { + "pinId": "U1.3", + "x": -0.8, + "y": -0.3 + } ] }, { "chipId": "C1", - "center": { "x": -2.5, "y": 0.3 }, + "center": { + "x": -2.5, + "y": 0.3 + }, "width": 0.5, "height": 0.5, "pins": [ - { "pinId": "C1.1", "x": -2.5, "y": 0.55 }, - { "pinId": "C1.2", "x": -2.5, "y": 0.05 } + { + "pinId": "C1.1", + "x": -2.5, + "y": 0.55 + }, + { + "pinId": "C1.2", + "x": -2.5, + "y": 0.05 + } ] }, { "chipId": "C2", - "center": { "x": -2.5, "y": -0.3 }, + "center": { + "x": -2.5, + "y": -0.3 + }, "width": 0.5, "height": 0.5, "pins": [ - { "pinId": "C2.1", "x": -2.5, "y": -0.05 }, - { "pinId": "C2.2", "x": -2.5, "y": -0.55 } + { + "pinId": "C2.1", + "x": -2.5, + "y": -0.05 + }, + { + "pinId": "C2.2", + "x": -2.5, + "y": -0.55 + } ] } ], "directConnections": [ - { "pinIds": ["U1.1", "C1.1"], "netId": "VCC" }, - { "pinIds": ["U1.2", "C2.1"], "netId": "EN" } + { + "pinIds": [ + "U1.1", + "C1.1" + ], + "netId": "VCC" + }, + { + "pinIds": [ + "U1.2", + "C2.1" + ], + "netId": "EN" + } + ], + "netConnections": [ + { + "netId": "GND", + "pinIds": [ + "U1.3", + "C1.2", + "C2.2" + ] + } ], - "netConnections": [{ "netId": "GND", "pinIds": ["U1.3", "C1.2", "C2.2"] }], "availableNetLabelOrientations": { - "VCC": ["y+"], - "EN": ["x-"], - "GND": ["y-"] + "VCC": [ + "y+" + ], + "EN": [ + "x-" + ], + "GND": [ + "y-" + ] } }, "traces": [ @@ -52,12 +116,27 @@ "userNetId": "GND", "pins": [], "mspConnectionPairIds": [], - "pinIds": ["U1.3", "C1.2"], + "pinIds": [ + "U1.3", + "C1.2" + ], "tracePath": [ - { "x": -0.8, "y": -0.3 }, - { "x": -1.8, "y": -0.3 }, - { "x": -1.8, "y": 0.05 }, - { "x": -2.5, "y": 0.05 } + { + "x": -0.8, + "y": -0.3 + }, + { + "x": -1.8, + "y": -0.3 + }, + { + "x": -1.8, + "y": 0.05 + }, + { + "x": -2.5, + "y": 0.05 + } ] }, { @@ -67,12 +146,27 @@ "userNetId": "GND", "pins": [], "mspConnectionPairIds": [], - "pinIds": ["U1.3", "C2.2"], + "pinIds": [ + "U1.3", + "C2.2" + ], "tracePath": [ - { "x": -0.8, "y": -0.3 }, - { "x": -1.82, "y": -0.3 }, - { "x": -1.82, "y": -0.55 }, - { "x": -2.5, "y": -0.55 } + { + "x": -0.8, + "y": -0.3 + }, + { + "x": -1.82, + "y": -0.3 + }, + { + "x": -1.82, + "y": -0.55 + }, + { + "x": -2.5, + "y": -0.55 + } ] } ] From 5cfea244e47b27319b1cd06bf24146672294cb4d Mon Sep 17 00:00:00 2001 From: kuaaq <130150518+kuaaq@users.noreply.github.com> Date: Tue, 26 May 2026 03:26:45 +0800 Subject: [PATCH 15/19] style: apply biome format to test file From 976f44ddefcc1892397a0613e0b50bd3b4f11f71 Mon Sep 17 00:00:00 2001 From: kuaaq <130150518+kuaaq@users.noreply.github.com> Date: Tue, 26 May 2026 03:30:03 +0800 Subject: [PATCH 16/19] fix: collapse all short arrays to one line for Biome JSON format --- .../assets/SameNetTraceMerger.test.input.json | 146 ++++-------------- 1 file changed, 26 insertions(+), 120 deletions(-) diff --git a/tests/assets/SameNetTraceMerger.test.input.json b/tests/assets/SameNetTraceMerger.test.input.json index a74555510..981aefbbd 100644 --- a/tests/assets/SameNetTraceMerger.test.input.json +++ b/tests/assets/SameNetTraceMerger.test.input.json @@ -3,109 +3,45 @@ "chips": [ { "chipId": "U1", - "center": { - "x": 0, - "y": 0 - }, + "center": { "x": 0, "y": 0 }, "width": 1.6, "height": 1.0, "pins": [ - { - "pinId": "U1.1", - "x": -0.8, - "y": 0.3 - }, - { - "pinId": "U1.2", - "x": -0.8, - "y": 0.0 - }, - { - "pinId": "U1.3", - "x": -0.8, - "y": -0.3 - } + { "pinId": "U1.1", "x": -0.8, "y": 0.3 }, + { "pinId": "U1.2", "x": -0.8, "y": 0.0 }, + { "pinId": "U1.3", "x": -0.8, "y": -0.3 } ] }, { "chipId": "C1", - "center": { - "x": -2.5, - "y": 0.3 - }, + "center": { "x": -2.5, "y": 0.3 }, "width": 0.5, "height": 0.5, "pins": [ - { - "pinId": "C1.1", - "x": -2.5, - "y": 0.55 - }, - { - "pinId": "C1.2", - "x": -2.5, - "y": 0.05 - } + { "pinId": "C1.1", "x": -2.5, "y": 0.55 }, + { "pinId": "C1.2", "x": -2.5, "y": 0.05 } ] }, { "chipId": "C2", - "center": { - "x": -2.5, - "y": -0.3 - }, + "center": { "x": -2.5, "y": -0.3 }, "width": 0.5, "height": 0.5, "pins": [ - { - "pinId": "C2.1", - "x": -2.5, - "y": -0.05 - }, - { - "pinId": "C2.2", - "x": -2.5, - "y": -0.55 - } + { "pinId": "C2.1", "x": -2.5, "y": -0.05 }, + { "pinId": "C2.2", "x": -2.5, "y": -0.55 } ] } ], "directConnections": [ - { - "pinIds": [ - "U1.1", - "C1.1" - ], - "netId": "VCC" - }, - { - "pinIds": [ - "U1.2", - "C2.1" - ], - "netId": "EN" - } - ], - "netConnections": [ - { - "netId": "GND", - "pinIds": [ - "U1.3", - "C1.2", - "C2.2" - ] - } + { "pinIds": ["U1.1", "C1.1"], "netId": "VCC" }, + { "pinIds": ["U1.2", "C2.1"], "netId": "EN" } ], + "netConnections": [{ "netId": "GND", "pinIds": ["U1.3", "C1.2", "C2.2"] }], "availableNetLabelOrientations": { - "VCC": [ - "y+" - ], - "EN": [ - "x-" - ], - "GND": [ - "y-" - ] + "VCC": ["y+"], + "EN": ["x-"], + "GND": ["y-"] } }, "traces": [ @@ -116,27 +52,12 @@ "userNetId": "GND", "pins": [], "mspConnectionPairIds": [], - "pinIds": [ - "U1.3", - "C1.2" - ], + "pinIds": ["U1.3", "C1.2"], "tracePath": [ - { - "x": -0.8, - "y": -0.3 - }, - { - "x": -1.8, - "y": -0.3 - }, - { - "x": -1.8, - "y": 0.05 - }, - { - "x": -2.5, - "y": 0.05 - } + { "x": -0.8, "y": -0.3 }, + { "x": -1.8, "y": -0.3 }, + { "x": -1.8, "y": 0.05 }, + { "x": -2.5, "y": 0.05 } ] }, { @@ -146,27 +67,12 @@ "userNetId": "GND", "pins": [], "mspConnectionPairIds": [], - "pinIds": [ - "U1.3", - "C2.2" - ], + "pinIds": ["U1.3", "C2.2"], "tracePath": [ - { - "x": -0.8, - "y": -0.3 - }, - { - "x": -1.82, - "y": -0.3 - }, - { - "x": -1.82, - "y": -0.55 - }, - { - "x": -2.5, - "y": -0.55 - } + { "x": -0.8, "y": -0.3 }, + { "x": -1.82, "y": -0.3 }, + { "x": -1.82, "y": -0.55 }, + { "x": -2.5, "y": -0.55 } ] } ] From ee32af4eeca5b1f58a8b72c467d7bcbb15ea7f6e Mon Sep 17 00:00:00 2001 From: kuaaq <130150518+kuaaq@users.noreply.github.com> Date: Tue, 26 May 2026 03:30:20 +0800 Subject: [PATCH 17/19] style: biome format - inline short arrays in test input JSON --- tests/assets/SameNetTraceMerger.test.input.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/assets/SameNetTraceMerger.test.input.json b/tests/assets/SameNetTraceMerger.test.input.json index 981aefbbd..2324c5646 100644 --- a/tests/assets/SameNetTraceMerger.test.input.json +++ b/tests/assets/SameNetTraceMerger.test.input.json @@ -37,7 +37,9 @@ { "pinIds": ["U1.1", "C1.1"], "netId": "VCC" }, { "pinIds": ["U1.2", "C2.1"], "netId": "EN" } ], - "netConnections": [{ "netId": "GND", "pinIds": ["U1.3", "C1.2", "C2.2"] }], + "netConnections": [ + { "netId": "GND", "pinIds": ["U1.3", "C1.2", "C2.2"] } + ], "availableNetLabelOrientations": { "VCC": ["y+"], "EN": ["x-"], From d4c1e8c34800e4e26a6acfd67064cb922c3f1f92 Mon Sep 17 00:00:00 2001 From: kuaaq <130150518+kuaaq@users.noreply.github.com> Date: Tue, 26 May 2026 03:32:22 +0800 Subject: [PATCH 18/19] style: biome format - inline single-element netConnections array --- tests/assets/SameNetTraceMerger.test.input.json | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/tests/assets/SameNetTraceMerger.test.input.json b/tests/assets/SameNetTraceMerger.test.input.json index 2324c5646..981aefbbd 100644 --- a/tests/assets/SameNetTraceMerger.test.input.json +++ b/tests/assets/SameNetTraceMerger.test.input.json @@ -37,9 +37,7 @@ { "pinIds": ["U1.1", "C1.1"], "netId": "VCC" }, { "pinIds": ["U1.2", "C2.1"], "netId": "EN" } ], - "netConnections": [ - { "netId": "GND", "pinIds": ["U1.3", "C1.2", "C2.2"] } - ], + "netConnections": [{ "netId": "GND", "pinIds": ["U1.3", "C1.2", "C2.2"] }], "availableNetLabelOrientations": { "VCC": ["y+"], "EN": ["x-"], From 96bde5387ee60920df52b9772fe4917ef70e25ca Mon Sep 17 00:00:00 2001 From: kuaaq <130150518+kuaaq@users.noreply.github.com> Date: Tue, 26 May 2026 03:34:33 +0800 Subject: [PATCH 19/19] fix: pretty-print JSON fixture for Biome format check