ShiboSoftwareDev/muse-sensecap-indicator-replacement

A four-layer ESP32-S3 carrier board providing USB-C 5V input, AMS1117 3.3V regulation, reset/boot/user buttons, status LED, test points, and a 24-pin FFC peripheral interface.

Version
0.0.1
License
unset
Stars
0

src/jlc-autorouter.ts

import {
  SOLVERS,
  type AutorouterCompleteEvent,
  type AutorouterErrorEvent,
  type AutorouterProgressEvent,
  type GenericLocalAutorouter,
  type SimpleRouteJson,
  type SimplifiedPcbTrace,
} from "tscircuit"

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

/**
 * Run tscircuit's multilayer router and normalize every generated through-via
 * to the fabrication requirement: 0.30 mm finished drill, 0.45 mm land.
 */
const interfaceFanoutPositions = Array.from({ length: 10 }, (_, index) => ({
  x: index % 2 === 0 ? -12.2 : -11.0,
  y: 4.75 - index * 0.5,
}))

function normalizeVias(traces: SimplifiedPcbTrace[]): SimplifiedPcbTrace[] {
  return traces.map((trace) => ({
    ...trace,
    route: trace.route
      // A plated fanout hole already spans all four copper layers. Some
      // router solutions add a second via almost concentrically; remove that
      // redundant transition before DRC evaluates the route.
      .filter(
        (point) =>
          point.route_type !== "via" ||
          !interfaceFanoutPositions.some(
            ({ x, y }) => Math.hypot(point.x - x, point.y - y) < 0.4,
          ),
      )
      .map((point) =>
        point.route_type === "via"
          ? {
              ...point,
              via_diameter: 0.45,
              via_hole_diameter: 0.3,
            }
          : point,
      ),
  }))
}

class JlcFourLayerAutorouter implements GenericLocalAutorouter {
  readonly input: SimpleRouteJson
  isRouting = false
  private timer: ReturnType<typeof setTimeout> | undefined
  private cycleCount = 0
  private readonly solver: any
  private readonly handlers: EventHandlers = {
    complete: [],
    error: [],
    progress: [],
  }

  constructor(input: SimpleRouteJson) {
    this.input = input
    this.solver = new SOLVERS.AutoroutingPipelineSolver7_MultiGraph(
      input as any,
      {},
    )
  }

  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 runCycle() {
    if (!this.isRouting) return
    try {
      if (this.solver.failed) {
        this.isRouting = false
        this.emitError(this.solver.error ?? "Routing failed")
        return
      }
      if (this.solver.solved) {
        this.isRouting = false
        const traces = normalizeVias(
          this.solver.getOutputSimpleRouteJson().traces ?? [],
        )
        for (const handler of this.handlers.complete) {
          handler({ type: "complete", traces })
        }
        return
      }

      const startedAt = Date.now()
      const startIterations = this.solver.iterations
      while (
        Date.now() - startedAt < 250 &&
        !this.solver.failed &&
        !this.solver.solved
      ) {
        if (typeof this.solver.stepAsync === "function") {
          await this.solver.stepAsync()
        } else {
          this.solver.step()
        }
      }
      this.cycleCount += 1
      const elapsed = Math.max(Date.now() - startedAt, 1)
      for (const handler of this.handlers.progress) {
        handler({
          type: "progress",
          steps: this.cycleCount,
          progress: this.solver.progress,
          phase: this.solver.getCurrentPhase(),
          iterationsPerSecond:
            ((this.solver.iterations - startIterations) / elapsed) * 1000,
          debugGraphics: this.solver.preview?.(),
        })
      }
      this.timer = setTimeout(() => void this.runCycle(), 0)
    } catch (error) {
      this.isRouting = false
      this.emitError(error)
    }
  }

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

  stop() {
    this.isRouting = false
    this.solver.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: keyof EventHandlers,
    callback:
      | ((event: AutorouterCompleteEvent) => void)
      | ((event: AutorouterErrorEvent) => void)
      | ((event: AutorouterProgressEvent) => void),
  ) {
    ;(this.handlers[event] as Array<(event: never) => void>).push(
      callback as (event: never) => void,
    )
  }

  solveSync() {
    this.solver.solve()
    if (this.solver.failed) {
      throw new Error(this.solver.error ?? "Routing failed")
    }
    return normalizeVias(this.solver.getOutputSimpleRouteJson().traces ?? [])
  }
}

export const createJlcFourLayerAutorouter = async (
  input: SimpleRouteJson,
): Promise<GenericLocalAutorouter> => new JlcFourLayerAutorouter(input)