Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,7 @@ export class AvailableNetOrientationSolver extends BaseSolver {
return {
traces: this.traces,
netLabelPlacements: this.outputNetLabelPlacements,
netLabelConnectorTraceIds: this.netLabelConnectorTraceIds,
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -467,12 +467,17 @@ export class SchematicTracePipelineSolver extends BaseSolver {
allLabelPlacements: collisionOutput.netLabelPlacements,
mergedLabelNetIdMap: labelMergingOutput.mergedLabelNetIdMap,
paddingBuffer: 0.1,
operations: ["aligning_same_net_rails"],
operations: [
"rerouting_generated_net_label_connector_crossings",
"aligning_same_net_rails",
],
eligibleTraceIds: new Set(
instance
.traceCleanupSolver!.getOutput()
.traces.map((trace) => trace.mspPairId),
),
netLabelConnectorTraceIds:
instance.availableNetOrientationSolver!.netLabelConnectorTraceIds,
},
]
},
Expand Down
25 changes: 25 additions & 0 deletions lib/solvers/TraceCleanupSolver/TraceCleanupSolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { alignSameNetRails } from "./alignSameNetRails"

export type TraceCleanupOperation =
| "untangling_traces"
| "rerouting_generated_net_label_connector_crossings"
| "minimizing_turns"
| "balancing_l_shapes"
| "aligning_same_net_rails"
Expand All @@ -25,10 +26,12 @@ export interface TraceCleanupSolverInput {
paddingBuffer: number
operations?: readonly TraceCleanupOperation[]
eligibleTraceIds?: ReadonlySet<string>
netLabelConnectorTraceIds?: ReadonlySet<string>
}

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

