0hmX/simplified-am62l-computer

This code defines components and scripts for physically representing and routing high-speed DDR memory signals, power lines, and ground planes on an 8-layer PCB with detailed pin assignments, copper pours, and precise via and trace layouts based on TI EVM reference data.

Version
1.0.16
License
unset
Stars
0

routing/am62l-classified-autorouter.ts

import {
  AutoroutingPipelineSolver7_MultiGraph,
  AutoroutingPipelineSolver9_PreloadedTraceGraph,
  type SimpleRouteConnection as CapacityRouteConnection,
} from "@tscircuit/capacity-autorouter"
import type {
  AutorouterCompleteEvent,
  AutorouterErrorEvent,
  AutorouterProgressEvent,
  GenericLocalAutorouter,
  SimpleRouteJson,
  SimplifiedPcbTrace,
} from "@tscircuit/core"
import { TI_AM62L_EVM_DDR_ROUTES } from "./ti-am62l-evm-ddr-routes"
import { TI_AM62L_EVM_GROUND_FANOUT } from "./ti-am62l-evm-ground-fanout"
import { TI_AM62L_EVM_MMC1_ESCAPES } from "./ti-am62l-evm-mmc1-escapes"

export type Am62lRouteClass =
  | "ddr"
  | "clock"
  | "sd"
  | "reset-control"
  | "usb-power"
  | "power"
  | "ground"
  | "general"

export type Am62lRoutingPhase = Exclude<Am62lRouteClass, "general"> | "mixed"

type Point = { x: number; y: number }
type RoutingLayer =
  | "top"
  | "inner2"
  | "inner3"
  | "inner4"
  | "inner5"
  | "bottom"
type RoutePoint = Point & {
  layer: string
  pointId?: string
  pcb_port_id?: string
}

const getLayerZ = (layer: string, layerCount: number) => {
  if (layer === "top") return 0
  if (layer === "bottom") return layerCount - 1
  return Number(layer.slice(5))
}

const getLayerName = (z: number, layerCount: number) => {
  if (z === 0) return "top"
  if (z === layerCount - 1) return "bottom"
  return `inner${z}`
}

const getLayersBetween = (
  fromLayer: string,
  toLayer: string,
  layerCount: number,
) => {
  const fromZ = getLayerZ(fromLayer, layerCount)
  const toZ = getLayerZ(toLayer, layerCount)
  const minZ = Math.min(fromZ, toZ)
  const maxZ = Math.max(fromZ, toZ)
  return Array.from({ length: maxZ - minZ + 1 }, (_, index) =>
    getLayerName(minZ + index, layerCount),
  )
}

/** Materialize committed copper as geometric obstacles for Pipeline1. */
const getTraceObstacles = (
  traces: SimplifiedPcbTrace[],
  layerCount: number,
) => {
  const obstacles: any[] = []
  for (const [traceIndex, trace] of traces.entries()) {
    const connectedTo = [
      trace.pcb_trace_id,
      trace.connection_name,
      ...(trace.connectsTo ?? []),
    ].filter((id): id is string => Boolean(id))
    for (const [pointIndex, point] of trace.route.entries()) {
      if (point.route_type !== "via") continue
      obstacles.push({
        obstacleId: `committed_${traceIndex}_${pointIndex}_via`,
        type: "rect",
        layers: getLayersBetween(
          point.from_layer,
          point.to_layer,
          layerCount,
        ),
        center: { x: point.x, y: point.y },
        width: point.via_diameter ?? 0.4572,
        height: point.via_diameter ?? 0.4572,
        connectedTo,
      })
    }
    for (let pointIndex = 0; pointIndex < trace.route.length - 1; pointIndex++) {
      const start = trace.route[pointIndex]
      const end = trace.route[pointIndex + 1]
      if (
        start.route_type !== "wire" ||
        end.route_type !== "wire" ||
        start.layer !== end.layer
      ) continue
      const dx = end.x - start.x
      const dy = end.y - start.y
      const length = Math.hypot(dx, dy)
      if (length <= 0.001) continue
      obstacles.push({
        obstacleId: `committed_${traceIndex}_${pointIndex}_wire`,
        type: "rect",
        layers: [start.layer],
        center: { x: (start.x + end.x) / 2, y: (start.y + end.y) / 2 },
        width: length,
        height: Math.max(start.width, 0.001),
        ccwRotationDegrees: (Math.atan2(dy, dx) * 180) / Math.PI,
        connectedTo,
      })
    }
  }
  return obstacles
}

const isPointInsideObstacle = (
  point: Point,
  obstacle: any,
  layer: string,
  margin: number,
) => {
  if (!obstacle.layers?.includes(layer) || !obstacle.center) return false
  const angle = -((obstacle.ccwRotationDegrees ?? 0) * Math.PI) / 180
  const dx = point.x - obstacle.center.x
  const dy = point.y - obstacle.center.y
  const localX = dx * Math.cos(angle) - dy * Math.sin(angle)
  const localY = dx * Math.sin(angle) + dy * Math.cos(angle)
  return (
    Math.abs(localX) <= (obstacle.width ?? 0) / 2 + margin &&
    Math.abs(localY) <= (obstacle.height ?? 0) / 2 + margin
  )
}

class MinHeap<T> {
  private readonly entries: Array<{ value: T; priority: number }> = []
  push(value: T, priority: number) {
    this.entries.push({ value, priority })
    let index = this.entries.length - 1
    while (index > 0) {
      const parent = Math.floor((index - 1) / 2)
      if (this.entries[parent].priority <= priority) break
      this.entries[index] = this.entries[parent]
      index = parent
    }
    this.entries[index] = { value, priority }
  }
  pop(): T | undefined {
    if (this.entries.length === 0) return undefined
    const first = this.entries[0].value
    const last = this.entries.pop()
    if (!last || this.entries.length === 0) return first
    let index = 0
    while (true) {
      const left = index * 2 + 1
      const right = left + 1
      if (left >= this.entries.length) break
      const child =
        right < this.entries.length &&
        this.entries[right].priority < this.entries[left].priority
          ? right
          : left
      if (this.entries[child].priority >= last.priority) break
      this.entries[index] = this.entries[child]
      index = child
    }
    this.entries[index] = last
    return first
  }
  get size() {
    return this.entries.length
  }
}

const simplifyGridPath = (points: Point[]): Point[] => {
  if (points.length < 3) return points
  const result = [points[0]]
  for (let index = 1; index < points.length - 1; index++) {
    const previous = result[result.length - 1]!
    const current = points[index]
    const next = points[index + 1]
    const firstDx = Math.sign(current.x - previous.x)
    const firstDy = Math.sign(current.y - previous.y)
    const nextDx = Math.sign(next.x - current.x)
    const nextDy = Math.sign(next.y - current.y)
    if (firstDx !== nextDx || firstDy !== nextDy) result.push(current)
  }
  result.push(points[points.length - 1]!)
  return result
}

export const findSignalLayerPath = (
  input: SimpleRouteJson,
  start: Point,
  end: Point,
  obstacles: any[],
  layer: RoutingLayer = "inner5",
): Point[] => {
  const step = 0.25
  const bounds = input.bounds
  const minX = bounds.minX + 0.75
  const minY = bounds.minY + 0.75
  const maxIx = Math.floor((bounds.maxX - 0.75 - minX) / step)
  const maxIy = Math.floor((bounds.maxY - 0.75 - minY) / step)
  const toGrid = (point: Point) => ({
    ix: Math.round((point.x - minX) / step),
    iy: Math.round((point.y - minY) / step),
  })
  const toPoint = ({ ix, iy }: { ix: number; iy: number }) => ({
    x: minX + ix * step,
    y: minY + iy * step,
  })
  const startGrid = toGrid(start)
  const endGrid = toGrid(end)
  const key = (ix: number, iy: number) => `${ix}:${iy}`
  const startKey = key(startGrid.ix, startGrid.iy)
  const endKey = key(endGrid.ix, endGrid.iy)
  const blockedCache = new Map<string, boolean>()
  const isBlocked = (ix: number, iy: number) => {
    const cellKey = key(ix, iy)
    if (cellKey === startKey || cellKey === endKey) return false
    const cached = blockedCache.get(cellKey)
    if (cached !== undefined) return cached
    const point = toPoint({ ix, iy })
    const blocked = obstacles.some((obstacle) =>
      isPointInsideObstacle(point, obstacle, layer, 0.13),
    )
    blockedCache.set(cellKey, blocked)
    return blocked
  }
  const open = new MinHeap<{ ix: number; iy: number }>()
  const cost = new Map<string, number>([[startKey, 0]])
  const previous = new Map<string, string>()
  open.push(startGrid, 0)
  const directions = [
    [1, 0], [-1, 0], [0, 1], [0, -1],
    [1, 1], [1, -1], [-1, 1], [-1, -1],
  ] as const
  let iterations = 0
  while (open.size > 0 && iterations++ < 250_000) {
    const current = open.pop()!
    const currentKey = key(current.ix, current.iy)
    if (currentKey === endKey) break
    for (const [dx, dy] of directions) {
      const ix = current.ix + dx
      const iy = current.iy + dy
      if (ix < 0 || iy < 0 || ix > maxIx || iy > maxIy || isBlocked(ix, iy)) {
        continue
      }
      const nextKey = key(ix, iy)
      const nextCost =
        (cost.get(currentKey) ?? Infinity) + (dx !== 0 && dy !== 0 ? Math.SQRT2 : 1)
      if (nextCost >= (cost.get(nextKey) ?? Infinity)) continue
      cost.set(nextKey, nextCost)
      previous.set(nextKey, currentKey)
      const heuristic = Math.hypot(endGrid.ix - ix, endGrid.iy - iy)
      open.push({ ix, iy }, nextCost + heuristic)
    }
  }
  if (!previous.has(endKey) && startKey !== endKey) {
    throw new Error(`Custom ${layer} grid router found no collision-free path`)
  }
  const gridPath = [endKey]
  while (gridPath[gridPath.length - 1] !== startKey) {
    const parent = previous.get(gridPath[gridPath.length - 1]!)
    if (!parent) throw new Error(`Custom ${layer} path reconstruction failed`)
    gridPath.push(parent)
  }
  return simplifyGridPath(
    gridPath.reverse().map((cellKey) => {
      const [ix, iy] = cellKey.split(":").map(Number)
      return toPoint({ ix, iy })
    }),
  )
}

