Skip to content
Open
25 changes: 23 additions & 2 deletions lib/solvers/TraceCleanupSolver/TraceCleanupSolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ interface TraceCleanupSolverInput {

import { UntangleTraceSubsolver } from "./sub-solver/UntangleTraceSubsolver"
import { is4PointRectangle } from "./is4PointRectangle"
import { mergeCollinearTraces } from "./mergeCollinearTraces"

/**
* Represents the different stages or steps within the trace cleanup pipeline.
Expand All @@ -28,13 +29,15 @@ type PipelineStep =
| "minimizing_turns"
| "balancing_l_shapes"
| "untangling_traces"
| "merging_collinear_traces"

/**
* The TraceCleanupSolver is responsible for improving the aesthetics and readability of schematic traces.
* 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 Collinear Traces**: Finally, it merges trace segments that belong to the same net, are collinear (aligned on the same axis), and are close together into single longer segments.
* The solver processes traces one by one, applying these cleanup steps sequentially to refine the overall trace layout.
*/
export class TraceCleanupSolver extends BaseSolver {
Expand Down Expand Up @@ -84,6 +87,9 @@ export class TraceCleanupSolver extends BaseSolver {
case "balancing_l_shapes":
this._runBalanceLShapesStep()
break
case "merging_collinear_traces":
this._runMergeCollinearTracesStep()
break
}
}

Expand All @@ -108,13 +114,28 @@ export class TraceCleanupSolver extends BaseSolver {

private _runBalanceLShapesStep() {
if (this.traceIdQueue.length === 0) {
this.solved = true
this.pipelineStep = "merging_collinear_traces"
return
}

this._processTrace("balancing_l_shapes")
}

private _runMergeCollinearTracesStep() {
// Apply the merging algorithm to all traces at once
const allTraces = Array.from(this.tracesMap.values())
const mergedTraces = mergeCollinearTraces(allTraces)

// Update the traces map with merged results
this.tracesMap.clear()
for (const trace of mergedTraces) {
this.tracesMap.set(trace.mspPairId, trace)
}
this.outputTraces = mergedTraces

this.solved = true
}

private _processTrace(step: "minimizing_turns" | "balancing_l_shapes") {
const targetMspConnectionPairId = this.traceIdQueue.shift()!
this.activeTraceId = targetMspConnectionPairId
Expand Down
188 changes: 188 additions & 0 deletions lib/solvers/TraceCleanupSolver/mergeCollinearTraces.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
import type { Point } from "@tscircuit/math-utils"
import type { SolvedTracePath } from "../SchematicTraceLinesSolver/SchematicTraceLinesSolver"

interface SimpleTrace {
trace: SolvedTracePath
start: Point
end: Point
isHorizontal: boolean
isVertical: boolean
}

/**
* Checks if a trace is a simple two-point line segment
*/
function isSimpleLineSegment(trace: SolvedTracePath): boolean {
return trace.tracePath.length === 2
}

/**
* Extracts simple line segment info from a trace
*/
function getSimpleTraceInfo(trace: SolvedTracePath): SimpleTrace | null {
if (!isSimpleLineSegment(trace)) return null

const start = trace.tracePath[0]
const end = trace.tracePath[1]
const isHorizontal = Math.abs(start.y - end.y) < 1e-6
const isVertical = Math.abs(start.x - end.x) < 1e-6

return {
trace,
start,
end,
isHorizontal,
isVertical,
}
}

/**
* Checks if two simple traces can be merged
*/
function canMergeSimpleTraces(
t1: SimpleTrace,
t2: SimpleTrace,
threshold: number = 0.05,
): boolean {
const netId1 = t1.trace.userNetId ?? t1.trace.globalConnNetId
const netId2 = t2.trace.userNetId ?? t2.trace.globalConnNetId

// Must be same net
if (netId1 !== netId2) return false

// Both must be horizontal or both vertical
if (t1.isHorizontal && t2.isHorizontal) {
// Check if they're on the same horizontal line
if (Math.abs(t1.start.y - t2.start.y) > threshold) return false

// Check if they overlap or are close in the x direction
const t1MinX = Math.min(t1.start.x, t1.end.x)
const t1MaxX = Math.max(t1.start.x, t1.end.x)
const t2MinX = Math.min(t2.start.x, t2.end.x)
const t2MaxX = Math.max(t2.start.x, t2.end.x)

// Check for overlap or closeness
return (
(t1MaxX >= t2MinX - threshold && t1MinX <= t2MaxX + threshold) ||
(t2MaxX >= t1MinX - threshold && t2MinX <= t1MaxX + threshold)
)
} else if (t1.isVertical && t2.isVertical) {
// Check if they're on the same vertical line
if (Math.abs(t1.start.x - t2.start.x) > threshold) return false

// Check if they overlap or are close in the y direction
const t1MinY = Math.min(t1.start.y, t1.end.y)
const t1MaxY = Math.max(t1.start.y, t1.end.y)
const t2MinY = Math.min(t2.start.y, t2.end.y)
const t2MaxY = Math.max(t2.start.y, t2.end.y)

// Check for overlap or closeness
return (
(t1MaxY >= t2MinY - threshold && t1MinY <= t2MaxY + threshold) ||
(t2MaxY >= t1MinY - threshold && t2MinY <= t1MaxY + threshold)
)
}

return false
}

/**
* Merges two simple traces into one
*/
function mergeSimpleTraces(t1: SimpleTrace, t2: SimpleTrace): SolvedTracePath {
if (t1.isHorizontal) {
const minX = Math.min(t1.start.x, t1.end.x, t2.start.x, t2.end.x)
const maxX = Math.max(t1.start.x, t1.end.x, t2.start.x, t2.end.x)
const y = (t1.start.y + t2.start.y) / 2

return {
...t1.trace,
tracePath: [
{ x: minX, y },
{ x: maxX, y },
],
mspConnectionPairIds: [
...t1.trace.mspConnectionPairIds,
...t2.trace.mspConnectionPairIds,
],
pinIds: [...t1.trace.pinIds, ...t2.trace.pinIds],
}
} else {
// Vertical
const minY = Math.min(t1.start.y, t1.end.y, t2.start.y, t2.end.y)
const maxY = Math.max(t1.start.y, t1.end.y, t2.start.y, t2.end.y)
const x = (t1.start.x + t2.start.x) / 2

return {
...t1.trace,
tracePath: [
{ x, y: minY },
{ x, y: maxY },
],
mspConnectionPairIds: [
...t1.trace.mspConnectionPairIds,
...t2.trace.mspConnectionPairIds,
],
pinIds: [...t1.trace.pinIds, ...t2.trace.pinIds],
}
}
}

/**
* Groups segments by net and orientation, then merges collinear segments that are close together.
* Only merges simple two-point line segments.
*/
export function mergeCollinearTraces(
traces: SolvedTracePath[],
threshold: number = 0.05,
): SolvedTracePath[] {
if (traces.length === 0) return traces

// Separate simple traces from complex ones
const simpleTraces: SimpleTrace[] = []
const complexTraces: SolvedTracePath[] = []

for (const trace of traces) {
const simpleInfo = getSimpleTraceInfo(trace)
if (simpleInfo) {
simpleTraces.push(simpleInfo)
} else {
complexTraces.push(trace)
}
}

// Merge simple traces
const merged = new Set<number>()
const mergedTraces: SolvedTracePath[] = []

for (let i = 0; i < simpleTraces.length; i++) {
if (merged.has(i)) continue

let current = simpleTraces[i]
merged.add(i)

// Try to merge with other traces
let foundMerge = true
while (foundMerge) {
foundMerge = false

for (let j = 0; j < simpleTraces.length; j++) {
if (merged.has(j)) continue

if (canMergeSimpleTraces(current, simpleTraces[j], threshold)) {
current = getSimpleTraceInfo(
mergeSimpleTraces(current, simpleTraces[j]),
)!
merged.add(j)
foundMerge = true
break // Start over to find more merges
}
}
}

mergedTraces.push(current.trace)
}

// Return merged traces + complex traces
return [...mergedTraces, ...complexTraces]
}
Loading