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

A tscircuit package created by ShiboSoftwareDev.

Version
1.0.47
License
unset
Stars
0

scripts/check-trace-cutout-clearance.mjs

import fs from "node:fs"

const circuitPath = process.argv[2] ?? "dist/index/circuit.json"
const requiredClearance = Number(process.env.CUTOUT_CLEARANCE_MM ?? 0.1)
const circuit = JSON.parse(fs.readFileSync(circuitPath, "utf8"))
const cutouts = circuit.filter((element) => element.type === "pcb_cutout")
const holes = circuit.filter((element) => element.type === "pcb_hole")
const holeClearance = Number(process.env.HOLE_CLEARANCE_MM ?? circuit
  .find((element) => element.type === "pcb_board")?.min_trace_to_hole_edge_clearance ?? 0.2)
const obstacles = [...cutouts, ...holes]
const traces = circuit.filter((element) => element.type === "pcb_trace")

const pointSegmentDistance = (point, start, end) => {
  const dx = end.x - start.x
  const dy = end.y - start.y
  const lengthSquared = dx * dx + dy * dy
  const t = lengthSquared === 0 ? 0 : Math.max(0, Math.min(1,
    ((point.x - start.x) * dx + (point.y - start.y) * dy) / lengthSquared))
  return Math.hypot(point.x - (start.x + t * dx), point.y - (start.y + t * dy))
}

const orientation = (a, b, c) =>
  Math.sign((b.x - a.x) * (c.y - a.y) - (b.y - a.y) * (c.x - a.x))
const segmentsIntersect = (a, b, c, d) => {
  const o1 = orientation(a, b, c)
  const o2 = orientation(a, b, d)
  const o3 = orientation(c, d, a)
  const o4 = orientation(c, d, b)
  if (o1 !== o2 && o3 !== o4) return true
  const onSegment = (p, q, r) => orientation(p, q, r) === 0 &&
    q.x >= Math.min(p.x, r.x) - 1e-9 && q.x <= Math.max(p.x, r.x) + 1e-9 &&
    q.y >= Math.min(p.y, r.y) - 1e-9 && q.y <= Math.max(p.y, r.y) + 1e-9
  return onSegment(a, c, b) || onSegment(a, d, b) ||
    onSegment(c, a, d) || onSegment(c, b, d)
}

const pointInPolygon = (point, polygon) => {
  let inside = false
  for (let index = 0, previous = polygon.length - 1;
    index < polygon.length; previous = index++) {
    const a = polygon[index]
    const b = polygon[previous]
    if (((a.y > point.y) !== (b.y > point.y)) &&
      point.x < ((b.x - a.x) * (point.y - a.y)) / (b.y - a.y) + a.x) inside = !inside
  }
  return inside
}

const polygonEdges = (points) => points.map((point, index) =>
  [point, points[(index + 1) % points.length]])
const segmentPolygonDistance = (start, end, polygon) => {
  if (pointInPolygon(start, polygon) || pointInPolygon(end, polygon)) return 0
  const edges = polygonEdges(polygon)
  if (edges.some(([a, b]) => segmentsIntersect(start, end, a, b))) return 0
  return Math.min(...edges.flatMap(([a, b]) => [
    pointSegmentDistance(start, a, b),
    pointSegmentDistance(end, a, b),
    pointSegmentDistance(a, start, end),
    pointSegmentDistance(b, start, end),
  ]))
}

const pointCutoutDistance = (point, cutout) => {
  if (cutout.shape === "circle") {
    return Math.max(0, Math.hypot(point.x - cutout.center.x,
      point.y - cutout.center.y) - cutout.radius)
  }
  if (cutout.shape === "rect") {
    const angle = -((cutout.rotation ?? 0) * Math.PI) / 180
    const dx = point.x - cutout.center.x
    const dy = point.y - cutout.center.y
    const x = dx * Math.cos(angle) - dy * Math.sin(angle)
    const y = dx * Math.sin(angle) + dy * Math.cos(angle)
    return Math.hypot(Math.max(Math.abs(x) - cutout.width / 2, 0),
      Math.max(Math.abs(y) - cutout.height / 2, 0))
  }
  if (cutout.shape === "polygon") {
    if (pointInPolygon(point, cutout.points)) return 0
    return Math.min(...polygonEdges(cutout.points)
      .map(([a, b]) => pointSegmentDistance(point, a, b)))
  }
  return Infinity
}