const CLASS_PRIORITY: Record<Am62lRouteClass, number> = {
  ddr: 0,
  clock: 1,
  sd: 2,
  "reset-control": 3,
  "usb-power": 4,
  power: 5,
  ground: 6,
  general: 7,
}

const connectionSearchText = (connection: CapacityRouteConnection): string =>
  [
    connection.name,
    connection.rootConnectionName,
    connection.netConnectionName,
    connection.__netConnectionName,
    ...(connection.mergedConnectionNames ?? []),
    ...(connection.__rootConnectionNames ?? []),
  ]
    .filter(Boolean)
    .join(" ")
    .toUpperCase()

export const classifyAm62lConnection = (
  connection: CapacityRouteConnection,
): Am62lRouteClass => {
  const text = connectionSearchText(connection)
  if (/DDR0_|LPDDR|VDD_DDR/.test(text)) return "ddr"
  if (/OSC_|XRCGB|_CK0|_DQS/.test(text)) return "clock"
  if (/SD_PWR/.test(text)) return "reset-control"
  if (/SD_3V3|SD_IO|SD_LOADSW|CAP_VDDS_MMC1/.test(text)) return "power"
  if (/SD_|MMC1/.test(text)) return "sd"
  if (/USB_|VBUS|TUSB|EFUSE|PROTECTED_5V|AON_3V3/.test(text)) {
    return "usb-power"
  }
  if (/POR|RESET|BOOTMODE|PMIC_|WAKEUP|UART|I2C/.test(text)) {
    return "reset-control"
  }
  if (/GND|VSS/.test(text)) return "ground"
  if (/VDD|VDDA|CAP_|3V3|1V8|0V75|_SW|_LX/.test(text)) return "power"
  return "general"
}

const getBounds = (connection: CapacityRouteConnection) => {
  const points = connection.pointsToConnect as Point[]
  return {
    minX: Math.min(...points.map((point) => point.x)),
    maxX: Math.max(...points.map((point) => point.x)),
    minY: Math.min(...points.map((point) => point.y)),
    maxY: Math.max(...points.map((point) => point.y)),
  }
}

const boundsOverlap = (
  first: ReturnType<typeof getBounds>,
  second: ReturnType<typeof getBounds>,
): boolean =>
  first.minX <= second.maxX &&
  first.maxX >= second.minX &&
  first.minY <= second.maxY &&
  first.maxY >= second.minY

const manhattanSpan = (connection: CapacityRouteConnection): number => {
  const bounds = getBounds(connection)
  return bounds.maxX - bounds.minX + bounds.maxY - bounds.minY
}

/**
 * Estimate route contention before invoking the geometric solver. Connections
 * whose endpoint envelopes overlap many other envelopes are routed first so
 * later, easier nets can flow around the committed critical paths.
 */
const conflictScore = (
  connection: CapacityRouteConnection,
  allConnections: CapacityRouteConnection[],
): number => {
  const bounds = getBounds(connection)
  let score = 0
  for (const candidate of allConnections) {
    if (candidate === connection) continue
    if (boundsOverlap(bounds, getBounds(candidate))) score += 1
  }
  return score
}

export const orderAm62lConnections = (
  connections: CapacityRouteConnection[],
): CapacityRouteConnection[] => {
  const scoreByConnection = new Map(
    connections.map((connection) => [
      connection,
      conflictScore(connection, connections),
    ]),
  )
  return [...connections].sort((first, second) => {
    const firstSpan = manhattanSpan(first)
    const secondSpan = manhattanSpan(second)
    const firstIsLocal = firstSpan <= 8
    const secondIsLocal = secondSpan <= 8
    if (firstIsLocal !== secondIsLocal) return firstIsLocal ? -1 : 1
    if (firstIsLocal && secondIsLocal) {
      const localSpanDelta = firstSpan - secondSpan
      if (Math.abs(localSpanDelta) > 1e-9) return localSpanDelta
    }

    const classDelta =
      CLASS_PRIORITY[classifyAm62lConnection(first)] -
      CLASS_PRIORITY[classifyAm62lConnection(second)]
    if (classDelta !== 0) return classDelta

    const conflictDelta =
      (scoreByConnection.get(second) ?? 0) -
      (scoreByConnection.get(first) ?? 0)
    if (conflictDelta !== 0) return conflictDelta

    const spanDelta = secondSpan - firstSpan
    if (Math.abs(spanDelta) > 1e-9) return spanDelta
    return first.name.localeCompare(second.name)
  })
}

type PreparedRouteConnection = CapacityRouteConnection & {
  __originalConnectionName?: string
  __originalPointCount?: number
}

/**
 * Convert a multi-terminal electrical net into a deterministic minimum-span
 * tree of two-point routing branches.  This preserves one logical net name,
 * while avoiding the capacity solver's expensive all-terminals-at-once search.
 */
const expandConnectionsIntoMinimumSpanBranches = (
  connections: CapacityRouteConnection[],
): PreparedRouteConnection[] => {
  const expanded: PreparedRouteConnection[] = []
  for (const connection of connections) {
    const points = connection.pointsToConnect as RoutePoint[]
    if (points.length <= 2) {
      expanded.push({
        ...connection,
        __originalConnectionName: connection.name,
        __originalPointCount: points.length,
        mergedConnectionNames: [
          connection.name,
          ...(connection.mergedConnectionNames ?? []),
        ],
      })
      continue
    }
    const edges: Array<[number, number]> = []
    const inTree = new Set([0])
    while (inTree.size < points.length) {
      let best:
        | { from: number; to: number; cost: number }
        | undefined
      for (const from of inTree) {
        for (let to = 0; to < points.length; to++) {
          if (inTree.has(to)) continue
          const layerPenalty = points[from].layer === points[to].layer ? 0 : 2
          const cost =
            Math.abs(points[from].x - points[to].x) +
            Math.abs(points[from].y - points[to].y) +
            layerPenalty
          if (
            !best ||
            cost < best.cost - 1e-9 ||
            (Math.abs(cost - best.cost) < 1e-9 &&
              `${from}:${to}` < `${best.from}:${best.to}`)
          ) best = { from, to, cost }
        }
      }
      if (!best) throw new Error(`Could not branch net ${connection.name}`)
      edges.push([best.from, best.to])
      inTree.add(best.to)
    }
    const branchNames = edges.map(
      (_, index) => `${connection.name}__branch_${index + 1}`,
    )
    for (const [index, [from, to]] of edges.entries()) {
      expanded.push({
        ...connection,
        name: branchNames[index],
        rootConnectionName: connection.rootConnectionName ?? connection.name,
        __originalConnectionName: connection.name,
        __originalPointCount: points.length,
        mergedConnectionNames: [
          connection.name,
          ...branchNames,
          ...(connection.mergedConnectionNames ?? []),
        ],
        pointsToConnect: [points[from], points[to]],
      })
    }
  }
  return expanded
}

type RouterEventHandlers = {
  complete: Array<(event: AutorouterCompleteEvent) => void>
  error: Array<(event: AutorouterErrorEvent) => void>
  progress: Array<(event: AutorouterProgressEvent) => void>
}

type TiRoutePoint = (typeof TI_AM62L_EVM_DDR_ROUTES)[keyof typeof TI_AM62L_EVM_DDR_ROUTES]["route"][number]

const distance = (first: Point, second: Point) =>
  Math.hypot(first.x - second.x, first.y - second.y)

const getComponentCenterForPort = (
  input: SimpleRouteJson,
  point: RoutePoint,
): Point | undefined => {
  if (!point.pointId) return undefined
  const obstacles = (input.obstacles ?? []) as any[]
  const portObstacle = obstacles.find(
    (obstacle) =>
      obstacle.center &&
      distance(obstacle.center, point) < 0.03 &&
      obstacle.connectedTo?.includes(point.pointId),
  )
  if (!portObstacle?.componentId) return undefined
  const componentObstacles = obstacles.filter(
    (obstacle) => obstacle.componentId === portObstacle.componentId && obstacle.center,
  )
  // The AM62L package contributes hundreds of ball obstacles. This guard keeps
  // the coordinate matcher from treating a peripheral IC as the SoC.
  if (componentObstacles.length < 300) return undefined
  const xs = componentObstacles.map((obstacle) => obstacle.center.x)
  const ys = componentObstacles.map((obstacle) => obstacle.center.y)
  return {
    x: (Math.min(...xs) + Math.max(...xs)) / 2,
    y: (Math.min(...ys) + Math.max(...ys)) / 2,
  }
}

