AnasSarkiz/ble-pedometer

This code defines and configures various electronic hardware components such as surface-mount resistors, crystals, diodes, transistors, connectors, and switches with their physical footprints, schematic symbols, and 3D CAD models for PCB assembly.

Version
1.0.7
License
unset
Stars
0

lib/get-silkscreen-text-issues.ts

import type { AnyCircuitElement, PcbSilkscreenText } from "circuit-json"

// Core emits null ownership for board-level legends, which are not footprints.
type BoardSilkscreenText = Omit<PcbSilkscreenText, "pcb_component_id"> & { pcb_component_id?: string | null }

interface Bounds { minX: number; maxX: number; minY: number; maxY: number }

function centeredBounds(box: { x: number; y: number; width: number; height: number }): Bounds {
  return { minX: box.x - box.width / 2, maxX: box.x + box.width / 2, minY: box.y - box.height / 2, maxY: box.y + box.height / 2 }
}

function pointBounds(points: { x: number; y: number }[]): Bounds {
  if (!points.length) throw new Error("Cannot check empty geometry")
  return { minX: Math.min(...points.map(p => p.x)), maxX: Math.max(...points.map(p => p.x)), minY: Math.min(...points.map(p => p.y)), maxY: Math.max(...points.map(p => p.y)) }
}

function overlaps(a: Bounds, b: Bounds) {
  const clearanceMm = 0.15
  return a.minX < b.maxX + clearanceMm && a.maxX > b.minX - clearanceMm && a.minY < b.maxY + clearanceMm && a.maxY > b.minY - clearanceMm
}

/** Conservative text envelopes, not a substitute for the assembler's CAM check. */
export function getSilkscreenTextIssues(circuitJson: (AnyCircuitElement | BoardSilkscreenText)[]) {
  const obstacles: Bounds[] = []
  for (const element of circuitJson) {
    if (element.type === "pcb_courtyard_outline" && element.layer === "top") obstacles.push(pointBounds(element.outline))
    if (element.type === "pcb_smtpad" && element.layer === "top") {
      if (element.shape === "polygon") obstacles.push(pointBounds(element.points))
      else if (element.shape === "circle") obstacles.push(centeredBounds({ ...element, width: element.radius * 2, height: element.radius * 2 }))
      else if (element.shape === "rotated_rect") {
        // A circumscribed square is conservative for every pad rotation.
        const diameter = Math.hypot(element.width, element.height)
        obstacles.push(centeredBounds({ ...element, width: diameter, height: diameter }))
      } else if (element.shape === "rect") obstacles.push(centeredBounds(element))
      else throw new Error(`Unsupported pad shape: ${element.shape}`)
    }
    if (element.type === "pcb_via") obstacles.push(centeredBounds({ ...element, width: element.outer_diameter, height: element.outer_diameter }))
    if (element.type === "pcb_hole") {
      if (element.hole_shape !== "circle") throw new Error("Unsupported hole shape")
      obstacles.push(centeredBounds({ ...element, width: element.hole_diameter, height: element.hole_diameter }))
    }
    if (element.type === "pcb_plated_hole") {
      const diameter = "outer_diameter" in element ? element.outer_diameter : "outer_width" in element ? Math.hypot(element.outer_width, element.outer_height) : undefined
      if (diameter === undefined) throw new Error("Unsupported plated-hole shape")
      obstacles.push(centeredBounds({ ...element, width: diameter, height: diameter }))
    }
  }
  const issues: string[] = []
  const labels: { text: string; bounds: Bounds }[] = []
  for (const element of circuitJson) {
    if (element.type !== "pcb_silkscreen_text" || element.layer !== "top") continue
    if (element.ccw_rotation !== 0 || element.anchor_alignment !== "center") {
      issues.push(`Unsupported label orientation/alignment: ${element.text}`)
      continue
    }
    // Deliberately larger than the narrow tscircuit2024 glyphs; includes ink height.
    const lines = element.text.split("\n")
    const bounds = centeredBounds({ ...element.anchor_position, width: Math.max(...lines.map(line => line.length)) * element.font_size * 0.7, height: lines.length * element.font_size })
    if (bounds.minX < -29.8 || bounds.maxX > 29.8 || bounds.minY < -18.8 || bounds.maxY > 18.8) issues.push(`Label outside board margin: ${element.text}`)
    if (obstacles.some(obstacle => overlaps(bounds, obstacle))) issues.push(`Label overlaps pad, drill or courtyard: ${element.text}`)
    for (const other of labels) if (overlaps(bounds, other.bounds)) issues.push(`Label overlap: ${element.text} / ${other.text}`)
    labels.push({ text: element.text, bounds })
  }
  return issues
}