app
AI Flow: Reverse Engineering
Public Made by Adomby adom
Photos of a PCB to a tagged KiCad copy: parts placed, copper and nets with their evidence, a schematic, a measured confidence score, bench tests and a video of the run.
main
Caleb
Publish 0.5.0
1984b40
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335
//! Schematic symbols. Standard shapes come from KiCad's own libraries: fetched once (a local KiCad
//! install, or service-kicad), flattened when a symbol `extends` another, and cached one symbol per file
//! under reverse-flow/lib/sym so a run draws the same sheet every time. What the libraries cannot give is
//! drawn here: a box for an IC with no library symbol, the rail and ground symbols, the PWR_FLAG and the
//! "?" marker for a pin whose net is not known. Every symbol lands in the project library `RE`.
//!
//! Geometry is in steps of 1.27 mm (50 mil), y down as on the sheet. A `Term` is one connection point
//! of a symbol and may carry several pad numbers (pins a library stacks on one point).
use crate::sexp::{self, N};
use std::collections::HashMap;
use std::path::{Path, PathBuf};
/// One layout step in mm (50 mil).
pub const STEP: f64 = 1.27;
/// The project symbol library every embedded symbol is filed under.
pub const LIB: &str = "RE";
#[derive(Clone, Debug, PartialEq)]
pub struct Term { pub nums: Vec<String>, pub name: String, pub etype: String, pub x: i32, pub y: i32, /** the way the wire leaves, sheet sense */ pub dir: (i32, i32) }
#[derive(Clone, Debug)]
pub struct Sym { pub name: String, /** everything after the name in `(symbol "<name>" ...)` */ pub body: String, pub terms: Vec<Term>, /** drawn body in steps: x0, y0, x1, y1 */ pub bb: (f64, f64, f64, f64), pub prefix: String }
impl Sym {
pub fn id(&self) -> String { format!("{LIB}:{}", self.name) }
/// The symbol as a schematic embeds it (`lib` true) or as the library file holds it.
pub fn text(&self, lib: bool) -> String { format!("(symbol \"{}{}\" {}", if lib { format!("{LIB}:") } else { String::new() }, self.name, self.body) }
pub fn term(&self, num: &str) -> Option<usize> { self.terms.iter().position(|t| t.nums.iter().any(|n| n == num)) }
}
pub fn esc(s: &str) -> String { s.replace('\\', "/").replace('"', "'").replace('\n', " ") }
/// Text a sheet shows: ASCII only, units spelled the way the sheet rules ask.
pub fn ascii(s: &str) -> String {
esc(s).chars().map(|c| match c { 'Ω' | 'Ω' => 'R', 'µ' | 'μ' => 'u', '−' | '–' | '—' => '-', '±' => '~', c if c.is_ascii() => c, _ => '?' }).collect()
}
pub fn safe(s: &str) -> String { s.chars().map(|c| if c.is_ascii_alphanumeric() || "_-+.".contains(c) { c } else { '_' }).collect() }
const FONT: &str = "(effects (font (size 1.27 1.27)))";
const HIDE: &str = "(effects (font (size 1.27 1.27)) (hide yes))";
fn props(prefix: &str, value: &str, show_value: bool) -> String {
format!("(property \"Reference\" \"{}\" (at 0 0 0) {})\n\t\t\t(property \"Value\" \"{}\" (at 0 0 0) {})\n\t\t\t(property \"Footprint\" \"\" (at 0 0 0) {HIDE})\n\t\t\t(property \"Datasheet\" \"~\" (at 0 0 0) {HIDE})\n",
esc(prefix), if prefix.starts_with('#') { HIDE } else { FONT }, esc(value), if show_value { FONT } else { HIDE })
}
fn pin_text(etype: &str, x: f64, y: f64, ang: i32, len: f64, name: &str, num: &str, hide: bool) -> String {
format!("\t\t\t\t(pin {etype} line (at {x:.3} {y:.3} {ang}) (length {len:.3}){} (name \"{}\" {FONT}) (number \"{}\" {FONT}))\n", if hide { " hide" } else { "" }, esc(name), esc(num))
}
/// KiCad pin angle to the way the wire leaves, sheet sense (y down): the angle is the way the pin runs from its end into the body.
fn out_dir(ang: i32) -> (i32, i32) { match ang.rem_euclid(360) { 0 => (-1, 0), 180 => (1, 0), 90 => (0, 1), _ => (0, -1) } }
fn steps(mm: f64) -> i32 { (mm / STEP).round() as i32 }
/// Where KiCad's standard symbol libraries can be read on this machine (first that exists wins).
fn local_dirs() -> Vec<PathBuf> {
let mut v = vec![];
if let Ok(d) = std::env::var("ADOM_AIFLOW_KICAD_SYMBOLS") { v.push(PathBuf::from(d)); }
v.push(PathBuf::from("/usr/share/kicad/symbols"));
if let Ok(rd) = std::fs::read_dir("/mnt/c/Program Files/KiCad") {
let mut vs: Vec<PathBuf> = rd.flatten().map(|e| e.path().join("share/kicad/symbols")).filter(|p| p.is_dir()).collect();
vs.sort(); vs.reverse(); v.extend(vs);
}
v
}
/// The text of `(symbol "name" ...)` inside a library file.
pub fn extract<'a>(lib: &'a str, name: &str) -> Option<&'a str> {
let key = format!("(symbol \"{name}\"");
let i = lib.find(&key)?;
let b = lib.as_bytes();
let (mut d, mut j, mut in_str) = (0i32, i, false);
while j < b.len() {
match b[j] {
b'\\' if in_str => { j += 1; }
b'"' => in_str = !in_str,
b'(' if !in_str => d += 1,
b')' if !in_str => { d -= 1; if d == 0 { return Some(&lib[i..=j]); } }
_ => {}
}
j += 1;
}
None
}
pub struct Libs { cache: PathBuf, texts: HashMap<String, Option<String>>, pub notes: Vec<String> }
impl Libs {
pub fn new(cache: &Path) -> Self { Libs { cache: cache.to_path_buf(), texts: HashMap::new(), notes: vec![] } }
fn lib_text(&mut self, lib: &str) -> Option<&str> {
if !self.texts.contains_key(lib) {
let mut t = local_dirs().iter().find_map(|d| std::fs::read_to_string(d.join(format!("{lib}.kicad_sym"))).ok());
if t.is_none() {
let tmp = self.cache.join(format!(".{}.kicad_sym", safe(lib)));
std::fs::create_dir_all(&self.cache).ok();
let _ = std::process::Command::new("service-kicad").args(["sym", "fetch", lib, "--out"]).arg(&tmp).output();
t = std::fs::read_to_string(&tmp).ok().filter(|s| s.contains("(kicad_symbol_lib"));
let _ = std::fs::remove_file(&tmp);
}
self.texts.insert(lib.to_string(), t);
}
self.texts[lib].as_deref()
}
/// The flattened node of `Library:Name`, from the run's cache or KiCad's library.
pub fn node(&mut self, lib: &str, name: &str) -> Result<N, String> {
let p = self.cache.join(safe(lib)).join(format!("{}.kicad_sym", safe(name)));
if let Ok(t) = std::fs::read_to_string(&p) { return sexp::parse(&t); }
let raw = { let text = self.lib_text(lib).ok_or(format!("symbol library {lib} is not readable (no local KiCad symbols, and service-kicad did not answer)"))?; extract(text, name).ok_or(format!("{lib}:{name} is not in KiCad's library"))?.to_string() };
let mut n = sexp::parse(&raw)?;
if let Some(base) = n.child("extends").and_then(|e| e.arg(0)) {
let mut b = self.node(lib, &base)?;
// the child keeps its name; the parent gives the drawing and the pins
for c in b.items_mut().iter_mut() {
if c.head() == "symbol" { if let Some(old) = c.arg(0) { if let Some(rest) = old.strip_prefix(&base) { c.items_mut()[1] = sexp::q(&format!("{name}{rest}")); } } }
}
b.items_mut()[1] = sexp::q(name);
n = b;
}
if let Some(d) = p.parent() { std::fs::create_dir_all(d).ok(); }
std::fs::write(&p, n.to_string()).ok();
Ok(n)
}
/// A library symbol filed as `RE:<as_name>`. `renumber`: pin name to the pad numbers it stands for (a
/// transistor drawing on a package whose pads the library does not number that way). `hide_pins`: hide
/// pin names and numbers (passives and LEDs).
pub fn symbol(&mut self, lib: &str, name: &str, as_name: &str, renumber: Option<&HashMap<String, Vec<String>>>, hide_pins: bool) -> Result<Sym, String> {
let n = self.node(lib, name)?;
from_node(&n, as_name, renumber, hide_pins)
}
}
fn grow(bb: &mut (f64, f64, f64, f64), x: f64, y: f64) { bb.0 = bb.0.min(x); bb.1 = bb.1.min(y); bb.2 = bb.2.max(x); bb.3 = bb.3.max(y); }
/// The drawn extent of a unit's graphics, in mm, library sense (y up).
fn graphics_box(n: &N, bb: &mut (f64, f64, f64, f64)) {
for g in n.items() {
match g.head() {
"rectangle" => { for k in ["start", "end"] { if let Some(p) = g.child(k) { grow(bb, p.num(0).unwrap_or(0.0), p.num(1).unwrap_or(0.0)); } } }
"polyline" | "bezier" => { for p in g.child("pts").into_iter().flat_map(|p| p.children("xy")) { grow(bb, p.num(0).unwrap_or(0.0), p.num(1).unwrap_or(0.0)); } }
"circle" => { if let (Some(c), Some(r)) = (g.child("center"), g.child("radius").and_then(|r| r.num(0))) { let (x, y) = (c.num(0).unwrap_or(0.0), c.num(1).unwrap_or(0.0)); grow(bb, x - r, y - r); grow(bb, x + r, y + r); } }
"arc" => { for k in ["start", "mid", "end"] { if let Some(p) = g.child(k) { grow(bb, p.num(0).unwrap_or(0.0), p.num(1).unwrap_or(0.0)); } } }
_ => {}
}
}
}
pub fn from_node(n: &N, as_name: &str, renumber: Option<&HashMap<String, Vec<String>>>, hide_pins: bool) -> Result<Sym, String> {
let orig = n.arg(0).unwrap_or_default();
let prefix = n.children("property").find(|p| p.arg(0).as_deref() == Some("Reference")).and_then(|p| p.arg(1)).unwrap_or("U".into());
let mut body = String::new();
if hide_pins { body += "(pin_numbers (hide yes)) (pin_names (offset 0) (hide yes))"; }
else { for k in ["pin_numbers", "pin_names"] { if let Some(c) = n.child(k) { body += &c.to_string().replace('\n', " ").replace('\t', ""); body.push(' '); } } }
body += " (exclude_from_sim no) (in_bom yes) (on_board yes)\n\t\t\t";
body += &props(&prefix, as_name, true);
let mut bb = (f64::MAX, f64::MAX, f64::MIN, f64::MIN);
let mut terms: Vec<Term> = vec![];
for sub in n.children("symbol") {
let sname = sub.arg(0).unwrap_or_default();
let tail = sname.strip_prefix(&orig).unwrap_or("");
let mut it = tail.trim_start_matches('_').split('_');
let (unit, style) = (it.next().and_then(|s| s.parse::<u32>().ok()).unwrap_or(0), it.next().and_then(|s| s.parse::<u32>().ok()).unwrap_or(1));
if unit > 1 { return Err(format!("{orig} has more than one unit: a multi-unit symbol is not placed (give the part `pins` instead)")); }
if style > 1 { continue; }
graphics_box(sub, &mut bb);
let mut out = format!("\t\t\t(symbol \"{as_name}_{unit}_{style}\"\n");
for g in sub.items().iter().skip(2) {
// text too small to read on a printed sheet (under 40 mil) is left out of the copy
if g.head() == "text" && g.child("effects").and_then(|e| e.child("font")).and_then(|f| f.child("size")).and_then(|z| z.num(0)).map(|z| z < 1.016 - 1e-6).unwrap_or(false) { continue; }
if g.head() != "pin" { out += "\t\t\t\t"; out += &g.to_string().replace('\n', " ").replace('\t', ""); out.push('\n'); continue; }
let etype = match g.items().get(1) { Some(N::A(s)) => s.clone(), _ => "passive".into() };
let at = g.child("at"); let (x, y, ang) = (at.and_then(|a| a.num(0)).unwrap_or(0.0), at.and_then(|a| a.num(1)).unwrap_or(0.0), at.and_then(|a| a.num(2)).unwrap_or(0.0) as i32);
let len = g.child("length").and_then(|l| l.num(0)).unwrap_or(2.54);
let name = g.child("name").and_then(|c| c.arg(0)).unwrap_or_default();
let num = g.child("number").and_then(|c| c.arg(0)).unwrap_or_default();
let hidden = g.items().iter().any(|i| matches!(i, N::A(s) if s == "hide")) || g.child("hide").map(|h| h.arg(0).as_deref() != Some("no")).unwrap_or(false);
let nums: Vec<String> = match renumber { Some(m) => match m.get(&name) { Some(v) => v.clone(), None => return Err(format!("{orig}: pin {name} has no pad in `pins`")) }, None => vec![num] };
if (x / STEP - (x / STEP).round()).abs() > 0.02 || (y / STEP - (y / STEP).round()).abs() > 0.02 { return Err(format!("{orig}: pin {name} is off the 50 mil grid")); }
let (tx, ty) = (steps(x), steps(-y));
// the pin's line belongs to the drawn body's reach
let d = out_dir(ang); grow(&mut bb, x + if d.0 != 0 { -(d.0 as f64) * len } else { 0.0 }, y + if d.1 != 0 { d.1 as f64 * len } else { 0.0 });
for (k, nm) in nums.iter().enumerate() { out += &pin_text(&etype, x, y, ang, len, &name, nm, hidden || k > 0); }
match terms.iter_mut().find(|t| t.x == tx && t.y == ty) {
Some(t) => { t.nums.extend(nums); if t.etype == "passive" { t.etype = etype; } }
None => terms.push(Term { nums, name, etype, x: tx, y: ty, dir: d }),
}
}
out += "\t\t\t)\n";
body += &out;
}
body += "\t\t)";
if bb.0 > bb.2 { bb = (-1.0, -1.0, 1.0, 1.0); }
Ok(Sym { name: as_name.to_string(), body, terms, bb: (bb.0 / STEP, -bb.3 / STEP, bb.2 / STEP, -bb.1 / STEP), prefix })
}
/// One pin of a box: its pad number, name (may be empty), electrical type and row (steps down from the first row).
#[derive(Clone, Debug)]
pub struct BoxPin { pub num: String, pub name: String, pub etype: String, pub y: i32 }
/// A box for a part with no library symbol: pins left and right at the rows the layout chose, at least `min_w` steps wide.
pub fn box_sym(name: &str, prefix: &str, left: &[BoxPin], right: &[BoxPin], min_w: i32) -> Sym {
let chars = |v: &[BoxPin]| v.iter().map(|p| p.name.chars().count()).max().unwrap_or(0) as f64;
let mut w = (((chars(left) + chars(right)) * 0.95).ceil() as i32 + 6).max(min_w);
if w % 4 != 0 { w += 4 - w % 4; }
let w = w.max(12);
let rows = left.iter().chain(right.iter()).map(|p| p.y).max().unwrap_or(0);
let (x0, x1, y0, y1) = (-w / 2, w / 2, -2, rows + 2);
let named = left.iter().chain(right.iter()).any(|p| !p.name.is_empty());
let mut body = format!("(pin_names (offset 0.508){}) (exclude_from_sim no) (in_bom yes) (on_board yes)\n\t\t\t", if named { "" } else { " (hide yes)" });
body += &props(prefix, name, true);
body += &format!("\t\t\t(symbol \"{name}_0_1\"\n\t\t\t\t(rectangle (start {:.3} {:.3}) (end {:.3} {:.3}) (stroke (width 0.254) (type default)) (fill (type background)))\n\t\t\t)\n\t\t\t(symbol \"{name}_1_1\"\n", x0 as f64 * STEP, -(y0 as f64) * STEP, x1 as f64 * STEP, -(y1 as f64) * STEP);
let mut terms = vec![];
for (side, v) in [(-1, left), (1, right)] {
for p in v {
let tx = if side < 0 { x0 - 2 } else { x1 + 2 };
body += &pin_text(&p.etype, tx as f64 * STEP, -(p.y as f64) * STEP, if side < 0 { 0 } else { 180 }, 2.0 * STEP, &p.name, &p.num, false);
terms.push(Term { nums: vec![p.num.clone()], name: p.name.clone(), etype: p.etype.clone(), x: tx, y: p.y, dir: (side, 0) });
}
}
body += "\t\t\t)\n\t\t)";
Sym { name: name.to_string(), body, terms, bb: (x0 as f64, y0 as f64, x1 as f64, y1 as f64), prefix: prefix.to_string() }
}
/// A rail: the bar on a short stem for a supply, the triangle for a ground. The wire arrives at (0, 0);
/// the symbol points up for a supply and down for a ground until the layout turns it.
pub fn rail_sym(net: &str, ground: bool) -> Sym {
let name = format!("PWR_{}", safe(net));
let mut body = format!("(power) (pin_numbers (hide yes)) (pin_names (offset 0) (hide yes)) (exclude_from_sim no) (in_bom yes) (on_board yes)\n\t\t\t");
body += &props("#PWR", net, !(ground && net == "GND"));
let line = "(stroke (width 0) (type default)) (fill (type none))";
if ground {
body += &format!("\t\t\t(symbol \"{name}_0_1\"\n\t\t\t\t(polyline (pts (xy 0 0) (xy 0 -1.27) (xy 1.27 -1.27) (xy 0 -2.54) (xy -1.27 -1.27) (xy 0 -1.27)) {line})\n\t\t\t)\n\t\t\t(symbol \"{name}_1_1\"\n");
body += &pin_text("power_in", 0.0, 0.0, 270, 0.0, net, "1", false);
} else {
body += &format!("\t\t\t(symbol \"{name}_0_1\"\n\t\t\t\t(polyline (pts (xy 0 0) (xy 0 1.27)) {line})\n\t\t\t\t(polyline (pts (xy -1.016 1.27) (xy 1.016 1.27)) (stroke (width 0.254) (type default)) (fill (type none)))\n\t\t\t)\n\t\t\t(symbol \"{name}_1_1\"\n");
body += &pin_text("power_in", 0.0, 0.0, 90, 0.0, net, "1", false);
}
body += "\t\t\t)\n\t\t)";
let (dir, bb) = if ground { ((0, -1), (-1.0, 0.0, 1.0, 2.0)) } else { ((0, 1), (-0.8, -1.0, 0.8, 0.0)) };
Sym { name, body, terms: vec![Term { nums: vec!["1".into()], name: net.to_string(), etype: "power_in".into(), x: 0, y: 0, dir }], bb, prefix: "#PWR".into() }
}
/// KiCad's PWR_FLAG: tells the ERC a rail is fed from off the board (or from a part whose pin type is not known).
/// It is not marked as a power symbol: KiCad's ERC calls a flag wired to nothing but a rail symbol an open
/// pin when both are power symbols, and the flags sit with the rail symbols on the root sheet.
pub fn flag_sym() -> Sym {
let name = "PWR_FLAG";
let mut body = "(pin_numbers (hide yes)) (pin_names (offset 0) (hide yes)) (exclude_from_sim no) (in_bom yes) (on_board yes)\n\t\t\t".to_string();
body += &props("#FLG", "PWR_FLAG", true);
body += &format!("\t\t\t(symbol \"{name}_0_1\"\n\t\t\t\t(polyline (pts (xy 0 0) (xy 0 1.27) (xy -1.016 1.905) (xy 0 2.54) (xy 1.016 1.905) (xy 0 1.27)) (stroke (width 0) (type default)) (fill (type none)))\n\t\t\t)\n\t\t\t(symbol \"{name}_1_1\"\n");
body += &pin_text("power_out", 0.0, 0.0, 90, 0.0, "pwr", "1", false);
body += "\t\t\t)\n\t\t)";
Sym { name: name.into(), body, terms: vec![Term { nums: vec!["1".into()], name: "pwr".into(), etype: "power_out".into(), x: 0, y: 0, dir: (0, 1) }], bb: (-0.8, -2.0, 0.8, 0.0), prefix: "#FLG".into() }
}
/// The "?" marker: this pin's net is not known. It is no part (its reference starts with `#`, so it is in
/// no netlist and no BOM); it gives the pin something to connect to so the ERC does not call it an open
/// pin, and it is no no-connect flag, which would say the pin is known to be unused. `point`: which way the
/// circle sits from the wire end. `supply`: the marker for a supply input, whose unknown net must also
/// count as driven.
pub fn unknown_sym(point: (i32, i32), supply: bool) -> Sym {
let side = match point { (1, 0) => "R", (-1, 0) => "L", (0, -1) => "U", _ => "D" };
let name = format!("Unknown{}_{side}", if supply { "Supply" } else { "" });
// a bare question mark, half a step off the wire end: small enough to sit on every row of a connector
let (cx, cy) = (point.0 as f64 * 0.9, -(point.1 as f64) * 0.9);
let mut body = "(pin_numbers (hide yes)) (pin_names (offset 0) (hide yes)) (exclude_from_sim yes) (in_bom no) (on_board no)\n\t\t\t".to_string();
body += &props("#UNK", "net not known", false);
body += &format!("\t\t\t(symbol \"{name}_0_1\"\n\t\t\t\t(text \"?\" (at {cx:.3} {cy:.3} 0) (effects (font (size 1.27 1.27) (thickness 0.254) (bold yes))))\n\t\t\t)\n\t\t\t(symbol \"{name}_1_1\"\n");
let ang = match point { (1, 0) => 0, (-1, 0) => 180, (0, -1) => 90, _ => 270 };
body += &pin_text(if supply { "power_out" } else { "passive" }, 0.0, 0.0, ang, 0.0, "unknown", "1", false);
body += "\t\t\t)\n\t\t)";
let (px, py) = (point.0 as f64 * 0.7, point.1 as f64 * 0.7);
Sym { name, body, terms: vec![Term { nums: vec!["1".into()], name: "unknown".into(), etype: "passive".into(), x: 0, y: 0, dir: (-point.0, -point.1) }], bb: (px - 0.5, py - 0.5, px + 0.5, py + 0.5), prefix: "#UNK".into() }
}
#[cfg(test)]
pub mod tests {
use super::*;
pub const R: &str = r#"(symbol "R" (pin_numbers (hide yes)) (pin_names (offset 0)) (property "Reference" "R" (at 2.032 0 90))
(symbol "R_0_1" (rectangle (start -1.016 -2.54) (end 1.016 2.54) (stroke (width 0.254) (type default)) (fill (type none))))
(symbol "R_1_1" (pin passive line (at 0 3.81 270) (length 1.27) (name "" (effects (font (size 1.27 1.27)))) (number "1" (effects (font (size 1.27 1.27)))))
(pin passive line (at 0 -3.81 90) (length 1.27) (name "" (effects (font (size 1.27 1.27)))) (number "2" (effects (font (size 1.27 1.27)))))))"#;
pub const NMOS: &str = r#"(symbol "Q_NMOS" (pin_numbers (hide yes)) (pin_names (offset 0) (hide yes)) (property "Reference" "Q" (at 5.08 1.27 0))
(symbol "Q_NMOS_0_1" (circle (center 1.651 0) (radius 2.794) (stroke (width 0.254) (type default)) (fill (type none))))
(symbol "Q_NMOS_1_1" (pin passive line (at 2.54 5.08 270) (length 2.54) (name "D" (effects (font (size 1.27 1.27)))) (number "D" (effects (font (size 1.27 1.27)))))
(pin input line (at -5.08 0 0) (length 2.54) (name "G" (effects (font (size 1.27 1.27)))) (number "G" (effects (font (size 1.27 1.27)))))
(pin passive line (at 2.54 -5.08 90) (length 2.54) (name "S" (effects (font (size 1.27 1.27)))) (number "S" (effects (font (size 1.27 1.27)))))))"#;
#[test]
fn a_library_symbol_keeps_its_drawing_and_pins() {
let s = from_node(&sexp::parse(R).unwrap(), "R", None, true).unwrap();
assert_eq!(s.id(), "RE:R");
assert_eq!(s.terms.len(), 2);
assert_eq!((s.terms[0].x, s.terms[0].y, s.terms[0].dir), (0, -3, (0, -1)), "pin 1 is the top end, its wire leaves upward");
assert_eq!((s.terms[1].x, s.terms[1].y, s.terms[1].dir), (0, 3, (0, 1)));
assert!(s.text(true).starts_with("(symbol \"RE:R\"") && s.body.contains("(symbol \"R_1_1\"") && s.body.contains("rectangle"));
assert!(sexp::parse(&s.text(true)).is_ok(), "the embedded text is one well-formed list");
}
#[test]
fn a_transistor_takes_the_pads_the_run_names() {
let m: HashMap<String, Vec<String>> = [("G", vec!["4"]), ("S", vec!["1", "2", "3"]), ("D", vec!["5"])].into_iter().map(|(k, v)| (k.to_string(), v.into_iter().map(String::from).collect())).collect();
let s = from_node(&sexp::parse(NMOS).unwrap(), "Q_NMOS_x", Some(&m), false).unwrap();
let src = s.terms.iter().find(|t| t.name == "S").unwrap();
assert_eq!(src.nums, vec!["1", "2", "3"], "the three source pads share the one source lead");
assert_eq!(s.term("4"), s.terms.iter().position(|t| t.name == "G"));
assert_eq!(s.body.matches("(pin ").count(), 5);
let part: HashMap<String, Vec<String>> = [("G".to_string(), vec!["4".to_string()])].into_iter().collect();
assert!(from_node(&sexp::parse(NMOS).unwrap(), "x", Some(&part), false).is_err(), "a lead with no pad is refused, never guessed");
}
#[test]
fn extends_is_found_and_extracted_whole() {
let lib = format!("(kicad_symbol_lib (version 1)\n\t{R}\n\t(symbol \"R_Small\" (extends \"R\") (property \"Value\" \"a ) in text\"))\n)");
assert!(extract(&lib, "R").unwrap().ends_with("))))"));
assert_eq!(extract(&lib, "R_Small").unwrap(), "(symbol \"R_Small\" (extends \"R\") (property \"Value\" \"a ) in text\"))");
assert!(extract(&lib, "R_Sm").is_none());
}
#[test]
fn generated_symbols_are_well_formed() {
let b = box_sym("U9", "U", &[BoxPin { num: "1".into(), name: "VIN".into(), etype: "power_in".into(), y: 0 }, BoxPin { num: "2".into(), name: "EN".into(), etype: "input".into(), y: 6 }], &[BoxPin { num: "3".into(), name: "OUT".into(), etype: "output".into(), y: 0 }], 0);
assert_eq!(b.terms.len(), 3);
assert_eq!(b.terms[1].y, 6);
assert!(b.terms[0].x < 0 && b.terms[2].x > 0 && b.terms[0].x % 2 == 0, "pin ends sit on the 100 mil grid");
for s in [b, rail_sym("+3V3", false), rail_sym("GND", true), flag_sym(), unknown_sym((1, 0), false), unknown_sym((0, 1), true)] {
assert!(sexp::parse(&s.text(true)).is_ok(), "{}", s.name);
assert!(!s.body.contains("(size 1.0 "), "no text under 40 mil");
}
assert!(rail_sym("GND", true).body.contains("\"GND\" (at 0 0 0) (effects (font (size 1.27 1.27)) (hide yes))"), "the GND text is hidden");
assert!(unknown_sym((1, 0), true).body.contains("power_out"));
}
}