diff --git a/lib/solvers/TraceCleanupSolver/TraceCleanupSolver.ts b/lib/solvers/TraceCleanupSolver/TraceCleanupSolver.ts index e9bac7ca3..452618eac 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" /** @@ -34,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 { @@ -84,6 +87,9 @@ export class TraceCleanupSolver extends BaseSolver { case "balancing_l_shapes": this._runBalanceLShapesStep() break + case "merging_close_same_net_segments": + this._runMergeCloseSameNetSegmentsStep() + break } } @@ -108,13 +114,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..e4e9a242c --- /dev/null +++ b/lib/solvers/TraceCleanupSolver/mergeNearbySameNetSegments.ts @@ -0,0 +1,160 @@ +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 })), + })) + + 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 candidateIndex = 0; + candidateIndex < segments.length; + candidateIndex++ + ) { + 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 (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 new file mode 100644 index 000000000..bc8f8a571 --- /dev/null +++ b/tests/solvers/TraceCleanupSolver/mergeNearbySameNetSegments.test.ts @@ -0,0 +1,90 @@ +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) +}) + +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) +})