type Mmc1Escape = (typeof TI_AM62L_EVM_MMC1_ESCAPES)[keyof typeof TI_AM62L_EVM_MMC1_ESCAPES]

export const prepareTiMmc1Escapes = (
  input: SimpleRouteJson,
): { input: SimpleRouteJson; escapeTraces: SimplifiedPcbTrace[] } => {
  const escapeTraces: SimplifiedPcbTrace[] = []
  const connections = (input.connections as CapacityRouteConnection[]).map(
    (connection) => {
      let match:
        | {
            pointIndex: number
            center: Point
            signal: string
            escape: Mmc1Escape
          }
        | undefined
      const points = connection.pointsToConnect as RoutePoint[]
      for (const [pointIndex, point] of points.entries()) {
        const center = getComponentCenterForPort(input, point)
        if (!center) continue
        for (const [signal, escape] of Object.entries(
          TI_AM62L_EVM_MMC1_ESCAPES,
        ) as Array<[string, Mmc1Escape]>) {
          const start = escape.route.find(
            (candidate) => candidate.route_type === "wire",
          )
          if (!start) continue
          if (
            distance(point, { x: center.x + start.x, y: center.y + start.y }) <
            0.03
          ) {
            match = { pointIndex, center, signal, escape }
            break
          }
        }
        if (match) break
      }
      if (!match) return connection

      const alignedRoute = match.escape.route.map((point) => ({
        ...point,
        x: point.x + match!.center.x,
        y: point.y + match!.center.y,
      })) as SimplifiedPcbTrace["route"]
      const finalWire = [...alignedRoute]
        .reverse()
        .find((point) => point.route_type === "wire")
      if (!finalWire || finalWire.route_type !== "wire") {
        throw new Error(`TI ${match.signal} escape has no wire exit`)
      }
      const originalPoint = points[match.pointIndex]
      const syntheticPointId =
        `ti_mmc1_escape:${match.signal}:${connection.name}:${originalPoint.pointId}`
      const transformedPoints = [...points]
      transformedPoints[match.pointIndex] = {
        x: finalWire.x,
        y: finalWire.y,
        layer: finalWire.layer,
        pointId: syntheticPointId,
      }
      const connectedTo = [
        connection.name,
        connection.rootConnectionName,
        connection.netConnectionName,
        connection.__netConnectionName,
        originalPoint.pointId,
        originalPoint.pcb_port_id,
        syntheticPointId,
      ].filter((id): id is string => Boolean(id))
      const firstWire = alignedRoute.find(
        (point) => point.route_type === "wire",
      )
      const lastWire = [...alignedRoute]
        .reverse()
        .find((point) => point.route_type === "wire")
      if (firstWire?.route_type === "wire" && originalPoint.pcb_port_id) {
        ;(firstWire as typeof firstWire & { start_pcb_port_id?: string }).start_pcb_port_id =
          originalPoint.pcb_port_id
      }
      if (lastWire?.route_type === "wire") {
        ;(lastWire as typeof lastWire & { end_pcb_port_id?: string }).end_pcb_port_id =
          syntheticPointId
      }
      escapeTraces.push({
        type: "pcb_trace",
        pcb_trace_id: `ti_mmc1_escape_${match.signal}_${connection.name}`,
        connection_name: connection.name,
        connectsTo: connectedTo,
        route: alignedRoute,
      })
      return { ...connection, pointsToConnect: transformedPoints }
    },
  )
  return {
    input: { ...input, connections: connections as SimpleRouteJson["connections"] },
    escapeTraces,
  }
}

/**
 * Dense BGAs are escaped before the global route. Keep every pad as a copper
 * obstacle, but do not ask Pipeline 7 to build a second component-local mesh
 * around hundreds of pads and then merge it across the fixed fanout copper.
 */
const useGlobalTopologyForDenseComponents = (
  input: SimpleRouteJson,
): SimpleRouteJson => {
  const obstacleCountByComponent = new Map<string, number>()
  for (const obstacle of input.obstacles as Array<{ componentId?: string }>) {
    if (!obstacle.componentId) continue
    obstacleCountByComponent.set(
      obstacle.componentId,
      (obstacleCountByComponent.get(obstacle.componentId) ?? 0) + 1,
    )
  }
  const denseComponentIds = new Set(
    [...obstacleCountByComponent.entries()]
      .filter(([, obstacleCount]) => obstacleCount >= 64)
      .map(([componentId]) => componentId),
  )
  return {
    ...input,
    obstacles: input.obstacles.map((obstacle) => {
      if (!obstacle.componentId || !denseComponentIds.has(obstacle.componentId)) {
        return obstacle
      }
      const globalObstacle = { ...obstacle }
      delete globalObstacle.componentId
      return globalObstacle
    }),
  }
}

const prepareSdInner5Trunks = (
  input: SimpleRouteJson,
  connections: PreparedRouteConnection[],
  initialTraces: SimplifiedPcbTrace[],
) => {
  const remaining: PreparedRouteConnection[] = []
  const trunkConnections: PreparedRouteConnection[] = []
  for (const connection of connections) {
    const points = connection.pointsToConnect as RoutePoint[]
    const hasTiEscape = points.some((point) =>
      point.pointId?.startsWith("ti_mmc1_escape:"),
    )
    if (hasTiEscape && points.length === 2 && manhattanSpan(connection) > 8) {
      trunkConnections.push(connection)
    } else {
      remaining.push(connection)
    }
  }
  trunkConnections.sort((first, second) => first.name.localeCompare(second.name))
  const traces = [...initialTraces]
  for (const connection of trunkConnections) {
    const points = connection.pointsToConnect as RoutePoint[]
    const start = points.find((point) =>
      point.pointId?.startsWith("ti_mmc1_escape:"),
    )!
    const end = points.find((point) => point !== start)!
    const startAccess = { x: start.x, y: start.y }
    const endAccess = { x: end.x, y: end.y + (end.y >= 0 ? -1 : 1) }
    const logicalName = connection.__originalConnectionName ?? connection.name
    const committed = [
      ...(input.traces ?? []),
      ...traces,
    ].filter(
      (trace) => trace.connection_name !== logicalName,
    ) as SimplifiedPcbTrace[]
    const obstacles = [
      ...(input.obstacles ?? []),
      ...getTraceObstacles(committed, input.layerCount),
    ] as any[]
    let signalLayer: "inner2" | "inner5" = "inner5"
    let gridPath: Point[]
    try {
      gridPath = findSignalLayerPath(
        input,
        startAccess,
        endAccess,
        obstacles,
        signalLayer,
      )
    } catch {
      signalLayer = "inner2"
      try {
        gridPath = findSignalLayerPath(
          input,
          startAccess,
          endAccess,
          obstacles,
          signalLayer,
        )
      } catch (caught) {
        const message = caught instanceof Error ? caught.message : String(caught)
        throw new Error(`${connection.name}: ${message}`)
      }
    }
    const route: SimplifiedPcbTrace["route"] = [
      {
        route_type: "wire",
        x: start.x,
        y: start.y,
        width: 0.09398,
        layer: start.layer as any,
      },
    ]
    if (start.layer !== signalLayer) {
      route.push({
        route_type: "via",
        x: startAccess.x,
        y: startAccess.y,
        from_layer: "top",
        to_layer: "bottom",
        via_diameter: 0.4572,
        via_hole_diameter: 0.2032,
      })
    }
    route.push(
      ...gridPath.map((point) => ({
        route_type: "wire" as const,
        x: point.x,
        y: point.y,
        width: 0.09398,
        layer: signalLayer,
      })),
    )
    if (end.layer !== signalLayer) {
      route.push({
        route_type: "via",
        x: endAccess.x,
        y: endAccess.y,
        from_layer: "top",
        to_layer: "bottom",
        via_diameter: 0.4572,
        via_hole_diameter: 0.2032,
      })
    }
    route.push(
      {
        route_type: "wire",
        x: endAccess.x,
        y: endAccess.y,
        width: 0.08128,
        layer: end.layer as any,
      },
      {
        route_type: "wire",
        x: end.x,
        y: end.y,
        width: 0.08128,
        layer: end.layer as any,
      },
    )
    traces.push({
      type: "pcb_trace",
      pcb_trace_id: `custom_sd_trunk_${connection.name}`,
      connection_name: logicalName,
      connectsTo: [
        connection.name,
        logicalName,
        ...(connection.mergedConnectionNames ?? []),
        ...points.flatMap((point) => [point.pointId, point.pcb_port_id]),
      ].filter((id): id is string => Boolean(id)),
      route,
    })
  }
  return { connections: remaining, traces }
}

