diff --git a/lib/solvers/TraceCleanupSolver/TraceCleanupSolver.ts b/lib/solvers/TraceCleanupSolver/TraceCleanupSolver.ts index e9bac7ca3..5e142fab2 100644 --- a/lib/solvers/TraceCleanupSolver/TraceCleanupSolver.ts +++ b/lib/solvers/TraceCleanupSolver/TraceCleanupSolver.ts @@ -1,11 +1,11 @@ -import type { InputProblem } from "lib/types/InputProblem" import type { GraphicsObject, Line } from "graphics-debug" -import { minimizeTurnsWithFilteredLabels } from "./minimizeTurnsWithFilteredLabels" -import { balanceZShapes } from "./balanceZShapes" import { BaseSolver } from "lib/solvers/BaseSolver/BaseSolver" import type { SolvedTracePath } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceLinesSolver" import { visualizeInputProblem } from "lib/solvers/SchematicTracePipelineSolver/visualizeInputProblem" +import type { InputProblem } from "lib/types/InputProblem" import type { NetLabelPlacement } from "../NetLabelPlacementSolver/NetLabelPlacementSolver" +import { balanceZShapes } from "./balanceZShapes" +import { minimizeTurnsWithFilteredLabels } from "./minimizeTurnsWithFilteredLabels" /** * Defines the input structure for the TraceCleanupSolver. @@ -18,8 +18,9 @@ interface TraceCleanupSolverInput { paddingBuffer: number } -import { UntangleTraceSubsolver } from "./sub-solver/UntangleTraceSubsolver" +import { combineSameNetTraceSegments } from "./combineSameNetTraceSegments" import { is4PointRectangle } from "./is4PointRectangle" +import { UntangleTraceSubsolver } from "./sub-solver/UntangleTraceSubsolver" /** * 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" + | "combining_same_net_segments" | "untangling_traces" /** @@ -84,6 +86,9 @@ export class TraceCleanupSolver extends BaseSolver { case "balancing_l_shapes": this._runBalanceLShapesStep() break + case "combining_same_net_segments": + this._runCombineSameNetSegmentsStep() + break } } @@ -108,13 +113,25 @@ export class TraceCleanupSolver extends BaseSolver { private _runBalanceLShapesStep() { if (this.traceIdQueue.length === 0) { - this.solved = true + this.pipelineStep = "combining_same_net_segments" return } this._processTrace("balancing_l_shapes") } + private _runCombineSameNetSegmentsStep() { + this.outputTraces = combineSameNetTraceSegments({ + traces: this.outputTraces, + inputProblem: this.input.inputProblem, + allLabelPlacements: this.input.allLabelPlacements, + mergedLabelNetIdMap: this.input.mergedLabelNetIdMap, + maxDistance: this.input.paddingBuffer, + }) + 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/combineSameNetTraceSegments.ts b/lib/solvers/TraceCleanupSolver/combineSameNetTraceSegments.ts new file mode 100644 index 000000000..8bd19f759 --- /dev/null +++ b/lib/solvers/TraceCleanupSolver/combineSameNetTraceSegments.ts @@ -0,0 +1,282 @@ +import type { Point } from "@tscircuit/math-utils" +import type { SolvedTracePath } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceLinesSolver" +import { segmentIntersectsRect } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver2/collisions" +import { getObstacleRects } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver2/rect" +import type { InputProblem } from "lib/types/InputProblem" +import type { NetLabelPlacement } from "../NetLabelPlacementSolver/NetLabelPlacementSolver" +import { simplifyPath } from "./simplifyPath" + +type Axis = "horizontal" | "vertical" + +interface SegmentRef { + traceIndex: number + segmentIndex: number + axis: Axis + fixedCoord: number + min: number + max: number + length: number +} + +type BlockingRect = { + chipId: string + minX: number + maxX: number + minY: number + maxY: number +} + +const EPS = 1e-6 +const TRACE_WIDTH = 0.01 +const MAX_PASSES = 50 + +const isHorizontalSegment = (p1: Point, p2: Point) => + Math.abs(p1.y - p2.y) < EPS + +const isVerticalSegment = (p1: Point, p2: Point) => Math.abs(p1.x - p2.x) < EPS + +const rangesOverlap = (a: SegmentRef, b: SegmentRef) => + Math.min(a.max, b.max) - Math.max(a.min, b.min) > EPS + +const getInternalSegments = (traces: SolvedTracePath[]): Array => { + const refs: Array = [] + + for (let traceIndex = 0; traceIndex < traces.length; traceIndex++) { + const tracePath = traces[traceIndex]!.tracePath + for ( + let segmentIndex = 1; + segmentIndex < tracePath.length - 2; + segmentIndex++ + ) { + const p1 = tracePath[segmentIndex]! + const p2 = tracePath[segmentIndex + 1]! + if (isHorizontalSegment(p1, p2)) { + const min = Math.min(p1.x, p2.x) + const max = Math.max(p1.x, p2.x) + refs.push({ + traceIndex, + segmentIndex, + axis: "horizontal", + fixedCoord: (p1.y + p2.y) / 2, + min, + max, + length: max - min, + }) + } else if (isVerticalSegment(p1, p2)) { + const min = Math.min(p1.y, p2.y) + const max = Math.max(p1.y, p2.y) + refs.push({ + traceIndex, + segmentIndex, + axis: "vertical", + fixedCoord: (p1.x + p2.x) / 2, + min, + max, + length: max - min, + }) + } + } + } + + return refs +} + +const getAffectedSegments = (path: Point[], segmentIndex: number): Point[] => { + const startIndex = Math.max(0, segmentIndex - 1) + const endIndex = Math.min(path.length - 1, segmentIndex + 2) + return path.slice(startIndex, endIndex + 1) +} + +const moveSegmentToCoord = ( + trace: SolvedTracePath, + segmentIndex: number, + axis: Axis, + targetCoord: number, +): Point[] => { + const path = trace.tracePath.map((point) => ({ ...point })) + if (axis === "horizontal") { + path[segmentIndex]!.y = targetCoord + path[segmentIndex + 1]!.y = targetCoord + } else { + path[segmentIndex]!.x = targetCoord + path[segmentIndex + 1]!.x = targetCoord + } + return path +} + +const segmentIntersectsAnyRect = ( + points: Point[], + rects: BlockingRect[], +): boolean => { + for (let i = 0; i < points.length - 1; i++) { + for (const rect of rects) { + if (segmentIntersectsRect(points[i]!, points[i + 1]!, rect)) { + return true + } + } + } + return false +} + +const getBlockingRects = ({ + traces, + targetTrace, + inputProblem, + allLabelPlacements, + mergedLabelNetIdMap, +}: { + traces: SolvedTracePath[] + targetTrace: SolvedTracePath + inputProblem: InputProblem + allLabelPlacements: NetLabelPlacement[] + mergedLabelNetIdMap: Record> +}) => { + const staticObstacles = getObstacleRects(inputProblem).map((obs) => ({ + ...obs, + minX: obs.minX + EPS, + maxX: obs.maxX - EPS, + minY: obs.minY + EPS, + maxY: obs.maxY - EPS, + })) + + const traceObstacles = traces.flatMap((trace, traceIndex) => { + if (trace.globalConnNetId === targetTrace.globalConnNetId) return [] + + return trace.tracePath.slice(0, -1).map((p1, segmentIndex) => { + const p2 = trace.tracePath[segmentIndex + 1]! + return { + chipId: `trace-obstacle-${traceIndex}-${segmentIndex}`, + minX: Math.min(p1.x, p2.x) - TRACE_WIDTH / 2, + minY: Math.min(p1.y, p2.y) - TRACE_WIDTH / 2, + maxX: Math.max(p1.x, p2.x) + TRACE_WIDTH / 2, + maxY: Math.max(p1.y, p2.y) + TRACE_WIDTH / 2, + } + }) + }) + + const labelBounds = allLabelPlacements + .filter((label) => { + const originalNetIds = mergedLabelNetIdMap[label.globalConnNetId] + if (originalNetIds) { + return !originalNetIds.has(targetTrace.globalConnNetId) + } + return label.globalConnNetId !== targetTrace.globalConnNetId + }) + .map((label) => ({ + chipId: `net-label-${label.globalConnNetId}`, + minX: label.center.x - label.width / 2, + maxX: label.center.x + label.width / 2, + minY: label.center.y - label.height / 2, + maxY: label.center.y + label.height / 2, + })) + + return [...staticObstacles, ...traceObstacles, ...labelBounds] +} + +const trySnapSegment = ({ + traces, + fromSegment, + toSegment, + inputProblem, + allLabelPlacements, + mergedLabelNetIdMap, +}: { + traces: SolvedTracePath[] + fromSegment: SegmentRef + toSegment: SegmentRef + inputProblem: InputProblem + allLabelPlacements: NetLabelPlacement[] + mergedLabelNetIdMap: Record> +}): boolean => { + const targetTrace = traces[fromSegment.traceIndex]! + const candidatePath = moveSegmentToCoord( + targetTrace, + fromSegment.segmentIndex, + fromSegment.axis, + toSegment.fixedCoord, + ) + + const blockingRects = getBlockingRects({ + traces, + targetTrace, + inputProblem, + allLabelPlacements, + mergedLabelNetIdMap, + }) + + const affectedSegments = getAffectedSegments( + candidatePath, + fromSegment.segmentIndex, + ) + if (segmentIntersectsAnyRect(affectedSegments, blockingRects)) { + return false + } + + traces[fromSegment.traceIndex] = { + ...targetTrace, + tracePath: simplifyPath(candidatePath), + } + return true +} + +export const combineSameNetTraceSegments = ({ + traces, + inputProblem, + allLabelPlacements, + mergedLabelNetIdMap, + maxDistance, +}: { + traces: SolvedTracePath[] + inputProblem: InputProblem + allLabelPlacements: NetLabelPlacement[] + mergedLabelNetIdMap: Record> + maxDistance: number +}): SolvedTracePath[] => { + const combinedTraces = traces.map((trace) => ({ + ...trace, + tracePath: trace.tracePath.map((point) => ({ ...point })), + })) + + for (let pass = 0; pass < MAX_PASSES; pass++) { + const segments = getInternalSegments(combinedTraces) + let changed = false + + for (let i = 0; i < segments.length; i++) { + const segmentA = segments[i]! + const traceA = combinedTraces[segmentA.traceIndex]! + + for (let j = i + 1; j < segments.length; j++) { + const segmentB = segments[j]! + const traceB = combinedTraces[segmentB.traceIndex]! + + if (traceA.globalConnNetId !== traceB.globalConnNetId) continue + if (segmentA.axis !== segmentB.axis) continue + if (!rangesOverlap(segmentA, segmentB)) continue + + const distance = Math.abs(segmentA.fixedCoord - segmentB.fixedCoord) + if (distance <= EPS || distance > maxDistance) continue + + const [fromSegment, toSegment] = + segmentA.length < segmentB.length + ? [segmentA, segmentB] + : [segmentB, segmentA] + + changed = trySnapSegment({ + traces: combinedTraces, + fromSegment, + toSegment, + inputProblem, + allLabelPlacements, + mergedLabelNetIdMap, + }) + + if (changed) break + } + if (changed) break + } + + if (!changed) break + } + + return combinedTraces +} diff --git a/tests/solvers/TraceCleanupSolver/combine-same-net-trace-segments.test.ts b/tests/solvers/TraceCleanupSolver/combine-same-net-trace-segments.test.ts new file mode 100644 index 000000000..8509220ac --- /dev/null +++ b/tests/solvers/TraceCleanupSolver/combine-same-net-trace-segments.test.ts @@ -0,0 +1,116 @@ +import { expect, test } from "bun:test" +import type { SolvedTracePath } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceLinesSolver" +import { combineSameNetTraceSegments } from "lib/solvers/TraceCleanupSolver/combineSameNetTraceSegments" +import type { InputProblem } from "lib/types/InputProblem" + +const inputProblem: InputProblem = { + chips: [], + directConnections: [], + netConnections: [], + availableNetLabelOrientations: {}, +} + +const makeTrace = ( + mspPairId: string, + globalConnNetId: string, + tracePath: SolvedTracePath["tracePath"], +): SolvedTracePath => + ({ + mspPairId, + dcConnNetId: globalConnNetId, + globalConnNetId, + pins: [] as any, + mspConnectionPairIds: [mspPairId], + pinIds: [], + tracePath, + }) as SolvedTracePath + +test("combines close overlapping same-net internal horizontal segments", () => { + const [traceA, traceB] = combineSameNetTraceSegments({ + traces: [ + makeTrace("trace-a", "net-1", [ + { x: 0, y: 0 }, + { x: 1, y: 0 }, + { x: 1, y: 1 }, + { x: 4, y: 1 }, + { x: 4, y: 0 }, + { x: 5, y: 0 }, + ]), + makeTrace("trace-b", "net-1", [ + { x: 0, y: 2 }, + { x: 1, y: 2 }, + { x: 1, y: 1.05 }, + { x: 4, y: 1.05 }, + { x: 4, y: 2 }, + { x: 5, y: 2 }, + ]), + ], + inputProblem, + allLabelPlacements: [], + mergedLabelNetIdMap: {}, + maxDistance: 0.1, + }) + + expect(traceA!.tracePath[2]!.y).toBe(1) + expect(traceA!.tracePath[3]!.y).toBe(1) + expect(traceB!.tracePath[2]!.y).toBe(1) + expect(traceB!.tracePath[3]!.y).toBe(1) +}) + +test("does not combine close segments from different nets", () => { + const [, traceB] = combineSameNetTraceSegments({ + traces: [ + makeTrace("trace-a", "net-1", [ + { x: 0, y: 0 }, + { x: 1, y: 0 }, + { x: 1, y: 1 }, + { x: 4, y: 1 }, + { x: 4, y: 0 }, + { x: 5, y: 0 }, + ]), + makeTrace("trace-b", "net-2", [ + { x: 0, y: 2 }, + { x: 1, y: 2 }, + { x: 1, y: 1.05 }, + { x: 4, y: 1.05 }, + { x: 4, y: 2 }, + { x: 5, y: 2 }, + ]), + ], + inputProblem, + allLabelPlacements: [], + mergedLabelNetIdMap: {}, + maxDistance: 0.1, + }) + + expect(traceB!.tracePath[2]!.y).toBe(1.05) + expect(traceB!.tracePath[3]!.y).toBe(1.05) +}) + +test("leaves endpoint-only traces anchored to their pins", () => { + const [traceA, traceB] = combineSameNetTraceSegments({ + traces: [ + makeTrace("trace-a", "net-1", [ + { x: 0, y: 1 }, + { x: 4, y: 1 }, + ]), + makeTrace("trace-b", "net-1", [ + { x: 0, y: 1.05 }, + { x: 4, y: 1.05 }, + ]), + ], + inputProblem, + allLabelPlacements: [], + mergedLabelNetIdMap: {}, + maxDistance: 0.1, + }) + + expect(traceA!.tracePath).toEqual([ + { x: 0, y: 1 }, + { x: 4, y: 1 }, + ]) + expect(traceB!.tracePath).toEqual([ + { x: 0, y: 1.05 }, + { x: 4, y: 1.05 }, + ]) +})