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 #358: kicad-lane: 1.0.91, cleanup_libs, build_symbol_modes, add_symbol_art (#29)
e1fa2ce
11d ago
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111
//! The standard's cleanup_libs for KiCad: a library's parts that no given project uses, and parts that repeat
//! another part under a different name. A plan by default; removing takes the names to remove.
use std::collections::{BTreeMap, BTreeSet};
use std::path::Path;
use serde_json::{json, Value};
use crate::schematic::Document;
/// Every `Lib:Name` a project's schematic sheets place (`lib_id`) and its boards use (`footprint`), from the files in
/// the project's folder.
pub fn project_uses(project_dir: &Path) -> BTreeSet<String> {
let mut out = BTreeSet::new();
for e in std::fs::read_dir(project_dir).into_iter().flatten().flatten() {
let p = e.path();
let ext = p.extension().map(|x| x.to_string_lossy().into_owned()).unwrap_or_default();
let key = match ext.as_str() { "kicad_sch" => "(lib_id \"", "kicad_pcb" => "(footprint \"", _ => continue };
let Ok(t) = std::fs::read_to_string(&p) else { continue };
let mut rest = t.as_str();
while let Some(i) = rest.find(key) {
let after = &rest[i + key.len()..];
if let Some(q) = after.find('"') {
out.insert(after[..q].to_string());
}
rest = &after[after.find('"').unwrap_or(0) + 1..];
}
}
out
}
/// A symbol's text with its own name taken out, so two symbols that differ only in name compare equal.
fn symbol_shape(lib_text: &str, name: &str) -> Option<String> {
let flat = crate::schematic::flatten_lib_symbol(lib_text, name).ok()?;
let mut t = flat.to_text();
for n in [format!("\"{name}\""), format!("\"{name}_")] {
t = t.replace(&n, "\"@_");
}
// the Value field usually repeats the name
Some(t.split_whitespace().collect::<Vec<_>>().join(" "))
}
/// A footprint's text without its name, uuids and timestamps.
fn footprint_shape(text: &str, name: &str) -> String {
let t = text.replacen(&format!("(footprint \"{name}\""), "(footprint \"@\"", 1).replace(&format!("\"{name}\""), "\"@\"");
t.lines().filter(|l| !l.trim_start().starts_with("(uuid") && !l.trim_start().starts_with("(tedit") && !l.trim_start().starts_with("(generator")).map(str::trim).collect::<Vec<_>>().join(" ")
}
/// The plan for one symbol library: `uses` are the projects' `Lib:Name` ids (None: no project given, so nothing can be
/// called unused), `nick` the library's nickname in those ids.
pub fn plan_symbols(lib_text: &str, nick: &str, uses: Option<&BTreeSet<String>>) -> Result<Value, String> {
let doc = Document::parse(lib_text)?;
let syms: Vec<(String, Option<String>)> = doc.root.children().iter().filter(|c| c.head() == Some("symbol"))
.filter_map(|c| Some((c.arg(0)?, c.child_value("extends")))).collect();
let extended: BTreeSet<&str> = syms.iter().filter_map(|(_, e)| e.as_deref()).collect();
let unused: Vec<&str> = match uses {
Some(u) => syms.iter().map(|(n, _)| n.as_str()).filter(|n| !u.contains(&format!("{nick}:{n}")) && !extended.contains(n)).collect(),
None => Vec::new(),
};
Ok(json!({"kind": "symbols", "parts": syms.len(), "unused": unused, "duplicates": groups(syms.iter().filter_map(|(n, _)| symbol_shape(lib_text, n).map(|s| (n.clone(), s))))}))
}
/// The plan for one footprint library (`files`: name and .kicad_mod text).
pub fn plan_footprints(files: &[(String, String)], nick: &str, uses: Option<&BTreeSet<String>>) -> Value {
let unused: Vec<&str> = match uses {
Some(u) => files.iter().map(|(n, _)| n.as_str()).filter(|n| !u.contains(&format!("{nick}:{n}"))).collect(),
None => Vec::new(),
};
json!({"kind": "footprints", "parts": files.len(), "unused": unused, "duplicates": groups(files.iter().map(|(n, t)| (n.clone(), footprint_shape(t, n))))})
}
/// Names whose shapes are identical, in groups of two or more (first name first).
fn groups(items: impl Iterator<Item = (String, String)>) -> Vec<Vec<String>> {
let mut by: BTreeMap<String, Vec<String>> = BTreeMap::new();
for (n, s) in items {
by.entry(s).or_default().push(n);
}
let mut g: Vec<Vec<String>> = by.into_values().filter(|v| v.len() > 1).collect();
g.sort();
g
}
#[cfg(test)]
mod tests {
use super::*;
fn sym(name: &str, body: &str) -> String {
format!("\t(symbol \"{name}\"\n\t\t(property \"Reference\" \"R\"\n\t\t\t(at 0 0 0)\n\t\t)\n\t\t(property \"Value\" \"{name}\"\n\t\t\t(at 0 0 0)\n\t\t)\n\t\t(symbol \"{name}_0_1\"\n\t\t\t{body}\n\t\t)\n\t)\n")
}
#[test]
fn unused_and_duplicate_symbols_are_found_and_extended_ones_kept() {
let rect = "(rectangle (start -1 1) (end 1 -1))";
let lib = format!("(kicad_symbol_lib\n\t(version 20241209)\n{}{}{}\t(symbol \"R_alias\"\n\t\t(extends \"R_base\")\n\t)\n)\n", sym("R_base", rect), sym("R_copy", rect), sym("C_other", "(circle (center 0 0) (radius 1))"));
let uses: BTreeSet<String> = ["Mine:R_alias".to_string()].into();
let p = plan_symbols(&lib, "Mine", Some(&uses)).unwrap();
assert_eq!(p["unused"], json!(["R_copy", "C_other"]), "{p}");
assert_eq!(p["duplicates"], json!([["R_base", "R_copy"]]));
let none = plan_symbols(&lib, "Mine", None).unwrap();
assert_eq!(none["unused"], json!([]));
}
#[test]
fn duplicate_footprints_ignore_names_and_uuids() {
let fp = |n: &str, u: &str| format!("(footprint \"{n}\"\n\t(uuid \"{u}\")\n\t(pad \"1\" smd rect (at 0 0) (size 1 1) (layers \"F.Cu\"))\n)\n");
let files = vec![("A".to_string(), fp("A", "1")), ("B".to_string(), fp("B", "2")), ("C".to_string(), fp("C", "3").replace("(size 1 1)", "(size 2 2)"))];
let uses: BTreeSet<String> = ["Lib:A".to_string()].into();
let p = plan_footprints(&files, "Lib", Some(&uses));
assert_eq!(p["duplicates"], json!([["A", "B"]]));
assert_eq!(p["unused"], json!(["B", "C"]));
}
}