const getPortComponentGeometry = (
  input: SimpleRouteJson,
  point: RoutePoint,
) => {
  const pointIds = [point.pointId, point.pcb_port_id].filter(
    (id): id is string => Boolean(id),
  )
  const obstacles = (input.obstacles ?? []) as any[]
  const portObstacle = obstacles.find(
    (obstacle) =>
      obstacle.center &&
      distance(obstacle.center, point) < 0.04 &&
      obstacle.connectedTo?.some((id: string) => pointIds.includes(id)),
  ) ?? obstacles.find(
    // Generated multi-terminal power branches do not always retain the
    // original pcb_port id.  Coordinate matching is still unambiguous at the
    // package pad centre and prevents the PTH fallback from becoming
    // via-in-pad when that metadata is absent.
    (obstacle) =>
      obstacle.componentId &&
      obstacle.center &&
      distance(obstacle.center, point) < 0.04,
  )
  if (!portObstacle?.componentId) return undefined
  const componentObstacles = obstacles.filter(
    (obstacle) =>
      obstacle.componentId === portObstacle.componentId && obstacle.center,
  )
  if (componentObstacles.length === 0) return undefined
  const minX = Math.min(
    ...componentObstacles.map(
      (obstacle) => obstacle.center.x - (obstacle.width ?? 0) / 2,
    ),
  )
  const maxX = Math.max(
    ...componentObstacles.map(
      (obstacle) => obstacle.center.x + (obstacle.width ?? 0) / 2,
    ),
  )
  const minY = Math.min(
    ...componentObstacles.map(
      (obstacle) => obstacle.center.y - (obstacle.height ?? 0) / 2,
    ),
  )
  const maxY = Math.max(
    ...componentObstacles.map(
      (obstacle) => obstacle.center.y + (obstacle.height ?? 0) / 2,
    ),
  )
  return {
    componentId: portObstacle.componentId as string,
    center: { x: (minX + maxX) / 2, y: (minY + maxY) / 2 },
    obstacleCount: componentObstacles.length,
  }
}

const getDogboneAccessPoint = (
  input: SimpleRouteJson,
  point: RoutePoint,
  otherPoint: RoutePoint,
  obstacles: any[],
  requireAllLayerPth = false,
): Point => {
  const geometry = getPortComponentGeometry(input, point)
  if (!geometry) return { x: point.x, y: point.y }
  const rawDirection = {
    x: point.x - geometry.center.x,
    y: point.y - geometry.center.y,
  }
  const fallbackDirection = {
    x: point.x - otherPoint.x,
    y: point.y - otherPoint.y,
  }
  const magnitude = Math.hypot(rawDirection.x, rawDirection.y)
  const fallbackMagnitude = Math.hypot(
    fallbackDirection.x,
    fallbackDirection.y,
  )
  const direction =
    magnitude > 0.05
      ? { x: rawDirection.x / magnitude, y: rawDirection.y / magnitude }
      : fallbackMagnitude > 0.05
        ? {
            x: fallbackDirection.x / fallbackMagnitude,
            y: fallbackDirection.y / fallbackMagnitude,
          }
        : { x: 1, y: 0 }
  const ownIds = [point.pointId, point.pcb_port_id].filter(
    (id): id is string => Boolean(id),
  )
  let leastConflictingCandidate:
    | { point: Point; blockerCount: number }
    | undefined
  const angularStep = 10
  const angleOffsets = Array.from(
    { length: Math.ceil(360 / angularStep) },
    (_, index) => {
    if (index === 0) return 0
    const magnitude = Math.ceil(index / 2) * angularStep
    return index % 2 === 1 ? magnitude : -magnitude
    },
  )
  // A 0.4572 mm PTH plus the 0.08128 mm manufacturing clearance needs about
  // 0.31 mm of radial keepout.  Inner AM62L balls can require a surface escape
  // all the way beyond the package edge before a through-via can be placed.
  for (const distanceFromPad of [
    0.6, 0.8, 1, 1.25, 1.5, 2, 2.5, 3, 4, 5, 6, 7, 8, 10, 12, 15,
  ]) {
    for (const angleOffset of angleOffsets) {
      const angle = (angleOffset * Math.PI) / 180
      const rotated = {
        x: direction.x * Math.cos(angle) - direction.y * Math.sin(angle),
        y: direction.x * Math.sin(angle) + direction.y * Math.cos(angle),
      }
      const candidate = {
        x: point.x + rotated.x * distanceFromPad,
        y: point.y + rotated.y * distanceFromPad,
      }
      const blockerCount = obstacles.reduce((count, obstacle) => {
        if (
          obstacle.connectedTo?.some((id: string) => ownIds.includes(id))
        ) {
          return count
        }
        const blocked = requireAllLayerPth
          ? (obstacle.layers ?? []).some((layer: string) =>
              isPointInsideObstacle(candidate, obstacle, layer, 0.31),
            )
          : isPointInsideObstacle(candidate, obstacle, point.layer, 0.13)
        return count + (blocked ? 1 : 0)
      }, 0)
      if (blockerCount === 0) return candidate
      if (
        requireAllLayerPth &&
        (!leastConflictingCandidate ||
          blockerCount < leastConflictingCandidate.blockerCount)
      ) {
        leastConflictingCandidate = { point: candidate, blockerCount }
      }
    }
  }
  if (!requireAllLayerPth) {
    return {
      x: point.x + direction.x,
      y: point.y + direction.y,
    }
  }
  // Preserve a complete routed artifact for DRC/visual review even when the
  // current placement has no fabrication-legal PTH site. The normal DRC pass
  // will report the chosen least-conflicting location; it is never treated as
  // a clean route.
  if (leastConflictingCandidate) return leastConflictingCandidate.point
  throw new Error(`No PTH candidate generated near (${point.x}, ${point.y})`)
}

/**
 * Complete deterministic point-to-point peripheral branches. SD leaves one
 * short branch to Pipeline 7; USB power leaves its longest branches so shared
 * capacity planning remains focused on the distribution paths that need it.
 */
const prepareCustomBranches = (
  input: SimpleRouteJson,
  connections: PreparedRouteConnection[],
  initialTraces: SimplifiedPcbTrace[],
  phase: "sd" | "usb-power" | "reset-control" | "power",
) => {
  // Pipeline 7's final short SD branch is nondeterministic on this dense
  // placement and can exhaust iterations. Route the complete SD set through
  // the same committed-copper-aware deterministic pathfinder.
  const pipelineConnections: PreparedRouteConnection[] = []
  const customConnections =
    phase === "power"
      ? [...connections].sort((first, second) => {
          const firstHasBottom = (first.pointsToConnect as RoutePoint[]).some(
            (point) => point.layer === "bottom",
          )
          const secondHasBottom = (second.pointsToConnect as RoutePoint[]).some(
            (point) => point.layer === "bottom",
          )
          if (firstHasBottom !== secondHasBottom) return firstHasBottom ? -1 : 1
          return (
            manhattanSpan(first) - manhattanSpan(second) ||
            first.name.localeCompare(second.name)
          )
        })
      : connections
  const traces = [...initialTraces]

  for (const connection of customConnections) {
    const points = connection.pointsToConnect as RoutePoint[]
    if (points.length <= 1) continue
    if (points.length !== 2) {
      throw new Error(
        `Custom ${phase} branch ${connection.name} must have exactly two endpoints`,
      )
    }
    const [start, end] = points
    const logicalName = connection.__originalConnectionName ?? connection.name
    const committed = [
      ...(input.traces ?? []),
      ...traces,
    ].filter(
      (trace) => trace.connection_name !== logicalName,
    ) as SimplifiedPcbTrace[]
    const obstacles = [
      ...(input.obstacles ?? []),
      ...getTraceObstacles(committed, input.layerCount),
    ] as any[]
    const hasDenseBgaEndpoint =
      phase === "power" &&
      [start, end].some(
        (point) =>
          (getPortComponentGeometry(input, point)?.obstacleCount ?? 0) >= 64,
      )
    const surfaceSpanLimit = hasDenseBgaEndpoint ? 15 : Number.POSITIVE_INFINITY
    const localSurfaceLayer =
      start.layer === end.layer &&
      (start.layer === "top" || start.layer === "bottom") &&
      manhattanSpan(connection) <= surfaceSpanLimit
        ? start.layer
        : undefined
    const requiresAllLayerPth = phase === "power" && !localSurfaceLayer
    const startAccess = getDogboneAccessPoint(
      input,
      start,
      end,
      obstacles,
      requiresAllLayerPth,
    )
    const endAccess = getDogboneAccessPoint(
      input,
      end,
      start,
      obstacles,
      requiresAllLayerPth,
    )
    let selected:
      | { signalLayer: RoutingLayer; gridPath: Point[] }
      | undefined
    let lastError: unknown
    const candidateLayers: RoutingLayer[] = []
    if (localSurfaceLayer) candidateLayers.push(localSurfaceLayer)
    // The two central layers are the split power-distribution pair. Power
    // branches enter them through PTH drops; signal phases retain inner2 and
    // inner5 as their preferred global-routing layers.
    if (phase === "power") {
      candidateLayers.push("inner3", "inner4", "bottom", "top", "inner5", "inner2")
    } else {
      candidateLayers.push("inner5", "inner2", "top", "bottom")
    }
    const isUsbPowerDistribution =
      phase === "usb-power" && (connection.__originalPointCount ?? 0) >= 8
    if (isUsbPowerDistribution) candidateLayers.push("inner3")
    if (!hasDenseBgaEndpoint) {
      for (const signalLayer of candidateLayers) {
        try {
          selected = {
            signalLayer,
            gridPath: findSignalLayerPath(
              input,
              startAccess,
              endAccess,
              obstacles,
              signalLayer,
            ),
          }
          break
        } catch (caught) {
          lastError = caught
        }
      }
    }
    // Backside decouplers sit directly below the SoC/LPDDR supply clusters, and
    // a few inner BGA rail balls are enclosed by the provisional PTH ground
    // fanout. Only after every collision-aware layer search fails, place that
    // branch on one of the split power layers. Short backside branches stay on
    // the capacitor side; longer trapped branches are deterministically spread
    // across inner3/inner4 instead of aborting the entire completed phase set.
    if (!selected && phase === "power") {
      const isLocalBacksideBranch = manhattanSpan(connection) <= 4
      const railHash = [...logicalName].reduce(
        (hash, character) => (hash * 33 + character.charCodeAt(0)) >>> 0,
        5381,
      )
      const signalLayer: RoutingLayer = isLocalBacksideBranch
        ? start.layer === "bottom" || end.layer === "bottom"
          ? "bottom"
          : "inner4"
        : railHash % 2 === 0
          ? "inner3"
          : "inner4"
      const elbow =
        railHash % 2 === 0
          ? { x: endAccess.x, y: startAccess.y }
          : { x: startAccess.x, y: endAccess.y }
      selected = {
        signalLayer,
        gridPath: [
          startAccess,
          elbow,
          endAccess,
        ],
      }
    }
    if (!selected) {
      const message =
        lastError instanceof Error ? lastError.message : String(lastError)
      throw new Error(`${connection.name}: ${message}`)
    }
    const { signalLayer, gridPath } = selected
    const nominalWidth = Math.max(
      connection.nominalTraceWidth ?? 0.08128,
      isUsbPowerDistribution ? 0.3 : 0,
      phase === "power"
        ? (connection.__originalPointCount ?? 0) >= 8
          ? 0.25
          : 0.15
        : 0,
      0.08128,
    )
    const route: SimplifiedPcbTrace["route"] = [
      {
        route_type: "wire",
        x: start.x,
        y: start.y,
        width: nominalWidth,
        layer: start.layer as any,
      },
      {
        route_type: "wire",
        x: startAccess.x,
        y: startAccess.y,
        width: nominalWidth,
        layer: start.layer as any,
      },
    ]
    if (start.layer !== signalLayer) {
      route.push({
        route_type: "via",
        x: startAccess.x,
        y: startAccess.y,
        from_layer: "top",
        to_layer: "bottom",
        via_diameter: 0.4572,
        via_hole_diameter: 0.2032,
      })
    }
    route.push(
      ...gridPath.map((point) => ({
        route_type: "wire" as const,
        x: point.x,
        y: point.y,
        width: Math.max(nominalWidth, 0.09398),
        layer: signalLayer,
      })),
    )
    if (end.layer !== signalLayer) {
      route.push({
        route_type: "via",
        x: endAccess.x,
        y: endAccess.y,
        from_layer: "top",
        to_layer: "bottom",
        via_diameter: 0.4572,
        via_hole_diameter: 0.2032,
      })
    }
    route.push(
      {
        route_type: "wire",
        x: endAccess.x,
        y: endAccess.y,
        width: nominalWidth,
        layer: end.layer as any,
      },
      {
        route_type: "wire",
        x: end.x,
        y: end.y,
        width: nominalWidth,
        layer: end.layer as any,
      },
    )
    const firstWire = route.find((point) => point.route_type === "wire")
    const lastWire = [...route]
      .reverse()
      .find((point) => point.route_type === "wire")
    if (firstWire?.route_type === "wire" && start.pcb_port_id) {
      ;(firstWire as typeof firstWire & { start_pcb_port_id?: string })
        .start_pcb_port_id = start.pcb_port_id
    }
    if (lastWire?.route_type === "wire" && end.pcb_port_id) {
      ;(lastWire as typeof lastWire & { end_pcb_port_id?: string })
        .end_pcb_port_id = end.pcb_port_id
    }
    traces.push({
      type: "pcb_trace",
      pcb_trace_id: `custom_${phase}_branch_${connection.name}`,
      connection_name: logicalName,
      connectsTo: [
        connection.name,
        logicalName,
        ...(connection.mergedConnectionNames ?? []),
        ...points.flatMap((point) => [point.pointId, point.pcb_port_id]),
      ].filter((id): id is string => Boolean(id)),
      route,
    })
  }

  return {
    connections: pipelineConnections,
    traces,
  }
}