const segmentCutoutDistance = (start, end, cutout) => {
  if (cutout.shape === "polygon") return segmentPolygonDistance(start, end, cutout.points)
  const distance = Math.hypot(end.x - start.x, end.y - start.y)
  const samples = Math.max(2, Math.ceil(distance / 0.02))
  let minimum = Infinity
  for (let index = 0; index <= samples; index++) {
    const t = index / samples
    minimum = Math.min(minimum, pointCutoutDistance({
      x: start.x + (end.x - start.x) * t,
      y: start.y + (end.y - start.y) * t,
    }, cutout))
  }
  return minimum
}

const holeCenterline = (hole) => {
  if (hole.hole_shape === "circle") {
    return { start: hole, end: hole, radius: hole.hole_diameter / 2 }
  }
  if (!["pill", "rotated_pill"].includes(hole.hole_shape)) {
    throw new Error(`Unsupported NPTH shape: ${hole.hole_shape}`)
  }
  const angle = ((hole.ccw_rotation ?? 0) * Math.PI) / 180 +
    (hole.hole_height > hole.hole_width ? Math.PI / 2 : 0)
  const halfLength = Math.abs(hole.hole_width - hole.hole_height) / 2
  const dx = halfLength * Math.cos(angle)
  const dy = halfLength * Math.sin(angle)
  return {
    start: { x: hole.x - dx, y: hole.y - dy },
    end: { x: hole.x + dx, y: hole.y + dy },
    radius: Math.min(hole.hole_width, hole.hole_height) / 2,
  }
}

const pointObstacleDistance = (point, obstacle) => {
  if (obstacle.type !== "pcb_hole") return pointCutoutDistance(point, obstacle)
  const { start, end, radius } = holeCenterline(obstacle)
  return Math.max(0, pointSegmentDistance(point, start, end) - radius)
}

const segmentObstacleDistance = ({ start, end }, obstacle) => {
  if (obstacle.type !== "pcb_hole") return segmentCutoutDistance(start, end, obstacle)
  const hole = holeCenterline(obstacle)
  if (segmentsIntersect(start, end, hole.start, hole.end)) return 0
  return Math.max(0, Math.min(
    pointSegmentDistance(start, hole.start, hole.end),
    pointSegmentDistance(end, hole.start, hole.end),
    pointSegmentDistance(hole.start, start, end),
    pointSegmentDistance(hole.end, start, end),
  ) - hole.radius)
}

const clearanceFor = (obstacle) => obstacle.type === "pcb_hole" ? holeClearance : requiredClearance
const obstacleId = (obstacle) => obstacle.pcb_hole_id ?? obstacle.pcb_cutout_id

const violations = []
for (const trace of traces) {
  let previous
  for (const point of trace.route ?? []) {
    if (point.route_type === "via") {
      const radius = (point.via_diameter ?? 0.45) / 2
      for (const obstacle of obstacles) {
        const distance = pointObstacleDistance(point, obstacle)
        const clearance = clearanceFor(obstacle)
        if (distance + 1e-9 < radius + clearance) {
          violations.push({ trace: trace.pcb_trace_id, kind: "via", distance,
            obstacle: obstacleId(obstacle), required: radius + clearance,
            point: { x: point.x, y: point.y } })
        }
      }
      previous = undefined
      continue
    }
    if (point.route_type !== "wire") continue
    if (previous?.layer === point.layer) {
      const radius = Math.max(previous.width ?? 0.15, point.width ?? 0.15) / 2
      for (const obstacle of obstacles) {
        const distance = segmentObstacleDistance({ start: previous, end: point }, obstacle)
        const clearance = clearanceFor(obstacle)
        if (distance + 1e-9 < radius + clearance) {
          violations.push({ trace: trace.pcb_trace_id, kind: "wire", layer: point.layer,
            obstacle: obstacleId(obstacle), distance, required: radius + clearance,
            start: { x: previous.x, y: previous.y }, end: { x: point.x, y: point.y } })
        }
      }
    }
    previous = point
  }
}

if (violations.length) {
  console.error(`Trace/hole/cutout clearance check failed: ${violations.length} violation(s)`)
  for (const violation of violations) console.error(JSON.stringify(violation))
  process.exit(1)
}
console.log(`Trace/hole/cutout clearance check passed: ${traces.length} traces, ${holes.length} holes, ${cutouts.length} cutouts; NPTH clearance ${holeClearance.toFixed(2)} mm`)