app
KiCad - the KiCad Bridge
Public Made by Adomby adom
Drive your own KiCad from an AI, in the background: open every editor, read the whole board as data (the EDA Bridge Standard board model), stack-up, part heights, netlists, place, route, pour, DRC and exports.
master
Colby Knox
Merge PR #307: kicad-lane: 1.0.64, live adds through KiCad's protobuf API, #126/#129, 17 standard core verbs
a8fbf9f
14d ago
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287
//! Library housekeeping the EDA Bridge Standard names in its core tier: rename a footprint's
//! text (for copy, rename and cross-library copy of `.kicad_mod` files), clone and rename a symbol
//! in a `.kicad_sym`, set a symbol's parameters and their visibility, and read a symbol's drawing.
//! Every edit goes through the lossless document model, so the rest of the file stays
//! byte-identical, and the result is checked like the other library writers.
use std::collections::BTreeMap;
use serde_json::{json, Map, Value};
use crate::schematic::{parse_node, pretty, quote, Document, Node};
fn symbols(doc: &Document) -> Vec<(usize, String)> {
doc.root.children().iter().enumerate().filter(|(_, c)| c.head() == Some("symbol")).filter_map(|(i, c)| c.arg(0).map(|n| (i, n))).collect()
}
fn library(text: &str) -> Result<Document, String> {
let doc = Document::parse(text)?;
if doc.root.head() != Some("kicad_symbol_lib") {
return Err("not a symbol library (the top-level list is not kicad_symbol_lib)".into());
}
Ok(doc)
}
fn find(doc: &Document, name: &str) -> Result<usize, String> {
let syms = symbols(doc);
syms.iter().find(|(_, n)| n == name).map(|(i, _)| *i).ok_or_else(|| {
let names: Vec<&str> = syms.iter().map(|(_, n)| n.as_str()).take(40).collect();
format!("no symbol {name} in the library (it has: {})", names.join(", "))
})
}
fn checked(doc: &Document, expect: &[&str]) -> Result<String, String> {
let out = doc.to_text();
crate::lint::check_symbol_lib_structure(&out)?;
let listed = crate::libraries::list_symbols_in_text(&out);
for n in expect {
if !listed.iter().any(|l| l == n) {
return Err(format!("{n} does not read back from the edited library"));
}
}
Ok(out)
}
/// A `.kicad_mod`'s text with its footprint named `to` (the name after `(footprint`).
pub fn rename_footprint_text(text: &str, to: &str) -> Result<String, String> {
let mut doc = Document::parse(text)?;
if doc.root.head() != Some("footprint") {
return Err("not a footprint file (the top-level list is not footprint)".into());
}
doc.root.set_arg(0, Node::atom(quote(to)));
Ok(doc.to_text())
}
/// Rename a symbol node and its unit sub-symbols (`<name>_<unit>_<style>`).
fn rename_node(n: &mut Node, from: &str, to: &str) {
n.set_arg(0, Node::atom(quote(to)));
if let Some(kids) = n.children_mut() {
for sub in kids.iter_mut().filter(|c| c.head() == Some("symbol")) {
if let Some(s) = sub.arg(0) {
if let Some(rest) = s.strip_prefix(&format!("{from}_")) {
sub.set_arg(0, Node::atom(quote(&format!("{to}_{rest}"))));
}
}
}
}
}
/// A copy of symbol `from` named `to`, every unit and body style included, placed after it.
pub fn clone_symbol(text: &str, from: &str, to: &str) -> Result<String, String> {
let mut doc = library(text)?;
let i = find(&doc, from)?;
if symbols(&doc).iter().any(|(_, n)| n == to) {
return Err(format!("symbol {to} already exists in the library"));
}
let kids = doc.root.children_mut().ok_or("empty library")?;
let mut copy = kids[i].clone();
rename_node(&mut copy, from, to);
kids.insert(i + 1, copy);
checked(&doc, &[from, to])
}
/// Rename symbol `from` to `to`, and every symbol that extends it with it.
pub fn rename_symbol(text: &str, from: &str, to: &str) -> Result<(String, Vec<String>), String> {
let mut doc = library(text)?;
let i = find(&doc, from)?;
if symbols(&doc).iter().any(|(_, n)| n == to) {
return Err(format!("symbol {to} already exists in the library"));
}
let kids = doc.root.children_mut().ok_or("empty library")?;
rename_node(&mut kids[i], from, to);
let mut children = Vec::new();
for k in kids.iter_mut().filter(|c| c.head() == Some("symbol")) {
if k.child_value("extends").as_deref() == Some(from) {
if let Some(e) = k.child_mut("extends") {
e.set_arg(0, Node::atom(quote(to)));
}
children.extend(k.arg(0));
}
}
Ok((checked(&doc, &[to])?, children))
}
/// A new hidden property block, KiCad 10's layout.
fn new_property(name: &str, value: &str) -> Result<Node, String> {
let mut n = parse_node(&format!(
"(property {} {} (at 0 0 0) (show_name no) (do_not_autoplace no) (hide yes) (effects (font (size 1.27 1.27))))",
quote(name),
quote(value)
))?;
pretty(&mut n, 2);
Ok(n)
}
/// Set (or add, hidden) properties on symbol `name`. Returns the text and per property what
/// happened: "set", "added" or "unchanged".
pub fn set_params(text: &str, name: &str, params: &BTreeMap<String, String>) -> Result<(String, Map<String, Value>), String> {
let mut doc = library(text)?;
let i = find(&doc, name)?;
let sym = &mut doc.root.children_mut().ok_or("empty library")?[i];
let mut what = Map::new();
for (k, v) in params {
let kids = sym.children_mut().ok_or("empty symbol")?;
match kids.iter_mut().find(|c| c.head() == Some("property") && c.arg(0).as_deref() == Some(k.as_str())) {
Some(p) => {
let same = p.arg(1).as_deref() == Some(v.as_str());
if !same {
p.set_arg(1, Node::atom(quote(v)));
}
what.insert(k.clone(), json!(if same { "unchanged" } else { "set" }));
}
None => {
let mut node = new_property(k, v)?;
let at = kids.iter().rposition(|c| c.head() == Some("property")).map(|j| j + 1).unwrap_or(1);
node.lead = kids.get(at.min(kids.len() - 1)).map(|c| c.lead.clone()).unwrap_or_else(|| "\n\t\t".into());
kids.insert(at, node);
what.insert(k.clone(), json!("added"));
}
}
}
Ok((checked(&doc, &[name])?, what))
}
/// Show or hide property `param` of symbol `name`. Returns the text and whether it changed.
pub fn set_visibility(text: &str, name: &str, param: &str, visible: bool) -> Result<(String, bool), String> {
let mut doc = library(text)?;
let i = find(&doc, name)?;
let sym = &mut doc.root.children_mut().ok_or("empty library")?[i];
let kids = sym.children_mut().ok_or("empty symbol")?;
let p = kids.iter_mut().find(|c| c.head() == Some("property") && c.arg(0).as_deref() == Some(param)).ok_or_else(|| format!("{name} has no property {param}"))?;
// KiCad 10 keeps (hide yes) on the property; older files keep it inside (effects ...)
let hidden_now = p.child_value("hide").as_deref() == Some("yes") || p.child("effects").map(|e| e.children().iter().any(|c| c.raw() == Some("hide") || (c.head() == Some("hide") && c.arg(0).as_deref() != Some("no")))).unwrap_or(false);
if hidden_now != visible {
return Ok((text.to_string(), false));
}
let pk = p.children_mut().ok_or("empty property")?;
pk.retain(|c| c.head() != Some("hide"));
if let Some(e) = pk.iter_mut().find(|c| c.head() == Some("effects")) {
if let Some(ek) = e.children_mut() {
ek.retain(|c| c.raw() != Some("hide") && c.head() != Some("hide"));
}
}
if !visible {
let at = pk.iter().position(|c| c.head() == Some("effects")).unwrap_or(pk.len());
let mut h = parse_node("(hide yes)")?;
h.lead = pk.get(at).map(|c| c.lead.clone()).unwrap_or_else(|| "\n\t\t\t".into());
pk.insert(at, h);
}
Ok((checked(&doc, &[name])?, true))
}
fn bbox(points: &[(f64, f64)]) -> Option<[f64; 4]> {
let (mut x0, mut y0, mut x1, mut y1) = (f64::MAX, f64::MAX, f64::MIN, f64::MIN);
for (x, y) in points {
x0 = x0.min(*x);
y0 = y0.min(*y);
x1 = x1.max(*x);
y1 = y1.max(*y);
}
(x0 <= x1).then_some([x0, y0, x1, y1])
}
/// A symbol's drawing: primitives counted by kind, the body rectangle, the polylines' bounding box,
/// units and body styles, and the footprint link and filters. Read-only.
pub fn symbol_prims(text: &str, name: &str) -> Result<Value, String> {
let doc = library(text)?;
find(&doc, name)?;
let flat = crate::schematic::flatten_lib_symbol(text, name)?;
let mut counts: BTreeMap<String, usize> = BTreeMap::new();
let mut rects: Vec<[f64; 4]> = Vec::new();
let mut poly_pts: Vec<(f64, f64)> = Vec::new();
let (mut units, mut styles) = (std::collections::BTreeSet::new(), std::collections::BTreeSet::new());
for sub in flat.children().iter().filter(|c| c.head() == Some("symbol")) {
let sname = sub.arg(0).unwrap_or_default();
let mut it = sname.rsplitn(3, '_');
styles.insert(it.next().and_then(|s| s.parse::<u32>().ok()).unwrap_or(1));
let unit = it.next().and_then(|s| s.parse::<u32>().ok()).unwrap_or(0);
if unit > 0 {
units.insert(unit);
}
for p in sub.children().iter().skip(1) {
let Some(h) = p.head() else { continue };
*counts.entry(h.to_string()).or_default() += 1;
match h {
"rectangle" => {
if let (Some(s), Some(e)) = (p.child("start"), p.child("end")) {
if let (Some(a), Some(b), Some(c), Some(d)) = (s.num(0), s.num(1), e.num(0), e.num(1)) {
rects.push([a.min(c), b.min(d), a.max(c), b.max(d)]);
}
}
}
"polyline" => {
if let Some(pts) = p.child("pts") {
poly_pts.extend(pts.children().iter().filter(|c| c.head() == Some("xy")).filter_map(|c| Some((c.num(0)?, c.num(1)?))));
}
}
_ => {}
}
}
}
let body = rects.iter().copied().max_by(|a, b| ((a[2] - a[0]) * (a[3] - a[1])).total_cmp(&((b[2] - b[0]) * (b[3] - b[1]))));
let prop = |k: &str| flat.children().iter().find(|c| c.head() == Some("property") && c.arg(0).as_deref() == Some(k)).and_then(|c| c.arg(1));
let filters: Vec<String> = flat.child("fp_filters").map(|f| f.children().iter().skip(1).filter_map(|c| c.value()).collect()).unwrap_or_default();
Ok(json!({
"symbol": name,
"counts": counts,
"pins": counts.get("pin").copied().unwrap_or(0),
"bodyRectangle": body,
"polylineBox": bbox(&poly_pts),
"units": units.len().max(1),
"bodyStyles": styles.len().max(1),
"footprint": prop("Footprint"),
"footprintFilters": filters,
"extends": doc.root.children().iter().find(|c| c.head() == Some("symbol") && c.arg(0).as_deref() == Some(name)).and_then(|c| c.child_value("extends")),
}))
}
#[cfg(test)]
mod tests {
use super::*;
fn lib() -> String {
std::fs::read_to_string(std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/sch-edit/MiniLib.kicad_sym")).unwrap()
}
#[test]
fn clone_and_rename_keep_units_and_the_rest_of_the_file() {
let t = lib();
let first = crate::libraries::list_symbols_in_text(&t)[0].clone();
let c = clone_symbol(&t, &first, "R_copy").unwrap();
assert!(c.contains("(symbol \"R_copy\"") && c.contains(&format!("(symbol \"{first}\"")));
assert!(c.contains("\"R_copy_0_1\"") || c.contains("\"R_copy_1_1\""), "unit sub-symbols renamed");
assert!(clone_symbol(&c, &first, "R_copy").is_err(), "an existing name is refused");
let (r, _) = rename_symbol(&c, "R_copy", "R_new").unwrap();
assert!(!r.contains("\"R_copy") && r.contains("(symbol \"R_new\""));
assert_eq!(r, c.replace("\"R_copy", "\"R_new"), "only the names changed");
}
#[test]
fn params_set_add_and_hide() {
let t = lib();
let first = crate::libraries::list_symbols_in_text(&t)[0].clone();
let mut p = BTreeMap::new();
p.insert("Footprint".to_string(), "Resistor_SMD:R_0603_1608Metric".to_string());
p.insert("MPN".to_string(), "RC0603FR-0710KL".to_string());
let (t2, what) = set_params(&t, &first, &p).unwrap();
assert_eq!(what["Footprint"], "set");
assert_eq!(what["MPN"], "added");
let info = crate::symedit::symbol_info(&t2, &first).unwrap();
assert_eq!(info["fields"]["MPN"], "RC0603FR-0710KL");
let (t3, changed) = set_visibility(&t2, &first, "MPN", true).unwrap();
assert!(changed);
let (_, again) = set_visibility(&t3, &first, "MPN", true).unwrap();
assert!(!again, "already shown");
let (t4, _) = set_visibility(&t3, &first, "MPN", false).unwrap();
assert!(t4.contains("(property \"MPN\""));
let prims = symbol_prims(&t4, &first).unwrap();
assert!(prims["pins"].as_u64().unwrap() >= 2, "{prims}");
}
#[test]
fn a_footprint_file_takes_a_new_name() {
let t = "(footprint \"Old\"\n\t(version 20241229)\n\t(layer \"F.Cu\")\n)\n";
let r = rename_footprint_text(t, "New name").unwrap();
assert!(r.starts_with("(footprint \"New name\"\n\t(version"));
}
}