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
16 changes: 14 additions & 2 deletions lib/solvers/TraceCleanupSolver/TraceCleanupSolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,21 +20,24 @@ interface TraceCleanupSolverInput {

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

/**
* Represents the different stages or steps within the trace cleanup pipeline.
*/
type PipelineStep =
| "minimizing_turns"
| "balancing_l_shapes"
| "merging_close_same_net_segments"
| "untangling_traces"
Comment on lines 28 to 32
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in dbcd4a3: updated the pipeline JSDoc with the new close same-net merge phase.


/**
* 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 Close Same-Net Segments**: Finally, it aligns nearby overlapping same-net segments onto shared coordinates.
* 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_close_same_net_segments":
this._runMergeCloseSameNetSegmentsStep()
break
}
}

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

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

this._processTrace("balancing_l_shapes")
}

private _runMergeCloseSameNetSegmentsStep() {
this.outputTraces = mergeNearbySameNetSegments(this.outputTraces)
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
Expand Down
160 changes: 160 additions & 0 deletions lib/solvers/TraceCleanupSolver/mergeNearbySameNetSegments.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
import type { Point } from "graphics-debug"
import type { SolvedTracePath } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceLinesSolver"
import { simplifyPath } from "./simplifyPath"

const DEFAULT_MERGE_DISTANCE = 0.18
const EPS = 1e-6

type Orientation = "horizontal" | "vertical"

type SegmentRef = {
traceIndex: number
startIndex: number
orientation: Orientation
fixedCoord: number
min: number
max: number
}

const getSegmentRef = (
trace: SolvedTracePath,
traceIndex: number,
startIndex: number,
): SegmentRef | null => {
const p1 = trace.tracePath[startIndex]!
const p2 = trace.tracePath[startIndex + 1]!

if (Math.abs(p1.y - p2.y) < EPS && Math.abs(p1.x - p2.x) > EPS) {
return {
traceIndex,
startIndex,
orientation: "horizontal",
fixedCoord: p1.y,
min: Math.min(p1.x, p2.x),
max: Math.max(p1.x, p2.x),
}
}

if (Math.abs(p1.x - p2.x) < EPS && Math.abs(p1.y - p2.y) > EPS) {
return {
traceIndex,
startIndex,
orientation: "vertical",
fixedCoord: p1.x,
min: Math.min(p1.y, p2.y),
max: Math.max(p1.y, p2.y),
}
}

return null
}

const getNetId = (trace: SolvedTracePath) =>
trace.userNetId ?? trace.globalConnNetId ?? trace.dcConnNetId

const rangesOverlap = (a: SegmentRef, b: SegmentRef) =>
Math.min(a.max, b.max) - Math.max(a.min, b.min) > EPS

const isInteriorSegment = (trace: SolvedTracePath, segmentStartIndex: number) =>
segmentStartIndex > 0 && segmentStartIndex + 1 < trace.tracePath.length - 1

const moveSegmentToFixedCoord = (
trace: SolvedTracePath,
segmentStartIndex: number,
orientation: Orientation,
fixedCoord: number,
) => {
const path = trace.tracePath.map((p) => ({ ...p }))
const p1 = path[segmentStartIndex]!
const p2 = path[segmentStartIndex + 1]!

if (orientation === "horizontal") {
path[segmentStartIndex] = { ...p1, y: fixedCoord }
path[segmentStartIndex + 1] = { ...p2, y: fixedCoord }
} else {
path[segmentStartIndex] = { ...p1, x: fixedCoord }
path[segmentStartIndex + 1] = { ...p2, x: fixedCoord }
}

return { ...trace, tracePath: simplifyPath(path as Point[]) }
}

export const mergeNearbySameNetSegments = (
traces: SolvedTracePath[],
mergeDistance = DEFAULT_MERGE_DISTANCE,
): SolvedTracePath[] => {
let output = traces.map((trace) => ({
...trace,
tracePath: trace.tracePath.map((p) => ({ ...p })),
}))

const segments: SegmentRef[] = []
for (let traceIndex = 0; traceIndex < output.length; traceIndex++) {
const trace = output[traceIndex]!
for (
let segmentIndex = 0;
segmentIndex < trace.tracePath.length - 1;
segmentIndex++
) {
if (!isInteriorSegment(trace, segmentIndex)) continue
const segment = getSegmentRef(trace, traceIndex, segmentIndex)
if (segment) segments.push(segment)
}
}

const visited = new Set<number>()

for (let startIndex = 0; startIndex < segments.length; startIndex++) {
if (visited.has(startIndex)) continue

const componentIndexes: number[] = []
const queue = [startIndex]
visited.add(startIndex)

while (queue.length > 0) {
const currentIndex = queue.shift()!
const current = segments[currentIndex]!
componentIndexes.push(currentIndex)

for (
let candidateIndex = 0;
candidateIndex < segments.length;
candidateIndex++
) {
if (visited.has(candidateIndex)) continue
const candidate = segments[candidateIndex]!
if (
getNetId(output[current.traceIndex]!) !==
getNetId(output[candidate.traceIndex]!)
)
continue
if (current.orientation !== candidate.orientation) continue
if (!rangesOverlap(current, candidate)) continue

const distance = Math.abs(current.fixedCoord - candidate.fixedCoord)
if (distance <= EPS || distance > mergeDistance) continue

visited.add(candidateIndex)
queue.push(candidateIndex)
}
}

if (componentIndexes.length < 2) continue

const component = componentIndexes.map((index) => segments[index]!)
const mergedCoord =
component.reduce((sum, segment) => sum + segment.fixedCoord, 0) /
component.length

for (const segment of component) {
output[segment.traceIndex] = moveSegmentToFixedCoord(
output[segment.traceIndex]!,
segment.startIndex,
segment.orientation,
mergedCoord,
)
}
}

return output
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import { expect, test } from "bun:test"
import { mergeNearbySameNetSegments } from "lib/solvers/TraceCleanupSolver/mergeNearbySameNetSegments"
import type { SolvedTracePath } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceLinesSolver"

const makeTrace = (
mspPairId: string,
globalConnNetId: string,
tracePath: Array<{ x: number; y: number }>,
): SolvedTracePath =>
({
mspPairId,
dcConnNetId: globalConnNetId,
globalConnNetId,
pins: [] as any,
mspConnectionPairIds: [mspPairId],
pinIds: [],
tracePath,
}) as any

test("mergeNearbySameNetSegments aligns close overlapping horizontal same-net segments", () => {
const [a, b] = mergeNearbySameNetSegments([
makeTrace("a", "net1", [
{ x: 0, y: 0 },
{ x: 0, y: 1 },
{ x: 3, y: 1 },
{ x: 3, y: 2 },
]),
makeTrace("b", "net1", [
{ x: 1, y: 0 },
{ x: 1, y: 1.1 },
{ x: 4, y: 1.1 },
{ x: 4, y: 2 },
]),
])

expect(a.tracePath[1]!.y).toBeCloseTo(1.05)
expect(a.tracePath[2]!.y).toBeCloseTo(1.05)
expect(b.tracePath[1]!.y).toBeCloseTo(1.05)
expect(b.tracePath[2]!.y).toBeCloseTo(1.05)
})

test("mergeNearbySameNetSegments does not align different-net segments", () => {
const [a, b] = mergeNearbySameNetSegments([
makeTrace("a", "net1", [
{ x: 0, y: 0 },
{ x: 0, y: 1 },
{ x: 3, y: 1 },
{ x: 3, y: 2 },
]),
makeTrace("b", "net2", [
{ x: 1, y: 0 },
{ x: 1, y: 1.1 },
{ x: 4, y: 1.1 },
{ x: 4, y: 2 },
]),
])

expect(a.tracePath[1]!.y).toBe(1)
expect(b.tracePath[1]!.y).toBe(1.1)
})

test("mergeNearbySameNetSegments aligns every close segment in a same-net group", () => {
const [a, b, c] = mergeNearbySameNetSegments([
makeTrace("a", "net1", [
{ x: 0, y: 0 },
{ x: 0, y: 1 },
{ x: 3, y: 1 },
{ x: 3, y: 2 },
]),
makeTrace("b", "net1", [
{ x: 1, y: 0 },
{ x: 1, y: 1.1 },
{ x: 4, y: 1.1 },
{ x: 4, y: 2 },
]),
makeTrace("c", "net1", [
{ x: 2, y: 0 },
{ x: 2, y: 1.2 },
{ x: 5, y: 1.2 },
{ x: 5, y: 2 },
]),
])

expect(a.tracePath[1]!.y).toBeCloseTo(1.1)
expect(a.tracePath[2]!.y).toBeCloseTo(1.1)
expect(b.tracePath[1]!.y).toBeCloseTo(1.1)
expect(b.tracePath[2]!.y).toBeCloseTo(1.1)
expect(c.tracePath[1]!.y).toBeCloseTo(1.1)
expect(c.tracePath[2]!.y).toBeCloseTo(1.1)
})
Loading