AnasSarkiz/usb-c-pd-brushed-motor
USB-C PD-powered motor controller with protected input, selectable 5/9/12 V buck supply, MCU qualification, PWM H-bridge drive, overvoltage sensing, regenerative energy-dump resistors, and safety interlocks.
- Version
- 0.0.1-a48-cloud-pipeline9-unrouted-c41d79a-305b1e7a
- License
- unset
- Stars
- 0
scripts/audit-product-artwork.ts
import { readFile, writeFile } from "node:fs/promises"
import { createHash } from "node:crypto"
import { any_circuit_element } from "circuit-json"
import { z } from "zod"
import { applyToPoint, rotateDEG } from "transformation-matrix"
import { inkBounds, type Bounds } from "./silkscreen-geometry"
const artifact = await readFile("dist/index/circuit.json", "utf8")
const elements = z.array(any_circuit_element).parse(JSON.parse(artifact))
const texts = elements.filter((e) => e.type === "pcb_silkscreen_text")
const issues: string[] = []
const evaluated = texts.map((text) => {
const local = inkBounds({
text: text.text,
fontSize: text.font_size,
x: 0,
y: 0,
})
const matrix = rotateDEG(text.ccw_rotation ?? 0)
const points = [
{ x: local.left, y: local.bottom },
{ x: local.left, y: local.top },
{ x: local.right, y: local.bottom },
{ x: local.right, y: local.top },
].map((p) => applyToPoint(matrix, p))
const bounds = {
left: Math.min(...points.map((p) => p.x)) + text.anchor_position.x,
right: Math.max(...points.map((p) => p.x)) + text.anchor_position.x,
bottom: Math.min(...points.map((p) => p.y)) + text.anchor_position.y,
top: Math.max(...points.map((p) => p.y)) + text.anchor_position.y,
}
if (
bounds.left < -39.5 ||
bounds.right > 39.5 ||
bounds.bottom < -32 ||
bounds.top > 32
)
issues.push(`${text.layer}:${text.text}: ink closer than0.5mm to edge`)
if (text.font_size < 1.2) issues.push(`${text.text}: unqualified small font`)
return { label: text.text, layer: text.layer, bounds }
})
function overlap(a: Bounds, b: Bounds) {
return (
a.left < b.right + 0.15 &&
a.right + 0.15 > b.left &&
a.bottom < b.top + 0.15 &&
a.top + 0.15 > b.bottom
)
}
for (const [index, text] of evaluated.entries())
for (const other of evaluated.slice(index + 1))
if (text.layer === other.layer && overlap(text.bounds, other.bounds))
issues.push(
`${text.layer}:${text.label}/${other.label}: ink overlap/clearance`,
)
for (const text of evaluated)
for (const pad of elements) {
let bounds: Bounds | undefined
if (pad.type === "pcb_smtpad" && pad.layer === text.layer) {
if (pad.shape === "polygon")
bounds = {
left: Math.min(...pad.points.map((p) => p.x)),
right: Math.max(...pad.points.map((p) => p.x)),
bottom: Math.min(...pad.points.map((p) => p.y)),
top: Math.max(...pad.points.map((p) => p.y)),
}
else if (pad.shape === "circle")
bounds = {
left: pad.x - pad.radius,
right: pad.x + pad.radius,
bottom: pad.y - pad.radius,
top: pad.y + pad.radius,
}
else if (pad.shape === "rect" || pad.shape === "pill")
bounds = {
left: pad.x - pad.width / 2,
right: pad.x + pad.width / 2,
bottom: pad.y - pad.height / 2,
top: pad.y + pad.height / 2,
}
else if (pad.shape === "rotated_rect" || pad.shape === "rotated_pill") {
const matrix = rotateDEG(pad.ccw_rotation)
const corners = [
{ x: -pad.width / 2, y: -pad.height / 2 },
{ x: -pad.width / 2, y: pad.height / 2 },
{ x: pad.width / 2, y: -pad.height / 2 },
{ x: pad.width / 2, y: pad.height / 2 },
].map((p) => applyToPoint(matrix, p))
bounds = {
left: pad.x + Math.min(...corners.map((p) => p.x)),
right: pad.x + Math.max(...corners.map((p) => p.x)),
bottom: pad.y + Math.min(...corners.map((p) => p.y)),
top: pad.y + Math.max(...corners.map((p) => p.y)),
}
} else throw new Error("Unaudited SMT shape")
} else if (pad.type === "pcb_via")
bounds = {
left: pad.x - pad.outer_diameter / 2,
right: pad.x + pad.outer_diameter / 2,
bottom: pad.y - pad.outer_diameter / 2,
top: pad.y + pad.outer_diameter / 2,
}
else if (pad.type === "pcb_hole" && pad.hole_shape === "circle")
bounds = {
left: pad.x - pad.hole_diameter / 2,
right: pad.x + pad.hole_diameter / 2,
bottom: pad.y - pad.hole_diameter / 2,
top: pad.y + pad.hole_diameter / 2,
}
else if (pad.type === "pcb_plated_hole") {
if (pad.shape === "circle")
bounds = {
left: pad.x - pad.outer_diameter / 2,
right: pad.x + pad.outer_diameter / 2,
bottom: pad.y - pad.outer_diameter / 2,
top: pad.y + pad.outer_diameter / 2,
}
else if (pad.shape === "pill") {
const matrix = rotateDEG(pad.ccw_rotation ?? 0)
const corners = [
{ x: -pad.outer_width / 2, y: -pad.outer_height / 2 },
{ x: -pad.outer_width / 2, y: pad.outer_height / 2 },
{ x: pad.outer_width / 2, y: -pad.outer_height / 2 },
{ x: pad.outer_width / 2, y: pad.outer_height / 2 },
].map((p) => applyToPoint(matrix, p))
bounds = {
left: pad.x + Math.min(...corners.map((p) => p.x)),
right: pad.x + Math.max(...corners.map((p) => p.x)),
bottom: pad.y + Math.min(...corners.map((p) => p.y)),
top: pad.y + Math.max(...corners.map((p) => p.y)),
}
} else throw new Error(`Unaudited PTH shape ${pad.shape}`)
}
if (bounds && overlap(text.bounds, bounds))
issues.push(
`${text.layer}:${text.label}: exposed pad/drill ink clearance (${pad.type} ${JSON.stringify(bounds)})`,
)
}
const report = {
revision: "A22",
artifactSha256: createHash("sha256").update(artifact).digest("hex"),
highPrecisionSilkscreenRequired: true,
texts: evaluated,
issues,
}
await writeFile(
"evidence/product-artwork-audit-A22.json",
JSON.stringify(report, null, 2) + "\n",
)
console.log(JSON.stringify({ labels: texts.length, issues }, null, 2))
if (issues.length) process.exitCode = 1