main
John Lauer Publish 0.1.0 55a9481 2d ago
//! The footprints of a .kicad_pcb, read straight from the file: reference, footprint name, placement, and each pad's
//! centre and size in the footprint's own frame (mm, y down).
use regex::Regex;
use std::collections::BTreeMap;

#[derive(Clone, Debug)]
pub struct Pad { pub x: f64, pub y: f64, pub w: f64, pub h: f64, pub rot: f64 }

#[derive(Clone, Debug)]
#[allow(dead_code)] // placement fields kept for stages that need them
pub struct Footprint { pub reference: String, pub name: String, pub x: f64, pub y: f64, pub rot: f64, pub bottom: bool, pub pads: BTreeMap<String, Pad> }

/// Each top-level `(footprint ...)` block, matching parentheses outside quoted strings.
fn blocks(t: &str) -> Vec<&str> {
    let b = t.as_bytes();
    let mut out = Vec::new();
    let mut i = 0;
    while let Some(j) = t[i..].find("(footprint ").map(|k| k + i) {
        let (mut depth, mut k, mut q) = (0i32, j, false);
        while k < b.len() {
            let c = b[k];
            if q { if c == b'\\' { k += 1; } else if c == b'"' { q = false; } }
            else if c == b'"' { q = true; }
            else if c == b'(' { depth += 1; }
            else if c == b')' { depth -= 1; if depth == 0 { k += 1; break; } }
            k += 1;
        }
        out.push(&t[j..k.min(b.len())]);
        i = k;
    }
    out
}

pub fn footprints(text: &str) -> BTreeMap<String, Footprint> {
    let re_ref = Regex::new(r#"\(property "Reference" "([^"]+)"|\(fp_text reference "([^"]+)""#).unwrap();
    let re_name = Regex::new(r#"\(footprint "([^"]+)""#).unwrap();
    let re_at = Regex::new(r"\(at ([-\d.]+) ([-\d.]+)(?: ([-\d.]+))?\)").unwrap();
    let re_pad = Regex::new(r#"(?s)\(pad "([^"]*)" (?:smd|thru_hole) \w+.*?\(at ([-\d.]+) ([-\d.]+)(?: ([-\d.]+))?\).*?\(size ([-\d.]+) ([-\d.]+)\)"#).unwrap();
    let f = |s: &str| s.parse::<f64>().unwrap_or(0.0);
    let mut out = BTreeMap::new();
    for b in blocks(text) {
        let Some(r) = re_ref.captures(b) else { continue };
        let reference = r.get(1).or(r.get(2)).unwrap().as_str().to_string();
        let name = re_name.captures(b).map(|c| c[1].to_string()).unwrap_or_default();
        let (x, y, rot) = re_at.captures(b).map(|c| (f(&c[1]), f(&c[2]), c.get(3).map(|m| f(m.as_str())).unwrap_or(0.0))).unwrap_or((0.0, 0.0, 0.0));
        let head = &b[..b.len().min(400)];
        let mut pads = BTreeMap::new();
        for c in re_pad.captures_iter(b) {
            pads.entry(c[1].to_string()).or_insert(Pad { x: f(&c[2]), y: f(&c[3]), rot: c.get(4).map(|m| f(m.as_str())).unwrap_or(0.0), w: f(&c[5]), h: f(&c[6]) });
        }
        out.insert(reference.clone(), Footprint { reference, name, x, y, rot, bottom: head.contains("(layer \"B.Cu\")"), pads });
    }
    out
}

/// The reference designator's letters and number, so R2 sorts before R10.
pub fn ref_key(s: &str) -> (String, u64) {
    (s.chars().filter(|c| !c.is_ascii_digit()).collect(), s.chars().filter(|c| c.is_ascii_digit()).collect::<String>().parse().unwrap_or(0))
}

/// Python's "%g" for the values a CPL carries (whole degrees print without a decimal point).
pub fn g(v: f64) -> String {
    if (v - v.round()).abs() < 1e-9 { format!("{}", v.round() as i64) } else { let s = format!("{v:.6}"); s.trim_end_matches('0').trim_end_matches('.').to_string() }
}

#[cfg(test)]
mod tests {
    #[test]
    fn reads_a_footprint_and_its_pads() {
        let t = r#"(kicad_pcb (footprint "Package_TO_SOT_SMD:TSOT-23-6" (layer "F.Cu") (at 10 20 180) (property "Reference" "U1" (at 0 0)) (pad "1" smd roundrect (at -0.95 1.3 180) (size 0.6 1.1) (layers "F.Cu")) (pad "2" smd roundrect (at 0 1.3 180) (size 0.6 1.1))) (footprint "R" (at 1 2) (property "Reference" "R1 (x)" (at 0 0))))"#;
        let f = super::footprints(t);
        let u = &f["U1"];
        assert_eq!((u.x, u.y, u.rot, u.bottom), (10.0, 20.0, 180.0, false));
        assert_eq!(u.pads.len(), 2);
        assert_eq!(u.pads["1"].x, -0.95);
        assert!(f.contains_key("R1 (x)"));
        assert_eq!(super::g(90.0), "90");
        assert_eq!(super::ref_key("R10") > super::ref_key("R2"), true);
    }
}