ShiboSoftwareDev/linux-gameboy-advance

Restores PCB assembly metadata by assigning part numbers and 3D CAD models, removing test-point bodies, and validating top-side SMT solder-paste features without altering copper or pads.

Version
1.0.17
License
unset
Stars
0

scripts/reroute-carrier-trace.mjs

import fs from "node:fs"
import {
  MultilayerIjump,
  getObstaclesFromCircuitJson,
  markObstaclesAsConnected,
} from "@tscircuit/infgrid-ijump-astar"
import {
  getFullConnectivityMapFromCircuitJson,
  PcbConnectivityMap,
} from "circuit-json-to-connectivity-map"

const targetId = process.argv[2]
if (!targetId) throw new Error("Usage: node scripts/reroute-carrier-trace.mjs <pcb_trace_id> [routes.json]")
const routeFile = process.argv[3] ?? "work/optimized-carrier-traces.json"
const tailStartIndex = process.argv[4] === undefined ? null : Number(process.argv[4])
const segmentEndIndex = process.argv[5] === undefined ? null : Number(process.argv[5])
const circuit = JSON.parse(fs.readFileSync("dist/index/circuit.json", "utf8"))
const cache = JSON.parse(fs.readFileSync("carrier-route-cache.json", "utf8"))
const traces = JSON.parse(fs.readFileSync(routeFile, "utf8"))
const carrierIds = new Set(cache.traces.map((trace) => trace.pcb_trace_id))
const routeById = new Map(traces.map((trace) => [trace.pcb_trace_id, trace.route]))
const target = circuit.find((element) =>
  element.type === "pcb_trace" && element.pcb_trace_id === targetId)
if (!target) throw new Error(`Missing target pcb trace ${targetId}`)
const editableTarget = traces.find((trace) => trace.pcb_trace_id === targetId)
if (!editableTarget) throw new Error(`Missing editable route ${targetId} in ${routeFile}`)
// Tail repairs intentionally seed the route file with a reviewed prefix. Use
// that prefix instead of the last generated Circuit JSON's route, which may be
// the failed candidate that prompted this repair.
if (tailStartIndex !== null) target.route = structuredClone(editableTarget.route)

// Install the frozen carrier solution into the full board so every existing
// pad, via, and routed segment becomes an obstacle to this one-trace repair.
for (const trace of circuit.filter((element) => element.type === "pcb_trace")) {
  const route = routeById.get(trace.pcb_trace_id)
  if (route) trace.route = structuredClone(route)
}
const synthetic = circuit.filter((element) =>
  element.type !== "pcb_copper_pour" &&
  !(element.type === "pcb_trace" && element.pcb_trace_id === targetId) &&
  !(element.type === "pcb_via" && carrierIds.has(element.pcb_trace_id)))

let viaIndex = 0
for (const trace of circuit.filter((element) =>
  element.type === "pcb_trace" && carrierIds.has(element.pcb_trace_id) &&
  element.pcb_trace_id !== targetId)) {
  for (const point of trace.route.filter((point) => point.route_type === "via")) {
    synthetic.push({
      type: "pcb_via",
      pcb_via_id: `reroute_obstacle_via_${viaIndex++}`,
      pcb_trace_id: trace.pcb_trace_id,
      x: point.x,
      y: point.y,
      hole_diameter: point.via_hole_diameter ?? 0.3,
      outer_diameter: point.via_diameter ?? 0.45,
      layers: ["top", "inner1", "inner2", "bottom"],
      from_layer: point.from_layer,
      to_layer: point.to_layer,
      subcircuit_id: trace.subcircuit_id,
    })
  }
}