/**
 * The two unbroken GND pours are the electrical backbone for ground. Turning a
 * 291-terminal ground net into a point-to-point MST is both slow and physically
 * wrong, so emit one local plane drop per pad and remove GND from global search.
 * Every pad uses a dogbone access point before dropping to the ground plane;
 * PTH via-in-pad is intentionally avoided for the dense BGA packages.
 */
const prepareGroundPlaneFanout = (
  input: SimpleRouteJson,
): { input: SimpleRouteJson; traces: SimplifiedPcbTrace[] } => {
  const groundConnection = (
    input.connections as CapacityRouteConnection[]
  ).find((connection) => connection.pointsToConnect.length >= 200)
  if (!groundConnection) return { input, traces: [] }

  const traces: SimplifiedPcbTrace[] = []
  const points = groundConnection.pointsToConnect as RoutePoint[]
  const committed = (input.traces ?? []) as SimplifiedPcbTrace[]
  const obstacles = [
    ...(input.obstacles ?? []),
    ...getTraceObstacles(committed, input.layerCount),
  ] as any[]
  const planeAnchor = `${groundConnection.name}:unbroken-ground-planes`

  for (const [pointIndex, point] of points.entries()) {
    const access = getDogboneAccessPoint(input, point, point, obstacles, true)
    const width = Math.max(groundConnection.nominalTraceWidth ?? 0.08128, 0.2)
    const route: SimplifiedPcbTrace["route"] = [
      {
        route_type: "wire",
        x: point.x,
        y: point.y,
        width,
        layer: point.layer as any,
      },
    ]
    if (distance(point, access) > 0.001) {
      route.push({
        route_type: "wire",
        x: access.x,
        y: access.y,
        width,
        layer: point.layer as any,
      })
    }
    route.push(
      {
        route_type: "via",
        x: access.x,
        y: access.y,
        from_layer: "top",
        to_layer: "bottom",
        via_diameter: 0.4572,
        via_hole_diameter: 0.2032,
      },
      {
        route_type: "wire",
        x: access.x,
        y: access.y,
        width,
        layer: "inner1",
      },
    )
    const firstWire = route[0]
    if (firstWire.route_type === "wire" && point.pcb_port_id) {
      ;(firstWire as typeof firstWire & { start_pcb_port_id?: string })
        .start_pcb_port_id = point.pcb_port_id
    }
    traces.push({
      type: "pcb_trace",
      pcb_trace_id: `custom_ground_plane_drop_${pointIndex + 1}`,
      connection_name: groundConnection.name,
      connectsTo: [
        groundConnection.name,
        planeAnchor,
        point.pointId,
        point.pcb_port_id,
      ].filter((id): id is string => Boolean(id)),
      route,
    })
  }

  return {
    input: {
      ...input,
      connections: input.connections.filter(
        (connection) => connection !== groundConnection,
      ),
    },
    traces,
  }
}

/**
 * Replays TI's shared package-local DGND breakout for U1/U2, then converts
 * every remaining peripheral ground pad into a small independent plane-drop
 * connection. This avoids presenting one 291-terminal hyper-net to the global
 * router and preserves TI's 18/8 mil PTH convention for the two BGAs.
 */
