ShiboSoftwareDev/nema8-20mm-dual-usbc-stepper-controller

A compact PCB component set comprising a pushbutton, logic buffer, Schottky diodes, USB connector, resettable fuse, microcontroller, voltage regulator, and polarized capacitors.

Version
1.0.35
License
unset
Stars
0

scripts/find-accessory-placement.mjs

import fs from "node:fs"
import {
  runAllPlacementChecks,
  checkPadPadClearance,
  checkPadTraceClearance,
  checkViaPadClearance,
  checkEachPcbTraceNonOverlapping,
} from "@tscircuit/checks"

const keepAccessories = process.argv.includes("--keep-accessories")
const keepNames = new Set((process.argv.find((arg) => arg.startsWith("--keep-names="))
  ?.slice("--keep-names=".length) ?? "").split(",").filter(Boolean))
const requestedLayer = process.argv.find((arg) => arg.startsWith("--layer="))?.split("=")[1]
const baseCircuitPath = process.argv.find((arg) => arg.startsWith("--base="))?.slice("--base=".length)
const numericOption = (name, fallback) => Number(
  process.argv.find((arg) => arg.startsWith(`--${name}=`))?.split("=")[1] ?? fallback,
)
const bestCount = numericOption("best", 0)
const excludedTraceIndexes = new Set(
  (process.argv.find((arg) => arg.startsWith("--exclude-traces="))
    ?.slice("--exclude-traces=".length) ?? "")
    .split(",").filter(Boolean).map(Number),
)
const isExcludedTrace = (element) => {
  const ids = `${element.source_trace_id ?? ""} ${element.pcb_trace_id ?? ""}`
    .match(/source_trace_(\d+)/g) ?? []
  return ids.some((id) => excludedTraceIndexes.has(Number(id.slice("source_trace_".length))))
}
const args = process.argv.slice(2).filter((arg) => !arg.startsWith("--"))
const targetName = args[0]
if (!targetName) throw new Error("usage: node scripts/find-accessory-placement.mjs COMPONENT [step]")
const step = Number(args[1] ?? 0.25)
const probeX = args[2] === undefined ? undefined : Number(args[2])
const probeY = args[3] === undefined ? undefined : Number(args[3])
const circuit = JSON.parse(fs.readFileSync("dist/index/circuit.json", "utf8"))
  .filter((element) => !String(element.type).endsWith("error"))

const accessoryNames = new Set([
  "U_TEMP", "R_TEMP_SDA", "R_TEMP_SCL", "C_TEMP", "D_STATUS",
  "C_STATUS", "R_LED_R", "R_LED_G", "R_LED_B", "U_STATUS_BUF", "C_STATUS_BUF", "BZ1", "Q_BUZZER", "D_BUZZER",
  "Q_BZ", "D_BZ", "R_BZ_GATE", "R_BZ_PD", "R_BZ_PWR",
  "R_BUZZER_GATE", "R_BUZZER_PD", "R_BUZZER_SUPPLY", "C_BUZZER",
])
const sourceById = new Map(circuit
  .filter((element) => element.type === "source_component")
  .map((element) => [element.source_component_id, element]))
const componentById = new Map(circuit
  .filter((element) => element.type === "pcb_component")
  .map((element) => [element.pcb_component_id, element]))
const nameForPcbId = (id) => sourceById.get(componentById.get(id)?.source_component_id)?.name
const target = [...componentById.values()].find((component) => nameForPcbId(component.pcb_component_id) === targetName)
if (!target) throw new Error(`missing ${targetName}`)

const removedIds = new Set([...componentById.values()]
  .filter((component) => !keepAccessories &&
    accessoryNames.has(nameForPcbId(component.pcb_component_id)) &&
    !keepNames.has(nameForPcbId(component.pcb_component_id)) &&
    component !== target)
  .map((component) => component.pcb_component_id))
const keptAccessoryIds = new Set([...componentById.values()]
  .filter((component) => (keepAccessories || keepNames.has(nameForPcbId(component.pcb_component_id))) &&
    accessoryNames.has(nameForPcbId(component.pcb_component_id)) && component !== target)
  .map((component) => component.pcb_component_id))
// Placement scans must retain every routed baseline trace.  The old numeric
// source-trace cutoff became stale as schematic traces were added and hid real
// collisions with motor/control routes.  Accessory traces can be omitted
// explicitly with --exclude-traces after they exist.
const isAccessoryTrace = () => false
const targetElements = circuit.filter((element) =>
  element.pcb_component_id === target.pcb_component_id)
const base = baseCircuitPath
  ? [
    ...JSON.parse(fs.readFileSync(baseCircuitPath, "utf8"))
      .filter((element) =>
        !String(element.type).endsWith("error") &&
        element.pcb_component_id !== target.pcb_component_id &&
        !isExcludedTrace(element)),
    ...circuit.filter((element) => keptAccessoryIds.has(element.pcb_component_id)),
    ...targetElements,
  ]
  : circuit.filter((element) =>
    (!element.pcb_component_id || !removedIds.has(element.pcb_component_id)) &&
    !isAccessoryTrace(element) && !isExcludedTrace(element))