const connMap = getFullConnectivityMapFromCircuitJson(synthetic)
// The iJump package's stock obstacle converter rejects diagonal copper. Build
// accurate-enough axis-aligned envelopes for routed wire segments, while its
// converter handles pads, holes, vias, and component bodies.
const obstacleSoup = synthetic.filter((element) => element.type !== "pcb_trace")
const obstacles = getObstaclesFromCircuitJson(obstacleSoup, connMap)
for (const trace of synthetic.filter((element) => element.type === "pcb_trace")) {
  for (let index = 0; index < trace.route.length - 1; index++) {
    const a = trace.route[index]
    const b = trace.route[index + 1]
    if (a.route_type !== "wire" || b.route_type !== "wire" || a.layer !== b.layer) continue
    const width = Math.max(a.width ?? 0.12, b.width ?? 0.12)
    obstacles.push({
      type: "rect",
      center: { x: (a.x + b.x) / 2, y: (a.y + b.y) / 2 },
      width: Math.abs(a.x - b.x) + width,
      height: Math.abs(a.y - b.y) + width,
      layers: [a.layer],
      connectedTo: [trace.source_trace_id, trace.pcb_trace_id].filter(Boolean),
    })
  }
}
let pointsToConnect = target.connectsTo.map((pcbPortId) => {
  const port = circuit.find((element) =>
    element.type === "pcb_port" && element.pcb_port_id === pcbPortId)
  if (!port) throw new Error(`Missing endpoint ${pcbPortId}`)
  return {
    x: port.x,
    y: port.y,
    layer: port.layers?.[0] ?? "top",
    pcb_port_id: port.pcb_port_id,
  }
})
if (tailStartIndex !== null) {
  const start = target.route[tailStartIndex]
  if (!start || start.route_type !== "wire")
    throw new Error(`Tail start ${tailStartIndex} must be a wire route point`)
  pointsToConnect = [
    { x: start.x, y: start.y, layer: start.layer },
    segmentEndIndex === null
      ? pointsToConnect.at(-1)
      : (() => {
          const end = target.route[segmentEndIndex]
          if (!end || end.route_type !== "wire")
            throw new Error(`Segment end ${segmentEndIndex} must be a wire route point`)
          return { x: end.x, y: end.y, layer: end.layer }
        })(),
  ]
}
markObstaclesAsConnected(obstacles, pointsToConnect, target.source_trace_id)
const input = {
  layerCount: 4,
  minTraceWidth: 0.12,
  obstacles,
  connections: [{ name: target.source_trace_id, pointsToConnect }],
  bounds: { minX: -72.25, maxX: 72.25, minY: -41, maxY: 41 },
}
console.log(JSON.stringify({
  targetId,
  connection: input.connections[0],
  obstacles: input.obstacles.length,
  bounds: input.bounds,
}, null, 2))

const autorouter = new MultilayerIjump({
  input,
  connMap,
  pcbConnMap: new PcbConnectivityMap(synthetic),
  GRID_STEP: 0.05,
  OBSTACLE_MARGIN: 0.22,
  MAX_ITERATIONS: 2_000_000,
  VIA_COST: 8,
  isRemovePathLoopsEnabled: true,
  isShortenPathWithShortcutsEnabled: true,
  optimizeWithGoalBoxes: false,
})
autorouter.VIA_DIAMETER = 0.45
const solved = autorouter.solveAndMapToTraces()
if (solved.length !== 1) throw new Error(`Router returned ${solved.length} traces: ${autorouter.debugMessage}`)
let route = solved[0].route.map((point) => point.route_type === "via"
  ? { ...point, via_diameter: 0.45, via_hole_diameter: 0.3 }
  : { ...point, width: 0.12 })
// Circuit JSON requires a wire vertex at the via coordinate on both sides.
for (let index = 0; index < route.length; index++) {
  const point = route[index]
  if (point.route_type !== "via") continue
  const previous = route[index - 1]
  if (!previous || previous.x !== point.x || previous.y !== point.y ||
    previous.layer !== point.from_layer) {
    route.splice(index, 0, {
      route_type: "wire", x: point.x, y: point.y,
      width: 0.12, layer: point.from_layer,
    })
    index++
  }
  const next = route[index + 1]
  if (!next || next.x !== point.x || next.y !== point.y ||
    next.layer !== point.to_layer) {
    route.splice(index + 1, 0, {
      route_type: "wire", x: point.x, y: point.y,
      width: 0.12, layer: point.to_layer,
    })
  }
}
const replacement = editableTarget
replacement.route = tailStartIndex === null
  ? route
  : segmentEndIndex === null
    ? [...target.route.slice(0, tailStartIndex), ...route]
    : [
        ...target.route.slice(0, tailStartIndex),
        ...route,
        ...target.route.slice(segmentEndIndex + 1),
      ]
fs.writeFileSync(routeFile, JSON.stringify(traces, null, 2) + "\n")
console.log(`Replaced ${targetId} with ${route.length} route points (${route.filter((p) => p.route_type === "via").length} vias)`)