const prepareTiGroundPlaneFanout = (
  input: SimpleRouteJson,
  options: { denseBga?: boolean; peripheral?: boolean } = {},
): { input: SimpleRouteJson; traces: SimplifiedPcbTrace[] } => {
  const { denseBga = true, peripheral = true } = options
  const groundConnection = (
    input.connections as CapacityRouteConnection[]
  ).find((connection) => connection.pointsToConnect.length >= 200)
  if (!groundConnection) return { input, traces: [] }

  const obstacles = (input.obstacles ?? []) as any[]
  const groups = new Map<string, any[]>()
  for (const obstacle of obstacles) {
    if (!obstacle.componentId || !obstacle.center) continue
    const members = groups.get(obstacle.componentId) ?? []
    members.push(obstacle)
    groups.set(obstacle.componentId, members)
  }
  const denseComponents = [...groups.entries()]
    .filter(([, members]) => members.length >= 150)
    .map(([componentId, members]) => {
      const xs = members.map((member) => member.center.x)
      const ys = members.map((member) => member.center.y)
      return {
        componentId,
        obstacleCount: members.length,
        center: {
          x: (Math.min(...xs) + Math.max(...xs)) / 2,
          y: (Math.min(...ys) + Math.max(...ys)) / 2,
        },
      }
    })
    .sort((first, second) => second.obstacleCount - first.obstacleCount)
  const componentCenters: Record<"U1" | "U2", Point> = {
    U1: denseComponents[0]?.center ?? { x: 2, y: 1 },
    U2: denseComponents[1]?.center ?? { x: 22.117, y: 0.949 },
  }

  const planeAnchor = `${groundConnection.name}:inner1-ground-plane`
  const fanoutTraces: SimplifiedPcbTrace[] = []
  if (denseBga) {
    for (const [segmentIndex, segment] of
      TI_AM62L_EVM_GROUND_FANOUT.segments.entries()) {
      const center = componentCenters[segment.component]
      fanoutTraces.push({
        type: "pcb_trace",
        pcb_trace_id: `ti_ground_segment_${segmentIndex + 1}`,
        connection_name: groundConnection.name,
        connectsTo: [groundConnection.name, planeAnchor],
        route: [
          {
            route_type: "wire",
            x: center.x + segment.x1,
            y: center.y + segment.y1,
            width: segment.width,
            layer: "top",
          },
          {
            route_type: "wire",
            x: center.x + segment.x2,
            y: center.y + segment.y2,
            width: segment.width,
            layer: "top",
          },
        ],
      })
    }
    for (const [viaIndex, via] of TI_AM62L_EVM_GROUND_FANOUT.vias.entries()) {
      const center = componentCenters[via.component]
      const x = center.x + via.x
      const y = center.y + via.y
      fanoutTraces.push({
        type: "pcb_trace",
        pcb_trace_id: `ti_ground_via_${viaIndex + 1}`,
        connection_name: groundConnection.name,
        connectsTo: [groundConnection.name, planeAnchor],
        route: [
          {
            route_type: "wire",
            x,
            y,
            width: 0.127,
            layer: "top",
          },
          {
            route_type: "via",
            x,
            y,
            from_layer: "top",
            to_layer: "bottom",
            via_diameter: via.diameter,
            via_hole_diameter: via.holeDiameter,
          },
          {
            route_type: "wire",
            x,
            y,
            width: 0.127,
            layer: "inner1",
          },
        ],
      })
    }
  }

  const committedObstacles = [
    ...obstacles,
    ...getTraceObstacles(
      [...((input.traces ?? []) as SimplifiedPcbTrace[]), ...fanoutTraces],
      input.layerCount,
    ),
  ] as any[]
  if (peripheral) for (const [pointIndex, point] of (
    groundConnection.pointsToConnect as RoutePoint[]
  ).entries()) {
    const geometry = getPortComponentGeometry(input, point)
    if ((geometry?.obstacleCount ?? 0) >= 150) continue
    const access = getDogboneAccessPoint(
      input,
      point,
      point,
      committedObstacles,
      true,
    )
    const syntheticPointId =
      `${groundConnection.name}:plane-drop:${pointIndex + 1}`
    const width = Math.max(groundConnection.nominalTraceWidth ?? 0.08128, 0.2)
    const route: SimplifiedPcbTrace["route"] = [
      {
        route_type: "wire",
        x: point.x,
        y: point.y,
        width,
        layer: point.layer as any,
      },
      {
        route_type: "wire",
        x: access.x,
        y: access.y,
        width,
        layer: point.layer as any,
      },
      {
        route_type: "via",
        x: access.x,
        y: access.y,
        from_layer: "top",
        to_layer: "bottom",
        via_diameter: 0.4572,
        via_hole_diameter: 0.2032,
      },
      {
        route_type: "wire",
        x: access.x,
        y: access.y,
        width,
        layer: "inner1",
      },
    ]
    const firstWire = route[0]
    if (firstWire.route_type === "wire" && point.pcb_port_id) {
      ;(firstWire as typeof firstWire & { start_pcb_port_id?: string })
        .start_pcb_port_id = point.pcb_port_id
    }
    fanoutTraces.push({
      type: "pcb_trace",
      pcb_trace_id: `ground_plane_drop_${pointIndex + 1}`,
      connection_name: groundConnection.name,
      connectsTo: [
        groundConnection.name,
        planeAnchor,
        point.pointId,
        point.pcb_port_id,
        syntheticPointId,
      ].filter((id): id is string => Boolean(id)),
      route,
    })
  }

  return {
    input: {
      ...input,
      connections: input.connections.filter(
        (connection) => connection !== groundConnection,
      ),
      traces: [
        ...(input.traces ?? []),
        ...fanoutTraces,
      ] as SimpleRouteJson["traces"],
    },
    traces: fanoutTraces,
  }
}

const getWireEndpoint = (route: readonly TiRoutePoint[], fromStart: boolean) => {
  const ordered = fromStart ? route : [...route].reverse()
  const point = ordered.find((candidate) => candidate.route_type === "wire")
  if (!point || point.route_type !== "wire") {
    throw new Error("TI EVM DDR route has no wire endpoint")
  }
  return point
}

const reverseTiRoute = (route: readonly TiRoutePoint[]): TiRoutePoint[] =>
  [...route].reverse().map((point) =>
    point.route_type === "via"
      ? {
          ...point,
          from_layer: point.to_layer,
          to_layer: point.from_layer,
        }
      : point,
  ) as TiRoutePoint[]

const findTiSignal = (connection: CapacityRouteConnection) => {
  const text = connectionSearchText(connection)
  for (const signal of Object.keys(TI_AM62L_EVM_DDR_ROUTES).sort(
    (first, second) => second.length - first.length,
  )) {
    if (text.includes(signal.toUpperCase())) return signal
  }
  if (text.includes("DDR_LINK_RESET0_N")) return "DDR0_RESET0_n"
  const points = connection.pointsToConnect as Point[]
  if (points.length !== 2) return undefined
  const actualForward = {
    x: points[1].x - points[0].x,
    y: points[1].y - points[0].y,
  }
  const actualReverse = { x: -actualForward.x, y: -actualForward.y }
  const candidates = Object.entries(TI_AM62L_EVM_DDR_ROUTES)
    .map(([signal, reference]) => {
      const start = getWireEndpoint(reference.route, true)
      const end = getWireEndpoint(reference.route, false)
      const expected = { x: end.x - start.x, y: end.y - start.y }
      return {
        signal,
        error: Math.min(
          distance(actualForward, expected),
          distance(actualReverse, expected),
        ),
      }
    })
    .sort((first, second) => first.error - second.error)
  return candidates[0]?.error < 0.03 ? candidates[0].signal : undefined
}

/**
 * Replays TI's released TMDS62LEVM U28-to-U29 escape topology after aligning
 * it to this board's two package endpoints.  This is deterministic: every DDR
 * net has its own released path, PTH-via positions, layer changes, and width.
 */
class TiEvmDdrAutorouter implements GenericLocalAutorouter {
  isRouting = false
  private readonly handlers: RouterEventHandlers = {
    complete: [],
    error: [],
    progress: [],
  }
  private traces: SimplifiedPcbTrace[] = []

  constructor(public readonly input: SimpleRouteJson) {}

  private solve(): SimplifiedPcbTrace[] {
    const traces: SimplifiedPcbTrace[] = []
    const seenSignals = new Set<string>()
    for (const connection of this.input.connections as CapacityRouteConnection[]) {
      const signal = findTiSignal(connection)
      if (!signal) {
        throw new Error(
          `TI DDR phase received an unclassified connection: ${connection.name}`,
        )
      }
      if (seenSignals.has(signal)) {
        throw new Error(`TI DDR phase received duplicate connection ${signal}`)
      }
      seenSignals.add(signal)
      const reference =
        TI_AM62L_EVM_DDR_ROUTES[
          signal as keyof typeof TI_AM62L_EVM_DDR_ROUTES
        ]
      const points = connection.pointsToConnect as Array<Point & { layer: string }>
      if (points.length !== 2) {
        throw new Error(`${signal} must be point-to-point; got ${points.length} endpoints`)
      }
      const referenceStart = getWireEndpoint(reference.route, true)
      const referenceEnd = getWireEndpoint(reference.route, false)
      const directError = distance(
        {
          x: points[1].x - points[0].x,
          y: points[1].y - points[0].y,
        },
        {
          x: referenceEnd.x - referenceStart.x,
          y: referenceEnd.y - referenceStart.y,
        },
      )
      const reverseError = distance(
        {
          x: points[0].x - points[1].x,
          y: points[0].y - points[1].y,
        },
        {
          x: referenceEnd.x - referenceStart.x,
          y: referenceEnd.y - referenceStart.y,
        },
      )
      const reversed = reverseError < directError
      const actualStart = reversed ? points[1] : points[0]
      const actualEnd = reversed ? points[0] : points[1]
      const alignedRoute = reversed
        ? reverseTiRoute(reference.route)
        : [...reference.route]
      const alignedStart = getWireEndpoint(alignedRoute, true)
      const alignedEnd = getWireEndpoint(alignedRoute, false)
      const offset = {
        x: actualStart.x - alignedStart.x,
        y: actualStart.y - alignedStart.y,
      }
      const endpointError = distance(actualEnd, {
        x: alignedEnd.x + offset.x,
        y: alignedEnd.y + offset.y,
      })
      if (endpointError > 0.03) {
        throw new Error(
          `${signal} placement differs from TI EVM topology by ${endpointError.toFixed(3)}mm; ` +
            "keep U1/U2 orientation and spacing aligned to the reference",
        )
      }
      traces.push({
        type: "pcb_trace",
        pcb_trace_id: connection.name,
        connection_name: connection.name,
        connectsTo: points
          .map((point) => (point as typeof point & { pointId?: string }).pointId)
          .filter((pointId): pointId is string => Boolean(pointId)),
        route: alignedRoute.map((point) =>
          point.route_type === "via"
            ? {
                ...point,
                x: point.x + offset.x,
                y: point.y + offset.y,
                // TI specifies conventional PTH escape vias. The trace may
                // connect on inner2, but the drilled/plated barrel spans the
                // complete eight-layer stack rather than stopping there.
                from_layer: "top",
                to_layer: "bottom",
              }
            : {
                ...point,
                x: point.x + offset.x,
                y: point.y + offset.y,
              },
        ) as SimplifiedPcbTrace["route"],
      })
    }
    if (traces.length !== Object.keys(TI_AM62L_EVM_DDR_ROUTES).length) {
      throw new Error(
        `TI DDR phase routed ${traces.length}/` +
          `${Object.keys(TI_AM62L_EVM_DDR_ROUTES).length} released DDR nets`,
      )
    }
    this.traces = traces
    return traces
  }

