app
AI Flow: JLCPCB
Public Made by Adomby adom
The whole JLCPCB order, from gerbers to assembled boards in hand: proven part swaps, design rules, rotation checks, quote and order.
main
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980
//! 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);
}
}