diff --git a/kicad-query.mjs b/kicad-query.mjs index 83fd14a..5857207 100644 --- a/kicad-query.mjs +++ b/kicad-query.mjs @@ -136,15 +136,6 @@ function collect(node, tag, 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 ────────────────────────────────────────────────────────── function pad_table(rows, sep = ' ') { @@ -175,7 +166,7 @@ function parse_filters(args) { return filters; } -// ── Schematic queries ─────────────────────────────────────────────────────── +// ── Schematic: data functions ──────────────────────────────────────────────── // Placed symbols (not lib_symbols definitions, not power-only, not unit>1 duplicates) function get_placed_symbols(root) { @@ -201,48 +192,29 @@ function get_placed_symbols(root) { function sch_summary(root) { const all_placed = get_placed_symbols(root); - const real = all_placed.filter(s => { - const lib_id = first_str(find_child(s, 'lib_id')) ?? ''; - 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 real = all_placed.filter(s => !(first_str(find_child(s, 'lib_id')) ?? '').startsWith('power:')); + const power_syms = all_placed.filter(s => (first_str(find_child(s, '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 = {}; for (const s of real) { - const lib_id = first_str(find_child(s, 'lib_id')) ?? 'unknown'; - const lib = lib_id.split(':')[0]; + const lib = (first_str(find_child(s, 'lib_id')) ?? 'unknown').split(':')[0]; libs[lib] = (libs[lib] ?? 0) + 1; } - if (Object.keys(libs).length) { - console.log('\nLibraries:'); - for (const [lib, count] of Object.entries(libs).sort()) { - console.log(` ${lib}: ${count}`); - } - } + + return { + components : real.length, + 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 = {}) { const placed = get_placed_symbols(root); let comps = placed - .filter(s => { - const lib_id = first_str(find_child(s, 'lib_id')) ?? ''; - return !lib_id.startsWith('power:'); - }) + .filter(s => !(first_str(find_child(s, 'lib_id')) ?? '').startsWith('power:')) .map(s => ({ ref : get_prop(s, 'Reference') ?? '?', value : get_prop(s, 'Value') ?? '?', @@ -252,223 +224,102 @@ function sch_components(root, filters = {}) { 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.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.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 })); - - 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)' : ''}`); + return { components: comps }; } function sch_component(root, ref) { const placed = get_placed_symbols(root); - // Include all units for this ref - const all_placed_all_units = (root?.items ?? []).filter(item => { - if (item?.tag !== 'symbol') return false; - if (!find_child(item, 'lib_id')) return false; - return get_prop(item, 'Reference') === ref; - }); + const all_units = (root?.items ?? []).filter(item => + item?.tag === 'symbol' && find_child(item, 'lib_id') && get_prop(item, 'Reference') === ref + ); const s = placed.find(s => get_prop(s, 'Reference') === ref); - if (!s && all_placed_all_units.length === 0) { - console.log(`Component '${ref}' not found.`); - return; - } + if (!s && all_units.length === 0) return null; - const target = s ?? all_placed_all_units[0]; - const lib_id = first_str(find_child(target, 'lib_id')) ?? ''; - const at = find_child(target, 'at'); - const pos = at?.items?.slice(0, 2).join(', ') ?? '?'; + const target = s ?? all_units[0]; + const at = find_child(target, 'at'); - console.log(`=== Component: ${ref} ===`); - console.log(`Library/Symbol : ${lib_id}`); - console.log(`Position : (${pos})`); - - // Collect all properties from all units const props = {}; - for (const unit_sym of all_placed_all_units) { - for (const item of (unit_sym?.items ?? [])) { + for (const unit of all_units) { + for (const item of (unit?.items ?? [])) { if (item?.tag === 'property') { - const k = item.items?.[0]; - const v = item.items?.[1]; - if (typeof k === 'string' && typeof v === 'string') { - if (!(k in props)) props[k] = v; - } + const k = item.items?.[0], v = item.items?.[1]; + if (typeof k === 'string' && typeof v === 'string' && !(k in props)) props[k] = v; } } } - console.log('\nProperties:'); - for (const [k, v] of Object.entries(props)) { - if (v && v !== '~') console.log(` ${k}: ${v}`); - } + const pins = all_units.flatMap(u => find_children(u, 'pin')).map(pin => { + const net = find_child(pin, 'net'); + const net_name = net ? (first_str(find_child(net, 'name')) ?? first_str(net)) : null; + return { num: first_str(pin), net: net_name }; + }); - // 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_name = net ? first_str(find_child(net, 'name')) ?? first_str(net) : null; - console.log(` pin ${num}${net_name ? ` → ${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) { const net_labels = collect(root, 'net_label'); const global_labels = collect(root, 'global_label'); - const power_syms = (root?.items ?? []).filter(item => { - if (item?.tag !== 'symbol') return false; - const lib_id = first_str(find_child(item, 'lib_id')) ?? ''; - return lib_id.startsWith('power:'); - }); + 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]); - const all_nets = new Set([...locals, ...globals, ...power]); - const sorted = [...all_nets].sort((a, b) => a.localeCompare(b)); - - console.log(`=== Net Names (${sorted.length} total) ===`); - for (const name of sorted) { - const tags = []; - if (globals.has(name)) tags.push('global'); - if (power.has(name)) tags.push('power'); - console.log(` ${name}${tags.length ? ' [' + tags.join(', ') + ']' : ''}`); - } + return { + nets: [...all].sort().map(name => ({ + name, + global: globals.has(name), + power : power.has(name), + })), + }; } -// ── PCB queries ───────────────────────────────────────────────────────────── +// ── PCB: data functions ────────────────────────────────────────────────────── function pcb_summary(root) { - const nets = find_children(root, 'net'); - const footprints = collect(root, 'footprint'); - const segments = collect(root, 'segment'); - const vias = collect(root, 'via'); - const zones = collect(root, 'zone'); - - // Exclude net 0 (unconnected sentinel) - 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}`); + const real_nets = find_children(root, 'net').filter(n => first_str(n) !== '0'); + return { + nets : real_nets.length, + footprints: collect(root, 'footprint').length, + segments : collect(root, 'segment').length, + vias : collect(root, 'via').length, + zones : collect(root, 'zone').length, + }; } function pcb_nets(root) { - const nets = find_children(root, 'net') - .filter(n => first_str(n) !== '0') - .map(n => ({ id: first_str(n), name: n.items?.[1] ?? '' })) - .sort((a, b) => Number(a.id) - Number(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]), - ]); + return { + 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), + }; } -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) { - // Find net id 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); - console.log(`=== Net: ${name} (id=${net_id}) ===`); - - // Pads on this net - const fps = collect(root, 'footprint'); const connected_pads = []; - for (const fp of fps) { + for (const fp of collect(root, 'footprint')) { const ref = get_prop(fp, 'Reference') ?? '?'; for (const pad of find_children(fp, 'pad')) { 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) { - 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.'); - } + return { name, id: Number(net_id), connected_pads }; +} + +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 ───────────────────────────────────────────────────── @@ -619,161 +499,53 @@ function trace_from(seed_points, net_id, segs, vias) { return { segs: result_segs, vias: result_vias }; } -function pcb_pin_traces(root, ref, pad_num) { - // Find the footprint +function pcb_traces(root, ref, pad_num) { 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]; - - // Find the pad + if (fps.length === 0) return { error: `Footprint '${ref}' not found` }; + const fp = fps[0]; const pads = find_children(fp, 'pad'); - const pad = pad_num - ? pads.find(p => p.items?.[0] === String(pad_num)) - : pads[0]; + const pad = pad_num ? pads.find(p => p.items?.[0] === String(pad_num)) : pads[0]; - if (!pad) { - console.log(`Pad '${pad_num}' not found on '${ref}'.`); - if (pads.length) console.log(`Available pads: ${pads.map(p => p.items?.[0]).join(', ')}`); - return; - } + if (!pad) return { + error : `Pad '${pad_num}' not found on '${ref}'`, + available_pads : pads.map(p => p.items?.[0]), + }; const [abs_x, abs_y] = pad_abs_pos(fp, pad); - const net_node = find_child(pad, 'net'); - const net_id = first_str(net_node) ?? ''; - const net_name = net_node?.items?.[1] ?? '(unconnected)'; + const net_node = find_child(pad, 'net'); + const net_id = first_str(net_node) ?? ''; + 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) - const layers_node = find_child(pad, 'layers'); - 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; } + if (!net_id || net_id === '0') return { + ref, pad: pad.items?.[0], position: { x: abs_x, y: abs_y }, connected: false, + }; const { segs, vias } = build_segment_index(root); 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); - // Group segments by layer - 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, - pad: pad.items?.[0], - net: { id: net_id, name: net_name }, - position: { x: abs_x, y: abs_y }, - traces: found_segs.map(s => ({ - layer: s.layer, - start: { x: s.sx, y: s.sy }, - end : { x: s.ex, y: s.ey }, - width: s.width, - })), - vias: found_vias.map(v => ({ - position: { x: v.x, y: v.y }, - layers : v.layers, - })), - }); - } -} - -// ── JSON output variants ───────────────────────────────────────────────────── - -function sch_components_json(root, filters = {}) { - 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), + return { + ref, + pad : pad.items?.[0], + net : { id: Number(net_id), name: net_name }, + position : { x: abs_x, y: abs_y }, + pad_layers, + traces : found_segs.map(s => ({ + layer: s.layer, + start: { x: s.sx, y: s.sy }, + end : { x: s.ex, y: s.ey }, + width: s.width, })), - }); + vias: found_vias.map(v => ({ position: { x: v.x, y: v.y }, layers: v.layers })), + }; } -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 }); -} +// ── Generic: data functions ────────────────────────────────────────────────── -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) { +function generic_tags(root) { const counts = {}; function walk(node) { if (!node?.items) return; @@ -781,60 +553,237 @@ function query_tags(root) { for (const item of node.items) walk(item); } walk(root); - const sorted = Object.entries(counts).sort((a, b) => b[1] - a[1]); - console.log('=== All tags (by frequency) ==='); - pad_table([ - ['Tag', 'Count'], - ['---', '-----'], - ...sorted.map(([tag, count]) => [tag, count]), - ]); + return { tags: Object.entries(counts).sort((a, b) => b[1] - a[1]).map(([tag, count]) => ({ tag, count })) }; } -function query_raw(root, tag) { - const nodes = collect(root, tag); - if (nodes.length === 0) { console.log(`No nodes with tag '${tag}'.`); return; } - console.log(`=== Raw nodes: ${tag} (${nodes.length}) ===`); - for (const n of nodes) { - // Print a compact one-line representation - const parts = (n.items ?? []).map(item => - typeof item === 'string' - ? item - : item?.items - ? `(${item.tag} ...)` - : String(item) - ); - console.log(` (${n.tag} ${parts.join(' ')})`); - } +function generic_raw(root, tag) { + return { + tag, + nodes: collect(root, tag).map(n => ({ + tag : n.tag, + items: (n.items ?? []).map(item => + typeof item === 'string' ? item : item?.items ? `(${item.tag} ...)` : String(item) + ), + })), + }; } -// ── Project file (.kicad_pro is JSON) ─────────────────────────────────────── - -function query_pro(text, query, args) { +function pro_summary(text) { const proj = JSON.parse(text); + const meta = proj.metadata ?? {}; + const board = proj.board ?? {}; + return { + filename : meta.filename ?? null, + version : meta.version ?? null, + min_clearance: board.design_settings?.rules?.min_clearance ?? null, + sections : Object.keys(proj), + }; +} + +// ── Query dispatcher (returns data) ───────────────────────────────────────── + +function run_query(ext, query, root_or_text, args) { + const filters = parse_filters(args); + + 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'] }; + } + } + + const root = root_or_text; // parsed S-expression tree switch (query) { - case 'summary': - case undefined: { - const meta = proj.metadata ?? {}; - const board = proj.board ?? {}; - const sch = proj.schematic ?? {}; - console.log('=== KiCad Project ==='); - if (meta.filename) console.log(`File : ${meta.filename}`); - if (meta.version) console.log(`Version : ${meta.version}`); - const design_settings = board.design_settings ?? {}; - if (design_settings.rules?.min_clearance) { - console.log(`Min clearance: ${design_settings.rules.min_clearance}`); - } - console.log('\nRaw sections:', Object.keys(proj).join(', ')); - break; + 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; } - default: - console.log('Project file is JSON. Available queries: summary'); - console.log('Raw JSON keys:', Object.keys(proj).join(', ')); } } -// ── Dispatch ──────────────────────────────────────────────────────────────── +// ── JSON renderer ──────────────────────────────────────────────────────────── + +function render_json(data) { + console.log(JSON.stringify(data, null, 2)); +} + +// ── Entry point ────────────────────────────────────────────────────────────── function usage() { console.log(`Usage: node kicad-query.mjs [--format=json] [args...] @@ -867,13 +816,10 @@ Generic queries (any file): function main() { let argv = process.argv.slice(2); - // Strip --format= option + let format = 'human'; for (let i = argv.length - 1; i >= 0; i--) { const m = argv[i].match(/^--format=(\w+)$/); - if (m) { - OUTPUT_FORMAT = m[1]; - argv.splice(i, 1); - } + if (m) { format = m[1]; argv.splice(i, 1); } } const [file_arg, query_arg, ...rest_args] = argv; @@ -891,80 +837,29 @@ function main() { process.exit(1); } - const ext = extname(file_arg).toLowerCase(); - - // .kicad_pro is JSON - if (ext === '.kicad_pro') { - query_pro(text, query_arg ?? 'summary', rest_args); - return; - } - - // All others are S-expressions - let root; - try { - root = parse_sexp(text); - } catch (e) { - console.error(`Parse error: ${e.message}`); - process.exit(1); - } - + const ext = extname(file_arg).toLowerCase(); 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); + // .kicad_pro stays as text (JSON); everything else gets parsed as S-expression + let root_or_text; + if (ext === '.kicad_pro') { + root_or_text = text; + } else { + try { + root_or_text = parse_sexp(text); + } catch (e) { + console.error(`Parse error: ${e.message}`); + process.exit(1); } - return; } - if (ext === '.kicad_pcb') { - 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; - } + const data = run_query(ext, query, root_or_text, rest_args); - console.error(`Unknown file type '${ext}'. Supported: .kicad_sch, .kicad_pcb, .kicad_pro`); - process.exit(1); + if (format === 'json') { + render_json(data); + } else { + render_human(ext, query, data); + } } main();