From 196df85138f3c4eb66ed60191bae8449efcac3b7 Mon Sep 17 00:00:00 2001 From: ron1nrest Date: Sat, 16 May 2026 12:56:20 +0500 Subject: [PATCH 1/6] feat: merge nearby parallel same-net traces (#34) Add MergeParallelTracesSolver to align and consolidate close parallel segments on the same net, wired into the pipeline after overlap shift. --- .../MergeParallelTracesSolver.ts | 72 ++++ .../mergeParallelTraceSegments.ts | 322 ++++++++++++++++++ .../SchematicTracePipelineSolver.ts | 45 ++- .../mergeParallelTraceSegments.test.ts | 141 ++++++++ 4 files changed, 566 insertions(+), 14 deletions(-) create mode 100644 lib/solvers/MergeParallelTracesSolver/MergeParallelTracesSolver.ts create mode 100644 lib/solvers/MergeParallelTracesSolver/mergeParallelTraceSegments.ts create mode 100644 tests/solvers/MergeParallelTracesSolver/mergeParallelTraceSegments.test.ts diff --git a/lib/solvers/MergeParallelTracesSolver/MergeParallelTracesSolver.ts b/lib/solvers/MergeParallelTracesSolver/MergeParallelTracesSolver.ts new file mode 100644 index 00000000..79fd1b32 --- /dev/null +++ b/lib/solvers/MergeParallelTracesSolver/MergeParallelTracesSolver.ts @@ -0,0 +1,72 @@ +import { BaseSolver } from "lib/solvers/BaseSolver/BaseSolver" +import type { SolvedTracePath } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceLinesSolver" +import type { MspConnectionPairId } from "lib/solvers/MspConnectionPairSolver/MspConnectionPairSolver" +import { visualizeInputProblem } from "lib/solvers/SchematicTracePipelineSolver/visualizeInputProblem" +import type { InputProblem } from "lib/types/InputProblem" +import { + DEFAULT_MERGE_DISTANCE, + mergeParallelTraceSegments, +} from "./mergeParallelTraceSegments" + +export class MergeParallelTracesSolver extends BaseSolver { + inputProblem: InputProblem + inputTracePaths: SolvedTracePath[] + mergeDistance: number + + correctedTraceMap: Record = {} + + constructor(params: { + inputProblem: InputProblem + inputTracePaths: SolvedTracePath[] + mergeDistance?: number + }) { + super() + this.inputProblem = params.inputProblem + this.inputTracePaths = params.inputTracePaths + this.mergeDistance = params.mergeDistance ?? DEFAULT_MERGE_DISTANCE + + for (const tracePath of this.inputTracePaths) { + this.correctedTraceMap[tracePath.mspPairId] = tracePath + } + } + + override getConstructorParams(): ConstructorParameters< + typeof MergeParallelTracesSolver + >[0] { + return { + inputProblem: this.inputProblem, + inputTracePaths: this.inputTracePaths, + mergeDistance: this.mergeDistance, + } + } + + override _step() { + const merged = mergeParallelTraceSegments( + this.inputTracePaths, + this.mergeDistance, + ) + + this.correctedTraceMap = Object.fromEntries( + merged.map((trace) => [trace.mspPairId, trace]), + ) + this.solved = true + } + + getOutput(): { traces: SolvedTracePath[] } { + return { traces: Object.values(this.correctedTraceMap) } + } + + override visualize() { + const graphics = visualizeInputProblem(this.inputProblem) + graphics.lines = graphics.lines || [] + + for (const trace of Object.values(this.correctedTraceMap)) { + graphics.lines.push({ + points: trace.tracePath, + strokeColor: "teal", + }) + } + + return graphics + } +} diff --git a/lib/solvers/MergeParallelTracesSolver/mergeParallelTraceSegments.ts b/lib/solvers/MergeParallelTracesSolver/mergeParallelTraceSegments.ts new file mode 100644 index 00000000..cf9b554a --- /dev/null +++ b/lib/solvers/MergeParallelTracesSolver/mergeParallelTraceSegments.ts @@ -0,0 +1,322 @@ +import type { Point } from "@tscircuit/math-utils" +import type { SolvedTracePath } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceLinesSolver" +import { simplifyPath } from "lib/solvers/TraceCleanupSolver/simplifyPath" + +const EPS = 1e-6 +export const DEFAULT_MERGE_DISTANCE = 0.15 + +type Orientation = "horizontal" | "vertical" + +interface SegmentRef { + traceIndex: number + segmentIndex: number + orientation: Orientation + fixedCoordinate: number + rangeStart: number + rangeEnd: number +} + +const getSegmentRef = ( + traceIndex: number, + segmentIndex: number, + start: Point, + end: Point, +): SegmentRef | null => { + if (Math.abs(start.y - end.y) < EPS) { + return { + traceIndex, + segmentIndex, + orientation: "horizontal", + fixedCoordinate: start.y, + rangeStart: Math.min(start.x, end.x), + rangeEnd: Math.max(start.x, end.x), + } + } + + if (Math.abs(start.x - end.x) < EPS) { + return { + traceIndex, + segmentIndex, + orientation: "vertical", + fixedCoordinate: start.x, + rangeStart: Math.min(start.y, end.y), + rangeEnd: Math.max(start.y, end.y), + } + } + + return null +} + +const getSegments = ( + traces: SolvedTracePath[], + traceIndex: number, + options: { includeTerminals: boolean }, +): SegmentRef[] => { + const trace = traces[traceIndex]! + const refs: SegmentRef[] = [] + const startIndex = options.includeTerminals ? 0 : 1 + const endIndex = options.includeTerminals + ? trace.tracePath.length - 1 + : trace.tracePath.length - 2 + + for (let segmentIndex = startIndex; segmentIndex < endIndex; segmentIndex++) { + const start = trace.tracePath[segmentIndex]! + const end = trace.tracePath[segmentIndex + 1]! + const ref = getSegmentRef(traceIndex, segmentIndex, start, end) + if (ref) refs.push(ref) + } + + return refs +} + +const rangesOverlap = (a: SegmentRef, b: SegmentRef) => + Math.min(a.rangeEnd, b.rangeEnd) - Math.max(a.rangeStart, b.rangeStart) > EPS + +const wouldOverlapDifferentNet = ( + traces: SolvedTracePath[], + source: SegmentRef, + fixedCoordinate: number, +) => { + for (let traceIndex = 0; traceIndex < traces.length; traceIndex++) { + const trace = traces[traceIndex]! + if (trace.globalConnNetId === traces[source.traceIndex]!.globalConnNetId) { + continue + } + + for ( + let segmentIndex = 0; + segmentIndex < trace.tracePath.length - 1; + segmentIndex++ + ) { + const ref = getSegmentRef( + traceIndex, + segmentIndex, + trace.tracePath[segmentIndex]!, + trace.tracePath[segmentIndex + 1]!, + ) + if (!ref) continue + if (ref.orientation !== source.orientation) continue + if (Math.abs(ref.fixedCoordinate - fixedCoordinate) > EPS) continue + if (rangesOverlap(source, ref)) return true + } + } + + return false +} + +const findConsolidatableSegmentPair = ( + traces: SolvedTracePath[], + indexA: number, + indexB: number, + mergeDistance: number, +): SegmentRef | null => { + const traceA = traces[indexA]! + const traceB = traces[indexB]! + if (traceA.tracePath.length !== 2 || traceB.tracePath.length !== 2) { + return null + } + + const segmentA = getSegmentRef( + indexA, + 0, + traceA.tracePath[0]!, + traceA.tracePath[1]!, + ) + const segmentB = getSegmentRef( + indexB, + 0, + traceB.tracePath[0]!, + traceB.tracePath[1]!, + ) + if (!segmentA || !segmentB) return null + if (segmentA.orientation !== segmentB.orientation) return null + if ( + Math.abs(segmentA.fixedCoordinate - segmentB.fixedCoordinate) > mergeDistance + ) { + return null + } + if (!rangesOverlap(segmentA, segmentB)) return null + + return segmentA +} + +const mergeTracePair = ( + kept: SolvedTracePath, + removed: SolvedTracePath, + canonical: SegmentRef, +): SolvedTracePath => { + const tracePath = kept.tracePath.map((point) => { + const p = { ...point } + if (canonical.orientation === "horizontal") { + p.y = canonical.fixedCoordinate + } else { + p.x = canonical.fixedCoordinate + } + return p + }) + + const pinIds = [...new Set([...kept.pinIds, ...removed.pinIds])] + const pinsById = new Map( + [...kept.pins, ...removed.pins].map((pin) => [pin.pinId, pin]), + ) + + return { + ...kept, + tracePath: simplifyPath(tracePath), + mspConnectionPairIds: [ + ...new Set([ + ...kept.mspConnectionPairIds, + ...removed.mspConnectionPairIds, + ]), + ], + pinIds, + pins: pinIds.map((pinId) => pinsById.get(pinId)!), + } +} + +const consolidateRedundantParallelTraces = ( + traces: SolvedTracePath[], + mergeDistance: number, +): SolvedTracePath[] => { + const result = traces.map((trace) => ({ + ...trace, + tracePath: trace.tracePath.map((point) => ({ ...point })), + })) + + let changed = true + while (changed) { + changed = false + + outer: for (let indexA = 0; indexA < result.length; indexA++) { + for (let indexB = indexA + 1; indexB < result.length; indexB++) { + if (result[indexA]!.globalConnNetId !== result[indexB]!.globalConnNetId) { + continue + } + + const canonical = findConsolidatableSegmentPair( + result, + indexA, + indexB, + mergeDistance, + ) + if (!canonical) continue + if ( + wouldOverlapDifferentNet(result, canonical, canonical.fixedCoordinate) + ) { + continue + } + + result[indexA] = mergeTracePair( + result[indexA]!, + result[indexB]!, + canonical, + ) + result.splice(indexB, 1) + changed = true + break outer + } + } + } + + return result +} + +const snapSegmentFixedCoordinate = ( + trace: SolvedTracePath, + segmentIndex: number, + orientation: Orientation, + fixedCoordinate: number, +) => { + const tracePath = trace.tracePath.map((point) => ({ ...point })) + const start = tracePath[segmentIndex]! + const end = tracePath[segmentIndex + 1]! + + if (orientation === "horizontal") { + start.y = fixedCoordinate + end.y = fixedCoordinate + } else { + start.x = fixedCoordinate + end.x = fixedCoordinate + } + + return { + ...trace, + tracePath: simplifyPath(tracePath), + } +} + +/** + * Snaps nearby parallel same-net trace segments onto a shared X or Y axis. + * Internal segments align to overlapping segments on sibling traces in the net. + */ +export const mergeParallelTraceSegments = ( + traces: SolvedTracePath[], + mergeDistance = DEFAULT_MERGE_DISTANCE, +): SolvedTracePath[] => { + const mergedTraces = traces.map((trace) => ({ + ...trace, + tracePath: trace.tracePath.map((point) => ({ ...point })), + })) + + const traceIndexesByNet = new Map() + for (let traceIndex = 0; traceIndex < mergedTraces.length; traceIndex++) { + const netId = mergedTraces[traceIndex]!.globalConnNetId + const traceIndexes = traceIndexesByNet.get(netId) ?? [] + traceIndexes.push(traceIndex) + traceIndexesByNet.set(netId, traceIndexes) + } + + for (const traceIndexes of traceIndexesByNet.values()) { + if (traceIndexes.length < 2) continue + + let changed = true + while (changed) { + changed = false + + for (const traceIndex of traceIndexes.slice(1)) { + const candidates = getSegments(mergedTraces, traceIndex, { + includeTerminals: false, + }) + + for (const candidate of candidates) { + const target = traceIndexes + .filter((targetTraceIndex) => targetTraceIndex !== traceIndex) + .flatMap((targetTraceIndex) => + getSegments(mergedTraces, targetTraceIndex, { + includeTerminals: true, + }), + ) + .find( + (other) => + other.orientation === candidate.orientation && + Math.abs(other.fixedCoordinate - candidate.fixedCoordinate) <= + mergeDistance && + Math.abs(other.fixedCoordinate - candidate.fixedCoordinate) > + EPS && + rangesOverlap(candidate, other) && + !wouldOverlapDifferentNet( + mergedTraces, + candidate, + other.fixedCoordinate, + ), + ) + + if (!target) continue + + mergedTraces[traceIndex] = snapSegmentFixedCoordinate( + mergedTraces[traceIndex]!, + candidate.segmentIndex, + candidate.orientation, + target.fixedCoordinate, + ) + changed = true + break + } + + if (changed) break + } + } + } + + return consolidateRedundantParallelTraces(mergedTraces, mergeDistance) +} diff --git a/lib/solvers/SchematicTracePipelineSolver/SchematicTracePipelineSolver.ts b/lib/solvers/SchematicTracePipelineSolver/SchematicTracePipelineSolver.ts index 59821f0c..176f9105 100644 --- a/lib/solvers/SchematicTracePipelineSolver/SchematicTracePipelineSolver.ts +++ b/lib/solvers/SchematicTracePipelineSolver/SchematicTracePipelineSolver.ts @@ -12,6 +12,7 @@ import { type SolvedTracePath, } from "../SchematicTraceLinesSolver/SchematicTraceLinesSolver" import { TraceOverlapShiftSolver } from "../TraceOverlapShiftSolver/TraceOverlapShiftSolver" +import { MergeParallelTracesSolver } from "../MergeParallelTracesSolver/MergeParallelTracesSolver" import { NetLabelPlacementSolver } from "../NetLabelPlacementSolver/NetLabelPlacementSolver" import { colorAvailableNetOrientationLabels } from "./colorAvailableNetOrientationLabels" import { visualizeInputProblem } from "./visualizeInputProblem" @@ -71,6 +72,7 @@ export class SchematicTracePipelineSolver extends BaseSolver { schematicTraceLinesSolver?: SchematicTraceLinesSolver longDistancePairSolver?: LongDistancePairSolver traceOverlapShiftSolver?: TraceOverlapShiftSolver + mergeParallelTracesSolver?: MergeParallelTracesSolver netLabelPlacementSolver?: NetLabelPlacementSolver labelMergingSolver?: MergedNetLabelObstacleSolver traceLabelOverlapAvoidanceSolver?: TraceLabelOverlapAvoidanceSolver @@ -154,19 +156,25 @@ export class SchematicTracePipelineSolver extends BaseSolver { onSolved: (_solver) => {}, }, ), + definePipelineStep( + "mergeParallelTracesSolver", + MergeParallelTracesSolver, + () => [ + { + inputProblem: this.inputProblem, + inputTracePaths: Object.values( + this.traceOverlapShiftSolver!.correctedTraceMap, + ), + }, + ], + ), definePipelineStep( "netLabelPlacementSolver", NetLabelPlacementSolver, () => [ { inputProblem: this.inputProblem, - inputTraceMap: - this.traceOverlapShiftSolver?.correctedTraceMap ?? - Object.fromEntries( - this.longDistancePairSolver!.getOutput().allTracesMerged.map( - (p) => [p.mspPairId, p], - ), - ), + inputTraceMap: this.getRoutedTraceMap(), }, ], { @@ -179,13 +187,7 @@ 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 = instance.getRoutedTraceMap() const traces = Object.values(traceMap) const netLabelPlacements = instance.netLabelPlacementSolver!.netLabelPlacements @@ -320,6 +322,21 @@ export class SchematicTracePipelineSolver extends BaseSolver { currentPipelineStepIndex = 0 + getRoutedTraceMap(): Record { + if (this.mergeParallelTracesSolver) { + return this.mergeParallelTracesSolver.correctedTraceMap + } + if (this.traceOverlapShiftSolver) { + return this.traceOverlapShiftSolver.correctedTraceMap + } + return Object.fromEntries( + this.longDistancePairSolver!.getOutput().allTracesMerged.map((p) => [ + p.mspPairId, + p, + ]), + ) + } + private cloneAndCorrectInputProblem(original: InputProblem): InputProblem { const cloned: InputProblem = structuredClone({ ...original, diff --git a/tests/solvers/MergeParallelTracesSolver/mergeParallelTraceSegments.test.ts b/tests/solvers/MergeParallelTracesSolver/mergeParallelTraceSegments.test.ts new file mode 100644 index 00000000..7b1c6e12 --- /dev/null +++ b/tests/solvers/MergeParallelTracesSolver/mergeParallelTraceSegments.test.ts @@ -0,0 +1,141 @@ +import { expect, test } from "bun:test" +import type { SolvedTracePath } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceLinesSolver" +import { mergeParallelTraceSegments } from "lib/solvers/MergeParallelTracesSolver/mergeParallelTraceSegments" + +const makeTrace = ( + mspPairId: string, + globalConnNetId: string, + tracePath: Array<{ x: number; y: number }>, +): SolvedTracePath => + ({ + mspPairId, + dcConnNetId: globalConnNetId, + globalConnNetId, + pins: [ + { pinId: `${mspPairId}-a`, chipId: "U1", ...tracePath[0]! }, + { + pinId: `${mspPairId}-b`, + chipId: "U1", + ...tracePath[tracePath.length - 1]!, + }, + ], + tracePath, + mspConnectionPairIds: [mspPairId], + pinIds: [`${mspPairId}-a`, `${mspPairId}-b`], + }) as SolvedTracePath + +test("snaps nearby internal same-net horizontal segments onto the same y", () => { + const [first, second] = mergeParallelTraceSegments([ + makeTrace("trace-a", "net-1", [ + { x: 0, y: 0 }, + { x: 0, y: 1 }, + { x: 4, y: 1 }, + { x: 4, y: 0 }, + ]), + makeTrace("trace-b", "net-1", [ + { x: 0, y: 0.12 }, + { x: 0, y: 1.12 }, + { x: 4, y: 1.12 }, + { x: 4, y: 0.12 }, + ]), + ]) + + expect(first!.tracePath[1]!.y).toBe(1) + expect(first!.tracePath[2]!.y).toBe(1) + expect(second!.tracePath[1]!.y).toBe(1) + expect(second!.tracePath[2]!.y).toBe(1) + expect(second!.tracePath[0]!.y).toBe(0.12) + expect(second!.tracePath[3]!.y).toBe(0.12) +}) + +test("snaps nearby internal same-net vertical segments onto the same x", () => { + const [first, second] = mergeParallelTraceSegments([ + makeTrace("trace-a", "net-1", [ + { x: 0, y: 0 }, + { x: 1, y: 0 }, + { x: 1, y: 4 }, + { x: 0, y: 4 }, + ]), + makeTrace("trace-b", "net-1", [ + { x: 0.12, y: 0 }, + { x: 1.12, y: 0 }, + { x: 1.12, y: 4 }, + { x: 0.12, y: 4 }, + ]), + ]) + + expect(first!.tracePath[1]!.x).toBe(1) + expect(first!.tracePath[2]!.x).toBe(1) + expect(second!.tracePath[1]!.x).toBe(1) + expect(second!.tracePath[2]!.x).toBe(1) + expect(second!.tracePath[0]!.x).toBe(0.12) + expect(second!.tracePath[3]!.x).toBe(0.12) +}) + +test("merges two close parallel same-net traces into one", () => { + const result = mergeParallelTraceSegments([ + makeTrace("trace-a", "net-1", [ + { x: 0, y: 1 }, + { x: 4, y: 1 }, + ]), + makeTrace("trace-b", "net-1", [ + { x: 0, y: 1.1 }, + { x: 4, y: 1.1 }, + ]), + ]) + + expect(result).toHaveLength(1) + expect(result[0]!.tracePath).toEqual([ + { x: 0, y: 1 }, + { x: 4, y: 1 }, + ]) + expect(result[0]!.globalConnNetId).toBe("net-1") + expect(result[0]!.mspConnectionPairIds).toEqual(["trace-a", "trace-b"]) + expect(result[0]!.pinIds).toEqual([ + "trace-a-a", + "trace-a-b", + "trace-b-a", + "trace-b-b", + ]) +}) + +test("merges two close parallel vertical same-net traces into one", () => { + const result = mergeParallelTraceSegments([ + makeTrace("trace-a", "net-1", [ + { x: 2, y: 0 }, + { x: 2, y: 5 }, + ]), + makeTrace("trace-b", "net-1", [ + { x: 2.12, y: 0 }, + { x: 2.12, y: 5 }, + ]), + ]) + + expect(result).toHaveLength(1) + expect(result[0]!.tracePath).toEqual([ + { x: 2, y: 0 }, + { x: 2, y: 5 }, + ]) +}) + +test("does not snap nearby segments from different nets", () => { + const [first, second] = mergeParallelTraceSegments([ + makeTrace("trace-a", "net-1", [ + { x: 0, y: 0 }, + { x: 0, y: 1 }, + { x: 4, y: 1 }, + { x: 4, y: 0 }, + ]), + makeTrace("trace-b", "net-2", [ + { x: 0, y: 0.12 }, + { x: 0, y: 1.12 }, + { x: 4, y: 1.12 }, + { x: 4, y: 0.12 }, + ]), + ]) + + expect(first!.tracePath[1]!.y).toBe(1) + expect(first!.tracePath[2]!.y).toBe(1) + expect(second!.tracePath[1]!.y).toBe(1.12) + expect(second!.tracePath[2]!.y).toBe(1.12) +}) From dd42118f87d547bc1dd2a15dfd9b63c4e43d39e8 Mon Sep 17 00:00:00 2001 From: ron1nrest Date: Sat, 16 May 2026 13:16:40 +0500 Subject: [PATCH 2/6] style: fix biome format in mergeParallelTraceSegments Co-authored-by: Cursor --- .../mergeParallelTraceSegments.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/lib/solvers/MergeParallelTracesSolver/mergeParallelTraceSegments.ts b/lib/solvers/MergeParallelTracesSolver/mergeParallelTraceSegments.ts index cf9b554a..b0ae7717 100644 --- a/lib/solvers/MergeParallelTracesSolver/mergeParallelTraceSegments.ts +++ b/lib/solvers/MergeParallelTracesSolver/mergeParallelTraceSegments.ts @@ -131,7 +131,8 @@ const findConsolidatableSegmentPair = ( if (!segmentA || !segmentB) return null if (segmentA.orientation !== segmentB.orientation) return null if ( - Math.abs(segmentA.fixedCoordinate - segmentB.fixedCoordinate) > mergeDistance + Math.abs(segmentA.fixedCoordinate - segmentB.fixedCoordinate) > + mergeDistance ) { return null } @@ -189,7 +190,9 @@ const consolidateRedundantParallelTraces = ( outer: for (let indexA = 0; indexA < result.length; indexA++) { for (let indexB = indexA + 1; indexB < result.length; indexB++) { - if (result[indexA]!.globalConnNetId !== result[indexB]!.globalConnNetId) { + if ( + result[indexA]!.globalConnNetId !== result[indexB]!.globalConnNetId + ) { continue } From 86112e7b4e93e8a7754508b25fde4af36e08e40a Mon Sep 17 00:00:00 2001 From: ron1nrest Date: Sat, 16 May 2026 13:18:12 +0500 Subject: [PATCH 3/6] fix: keep pins tuple when merging parallel trace pairs Co-authored-by: Cursor --- .../mergeParallelTraceSegments.ts | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/lib/solvers/MergeParallelTracesSolver/mergeParallelTraceSegments.ts b/lib/solvers/MergeParallelTracesSolver/mergeParallelTraceSegments.ts index b0ae7717..6b6a9482 100644 --- a/lib/solvers/MergeParallelTracesSolver/mergeParallelTraceSegments.ts +++ b/lib/solvers/MergeParallelTracesSolver/mergeParallelTraceSegments.ts @@ -156,11 +156,6 @@ const mergeTracePair = ( return p }) - const pinIds = [...new Set([...kept.pinIds, ...removed.pinIds])] - const pinsById = new Map( - [...kept.pins, ...removed.pins].map((pin) => [pin.pinId, pin]), - ) - return { ...kept, tracePath: simplifyPath(tracePath), @@ -170,8 +165,7 @@ const mergeTracePair = ( ...removed.mspConnectionPairIds, ]), ], - pinIds, - pins: pinIds.map((pinId) => pinsById.get(pinId)!), + pinIds: [...new Set([...kept.pinIds, ...removed.pinIds])], } } From 7030ad0b5ad9fa10cf131a3939f47ce917d6312a Mon Sep 17 00:00:00 2001 From: ron1nrest Date: Sun, 17 May 2026 23:16:47 +0500 Subject: [PATCH 4/6] test: add example35 with before/after snapshots for issue #34 Minimal JP6-R1-SJ2 repro; snapshots at traceOverlapShift vs mergeParallelTracesSolver. Co-authored-by: Cursor --- site/examples/example35.page.tsx | 6 + tests/assets/example35.json | 50 +++++++ .../after-mergeParallelTraces.snap.svg | 123 ++++++++++++++++ .../before-mergeParallelTraces.snap.svg | 123 ++++++++++++++++ .../examples/__snapshots__/example35.snap.svg | 137 ++++++++++++++++++ tests/examples/example35.test.ts | 32 ++++ 6 files changed, 471 insertions(+) create mode 100644 site/examples/example35.page.tsx create mode 100644 tests/assets/example35.json create mode 100644 tests/examples/__snapshots__/after-mergeParallelTraces.snap.svg create mode 100644 tests/examples/__snapshots__/before-mergeParallelTraces.snap.svg create mode 100644 tests/examples/__snapshots__/example35.snap.svg create mode 100644 tests/examples/example35.test.ts diff --git a/site/examples/example35.page.tsx b/site/examples/example35.page.tsx new file mode 100644 index 00000000..055ab187 --- /dev/null +++ b/site/examples/example35.page.tsx @@ -0,0 +1,6 @@ +import { PipelineDebugger } from "site/components/PipelineDebugger" +import inputProblem from "../../tests/assets/example35.json" + +export { inputProblem } + +export default () => diff --git a/tests/assets/example35.json b/tests/assets/example35.json new file mode 100644 index 00000000..a74d677a --- /dev/null +++ b/tests/assets/example35.json @@ -0,0 +1,50 @@ +{ + "chips": [ + { + "chipId": "JP6", + "center": { "x": -3, "y": 0 }, + "width": 1, + "height": 1.2, + "pins": [ + { "pinId": "JP6.VOUT", "x": -3, "y": 0.3 }, + { "pinId": "JP6.GND", "x": -3, "y": -0.3 } + ] + }, + { + "chipId": "R1", + "center": { "x": 0, "y": 0 }, + "width": 1.5, + "height": 0.5, + "pins": [ + { "pinId": "R1.1", "x": -0.6, "y": 0 }, + { "pinId": "R1.2", "x": 0.6, "y": 0 } + ] + }, + { + "chipId": "SJ2", + "center": { "x": 3, "y": 0 }, + "width": 0.8, + "height": 0.8, + "pins": [ + { "pinId": "SJ2.1", "x": 3, "y": 0.2 }, + { "pinId": "SJ2.2", "x": 3, "y": -0.2 } + ] + } + ], + "directConnections": [], + "netConnections": [ + { + "pinIds": ["JP6.GND", "R1.1", "SJ2.2"], + "netId": "GND" + }, + { + "pinIds": ["JP6.VOUT", "R1.2", "SJ2.1"], + "netId": "VOUT" + } + ], + "availableNetLabelOrientations": { + "GND": ["y-"], + "VOUT": ["x+"] + }, + "maxMspPairDistance": 8 +} diff --git a/tests/examples/__snapshots__/after-mergeParallelTraces.snap.svg b/tests/examples/__snapshots__/after-mergeParallelTraces.snap.svg new file mode 100644 index 00000000..09217c2b --- /dev/null +++ b/tests/examples/__snapshots__/after-mergeParallelTraces.snap.svg @@ -0,0 +1,123 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/tests/examples/__snapshots__/before-mergeParallelTraces.snap.svg b/tests/examples/__snapshots__/before-mergeParallelTraces.snap.svg new file mode 100644 index 00000000..952fbcac --- /dev/null +++ b/tests/examples/__snapshots__/before-mergeParallelTraces.snap.svg @@ -0,0 +1,123 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/tests/examples/__snapshots__/example35.snap.svg b/tests/examples/__snapshots__/example35.snap.svg new file mode 100644 index 00000000..e25055e8 --- /dev/null +++ b/tests/examples/__snapshots__/example35.snap.svg @@ -0,0 +1,137 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/tests/examples/example35.test.ts b/tests/examples/example35.test.ts new file mode 100644 index 00000000..c9fd0699 --- /dev/null +++ b/tests/examples/example35.test.ts @@ -0,0 +1,32 @@ +import { expect, test } from "bun:test" +import { SchematicTracePipelineSolver } from "lib/solvers/SchematicTracePipelineSolver/SchematicTracePipelineSolver" +import inputProblem from "../assets/example35.json" +import "tests/fixtures/matcher" + +/** Issue #34: parallel same-net traces before/after MergeParallelTracesSolver */ +test("example35 before/after mergeParallelTracesSolver", () => { + const solver = new SchematicTracePipelineSolver(inputProblem as any) + + solver.solveUntilPhase("mergeParallelTracesSolver") + expect(solver.traceOverlapShiftSolver).toBeDefined() + + expect(solver.traceOverlapShiftSolver!).toMatchSolverSnapshot( + import.meta.path, + "before-mergeParallelTraces", + ) + + while (!solver.mergeParallelTracesSolver?.solved) { + solver.step() + } + + expect(solver.mergeParallelTracesSolver!).toMatchSolverSnapshot( + import.meta.path, + "after-mergeParallelTraces", + ) +}) + +test("example35 full pipeline", () => { + const solver = new SchematicTracePipelineSolver(inputProblem as any) + solver.solve() + expect(solver).toMatchSolverSnapshot(import.meta.path) +}) From 337a1996c6068bb81f2ee90cfd59074e74e67a7a Mon Sep 17 00:00:00 2001 From: ron1nrest Date: Sun, 17 May 2026 23:29:02 +0500 Subject: [PATCH 5/6] test: fix example35 before/after merge snapshots for issue #34 - Use explicit pre-merge trace repro (two parallel same-net segments) - Snapshot single MergeParallelTracesSolver before/after solve() - Deep-clone traces in solver constructor to avoid shared refs Co-authored-by: Cursor --- .../MergeParallelTracesSolver.ts | 7 +- tests/assets/example35-pre-merge-traces.json | 24 +++++++ tests/assets/example35.json | 45 ++++-------- .../after-mergeParallelTraces.snap.svg | 62 ++++++---------- .../before-mergeParallelTraces.snap.svg | 61 ++++++---------- .../examples/__snapshots__/example35.snap.svg | 71 +++++++------------ tests/examples/example35.test.ts | 45 +++++++++--- 7 files changed, 149 insertions(+), 166 deletions(-) create mode 100644 tests/assets/example35-pre-merge-traces.json diff --git a/lib/solvers/MergeParallelTracesSolver/MergeParallelTracesSolver.ts b/lib/solvers/MergeParallelTracesSolver/MergeParallelTracesSolver.ts index 79fd1b32..047112d5 100644 --- a/lib/solvers/MergeParallelTracesSolver/MergeParallelTracesSolver.ts +++ b/lib/solvers/MergeParallelTracesSolver/MergeParallelTracesSolver.ts @@ -26,7 +26,12 @@ export class MergeParallelTracesSolver extends BaseSolver { this.mergeDistance = params.mergeDistance ?? DEFAULT_MERGE_DISTANCE for (const tracePath of this.inputTracePaths) { - this.correctedTraceMap[tracePath.mspPairId] = tracePath + this.correctedTraceMap[tracePath.mspPairId] = { + ...tracePath, + tracePath: tracePath.tracePath.map((point) => ({ ...point })), + mspConnectionPairIds: [...tracePath.mspConnectionPairIds], + pinIds: [...tracePath.pinIds], + } } } diff --git a/tests/assets/example35-pre-merge-traces.json b/tests/assets/example35-pre-merge-traces.json new file mode 100644 index 00000000..094a7748 --- /dev/null +++ b/tests/assets/example35-pre-merge-traces.json @@ -0,0 +1,24 @@ +[ + { + "mspPairId": "trace-a", + "dcConnNetId": "GND", + "globalConnNetId": "GND", + "tracePath": [ + { "x": 0, "y": 1 }, + { "x": 4, "y": 1 } + ], + "mspConnectionPairIds": ["trace-a"], + "pinIds": ["U1.1", "U2.1"] + }, + { + "mspPairId": "trace-b", + "dcConnNetId": "GND", + "globalConnNetId": "GND", + "tracePath": [ + { "x": 0, "y": 1.1 }, + { "x": 4, "y": 1.1 } + ], + "mspConnectionPairIds": ["trace-b"], + "pinIds": ["U1.2", "U2.2"] + } +] diff --git a/tests/assets/example35.json b/tests/assets/example35.json index a74d677a..f2eed1cf 100644 --- a/tests/assets/example35.json +++ b/tests/assets/example35.json @@ -1,50 +1,35 @@ { "chips": [ { - "chipId": "JP6", - "center": { "x": -3, "y": 0 }, - "width": 1, - "height": 1.2, + "chipId": "U1", + "center": { "x": 0, "y": 1.05 }, + "width": 0.5, + "height": 0.3, "pins": [ - { "pinId": "JP6.VOUT", "x": -3, "y": 0.3 }, - { "pinId": "JP6.GND", "x": -3, "y": -0.3 } + { "pinId": "U1.1", "x": 0, "y": 1 }, + { "pinId": "U1.2", "x": 0, "y": 1.1 } ] }, { - "chipId": "R1", - "center": { "x": 0, "y": 0 }, - "width": 1.5, - "height": 0.5, + "chipId": "U2", + "center": { "x": 4, "y": 1.05 }, + "width": 0.5, + "height": 0.3, "pins": [ - { "pinId": "R1.1", "x": -0.6, "y": 0 }, - { "pinId": "R1.2", "x": 0.6, "y": 0 } - ] - }, - { - "chipId": "SJ2", - "center": { "x": 3, "y": 0 }, - "width": 0.8, - "height": 0.8, - "pins": [ - { "pinId": "SJ2.1", "x": 3, "y": 0.2 }, - { "pinId": "SJ2.2", "x": 3, "y": -0.2 } + { "pinId": "U2.1", "x": 4, "y": 1 }, + { "pinId": "U2.2", "x": 4, "y": 1.1 } ] } ], "directConnections": [], "netConnections": [ { - "pinIds": ["JP6.GND", "R1.1", "SJ2.2"], + "pinIds": ["U1.1", "U1.2", "U2.1", "U2.2"], "netId": "GND" - }, - { - "pinIds": ["JP6.VOUT", "R1.2", "SJ2.1"], - "netId": "VOUT" } ], "availableNetLabelOrientations": { - "GND": ["y-"], - "VOUT": ["x+"] + "GND": ["x+"] }, - "maxMspPairDistance": 8 + "maxMspPairDistance": 10 } diff --git a/tests/examples/__snapshots__/after-mergeParallelTraces.snap.svg b/tests/examples/__snapshots__/after-mergeParallelTraces.snap.svg index 09217c2b..748d7c9b 100644 --- a/tests/examples/__snapshots__/after-mergeParallelTraces.snap.svg +++ b/tests/examples/__snapshots__/after-mergeParallelTraces.snap.svg @@ -1,67 +1,47 @@ - + - + - + - + - + - + - + - + - + - + - + - + - - - - - - - - - - - - - - - - - - - +