diff --git a/lib/solvers/AvailableNetOrientationSolver/AvailableNetOrientationSolver.ts b/lib/solvers/AvailableNetOrientationSolver/AvailableNetOrientationSolver.ts index c07f90d20..1032d37c8 100644 --- a/lib/solvers/AvailableNetOrientationSolver/AvailableNetOrientationSolver.ts +++ b/lib/solvers/AvailableNetOrientationSolver/AvailableNetOrientationSolver.ts @@ -127,6 +127,7 @@ export class AvailableNetOrientationSolver extends BaseSolver { return { traces: this.traces, netLabelPlacements: this.outputNetLabelPlacements, + netLabelConnectorTraceIds: this.netLabelConnectorTraceIds, } } diff --git a/lib/solvers/SchematicTracePipelineSolver/SchematicTracePipelineSolver.ts b/lib/solvers/SchematicTracePipelineSolver/SchematicTracePipelineSolver.ts index 435fad6f7..1a8aa4246 100644 --- a/lib/solvers/SchematicTracePipelineSolver/SchematicTracePipelineSolver.ts +++ b/lib/solvers/SchematicTracePipelineSolver/SchematicTracePipelineSolver.ts @@ -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, }, ] }, diff --git a/lib/solvers/TraceCleanupSolver/TraceCleanupSolver.ts b/lib/solvers/TraceCleanupSolver/TraceCleanupSolver.ts index da39fd852..5f63508c1 100644 --- a/lib/solvers/TraceCleanupSolver/TraceCleanupSolver.ts +++ b/lib/solvers/TraceCleanupSolver/TraceCleanupSolver.ts @@ -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" @@ -25,10 +26,12 @@ export interface TraceCleanupSolverInput { paddingBuffer: number operations?: readonly TraceCleanupOperation[] eligibleTraceIds?: ReadonlySet + netLabelConnectorTraceIds?: ReadonlySet } 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. @@ -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 @@ -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() diff --git a/lib/solvers/TraceCleanupSolver/rerouteGeneratedNetLabelConnectorCrossings.ts b/lib/solvers/TraceCleanupSolver/rerouteGeneratedNetLabelConnectorCrossings.ts new file mode 100644 index 000000000..578348856 --- /dev/null +++ b/lib/solvers/TraceCleanupSolver/rerouteGeneratedNetLabelConnectorCrossings.ts @@ -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, + eligibleTraceIds?: ReadonlySet, +) => { + 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> + clearance: number + eligibleTraceIds?: ReadonlySet + connectorTraceIds: ReadonlySet +}) => { + 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, + } +} diff --git a/lib/solvers/TraceCleanupSolver/sub-solver/findIntersectionsWithObstacles.ts b/lib/solvers/TraceCleanupSolver/sub-solver/findIntersectionsWithObstacles.ts index 863d07f99..17042ab23 100644 --- a/lib/solvers/TraceCleanupSolver/sub-solver/findIntersectionsWithObstacles.ts +++ b/lib/solvers/TraceCleanupSolver/sub-solver/findIntersectionsWithObstacles.ts @@ -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. @@ -48,14 +52,24 @@ 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]! @@ -63,8 +77,8 @@ export const findPerpendicularPathCrossings = ( 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]! diff --git a/lib/solvers/TraceCleanupSolver/sub-solver/generateLShapeRerouteCandidates.ts b/lib/solvers/TraceCleanupSolver/sub-solver/generateLShapeRerouteCandidates.ts index 6fbf3ed49..2db96fbcd 100644 --- a/lib/solvers/TraceCleanupSolver/sub-solver/generateLShapeRerouteCandidates.ts +++ b/lib/solvers/TraceCleanupSolver/sub-solver/generateLShapeRerouteCandidates.ts @@ -113,6 +113,7 @@ export interface PerpendicularTraceDetourInput { segmentIndex: number obstacleStart: Point obstacleEnd: Point + obstacleBounds?: Bounds chipBounds: Bounds[] clearance: number } @@ -179,6 +180,7 @@ export const generatePerpendicularTraceDetours = ({ segmentIndex, obstacleStart, obstacleEnd, + obstacleBounds, chipBounds, clearance, }: PerpendicularTraceDetourInput): TraceDetourCandidate[] => { @@ -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([ diff --git a/tests/repros/__snapshots__/repro-wireless-mouse-charger-section.snap.svg b/tests/repros/__snapshots__/repro-wireless-mouse-charger-section.snap.svg index 5a1f71f3c..6183c9ca6 100644 --- a/tests/repros/__snapshots__/repro-wireless-mouse-charger-section.snap.svg +++ b/tests/repros/__snapshots__/repro-wireless-mouse-charger-section.snap.svg @@ -1,6 +1,6 @@ - STATVSSVBATVDDPROGanodecathode12anodecathodeanodecathode1234STATVSSVBATVDDPROGanodecathode12anodecathodeanodecathode1234 trace.mspPairId === "U3.3-U3.7")! - const labelConnector = traces.find((trace) => - trace.mspPairId.startsWith("available-net-orientation-"), + const [labelConnectorTraceId] = + solver.availableNetOrientationSolver!.netLabelConnectorTraceIds + const labelConnector = traces.find( + (trace) => trace.mspPairId === labelConnectorTraceId, )! expect(realTrace.tracePath).toEqual([ { x: 1.4, y: -0.3 }, diff --git a/tests/solvers/TraceCleanupSolver/reroute-generated-net-label-connector-crossings.test.ts b/tests/solvers/TraceCleanupSolver/reroute-generated-net-label-connector-crossings.test.ts new file mode 100644 index 000000000..57abf9cf1 --- /dev/null +++ b/tests/solvers/TraceCleanupSolver/reroute-generated-net-label-connector-crossings.test.ts @@ -0,0 +1,160 @@ +import { expect, test } from "bun:test" +import { getPathLength } from "lib/solvers/Example28Solver/geometry" +import type { SolvedTracePath } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceLinesSolver" +import { rerouteGeneratedNetLabelConnectorCrossings } from "lib/solvers/TraceCleanupSolver/rerouteGeneratedNetLabelConnectorCrossings" +import { findPerpendicularPathCrossings } from "lib/solvers/TraceCleanupSolver/sub-solver/findIntersectionsWithObstacles" + +const makeTrace = ({ + mspPairId, + globalConnNetId, + tracePath, + pinIds, +}: { + mspPairId: string + globalConnNetId: string + tracePath: Array<{ x: number; y: number }> + pinIds: string[] +}): SolvedTracePath => { + const firstPoint = tracePath[0]! + const lastPoint = tracePath.at(-1)! + return { + mspPairId, + dcConnNetId: globalConnNetId, + globalConnNetId, + pins: [ + { + pinId: pinIds[0]!, + chipId: `${mspPairId}-start-chip`, + ...firstPoint, + }, + { + pinId: pinIds[1] ?? pinIds[0]!, + chipId: `${mspPairId}-end-chip`, + ...lastPoint, + }, + ], + pinIds, + mspConnectionPairIds: [mspPairId], + tracePath, + } +} + +const signalTrace = makeTrace({ + mspPairId: "signal-trace", + globalConnNetId: "signal-net", + pinIds: ["signal-a", "signal-b"], + tracePath: [ + { x: 1, y: 3 }, + { x: 2, y: 3 }, + { x: 2, y: 0 }, + { x: -6, y: 0 }, + ], +}) + +const connectorTrace = makeTrace({ + mspPairId: "vbat-label-connector", + globalConnNetId: "vbat-net", + pinIds: ["vbat"], + tracePath: [ + { x: 3, y: 1 }, + { x: 1.5, y: 1 }, + { x: 1.5, y: 1.5 }, + ], +}) + +const vbatLabel = { + globalConnNetId: "vbat-net", + pinIds: ["vbat"], + anchorPoint: { x: 1.5, y: 1.5 }, + center: { x: 1.5, y: 1.7 }, + width: 1, + height: 0.4, +} + +const inputProblem = { chips: [], textBoxes: [] } as any + +test("reroutes a component trace through an equally short clean corridor", () => { + const result = rerouteGeneratedNetLabelConnectorCrossings({ + inputProblem, + traces: [signalTrace, connectorTrace], + netLabelPlacements: [vbatLabel as any], + mergedLabelNetIdMap: {}, + clearance: 0.1, + eligibleTraceIds: new Set([signalTrace.mspPairId]), + connectorTraceIds: new Set([connectorTrace.mspPairId]), + }) + + expect(result.reroutedTraceCount).toBe(1) + expect(result.traces[1]!.tracePath).toEqual(connectorTrace.tracePath) + expect(getPathLength(result.traces[0]!.tracePath)).toBeLessThanOrEqual( + getPathLength(signalTrace.tracePath), + ) + expect( + findPerpendicularPathCrossings( + result.traces[0]!.tracePath, + result.traces[1]!.tracePath, + { includeTerminalSegments: true }, + ), + ).toEqual([]) +}) + +test("preserves a crossing when avoiding it would lengthen the routed trace", () => { + const straightSignal = makeTrace({ + mspPairId: "straight-signal", + globalConnNetId: "signal-net", + pinIds: ["signal-a", "signal-b"], + tracePath: [ + { x: -2, y: 0 }, + { x: 2, y: 0 }, + ], + }) + const verticalConnector = makeTrace({ + mspPairId: "vertical-label-connector", + globalConnNetId: "label-net", + pinIds: ["label-pin"], + tracePath: [ + { x: 0, y: -1 }, + { x: 0, y: 1 }, + ], + }) + + const result = rerouteGeneratedNetLabelConnectorCrossings({ + inputProblem, + traces: [straightSignal, verticalConnector], + netLabelPlacements: [], + mergedLabelNetIdMap: {}, + clearance: 0.1, + eligibleTraceIds: new Set([straightSignal.mspPairId]), + connectorTraceIds: new Set([verticalConnector.mspPairId]), + }) + + expect(result.reroutedTraceCount).toBe(0) + expect(result.traces[0]!.tracePath).toEqual(straightSignal.tracePath) +}) + +test("allows crossings between generated label connectors", () => { + const otherConnector = makeTrace({ + mspPairId: "other-label-connector", + globalConnNetId: "other-label-net", + pinIds: ["other-label-pin"], + tracePath: [ + { x: 2, y: 0 }, + { x: 2, y: 2 }, + ], + }) + + const result = rerouteGeneratedNetLabelConnectorCrossings({ + inputProblem, + traces: [connectorTrace, otherConnector], + netLabelPlacements: [vbatLabel as any], + mergedLabelNetIdMap: {}, + clearance: 0.1, + connectorTraceIds: new Set([ + connectorTrace.mspPairId, + otherConnector.mspPairId, + ]), + }) + + expect(result.reroutedTraceCount).toBe(0) + expect(result.traces).toEqual([connectorTrace, otherConnector]) +})