  start(): void {
    if (this.isRouting) return
    this.isRouting = true
    queueMicrotask(() => {
      try {
        const traces = this.solve()
        this.isRouting = false
        for (const handler of this.handlers.progress) {
          handler({
            type: "progress",
            steps: traces.length,
            progress: 1,
            phase: "ddr:ti-vca-reference",
          })
        }
        for (const handler of this.handlers.complete) {
          handler({ type: "complete", traces })
        }
      } catch (caught) {
        this.isRouting = false
        const error = caught instanceof Error ? caught : new Error(String(caught))
        for (const handler of this.handlers.error) handler({ type: "error", error })
      }
    })
  }

  stop(): void {
    this.isRouting = false
  }

  on(event: "complete", callback: (event: AutorouterCompleteEvent) => void): void
  on(event: "error", callback: (event: AutorouterErrorEvent) => void): void
  on(event: "progress", callback: (event: AutorouterProgressEvent) => void): void
  on(
    event: "complete" | "error" | "progress",
    callback:
      | ((event: AutorouterCompleteEvent) => void)
      | ((event: AutorouterErrorEvent) => void)
      | ((event: AutorouterProgressEvent) => void),
  ): void {
    ;(this.handlers[event] as Array<(event: any) => void>).push(callback)
  }

  solveSync(): SimplifiedPcbTrace[] {
    return this.solve()
  }

  getOutputSimpleRouteJson(): SimpleRouteJson | undefined {
    if (this.traces.length === 0) return undefined
    return {
      ...this.input,
      traces: [...(this.input.traces ?? []), ...this.traces] as SimpleRouteJson["traces"],
    }
  }
}

class Am62lClassifiedAutorouter implements GenericLocalAutorouter {
  isRouting = false
  private timer: ReturnType<typeof setTimeout> | undefined
  private solver: AutoroutingPipelineSolver7_MultiGraph | undefined
  private readonly handlers: RouterEventHandlers = {
    complete: [],
    error: [],
    progress: [],
  }
  private steps = 0
  private readonly routedTraces: SimplifiedPcbTrace[]
  private readonly connectionBatches: SimpleRouteJson["connections"][]
  private batchIndex = 0

  constructor(
    public readonly input: SimpleRouteJson,
    private readonly phase: Am62lRoutingPhase,
    private readonly effort: number,
    initialTraces: SimplifiedPcbTrace[] = [],
  ) {
    this.routedTraces = [...initialTraces]
    const batchSize = this.phase === "usb-power" ? 1 : Infinity
    this.connectionBatches = []
    for (
      let startIndex = 0;
      startIndex < this.input.connections.length;
      startIndex += batchSize
    ) {
      this.connectionBatches.push(
        this.input.connections.slice(startIndex, startIndex + batchSize),
      )
    }
    this.startPipelineBatch()
  }

  private startPipelineBatch(): void {
    const connections = this.connectionBatches[this.batchIndex]
    if (!connections || connections.length === 0) {
      this.solver = undefined
      return
    }
    this.solver = new AutoroutingPipelineSolver7_MultiGraph(
      {
        ...this.input,
        connections,
        buses: [
          ...(this.input.buses ?? []),
          {
            busId: `am62l_${this.phase}_batch_${this.batchIndex}`,
            name: `${this.phase} signal-layer reservation`,
            connectionNames: connections.map((connection) => connection.name),
            allowedLayers: ["top", "inner2", "inner5", "bottom"],
          },
        ],
        traces: [
          ...(this.input.traces ?? []),
          ...this.routedTraces,
        ],
      } as any,
      {
        // Custom escape/trunk copper has already reduced the search space.
        // Pipeline 7 negotiates each shared phase/batch against committed
        // copper, avoiding an expensive mesh rebuild for every single net.
        effort: Math.min(this.effort, 1),
      },
    )
  }

  private acceptPipelineOutput(): void {
    if (!this.solver) return
    const output = this.solver.getOutputSimpleRouteJson()
    const existingTraceIds = new Set([
      ...(this.input.traces ?? []).map((trace) => trace.pcb_trace_id),
      ...this.routedTraces.map((trace) => trace.pcb_trace_id),
    ])
    const activeConnections = this.connectionBatches[
      this.batchIndex
    ] as PreparedRouteConnection[]
    const activeEndpointAliases = activeConnections.map((connection) =>
      (connection.pointsToConnect as RoutePoint[]).map((point) =>
        [point.pointId, point.pcb_port_id].filter(
          (id): id is string => Boolean(id),
        ),
      ),
    )
    const generatedTraces = ((output.traces ?? []) as SimplifiedPcbTrace[])
      .filter((trace) => {
        if (existingTraceIds.has(trace.pcb_trace_id)) return false
        const connectedIds = new Set(trace.connectsTo ?? [])
        return activeEndpointAliases.some((connectionEndpoints) =>
          connectionEndpoints.every((endpointAliases) =>
            endpointAliases.some((id) => connectedIds.has(id)),
          ),
        )
      })
    if (generatedTraces.length === 0) {
      throw new Error(
        `Pipeline 7 batch ${this.batchIndex + 1} during ${this.phase} ` +
        "emitted no new traces",
      )
    }
    this.routedTraces.push(
      ...generatedTraces.map((trace, traceIndex) => ({
        ...trace,
        pcb_trace_id:
          `${trace.pcb_trace_id}_pipeline7_${this.phase}_` +
          `${this.batchIndex}_${traceIndex}`,
        connectsTo: [
          trace.connection_name,
          ...(trace.connectsTo ?? []),
        ].filter((id): id is string => Boolean(id)),
      })),
    )
    this.batchIndex += 1
    this.solver = undefined
    this.startPipelineBatch()
  }

  private emitError(caught: unknown) {
    const error = caught instanceof Error ? caught : new Error(String(caught))
    for (const handler of this.handlers.error) {
      handler({ type: "error", error })
    }
  }

  private async runChunk() {
    if (!this.isRouting) return
    try {
      if (!this.solver) {
        this.isRouting = false
        for (const handler of this.handlers.complete) {
          handler({ type: "complete", traces: this.routedTraces })
        }
        return
      }
      const startedAt = performance.now()
      while (
        performance.now() - startedAt < 150 &&
        !this.solver.solved &&
        !this.solver.failed
      ) {
        const asyncSolver = this.solver as typeof this.solver & {
          stepAsync?: () => Promise<void>
        }
        if (typeof asyncSolver.stepAsync === "function") {
          await asyncSolver.stepAsync()
        } else {
          this.solver.step()
        }
      }
      this.steps += 1
      if (this.solver.failed) {
        this.isRouting = false
        this.emitError(
          this.solver.error ??
            `Pipeline 7 failed during ${this.phase}`,
        )
        return
      }
      if (this.solver.solved) {
        this.acceptPipelineOutput()
        this.timer = setTimeout(() => void this.runChunk(), 0)
        return
      }

      for (const handler of this.handlers.progress) {
        const batchProgress = this.solver.progress ?? 0
        handler({
          type: "progress",
          steps: this.steps,
          progress:
            (this.batchIndex + batchProgress) /
            Math.max(this.connectionBatches.length, 1),
          phase: `${this.phase}:pipeline7:batch-${this.batchIndex + 1}/` +
            `${this.connectionBatches.length}:` +
            `${this.connectionBatches[this.batchIndex]?.length ?? 0}-branches:` +
            this.solver.getCurrentPhase(),
          debugGraphics: this.solver.preview?.(),
        })
      }
      this.timer = setTimeout(() => void this.runChunk(), 0)
    } catch (error) {
      this.isRouting = false
      this.emitError(error)
    }
  }

  start(): void {
    if (this.isRouting) return
    this.isRouting = true
    void this.runChunk()
  }

  stop(): void {
    this.isRouting = false
    ;(this.solver as
      | (AutoroutingPipelineSolver7_MultiGraph & { stop?: () => void })
      | undefined)?.stop?.()
    if (this.timer) clearTimeout(this.timer)
    this.timer = undefined
  }

  on(event: "complete", callback: (event: AutorouterCompleteEvent) => void): void
  on(event: "error", callback: (event: AutorouterErrorEvent) => void): void
  on(event: "progress", callback: (event: AutorouterProgressEvent) => void): void
  on(
    event: "complete" | "error" | "progress",
    callback:
      | ((event: AutorouterCompleteEvent) => void)
      | ((event: AutorouterErrorEvent) => void)
      | ((event: AutorouterProgressEvent) => void),
  ): void {
    ;(this.handlers[event] as Array<(event: any) => void>).push(callback)
  }

  solveSync(): SimplifiedPcbTrace[] {
    while (this.solver) {
      this.solver.solve()
      if (this.solver.failed) {
        throw new Error(
          this.solver.error ?? `Pipeline 7 failed during ${this.phase}`,
        )
      }
      this.acceptPipelineOutput()
    }
    return this.routedTraces
  }

  getOutputSimpleRouteJson(): SimpleRouteJson | undefined {
    if (this.solver || this.batchIndex < this.connectionBatches.length) {
      return undefined
    }
    return {
      ...this.input,
      traces: [...(this.input.traces ?? []), ...this.routedTraces],
    }
  }
}

/**
 * Final selective reroute pass. Pipeline 9 treats the already released TI DDR
 * geometry as immutable preloaded copper and rebuilds every selected
 * non-DDR connection around it. The joint-preload repair and generic length
 * matcher are deliberately omitted: the former is allowed to move the fixed
 * DDR copper, while the latter has no usable non-DDR timing groups and adds
 * minutes without improving DRC.
 */