/**
* Represents the different stages or steps within the trace cleanup pipeline.
Expand Down Expand Up @@ -97,6 +100,9 @@ export class TraceCleanupSolver extends BaseSolver {
case "untangling_traces":
this._runUntangleTracesStep()
break
case "rerouting_generated_net_label_connector_crossings":
this._runGeneratedNetLabelConnectorCrossingRerouteStep()
break
case "minimizing_turns":
this._runMinimizeTurnsStep()
break
Expand All @@ -123,6 +129,25 @@ export class TraceCleanupSolver extends BaseSolver {
})
}

private _runGeneratedNetLabelConnectorCrossingRerouteStep() {
const result = rerouteGeneratedNetLabelConnectorCrossings({
inputProblem: this.input.inputProblem,
traces: this.outputTraces,
netLabelPlacements: this.input.allLabelPlacements,
mergedLabelNetIdMap: this.input.mergedLabelNetIdMap,
clearance: this.input.paddingBuffer,
eligibleTraceIds: this.input.eligibleTraceIds,
connectorTraceIds: this.input.netLabelConnectorTraceIds ?? new Set(),
})
this.outputTraces = result.traces
this.tracesMap = new Map(
this.outputTraces.map((trace) => [trace.mspPairId, trace]),
)
this.stats.reroutedGeneratedConnectorCrossingTraceCount =
result.reroutedTraceCount
this._advancePipeline()
}

private _runMinimizeTurnsStep() {
if (this.traceIdQueue.length === 0) {
this._advancePipeline()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
import type { Bounds } from "@tscircuit/math-utils"
import { getPathLength } from "lib/solvers/Example28Solver/geometry"
import type { NetLabelPlacement } from "lib/solvers/NetLabelPlacementSolver/NetLabelPlacementSolver"
import { getRectBounds } from "lib/solvers/NetLabelPlacementSolver/SingleNetLabelPlacementSolver/geometry"
import { tracePathContainsPoint } from "lib/solvers/RailNetLabelCornerPlacementSolver/geometry"
import type { SolvedTracePath } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceLinesSolver"
import { isPathCollidingWithObstacles } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver2/collisions"
import { getObstacleRects } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver2/rect"
import type { InputProblem } from "lib/types/InputProblem"
import { doesPathCoincideWithTraces } from "lib/utils/doesPathCoincideWithTraces"
import { countTurns } from "./countTurns"
import { hasCollisionsWithLabels } from "./hasCollisionsWithLabels"
import { findPerpendicularPathCrossings } from "./sub-solver/findIntersectionsWithObstacles"
import { generatePerpendicularTraceDetours } from "./sub-solver/generateLShapeRerouteCandidates"
import { isPathColliding } from "./sub-solver/isPathColliding"

const EPS = 1e-6

const getConnectorObstacleBounds = (
connector: SolvedTracePath,
segmentIndex: number,
labels: NetLabelPlacement[],
): Bounds => {
const start = connector.tracePath[segmentIndex]!
const end = connector.tracePath[segmentIndex + 1]!
const bounds = {
minX: Math.min(start.x, end.x),
minY: Math.min(start.y, end.y),
maxX: Math.max(start.x, end.x),
maxY: Math.max(start.y, end.y),
}

for (const label of labels) {
if (label.globalConnNetId !== connector.globalConnNetId) continue
if (!label.pinIds.every((pinId) => connector.pinIds.includes(pinId)))
continue
if (!tracePathContainsPoint(connector.tracePath, label.anchorPoint))
continue

const labelBounds = getRectBounds(label.center, label.width, label.height)
bounds.minX = Math.min(bounds.minX, labelBounds.minX)
bounds.minY = Math.min(bounds.minY, labelBounds.minY)
bounds.maxX = Math.max(bounds.maxX, labelBounds.maxX)
bounds.maxY = Math.max(bounds.maxY, labelBounds.maxY)
}

return bounds
}

const getEligibleConnectorCrossings = (
traces: SolvedTracePath[],
connectorTraceIds: ReadonlySet<string>,
eligibleTraceIds?: ReadonlySet<string>,
) => {
const crossings: Array<{
traceIndex: number
connector: SolvedTracePath
connectorSegmentIndex: number
traceSegmentIndex: number
}> = []

for (const connector of traces) {
if (!connectorTraceIds.has(connector.mspPairId)) continue

for (let traceIndex = 0; traceIndex < traces.length; traceIndex++) {
const trace = traces[traceIndex]!
if (trace.mspPairId === connector.mspPairId) continue
if (connectorTraceIds.has(trace.mspPairId)) continue
if (trace.globalConnNetId === connector.globalConnNetId) continue
if (eligibleTraceIds && !eligibleTraceIds.has(trace.mspPairId)) continue

for (const crossing of findPerpendicularPathCrossings(
trace.tracePath,
connector.tracePath,
{ includeTerminalSegments: true },
)) {
crossings.push({
traceIndex,
connector,
connectorSegmentIndex: crossing.otherPathSegmentIndex,
traceSegmentIndex: crossing.pathSegmentIndex,
})
}
}
}

return crossings
}

// Generated label connectors are added after the component traces are routed.
// Reuse the normal perpendicular-detour candidates here, keeping connectors
// fixed and considering only traces that came from the original routing pass.
export const rerouteGeneratedNetLabelConnectorCrossings = ({
inputProblem,
traces,
netLabelPlacements,
mergedLabelNetIdMap,
clearance,
eligibleTraceIds,
connectorTraceIds,
}: {
inputProblem: InputProblem
traces: SolvedTracePath[]
netLabelPlacements: NetLabelPlacement[]
mergedLabelNetIdMap: Record<string, Set<string>>
clearance: number
eligibleTraceIds?: ReadonlySet<string>
connectorTraceIds: ReadonlySet<string>
}) => {
const outputTraces = [...traces]
const componentAndTextObstacles = getObstacleRects(inputProblem).filter(
(obstacle) => obstacle.kind === "chip" || obstacle.kind === "text_box",
)
let reroutedTraceCount = 0

while (true) {
const candidates = getEligibleConnectorCrossings(
outputTraces,
connectorTraceIds,
eligibleTraceIds,
).flatMap((crossing) => {
const trace = outputTraces[crossing.traceIndex]!
const obstacleBounds = getConnectorObstacleBounds(
crossing.connector,
crossing.connectorSegmentIndex,
netLabelPlacements,
)
const foreignTraces = outputTraces.filter(
(otherTrace) =>
otherTrace.mspPairId !== trace.mspPairId &&
otherTrace.globalConnNetId !== trace.globalConnNetId,
)
const foreignLabelBounds = netLabelPlacements
.filter((label) => {
const mergedNetIds = mergedLabelNetIdMap[label.globalConnNetId]
return mergedNetIds
? !mergedNetIds.has(trace.globalConnNetId)
: label.globalConnNetId !== trace.globalConnNetId
})
.map((label) => getRectBounds(label.center, label.width, label.height))

return generatePerpendicularTraceDetours({
trace,
segmentIndex: crossing.traceSegmentIndex,
obstacleStart:
crossing.connector.tracePath[crossing.connectorSegmentIndex]!,
obstacleEnd:
crossing.connector.tracePath[crossing.connectorSegmentIndex + 1]!,
obstacleBounds,
chipBounds: [],
clearance,
})
.filter(
(candidate) =>
getPathLength(candidate.path) <=
getPathLength(trace.tracePath) + EPS &&
countTurns(candidate.path) <= countTurns(trace.tracePath) + 2 &&
!isPathCollidingWithObstacles(
candidate.path,
componentAndTextObstacles,
) &&
!hasCollisionsWithLabels(candidate.path, foreignLabelBounds) &&
!isPathColliding(candidate.path, foreignTraces, trace.mspPairId)
.isColliding &&
!doesPathCoincideWithTraces(candidate.path, foreignTraces),
)
.map((candidate) => ({
...candidate,
traceIndex: crossing.traceIndex,
}))
})

candidates.sort((first, second) => {
const lengthDifference =
getPathLength(first.path) - getPathLength(second.path)
return Math.abs(lengthDifference) > EPS
? lengthDifference
: countTurns(first.path) - countTurns(second.path)
})
const bestCandidate = candidates[0]
if (!bestCandidate) break

outputTraces[bestCandidate.traceIndex] = {
...outputTraces[bestCandidate.traceIndex]!,
tracePath: bestCandidate.path,
}
reroutedTraceCount++
}

return {
traces: outputTraces,
reroutedTraceCount,
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ export interface PerpendicularPathCrossing {
otherPathSegmentIndex: number
}

interface FindPerpendicularPathCrossingsOptions {
includeTerminalSegments?: boolean
}

/**
* Finds all intersection points between a given line segment (p1-p2) and a list of trace obstacles.
* It iterates through each segment of every obstacle and checks for intersections with the input segment.
Expand Down Expand Up @@ -48,23 +52,33 @@ const isSamePoint = (first: Point, second: Point) =>
export const findPerpendicularPathCrossings = (
path: Point[],
otherPath: Point[],
options: FindPerpendicularPathCrossingsOptions = {},
): PerpendicularPathCrossing[] => {
const crossings: PerpendicularPathCrossing[] = []
const firstPathSegmentIndex = options.includeTerminalSegments ? 0 : 1
const lastPathSegmentIndex = options.includeTerminalSegments
? path.length - 1
: path.length - 2
const firstOtherPathSegmentIndex = options.includeTerminalSegments ? 0 : 1
const lastOtherPathSegmentIndex = options.includeTerminalSegments
? otherPath.length - 1
: otherPath.length - 2

// Terminal segments connect to pins and are allowed to meet other traces at
// their endpoints. Only internal, strict crossings need to be untangled.
// their endpoints. Callers may include them when looking for strict
// crossings through a segment's interior after all connector traces exist.
for (
let pathSegmentIndex = 1;
pathSegmentIndex < path.length - 2;
let pathSegmentIndex = firstPathSegmentIndex;
pathSegmentIndex < lastPathSegmentIndex;
pathSegmentIndex++
) {
const start = path[pathSegmentIndex]!
const end = path[pathSegmentIndex + 1]!
const isVertical = Math.abs(start.x - end.x) < EPS

for (
let otherPathSegmentIndex = 1;
otherPathSegmentIndex < otherPath.length - 2;
let otherPathSegmentIndex = firstOtherPathSegmentIndex;
otherPathSegmentIndex < lastOtherPathSegmentIndex;
otherPathSegmentIndex++
) {
const otherStart = otherPath[otherPathSegmentIndex]!
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@ export interface PerpendicularTraceDetourInput {
segmentIndex: number
obstacleStart: Point
obstacleEnd: Point
obstacleBounds?: Bounds
chipBounds: Bounds[]
clearance: number
}
Expand Down Expand Up @@ -179,6 +180,7 @@ export const generatePerpendicularTraceDetours = ({
segmentIndex,
obstacleStart,
obstacleEnd,
obstacleBounds,
chipBounds,
clearance,
}: PerpendicularTraceDetourInput): TraceDetourCandidate[] => {
Expand All @@ -187,20 +189,48 @@ export const generatePerpendicularTraceDetours = ({
const end = path[index + 1]!
const movingAxis: "x" | "y" = Math.abs(start.x - end.x) < EPS ? "y" : "x"
const detourAxis = movingAxis === "x" ? "y" : "x"
const gate =
const bounds = obstacleBounds ?? {
minX: Math.min(obstacleStart.x, obstacleEnd.x),
minY: Math.min(obstacleStart.y, obstacleEnd.y),
maxX: Math.max(obstacleStart.x, obstacleEnd.x),
maxY: Math.max(obstacleStart.y, obstacleEnd.y),
}
const movingLowBound = movingAxis === "x" ? "minX" : "minY"
const movingHighBound = movingAxis === "x" ? "maxX" : "maxY"
let gate =
obstacleStart[movingAxis] +
Math.sign(start[movingAxis] - obstacleStart[movingAxis]) * clearance
const obstacleRange = [obstacleStart[detourAxis], obstacleEnd[detourAxis]]
if (obstacleBounds) {
const startSide =
start[movingAxis] <
(bounds[movingLowBound] + bounds[movingHighBound]) / 2
? -1
: 1
gate =
bounds[startSide < 0 ? movingLowBound : movingHighBound] +
startSide * clearance
}
const lowBound = detourAxis === "x" ? "minX" : "minY"
const highBound = detourAxis === "x" ? "maxX" : "maxY"
const minDetour = bounds[lowBound] - clearance
const maxDetour = bounds[highBound] + clearance
const nextAnchor = obstacleBounds ? path[index + 2] : undefined
const balancedDetour = nextAnchor
? nextAnchor[detourAxis] < minDetour
? (nextAnchor[detourAxis] + minDetour) / 2
: nextAnchor[detourAxis] > maxDetour
? (nextAnchor[detourAxis] + maxDetour) / 2
: undefined
: undefined
const detourCoordinates = [
Math.min(...obstacleRange) - clearance,
Math.max(...obstacleRange) + clearance,
balancedDetour,
minDetour,
maxDetour,
...chipBounds.flatMap((bounds) => [
bounds[lowBound] - clearance,
bounds[highBound] + clearance,
]),
]
].filter((coordinate): coordinate is number => coordinate !== undefined)

return [...new Set(detourCoordinates)].map((detour) =>
simplifyPath([
Expand Down
Loading
Loading