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
279 changes: 279 additions & 0 deletions lib/solvers/SameNetTraceMergeSolver/SameNetTraceMergeSolver.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,279 @@
import { BaseSolver } from "../BaseSolver/BaseSolver"
import type { SolvedTracePath } from "../SchematicTraceLinesSolver/SchematicTraceLinesSolver"
import type { Point } from "@tscircuit/math-utils"

export interface SameNetTraceMergeSolverParams {
traces: SolvedTracePath[]
}

export class SameNetTraceMergeSolver extends BaseSolver {
traces: SolvedTracePath[]
mergedTraces: SolvedTracePath[] = []

constructor(params: SameNetTraceMergeSolverParams) {
super()
this.traces = params.traces
}

override _step() {
this.mergedTraces = this.mergeTraces(this.traces)
this.solved = true
}

public getOutput() {
return {
traces: this.mergedTraces,
}
}

private mergeTraces(traces: SolvedTracePath[]): SolvedTracePath[] {
if (traces.length === 0) return []

// Group traces by globalConnNetId
const netGroups: Record<string, SolvedTracePath[]> = {}
for (const trace of traces) {
const netId = trace.globalConnNetId
if (!netGroups[netId]) {
netGroups[netId] = []
}
netGroups[netId].push(trace)
}

const allMergedTraces: SolvedTracePath[] = []

for (const netId in netGroups) {
const mergedForNet = this.mergeTracesForNet(netGroups[netId])
allMergedTraces.push(...mergedForNet)
}

return allMergedTraces
}

private mergeTracesForNet(netTraces: SolvedTracePath[]): SolvedTracePath[] {
if (netTraces.length === 0) return []

// 1. Extract all segments
const segments: { p1: Point; p2: Point; trace: SolvedTracePath }[] = []
for (const trace of netTraces) {
for (let i = 0; i < trace.tracePath.length - 1; i++) {
const p1 = trace.tracePath[i]
const p2 = trace.tracePath[i + 1]
// Normalize segment: p1 is "smaller" than p2
const [sp1, sp2] = this.sortPoints(p1, p2)
segments.push({ p1: sp1, p2: sp2, trace })
}
}

// 2. Merge collinear and overlapping segments
const mergedSegments = this.mergeCollinearSegments(segments)

// 3. Reconstruct paths from merged segments
return this.reconstructPaths(mergedSegments, netTraces[0].globalConnNetId)
}

private sortPoints(p1: Point, p2: Point): [Point, Point] {
if (p1.x < p2.x || (p1.x === p2.x && p1.y < p2.y)) {
return [p1, p2]
}
return [p2, p1]
}

private mergeCollinearSegments(
segments: { p1: Point; p2: Point; trace: SolvedTracePath }[]
): { p1: Point; p2: Point; mspIds: Set<string>; pinIds: Set<string> }[] {
const horizontal = new Map<number, typeof segments>()
const vertical = new Map<number, typeof segments>()
const diag = new Map<string, typeof segments>() // For non-HV segments if any

const threshold = 0.001

for (const seg of segments) {
if (Math.abs(seg.p1.y - seg.p2.y) < threshold) {
// Horizontal
const y = Math.round(seg.p1.y / threshold) * threshold
if (!horizontal.has(y)) horizontal.set(y, [])
horizontal.get(y)!.push(seg)
} else if (Math.abs(seg.p1.x - seg.p2.x) < threshold) {
// Vertical
const x = Math.round(seg.p1.x / threshold) * threshold
if (!vertical.has(x)) vertical.set(x, [])
vertical.get(x)!.push(seg)
} else {
// Diagonal - group by slope and intercept
const slope = (seg.p2.y - seg.p1.y) / (seg.p2.x - seg.p1.x)
const intercept = seg.p1.y - slope * seg.p1.x
const key = `${Math.round(slope / threshold)},${Math.round(intercept / threshold)}`
if (!diag.has(key)) diag.set(key, [])
diag.get(key)!.push(seg)
}
}

const result: { p1: Point; p2: Point; mspIds: Set<string>; pinIds: Set<string> }[] = []

const mergeInGroup = (group: typeof segments, axis: "x" | "y") => {
if (group.length === 0) return
// Sort by the variable axis
group.sort((a, b) => a.p1[axis] - b.p1[axis])

let current = {
p1: group[0].p1,
p2: group[0].p2,
mspIds: new Set(group[0].trace.mspConnectionPairIds),
pinIds: new Set(group[0].trace.pinIds),
}

for (let i = 1; i < group.length; i++) {
const seg = group[i]
if (seg.p1[axis] <= current.p2[axis] + threshold) {
// Overlap or touch
if (seg.p2[axis] > current.p2[axis]) {
current.p2 = seg.p2
}
// Merge metadata
for (const id of seg.trace.mspConnectionPairIds) current.mspIds.add(id)
for (const id of seg.trace.pinIds) current.pinIds.add(id)
} else {
result.push(current)
current = {
p1: seg.p1,
p2: seg.p2,
mspIds: new Set(seg.trace.mspConnectionPairIds),
pinIds: new Set(seg.trace.pinIds),
}
}
}
result.push(current)
}

horizontal.forEach((g) => mergeInGroup(g, "x"))
vertical.forEach((g) => mergeInGroup(g, "y"))
diag.forEach((g) => mergeInGroup(g, "x")) // Use x for diagonal merging

return result
}

private reconstructPaths(
segments: { p1: Point; p2: Point; mspIds: Set<string>; pinIds: Set<string> }[],
netId: string
): SolvedTracePath[] {
const threshold = 0.001
const pointToKey = (p: Point) =>
`${Math.round(p.x / threshold)},${Math.round(p.y / threshold)}`

const adj = new Map<string, { point: Point; edges: number[] }>()

for (let i = 0; i < segments.length; i++) {
const { p1, p2 } = segments[i]
const k1 = pointToKey(p1)
const k2 = pointToKey(p2)

if (!adj.has(k1)) adj.set(k1, { point: p1, edges: [] })
if (!adj.has(k2)) adj.set(k2, { point: p2, edges: [] })

adj.get(k1)!.edges.push(i)
adj.get(k2)!.edges.push(i)
}

const visitedEdges = new Set<number>()
const traces: SolvedTracePath[] = []

// Find paths starting from leaf nodes (degree 1) or junctions (degree > 2)
const points = Array.from(adj.keys())

// Helper to traverse and build a path
const buildPathFrom = (startKey: string) => {
const node = adj.get(startKey)!
for (const edgeIdx of node.edges) {
if (visitedEdges.has(edgeIdx)) continue

const path: Point[] = [node.point]
const mspIds = new Set<string>()
const pinIds = new Set<string>()

let currentKey = startKey
let currentEdgeIdx = edgeIdx

while (currentEdgeIdx !== -1) {
visitedEdges.add(currentEdgeIdx)
const seg = segments[currentEdgeIdx]
for (const id of seg.mspIds) mspIds.add(id)
for (const id of seg.pinIds) pinIds.add(id)

const k1 = pointToKey(seg.p1)
const k2 = pointToKey(seg.p2)
const nextKey = k1 === currentKey ? k2 : k1
const nextNode = adj.get(nextKey)!

path.push(nextNode.point)

// Continue if next node has degree 2 (simple path)
// Stop if it's a junction (degree > 2) or leaf (degree 1)
if (nextNode.edges.length === 2) {
const nextEdgeIdx = nextNode.edges.find((e) => !visitedEdges.has(e))
if (nextEdgeIdx !== undefined) {
currentKey = nextKey
currentEdgeIdx = nextEdgeIdx
continue
}
}
currentEdgeIdx = -1
}

traces.push({
mspPairId: `merged_${netId}_${traces.length}`,
globalConnNetId: netId,
dcConnNetId: netId,
tracePath: this.simplifyCollinearPoints(path),
mspConnectionPairIds: Array.from(mspIds),
pinIds: Array.from(pinIds),
pins: [], // Not populated by this solver
})
}
}

// 1. Start from leaves (degree 1)
for (const k of points) {
if (adj.get(k)!.edges.length === 1) buildPathFrom(k)
}
// 2. Start from junctions (degree > 2)
for (const k of points) {
if (adj.get(k)!.edges.length > 2) buildPathFrom(k)
}
// 3. Handle isolated loops (degree 2 nodes that weren't visited)
for (const k of points) {
if (adj.get(k)!.edges.length === 2) buildPathFrom(k)
}

return traces
}

private simplifyCollinearPoints(points: Point[]): Point[] {
if (points.length <= 2) return points

const simplified: Point[] = [points[0]]
const threshold = 0.001

for (let i = 1; i < points.length - 1; i++) {
const prev = simplified[simplified.length - 1]
const curr = points[i]
const next = points[i + 1]

const isCollinear = this.areCollinear(prev, curr, next, threshold)
if (!isCollinear) {
simplified.push(curr)
}
}

simplified.push(points[points.length - 1])
return simplified
}

private areCollinear(p1: Point, p2: Point, p3: Point, threshold: number): boolean {
// Area of triangle = 0.5 * |x1(y2-y3) + x2(y3-y1) + x3(y1-y2)|
// Using area / distance as a measure of collinearity
const area = Math.abs(
p1.x * (p2.y - p3.y) + p2.x * (p3.y - p1.y) + p3.x * (p1.y - p2.y)
)
return area < threshold * 2 // Increase threshold slightly for area calculation
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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 { SameNetTraceMergeSolver } from "../SameNetTraceMergeSolver/SameNetTraceMergeSolver"

type PipelineStep<T extends new (...args: any[]) => BaseSolver> = {
solverName: string
Expand Down Expand Up @@ -69,6 +70,7 @@ export class SchematicTracePipelineSolver extends BaseSolver {
labelMergingSolver?: MergedNetLabelObstacleSolver
traceLabelOverlapAvoidanceSolver?: TraceLabelOverlapAvoidanceSolver
traceCleanupSolver?: TraceCleanupSolver
sameNetTraceMergeSolver?: SameNetTraceMergeSolver

startTimeOfPhase: Record<string, number>
endTimeOfPhase: Record<string, number>
Expand Down Expand Up @@ -188,9 +190,21 @@ export class SchematicTracePipelineSolver extends BaseSolver {
]
},
),
definePipelineStep(
"sameNetTraceMergeSolver",
SameNetTraceMergeSolver,
(instance) => {
const prevSolverOutput =
instance.traceLabelOverlapAvoidanceSolver!.getOutput()
return [
{
traces: prevSolverOutput.traces,
},
]
},
),
definePipelineStep("traceCleanupSolver", TraceCleanupSolver, (instance) => {
const prevSolverOutput =
instance.traceLabelOverlapAvoidanceSolver!.getOutput()
const prevSolverOutput = instance.sameNetTraceMergeSolver!.getOutput()
const traces = prevSolverOutput.traces

const labelMergingOutput =
Expand Down
26 changes: 22 additions & 4 deletions lib/solvers/TraceCleanupSolver/TraceCleanupSolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import { BaseSolver } from "lib/solvers/BaseSolver/BaseSolver"
import type { SolvedTracePath } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceLinesSolver"
import { visualizeInputProblem } from "lib/solvers/SchematicTracePipelineSolver/visualizeInputProblem"
import type { NetLabelPlacement } from "../NetLabelPlacementSolver/NetLabelPlacementSolver"
import { mergeSameNetSegments } from "./mergeSameNetSegments"
import { simplifyPath } from "./simplifyPath"

/**
* Defines the input structure for the TraceCleanupSolver.
Expand All @@ -26,6 +28,7 @@ import { is4PointRectangle } from "./is4PointRectangle"
*/
type PipelineStep =
| "minimizing_turns"
| "merging_collinear_segments"
| "balancing_l_shapes"
| "untangling_traces"

Expand Down Expand Up @@ -81,6 +84,9 @@ export class TraceCleanupSolver extends BaseSolver {
case "minimizing_turns":
this._runMinimizeTurnsStep()
break
case "merging_collinear_segments":
this._runMergeCollinearSegmentsStep()
break
case "balancing_l_shapes":
this._runBalanceLShapesStep()
break
Expand All @@ -96,16 +102,28 @@ export class TraceCleanupSolver extends BaseSolver {

private _runMinimizeTurnsStep() {
if (this.traceIdQueue.length === 0) {
this.pipelineStep = "balancing_l_shapes"
this.traceIdQueue = Array.from(
this.input.allTraces.map((e) => e.mspPairId),
)
this.pipelineStep = "merging_collinear_segments"
return
}

this._processTrace("minimizing_turns")
}

private _runMergeCollinearSegmentsStep() {
this.outputTraces = mergeSameNetSegments(this.outputTraces)

// Simplify paths after merging to remove redundant points
for (const trace of this.outputTraces) {
trace.tracePath = simplifyPath(trace.tracePath)
}

this.tracesMap = new Map(this.outputTraces.map((t) => [t.mspPairId, t]))
this.pipelineStep = "balancing_l_shapes"
this.traceIdQueue = Array.from(
this.input.allTraces.map((e) => e.mspPairId),
)
}

private _runBalanceLShapesStep() {
if (this.traceIdQueue.length === 0) {
this.solved = true
Expand Down
Loading