diff --git a/lib/solvers/SchematicTracePipelineSolver/SchematicTracePipelineSolver.ts b/lib/solvers/SchematicTracePipelineSolver/SchematicTracePipelineSolver.ts index c9d5a995..86ccbcb2 100644 --- a/lib/solvers/SchematicTracePipelineSolver/SchematicTracePipelineSolver.ts +++ b/lib/solvers/SchematicTracePipelineSolver/SchematicTracePipelineSolver.ts @@ -20,6 +20,7 @@ import { expandChipsToFitPins } from "./expandChipsToFitPins" import { LongDistancePairSolver } from "../LongDistancePairSolver/LongDistancePairSolver" import { MergedNetLabelObstacleSolver } from "../TraceLabelOverlapAvoidanceSolver/sub-solvers/LabelMergingSolver/LabelMergingSolver" import { TraceCleanupSolver } from "../TraceCleanupSolver/TraceCleanupSolver" +import { TraceCombineSolver } from "../TraceCombineSolver/TraceCombineSolver" type PipelineStep BaseSolver> = { solverName: string @@ -69,6 +70,7 @@ export class SchematicTracePipelineSolver extends BaseSolver { labelMergingSolver?: MergedNetLabelObstacleSolver traceLabelOverlapAvoidanceSolver?: TraceLabelOverlapAvoidanceSolver traceCleanupSolver?: TraceCleanupSolver + traceCombineSolver?: TraceCombineSolver startTimeOfPhase: Record endTimeOfPhase: Record @@ -143,6 +145,15 @@ export class SchematicTracePipelineSolver extends BaseSolver { onSolved: (_solver) => {}, }, ), + definePipelineStep("traceCombineSolver", TraceCombineSolver, () => [ + { + inputProblem: this.inputProblem, + inputTracePaths: Object.values( + this.traceOverlapShiftSolver!.correctedTraceMap, + ), + globalConnMap: this.mspConnectionPairSolver!.globalConnMap, + }, + ]), definePipelineStep( "netLabelPlacementSolver", NetLabelPlacementSolver, @@ -150,6 +161,7 @@ export class SchematicTracePipelineSolver extends BaseSolver { { inputProblem: this.inputProblem, inputTraceMap: + this.traceCombineSolver?.correctedTraceMap ?? this.traceOverlapShiftSolver?.correctedTraceMap ?? Object.fromEntries( this.longDistancePairSolver!.getOutput().allTracesMerged.map( @@ -169,6 +181,7 @@ export class SchematicTracePipelineSolver extends BaseSolver { TraceLabelOverlapAvoidanceSolver, (instance) => { const traceMap = + instance.traceCombineSolver?.correctedTraceMap ?? instance.traceOverlapShiftSolver?.correctedTraceMap ?? Object.fromEntries( instance diff --git a/lib/solvers/TraceCombineSolver/TraceCombineSolver.ts b/lib/solvers/TraceCombineSolver/TraceCombineSolver.ts new file mode 100644 index 00000000..470390b8 --- /dev/null +++ b/lib/solvers/TraceCombineSolver/TraceCombineSolver.ts @@ -0,0 +1,208 @@ +import { BaseSolver } from "lib/solvers/BaseSolver/BaseSolver" +import { visualizeInputProblem } from "../SchematicTracePipelineSolver/visualizeInputProblem" +import type { InputProblem } from "lib/types/InputProblem" +import type { SolvedTracePath } from "../SchematicTraceLinesSolver/SchematicTraceLinesSolver" +import type { ConnectivityMap } from "connectivity-map" +import type { MspConnectionPairId } from "../MspConnectionPairSolver/MspConnectionPairSolver" +import type { GraphicsObject, Line } from "graphics-debug" +import { simplifyPath } from "../TraceCleanupSolver/simplifyPath" + +type ConnNetId = string + +/** + * TraceCombineSolver finds same-net trace segments that are parallel and close + * together (or overlapping) and merges them into a single segment to simplify + * the routing and improve aesthetics. + */ +export class TraceCombineSolver extends BaseSolver { + inputProblem: InputProblem + inputTracePaths: Array + globalConnMap: ConnectivityMap + correctedTraceMap: Record = {} + traceNetIslands: Record> = {} + + constructor(params: { + inputProblem: InputProblem + inputTracePaths: Array + globalConnMap: ConnectivityMap + }) { + super() + this.inputProblem = params.inputProblem + this.inputTracePaths = params.inputTracePaths + this.globalConnMap = params.globalConnMap + + for (const tracePath of this.inputTracePaths) { + this.correctedTraceMap[tracePath.mspPairId] = tracePath + } + + this.traceNetIslands = this.computeTraceNetIslands() + } + + computeTraceNetIslands(): Record> { + const islands: Record> = {} + for (const original of Object.values(this.correctedTraceMap)) { + const key: ConnNetId = original.globalConnNetId + if (!islands[key]) islands[key] = [] + islands[key].push(original) + } + return islands + } + + override _step() { + const COMBINE_THRESHOLD = 0.1 + const EPS = 1e-6 + let anyChanges = false + + for (const [netId, traces] of Object.entries(this.traceNetIslands)) { + const horizontalSegments: Array<{ + y: number + x1: number + x2: number + traceId: string + segmentIndex: number + }> = [] + const verticalSegments: Array<{ + x: number + y1: number + y2: number + traceId: string + segmentIndex: number + }> = [] + + // Collect all segments for this net + for (const trace of traces) { + for (let i = 0; i < trace.tracePath.length - 1; i++) { + const p1 = trace.tracePath[i]! + const p2 = trace.tracePath[i + 1]! + if (Math.abs(p1.y - p2.y) < EPS) { + horizontalSegments.push({ + y: p1.y, + x1: Math.min(p1.x, p2.x), + x2: Math.max(p1.x, p2.x), + traceId: trace.mspPairId, + segmentIndex: i, + }) + } else if (Math.abs(p1.x - p2.x) < EPS) { + verticalSegments.push({ + x: p1.x, + y1: Math.min(p1.y, p2.y), + y2: Math.max(p1.y, p2.y), + traceId: trace.mspPairId, + segmentIndex: i, + }) + } + } + } + + // Find close parallel horizontal segments and snap them + for (let i = 0; i < horizontalSegments.length; i++) { + for (let j = i + 1; j < horizontalSegments.length; j++) { + const s1 = horizontalSegments[i]! + const s2 = horizontalSegments[j]! + if ( + Math.abs(s1.y - s2.y) < COMBINE_THRESHOLD && + Math.abs(s1.y - s2.y) > EPS + ) { + // Check if they overlap in X + const overlapX = Math.min(s1.x2, s2.x2) - Math.max(s1.x1, s2.x1) + if (overlapX > 0) { + this.snapSegment(s2.traceId, s2.segmentIndex, "y", s1.y) + anyChanges = true + // Refresh segments and restart (simplest for now) + this.traceNetIslands = this.computeTraceNetIslands() + return + } + } + } + } + + // Find close parallel vertical segments and snap them + for (let i = 0; i < verticalSegments.length; i++) { + for (let j = i + 1; j < verticalSegments.length; j++) { + const s1 = verticalSegments[i]! + const s2 = verticalSegments[j]! + if ( + Math.abs(s1.x - s2.x) < COMBINE_THRESHOLD && + Math.abs(s1.x - s2.x) > EPS + ) { + // Check if they overlap in Y + const overlapY = Math.min(s1.y2, s2.y2) - Math.max(s1.y1, s2.y1) + if (overlapY > 0) { + this.snapSegment(s2.traceId, s2.segmentIndex, "x", s1.x) + anyChanges = true + this.traceNetIslands = this.computeTraceNetIslands() + return + } + } + } + } + } + + if (!anyChanges) { + // Final pass: simplify all paths + for (const [traceId, trace] of Object.entries(this.correctedTraceMap)) { + this.correctedTraceMap[traceId] = { + ...trace, + tracePath: simplifyPath(trace.tracePath), + } + } + this.solved = true + } + } + + private snapSegment( + traceId: string, + segmentIndex: number, + axis: "x" | "y", + value: number, + ) { + const trace = this.correctedTraceMap[traceId]! + const newPath = [...trace.tracePath] + const p1 = newPath[segmentIndex]! + const p2 = newPath[segmentIndex + 1]! + + if (axis === "y") { + p1.y = value + p2.y = value + } else { + p1.x = value + p2.x = value + } + + // After snapping, we might have created "non-orthogonal" connections from the neighbor segments + // But they will be fixed in the next snap if they were already horizontal/vertical + // If not, they might become diagonal and need fixing. + // Actually, snapping horizontal segments to same Y maintains orthogonality for the neighbors + // IF the neighbors were vertical. + + this.correctedTraceMap[traceId] = { + ...trace, + tracePath: newPath, + } + } + + getOutput() { + return { + traces: Object.values(this.correctedTraceMap), + } + } + + override visualize(): GraphicsObject { + const graphics = visualizeInputProblem(this.inputProblem, { + chipAlpha: 0.1, + connectionAlpha: 0.1, + }) + + if (!graphics.lines) graphics.lines = [] + + for (const trace of Object.values(this.correctedTraceMap)) { + graphics.lines.push({ + points: trace.tracePath, + strokeColor: "green", + strokeWidth: 0.02, + }) + } + + return graphics + } +} diff --git a/site/examples/example28-trace-combine.page.tsx b/site/examples/example28-trace-combine.page.tsx new file mode 100644 index 00000000..509c5061 --- /dev/null +++ b/site/examples/example28-trace-combine.page.tsx @@ -0,0 +1,38 @@ +import { PipelineDebugger } from "site/components/PipelineDebugger" +import type { InputProblem } from "lib/types/InputProblem" + +export const inputProblem: InputProblem = { + chips: [ + { + chipId: "JP6", + center: { x: 0, y: 0 }, + width: 1, + height: 0.6, + pins: [ + { pinId: "JP6.1", x: 0.5, y: -0.1 }, + { pinId: "JP6.2", x: 0.5, y: 0.1 }, + ], + }, + { + chipId: "R1", + center: { x: 4, y: 0 }, + width: 0.4, + height: 1, + pins: [ + { pinId: "R1.1", x: 4, y: -0.4 }, + { pinId: "R1.2", x: 4, y: 0.4 }, + ], + }, + ], + directConnections: [], + netConnections: [ + { + netId: "GND", + pinIds: ["JP6.1", "JP6.2", "R1.1"], + }, + ], + availableNetLabelOrientations: {}, + maxMspPairDistance: 10, +} + +export default () => diff --git a/tests/examples/__snapshots__/example29.snap.svg b/tests/examples/__snapshots__/example29.snap.svg index c931ee37..17991423 100644 --- a/tests/examples/__snapshots__/example29.snap.svg +++ b/tests/examples/__snapshots__/example29.snap.svg @@ -498,7 +498,7 @@ x+" data-x="-8.4" data-y="-17" cx="198.31710258539454" cy="399.68032912258366" r - + @@ -516,7 +516,7 @@ x+" data-x="-8.4" data-y="-17" cx="198.31710258539454" cy="399.68032912258366" r - + @@ -813,7 +813,7 @@ x+" data-x="-8.4" data-y="-17" cx="198.31710258539454" cy="399.68032912258366" r - + @@ -831,7 +831,7 @@ x+" data-x="-8.4" data-y="-17" cx="198.31710258539454" cy="399.68032912258366" r - + @@ -849,7 +849,7 @@ x+" data-x="-8.4" data-y="-17" cx="198.31710258539454" cy="399.68032912258366" r - + @@ -1020,7 +1020,7 @@ x+" data-x="-8.4" data-y="-17" cx="198.31710258539454" cy="399.68032912258366" r - + @@ -1038,7 +1038,7 @@ x+" data-x="-8.4" data-y="-17" cx="198.31710258539454" cy="399.68032912258366" r - + diff --git a/tests/solvers/TraceCombineSolver/TraceCombineSolver.test.ts b/tests/solvers/TraceCombineSolver/TraceCombineSolver.test.ts new file mode 100644 index 00000000..7e8e129a --- /dev/null +++ b/tests/solvers/TraceCombineSolver/TraceCombineSolver.test.ts @@ -0,0 +1,64 @@ +import { expect, test } from "bun:test" +import { TraceCombineSolver } from "lib/solvers/TraceCombineSolver/TraceCombineSolver" +import type { InputProblem } from "lib/types/InputProblem" +import { MspConnectionPairSolver } from "lib/solvers/MspConnectionPairSolver/MspConnectionPairSolver" +import { SchematicTraceLinesSolver } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceLinesSolver" + +test("TraceCombineSolver merges parallel same-net traces", () => { + const inputProblem: InputProblem = { + chips: [ + { + chipId: "U1", + center: { x: 0, y: 0 }, + width: 1, + height: 1, + pins: [ + { pinId: "U1.1", x: 0.5, y: -0.1 }, + { pinId: "U1.2", x: 0.5, y: 0.1 }, + ], + }, + { + chipId: "U2", + center: { x: 4, y: 0 }, + width: 1, + height: 1, + pins: [{ pinId: "U2.1", x: 3.5, y: -0.4 }], + }, + ], + directConnections: [], + netConnections: [ + { + netId: "GND", + pinIds: ["U1.1", "U1.2", "U2.1"], + }, + ], + availableNetLabelOrientations: {}, + maxMspPairDistance: 10, + } + + const mspSolver = new MspConnectionPairSolver({ inputProblem }) + mspSolver.solve() + + const linesSolver = new SchematicTraceLinesSolver({ + mspConnectionPairs: mspSolver.mspConnectionPairs, + dcConnMap: mspSolver.dcConnMap, + globalConnMap: mspSolver.globalConnMap, + inputProblem: inputProblem, + chipMap: mspSolver.chipMap, + }) + linesSolver.solve() + + const combineSolver = new TraceCombineSolver({ + inputProblem, + inputTracePaths: linesSolver.solvedTracePaths, + globalConnMap: mspSolver.globalConnMap, + }) + combineSolver.solve() + + const output = combineSolver.getOutput() + + // Verify that overlapping segments are snapped to the same coordinate + // We expect at least one horizontal segment to have been modified if it was close but not identical + expect(combineSolver.solved).toBe(true) + expect(output.traces.length).toBeGreaterThan(0) +}) diff --git a/tests/solvers/TraceLabelOverlapAvoidanceSolver/renderComparisonView/__snapshots__/renderComparisonView01.snap.svg b/tests/solvers/TraceLabelOverlapAvoidanceSolver/renderComparisonView/__snapshots__/renderComparisonView01.snap.svg index 9c846d8b..38021f4c 100644 --- a/tests/solvers/TraceLabelOverlapAvoidanceSolver/renderComparisonView/__snapshots__/renderComparisonView01.snap.svg +++ b/tests/solvers/TraceLabelOverlapAvoidanceSolver/renderComparisonView/__snapshots__/renderComparisonView01.snap.svg @@ -155,13 +155,13 @@ x-" data-x="7.850000000000001" data-y="-2.295" cx="490.488888888889" cy="385.364 - + - + NetLabelPlacementSolverMergedNetLabelObstacles