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 #315: kicad-lane: 1.0.69, length tuning, mouse-bite panels, silkscreen prep, component classes, build/test boa…
df9e892
13d ago
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173
//! A new board from a parts list and a netlist: the EDA Bridge Standard's build_board, and
//! test_board's row of library parts. The blank KiCad 10 board (`project::blank_board`), an outline
//! on Edge.Cuts (a rectangle with optional rounded corners, or a polygon), every footprint placed
//! from its library text by its own origin (`fp_text::place_library`), and every listed pad given
//! its net (KiCad 10 names nets on the pads; there is no net table). Then read back pin by pin.
//! Pure: the verbs resolve the library files and write the board.
use std::collections::BTreeMap;
use serde_json::{json, Value};
use crate::pcb::NetFormat;
/// One part to place, in KiCad's frame (y down): its library text and where its origin goes.
pub struct Placed {
pub reference: String,
pub lib_id: String,
pub text: String,
pub x: f64,
pub y: f64,
pub rotation: f64,
pub back: bool,
pub value: Option<String>,
}
fn n(v: f64) -> String {
let r = (v * 1e6).round() / 1e6 + 0.0;
format!("{r}")
}
fn edge_line(a: (f64, f64), b: (f64, f64)) -> String {
format!("\t(gr_line (start {} {}) (end {} {}) (stroke (width 0.1) (type default)) (layer \"Edge.Cuts\") (uuid \"{}\"))", n(a.0), n(a.1), n(b.0), n(b.1), crate::schematic::new_uuid())
}
fn edge_arc(a: (f64, f64), m: (f64, f64), b: (f64, f64)) -> String {
format!("\t(gr_arc (start {} {}) (mid {} {}) (end {} {}) (stroke (width 0.1) (type default)) (layer \"Edge.Cuts\") (uuid \"{}\"))", n(a.0), n(a.1), n(m.0), n(m.1), n(b.0), n(b.1), crate::schematic::new_uuid())
}
/// The outline as Edge.Cuts items: `{rect: [x0, y0, w, h], cornerRadius?}` or `{points: [[x, y], ...]}`,
/// in the standard's frame (y up) when `y_up`, else KiCad's.
pub fn outline_items(outline: &Value, y_up: bool) -> Result<(Vec<String>, [f64; 4]), String> {
let fy = |y: f64| if y_up { -y } else { y };
let num = |v: &Value| v.as_f64().ok_or("outline numbers must be numbers (mm)");
if let Some(r) = outline.get("rect").and_then(Value::as_array) {
if r.len() != 4 {
return Err("outline.rect is [x0, y0, width, height] in mm".into());
}
let (x0, y0, w, h) = (num(&r[0])?, num(&r[1])?, num(&r[2])?, num(&r[3])?);
if w <= 0.0 || h <= 0.0 {
return Err("the outline's width and height must be positive".into());
}
let (ya, yb) = (fy(y0), fy(y0 + h));
let (top, bot) = (ya.min(yb), ya.max(yb));
let (l, rt) = (x0, x0 + w);
let rad = outline.get("cornerRadius").and_then(Value::as_f64).unwrap_or(0.0).clamp(0.0, w.min(h) / 2.0);
let mut items = Vec::new();
if rad <= 0.0 {
for (a, b) in [((l, top), (rt, top)), ((rt, top), (rt, bot)), ((rt, bot), (l, bot)), ((l, bot), (l, top))] {
items.push(edge_line(a, b));
}
} else {
let k = rad * (1.0 - std::f64::consts::FRAC_1_SQRT_2);
items.push(edge_line((l + rad, top), (rt - rad, top)));
items.push(edge_arc((rt - rad, top), (rt - k, top + k), (rt, top + rad)));
items.push(edge_line((rt, top + rad), (rt, bot - rad)));
items.push(edge_arc((rt, bot - rad), (rt - k, bot - k), (rt - rad, bot)));
items.push(edge_line((rt - rad, bot), (l + rad, bot)));
items.push(edge_arc((l + rad, bot), (l + k, bot - k), (l, bot - rad)));
items.push(edge_line((l, bot - rad), (l, top + rad)));
items.push(edge_arc((l, top + rad), (l + k, top + k), (l + rad, top)));
}
return Ok((items, [l, top, rt, bot]));
}
if let Some(p) = outline.get("points").and_then(Value::as_array) {
let pts: Vec<(f64, f64)> = p.iter().map(|q| Ok::<_, String>((num(&q[0])?, fy(num(&q[1])?)))).collect::<Result<_, _>>()?;
if pts.len() < 3 {
return Err("outline.points needs 3 points or more".into());
}
let items = (0..pts.len()).map(|i| edge_line(pts[i], pts[(i + 1) % pts.len()])).collect();
let (x0, x1) = pts.iter().fold((f64::MAX, f64::MIN), |m, q| (m.0.min(q.0), m.1.max(q.0)));
let (y0, y1) = pts.iter().fold((f64::MAX, f64::MIN), |m, q| (m.0.min(q.1), m.1.max(q.1)));
return Ok((items, [x0, y0, x1, y1]));
}
Err("outline is {rect: [x0, y0, w, h], cornerRadius?} or {points: [[x, y], ...]} (mm)".into())
}
/// The board text: `board` (a blank board or one to append to), the outline items, the parts placed
/// and the nets set on their pads (`nets`: net -> ["R1.1", ...]). Returns the text and a report of
/// what was placed and wired, and the pins that could not be (no such part on the build, no such pad).
pub fn build(board: &str, outline: &[String], parts: &[Placed], nets: &BTreeMap<String, Vec<String>>) -> Result<(String, Value), String> {
let copper = crate::fp_text::copper_count(&crate::sxedit::parse(board)?);
let mut blocks: BTreeMap<String, String> = BTreeMap::new();
for p in parts {
let block = crate::fp_text::place_library(&p.text, &p.lib_id, p.x, p.y, p.rotation, p.back, &p.reference, p.value.as_deref(), copper)
.map_err(|e| format!("{}: {e}", p.reference))?;
blocks.insert(p.reference.clone(), block);
}
let empty = BTreeMap::new();
let mut wired = 0;
let mut wrong = Vec::new();
for (net, pins) in nets {
for pin in pins {
let Some((r, pad)) = pin.rsplit_once('.') else {
wrong.push(json!({"pin": pin, "want": net, "got": "not Ref.Pad"}));
continue;
};
let Some(block) = blocks.get_mut(r) else {
wrong.push(json!({"pin": pin, "want": net, "got": "no such part on the board"}));
continue;
};
if !block.contains(&format!("(pad \"{pad}\" ")) {
wrong.push(json!({"pin": pin, "want": net, "got": "the footprint has no such pad"}));
continue;
}
*block = crate::pcb_sync::set_pad_net(block, pad, Some(net), NetFormat::Name, &empty);
wired += 1;
}
}
let mut items: Vec<String> = outline.to_vec();
items.extend(blocks.into_values());
let text = crate::pcb::splice(board, &items);
// read back: every requested pin carries its net
let parsed = crate::pcb::parse_pcb_text(&text, "board.kicad_pcb")?;
let pads = crate::pcb::pads(&parsed);
for (net, pins) in nets {
for pin in pins {
let Some((r, pad)) = pin.rsplit_once('.') else { continue };
if let Some(p) = pads.iter().find(|q| q.reference == r && q.pad == pad) {
if p.net_name != *net {
wrong.push(json!({"pin": pin, "want": net, "got": p.net_name}));
}
}
}
}
wrong.dedup();
Ok((text, json!({"placed": parts.len(), "padsWired": wired, "wrongPins": wrong})))
}
#[cfg(test)]
mod tests {
use super::*;
const R0603: &str = "(footprint \"R_0603\" (layer \"F.Cu\") (property \"Reference\" \"REF**\" (at 0 -1.4 0) (layer \"F.SilkS\") (uuid \"a\") (effects (font (size 1 1) (thickness 0.15))))
(property \"Value\" \"R\" (at 0 1.4 0) (layer \"F.Fab\") (uuid \"b\") (effects (font (size 1 1) (thickness 0.15))))
(pad \"1\" smd roundrect (at -0.8 0) (size 0.8 0.9) (layers \"F.Cu\" \"F.Mask\" \"F.Paste\") (roundrect_rratio 0.25) (uuid \"c\"))
(pad \"2\" smd roundrect (at 0.8 0) (size 0.8 0.9) (layers \"F.Cu\" \"F.Mask\" \"F.Paste\") (roundrect_rratio 0.25) (uuid \"d\")))";
#[test]
fn a_board_from_parts_and_nets_reads_back_pin_by_pin() {
let blank = crate::project::blank_board(None, 2).unwrap();
let (outline, bb) = outline_items(&json!({"rect": [0, 0, 20, 10], "cornerRadius": 1}), true).unwrap();
assert_eq!(bb, [0.0, -10.0, 20.0, 0.0]);
assert_eq!(outline.len(), 8, "four sides and four rounded corners");
let parts = vec![
Placed { reference: "R1".into(), lib_id: "Lib:R_0603".into(), text: R0603.into(), x: 5.0, y: -5.0, rotation: 0.0, back: false, value: Some("10k".into()) },
Placed { reference: "R2".into(), lib_id: "Lib:R_0603".into(), text: R0603.into(), x: 15.0, y: -5.0, rotation: 90.0, back: false, value: None },
];
let mut nets = BTreeMap::new();
nets.insert("VIN".to_string(), vec!["R1.1".to_string()]);
nets.insert("MID".to_string(), vec!["R1.2".to_string(), "R2.1".to_string()]);
nets.insert("GND".to_string(), vec!["R2.2".to_string(), "R3.1".to_string(), "R2.9".to_string()]);
let (text, rep) = build(&blank, &outline, &parts, &nets).unwrap();
assert_eq!(rep["placed"], 2);
assert_eq!(rep["padsWired"], 4);
let wrong = rep["wrongPins"].as_array().unwrap();
assert_eq!(wrong.len(), 2, "{rep}");
assert!(wrong.iter().any(|w| w["pin"] == "R3.1") && wrong.iter().any(|w| w["pin"] == "R2.9"));
let parsed = crate::pcb::parse_pcb_text(&text, "b.kicad_pcb").unwrap();
let pads = crate::pcb::pads(&parsed);
assert_eq!(pads.iter().find(|p| p.reference == "R2" && p.pad == "1").unwrap().net_name, "MID");
assert!(text.contains("(property \"Value\" \"10k\"") && text.contains("gr_arc"));
}
}