const dx0 = target.center.x
const dy0 = target.center.y
const targetPadIds = new Set(base
  .filter((element) => element.pcb_component_id === target.pcb_component_id)
  .map((element) => element.pcb_smtpad_id ?? element.pcb_plated_hole_id)
  .filter(Boolean))
const touchesTarget = (error) =>
  error.pcb_component_id === target.pcb_component_id ||
  error.pcb_component_ids?.includes(target.pcb_component_id) ||
  targetPadIds.has(error.pcb_pad_id) ||
  targetPadIds.has(error.pcb_smtpad_id) ||
  targetPadIds.has(error.pcb_plated_hole_id) ||
  error.pcb_pad_ids?.some((id) => targetPadIds.has(id)) ||
  error.pcb_smtpad_ids?.some((id) => targetPadIds.has(id)) ||
  error.pcb_plated_hole_ids?.some((id) => targetPadIds.has(id)) ||
  JSON.stringify(error).includes(targetName)
const isNonGeometricTargetWarning = (error) =>
  error.message === `${targetName} has no courtyard`

const moved = (x, y) => base.map((element) => {
  if (element.pcb_component_id !== target.pcb_component_id) return element
  const dx = x - dx0
  const dy = y - dy0
  const layerPatch = requestedLayer
    ? {
      ...(element.layer === "top" || element.layer === "bottom" ? { layer: requestedLayer } : {}),
      ...(Array.isArray(element.layers) && element.layers.length === 1 ? { layers: [requestedLayer] } : {}),
    }
    : {}
  if (element.type === "pcb_component") return { ...element, ...layerPatch, center: { x, y } }
  if (typeof element.x === "number" && typeof element.y === "number") {
    return { ...element, ...layerPatch, x: element.x + dx, y: element.y + dy }
  }
  if (element.center) return { ...element, ...layerPatch, center: { x: element.center.x + dx, y: element.center.y + dy } }
  if (Array.isArray(element.points)) return {
    ...element, ...layerPatch,
    points: element.points.map((point) => ({ ...point, x: point.x + dx, y: point.y + dy })),
  }
  if (Array.isArray(element.outline)) return {
    ...element, ...layerPatch,
    outline: element.outline.map((point) => ({ ...point, x: point.x + dx, y: point.y + dy })),
  }
  return element
})

// The trace-overlap checker scales with every pad on every candidate.  For a
// placement probe we only need to test routed copper against the target pads;
// retaining the remaining metadata still lets it resolve trace/net names.
const targetPadTraceCircuit = (candidate) => candidate.filter((element) =>
  !["pcb_smtpad", "pcb_plated_hole"].includes(element.type) ||
  element.pcb_component_id === target.pcb_component_id)

const legal = []
const scored = []
if (probeX !== undefined && probeY !== undefined) {
  const candidate = moved(probeX, probeY)
  const targetTraceCandidate = targetPadTraceCircuit(candidate)
  const errors = [
    ...await runAllPlacementChecks(candidate),
    ...checkPadPadClearance(candidate),
    ...checkPadTraceClearance(targetTraceCandidate),
    ...checkViaPadClearance(candidate),
    ...checkEachPcbTraceNonOverlapping(targetTraceCandidate),
  ]
  console.log(JSON.stringify(errors.filter(touchesTarget).filter((error) => !isNonGeometricTargetWarning(error)), null, 2))
  process.exit(0)
}
for (let y = numericOption("ymin", -9); y <= numericOption("ymax", 9) + 1e-9; y += step) {
  for (let x = numericOption("xmin", -9); x <= numericOption("xmax", 9) + 1e-9; x += step) {
    const cx = Number(x.toFixed(3))
    const cy = Number(y.toFixed(3))
    const candidate = moved(cx, cy)
    const targetTraceCandidate = targetPadTraceCircuit(candidate)
    const errors = [
      ...await runAllPlacementChecks(candidate),
      ...checkPadPadClearance(candidate),
      ...checkPadTraceClearance(targetTraceCandidate),
      ...checkViaPadClearance(candidate),
      ...checkEachPcbTraceNonOverlapping(targetTraceCandidate),
    ]
    const targetErrors = errors.filter(touchesTarget).filter((error) => !isNonGeometricTargetWarning(error))
    scored.push({ x: cx, y: cy, errors: targetErrors.length,
      messages: targetErrors.map((error) => error.message) })
    if (targetErrors.length === 0) legal.push([cx, cy])
  }
}
const best = bestCount > 0
  ? scored.sort((a, b) => a.errors - b.errors).slice(0, bestCount)
  : undefined
console.log(JSON.stringify({ targetName, layer: requestedLayer ?? target.layer, rotation: target.rotation, count: legal.length, legal, best }))