From e680c289c7dd705b45d18a317deafcf25716430a Mon Sep 17 00:00:00 2001 From: Molham Hamwi <66991851+MolhamHamwi@users.noreply.github.com> Date: Sat, 23 May 2026 09:55:23 +0200 Subject: [PATCH 1/2] fix: merge nearby same-net trace segments --- .../TraceCleanupSolver/TraceCleanupSolver.ts | 13 +- .../mergeNearbySameNetSegments.ts | 159 ++++++++++++++++++ .../mergeNearbySameNetSegments.test.ts | 60 +++++++ 3 files changed, 231 insertions(+), 1 deletion(-) create mode 100644 lib/solvers/TraceCleanupSolver/mergeNearbySameNetSegments.ts create mode 100644 tests/solvers/TraceCleanupSolver/mergeNearbySameNetSegments.test.ts diff --git a/lib/solvers/TraceCleanupSolver/TraceCleanupSolver.ts b/lib/solvers/TraceCleanupSolver/TraceCleanupSolver.ts index e9bac7ca3..0960d4d55 100644 --- a/lib/solvers/TraceCleanupSolver/TraceCleanupSolver.ts +++ b/lib/solvers/TraceCleanupSolver/TraceCleanupSolver.ts @@ -20,6 +20,7 @@ interface TraceCleanupSolverInput { import { UntangleTraceSubsolver } from "./sub-solver/UntangleTraceSubsolver" import { is4PointRectangle } from "./is4PointRectangle" +import { mergeNearbySameNetSegments } from "./mergeNearbySameNetSegments" /** * Represents the different stages or steps within the trace cleanup pipeline. @@ -27,6 +28,7 @@ import { is4PointRectangle } from "./is4PointRectangle" type PipelineStep = | "minimizing_turns" | "balancing_l_shapes" + | "merging_close_same_net_segments" | "untangling_traces" /** @@ -84,6 +86,9 @@ export class TraceCleanupSolver extends BaseSolver { case "balancing_l_shapes": this._runBalanceLShapesStep() break + case "merging_close_same_net_segments": + this._runMergeCloseSameNetSegmentsStep() + break } } @@ -108,13 +113,19 @@ export class TraceCleanupSolver extends BaseSolver { private _runBalanceLShapesStep() { if (this.traceIdQueue.length === 0) { - this.solved = true + this.pipelineStep = "merging_close_same_net_segments" return } this._processTrace("balancing_l_shapes") } + private _runMergeCloseSameNetSegmentsStep() { + this.outputTraces = mergeNearbySameNetSegments(this.outputTraces) + this.tracesMap = new Map(this.outputTraces.map((t) => [t.mspPairId, t])) + this.solved = true + } + private _processTrace(step: "minimizing_turns" | "balancing_l_shapes") { const targetMspConnectionPairId = this.traceIdQueue.shift()! this.activeTraceId = targetMspConnectionPairId diff --git a/lib/solvers/TraceCleanupSolver/mergeNearbySameNetSegments.ts b/lib/solvers/TraceCleanupSolver/mergeNearbySameNetSegments.ts new file mode 100644 index 000000000..7cd475c46 --- /dev/null +++ b/lib/solvers/TraceCleanupSolver/mergeNearbySameNetSegments.ts @@ -0,0 +1,159 @@ +import type { Point } from "graphics-debug" +import type { SolvedTracePath } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceLinesSolver" +import { simplifyPath } from "./simplifyPath" + +const DEFAULT_MERGE_DISTANCE = 0.18 +const EPS = 1e-6 + +type Orientation = "horizontal" | "vertical" + +type SegmentRef = { + traceIndex: number + startIndex: number + orientation: Orientation + fixedCoord: number + min: number + max: number +} + +const getSegmentRef = ( + trace: SolvedTracePath, + traceIndex: number, + startIndex: number, +): SegmentRef | null => { + const p1 = trace.tracePath[startIndex]! + const p2 = trace.tracePath[startIndex + 1]! + + if (Math.abs(p1.y - p2.y) < EPS && Math.abs(p1.x - p2.x) > EPS) { + return { + traceIndex, + startIndex, + orientation: "horizontal", + fixedCoord: p1.y, + min: Math.min(p1.x, p2.x), + max: Math.max(p1.x, p2.x), + } + } + + if (Math.abs(p1.x - p2.x) < EPS && Math.abs(p1.y - p2.y) > EPS) { + return { + traceIndex, + startIndex, + orientation: "vertical", + fixedCoord: p1.x, + min: Math.min(p1.y, p2.y), + max: Math.max(p1.y, p2.y), + } + } + + return null +} + +const getNetId = (trace: SolvedTracePath) => + trace.userNetId ?? trace.globalConnNetId ?? trace.dcConnNetId + +const rangesOverlap = (a: SegmentRef, b: SegmentRef) => + Math.min(a.max, b.max) - Math.max(a.min, b.min) > EPS + +const isInteriorSegment = (trace: SolvedTracePath, segmentStartIndex: number) => + segmentStartIndex > 0 && segmentStartIndex + 1 < trace.tracePath.length - 1 + +const moveSegmentToFixedCoord = ( + trace: SolvedTracePath, + segmentStartIndex: number, + orientation: Orientation, + fixedCoord: number, +) => { + const path = trace.tracePath.map((p) => ({ ...p })) + const p1 = path[segmentStartIndex]! + const p2 = path[segmentStartIndex + 1]! + + if (orientation === "horizontal") { + path[segmentStartIndex] = { ...p1, y: fixedCoord } + path[segmentStartIndex + 1] = { ...p2, y: fixedCoord } + } else { + path[segmentStartIndex] = { ...p1, x: fixedCoord } + path[segmentStartIndex + 1] = { ...p2, x: fixedCoord } + } + + return { ...trace, tracePath: simplifyPath(path as Point[]) } +} + +export const mergeNearbySameNetSegments = ( + traces: SolvedTracePath[], + mergeDistance = DEFAULT_MERGE_DISTANCE, +): SolvedTracePath[] => { + let output = traces.map((trace) => ({ + ...trace, + tracePath: trace.tracePath.map((p) => ({ ...p })), + })) + + // Two passes lets a segment align with the first close same-net neighbor, then + // lets any newly simplified path expose another merge candidate. + for (let pass = 0; pass < 2; pass++) { + let changed = false + + for (let traceIndexA = 0; traceIndexA < output.length; traceIndexA++) { + const traceA = output[traceIndexA]! + for ( + let traceIndexB = traceIndexA + 1; + traceIndexB < output.length; + traceIndexB++ + ) { + const traceB = output[traceIndexB]! + if (getNetId(traceA) !== getNetId(traceB)) continue + + for ( + let segmentIndexA = 0; + segmentIndexA < traceA.tracePath.length - 1; + segmentIndexA++ + ) { + if (!isInteriorSegment(traceA, segmentIndexA)) continue + const segmentA = getSegmentRef(traceA, traceIndexA, segmentIndexA) + if (!segmentA) continue + + for ( + let segmentIndexB = 0; + segmentIndexB < traceB.tracePath.length - 1; + segmentIndexB++ + ) { + if (!isInteriorSegment(traceB, segmentIndexB)) continue + const segmentB = getSegmentRef(traceB, traceIndexB, segmentIndexB) + if (!segmentB) continue + if (segmentA.orientation !== segmentB.orientation) continue + if (!rangesOverlap(segmentA, segmentB)) continue + + const distance = Math.abs(segmentA.fixedCoord - segmentB.fixedCoord) + if (distance <= EPS || distance > mergeDistance) continue + + const mergedCoord = (segmentA.fixedCoord + segmentB.fixedCoord) / 2 + output[traceIndexA] = moveSegmentToFixedCoord( + output[traceIndexA]!, + segmentIndexA, + segmentA.orientation, + mergedCoord, + ) + output[traceIndexB] = moveSegmentToFixedCoord( + output[traceIndexB]!, + segmentIndexB, + segmentB.orientation, + mergedCoord, + ) + changed = true + break + } + + if (changed) break + } + + if (changed) break + } + + if (changed) break + } + + if (!changed) break + } + + return output +} diff --git a/tests/solvers/TraceCleanupSolver/mergeNearbySameNetSegments.test.ts b/tests/solvers/TraceCleanupSolver/mergeNearbySameNetSegments.test.ts new file mode 100644 index 000000000..dfe4aa0a4 --- /dev/null +++ b/tests/solvers/TraceCleanupSolver/mergeNearbySameNetSegments.test.ts @@ -0,0 +1,60 @@ +import { expect, test } from "bun:test" +import { mergeNearbySameNetSegments } from "lib/solvers/TraceCleanupSolver/mergeNearbySameNetSegments" +import type { SolvedTracePath } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceLinesSolver" + +const makeTrace = ( + mspPairId: string, + globalConnNetId: string, + tracePath: Array<{ x: number; y: number }>, +): SolvedTracePath => + ({ + mspPairId, + dcConnNetId: globalConnNetId, + globalConnNetId, + pins: [] as any, + mspConnectionPairIds: [mspPairId], + pinIds: [], + tracePath, + }) as any + +test("mergeNearbySameNetSegments aligns close overlapping horizontal same-net segments", () => { + const [a, b] = mergeNearbySameNetSegments([ + makeTrace("a", "net1", [ + { x: 0, y: 0 }, + { x: 0, y: 1 }, + { x: 3, y: 1 }, + { x: 3, y: 2 }, + ]), + makeTrace("b", "net1", [ + { x: 1, y: 0 }, + { x: 1, y: 1.1 }, + { x: 4, y: 1.1 }, + { x: 4, y: 2 }, + ]), + ]) + + expect(a.tracePath[1]!.y).toBeCloseTo(1.05) + expect(a.tracePath[2]!.y).toBeCloseTo(1.05) + expect(b.tracePath[1]!.y).toBeCloseTo(1.05) + expect(b.tracePath[2]!.y).toBeCloseTo(1.05) +}) + +test("mergeNearbySameNetSegments does not align different-net segments", () => { + const [a, b] = mergeNearbySameNetSegments([ + makeTrace("a", "net1", [ + { x: 0, y: 0 }, + { x: 0, y: 1 }, + { x: 3, y: 1 }, + { x: 3, y: 2 }, + ]), + makeTrace("b", "net2", [ + { x: 1, y: 0 }, + { x: 1, y: 1.1 }, + { x: 4, y: 1.1 }, + { x: 4, y: 2 }, + ]), + ]) + + expect(a.tracePath[1]!.y).toBe(1) + expect(b.tracePath[1]!.y).toBe(1.1) +}) From dbcd4a333f2f0b47d893dbb0f889d507f1c16f60 Mon Sep 17 00:00:00 2001 From: Molham Hamwi <66991851+MolhamHamwi@users.noreply.github.com> Date: Sat, 23 May 2026 10:19:01 +0200 Subject: [PATCH 2/2] fix: address same-net merge review feedback --- .../TraceCleanupSolver/TraceCleanupSolver.ts | 3 +- .../mergeNearbySameNetSegments.ts | 119 +++++++++--------- .../mergeNearbySameNetSegments.test.ts | 30 +++++ 3 files changed, 92 insertions(+), 60 deletions(-) diff --git a/lib/solvers/TraceCleanupSolver/TraceCleanupSolver.ts b/lib/solvers/TraceCleanupSolver/TraceCleanupSolver.ts index 0960d4d55..452618eac 100644 --- a/lib/solvers/TraceCleanupSolver/TraceCleanupSolver.ts +++ b/lib/solvers/TraceCleanupSolver/TraceCleanupSolver.ts @@ -36,7 +36,8 @@ type PipelineStep = * It operates in a multi-step pipeline: * 1. **Untangling Traces**: It first attempts to untangle any overlapping or highly convoluted traces using a sub-solver. * 2. **Minimizing Turns**: After untangling, it iterates through each trace to minimize the number of turns, simplifying their paths. - * 3. **Balancing L-Shapes**: Finally, it balances L-shaped trace segments to create more visually appealing and consistent layouts. + * 3. **Balancing L-Shapes**: It balances L-shaped trace segments to create more visually appealing and consistent layouts. + * 4. **Merging Close Same-Net Segments**: Finally, it aligns nearby overlapping same-net segments onto shared coordinates. * The solver processes traces one by one, applying these cleanup steps sequentially to refine the overall trace layout. */ export class TraceCleanupSolver extends BaseSolver { diff --git a/lib/solvers/TraceCleanupSolver/mergeNearbySameNetSegments.ts b/lib/solvers/TraceCleanupSolver/mergeNearbySameNetSegments.ts index 7cd475c46..e4e9a242c 100644 --- a/lib/solvers/TraceCleanupSolver/mergeNearbySameNetSegments.ts +++ b/lib/solvers/TraceCleanupSolver/mergeNearbySameNetSegments.ts @@ -88,71 +88,72 @@ export const mergeNearbySameNetSegments = ( tracePath: trace.tracePath.map((p) => ({ ...p })), })) - // Two passes lets a segment align with the first close same-net neighbor, then - // lets any newly simplified path expose another merge candidate. - for (let pass = 0; pass < 2; pass++) { - let changed = false + const segments: SegmentRef[] = [] + for (let traceIndex = 0; traceIndex < output.length; traceIndex++) { + const trace = output[traceIndex]! + for ( + let segmentIndex = 0; + segmentIndex < trace.tracePath.length - 1; + segmentIndex++ + ) { + if (!isInteriorSegment(trace, segmentIndex)) continue + const segment = getSegmentRef(trace, traceIndex, segmentIndex) + if (segment) segments.push(segment) + } + } + + const visited = new Set() + + for (let startIndex = 0; startIndex < segments.length; startIndex++) { + if (visited.has(startIndex)) continue + + const componentIndexes: number[] = [] + const queue = [startIndex] + visited.add(startIndex) + + while (queue.length > 0) { + const currentIndex = queue.shift()! + const current = segments[currentIndex]! + componentIndexes.push(currentIndex) - for (let traceIndexA = 0; traceIndexA < output.length; traceIndexA++) { - const traceA = output[traceIndexA]! for ( - let traceIndexB = traceIndexA + 1; - traceIndexB < output.length; - traceIndexB++ + let candidateIndex = 0; + candidateIndex < segments.length; + candidateIndex++ ) { - const traceB = output[traceIndexB]! - if (getNetId(traceA) !== getNetId(traceB)) continue - - for ( - let segmentIndexA = 0; - segmentIndexA < traceA.tracePath.length - 1; - segmentIndexA++ - ) { - if (!isInteriorSegment(traceA, segmentIndexA)) continue - const segmentA = getSegmentRef(traceA, traceIndexA, segmentIndexA) - if (!segmentA) continue - - for ( - let segmentIndexB = 0; - segmentIndexB < traceB.tracePath.length - 1; - segmentIndexB++ - ) { - if (!isInteriorSegment(traceB, segmentIndexB)) continue - const segmentB = getSegmentRef(traceB, traceIndexB, segmentIndexB) - if (!segmentB) continue - if (segmentA.orientation !== segmentB.orientation) continue - if (!rangesOverlap(segmentA, segmentB)) continue - - const distance = Math.abs(segmentA.fixedCoord - segmentB.fixedCoord) - if (distance <= EPS || distance > mergeDistance) continue - - const mergedCoord = (segmentA.fixedCoord + segmentB.fixedCoord) / 2 - output[traceIndexA] = moveSegmentToFixedCoord( - output[traceIndexA]!, - segmentIndexA, - segmentA.orientation, - mergedCoord, - ) - output[traceIndexB] = moveSegmentToFixedCoord( - output[traceIndexB]!, - segmentIndexB, - segmentB.orientation, - mergedCoord, - ) - changed = true - break - } - - if (changed) break - } - - if (changed) break + if (visited.has(candidateIndex)) continue + const candidate = segments[candidateIndex]! + if ( + getNetId(output[current.traceIndex]!) !== + getNetId(output[candidate.traceIndex]!) + ) + continue + if (current.orientation !== candidate.orientation) continue + if (!rangesOverlap(current, candidate)) continue + + const distance = Math.abs(current.fixedCoord - candidate.fixedCoord) + if (distance <= EPS || distance > mergeDistance) continue + + visited.add(candidateIndex) + queue.push(candidateIndex) } - - if (changed) break } - if (!changed) break + if (componentIndexes.length < 2) continue + + const component = componentIndexes.map((index) => segments[index]!) + const mergedCoord = + component.reduce((sum, segment) => sum + segment.fixedCoord, 0) / + component.length + + for (const segment of component) { + output[segment.traceIndex] = moveSegmentToFixedCoord( + output[segment.traceIndex]!, + segment.startIndex, + segment.orientation, + mergedCoord, + ) + } } return output diff --git a/tests/solvers/TraceCleanupSolver/mergeNearbySameNetSegments.test.ts b/tests/solvers/TraceCleanupSolver/mergeNearbySameNetSegments.test.ts index dfe4aa0a4..bc8f8a571 100644 --- a/tests/solvers/TraceCleanupSolver/mergeNearbySameNetSegments.test.ts +++ b/tests/solvers/TraceCleanupSolver/mergeNearbySameNetSegments.test.ts @@ -58,3 +58,33 @@ test("mergeNearbySameNetSegments does not align different-net segments", () => { expect(a.tracePath[1]!.y).toBe(1) expect(b.tracePath[1]!.y).toBe(1.1) }) + +test("mergeNearbySameNetSegments aligns every close segment in a same-net group", () => { + const [a, b, c] = mergeNearbySameNetSegments([ + makeTrace("a", "net1", [ + { x: 0, y: 0 }, + { x: 0, y: 1 }, + { x: 3, y: 1 }, + { x: 3, y: 2 }, + ]), + makeTrace("b", "net1", [ + { x: 1, y: 0 }, + { x: 1, y: 1.1 }, + { x: 4, y: 1.1 }, + { x: 4, y: 2 }, + ]), + makeTrace("c", "net1", [ + { x: 2, y: 0 }, + { x: 2, y: 1.2 }, + { x: 5, y: 1.2 }, + { x: 5, y: 2 }, + ]), + ]) + + expect(a.tracePath[1]!.y).toBeCloseTo(1.1) + expect(a.tracePath[2]!.y).toBeCloseTo(1.1) + expect(b.tracePath[1]!.y).toBeCloseTo(1.1) + expect(b.tracePath[2]!.y).toBeCloseTo(1.1) + expect(c.tracePath[1]!.y).toBeCloseTo(1.1) + expect(c.tracePath[2]!.y).toBeCloseTo(1.1) +})