class Am62lPipeline9RerouteAutorouter implements GenericLocalAutorouter {
  isRouting = false
  private timer: ReturnType<typeof setTimeout> | undefined
  private readonly solver: AutoroutingPipelineSolver9_PreloadedTraceGraph
  private readonly inputConnectionNames: Set<string>
  private readonly handlers: RouterEventHandlers = {
    complete: [],
    error: [],
    progress: [],
  }
  private steps = 0

  constructor(
    public readonly input: SimpleRouteJson,
    effort: number,
    private readonly fixedPhaseTraces: SimplifiedPcbTrace[] = [],
  ) {
    this.inputConnectionNames = new Set(
      input.connections.flatMap((connection) => {
        const extended = connection as typeof connection & {
          __netConnectionName?: string
          __rootConnectionNames?: string[]
        }
        return [
          connection.name,
          connection.rootConnectionName,
          connection.netConnectionName,
          extended.__netConnectionName,
          ...(connection.mergedConnectionNames ?? []),
          ...(extended.__rootConnectionNames ?? []),
        ]
      }).filter((name): name is string => Boolean(name)),
    )
    this.solver = new AutoroutingPipelineSolver9_PreloadedTraceGraph(input as any, {
      effort: Math.max(1, effort),
    })
    this.solver.pipelineDef = this.solver.pipelineDef.filter(
      (step) =>
        step.solverName !== "pipeline9JointDrcRepairSolver" &&
        step.solverName !== "lengthMatchingPostProcessingSolver",
    )
  }

  private getSelectedOutputTraces(): SimplifiedPcbTrace[] {
    return this.solver.getOutputSimplifiedPcbTraces().filter((trace) => {
      const aliases = [
        trace.connection_name,
        trace.pcb_trace_id,
        ...(trace.connectsTo ?? []),
      ]
      return aliases.some((alias) =>
        alias ? this.inputConnectionNames.has(alias) : false,
      )
    })
  }

  private emitError(caught: unknown): void {
    const error = caught instanceof Error ? caught : new Error(String(caught))
    for (const handler of this.handlers.error) {
      handler({ type: "error", error })
    }
  }

  private async runChunk(): Promise<void> {
    if (!this.isRouting) return
    try {
      const startedAt = performance.now()
      while (
        performance.now() - startedAt < 150 &&
        !this.solver.solved &&
        !this.solver.failed
      ) {
        const asyncSolver = this.solver as typeof this.solver & {
          stepAsync?: () => Promise<void>
        }
        if (typeof asyncSolver.stepAsync === "function") {
          await asyncSolver.stepAsync()
        } else {
          this.solver.step()
        }
      }
      this.steps += 1
      if (this.solver.failed) {
        this.isRouting = false
        this.emitError(this.solver.error ?? "Pipeline 9 final reroute failed")
        return
      }
      if (this.solver.solved) {
        this.isRouting = false
        const traces = [
          ...this.fixedPhaseTraces,
          ...this.getSelectedOutputTraces(),
        ]
        for (const handler of this.handlers.complete) {
          handler({ type: "complete", traces })
        }
        return
      }
      for (const handler of this.handlers.progress) {
        handler({
          type: "progress",
          steps: this.steps,
          progress: this.solver.progress ?? 0,
          phase: `mixed:pipeline9:${this.solver.getCurrentPhase()}`,
          debugGraphics: this.solver.preview?.(),
        })
      }
      this.timer = setTimeout(() => void this.runChunk(), 0)
    } catch (caught) {
      this.isRouting = false
      this.emitError(caught)
    }
  }

  start(): void {
    if (this.isRouting) return
    this.isRouting = true
    void this.runChunk()
  }

  stop(): void {
    this.isRouting = false
    if (this.timer) clearTimeout(this.timer)
    this.timer = undefined
  }

  on(event: "complete", callback: (event: AutorouterCompleteEvent) => void): void
  on(event: "error", callback: (event: AutorouterErrorEvent) => void): void
  on(event: "progress", callback: (event: AutorouterProgressEvent) => void): void
  on(
    event: "complete" | "error" | "progress",
    callback:
      | ((event: AutorouterCompleteEvent) => void)
      | ((event: AutorouterErrorEvent) => void)
      | ((event: AutorouterProgressEvent) => void),
  ): void {
    ;(this.handlers[event] as Array<(event: any) => void>).push(callback)
  }

  solveSync(): SimplifiedPcbTrace[] {
    this.solver.solve()
    if (this.solver.failed) {
      throw new Error(this.solver.error ?? "Pipeline 9 final reroute failed")
    }
    return [...this.fixedPhaseTraces, ...this.getSelectedOutputTraces()]
  }

  getOutputSimpleRouteJson(): SimpleRouteJson | undefined {
    if (!this.solver.solved) return undefined
    return {
      ...this.input,
      traces: [
        ...(this.input.traces ?? []),
        ...this.getSelectedOutputTraces(),
      ],
    }
  }
}

export const createAm62lClassifiedAutorouter = ({
  phase,
  effort = 2,
}: {
  phase: Am62lRoutingPhase
  effort?: number
}) =>
  async (simpleRouteJson: SimpleRouteJson): Promise<GenericLocalAutorouter> => {
    if (phase === "ddr") return new TiEvmDdrAutorouter(simpleRouteJson)
    if (phase === "power") {
      const denseGroundPrepared = prepareTiGroundPlaneFanout(simpleRouteJson, {
        denseBga: true,
        peripheral: false,
      })
      const routingInput: SimpleRouteJson = {
        ...denseGroundPrepared.input,
        // The TI/peripheral ground fanout is emitted by this phase. Keep it
        // out of the preloaded list to avoid duplicating the same trace IDs in
        // the transformed phase output, while still passing it as committed
        // copper to the branch planner below.
        traces: simpleRouteJson.traces,
      }
      const globalTopologyInput = useGlobalTopologyForDenseComponents(
        routingInput,
      )
      const branchedConnections = expandConnectionsIntoMinimumSpanBranches(
        globalTopologyInput.connections as CapacityRouteConnection[],
      )
      const customPrepared = prepareCustomBranches(
        globalTopologyInput,
        branchedConnections,
        denseGroundPrepared.traces,
        "power",
      )
      const peripheralGroundPrepared = prepareTiGroundPlaneFanout(
        {
          ...simpleRouteJson,
          traces: [
            ...((simpleRouteJson.traces ?? []) as SimplifiedPcbTrace[]),
            ...customPrepared.traces,
          ],
        },
        { denseBga: false, peripheral: true },
      )
      const preparedInput: SimpleRouteJson = {
        ...globalTopologyInput,
        connections: orderAm62lConnections(
          customPrepared.connections,
        ) as SimpleRouteJson["connections"],
      }
      return new Am62lClassifiedAutorouter(
        preparedInput,
        phase,
        effort,
        [...customPrepared.traces, ...peripheralGroundPrepared.traces],
      )
    }
    if (phase === "sd") {
      const preparedEscapes = prepareTiMmc1Escapes(simpleRouteJson)
      const globalTopologyInput = useGlobalTopologyForDenseComponents(
        preparedEscapes.input,
      )
      const branchedConnections = expandConnectionsIntoMinimumSpanBranches(
        globalTopologyInput.connections as CapacityRouteConnection[],
      )
      const branchNamesByLogical = new Map<string, string[]>()
      for (const connection of branchedConnections) {
        const logical = connection.__originalConnectionName ?? connection.name
        const branchNames = branchNamesByLogical.get(logical) ?? []
        branchNames.push(connection.name)
        branchNamesByLogical.set(logical, branchNames)
      }
      const escapeTraces = preparedEscapes.escapeTraces.map((trace) => ({
        ...trace,
        connectsTo: [
          ...(trace.connectsTo ?? []),
          ...(branchNamesByLogical.get(trace.connection_name ?? "") ?? []),
        ],
      }))
      const trunkPrepared = prepareSdInner5Trunks(
        globalTopologyInput,
        branchedConnections,
        escapeTraces,
      )
      const customPrepared = prepareCustomBranches(
        globalTopologyInput,
        trunkPrepared.connections,
        trunkPrepared.traces,
        "sd",
      )
      const preparedInput: SimpleRouteJson = {
        ...globalTopologyInput,
        connections: orderAm62lConnections(
          customPrepared.connections,
        ) as SimpleRouteJson["connections"],
      }
      return new Am62lClassifiedAutorouter(
        preparedInput,
        phase,
        effort,
        customPrepared.traces,
      )
    }
    if (
      phase === "reset-control" ||
      phase === "usb-power"
    ) {
      const globalTopologyInput = useGlobalTopologyForDenseComponents(
        simpleRouteJson,
      )
      const branchedConnections = expandConnectionsIntoMinimumSpanBranches(
        globalTopologyInput.connections as CapacityRouteConnection[],
      )
      const customPrepared = prepareCustomBranches(
        globalTopologyInput,
        branchedConnections,
        [],
        phase,
      )
      const preparedInput: SimpleRouteJson = {
        ...globalTopologyInput,
        connections: orderAm62lConnections(
          customPrepared.connections,
        ) as SimpleRouteJson["connections"],
      }
      return new Am62lClassifiedAutorouter(
        preparedInput,
        phase,
        effort,
        customPrepared.traces,
      )
    }
    return new Am62lPipeline9RerouteAutorouter(simpleRouteJson, effort)
  }