astra/am3352-sbc
A 4-layer AM3352 single-board computer with 128‑MiB DDR3, PMIC power regulation, USB‑C 5‑V PD input, dual USB host ports, HDMI output with ESD protection, microSD/UART/JTAG interfaces, addressable RGB indicators, and a PWM-driven buzzer.
- Version
- 0.1.19
- License
- unset
- Stars
- 0
scripts/measure-routing-quality.ts
import type { PcbTrace } from "circuit-json";
type Point = { x: number; y: number };
const distance = (a: Point, b: Point) => Math.hypot(a.x - b.x, a.y - b.y);
const angleDifference = (a: number, b: number) =>
Math.abs(((((a - b + 180) % 360) + 360) % 360) - 180);
/** Measurements use board-world millimeters. Count ordinary octilinear turns,
* excluding curve tessellation, using the published board audit's 0.2° tolerance. */
export function measureRoutingQuality(traces: PcbTrace[]) {
return traces.map((trace) => {
let planarLength = 0;
let ordinaryTurns = 0;
let shortJogs = 0;
let acuteCorners = 0;
const runs: Point[][] = [];
for (let i = 0; i < trace.route.length; i++) {
const p = trace.route[i];
const previous = trace.route[i - 1];
if (p.route_type !== "wire") continue;
if (previous?.route_type === "wire" && previous.layer === p.layer) {
planarLength += distance(previous, p);
if (distance(runs.at(-1)!.at(-1)!, p) > 1e-8) runs.at(-1)!.push(p);
} else runs.push([p]);
}
for (const points of runs) {
const headings = points
.slice(1)
.map(
(p, i) =>
(Math.atan2(p.y - points[i].y, p.x - points[i].x) * 180) / Math.PI,
);
const aligned = headings.map(
(h) => angleDifference(h, Math.round(h / 45) * 45) < 0.2,
);
const turns = new Set<number>();
for (let i = 1; i < headings.length; i++)
if (
aligned[i - 1] &&
aligned[i] &&
angleDifference(headings[i - 1], headings[i]) > 0.2
)
turns.add(i);
for (let i = 1; i < headings.length; i++)
if (angleDifference(headings[i - 1], headings[i]) > 90.2)
acuteCorners++;
ordinaryTurns += turns.size;
for (let i = 1; i < points.length; i++)
if (
turns.has(i - 1) &&
turns.has(i) &&
distance(points[i - 1], points[i]) < 0.25
)
shortJogs++;
}
const start = trace.route[0],
end = trace.route.at(-1)!;
if (start.route_type !== "wire" || end.route_type !== "wire")
throw Error("Expected pad-to-pad traces with wire endpoints");
return {
sourceTraceId: trace.source_trace_id,
planarLength,
detourRatio: planarLength / distance(start, end),
ordinaryTurns,
shortJogs,
acuteCorners,
};
});
}
const pointSegmentDistance = (p: Point, a: Point, b: Point) => {
const squared = (b.x - a.x) ** 2 + (b.y - a.y) ** 2;
const t = squared
? Math.max(
0,
Math.min(
1,
((p.x - a.x) * (b.x - a.x) + (p.y - a.y) * (b.y - a.y)) / squared,
),
)
: 0;
return distance(p, { x: a.x + t * (b.x - a.x), y: a.y + t * (b.y - a.y) });
};
/** Reference-specific audit: each end may use 6.2 mm to approach the shared
* corridor. This is a fixture acceptance limit, not an implicit solver default.
* Continuous DRC separately verifies clearance; sampling verifies coupling. */
export function measurePairInteriorGaps(first: PcbTrace, second: PcbTrace) {
const carrier = (trace: PcbTrace) => {
const vias = trace.route.flatMap((p, i) =>
p.route_type === "via" ? [i] : [],
);
if (vias.length !== 2) throw Error("Expected exactly two local dogbones");
const points = trace.route.slice(vias[0] + 1, vias[1]);
if (!points.every((p) => p.route_type === "wire"))
throw Error("Carrier contains vias");
if (new Set(points.map((p) => p.layer)).size !== 1)
throw Error("Carrier changes layers");
return points;
};
const paths = [carrier(first), carrier(second)];
if (paths[0][0].layer !== paths[1][0].layer)
throw Error("Pair layers differ");
const gaps: number[] = [];
for (let side = 0; side < 2; side++) {
const path = paths[side],
mate = paths[1 - side];
const total = path
.slice(1)
.reduce((n, p, i) => n + distance(path[i], p), 0);
let traveled = 0;
for (let i = 1; i < path.length; i++) {
const a = path[i - 1],
b = path[i],
span = distance(a, b);
const steps = Math.max(1, Math.ceil(span / 0.01));
for (let j = 0; j < steps; j++) {
const at = traveled + (span * j) / steps;
if (at < 6.2 || at > total - 6.2) continue;
const p = {
x: a.x + ((b.x - a.x) * j) / steps,
y: a.y + ((b.y - a.y) * j) / steps,
};
const nearest = Math.min(
...mate.slice(1).map((q, k) => pointSegmentDistance(p, mate[k], q)),
);
gaps.push(nearest - (a.width + mate[0].width) / 2);
}
traveled += span;
}
}
if (!gaps.length) throw Error("No coupled interior to audit");
return { min: Math.min(...gaps), max: Math.max(...gaps) };
}