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
33 changes: 30 additions & 3 deletions lib/solvers/TraceCleanupSolver/TraceCleanupSolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,17 @@ interface TraceCleanupSolverInput {

import { UntangleTraceSubsolver } from "./sub-solver/UntangleTraceSubsolver"
import { is4PointRectangle } from "./is4PointRectangle"
import {
findCloseSameNetTraceGroups,
type CloseSameNetTraceGroup,
} from "./sub-solver/findCloseSameNetTraceGroups"
import { combineCloseSameNetTraceGroups } from "./sub-solver/combineCloseSameNetTraceGroups"

/**
* Represents the different stages or steps within the trace cleanup pipeline.
*/
type PipelineStep =
| "combining_same_net_traces"
| "minimizing_turns"
| "balancing_l_shapes"
| "untangling_traces"
Expand All @@ -42,6 +48,7 @@ export class TraceCleanupSolver extends BaseSolver {
private outputTraces: SolvedTracePath[]
private traceIdQueue: string[]
private tracesMap: Map<string, SolvedTracePath>
private closeSameNetTraceGroups: CloseSameNetTraceGroup[] = []
private pipelineStep: PipelineStep = "untangling_traces"
private activeTraceId: string | null = null // New property
override activeSubSolver: BaseSolver | null = null
Expand All @@ -54,6 +61,9 @@ export class TraceCleanupSolver extends BaseSolver {
this.traceIdQueue = Array.from(
solverInput.allTraces.map((e) => e.mspPairId),
)
this.closeSameNetTraceGroups = findCloseSameNetTraceGroups(
solverInput.allTraces,
)
}

override _step() {
Expand All @@ -66,10 +76,10 @@ export class TraceCleanupSolver extends BaseSolver {
this.outputTraces = output.traces
this.tracesMap = new Map(this.outputTraces.map((t) => [t.mspPairId, t]))
this.activeSubSolver = null
this.pipelineStep = "minimizing_turns"
this.pipelineStep = "combining_same_net_traces"
} else if (this.activeSubSolver.failed) {
this.activeSubSolver = null
this.pipelineStep = "minimizing_turns"
this.pipelineStep = "combining_same_net_traces"
}
return
}
Expand All @@ -78,6 +88,9 @@ export class TraceCleanupSolver extends BaseSolver {
case "untangling_traces":
this._runUntangleTracesStep()
break
case "combining_same_net_traces":
this._runCombineSameNetTracesStep()
break
case "minimizing_turns":
this._runMinimizeTurnsStep()
break
Expand All @@ -94,6 +107,18 @@ export class TraceCleanupSolver extends BaseSolver {
})
}

private _runCombineSameNetTracesStep() {
this.outputTraces = combineCloseSameNetTraceGroups(
Array.from(this.tracesMap.values()),
)
this.tracesMap = new Map(this.outputTraces.map((t) => [t.mspPairId, t]))
this.traceIdQueue = Array.from(this.outputTraces.map((e) => e.mspPairId))
this.closeSameNetTraceGroups = findCloseSameNetTraceGroups(
this.outputTraces,
)
this.pipelineStep = "minimizing_turns"
}

private _runMinimizeTurnsStep() {
if (this.traceIdQueue.length === 0) {
this.pipelineStep = "balancing_l_shapes"
Expand Down Expand Up @@ -149,6 +174,7 @@ export class TraceCleanupSolver extends BaseSolver {
getOutput() {
return {
traces: this.outputTraces,
closeSameNetTraceGroups: this.closeSameNetTraceGroups,
}
}

Expand All @@ -171,10 +197,11 @@ export class TraceCleanupSolver extends BaseSolver {
for (const trace of this.outputTraces) {
const line: Line = {
points: trace.tracePath.map((p) => ({ x: p.x, y: p.y })),
strokeColor: trace.mspPairId === this.activeTraceId ? "red" : "blue", // Highlight active trace
strokeColor: trace.mspPairId === this.activeTraceId ? "red" : "blue",
}
graphics.lines!.push(line)
}

return graphics
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import type { Point } from "@tscircuit/math-utils"
import type { SolvedTracePath } from "../../SchematicTraceLinesSolver/SchematicTraceLinesSolver"
import { findCloseSameNetTraceGroups } from "./findCloseSameNetTraceGroups"

const pointsEqual = (a: Point, b: Point) => a.x === b.x && a.y === b.y

const distance = (a: Point, b: Point) => Math.hypot(a.x - b.x, a.y - b.y)

const getEndpointJoin = (left: SolvedTracePath, right: SolvedTracePath) => {
const leftStart = left.tracePath[0]
const leftEnd = left.tracePath[left.tracePath.length - 1]
const rightStart = right.tracePath[0]
const rightEnd = right.tracePath[right.tracePath.length - 1]

const candidates = [
{
distance: distance(leftEnd, rightStart),
path: [...left.tracePath, ...right.tracePath],
},
{
distance: distance(leftEnd, rightEnd),
path: [...left.tracePath, ...right.tracePath.toReversed()],
},
{
distance: distance(leftStart, rightEnd),
path: [...right.tracePath, ...left.tracePath],
},
{
distance: distance(leftStart, rightStart),
path: [...left.tracePath.toReversed(), ...right.tracePath],
},
]

return candidates.sort((a, b) => a.distance - b.distance)[0]
}

const dedupeConsecutivePoints = (points: Point[]) => {
const deduped: Point[] = []
for (const point of points) {
const previous = deduped[deduped.length - 1]
if (!previous || !pointsEqual(previous, point)) {
deduped.push(point)
}
}
return deduped
}

/**
* Combines close same-net trace segments into a single solved trace path.
*
* The join is intentionally conservative: it only joins the closest endpoint
* pair already discovered by `findCloseSameNetTraceGroups`, then carries over
* all source connection-pair ids and pin ids so downstream consumers still know
* which original connections are represented by the merged path.
*/
export const combineCloseSameNetTraceGroups = (
traces: SolvedTracePath[],
maxEndpointDistance = 0.5,
): SolvedTracePath[] => {
const tracesMap = new Map(traces.map((trace) => [trace.mspPairId, trace]))
const consumedTraceIds = new Set<string>()

for (const group of findCloseSameNetTraceGroups(
traces,
maxEndpointDistance,
)) {
const [leftId, rightId] = group.traceIds
if (consumedTraceIds.has(leftId) || consumedTraceIds.has(rightId)) continue

const left = tracesMap.get(leftId)
const right = tracesMap.get(rightId)
if (!left || !right) continue

const endpointJoin = getEndpointJoin(left, right)
if (endpointJoin.distance > maxEndpointDistance) continue

tracesMap.set(leftId, {
...left,
tracePath: dedupeConsecutivePoints(endpointJoin.path),
mspConnectionPairIds: [
...left.mspConnectionPairIds,
...right.mspConnectionPairIds,
],
pinIds: [...left.pinIds, ...right.pinIds],
})
tracesMap.delete(rightId)
consumedTraceIds.add(leftId)
consumedTraceIds.add(rightId)
}

return [...tracesMap.values()]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import type { SolvedTracePath } from "../../SchematicTraceLinesSolver/SchematicTraceLinesSolver"

export interface CloseSameNetTraceGroup {
netId: string
traceIds: string[]
maxEndpointDistance: number
}

const distance = (a: { x: number; y: number }, b: { x: number; y: number }) =>
Math.hypot(a.x - b.x, a.y - b.y)

const getTraceEndpoints = (trace: SolvedTracePath) => {
const start = trace.tracePath[0]
const end = trace.tracePath[trace.tracePath.length - 1]
return { start, end }
}

/**
* Finds same-net traces whose endpoints are already close enough that they are
* likely candidates for a later merge/join phase.
*
* This is intentionally conservative: it does not mutate paths, it only groups
* traces so the pipeline can decide whether to combine them.
*/
export const findCloseSameNetTraceGroups = (
traces: SolvedTracePath[],
maxEndpointDistance = 0.5,
): CloseSameNetTraceGroup[] => {
const groupedByNet = new Map<string, SolvedTracePath[]>()

for (const trace of traces) {
const netId = trace.userNetId ?? trace.globalConnNetId ?? trace.dcConnNetId
const current = groupedByNet.get(netId) ?? []
current.push(trace)
groupedByNet.set(netId, current)
}

const groups: CloseSameNetTraceGroup[] = []

for (const [netId, netTraces] of groupedByNet.entries()) {
if (netTraces.length < 2) continue

for (let i = 0; i < netTraces.length; i++) {
for (let j = i + 1; j < netTraces.length; j++) {
const left = netTraces[i]
const right = netTraces[j]
const a = getTraceEndpoints(left)
const b = getTraceEndpoints(right)

const distances = [
distance(a.start, b.start),
distance(a.start, b.end),
distance(a.end, b.start),
distance(a.end, b.end),
]
const minDistance = Math.min(...distances)

if (minDistance <= maxEndpointDistance) {
groups.push({
netId,
traceIds: [left.mspPairId as string, right.mspPairId as string],
maxEndpointDistance: minDistance,
})
}
}
}
}

return groups.sort((a, b) => a.maxEndpointDistance - b.maxEndpointDistance)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import { expect, test } from "bun:test"
import { combineCloseSameNetTraceGroups } from "lib/solvers/TraceCleanupSolver/sub-solver/combineCloseSameNetTraceGroups"

const trace = (
id: string,
netId: string,
path: Array<{ x: number; y: number }>,
pinIds = [`${id}-pin-a`, `${id}-pin-b`],
) =>
({
mspPairId: id,
mspConnectionPairIds: [id],
pinIds,
userNetId: netId,
globalConnNetId: `${netId}-global`,
dcConnNetId: `${netId}-dc`,
tracePath: path,
}) as any

test("combineCloseSameNetTraceGroups merges close same-net traces", () => {
const combined = combineCloseSameNetTraceGroups(
[
trace("a", "N1", [
{ x: 0, y: 0 },
{ x: 1, y: 0 },
]),
trace("b", "N1", [
{ x: 1.1, y: 0 },
{ x: 2, y: 0 },
]),
trace("c", "N2", [
{ x: 10, y: 10 },
{ x: 11, y: 10 },
]),
],
0.2,
)

expect(combined).toHaveLength(2)
const merged = combined.find((candidate) => candidate.mspPairId === "a")!
expect(merged.mspConnectionPairIds).toEqual(["a", "b"])
expect(merged.pinIds).toEqual(["a-pin-a", "a-pin-b", "b-pin-a", "b-pin-b"])
expect(merged.tracePath).toEqual([
{ x: 0, y: 0 },
{ x: 1, y: 0 },
{ x: 1.1, y: 0 },
{ x: 2, y: 0 },
])
})

test("combineCloseSameNetTraceGroups orients reversed traces before merging", () => {
const combined = combineCloseSameNetTraceGroups(
[
trace("a", "N1", [
{ x: 0, y: 0 },
{ x: 1, y: 0 },
]),
trace("b", "N1", [
{ x: 2, y: 0 },
{ x: 1, y: 0 },
]),
],
0.01,
)

expect(combined).toHaveLength(1)
expect(combined[0].tracePath).toEqual([
{ x: 0, y: 0 },
{ x: 1, y: 0 },
{ x: 2, y: 0 },
])
})

test("combineCloseSameNetTraceGroups leaves different nets separate", () => {
const combined = combineCloseSameNetTraceGroups(
[
trace("a", "N1", [
{ x: 0, y: 0 },
{ x: 1, y: 0 },
]),
trace("b", "N2", [
{ x: 1, y: 0 },
{ x: 2, y: 0 },
]),
],
0.01,
)

expect(combined.map((candidate) => candidate.mspPairId)).toEqual(["a", "b"])
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { expect, test } from "bun:test"
import { findCloseSameNetTraceGroups } from "lib/solvers/TraceCleanupSolver/sub-solver/findCloseSameNetTraceGroups"

const trace = (
id: string,
netId: string,
path: Array<{ x: number; y: number }>,
) =>
({
mspPairId: id,
userNetId: netId,
globalConnNetId: `${netId}-global`,
dcConnNetId: `${netId}-dc`,
tracePath: path,
}) as any

test("findCloseSameNetTraceGroups groups close same-net traces by endpoint distance", () => {
const groups = findCloseSameNetTraceGroups(
[
trace("a", "N1", [
{ x: 0, y: 0 },
{ x: 1, y: 0 },
]),
trace("b", "N1", [
{ x: 1.15, y: 0 },
{ x: 2, y: 0 },
]),
trace("c", "N2", [
{ x: 10, y: 10 },
{ x: 11, y: 10 },
]),
],
0.2,
)

expect(groups).toHaveLength(1)
expect(groups[0].netId).toBe("N1")
expect(groups[0].traceIds).toEqual(["a", "b"])
expect(groups[0].maxEndpointDistance).toBeLessThanOrEqual(0.2)
})