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
23 changes: 21 additions & 2 deletions lib/solvers/TraceCleanupSolver/TraceCleanupSolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type { InputProblem } from "lib/types/InputProblem"
import type { GraphicsObject, Line } from "graphics-debug"
import { minimizeTurnsWithFilteredLabels } from "./minimizeTurnsWithFilteredLabels"
import { balanceZShapes } from "./balanceZShapes"
import { mergeSameNetLines } from "./mergeSameNetLines"
import { BaseSolver } from "lib/solvers/BaseSolver/BaseSolver"
import type { SolvedTracePath } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceLinesSolver"
import { visualizeInputProblem } from "lib/solvers/SchematicTracePipelineSolver/visualizeInputProblem"
Expand All @@ -27,14 +28,17 @@ import { is4PointRectangle } from "./is4PointRectangle"
type PipelineStep =
| "minimizing_turns"
| "balancing_l_shapes"
| "merging_same_net_lines"
| "untangling_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 Same-Net Lines**: Finally, it merges same-net trace lines that are close together by aligning
* them to the same Y (for horizontal segments) or same X (for vertical 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 +88,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 +115,25 @@ 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() {
const mergedTraces = mergeSameNetLines({
traces: this.outputTraces,
inputProblem: this.input.inputProblem,
allLabelPlacements: this.input.allLabelPlacements,
mergedLabelNetIdMap: this.input.mergedLabelNetIdMap,
})
this.outputTraces = mergedTraces
this.tracesMap = new Map(mergedTraces.map((t) => [t.mspPairId, t]))
this.solved = true
}

private _processTrace(step: "minimizing_turns" | "balancing_l_shapes") {
const targetMspConnectionPairId = this.traceIdQueue.shift()!
this.activeTraceId = targetMspConnectionPairId
Expand Down
214 changes: 214 additions & 0 deletions lib/solvers/TraceCleanupSolver/mergeSameNetLines.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,214 @@
import type { Point } from "graphics-debug"
import type { InputProblem } from "lib/types/InputProblem"
import type { SolvedTracePath } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceLinesSolver"
import type { NetLabelPlacement } from "../NetLabelPlacementSolver/NetLabelPlacementSolver"
import { getObstacleRects } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver2/rect"
import { segmentIntersectsRect } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver2/collisions"
import { simplifyPath } from "./simplifyPath"

const MERGE_THRESHOLD = 0.15

interface Segment {
traceId: string
segIndex: number
orientation: "horizontal" | "vertical"
coordinate: number
}

/**
* Merges same-net trace lines that are close together by aligning them
* to the same Y (for horizontal segments) or same X (for vertical segments).
*
* When two traces belong to the same net and have parallel segments with
* close coordinates, they are adjusted to share the same coordinate value
* (the average), improving visual clarity.
*/
export const mergeSameNetLines = ({
traces,
inputProblem,
allLabelPlacements,
mergedLabelNetIdMap,
}: {
traces: SolvedTracePath[]
inputProblem: InputProblem
allLabelPlacements: NetLabelPlacement[]
mergedLabelNetIdMap: Record<string, Set<string>>
}): SolvedTracePath[] => {
const tracesByNet = new Map<string, SolvedTracePath[]>()
for (const trace of traces) {
const netId = trace.dcConnNetId
if (!tracesByNet.has(netId)) {
tracesByNet.set(netId, [])
}
tracesByNet.get(netId)!.push(trace)
}

const TOLERANCE = 1e-5
const staticObstacles = getObstacleRects(inputProblem).map((obs) => ({
...obs,
minX: obs.minX + TOLERANCE,
maxX: obs.maxX - TOLERANCE,
minY: obs.minY + TOLERANCE,
maxY: obs.maxY - TOLERANCE,
}))

const labelBounds = allLabelPlacements.map((nl) => ({
chipId: `label-${nl.globalConnNetId}`,
minX: nl.center.x - nl.width / 2 + TOLERANCE,
maxX: nl.center.x + nl.width / 2 - TOLERANCE,
minY: nl.center.y - nl.height / 2 + TOLERANCE,
maxY: nl.center.y + nl.height / 2 - TOLERANCE,
}))

const updatedTraces = traces.map((t) => ({
...t,
tracePath: t.tracePath.map((p) => ({ ...p })),
}))
const traceMap = new Map(updatedTraces.map((t) => [t.mspPairId, t]))
const modifiedTraceIds = new Set<string>()

for (const [_netId, netTraces] of tracesByNet) {
if (netTraces.length < 2) continue

const segments: Segment[] = []
for (const trace of netTraces) {
const path = traceMap.get(trace.mspPairId)!.tracePath
for (let i = 0; i < path.length - 1; i++) {
const p1 = path[i]
const p2 = path[i + 1]
const isHoriz = Math.abs(p1.y - p2.y) < TOLERANCE
const isVert = Math.abs(p1.x - p2.x) < TOLERANCE

if (isHoriz) {
segments.push({
traceId: trace.mspPairId,
segIndex: i,
orientation: "horizontal",
coordinate: p1.y,
})
} else if (isVert) {
segments.push({
traceId: trace.mspPairId,
segIndex: i,
orientation: "vertical",
coordinate: p1.x,
})
}
}
}

for (const orientation of ["horizontal", "vertical"] as const) {
const orientedSegments = segments.filter(
(s) => s.orientation === orientation,
)

orientedSegments.sort((a, b) => a.coordinate - b.coordinate)

const merged = new Set<number>()
for (let i = 0; i < orientedSegments.length; i++) {
if (merged.has(i)) continue

const cluster: number[] = [i]
const traceIdsInCluster = new Set([orientedSegments[i].traceId])

for (let j = i + 1; j < orientedSegments.length; j++) {
if (merged.has(j)) continue
const diff = Math.abs(
orientedSegments[j].coordinate - orientedSegments[i].coordinate,
)
if (diff > MERGE_THRESHOLD) break

if (traceIdsInCluster.has(orientedSegments[j].traceId)) continue

cluster.push(j)
traceIdsInCluster.add(orientedSegments[j].traceId)
}

if (cluster.length < 2) continue

const uniqueTraceIds = new Set(
cluster.map((idx) => orientedSegments[idx].traceId),
)
if (uniqueTraceIds.size < 2) continue

const avgCoord =
cluster.reduce(
(sum, idx) => sum + orientedSegments[idx].coordinate,
0,
) / cluster.length

let canMerge = true
for (const idx of cluster) {
const seg = orientedSegments[idx]
const trace = traceMap.get(seg.traceId)!
const path = trace.tracePath

// Skip endpoint segments (first and last)
if (seg.segIndex === 0 || seg.segIndex === path.length - 2) {
canMerge = false
break
}

// Create test path with the merged coordinate
const testPath = path.map((p) => ({ ...p }))

if (orientation === "horizontal") {
testPath[seg.segIndex].y = avgCoord
testPath[seg.segIndex + 1].y = avgCoord
} else {
testPath[seg.segIndex].x = avgCoord
testPath[seg.segIndex + 1].x = avgCoord
}

// Check collisions with obstacles
for (let k = 0; k < testPath.length - 1; k++) {
for (const obs of staticObstacles) {
if (segmentIntersectsRect(testPath[k], testPath[k + 1], obs)) {
canMerge = false
break
}
}
if (!canMerge) break
for (const lb of labelBounds) {
if (segmentIntersectsRect(testPath[k], testPath[k + 1], lb)) {
canMerge = false
break
}
}
if (!canMerge) break
}
if (!canMerge) break
}

if (!canMerge) continue

// Apply the merge
for (const idx of cluster) {
const seg = orientedSegments[idx]
const trace = traceMap.get(seg.traceId)!
const path = trace.tracePath

if (orientation === "horizontal") {
path[seg.segIndex].y = avgCoord
path[seg.segIndex + 1].y = avgCoord
} else {
path[seg.segIndex].x = avgCoord
path[seg.segIndex + 1].x = avgCoord
}

modifiedTraceIds.add(seg.traceId)
merged.add(idx)
}
}
}
}

// Only simplify modified traces to avoid unintended changes
for (const trace of updatedTraces) {
if (modifiedTraceIds.has(trace.mspPairId)) {
trace.tracePath = simplifyPath(trace.tracePath)
}
}

return updatedTraces
}
10 changes: 5 additions & 5 deletions tests/examples/__snapshots__/example02.snap.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
8 changes: 4 additions & 4 deletions tests/examples/__snapshots__/example18.snap.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading