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 #343: kicad-lane: 1.0.85, simulate sweeps and notSimulated, land pattern from the engine, engine 0.19.0
7f17c07
12d ago
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121
//! Footprints from a datasheet's land pattern, the EDA Bridge Standard's footprint_from_land_pattern.
//! The standard's engine (adom-eda-checks, footprint_from_land_pattern) lays the footprint out from the numbers the
//! datasheet prints: pads numbered the standard way, an exposed pad, the courtyard, silkscreen kept clear of the copper
//! with a pin-1 dot, and a fab outline with a pin-1 chamfer (mm, Y up, origin at the centre, pin 1 top-left), so the same
//! numbers give the same footprint in every tool. This module writes that layout in KiCad's terms: `from_engine` reads
//! the engine's reply, `fp_create_args` turns it into `fpedit::create`'s arguments in KiCad's frame (y down), and
//! `vrml_box` is the extruded 3D body.
use serde_json::{json, Value};
#[derive(Debug, Clone, PartialEq)]
pub struct Pad { pub name: String, pub x: f64, pub y: f64, pub w: f64, pub h: f64, pub shape: String, pub hole: f64 }
#[derive(Debug, Default)]
pub struct Fp { pub pads: Vec<Pad>, pub lines: Vec<(&'static str, f64, f64, f64, f64, f64)>, pub discs: Vec<(&'static str, f64, f64, f64)>, pub courtyard: (f64, f64, f64, f64) }
fn r3(v: f64) -> f64 { (v * 1000.0).round() / 1000.0 }
fn num(a: &Value, k: &str) -> Option<f64> { a.get(k).and_then(|v| v.as_f64().or_else(|| v.as_str().and_then(|s| s.parse().ok()))) }
/// The engine's layout reply (pads, lines, discs, courtyardMm; layers overlay, fab, courtyard) as an `Fp`.
pub fn from_engine(v: &Value) -> Result<Fp, String> {
let n = |x: &Value, k: &str| x[k].as_f64().unwrap_or(0.0);
let layer = |l: &str| -> Result<&'static str, String> {
match l { "overlay" => Ok("overlay"), "fab" => Ok("fab"), "courtyard" => Ok("courtyard"), o => Err(format!("the engine's layout has a layer KiCad's writer does not map: {o}")) }
};
let pads: Vec<Pad> = v["pads"].as_array().ok_or("the engine's layout has no pads")?.iter()
.map(|p| Pad { name: p["name"].as_str().unwrap_or("").to_string(), x: n(p, "x"), y: n(p, "y"), w: n(p, "w"), h: n(p, "h"), shape: p["shape"].as_str().unwrap_or("roundrect").to_string(), hole: n(p, "hole") })
.collect();
if pads.is_empty() { return Err("no pads".into()); }
let mut lines = Vec::new();
for l in v["lines"].as_array().into_iter().flatten() {
lines.push((layer(l["layer"].as_str().unwrap_or(""))?, n(l, "x1"), n(l, "y1"), n(l, "x2"), n(l, "y2"), n(l, "width")));
}
let mut discs = Vec::new();
for d in v["discs"].as_array().into_iter().flatten() {
discs.push((layer(d["layer"].as_str().unwrap_or(""))?, n(d, "x"), n(d, "y"), n(d, "r")));
}
let c = &v["courtyardMm"];
let cy = |i: usize| c[i].as_f64().unwrap_or(0.0);
Ok(Fp { pads, lines, discs, courtyard: (cy(0), cy(1), cy(2), cy(3)) })
}
/// A plain description: "SOIC-8 dual, 1.27 mm pitch, 8 pads (from the datasheet land pattern)".
pub fn describe(a: &Value, fp: &Fp) -> String {
let fam = a.get("family").and_then(|f| f.as_str()).unwrap_or("dual");
let pitch = num(a, "pitch").map(|p| format!(", {p} mm pitch")).unwrap_or_default();
format!("{} {}{}, {} pads (from the datasheet land pattern)", a.get("name").and_then(|n| n.as_str()).unwrap_or(""), fam, pitch, fp.pads.len())
}
/// `fpedit::create`'s arguments for the layout: KiCad's footprint frame (y down), pads with their
/// numbers and shapes, the layout's own silkscreen, fab and courtyard as explicit graphics.
pub fn fp_create_args(a: &Value, fp: &Fp) -> Value {
let shape = |s: &str| match s { "round" | "circle" => "circle", "oval" => "oval", "rect" => "rect", _ => "roundrect" };
let pads: Vec<Value> = fp.pads.iter().map(|p| json!({"number": p.name, "type": "smd", "shape": shape(&p.shape), "size": [p.w, p.h], "at": [p.x, r3(-p.y)]})).collect();
fn layer(l: &str) -> &str { if l == "overlay" { "silk" } else { l } }
let mut graphics: Vec<Value> = fp.lines.iter().map(|(l, x0, y0, x1, y1, w)| json!({"layer": layer(l), "start": [x0, r3(-y0)], "end": [x1, r3(-y1)], "width": w})).collect();
graphics.extend(fp.discs.iter().map(|(l, x, y, r)| json!({"layer": layer(l), "center": [x, r3(-y)], "r": r})));
let c = fp.courtyard;
let body = a.get("body").cloned().unwrap_or(json!({}));
let mut out = json!({"pads": pads, "graphics": graphics, "courtyardRect": [c.0, r3(-c.3), c.2, r3(-c.1)], "attr": "smd"});
if let (Some(w), Some(l)) = (num(&body, "w"), num(&body, "l").or_else(|| num(&body, "h"))) {
out["bodySize"] = json!([w, l]);
}
out
}
/// A box the size of the body, `standoff` above the board, as a VRML 2.0 file KiCad shows (mm, in
/// KiCad's 3D units of 2.54 mm per unit; z up from the board).
pub fn vrml_box(w: f64, l: f64, height: f64, standoff: f64) -> String {
let u = |mm: f64| mm / 2.54;
let h = (height - standoff).max(0.01);
format!("#VRML V2.0 utf8\n# body box from the datasheet land pattern: {w} x {l} x {height} mm\nTransform {{\n translation 0 0 {}\n children [\n Shape {{\n appearance Appearance {{ material Material {{ diffuseColor 0.15 0.15 0.15 }} }}\n geometry Box {{ size {} {} {} }}\n }}\n ]\n}}\n",
r6(u(standoff + h / 2.0)), r6(u(w)), r6(u(l)), r6(u(h)))
}
fn r6(v: f64) -> f64 {
(v * 1e6).round() / 1e6
}
#[cfg(test)]
mod tests {
use super::*;
/// A SOIC-8 as the engine lays it out (trimmed: pins 1 and 8, one silk line, the fab chamfer, the pin-1 dot).
fn soic() -> Value {
json!({"pads": [{"name": "1", "x": -2.7, "y": 1.905, "w": 1.55, "h": 0.6, "shape": "roundrect", "hole": 0.0},
{"name": "8", "x": 2.7, "y": 1.905, "w": 1.55, "h": 0.6, "shape": "roundrect", "hole": 0.0}],
"lines": [{"layer": "overlay", "x1": -1.95, "y1": 2.56, "x2": 1.95, "y2": 2.56, "width": 0.15},
{"layer": "fab", "x1": -0.95, "y1": 2.45, "x2": 1.95, "y2": 2.45, "width": 0.1},
{"layer": "courtyard", "x1": -3.73, "y1": -2.7, "x2": 3.73, "y2": 2.7, "width": 0.05}],
"discs": [{"layer": "overlay", "x": -3.8, "y": 1.905, "r": 0.15}],
"courtyardMm": [-3.73, -2.7, 3.73, 2.7]})
}
#[test]
fn the_engines_layout_reads_back() {
let fp = from_engine(&soic()).unwrap();
assert_eq!((fp.pads.len(), fp.lines.len(), fp.discs.len()), (2, 3, 1));
assert_eq!(fp.pads[0], Pad { name: "1".into(), x: -2.7, y: 1.905, w: 1.55, h: 0.6, shape: "roundrect".into(), hole: 0.0 });
assert_eq!(fp.courtyard, (-3.73, -2.7, 3.73, 2.7));
let mut odd = soic();
odd["lines"][0]["layer"] = json!("paste");
assert!(from_engine(&odd).unwrap_err().contains("paste"));
assert!(from_engine(&json!({"pads": []})).is_err());
}
#[test]
fn the_kicad_footprint_is_y_down_with_the_layouts_own_graphics() {
let a = json!({"family": "dual", "pins": 8, "pitch": 1.27, "span": 5.4, "padLength": 1.55, "padWidth": 0.6, "body": {"w": 3.9, "l": 4.9, "height": 1.75}});
let fp = from_engine(&soic()).unwrap();
let k = fp_create_args(&a, &fp);
// pin 1 top-left: y up 1.905 is KiCad's y down -1.905
assert_eq!(k["pads"][0], json!({"number": "1", "type": "smd", "shape": "roundrect", "size": [1.55, 0.6], "at": [-2.7, -1.905]}));
let c = k["courtyardRect"].as_array().unwrap();
assert!(c[1].as_f64().unwrap() < c[3].as_f64().unwrap());
assert!(k["graphics"].as_array().unwrap().iter().any(|g| g["layer"] == "fab") && k["graphics"].as_array().unwrap().iter().any(|g| g["r"].is_number()));
let (text, info) = crate::fpedit::create("SOIC-8_test", &k).unwrap();
assert_eq!(info["pads"], 2);
assert!(text.contains("(fp_line") && text.contains("F.CrtYd") && text.contains("F.Fab") && !text.contains("fp_rect"), "{text}");
assert!(vrml_box(3.9, 4.9, 1.75, 0.1).contains("geometry Box { size 1.535433 1.929134 0.649606 }"));
}
}