Refactor: separate data functions from rendering
Query functions now return plain data objects. Two renderers (render_human, render_json) handle output formatting. A single run_query dispatcher routes to the right data function by file type and query name, then main() calls the appropriate renderer based on --format. Eliminates duplicated *_json variants and the OUTPUT_FORMAT global. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
811
kicad-query.mjs
811
kicad-query.mjs
@@ -136,15 +136,6 @@ function collect(node, tag, acc = []) {
|
|||||||
return acc;
|
return acc;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Output format ───────────────────────────────────────────────────────────
|
|
||||||
// Set by --format=json flag; queries emit via output_*() helpers.
|
|
||||||
|
|
||||||
let OUTPUT_FORMAT = 'human';
|
|
||||||
|
|
||||||
function output_json(data) {
|
|
||||||
console.log(JSON.stringify(data, null, 2));
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Output helpers ──────────────────────────────────────────────────────────
|
// ── Output helpers ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
function pad_table(rows, sep = ' ') {
|
function pad_table(rows, sep = ' ') {
|
||||||
@@ -175,7 +166,7 @@ function parse_filters(args) {
|
|||||||
return filters;
|
return filters;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Schematic queries ───────────────────────────────────────────────────────
|
// ── Schematic: data functions ────────────────────────────────────────────────
|
||||||
|
|
||||||
// Placed symbols (not lib_symbols definitions, not power-only, not unit>1 duplicates)
|
// Placed symbols (not lib_symbols definitions, not power-only, not unit>1 duplicates)
|
||||||
function get_placed_symbols(root) {
|
function get_placed_symbols(root) {
|
||||||
@@ -201,48 +192,29 @@ function get_placed_symbols(root) {
|
|||||||
|
|
||||||
function sch_summary(root) {
|
function sch_summary(root) {
|
||||||
const all_placed = get_placed_symbols(root);
|
const all_placed = get_placed_symbols(root);
|
||||||
const real = all_placed.filter(s => {
|
const real = all_placed.filter(s => !(first_str(find_child(s, 'lib_id')) ?? '').startsWith('power:'));
|
||||||
const lib_id = first_str(find_child(s, 'lib_id')) ?? '';
|
const power_syms = all_placed.filter(s => (first_str(find_child(s, 'lib_id')) ?? '').startsWith('power:'));
|
||||||
return !lib_id.startsWith('power:');
|
|
||||||
});
|
|
||||||
const power_syms = all_placed.filter(s => {
|
|
||||||
const lib_id = first_str(find_child(s, 'lib_id')) ?? '';
|
|
||||||
return lib_id.startsWith('power:');
|
|
||||||
});
|
|
||||||
|
|
||||||
const net_labels = collect(root, 'net_label');
|
|
||||||
const global_labels = collect(root, 'global_label');
|
|
||||||
const wires = collect(root, 'wire');
|
|
||||||
|
|
||||||
console.log('=== Schematic Summary ===');
|
|
||||||
console.log(`Components : ${real.length}`);
|
|
||||||
console.log(`Power nets : ${power_syms.length} power symbols`);
|
|
||||||
console.log(`Net labels : ${net_labels.length}`);
|
|
||||||
console.log(`Global labels: ${global_labels.length}`);
|
|
||||||
console.log(`Wires : ${wires.length}`);
|
|
||||||
|
|
||||||
// Library breakdown
|
|
||||||
const libs = {};
|
const libs = {};
|
||||||
for (const s of real) {
|
for (const s of real) {
|
||||||
const lib_id = first_str(find_child(s, 'lib_id')) ?? 'unknown';
|
const lib = (first_str(find_child(s, 'lib_id')) ?? 'unknown').split(':')[0];
|
||||||
const lib = lib_id.split(':')[0];
|
|
||||||
libs[lib] = (libs[lib] ?? 0) + 1;
|
libs[lib] = (libs[lib] ?? 0) + 1;
|
||||||
}
|
}
|
||||||
if (Object.keys(libs).length) {
|
|
||||||
console.log('\nLibraries:');
|
return {
|
||||||
for (const [lib, count] of Object.entries(libs).sort()) {
|
components : real.length,
|
||||||
console.log(` ${lib}: ${count}`);
|
power_symbols : power_syms.length,
|
||||||
}
|
net_labels : collect(root, 'net_label').length,
|
||||||
}
|
global_labels : collect(root, 'global_label').length,
|
||||||
|
wires : collect(root, 'wire').length,
|
||||||
|
libraries : Object.fromEntries(Object.entries(libs).sort()),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function sch_components(root, filters = {}) {
|
function sch_components(root, filters = {}) {
|
||||||
const placed = get_placed_symbols(root);
|
const placed = get_placed_symbols(root);
|
||||||
let comps = placed
|
let comps = placed
|
||||||
.filter(s => {
|
.filter(s => !(first_str(find_child(s, 'lib_id')) ?? '').startsWith('power:'))
|
||||||
const lib_id = first_str(find_child(s, 'lib_id')) ?? '';
|
|
||||||
return !lib_id.startsWith('power:');
|
|
||||||
})
|
|
||||||
.map(s => ({
|
.map(s => ({
|
||||||
ref : get_prop(s, 'Reference') ?? '?',
|
ref : get_prop(s, 'Reference') ?? '?',
|
||||||
value : get_prop(s, 'Value') ?? '?',
|
value : get_prop(s, 'Value') ?? '?',
|
||||||
@@ -252,223 +224,102 @@ function sch_components(root, filters = {}) {
|
|||||||
dnp : first_str(find_child(s, 'dnp')) === 'yes',
|
dnp : first_str(find_child(s, 'dnp')) === 'yes',
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// Apply filters
|
|
||||||
if (filters.ref) comps = comps.filter(c => glob_match(filters.ref, c.ref));
|
if (filters.ref) comps = comps.filter(c => glob_match(filters.ref, c.ref));
|
||||||
if (filters.value) comps = comps.filter(c => glob_match(filters.value, c.value));
|
if (filters.value) comps = comps.filter(c => glob_match(filters.value, c.value));
|
||||||
if (filters.lib) comps = comps.filter(c => glob_match(filters.lib, c.lib_id.split(':')[0]));
|
if (filters.lib) comps = comps.filter(c => glob_match(filters.lib, c.lib_id.split(':')[0]));
|
||||||
if (filters.footprint) comps = comps.filter(c => glob_match(filters.footprint, c.footprint));
|
if (filters.footprint) comps = comps.filter(c => glob_match(filters.footprint, c.footprint));
|
||||||
|
|
||||||
// Sort by reference (alphanumeric)
|
|
||||||
comps.sort((a, b) => a.ref.localeCompare(b.ref, undefined, { numeric: true }));
|
comps.sort((a, b) => a.ref.localeCompare(b.ref, undefined, { numeric: true }));
|
||||||
|
return { components: comps };
|
||||||
if (comps.length === 0) {
|
|
||||||
console.log('No components match.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const header = ['Reference', 'Value', 'Library:Symbol', 'Footprint'];
|
|
||||||
const rows = [header, header.map(h => '-'.repeat(h.length))];
|
|
||||||
for (const c of comps) {
|
|
||||||
rows.push([
|
|
||||||
c.dnp ? `${c.ref}*` : c.ref,
|
|
||||||
c.value,
|
|
||||||
c.lib_id,
|
|
||||||
c.footprint,
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
pad_table(rows);
|
|
||||||
console.log(`\n${comps.length} component(s)${comps.some(c => c.dnp) ? ' (* = DNP)' : ''}`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function sch_component(root, ref) {
|
function sch_component(root, ref) {
|
||||||
const placed = get_placed_symbols(root);
|
const placed = get_placed_symbols(root);
|
||||||
// Include all units for this ref
|
const all_units = (root?.items ?? []).filter(item =>
|
||||||
const all_placed_all_units = (root?.items ?? []).filter(item => {
|
item?.tag === 'symbol' && find_child(item, 'lib_id') && get_prop(item, 'Reference') === ref
|
||||||
if (item?.tag !== 'symbol') return false;
|
);
|
||||||
if (!find_child(item, 'lib_id')) return false;
|
|
||||||
return get_prop(item, 'Reference') === ref;
|
|
||||||
});
|
|
||||||
|
|
||||||
const s = placed.find(s => get_prop(s, 'Reference') === ref);
|
const s = placed.find(s => get_prop(s, 'Reference') === ref);
|
||||||
if (!s && all_placed_all_units.length === 0) {
|
if (!s && all_units.length === 0) return null;
|
||||||
console.log(`Component '${ref}' not found.`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const target = s ?? all_placed_all_units[0];
|
const target = s ?? all_units[0];
|
||||||
const lib_id = first_str(find_child(target, 'lib_id')) ?? '';
|
|
||||||
const at = find_child(target, 'at');
|
const at = find_child(target, 'at');
|
||||||
const pos = at?.items?.slice(0, 2).join(', ') ?? '?';
|
|
||||||
|
|
||||||
console.log(`=== Component: ${ref} ===`);
|
|
||||||
console.log(`Library/Symbol : ${lib_id}`);
|
|
||||||
console.log(`Position : (${pos})`);
|
|
||||||
|
|
||||||
// Collect all properties from all units
|
|
||||||
const props = {};
|
const props = {};
|
||||||
for (const unit_sym of all_placed_all_units) {
|
for (const unit of all_units) {
|
||||||
for (const item of (unit_sym?.items ?? [])) {
|
for (const item of (unit?.items ?? [])) {
|
||||||
if (item?.tag === 'property') {
|
if (item?.tag === 'property') {
|
||||||
const k = item.items?.[0];
|
const k = item.items?.[0], v = item.items?.[1];
|
||||||
const v = item.items?.[1];
|
if (typeof k === 'string' && typeof v === 'string' && !(k in props)) props[k] = v;
|
||||||
if (typeof k === 'string' && typeof v === 'string') {
|
|
||||||
if (!(k in props)) props[k] = v;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('\nProperties:');
|
const pins = all_units.flatMap(u => find_children(u, 'pin')).map(pin => {
|
||||||
for (const [k, v] of Object.entries(props)) {
|
|
||||||
if (v && v !== '~') console.log(` ${k}: ${v}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Pins
|
|
||||||
const pins = all_placed_all_units.flatMap(u => find_children(u, 'pin'));
|
|
||||||
if (pins.length) {
|
|
||||||
console.log(`\nPins (${pins.length}):`);
|
|
||||||
for (const pin of pins) {
|
|
||||||
const num = first_str(pin);
|
|
||||||
const net = find_child(pin, 'net');
|
const net = find_child(pin, 'net');
|
||||||
const net_name = net ? first_str(find_child(net, 'name')) ?? first_str(net) : null;
|
const net_name = net ? (first_str(find_child(net, 'name')) ?? first_str(net)) : null;
|
||||||
console.log(` pin ${num}${net_name ? ` → ${net_name}` : ''}`);
|
return { num: first_str(pin), net: net_name };
|
||||||
}
|
});
|
||||||
}
|
|
||||||
|
return {
|
||||||
|
ref,
|
||||||
|
lib_id : first_str(find_child(target, 'lib_id')) ?? '',
|
||||||
|
position: { x: Number(at?.items?.[0] ?? 0), y: Number(at?.items?.[1] ?? 0) },
|
||||||
|
properties: props,
|
||||||
|
pins,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function sch_nets(root) {
|
function sch_nets(root) {
|
||||||
const net_labels = collect(root, 'net_label');
|
const net_labels = collect(root, 'net_label');
|
||||||
const global_labels = collect(root, 'global_label');
|
const global_labels = collect(root, 'global_label');
|
||||||
const power_syms = (root?.items ?? []).filter(item => {
|
const power_syms = (root?.items ?? []).filter(item =>
|
||||||
if (item?.tag !== 'symbol') return false;
|
item?.tag === 'symbol' && (first_str(find_child(item, 'lib_id')) ?? '').startsWith('power:')
|
||||||
const lib_id = first_str(find_child(item, 'lib_id')) ?? '';
|
);
|
||||||
return lib_id.startsWith('power:');
|
|
||||||
});
|
|
||||||
|
|
||||||
const locals = new Set(net_labels.map(n => first_str(n)).filter(Boolean));
|
const locals = new Set(net_labels.map(n => first_str(n)).filter(Boolean));
|
||||||
const globals = new Set(global_labels.map(n => first_str(n)).filter(Boolean));
|
const globals = new Set(global_labels.map(n => first_str(n)).filter(Boolean));
|
||||||
const power = new Set(power_syms.map(s => get_prop(s, 'Value')).filter(Boolean));
|
const power = new Set(power_syms.map(s => get_prop(s, 'Value')).filter(Boolean));
|
||||||
|
const all = new Set([...locals, ...globals, ...power]);
|
||||||
|
|
||||||
const all_nets = new Set([...locals, ...globals, ...power]);
|
return {
|
||||||
const sorted = [...all_nets].sort((a, b) => a.localeCompare(b));
|
nets: [...all].sort().map(name => ({
|
||||||
|
name,
|
||||||
console.log(`=== Net Names (${sorted.length} total) ===`);
|
global: globals.has(name),
|
||||||
for (const name of sorted) {
|
power : power.has(name),
|
||||||
const tags = [];
|
})),
|
||||||
if (globals.has(name)) tags.push('global');
|
};
|
||||||
if (power.has(name)) tags.push('power');
|
|
||||||
console.log(` ${name}${tags.length ? ' [' + tags.join(', ') + ']' : ''}`);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── PCB queries ─────────────────────────────────────────────────────────────
|
// ── PCB: data functions ──────────────────────────────────────────────────────
|
||||||
|
|
||||||
function pcb_summary(root) {
|
function pcb_summary(root) {
|
||||||
const nets = find_children(root, 'net');
|
const real_nets = find_children(root, 'net').filter(n => first_str(n) !== '0');
|
||||||
const footprints = collect(root, 'footprint');
|
return {
|
||||||
const segments = collect(root, 'segment');
|
nets : real_nets.length,
|
||||||
const vias = collect(root, 'via');
|
footprints: collect(root, 'footprint').length,
|
||||||
const zones = collect(root, 'zone');
|
segments : collect(root, 'segment').length,
|
||||||
|
vias : collect(root, 'via').length,
|
||||||
// Exclude net 0 (unconnected sentinel)
|
zones : collect(root, 'zone').length,
|
||||||
const real_nets = nets.filter(n => first_str(n) !== '0');
|
};
|
||||||
|
|
||||||
console.log('=== PCB Summary ===');
|
|
||||||
console.log(`Nets : ${real_nets.length}`);
|
|
||||||
console.log(`Footprints : ${footprints.length}`);
|
|
||||||
console.log(`Segments : ${segments.length}`);
|
|
||||||
console.log(`Vias : ${vias.length}`);
|
|
||||||
console.log(`Zones : ${zones.length}`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function pcb_nets(root) {
|
function pcb_nets(root) {
|
||||||
const nets = find_children(root, 'net')
|
return {
|
||||||
|
nets: find_children(root, 'net')
|
||||||
.filter(n => first_str(n) !== '0')
|
.filter(n => first_str(n) !== '0')
|
||||||
.map(n => ({ id: first_str(n), name: n.items?.[1] ?? '' }))
|
.map(n => ({ id: Number(first_str(n)), name: n.items?.[1] ?? '' }))
|
||||||
.sort((a, b) => Number(a.id) - Number(b.id));
|
.sort((a, b) => a.id - b.id),
|
||||||
|
};
|
||||||
if (nets.length === 0) { console.log('No nets found.'); return; }
|
|
||||||
|
|
||||||
console.log(`=== PCB Nets (${nets.length}) ===`);
|
|
||||||
pad_table([
|
|
||||||
['ID', 'Net Name'],
|
|
||||||
['--', '--------'],
|
|
||||||
...nets.map(n => [n.id, n.name]),
|
|
||||||
]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function pcb_footprints(root, filters = {}) {
|
|
||||||
let fps = collect(root, 'footprint').map(fp => {
|
|
||||||
const ref = get_prop(fp, 'Reference') ?? '?';
|
|
||||||
const value = get_prop(fp, 'Value') ?? '?';
|
|
||||||
const lib = first_str(fp) ?? '';
|
|
||||||
const layer = first_str(find_child(fp, 'layer')) ?? '';
|
|
||||||
return { ref, value, lib, layer };
|
|
||||||
});
|
|
||||||
|
|
||||||
if (filters.ref) fps = fps.filter(f => glob_match(filters.ref, f.ref));
|
|
||||||
if (filters.value) fps = fps.filter(f => glob_match(filters.value, f.value));
|
|
||||||
|
|
||||||
fps.sort((a, b) => a.ref.localeCompare(b.ref, undefined, { numeric: true }));
|
|
||||||
|
|
||||||
if (fps.length === 0) { console.log('No footprints match.'); return; }
|
|
||||||
|
|
||||||
pad_table([
|
|
||||||
['Reference', 'Value', 'Layer', 'Footprint'],
|
|
||||||
['---------', '-----', '-----', '---------'],
|
|
||||||
...fps.map(f => [f.ref, f.value, f.layer, f.lib]),
|
|
||||||
]);
|
|
||||||
console.log(`\n${fps.length} footprint(s)`);
|
|
||||||
}
|
|
||||||
|
|
||||||
function pcb_footprint(root, ref) {
|
|
||||||
const fps = collect(root, 'footprint').filter(fp => get_prop(fp, 'Reference') === ref);
|
|
||||||
if (fps.length === 0) { console.log(`Footprint '${ref}' not found.`); return; }
|
|
||||||
|
|
||||||
const fp = fps[0];
|
|
||||||
const lib = first_str(fp) ?? '';
|
|
||||||
const layer = first_str(find_child(fp, 'layer')) ?? '';
|
|
||||||
const at = find_child(fp, 'at');
|
|
||||||
const pos = at?.items?.slice(0, 2).join(', ') ?? '?';
|
|
||||||
|
|
||||||
console.log(`=== Footprint: ${ref} ===`);
|
|
||||||
console.log(`Library : ${lib}`);
|
|
||||||
console.log(`Layer : ${layer}`);
|
|
||||||
console.log(`Position : (${pos})`);
|
|
||||||
console.log(`Value : ${get_prop(fp, 'Value') ?? '?'}`);
|
|
||||||
|
|
||||||
// Pads and their nets
|
|
||||||
const pads = find_children(fp, 'pad');
|
|
||||||
if (pads.length) {
|
|
||||||
console.log(`\nPads (${pads.length}):`);
|
|
||||||
pad_table([
|
|
||||||
['Pad', 'Type', 'Net'],
|
|
||||||
['---', '----', '---'],
|
|
||||||
...pads.map(p => {
|
|
||||||
const num = p.items?.[0] ?? '?';
|
|
||||||
const type = p.items?.[1] ?? '?';
|
|
||||||
const net = find_child(p, 'net');
|
|
||||||
const net_name = net?.items?.[1] ?? '';
|
|
||||||
return [num, type, net_name];
|
|
||||||
}),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── PCB net detail ──────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
function pcb_net(root, name) {
|
function pcb_net(root, name) {
|
||||||
// Find net id
|
|
||||||
const net_node = find_children(root, 'net').find(n => n.items?.[1] === name);
|
const net_node = find_children(root, 'net').find(n => n.items?.[1] === name);
|
||||||
if (!net_node) { console.log(`Net '${name}' not found.`); return; }
|
if (!net_node) return null;
|
||||||
|
|
||||||
const net_id = first_str(net_node);
|
const net_id = first_str(net_node);
|
||||||
console.log(`=== Net: ${name} (id=${net_id}) ===`);
|
|
||||||
|
|
||||||
// Pads on this net
|
|
||||||
const fps = collect(root, 'footprint');
|
|
||||||
const connected_pads = [];
|
const connected_pads = [];
|
||||||
for (const fp of fps) {
|
for (const fp of collect(root, 'footprint')) {
|
||||||
const ref = get_prop(fp, 'Reference') ?? '?';
|
const ref = get_prop(fp, 'Reference') ?? '?';
|
||||||
for (const pad of find_children(fp, 'pad')) {
|
for (const pad of find_children(fp, 'pad')) {
|
||||||
const pad_net = find_child(pad, 'net');
|
const pad_net = find_child(pad, 'net');
|
||||||
@@ -477,15 +328,44 @@ function pcb_net(root, name) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
connected_pads.sort((a, b) => a.ref.localeCompare(b.ref, undefined, { numeric: true }));
|
||||||
|
|
||||||
if (connected_pads.length) {
|
return { name, id: Number(net_id), connected_pads };
|
||||||
console.log(`\nConnected pads (${connected_pads.length}):`);
|
|
||||||
for (const { ref, pad } of connected_pads.sort((a, b) => a.ref.localeCompare(b.ref, undefined, { numeric: true }))) {
|
|
||||||
console.log(` ${ref} pad ${pad}`);
|
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
console.log('No pads connected.');
|
function pcb_footprints(root, filters = {}) {
|
||||||
|
let fps = collect(root, 'footprint').map(fp => ({
|
||||||
|
ref : get_prop(fp, 'Reference') ?? '?',
|
||||||
|
value: get_prop(fp, 'Value') ?? '?',
|
||||||
|
lib : first_str(fp) ?? '',
|
||||||
|
layer: first_str(find_child(fp, 'layer')) ?? '',
|
||||||
|
}));
|
||||||
|
if (filters.ref) fps = fps.filter(f => glob_match(filters.ref, f.ref));
|
||||||
|
if (filters.value) fps = fps.filter(f => glob_match(filters.value, f.value));
|
||||||
|
fps.sort((a, b) => a.ref.localeCompare(b.ref, undefined, { numeric: true }));
|
||||||
|
return { footprints: fps };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function pcb_footprint(root, ref) {
|
||||||
|
const fps = collect(root, 'footprint').filter(fp => get_prop(fp, 'Reference') === ref);
|
||||||
|
if (fps.length === 0) return null;
|
||||||
|
|
||||||
|
const fp = fps[0];
|
||||||
|
const at = find_child(fp, 'at');
|
||||||
|
const pads = find_children(fp, 'pad').map(p => ({
|
||||||
|
num : p.items?.[0] ?? '?',
|
||||||
|
type : p.items?.[1] ?? '?',
|
||||||
|
net_name: find_child(p, 'net')?.items?.[1] ?? '',
|
||||||
|
}));
|
||||||
|
|
||||||
|
return {
|
||||||
|
ref,
|
||||||
|
lib : first_str(fp) ?? '',
|
||||||
|
layer : first_str(find_child(fp, 'layer')) ?? '',
|
||||||
|
position: { x: Number(at?.items?.[0] ?? 0), y: Number(at?.items?.[1] ?? 0) },
|
||||||
|
value : get_prop(fp, 'Value') ?? '?',
|
||||||
|
pads,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── PCB geometry helpers ─────────────────────────────────────────────────────
|
// ── PCB geometry helpers ─────────────────────────────────────────────────────
|
||||||
@@ -619,161 +499,53 @@ function trace_from(seed_points, net_id, segs, vias) {
|
|||||||
return { segs: result_segs, vias: result_vias };
|
return { segs: result_segs, vias: result_vias };
|
||||||
}
|
}
|
||||||
|
|
||||||
function pcb_pin_traces(root, ref, pad_num) {
|
function pcb_traces(root, ref, pad_num) {
|
||||||
// Find the footprint
|
|
||||||
const fps = collect(root, 'footprint').filter(fp => get_prop(fp, 'Reference') === ref);
|
const fps = collect(root, 'footprint').filter(fp => get_prop(fp, 'Reference') === ref);
|
||||||
if (fps.length === 0) { console.log(`Footprint '${ref}' not found.`); return; }
|
if (fps.length === 0) return { error: `Footprint '${ref}' not found` };
|
||||||
const fp = fps[0];
|
const fp = fps[0];
|
||||||
|
|
||||||
// Find the pad
|
|
||||||
const pads = find_children(fp, 'pad');
|
const pads = find_children(fp, 'pad');
|
||||||
const pad = pad_num
|
const pad = pad_num ? pads.find(p => p.items?.[0] === String(pad_num)) : pads[0];
|
||||||
? pads.find(p => p.items?.[0] === String(pad_num))
|
|
||||||
: pads[0];
|
|
||||||
|
|
||||||
if (!pad) {
|
if (!pad) return {
|
||||||
console.log(`Pad '${pad_num}' not found on '${ref}'.`);
|
error : `Pad '${pad_num}' not found on '${ref}'`,
|
||||||
if (pads.length) console.log(`Available pads: ${pads.map(p => p.items?.[0]).join(', ')}`);
|
available_pads : pads.map(p => p.items?.[0]),
|
||||||
return;
|
};
|
||||||
}
|
|
||||||
|
|
||||||
const [abs_x, abs_y] = pad_abs_pos(fp, pad);
|
const [abs_x, abs_y] = pad_abs_pos(fp, pad);
|
||||||
const net_node = find_child(pad, 'net');
|
const net_node = find_child(pad, 'net');
|
||||||
const net_id = first_str(net_node) ?? '';
|
const net_id = first_str(net_node) ?? '';
|
||||||
const net_name = net_node?.items?.[1] ?? '(unconnected)';
|
const net_name = net_node?.items?.[1] ?? null;
|
||||||
|
const pad_layers = (find_child(pad, 'layers')?.items ?? [])
|
||||||
|
.filter(i => typeof i === 'string' && i.endsWith('.Cu'));
|
||||||
|
if (!pad_layers.length) pad_layers.push('F.Cu');
|
||||||
|
|
||||||
// Pad layer(s)
|
if (!net_id || net_id === '0') return {
|
||||||
const layers_node = find_child(pad, 'layers');
|
ref, pad: pad.items?.[0], position: { x: abs_x, y: abs_y }, connected: false,
|
||||||
const pad_layers = layers_node?.items?.filter(i => typeof i === 'string').filter(l => l.endsWith('.Cu')) ?? ['F.Cu'];
|
};
|
||||||
|
|
||||||
console.log(`=== Traces from ${ref} pad ${pad.items?.[0]} ===`);
|
|
||||||
console.log(`Net : ${net_name} (id=${net_id})`);
|
|
||||||
console.log(`Position : (${abs_x.toFixed(3)}, ${abs_y.toFixed(3)})`);
|
|
||||||
console.log(`Pad layers : ${pad_layers.join(', ')}`);
|
|
||||||
|
|
||||||
if (!net_id || net_id === '0') { console.log('Pad is unconnected.'); return; }
|
|
||||||
|
|
||||||
const { segs, vias } = build_segment_index(root);
|
const { segs, vias } = build_segment_index(root);
|
||||||
const seeds = pad_layers.map(l => ({ x: abs_x, y: abs_y, layer: l }));
|
const seeds = pad_layers.map(l => ({ x: abs_x, y: abs_y, layer: l }));
|
||||||
const { segs: found_segs, vias: found_vias } = trace_from(seeds, net_id, segs, vias);
|
const { segs: found_segs, vias: found_vias } = trace_from(seeds, net_id, segs, vias);
|
||||||
|
|
||||||
// Group segments by layer
|
return {
|
||||||
const by_layer = {};
|
|
||||||
for (const seg of found_segs) {
|
|
||||||
(by_layer[seg.layer] ??= []).push(seg);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (found_segs.length === 0 && found_vias.length === 0) {
|
|
||||||
console.log('\nNo traces found connected to this pad.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const [layer, layer_segs] of Object.entries(by_layer).sort()) {
|
|
||||||
console.log(`\nLayer ${layer} — ${layer_segs.length} segment(s):`);
|
|
||||||
pad_table([
|
|
||||||
[' From (x, y)', '', 'To (x, y)', '', 'Width'],
|
|
||||||
[' -----------', '', '---------', '', '-----'],
|
|
||||||
...layer_segs.map(s => [
|
|
||||||
` (${s.sx.toFixed(3)},`, `${s.sy.toFixed(3)})`,
|
|
||||||
`→ (${s.ex.toFixed(3)},`, `${s.ey.toFixed(3)})`,
|
|
||||||
`${s.width}mm`,
|
|
||||||
]),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (found_vias.length) {
|
|
||||||
console.log(`\nVias (${found_vias.length}):`);
|
|
||||||
for (const v of found_vias) {
|
|
||||||
console.log(` (${v.x.toFixed(3)}, ${v.y.toFixed(3)}) ${v.layers.join(' ↔ ')}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (OUTPUT_FORMAT === 'json') {
|
|
||||||
output_json({
|
|
||||||
ref,
|
ref,
|
||||||
pad : pad.items?.[0],
|
pad : pad.items?.[0],
|
||||||
net: { id: net_id, name: net_name },
|
net : { id: Number(net_id), name: net_name },
|
||||||
position : { x: abs_x, y: abs_y },
|
position : { x: abs_x, y: abs_y },
|
||||||
|
pad_layers,
|
||||||
traces : found_segs.map(s => ({
|
traces : found_segs.map(s => ({
|
||||||
layer: s.layer,
|
layer: s.layer,
|
||||||
start: { x: s.sx, y: s.sy },
|
start: { x: s.sx, y: s.sy },
|
||||||
end : { x: s.ex, y: s.ey },
|
end : { x: s.ex, y: s.ey },
|
||||||
width: s.width,
|
width: s.width,
|
||||||
})),
|
})),
|
||||||
vias: found_vias.map(v => ({
|
vias: found_vias.map(v => ({ position: { x: v.x, y: v.y }, layers: v.layers })),
|
||||||
position: { x: v.x, y: v.y },
|
};
|
||||||
layers : v.layers,
|
|
||||||
})),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── JSON output variants ─────────────────────────────────────────────────────
|
// ── Generic: data functions ──────────────────────────────────────────────────
|
||||||
|
|
||||||
function sch_components_json(root, filters = {}) {
|
function generic_tags(root) {
|
||||||
const placed = get_placed_symbols(root);
|
|
||||||
let comps = placed
|
|
||||||
.filter(s => !(first_str(find_child(s, 'lib_id')) ?? '').startsWith('power:'))
|
|
||||||
.map(s => ({
|
|
||||||
ref : get_prop(s, 'Reference') ?? '?',
|
|
||||||
value : get_prop(s, 'Value') ?? '?',
|
|
||||||
footprint: get_prop(s, 'Footprint') ?? '',
|
|
||||||
lib_id : first_str(find_child(s, 'lib_id')) ?? '',
|
|
||||||
datasheet: get_prop(s, 'Datasheet') ?? '',
|
|
||||||
dnp : first_str(find_child(s, 'dnp')) === 'yes',
|
|
||||||
}));
|
|
||||||
|
|
||||||
if (filters.ref) comps = comps.filter(c => glob_match(filters.ref, c.ref));
|
|
||||||
if (filters.value) comps = comps.filter(c => glob_match(filters.value, c.value));
|
|
||||||
comps.sort((a, b) => a.ref.localeCompare(b.ref, undefined, { numeric: true }));
|
|
||||||
output_json({ components: comps });
|
|
||||||
}
|
|
||||||
|
|
||||||
function sch_nets_json(root) {
|
|
||||||
const net_labels = collect(root, 'net_label');
|
|
||||||
const global_labels = collect(root, 'global_label');
|
|
||||||
const power_syms = (root?.items ?? []).filter(item =>
|
|
||||||
item?.tag === 'symbol' && (first_str(find_child(item, 'lib_id')) ?? '').startsWith('power:')
|
|
||||||
);
|
|
||||||
|
|
||||||
const locals = new Set(net_labels.map(n => first_str(n)).filter(Boolean));
|
|
||||||
const globals = new Set(global_labels.map(n => first_str(n)).filter(Boolean));
|
|
||||||
const power = new Set(power_syms.map(s => get_prop(s, 'Value')).filter(Boolean));
|
|
||||||
const all = new Set([...locals, ...globals, ...power]);
|
|
||||||
|
|
||||||
output_json({
|
|
||||||
nets: [...all].sort().map(name => ({
|
|
||||||
name,
|
|
||||||
global: globals.has(name),
|
|
||||||
power : power.has(name),
|
|
||||||
})),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function pcb_nets_json(root) {
|
|
||||||
const nets = find_children(root, 'net')
|
|
||||||
.filter(n => first_str(n) !== '0')
|
|
||||||
.map(n => ({ id: Number(first_str(n)), name: n.items?.[1] ?? '' }))
|
|
||||||
.sort((a, b) => a.id - b.id);
|
|
||||||
output_json({ nets });
|
|
||||||
}
|
|
||||||
|
|
||||||
function pcb_footprints_json(root, filters = {}) {
|
|
||||||
let fps = collect(root, 'footprint').map(fp => ({
|
|
||||||
ref : get_prop(fp, 'Reference') ?? '?',
|
|
||||||
value : get_prop(fp, 'Value') ?? '?',
|
|
||||||
lib : first_str(fp) ?? '',
|
|
||||||
layer : first_str(find_child(fp, 'layer')) ?? '',
|
|
||||||
}));
|
|
||||||
if (filters.ref) fps = fps.filter(f => glob_match(filters.ref, f.ref));
|
|
||||||
if (filters.value) fps = fps.filter(f => glob_match(filters.value, f.value));
|
|
||||||
fps.sort((a, b) => a.ref.localeCompare(b.ref, undefined, { numeric: true }));
|
|
||||||
output_json({ footprints: fps });
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Generic / debug queries ─────────────────────────────────────────────────
|
|
||||||
|
|
||||||
function query_tags(root) {
|
|
||||||
const counts = {};
|
const counts = {};
|
||||||
function walk(node) {
|
function walk(node) {
|
||||||
if (!node?.items) return;
|
if (!node?.items) return;
|
||||||
@@ -781,60 +553,237 @@ function query_tags(root) {
|
|||||||
for (const item of node.items) walk(item);
|
for (const item of node.items) walk(item);
|
||||||
}
|
}
|
||||||
walk(root);
|
walk(root);
|
||||||
const sorted = Object.entries(counts).sort((a, b) => b[1] - a[1]);
|
return { tags: Object.entries(counts).sort((a, b) => b[1] - a[1]).map(([tag, count]) => ({ tag, count })) };
|
||||||
console.log('=== All tags (by frequency) ===');
|
|
||||||
pad_table([
|
|
||||||
['Tag', 'Count'],
|
|
||||||
['---', '-----'],
|
|
||||||
...sorted.map(([tag, count]) => [tag, count]),
|
|
||||||
]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function query_raw(root, tag) {
|
function generic_raw(root, tag) {
|
||||||
const nodes = collect(root, tag);
|
return {
|
||||||
if (nodes.length === 0) { console.log(`No nodes with tag '${tag}'.`); return; }
|
tag,
|
||||||
console.log(`=== Raw nodes: ${tag} (${nodes.length}) ===`);
|
nodes: collect(root, tag).map(n => ({
|
||||||
for (const n of nodes) {
|
tag : n.tag,
|
||||||
// Print a compact one-line representation
|
items: (n.items ?? []).map(item =>
|
||||||
const parts = (n.items ?? []).map(item =>
|
typeof item === 'string' ? item : item?.items ? `(${item.tag} ...)` : String(item)
|
||||||
typeof item === 'string'
|
),
|
||||||
? item
|
})),
|
||||||
: item?.items
|
};
|
||||||
? `(${item.tag} ...)`
|
|
||||||
: String(item)
|
|
||||||
);
|
|
||||||
console.log(` (${n.tag} ${parts.join(' ')})`);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Project file (.kicad_pro is JSON) ───────────────────────────────────────
|
function pro_summary(text) {
|
||||||
|
|
||||||
function query_pro(text, query, args) {
|
|
||||||
const proj = JSON.parse(text);
|
const proj = JSON.parse(text);
|
||||||
|
|
||||||
switch (query) {
|
|
||||||
case 'summary':
|
|
||||||
case undefined: {
|
|
||||||
const meta = proj.metadata ?? {};
|
const meta = proj.metadata ?? {};
|
||||||
const board = proj.board ?? {};
|
const board = proj.board ?? {};
|
||||||
const sch = proj.schematic ?? {};
|
return {
|
||||||
console.log('=== KiCad Project ===');
|
filename : meta.filename ?? null,
|
||||||
if (meta.filename) console.log(`File : ${meta.filename}`);
|
version : meta.version ?? null,
|
||||||
if (meta.version) console.log(`Version : ${meta.version}`);
|
min_clearance: board.design_settings?.rules?.min_clearance ?? null,
|
||||||
const design_settings = board.design_settings ?? {};
|
sections : Object.keys(proj),
|
||||||
if (design_settings.rules?.min_clearance) {
|
};
|
||||||
console.log(`Min clearance: ${design_settings.rules.min_clearance}`);
|
|
||||||
}
|
}
|
||||||
console.log('\nRaw sections:', Object.keys(proj).join(', '));
|
|
||||||
break;
|
// ── Query dispatcher (returns data) ─────────────────────────────────────────
|
||||||
}
|
|
||||||
default:
|
function run_query(ext, query, root_or_text, args) {
|
||||||
console.log('Project file is JSON. Available queries: summary');
|
const filters = parse_filters(args);
|
||||||
console.log('Raw JSON keys:', Object.keys(proj).join(', '));
|
|
||||||
|
if (ext === '.kicad_pro') {
|
||||||
|
switch (query) {
|
||||||
|
case 'summary': return pro_summary(root_or_text);
|
||||||
|
default: return { error: `Unknown query '${query}' for .kicad_pro`, available: ['summary'] };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Dispatch ────────────────────────────────────────────────────────────────
|
const root = root_or_text; // parsed S-expression tree
|
||||||
|
|
||||||
|
switch (query) {
|
||||||
|
case 'tags': return generic_tags(root);
|
||||||
|
case 'raw' : return generic_raw(root, args[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ext === '.kicad_sch') {
|
||||||
|
switch (query) {
|
||||||
|
case 'summary' : return sch_summary(root);
|
||||||
|
case 'components': return sch_components(root, filters);
|
||||||
|
case 'component' : return sch_component(root, args[0]) ?? { error: `Component '${args[0]}' not found` };
|
||||||
|
case 'nets' : return sch_nets(root);
|
||||||
|
default: return { error: `Unknown query '${query}' for .kicad_sch`, available: ['summary', 'components', 'component', 'nets', 'tags', 'raw'] };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ext === '.kicad_pcb') {
|
||||||
|
switch (query) {
|
||||||
|
case 'summary' : return pcb_summary(root);
|
||||||
|
case 'nets' : return pcb_nets(root);
|
||||||
|
case 'net' : return pcb_net(root, args[0]) ?? { error: `Net '${args[0]}' not found` };
|
||||||
|
case 'footprints': return pcb_footprints(root, filters);
|
||||||
|
case 'footprint' : return pcb_footprint(root, args[0]) ?? { error: `Footprint '${args[0]}' not found` };
|
||||||
|
case 'traces' : return pcb_traces(root, args[0], args[1]);
|
||||||
|
default: return { error: `Unknown query '${query}' for .kicad_pcb`, available: ['summary', 'nets', 'net', 'footprints', 'footprint', 'traces', 'tags', 'raw'] };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { error: `Unsupported file type '${ext}'` };
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Human renderer ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
function render_human(ext, query, data) {
|
||||||
|
if (data?.error) { console.error(`Error: ${data.error}`); return; }
|
||||||
|
|
||||||
|
// generic
|
||||||
|
if (query === 'tags') {
|
||||||
|
console.log('=== All tags (by frequency) ===');
|
||||||
|
pad_table([['Tag', 'Count'], ['---', '-----'], ...data.tags.map(t => [t.tag, t.count])]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (query === 'raw') {
|
||||||
|
console.log(`=== Raw nodes: ${data.tag} (${data.nodes.length}) ===`);
|
||||||
|
for (const n of data.nodes) console.log(` (${n.tag} ${n.items.join(' ')})`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// .kicad_pro
|
||||||
|
if (ext === '.kicad_pro') {
|
||||||
|
console.log('=== KiCad Project ===');
|
||||||
|
if (data.filename) console.log(`File : ${data.filename}`);
|
||||||
|
if (data.version) console.log(`Version : ${data.version}`);
|
||||||
|
if (data.min_clearance) console.log(`Min clearance: ${data.min_clearance}`);
|
||||||
|
console.log(`Sections : ${data.sections.join(', ')}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// .kicad_sch
|
||||||
|
if (ext === '.kicad_sch') {
|
||||||
|
if (query === 'summary') {
|
||||||
|
console.log('=== Schematic Summary ===');
|
||||||
|
console.log(`Components : ${data.components}`);
|
||||||
|
console.log(`Power symbols: ${data.power_symbols}`);
|
||||||
|
console.log(`Net labels : ${data.net_labels}`);
|
||||||
|
console.log(`Global labels: ${data.global_labels}`);
|
||||||
|
console.log(`Wires : ${data.wires}`);
|
||||||
|
if (Object.keys(data.libraries).length) {
|
||||||
|
console.log('\nLibraries:');
|
||||||
|
for (const [lib, count] of Object.entries(data.libraries)) console.log(` ${lib}: ${count}`);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (query === 'components') {
|
||||||
|
const comps = data.components;
|
||||||
|
if (comps.length === 0) { console.log('No components match.'); return; }
|
||||||
|
const header = ['Reference', 'Value', 'Library:Symbol', 'Footprint'];
|
||||||
|
pad_table([header, header.map(h => '-'.repeat(h.length)),
|
||||||
|
...comps.map(c => [c.dnp ? `${c.ref}*` : c.ref, c.value, c.lib_id, c.footprint])]);
|
||||||
|
console.log(`\n${comps.length} component(s)${comps.some(c => c.dnp) ? ' (* = DNP)' : ''}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (query === 'component') {
|
||||||
|
console.log(`=== Component: ${data.ref} ===`);
|
||||||
|
console.log(`Library/Symbol : ${data.lib_id}`);
|
||||||
|
console.log(`Position : (${data.position.x}, ${data.position.y})`);
|
||||||
|
console.log('\nProperties:');
|
||||||
|
for (const [k, v] of Object.entries(data.properties)) if (v && v !== '~') console.log(` ${k}: ${v}`);
|
||||||
|
if (data.pins.length) {
|
||||||
|
console.log(`\nPins (${data.pins.length}):`);
|
||||||
|
for (const p of data.pins) console.log(` pin ${p.num}${p.net ? ` → ${p.net}` : ''}`);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (query === 'nets') {
|
||||||
|
console.log(`=== Net Names (${data.nets.length} total) ===`);
|
||||||
|
for (const n of data.nets) {
|
||||||
|
const tags = [...(n.global ? ['global'] : []), ...(n.power ? ['power'] : [])];
|
||||||
|
console.log(` ${n.name}${tags.length ? ' [' + tags.join(', ') + ']' : ''}`);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// .kicad_pcb
|
||||||
|
if (ext === '.kicad_pcb') {
|
||||||
|
if (query === 'summary') {
|
||||||
|
console.log('=== PCB Summary ===');
|
||||||
|
console.log(`Nets : ${data.nets}`);
|
||||||
|
console.log(`Footprints : ${data.footprints}`);
|
||||||
|
console.log(`Segments : ${data.segments}`);
|
||||||
|
console.log(`Vias : ${data.vias}`);
|
||||||
|
console.log(`Zones : ${data.zones}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (query === 'nets') {
|
||||||
|
if (data.nets.length === 0) { console.log('No nets found.'); return; }
|
||||||
|
console.log(`=== PCB Nets (${data.nets.length}) ===`);
|
||||||
|
pad_table([['ID', 'Net Name'], ['--', '--------'], ...data.nets.map(n => [n.id, n.name])]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (query === 'net') {
|
||||||
|
console.log(`=== Net: ${data.name} (id=${data.id}) ===`);
|
||||||
|
if (data.connected_pads.length) {
|
||||||
|
console.log(`\nConnected pads (${data.connected_pads.length}):`);
|
||||||
|
for (const { ref, pad } of data.connected_pads) console.log(` ${ref} pad ${pad}`);
|
||||||
|
} else {
|
||||||
|
console.log('No pads connected.');
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (query === 'footprints') {
|
||||||
|
const fps = data.footprints;
|
||||||
|
if (fps.length === 0) { console.log('No footprints match.'); return; }
|
||||||
|
pad_table([['Reference', 'Value', 'Layer', 'Footprint'], ['---------', '-----', '-----', '---------'],
|
||||||
|
...fps.map(f => [f.ref, f.value, f.layer, f.lib])]);
|
||||||
|
console.log(`\n${fps.length} footprint(s)`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (query === 'footprint') {
|
||||||
|
console.log(`=== Footprint: ${data.ref} ===`);
|
||||||
|
console.log(`Library : ${data.lib}`);
|
||||||
|
console.log(`Layer : ${data.layer}`);
|
||||||
|
console.log(`Position : (${data.position.x}, ${data.position.y})`);
|
||||||
|
console.log(`Value : ${data.value}`);
|
||||||
|
if (data.pads.length) {
|
||||||
|
console.log(`\nPads (${data.pads.length}):`);
|
||||||
|
pad_table([['Pad', 'Type', 'Net'], ['---', '----', '---'], ...data.pads.map(p => [p.num, p.type, p.net_name])]);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (query === 'traces') {
|
||||||
|
if (data.connected === false) { console.log(`${data.ref} pad ${data.pad} is unconnected.`); return; }
|
||||||
|
console.log(`=== Traces from ${data.ref} pad ${data.pad} ===`);
|
||||||
|
console.log(`Net : ${data.net.name} (id=${data.net.id})`);
|
||||||
|
console.log(`Position : (${data.position.x.toFixed(3)}, ${data.position.y.toFixed(3)})`);
|
||||||
|
console.log(`Pad layers : ${data.pad_layers.join(', ')}`);
|
||||||
|
if (data.traces.length === 0 && data.vias.length === 0) {
|
||||||
|
console.log('\nNo traces found connected to this pad.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const by_layer = {};
|
||||||
|
for (const seg of data.traces) (by_layer[seg.layer] ??= []).push(seg);
|
||||||
|
for (const [layer, segs] of Object.entries(by_layer).sort()) {
|
||||||
|
console.log(`\nLayer ${layer} — ${segs.length} segment(s):`);
|
||||||
|
pad_table([
|
||||||
|
[' From (x, y)', '', 'To (x, y)', '', 'Width'],
|
||||||
|
[' -----------', '', '---------', '', '-----'],
|
||||||
|
...segs.map(s => [
|
||||||
|
` (${s.start.x.toFixed(3)},`, `${s.start.y.toFixed(3)})`,
|
||||||
|
`→ (${s.end.x.toFixed(3)},`, `${s.end.y.toFixed(3)})`,
|
||||||
|
`${s.width}mm`,
|
||||||
|
]),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
if (data.vias.length) {
|
||||||
|
console.log(`\nVias (${data.vias.length}):`);
|
||||||
|
for (const v of data.vias) console.log(` (${v.position.x.toFixed(3)}, ${v.position.y.toFixed(3)}) ${v.layers.join(' ↔ ')}`);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── JSON renderer ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
function render_json(data) {
|
||||||
|
console.log(JSON.stringify(data, null, 2));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Entry point ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
function usage() {
|
function usage() {
|
||||||
console.log(`Usage: node kicad-query.mjs [--format=json] <file> <query> [args...]
|
console.log(`Usage: node kicad-query.mjs [--format=json] <file> <query> [args...]
|
||||||
@@ -867,13 +816,10 @@ Generic queries (any file):
|
|||||||
function main() {
|
function main() {
|
||||||
let argv = process.argv.slice(2);
|
let argv = process.argv.slice(2);
|
||||||
|
|
||||||
// Strip --format= option
|
let format = 'human';
|
||||||
for (let i = argv.length - 1; i >= 0; i--) {
|
for (let i = argv.length - 1; i >= 0; i--) {
|
||||||
const m = argv[i].match(/^--format=(\w+)$/);
|
const m = argv[i].match(/^--format=(\w+)$/);
|
||||||
if (m) {
|
if (m) { format = m[1]; argv.splice(i, 1); }
|
||||||
OUTPUT_FORMAT = m[1];
|
|
||||||
argv.splice(i, 1);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const [file_arg, query_arg, ...rest_args] = argv;
|
const [file_arg, query_arg, ...rest_args] = argv;
|
||||||
@@ -892,79 +838,28 @@ function main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const ext = extname(file_arg).toLowerCase();
|
const ext = extname(file_arg).toLowerCase();
|
||||||
|
const query = query_arg ?? 'summary';
|
||||||
|
|
||||||
// .kicad_pro is JSON
|
// .kicad_pro stays as text (JSON); everything else gets parsed as S-expression
|
||||||
|
let root_or_text;
|
||||||
if (ext === '.kicad_pro') {
|
if (ext === '.kicad_pro') {
|
||||||
query_pro(text, query_arg ?? 'summary', rest_args);
|
root_or_text = text;
|
||||||
return;
|
} else {
|
||||||
}
|
|
||||||
|
|
||||||
// All others are S-expressions
|
|
||||||
let root;
|
|
||||||
try {
|
try {
|
||||||
root = parse_sexp(text);
|
root_or_text = parse_sexp(text);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(`Parse error: ${e.message}`);
|
console.error(`Parse error: ${e.message}`);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
const query = query_arg ?? 'summary';
|
|
||||||
|
|
||||||
// Generic queries work on any file
|
|
||||||
if (query === 'tags') { query_tags(root); return; }
|
|
||||||
if (query === 'raw') { query_raw(root, rest_args[0]); return; }
|
|
||||||
|
|
||||||
// File-type specific
|
|
||||||
if (ext === '.kicad_sch') {
|
|
||||||
const filters = parse_filters(rest_args);
|
|
||||||
switch (query) {
|
|
||||||
case 'summary': sch_summary(root); break;
|
|
||||||
case 'components':
|
|
||||||
OUTPUT_FORMAT === 'json'
|
|
||||||
? sch_components_json(root, filters)
|
|
||||||
: sch_components(root, filters);
|
|
||||||
break;
|
|
||||||
case 'component': sch_component(root, rest_args[0]); break;
|
|
||||||
case 'nets':
|
|
||||||
OUTPUT_FORMAT === 'json'
|
|
||||||
? sch_nets_json(root)
|
|
||||||
: sch_nets(root);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
console.error(`Unknown query '${query}' for .kicad_sch`);
|
|
||||||
usage();
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ext === '.kicad_pcb') {
|
const data = run_query(ext, query, root_or_text, rest_args);
|
||||||
const filters = parse_filters(rest_args);
|
|
||||||
switch (query) {
|
|
||||||
case 'summary': pcb_summary(root); break;
|
|
||||||
case 'nets':
|
|
||||||
OUTPUT_FORMAT === 'json'
|
|
||||||
? pcb_nets_json(root)
|
|
||||||
: pcb_nets(root);
|
|
||||||
break;
|
|
||||||
case 'net': pcb_net(root, rest_args[0]); break;
|
|
||||||
case 'footprints':
|
|
||||||
OUTPUT_FORMAT === 'json'
|
|
||||||
? pcb_footprints_json(root, filters)
|
|
||||||
: pcb_footprints(root, filters);
|
|
||||||
break;
|
|
||||||
case 'footprint': pcb_footprint(root, rest_args[0]); break;
|
|
||||||
case 'traces': pcb_pin_traces(root, rest_args[0], rest_args[1]); break;
|
|
||||||
default:
|
|
||||||
console.error(`Unknown query '${query}' for .kicad_pcb`);
|
|
||||||
usage();
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
console.error(`Unknown file type '${ext}'. Supported: .kicad_sch, .kicad_pcb, .kicad_pro`);
|
if (format === 'json') {
|
||||||
process.exit(1);
|
render_json(data);
|
||||||
|
} else {
|
||||||
|
render_human(ext, query, data);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
main();
|
main();
|
||||||
|
|||||||
Reference in New Issue
Block a user