Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 17 additions & 1 deletion lib/solvers/TraceCleanupSolver/TraceCleanupSolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,15 @@ interface TraceCleanupSolverInput {

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

/**
* Represents the different stages or steps within the trace cleanup pipeline.
*/
type PipelineStep =
| "minimizing_turns"
| "balancing_l_shapes"
| "merging_same_net_lines"
| "untangling_traces"

/**
Expand Down Expand Up @@ -84,6 +86,9 @@ export class TraceCleanupSolver extends BaseSolver {
case "balancing_l_shapes":
this._runBalanceLShapesStep()
break
case "merging_same_net_lines":
this._runMergeSameNetLinesStep()
break
}
}

Expand All @@ -108,13 +113,24 @@ export class TraceCleanupSolver extends BaseSolver {

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

this._processTrace("balancing_l_shapes")
}

private _runMergeSameNetLinesStep() {
this.outputTraces = mergeNearbySameNetTraceLines(
this.outputTraces,
this.input.paddingBuffer,
)
this.tracesMap = new Map(this.outputTraces.map((t) => [t.mspPairId, t]))
this.pipelineStep = "balancing_l_shapes"
this.traceIdQueue = []
this.solved = true
}

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

const EPS = 1e-6

type SegmentOrientation = "horizontal" | "vertical"

interface SegmentLocator {
traceIndex: number
segmentIndex: number
orientation: SegmentOrientation
coord: number
spanMin: number
spanMax: number
length: number
canMove: boolean
}

const getSegmentLocator = (
trace: SolvedTracePath,
traceIndex: number,
segmentIndex: number,
): SegmentLocator | null => {
const p1 = trace.tracePath[segmentIndex]
const p2 = trace.tracePath[segmentIndex + 1]
if (!p1 || !p2) return null

const isHorizontal = Math.abs(p1.y - p2.y) < EPS
const isVertical = Math.abs(p1.x - p2.x) < EPS
if (!isHorizontal && !isVertical) return null

if (isHorizontal) {
const spanMin = Math.min(p1.x, p2.x)
const spanMax = Math.max(p1.x, p2.x)
if (spanMax - spanMin < EPS) return null
return {
traceIndex,
segmentIndex,
orientation: "horizontal",
coord: p1.y,
spanMin,
spanMax,
length: spanMax - spanMin,
canMove: segmentIndex > 0 && segmentIndex < trace.tracePath.length - 2,
}
}

const spanMin = Math.min(p1.y, p2.y)
const spanMax = Math.max(p1.y, p2.y)
if (spanMax - spanMin < EPS) return null
return {
traceIndex,
segmentIndex,
orientation: "vertical",
coord: p1.x,
spanMin,
spanMax,
length: spanMax - spanMin,
canMove: segmentIndex > 0 && segmentIndex < trace.tracePath.length - 2,
}
}

const getSegmentsByNet = (traces: SolvedTracePath[]) => {
const segmentsByNet = new Map<string, SegmentLocator[]>()

traces.forEach((trace, traceIndex) => {
for (
let segmentIndex = 0;
segmentIndex < trace.tracePath.length - 1;
segmentIndex++
) {
const locator = getSegmentLocator(trace, traceIndex, segmentIndex)
if (!locator) continue
const existing = segmentsByNet.get(trace.globalConnNetId) ?? []
existing.push(locator)
segmentsByNet.set(trace.globalConnNetId, existing)
}
})

return segmentsByNet
}

const getAllSegments = (traces: SolvedTracePath[]) =>
traces.flatMap((trace, traceIndex) =>
trace.tracePath.flatMap((_, segmentIndex) => {
const locator = getSegmentLocator(trace, traceIndex, segmentIndex)
return locator ? [locator] : []
}),
)

const spansOverlap = (a: SegmentLocator, b: SegmentLocator) =>
Math.min(a.spanMax, b.spanMax) - Math.max(a.spanMin, b.spanMin) > EPS

const moveSegmentToCoord = (
trace: SolvedTracePath,
segmentIndex: number,
orientation: SegmentOrientation,
coord: number,
) => {
const p1 = trace.tracePath[segmentIndex]!
const p2 = trace.tracePath[segmentIndex + 1]!

if (orientation === "horizontal") {
p1.y = coord
p2.y = coord
} else {
p1.x = coord
p2.x = coord
}

trace.tracePath = simplifyPath(trace.tracePath)
}

const cloneAndMoveTrace = (
trace: SolvedTracePath,
segmentIndex: number,
orientation: SegmentOrientation,
coord: number,
): SolvedTracePath => {
const clonedTrace = {
...trace,
tracePath: trace.tracePath.map((point) => ({ ...point })),
}
moveSegmentToCoord(clonedTrace, segmentIndex, orientation, coord)
return clonedTrace
}

const wouldCreateDifferentNetOverlap = (
traces: SolvedTracePath[],
source: SegmentLocator,
target: SegmentLocator,
) => {
const movedTrace = cloneAndMoveTrace(
traces[source.traceIndex]!,
source.segmentIndex,
source.orientation,
target.coord,
)
const movedTraceSegments = getAllSegments([movedTrace])
const otherNetSegments = getAllSegments(traces).filter(
(segment) =>
traces[segment.traceIndex]!.globalConnNetId !==
movedTrace.globalConnNetId,
)

return movedTraceSegments.some((candidate) =>
otherNetSegments.some(
(other) =>
candidate.orientation === other.orientation &&
Math.abs(candidate.coord - other.coord) < EPS &&
spansOverlap(candidate, other),
),
)
}

const pickSourceAndTarget = (
a: SegmentLocator,
b: SegmentLocator,
): { source: SegmentLocator; target: SegmentLocator } | null => {
if (!a.canMove && !b.canMove) return null
if (a.canMove && !b.canMove) return { source: a, target: b }
if (!a.canMove && b.canMove) return { source: b, target: a }

// Move the shorter segment onto the longer one to preserve the dominant run.
return a.length <= b.length
? { source: a, target: b }
: { source: b, target: a }
}

/**
* Aligns nearby overlapping segments that belong to the same global net.
*
* Endpoint segments are left in place so traces remain connected to their pins.
* Internal segments can be moved onto a nearby same-net segment; adjacent
* orthogonal segments stretch or shrink naturally because they share endpoints.
*/
export const mergeNearbySameNetTraceLines = (
traces: SolvedTracePath[],
maxDistance: number,
): SolvedTracePath[] => {
const outputTraces = traces.map((trace) => ({
...trace,
tracePath: trace.tracePath.map((point) => ({ ...point })),
}))

let changed = true
let passCount = 0
const maxPasses = Math.max(1, outputTraces.length * 8)

while (changed && passCount < maxPasses) {
changed = false
passCount++

const segmentsByNet = getSegmentsByNet(outputTraces)

for (const segments of segmentsByNet.values()) {
for (let i = 0; i < segments.length; i++) {
for (let j = i + 1; j < segments.length; j++) {
const a = segments[i]!
const b = segments[j]!
if (
a.traceIndex === b.traceIndex &&
Math.abs(a.segmentIndex - b.segmentIndex) <= 1
) {
continue
}
if (a.orientation !== b.orientation) continue
if (Math.abs(a.coord - b.coord) < EPS) continue
if (Math.abs(a.coord - b.coord) > maxDistance) continue
if (!spansOverlap(a, b)) continue

const pair = pickSourceAndTarget(a, b)
if (!pair) continue
if (
wouldCreateDifferentNetOverlap(
outputTraces,
pair.source,
pair.target,
)
) {
continue
}

moveSegmentToCoord(
outputTraces[pair.source.traceIndex]!,
pair.source.segmentIndex,
pair.source.orientation,
pair.target.coord,
)
changed = true
break
}
if (changed) break
}
if (changed) break
}
}

return outputTraces
}
Loading
Loading