imrishabh18/pedometer
This code defines and assembles a simple radio receiver hardware circuit using specific imported capacitors, inductors, RF connectors, and oscillator components with precise footprints and schematic attributes.
- Version
- 1.1.3
- License
- unset
- Stars
- 0
scripts/routing-style-report.ts
import fs from "node:fs";
import path from "node:path";
/** Usage: bun scripts/routing-style-report.ts baseline/circuit.json current/circuit.json
* Writes JSON to stdout. This measures routed copper appearance, not DRC or
* electrical equivalence. Copper pours and ratsnest connections are excluded.
*/
const filePaths = process.argv.slice(2);
if (!filePaths.length) {
console.error("Usage: bun scripts/routing-style-report.ts <circuit.json> [<circuit.json> ...]");
process.exit(2);
}
const minimumLongSegmentMm = 0.5;
const angleToleranceDegrees = 1;
const zeroLengthToleranceMm = 1e-9;
const round = (value: number) => Math.round(value * 1e6) / 1e6;
type SegmentTotals = {
copperLengthMm: number;
segments: number;
longSegments: number;
non45Segments: number;
non45CopperLengthMm: number;
longNon45Segments: number;
longNon45CopperLengthMm: number;
};
const emptyTotals = (): SegmentTotals => ({
copperLengthMm: 0, segments: 0, longSegments: 0,
non45Segments: 0, non45CopperLengthMm: 0,
longNon45Segments: 0, longNon45CopperLengthMm: 0,
});
function accumulate(totals: SegmentTotals, length: number, deviation: number) {
totals.copperLengthMm += length;
totals.segments++;
const isLong = length > minimumLongSegmentMm;
if (isLong) totals.longSegments++;
if (deviation <= angleToleranceDegrees) return;
totals.non45Segments++;
totals.non45CopperLengthMm += length;
if (isLong) {
totals.longNon45Segments++;
totals.longNon45CopperLengthMm += length;
}
}
function finish(totals: SegmentTotals) {
return {
...Object.fromEntries(Object.entries(totals).map(([key, value]) => [key, round(value)])),
longNon45SegmentPercent: round(totals.longSegments
? 100 * totals.longNon45Segments / totals.longSegments : 0),
non45CopperLengthPercent: round(totals.copperLengthMm
? 100 * totals.non45CopperLengthMm / totals.copperLengthMm : 0),
};
}
function analyze(file: string) {
const circuit: any[] = JSON.parse(fs.readFileSync(file, "utf8"));
if (!Array.isArray(circuit)) throw new Error(`${file}: expected a Circuit JSON array`);
const netNames = new Map(circuit.filter(e => e.type === "source_net")
.map(e => [e.source_net_id, e.name]));
const sourceTraces = new Map(circuit.filter(e => e.type === "source_trace")
.map(e => [e.source_trace_id, e]));
const traces = circuit.filter(e => e.type === "pcb_trace");
const totals = emptyTotals();
const layers = new Map<string, SegmentTotals>();
const nets = new Map<string, SegmentTotals>();
for (const trace of traces) {
const source = sourceTraces.get(trace.source_trace_id);
const netName = (source?.connected_source_net_ids ?? [])
.map((id: string) => netNames.get(id) ?? id).join(", ")
|| netNames.get(trace.connection_name) || trace.connection_name || "unnamed";
const netTotals = nets.get(netName) ?? emptyTotals();
nets.set(netName, netTotals);
const route = trace.route ?? [];
for (let i = 1; i < route.length; i++) {
const start = route[i - 1], end = route[i];
// A planar copper segment is represented by consecutive wire points.
// Via and through-pad transitions have no planar routing angle.
if (start.route_type !== "wire" || end.route_type !== "wire"
|| start.layer !== end.layer) continue;
const dx = end.x - start.x, dy = end.y - start.y;
const length = Math.hypot(dx, dy);
if (!Number.isFinite(length)) throw new Error(`${file}: invalid coordinates in ${trace.pcb_trace_id}`);
if (length <= zeroLengthToleranceMm) continue;
const angle = Math.atan2(dy, dx) * 180 / Math.PI;
const deviation = Math.abs(angle - 45 * Math.round(angle / 45));
const layerTotals = layers.get(start.layer) ?? emptyTotals();
layers.set(start.layer, layerTotals);
for (const target of [totals, layerTotals, netTotals]) accumulate(target, length, deviation);
}
}
const board = circuit.find(e => e.type === "pcb_board");
return {
file: path.resolve(file),
boardSizeMm: board ? { width: board.width, height: board.height } : null,
components: circuit.filter(e => e.type === "pcb_component").length,
traces: traces.length,
vias: circuit.filter(e => e.type === "pcb_via").length,
...finish(totals),
layers: Object.fromEntries([...layers].sort(([a], [b]) => a.localeCompare(b))
.map(([layer, values]) => [layer, finish(values)])),
nets: Object.fromEntries([...nets].sort(([a], [b]) => a.localeCompare(b))
.map(([net, values]) => [net, finish(values)])),
};
}
try {
const reports = filePaths.map(analyze);
console.log(JSON.stringify({
schemaVersion: 1,
definitions: {
minimumLongSegmentMm,
angleToleranceDegrees,
thresholds: "strictly greater than the configured length and angular tolerance",
copperLength: "sum of nonzero consecutive same-layer wire-to-wire segments; excludes pours and vertical transitions",
non45: "deviation from the closest multiple of 45 degrees exceeds the angular tolerance",
vias: "number of pcb_via records",
comparisonScope: "routing appearance only; this is not a DRC or electrical-equivalence check",
},
reports,
}, null, 2));
} catch (error) {
console.error(error instanceof Error ? error.message : String(error));
process.exit(1);
}