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
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446
//! The schematic, generated from the run's own files (parts.json, blocks.json, nets.json,
//! net-evidence.json), so the schematic and the board cannot disagree: every symbol pin is a footprint
//! pad number and every connection is a net of nets.json.
//!
//! What a reader gets: a root sheet with the hierarchy and a legend, and one sheet per functional block
//! (A4 when it fits, A3 at most, split when it does not). Parts are real symbols by type (`schsym`).
//! Inside a block the circuit is drawn with wires at right angles: every pin of an IC or connector
//! starts a strip that runs straight out from the pin, parts in series sit in line on it, parts to a rail
//! hang from it (down to ground, up to a supply), and decoupling capacitors hang at the supply pin of
//! the IC they sit nearest on the board. A net that leaves the strip ends in a label at the end of its
//! wire, the text running back along the wire. Rails use power symbols.
//!
//! Evidence is on the sheet: a wire is solid when the pad it leads to was seen joined to that net's
//! copper in the photos and dashed when the join is inferred. A pin whose net is not known ends in a "?"
//! marker (not a no-connect flag, which would claim the pin is unused). No-connect flags appear only
//! for pins nets.json lists under `noConnect`. Each symbol carries Evidence, Test and Marking fields.
//!
//! Optional part fields the AI may fill in parts.json: `symbol` ("Library:Name" in KiCad's standard
//! libraries, used when its pin numbers are exactly the footprint's pads) and `pins` (pad number to pin
//! name, or to {"name", "type"}; a transistor named G/S/D or B/C/E gets its drawing with those pads, an
//! IC gets a box grouped by function).
use crate::kicad_out::{self, uuid};
use crate::schsym::{self, ascii, esc, safe, BoxPin, Libs, Sym, Term, STEP};
use serde_json::{json, Value};
use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet};
use std::path::Path;
/// Pad numbers of a footprint text, in order, without repeats or unnamed (mechanical) pads.
pub fn pad_numbers(text: &str) -> Vec<String> {
let mut v: Vec<String> = vec![];
if let Ok(n) = crate::sexp::parse(text) {
for p in n.children("pad") { let k = p.arg(0).unwrap_or_default(); if !k.is_empty() && !v.contains(&k) { v.push(k); } }
}
v.sort_by(|a, b| natural(a, b));
v
}
pub fn natural(a: &str, b: &str) -> std::cmp::Ordering {
let split = |s: &str| -> (String, u64, String) { let i = s.find(|c: char| c.is_ascii_digit()).unwrap_or(s.len()); let j = s[i..].find(|c: char| !c.is_ascii_digit()).map(|k| i + k).unwrap_or(s.len()); (s[..i].to_string(), s[i..j].parse().unwrap_or(u64::MAX), s[j..].to_string()) };
split(a).cmp(&split(b))
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum Kind { R, C, L, Fb, D, Led, Xtal, Q, Conn, Tp, Sw, Hole, Fid, Fuse, Ic, Other }
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum Rail { No, Supply, Ground }
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
enum Role { Anchor, Two, One, Zero }
pub fn kind_of(r: &str, footprint: &str) -> Kind {
let p: String = r.chars().take_while(|c| c.is_ascii_alphabetic()).collect::<String>().to_uppercase();
match p.as_str() {
"R" | "RN" => Kind::R, "C" => Kind::C, "L" => Kind::L, "FB" => Kind::Fb, "LED" => Kind::Led,
"D" | "Z" | "TVS" => if footprint.contains("LED") { Kind::Led } else { Kind::D },
"Y" | "X" | "XTAL" => Kind::Xtal, "Q" | "T" => Kind::Q, "J" | "JP" | "P" | "CN" | "CON" => Kind::Conn, "TP" => Kind::Tp,
"SW" | "S" | "BTN" => Kind::Sw, "H" | "MH" => Kind::Hole, "FID" => Kind::Fid, "F" => Kind::Fuse, "U" | "IC" => Kind::Ic, _ => Kind::Other,
}
}
fn is_ground(n: &str) -> bool { let u = n.to_uppercase(); u == "GND" || u == "VSS" || u == "0V" || u == "EP" || u == "PAD" || u.ends_with("GND") || u.starts_with("GND") || u.starts_with("VSS") }
fn is_supply(n: &str) -> bool {
let u = n.to_uppercase(); let b = u.as_bytes();
// VBUS_EN, VIN_SENSE and the like are signals about a rail, not the rail
let last = u.rsplit(['_', '.']).next().unwrap_or("").trim_start_matches('N');
if u.contains('_') && ["EN", "ENABLE", "SENSE", "SNS", "MON", "ADC", "DET", "OK", "PG", "PGOOD", "FAULT", "FLT", "OVERRIDE", "ON", "OFF", "CTRL", "SEL", "FB"].contains(&last) { return false; }
if u.starts_with('+') || ["VCC", "VDD", "VIN", "VBAT", "VBUS", "VREF", "AVDD", "DVDD", "VSYS", "VPP"].iter().any(|k| u.starts_with(k)) || u == "VM" { return true; }
// a voltage in the name: 3V3, 5V, 3.3V
(0..b.len()).any(|i| b[i] == b'V' && i > 0 && b[i - 1].is_ascii_digit() && (i + 1 == b.len() || b[i + 1].is_ascii_digit() || b[i + 1] == b'_'))
}
pub fn rail_of(name: &str) -> Rail { if is_ground(name) { Rail::Ground } else if is_supply(name) { Rail::Supply } else { Rail::No } }
/// A value as the sheet rules write it: capacitors without the F (100n, 10u), resistors with R, K, M
/// (0R002, 22R, 2.2K), everything in ASCII; a note in brackets stays in the Description.
pub fn sheet_value(kind: Kind, raw: &str) -> String {
let t = raw.split(" (").next().unwrap_or("").trim();
if t.is_empty() { return "?".into(); }
let s: String = t.replace(' ', "");
let num_end = s.find(|c: char| !(c.is_ascii_digit() || c == '.')).unwrap_or(s.len());
let (num, unit) = (&s[..num_end], &s[num_end..]);
let ok = !num.is_empty() && num.parse::<f64>().is_ok();
match kind {
Kind::C if ok => { let u = ascii(unit); let u = u.trim_end_matches(['F', 'f']); if matches!(u, "p" | "n" | "u" | "m" | "") { return format!("{num}{u}"); } }
Kind::R if ok => {
let x: f64 = num.parse().unwrap_or(0.0);
match unit { "mΩ" | "mR" | "mOhm" => { let ohm = x / 1000.0; let frac = format!("{:.6}", ohm.fract()); return format!("{}R{}", ohm.trunc() as u64, frac[2..].trim_end_matches('0')); }
"Ω" | "R" | "Ohm" | "" => return format!("{num}R"), "kΩ" | "k" | "K" | "kOhm" => return format!("{num}K"), "MΩ" | "M" | "MOhm" => return format!("{num}M"), _ => {} }
}
_ => {}
}
let a = ascii(t); if a.chars().count() > 30 { a.chars().take(29).collect::<String>() + "~" } else { a }
}
#[derive(Clone, Debug)]
struct BoxSpec { left: Vec<BoxPin>, right: Vec<BoxPin> }
struct Part { r: String, kind: Kind, pads: Vec<String>, sym: Option<Sym>, boxed: Option<BoxSpec>, value: String, fields: Vec<(String, String)>, pos: (f64, f64), role: Role }
struct Net { name: String, rail: Rail, pins: Vec<(usize, String)>, named: bool, measured: bool, fixed_rail: bool }
struct Block { id: String, name: String, parts: Vec<usize> }
pub struct Design {
name: String, parts: Vec<Part>, nets: Vec<Net>, blocks: Vec<Block>,
pin_net: HashMap<(usize, String), usize>, seen: HashSet<(usize, String)>, nc: HashSet<(usize, String)>, pad_pos: HashMap<(usize, String), (f64, f64)>,
/// footprints the program draws itself (lugs, holes), by their own names, for the project's footprint library
generated: BTreeMap<String, String>,
pub notes: Vec<String>,
/// What the root sheet must say in words under its legend: a face whose copper was not read (`notread`), so a reader of the
/// sheets knows why pins on that face end in a ? marker.
pub carried: Vec<String>,
}
fn pins_field(v: &Value) -> HashMap<String, (String, String)> {
v["pins"].as_object().map(|o| o.iter().map(|(k, p)| (k.clone(), match p { Value::String(s) => (s.clone(), String::new()), _ => (p["name"].as_str().unwrap_or("").to_string(), p["type"].as_str().unwrap_or("").to_string()) })).collect()).unwrap_or_default()
}
const ETYPES: [&str; 11] = ["input", "output", "bidirectional", "tri_state", "passive", "free", "unspecified", "power_in", "power_out", "open_collector", "open_emitter"];
impl Design {
/// Everything the generator draws from, read from the run's documents.
pub fn load(cache: &Path, libs: &mut Libs, parts: &[Value], blocks: &Value, nets_doc: &Value, evidence: &Value, name: &str) -> Design {
let mut d = Design { name: name.to_string(), parts: vec![], nets: vec![], blocks: vec![], pin_net: HashMap::new(), seen: HashSet::new(), nc: HashSet::new(), pad_pos: HashMap::new(), generated: BTreeMap::new(), notes: vec![], carried: vec![] };
let mut idx: HashMap<String, usize> = HashMap::new();
for p in parts {
let r = p["ref"].as_str().unwrap_or("?").to_string();
let mut fp = p["footprint"].as_str().unwrap_or("").to_string();
let pads = match kicad_out::footprint_text(cache, &fp, p) {
Ok(t) => {
// a generated footprint is filed under its own name, so the Footprint field names a real library entry
if let (Some(lib), Some(n)) = (fp.strip_suffix(":lug").or(fp.strip_suffix(":hole")), crate::sexp::parse(&t).ok().and_then(|n| n.arg(0))) { fp = format!("{lib}:{n}"); d.generated.insert(n, t.clone()); }
pad_numbers(&t)
}
Err(e) => { d.notes.push(format!("{r}: {e}; drawn without pins")); vec![] }
};
let kind = kind_of(&r, &fp);
let raw = p["value"].as_str().unwrap_or("");
let mut fields = vec![("Footprint".to_string(), fp.clone()), ("Datasheet".into(), "~".into())];
let desc: Vec<&str> = [p["function"].as_str().unwrap_or(""), raw, p["package"].as_str().unwrap_or("")].into_iter().filter(|s| !s.is_empty()).collect();
fields.push(("Description".into(), desc.join("; ")));
let mk = p["marking"].as_str().unwrap_or("");
if !mk.is_empty() && mk != "none" { fields.push(("Marking".into(), mk.to_string())); }
fields.push(("Evidence".into(), p["tag"].as_str().unwrap_or("UNKNOWN").to_string()));
let tests: Vec<&str> = p["tests"].as_array().into_iter().flatten().filter_map(|t| t.as_str()).collect();
if !tests.is_empty() { fields.push(("Test".into(), tests.join(" "))); }
idx.insert(r.clone(), d.parts.len());
d.parts.push(Part { r, kind, pads, sym: None, boxed: None, value: sheet_value(kind, raw), fields, pos: (p["pos_mm"][0].as_f64().unwrap_or(0.0), p["pos_mm"][1].as_f64().unwrap_or(0.0)), role: Role::Zero });
}
for pd in kicad_out::pads_of(cache, parts) { if let Some(&i) = idx.get(&pd.part) { d.pad_pos.entry((i, pd.num.clone())).or_insert((pd.x, pd.y)); } }
let mut used: HashSet<String> = HashSet::new();
for n in nets_doc["nets"].as_array().into_iter().flatten() {
let raw = n["name"].as_str().unwrap_or("").to_string();
let mut name: String = ascii(&raw).chars().map(|c| match c { ' ' | '/' | '\t' => '_', '{' => '(', '}' => ')', c => c }).collect();
if name.is_empty() { name = format!("N{}", d.nets.len()); }
while !used.insert(name.clone()) { name.push('_'); }
let mut pins = vec![];
for pin in n["pins"].as_array().into_iter().flatten().filter_map(|p| p.as_str()) {
let Some((r, pad)) = pin.rsplit_once('.') else { continue };
let Some(&i) = idx.get(r) else { continue }; // PHOTO1 and the like: metal no part explains
if !d.parts[i].pads.iter().any(|q| q == pad) { d.notes.push(format!("net {raw}: {pin} is no pad of {r}'s footprint; left off the sheet")); continue; }
if d.pin_net.insert((i, pad.to_string()), d.nets.len()).is_none() { pins.push((i, pad.to_string())); }
}
let unnamed = n["nameSource"].as_str().map(|s| s.starts_with("none")).unwrap_or(false);
// a net of one pin that nobody named says nothing a reader can use: the pin reads as unknown
if unnamed && pins.len() == 1 { d.pin_net.remove(&pins[0]); pins.clear(); }
let tag = n["tag"].as_str().unwrap_or("");
// nets.json may say outright what the name only suggests: "rail": "supply" | "ground" | "signal"
let rail = match n["rail"].as_str() { Some("supply") => Rail::Supply, Some("ground") => Rail::Ground, Some("signal") => Rail::No, _ => rail_of(&name) };
d.nets.push(Net { rail, name, pins, named: !unnamed, measured: matches!(tag, "SEEN" | "MEASURED"), fixed_rail: n["rail"].is_string() });
}
for p in nets_doc["noConnect"].as_array().into_iter().flatten() {
if let Some((r, pad)) = p.as_str().or_else(|| p["pin"].as_str()).and_then(|s| s.rsplit_once('.')) { if let Some(&i) = idx.get(r) { d.nc.insert((i, pad.to_string())); } }
}
// how each pin's join is known: the copper attribution's per-pin verdict, else the net's own tag
let mut have_ev = false;
for n in evidence["nets"].as_array().into_iter().flatten() {
for (pin, st) in n["pins"].as_object().into_iter().flatten() {
have_ev = true;
if matches!(st.as_str(), Some("copper") | Some("via")) { if let Some((r, pad)) = pin.rsplit_once('.') { if let Some(&i) = idx.get(r) { d.seen.insert((i, pad.to_string())); } } }
}
}
if !have_ev { for n in &d.nets { if n.measured { for p in &n.pins { d.seen.insert(p.clone()); } } } }
// symbols
for i in 0..d.parts.len() { d.choose_symbol(libs, i, &parts[i]); }
// a net that feeds a supply input is a rail whatever it is called
for i in 0..d.parts.len() {
if let Some(s) = &d.parts[i].sym { for t in &s.terms { if t.etype == "power_in" { if let Some(&n) = d.pin_net.get(&(i, t.nums[0].clone())) { if d.nets[n].rail == Rail::No && !d.nets[n].fixed_rail { d.nets[n].rail = Rail::Supply; } } } } }
if let Some(b) = &d.parts[i].boxed { for t in b.left.iter().chain(b.right.iter()) { if t.etype == "power_in" { if let Some(&n) = d.pin_net.get(&(i, t.num.clone())) { if d.nets[n].rail == Rail::No && !d.nets[n].fixed_rail { d.nets[n].rail = Rail::Supply; } } } } }
}
// blocks; parts in none get one of their own
let mut placed: HashSet<usize> = HashSet::new();
for b in blocks["blocks"].as_array().into_iter().flatten() {
let ps: Vec<usize> = b["parts"].as_array().into_iter().flatten().filter_map(|r| r.as_str()).filter_map(|r| idx.get(r).copied()).filter(|i| placed.insert(*i)).collect();
if ps.is_empty() { continue; }
let id = safe(b["id"].as_str().unwrap_or("block"));
d.blocks.push(Block { name: ascii(b["name"].as_str().unwrap_or(&id)).to_uppercase(), id, parts: ps });
}
let rest: Vec<usize> = (0..d.parts.len()).filter(|i| !placed.contains(i)).collect();
if !rest.is_empty() { d.blocks.push(Block { id: "unassigned".into(), name: "PARTS IN NO BLOCK".into(), parts: rest }); }
d
}
fn same_net(&self, pi: usize, nums: &[String]) -> bool {
match self.pin_net.get(&(pi, nums[0].clone())) { Some(n) => nums.iter().all(|k| self.pin_net.get(&(pi, k.clone())) == Some(n)), None => nums.len() == 1 }
}
/// A library symbol fits when its pin numbers are exactly the footprint's pads, and pads it stacks on one point share one known net.
fn fits(&self, pi: usize, s: &Sym) -> Result<(), String> {
let have: BTreeSet<&str> = s.terms.iter().flat_map(|t| t.nums.iter().map(|x| x.as_str())).collect();
let want: BTreeSet<&str> = self.parts[pi].pads.iter().map(|x| x.as_str()).collect();
if have != want { return Err(format!("its pins ({}) are not the footprint's pads ({})", have.iter().take(12).cloned().collect::<Vec<_>>().join(" "), want.iter().take(12).cloned().collect::<Vec<_>>().join(" "))); }
for t in &s.terms { if !self.same_net(pi, &t.nums) { return Err(format!("it draws pads {} as one lead, and nets.json does not put them on one net", t.nums.join(", "))); } }
Ok(())
}
fn choose_symbol(&mut self, libs: &mut Libs, pi: usize, v: &Value) {
let (kind, pads, r) = (self.parts[pi].kind, self.parts[pi].pads.clone(), self.parts[pi].r.clone());
let named = pins_field(v);
let text = format!("{} {} {}", v["value"].as_str().unwrap_or(""), v["function"].as_str().unwrap_or(""), v["footprint"].as_str().unwrap_or("")).to_lowercase();
let two = pads.len() == 2 && pads[0] == "1" && pads[1] == "2";
let mut tries: Vec<(String, String, String, Option<HashMap<String, Vec<String>>>, bool)> = vec![];
if let Some((lib, name)) = v["symbol"].as_str().and_then(|s| s.split_once(':')) { tries.push((lib.into(), name.into(), safe(name), None, false)); }
let dev = |n: &str, hide: bool| ("Device".to_string(), n.to_string(), n.to_string(), None, hide);
match kind {
Kind::R if two => tries.push(dev("R", true)),
Kind::C if two => tries.push(dev(if v["polarised"] == json!(true) || text.contains("cp_") || text.contains("tantal") || text.contains("elec") { "C_Polarized" } else { "C" }, true)),
Kind::L if two => tries.push(dev("L", true)), Kind::Fb if two => tries.push(dev("FerriteBead", true)), Kind::Fuse if two => tries.push(dev("Fuse", true)),
Kind::D if two => tries.push(dev(if text.contains("schottky") { "D_Schottky" } else if text.contains("zener") { "D_Zener" } else if text.contains("tvs") { "D_TVS" } else { "D" }, true)),
Kind::Led if two => tries.push(dev("LED", true)),
Kind::Xtal if two => tries.push(dev("Crystal", true)),
Kind::Sw if two => tries.push(("Switch".into(), "SW_Push".into(), "SW_Push".into(), None, true)),
Kind::Tp if pads.len() == 1 => tries.push(("Connector".into(), "TestPoint".into(), "TestPoint".into(), Some([("1".to_string(), pads.clone())].into_iter().collect()), true)),
Kind::Hole | Kind::Fid if pads.is_empty() => tries.push(("Mechanical".into(), if kind == Kind::Fid { "Fiducial" } else { "MountingHole" }.into(), if kind == Kind::Fid { "Fiducial" } else { "MountingHole" }.into(), None, true)),
Kind::Hole if pads.len() == 1 => tries.push(("Mechanical".into(), "MountingHole_Pad".into(), "MountingHole_Pad".into(), Some([("1".to_string(), pads.clone())].into_iter().collect()), true)),
Kind::Q if !named.is_empty() => {
// a transistor drawing on the pads the run names: all leads named, one drawing per pad map
let mut by: BTreeMap<String, Vec<String>> = BTreeMap::new();
for p in &pads { if let Some((n, _)) = named.get(p) { by.entry(n.to_uppercase()).or_default().push(p.clone()); } }
let leads: Vec<&str> = by.keys().map(|s| s.as_str()).collect();
let shape = if leads == ["D", "G", "S"] { if text.contains("p-mos") || text.contains("pmos") || text.contains("p-ch") { Some("Q_PMOS") } else if text.contains("n-mos") || text.contains("nmos") || text.contains("n-ch") { Some("Q_NMOS") } else { None } }
else if leads == ["B", "C", "E"] { if text.contains("pnp") { Some("Q_PNP") } else if text.contains("npn") { Some("Q_NPN") } else { None } } else { None };
if let (Some(s), true) = (shape, by.values().map(|v| v.len()).sum::<usize>() == pads.len()) {
let tag = by.iter().map(|(k, v)| format!("{k}{}", v.join("."))).collect::<Vec<_>>().join("_");
tries.push(("Device".into(), s.into(), format!("{s}_{}", safe(&tag)), Some(by.into_iter().collect()), false));
}
}
Kind::Conn if !pads.is_empty() => {
let nums: Vec<&String> = pads.iter().filter(|p| p.parse::<usize>().is_ok()).collect();
let n = nums.len(); let mp = pads.len() == n + 1 && pads.iter().any(|p| p == "MP");
let in_order = (1..=n).all(|k| nums.iter().any(|p| p.parse::<usize>().ok() == Some(k)));
if in_order && n >= 1 && n <= 40 && (pads.len() == n || mp) {
let two_rows = n % 2 == 0 && n >= 4 && (text.contains(&format!("_2x{:02}", n / 2)) || text.contains(&format!("_02x{:02}", n / 2)));
if mp { let s = format!("Conn_01x{n:02}_MountingPin"); tries.push(("Connector_Generic_MountingPin".into(), s.clone(), s, None, true)); }
else if two_rows { let s = format!("Conn_02x{:02}_Odd_Even", n / 2); tries.push(("Connector_Generic".into(), s.clone(), s, None, true)); }
else { let s = format!("Conn_01x{n:02}"); tries.push(("Connector_Generic".into(), s.clone(), s, None, true)); }
}
}
_ => {}
}
for (lib, name, as_name, ren, hide) in tries {
match libs.symbol(&lib, &name, &as_name, ren.as_ref(), hide) {
Ok(s) => match self.fits(pi, &s) { Ok(()) => { self.set_role(pi, Some(s)); return; } Err(e) => self.notes.push(format!("{r}: {lib}:{name} not used: {e}")) },
Err(e) => self.notes.push(format!("{r}: {lib}:{name} not used: {e}")),
}
}
// a box: by function when the run names the pins, by pad order otherwise
let etype = |pad: &str| -> String { match named.get(pad) { Some((n, t)) => if ETYPES.contains(&t.as_str()) { t.clone() } else if is_ground(n) || is_supply(n) { "power_in".into() } else { "passive".into() }, None => "passive".into() } };
let pin = |pad: &String, group: i32| BoxPin { num: pad.clone(), name: named.get(pad).map(|x| ascii(&x.0)).unwrap_or_default(), etype: etype(pad), y: group };
let mut spec = BoxSpec { left: vec![], right: vec![] };
if pads.iter().all(|p| named.contains_key(p)) && !pads.is_empty() {
// power on top, ground at the bottom, inputs left, outputs right, a gap between groups
for p in &pads {
let (n, t) = (&named[p].0, etype(p));
let out = matches!(t.as_str(), "output" | "power_out" | "open_collector" | "open_emitter" | "tri_state") || (named[p].1.is_empty() && n.to_uppercase().contains("OUT"));
if is_ground(n) { spec.left.push(pin(p, 3)); } else if t == "power_in" || is_supply(n) { spec.left.push(pin(p, 0)); }
else if out { spec.right.push(pin(p, 1)); } else if t == "input" { spec.left.push(pin(p, 1)); } else { spec.right.push(pin(p, 2)); }
}
if spec.right.is_empty() || spec.left.len() > 3 * spec.right.len().max(1) { let mid: Vec<BoxPin> = spec.left.iter().filter(|p| p.y == 1).cloned().collect(); if mid.len() > 3 { spec.left.retain(|p| p.y != 1); spec.right.splice(0..0, mid); } }
spec.left.sort_by_key(|p| p.y); spec.right.sort_by_key(|p| p.y);
} else {
// around the package the way the pads are numbered: down the left, up the right; a centre pad last
let n = pads.len(); let body = n - n % 2; let half = body / 2;
let wide = kind == Kind::Conn && n > 8;
for (k, p) in pads.iter().enumerate() {
if k < half || (!wide && n <= 3) { spec.left.push(pin(p, 0)); } else if k < body { spec.right.push(pin(p, 0)); } else { spec.left.push(pin(p, 3)); }
}
if !wide { spec.right.reverse(); }
}
self.parts[pi].boxed = Some(spec);
self.set_role(pi, None);
}
fn set_role(&mut self, pi: usize, s: Option<Sym>) {
let p = &mut self.parts[pi];
p.role = match &s {
Some(s) => match s.terms.len() {
0 => Role::Zero,
1 if p.kind != Kind::Conn => Role::One,
2 if p.kind != Kind::Conn && s.terms[0].dir == (-s.terms[1].dir.0, -s.terms[1].dir.1) && (s.terms[0].x == s.terms[1].x || s.terms[0].y == s.terms[1].y) => Role::Two,
_ => Role::Anchor,
},
None => if p.pads.is_empty() { Role::Zero } else { Role::Anchor },
};
p.sym = s;
}
fn net_of(&self, pi: usize, pad: &str) -> Option<usize> { self.pin_net.get(&(pi, pad.to_string())).copied() }
fn pos_of(&self, pi: usize, pad: &str) -> (f64, f64) { self.pad_pos.get(&(pi, pad.to_string())).copied().unwrap_or(self.parts[pi].pos) }
}
// ---------------------------------------------------------------------------------------------
// The drawing: everything in steps of 1.27 mm, y down.
// ---------------------------------------------------------------------------------------------
type Bx = (f64, f64, f64, f64);
#[derive(Clone, Debug)]
struct Wire { a: (i32, i32), b: (i32, i32), seen: bool }
#[derive(Clone, Debug)]
struct Lab { net: usize, x: i32, y: i32, run: (i32, i32) }
#[derive(Clone, Debug)]
struct Txt { s: String, x: f64, y: f64, size: f64, bold: bool, just: i32 }
#[derive(Clone, Debug)]
struct Inst { sym: String, x: i32, y: i32, rot: i32, part: Option<usize>, /** the net a rail symbol stands for */ rail: Option<usize>, value: String, ref_at: Option<(f64, f64)>, val_at: Option<(f64, f64)> }
#[derive(Clone, Debug, Default)]
struct Cv { insts: Vec<Inst>, wires: Vec<Wire>, labels: Vec<Lab>, texts: Vec<Txt>, ncs: Vec<(i32, i32)>, boxes: Vec<Bx> }
/// Width of a text in steps at 50 mil, on the generous side so wires are never shorter than their label.
fn tw(s: &str) -> f64 { s.chars().count() as f64 * 0.92 }
fn lab_len(s: &str) -> i32 { let n = tw(s).ceil() as i32 + 2; n + n % 2 }
fn rot(v: (i32, i32), r: i32) -> (i32, i32) { match r.rem_euclid(360) { 0 => v, 90 => (v.1, -v.0), 180 => (-v.0, -v.1), _ => (-v.1, v.0) } }
fn rotf(v: (f64, f64), r: i32) -> (f64, f64) { match r.rem_euclid(360) { 0 => v, 90 => (v.1, -v.0), 180 => (-v.0, -v.1), _ => (-v.1, v.0) } }
fn hit(a: &Bx, b: &Bx, m: f64) -> bool { a.0 < b.2 + m && b.0 < a.2 + m && a.1 < b.3 + m && b.1 < a.3 + m }
impl Cv {
fn bounds(&self) -> Bx { self.boxes.iter().fold((f64::MAX, f64::MAX, f64::MIN, f64::MIN), |a, b| (a.0.min(b.0), a.1.min(b.1), a.2.max(b.2), a.3.max(b.3))) }
fn wire(&mut self, a: (i32, i32), b: (i32, i32), seen: bool) {
if a == b { return; }
self.boxes.push((a.0.min(b.0) as f64 - 0.1, a.1.min(b.1) as f64 - 0.1, a.0.max(b.0) as f64 + 0.1, a.1.max(b.1) as f64 + 0.1));
self.wires.push(Wire { a, b, seen });
}
fn label(&mut self, d: &Design, net: usize, x: i32, y: i32, run: (i32, i32)) {
let len = tw(&d.nets[net].name) + 0.5;
let (x, y) = (x as f64, y as f64);
self.boxes.push(match run { (1, 0) => (x, y - 1.4, x + len, y - 0.25), (-1, 0) => (x - len, y - 1.4, x, y - 0.25), (0, -1) => (x - 1.4, y - len, x - 0.25, y), _ => (x - 1.4, y, x - 0.25, y + len) });
self.labels.push(Lab { net, x: x as i32, y: y as i32, run });
}
fn text(&mut self, s: &str, x: f64, y: f64, size: f64, bold: bool, just: i32) {
let w = tw(s) * size / 1.27;
let x0 = match just { -1 => x, 0 => x - w / 2.0, _ => x - w };
self.boxes.push((x0, y - 0.7 * size / 1.27, x0 + w, y + 0.7 * size / 1.27));
self.texts.push(Txt { s: s.to_string(), x, y, size, bold, just });
}
fn merge(&mut self, o: &Cv, dx: i32, dy: i32) {
let (fx, fy) = (dx as f64, dy as f64);
self.insts.extend(o.insts.iter().map(|i| Inst { x: i.x + dx, y: i.y + dy, ref_at: i.ref_at.map(|p| (p.0 + fx, p.1 + fy)), val_at: i.val_at.map(|p| (p.0 + fx, p.1 + fy)), ..i.clone() }));
self.wires.extend(o.wires.iter().map(|w| Wire { a: (w.a.0 + dx, w.a.1 + dy), b: (w.b.0 + dx, w.b.1 + dy), seen: w.seen }));
self.labels.extend(o.labels.iter().map(|l| Lab { x: l.x + dx, y: l.y + dy, ..l.clone() }));
self.texts.extend(o.texts.iter().map(|t| Txt { x: t.x + fx, y: t.y + fy, ..t.clone() }));
self.ncs.extend(o.ncs.iter().map(|p| (p.0 + dx, p.1 + dy)));
self.boxes.extend(o.boxes.iter().map(|b| (b.0 + fx, b.1 + fy, b.2 + fx, b.3 + fy)));
}
fn clashes(&self, o: &Cv, dx: i32, dy: i32, m: f64) -> bool {
let (fx, fy) = (dx as f64, dy as f64);
o.boxes.iter().any(|b| { let b = (b.0 + fx, b.1 + fy, b.2 + fx, b.3 + fy); self.boxes.iter().any(|a| hit(a, &b, m)) })
}
}
/// Seat a symbol so that one of its terminals sits at `at` with its wire leaving along `want`.
fn seat(s: &Sym, t: usize, at: (i32, i32), want: (i32, i32)) -> (i32, i32, i32) {
let r = [0, 90, 180, 270].into_iter().find(|r| rot(s.terms[t].dir, *r) == want).unwrap_or(0);
let o = rot((s.terms[t].x, s.terms[t].y), r);
(at.0 - o.0, at.1 - o.1, r)
}
fn term_at(s: &Sym, t: usize, x: i32, y: i32, r: i32) -> (i32, i32) { let o = rot((s.terms[t].x, s.terms[t].y), r); (x + o.0, y + o.1) }
fn body_box(s: &Sym, x: i32, y: i32, r: i32) -> Bx {
let (a, b) = (rotf((s.bb.0, s.bb.1), r), rotf((s.bb.2, s.bb.3), r));
(x as f64 + a.0.min(b.0), y as f64 + a.1.min(b.1), x as f64 + a.0.max(b.0), y as f64 + a.1.max(b.1))
}
#[derive(Clone, Debug)]
enum Far { Rail(usize), Unknown, Label(usize) }
#[derive(Clone, Debug)]
enum Item { Shunt { part: usize, near: String, far: String, to: Far }, Tap { part: usize, pad: String }, Series { part: usize, near: String, far: String, next: Box<Chain> } }
#[derive(Clone, Debug, PartialEq)]
enum End { None, Label(usize), Rail(usize), Unknown(bool), Solo(usize), Nc }
/// One strip: a net drawn straight out from a pin (or from a label when it starts nowhere), with what hangs on it.
#[derive(Clone, Debug)]
struct Chain { net: Option<usize>, from: Option<(usize, String)>, items: Vec<Item>, end: End, lead_label: bool, lead_rail: bool }
#[derive(Clone, Copy, PartialEq)]
enum TextAt { Right, Above, Ic }
struct Lay<'a> {
d: &'a Design, syms: BTreeMap<String, Sym>, box_syms: HashMap<usize, Sym>, claimed: HashSet<usize>,
/// satellite part to (its pad on the shared net, anchor part, anchor terminal)
attach: HashMap<usize, (String, usize, usize)>,
in_block: HashSet<usize>, free: bool, stats: BTreeMap<&'static str, usize>,
}
/// The widest a strip may grow from a pin, and from a label, in steps.
const PIN_STRIP: i32 = 76;
const FREE_STRIP: i32 = 150;
impl<'a> Lay<'a> {
fn count(&mut self, k: &'static str) { *self.stats.entry(k).or_default() += 1; }
fn sym_of(&self, pi: usize) -> Sym { self.d.parts[pi].sym.clone().or_else(|| self.box_syms.get(&pi).cloned()).expect("symbol chosen before drawing") }
fn reg(&mut self, s: Sym) -> String { let n = s.name.clone(); self.syms.entry(n.clone()).or_insert(s); n }
fn seen(&self, pi: usize, pad: &str) -> bool { self.d.seen.contains(&(pi, pad.to_string())) }
/// A part's symbol with its reference and value where they read without touching the body.
fn put(&mut self, cv: &mut Cv, pi: usize, x: i32, y: i32, r: i32, mode: TextAt, clear: &[Bx]) {
let s = self.sym_of(pi);
let b = body_box(&s, x, y, r);
let p = &{ self.d }.parts[pi];
let (wr, wv) = (tw(&p.r), tw(&p.value));
let (ra, va) = match mode {
TextAt::Right => ((b.2 + 0.8 + wr / 2.0, (b.1 + b.3) / 2.0 - 1.0), (b.2 + 0.8 + wv / 2.0, (b.1 + b.3) / 2.0 + 1.0)),
TextAt::Above => { let x0 = (b.0 + b.2) / 2.0 - (wr + 1.0 + wv) / 2.0; ((x0 + wr / 2.0, b.1 - 1.1), (x0 + wr + 1.0 + wv / 2.0, b.1 - 1.1)) }
TextAt::Ic => {
// above the body's corner, or below it, wherever no pin's wire runs
let w = wr + 1.0 + wv;
let mut spots = vec![(b.0, b.1 - 1.2), (b.2 - w, b.1 - 1.2), (b.0, b.3 + 1.2), (b.2 - w, b.3 + 1.2)];
// a transistor: under its one left lead, clear of the leads above and below
let lefts: Vec<&Term> = s.terms.iter().filter(|t| t.dir == (-1, 0)).collect();
if lefts.len() == 1 && !s.terms.iter().any(|t| t.dir == (1, 0)) { spots.insert(0, ((x + lefts[0].x) as f64 + 2.0 - w, (y + lefts[0].y) as f64 + 1.4)); }
let free = |q: &(f64, f64)| { let t = (q.0 - 0.3, q.1 - 0.9, q.0 + w + 0.3, q.1 + 0.9); !clear.iter().any(|c| hit(c, &t, 0.0)) };
let q = spots.iter().find(|q| free(q)).copied().unwrap_or((b.0, b.1 - 1.2 - clear.iter().filter(|c| c.3 <= b.1 + 0.5).map(|c| b.1 - c.1).fold(0.0, f64::max)));
((q.0 + wr / 2.0, q.1), (q.0 + wr + 1.0 + wv / 2.0, q.1))
}
};
cv.boxes.push(b);
cv.boxes.push((ra.0 - wr / 2.0, ra.1 - 0.7, ra.0 + wr / 2.0, ra.1 + 0.7));
cv.boxes.push((va.0 - wv / 2.0, va.1 - 0.7, va.0 + wv / 2.0, va.1 + 0.7));
let name = self.reg(s);
cv.insts.push(Inst { sym: name, x, y, rot: r, part: Some(pi), rail: None, value: p.value.clone(), ref_at: Some(ra), val_at: Some(va) });
}
/// A rail symbol at the end of a wire, pointing along `point`.
fn rail(&mut self, cv: &mut Cv, net: usize, at: (i32, i32), point: (i32, i32)) { self.rail_text(cv, net, at, point, 0) }
/// The same, with the name set off to one side (`side` -1 or 1) of a symbol that points up or down, clear of a wire beside it.
fn rail_text(&mut self, cv: &mut Cv, net: usize, at: (i32, i32), point: (i32, i32), side: i32) {
let n = &{ self.d }.nets[net];
let s = schsym::rail_sym(&n.name, n.rail == Rail::Ground);
let (x, y, r) = seat(&s, 0, at, (-point.0, -point.1));
cv.boxes.push(body_box(&s, x, y, r));
let shown = !(n.rail == Rail::Ground && n.name == "GND");
let reach = if n.rail == Rail::Ground { 3.2 } else { 2.0 };
let w = tw(&n.name);
let va = (at.0 as f64 + point.0 as f64 * (reach + w / 2.0) + side as f64 * (w / 2.0 + 0.4), at.1 as f64 + point.1 as f64 * (reach + 0.3));
if shown { cv.boxes.push((va.0 - w / 2.0, va.1 - 0.7, va.0 + w / 2.0, va.1 + 0.7)); }
let name = self.reg(s);
cv.insts.push(Inst { sym: name, x, y, rot: r, part: None, rail: Some(net), value: n.name.clone(), ref_at: None, val_at: if shown { Some(va) } else { None } });
self.count("railSymbols");
}
fn unknown(&mut self, cv: &mut Cv, at: (i32, i32), point: (i32, i32), supply: bool) {
let s = schsym::unknown_sym(point, supply);
cv.boxes.push(body_box(&s, at.0, at.1, 0));
let name = self.reg(s);
cv.insts.push(Inst { sym: name, x: at.0, y: at.1, rot: 0, part: None, rail: None, value: "net not known".into(), ref_at: None, val_at: None });
self.count("unknownPins");
}
// ---- planning ----
/// Satellites on `net` this chain may take: those attached to the anchor terminal it starts at, or,
/// away from any anchor, every part still free.
fn takers(&self, net: usize, root: Option<(usize, usize)>) -> Vec<(usize, String)> {
let mut v: Vec<(usize, String)> = self.d.nets[net].pins.iter().filter(|(pi, pad)| {
self.in_block.contains(pi) && !self.claimed.contains(pi) && matches!(self.d.parts[*pi].role, Role::Two | Role::One) && match root {
Some((a, t)) => self.attach.get(pi).map(|x| x.0 == *pad && x.1 == a && x.2 == t).unwrap_or(false),
None => self.free || !self.attach.contains_key(pi),
}
}).cloned().collect();
v.sort_by(|a, b| natural(&self.d.parts[a.0].r, &self.d.parts[b.0].r));
v.dedup_by(|a, b| a.0 == b.0);
v
}
fn anchored(&self, net: usize) -> bool { self.d.nets[net].pins.iter().any(|(pi, _)| self.in_block.contains(pi) && self.d.parts[*pi].role == Role::Anchor) }
fn plan(&mut self, net: usize, from: Option<(usize, String)>, covered: &[(usize, String)], root: Option<(usize, usize)>, depth: usize, room: i32, taken: &mut Vec<usize>) -> Chain {
let d = self.d;
let rail = d.nets[net].rail != Rail::No;
let mut c = Chain { net: Some(net), from, items: vec![], end: End::None, lead_label: false, lead_rail: false };
let mut covered: Vec<(usize, String)> = covered.to_vec();
let cands = self.takers(net, root);
let mut used = if rail { 4 } else { lab_len(&d.nets[net].name) + 2 };
let mut inline: Option<Item> = None;
let n_cands = cands.len();
for (pi, pad) in cands {
let p = &d.parts[pi];
let cost = 4 + tw(&p.r).max(tw(&p.value)).ceil() as i32;
if used + cost > room || depth > 6 { break; }
let s = p.sym.as_ref().unwrap();
let me = s.term(&pad).unwrap();
if p.role == Role::One { self.claimed.insert(pi); taken.push(pi); covered.push((pi, pad.clone())); c.items.push(Item::Tap { part: pi, pad }); used += cost; continue; }
let far = s.terms[1 - me].nums[0].clone();
let m = d.net_of(pi, &far);
self.claimed.insert(pi); taken.push(pi); covered.push((pi, pad.clone())); used += cost;
match m {
Some(m) if m == net => { covered.push((pi, far.clone())); c.items.push(Item::Shunt { part: pi, near: pad, far, to: if rail { Far::Rail(net) } else { Far::Label(net) } }); }
Some(m) if d.nets[m].rail != Rail::No => {
// a lone part to a rail at the end of a series run stays in line; otherwise it hangs
if inline.is_none() && n_cands == 1 && depth > 0 && !rail { inline = Some(Item::Series { part: pi, near: pad, far: far.clone(), next: Box::new(Chain { net: Some(m), from: Some((pi, far)), items: vec![], end: End::Rail(m), lead_label: false, lead_rail: false }) }); }
else { c.items.push(Item::Shunt { part: pi, near: pad, far, to: Far::Rail(m) }); }
}
Some(m) => {
if inline.is_none() && !rail {
let next = if self.free || !self.anchored(m) { self.plan(m, Some((pi, far.clone())), &[(pi, far.clone())], None, depth + 1, room - used - 8, taken) }
else { Chain { net: Some(m), from: Some((pi, far.clone())), items: vec![], end: End::Label(m), lead_label: false, lead_rail: false } };
inline = Some(Item::Series { part: pi, near: pad, far, next: Box::new(next) });
used += 8;
} else { c.items.push(Item::Shunt { part: pi, near: pad, far, to: Far::Label(m) }); used += lab_len(&d.nets[m].name); }
}
None => c.items.push(Item::Shunt { part: pi, near: pad, far, to: Far::Unknown }),
}
}
let others = d.nets[net].pins.iter().any(|p| !covered.contains(p));
let named = d.nets[net].named;
if rail { if c.from.is_some() { c.end = End::Rail(net); } else { c.lead_rail = true; } }
else if inline.is_some() || c.from.is_none() { c.lead_label = others || named; }
else if others { c.end = End::Label(net); }
else if c.items.is_empty() { c.end = End::Solo(net); }
else { c.lead_label = named; }
if let Some(i) = inline { c.items.push(i); }
c
}
// ---- drawing a strip ----
fn draw_chain(&mut self, cv: &mut Cv, c: &Chain, x0: i32, y: i32, dir: i32, lead: i32) {
let d = self.d;
let root_seen = match &c.from { Some((pi, pad)) => self.seen(*pi, pad), None => c.net.map(|n| d.nets[n].measured).unwrap_or(false) };
let mut cur = x0;
// a run that starts nowhere (no pin, label or rail symbol) has no wire before its first part
let mut started = c.from.is_some() || c.lead_label || c.lead_rail;
let mut edge = x0 as f64 + (dir * lead) as f64; // the strip is taken up to here
let far_of = |a: f64, b: f64| if dir > 0 { a.max(b) } else { a.min(b) };
if c.lead_label { let n = c.net.unwrap(); let len = lab_len(&d.nets[n].name); let off = if c.from.is_some() { dir * lead.max(1) } else { 0 }; cv.label(d, n, x0 + off, y, (dir, 0)); edge = far_of(edge, (x0 + off + dir * len) as f64); self.count("labels"); }
if c.lead_rail { let n = c.net.unwrap(); self.rail(cv, n, (x0, y), (0, if d.nets[n].rail == Rail::Ground { 1 } else { -1 })); edge = far_of(edge, (x0 + dir * 3) as f64); }
// the next junction on the trunk, clear of what is already drawn; `l` and `r` are the element's reach left and right
let next_x = |edge: f64, cur: i32, l: f64, r: f64| -> i32 {
let x = if dir > 0 { (edge + 1.0 + l).ceil() as i32 } else { (edge - 1.0 - r).floor() as i32 };
let x = if dir > 0 { x.max(cur + 2) } else { x.min(cur - 2) };
if x.rem_euclid(2) != 0 { x + dir } else { x }
};
// hanging parts in reference order as the sheet reads, left to right; the part in line comes last
let mut order: Vec<&Item> = c.items.iter().filter(|i| !matches!(i, Item::Series { .. })).collect();
if dir < 0 { order.reverse(); }
order.extend(c.items.iter().filter(|i| matches!(i, Item::Series { .. })));
for it in order {
match it {
Item::Shunt { part, near, far, to } => {
let v = if matches!(to, Far::Rail(n) if d.nets[*n].rail == Rail::Supply) { -1 } else { 1 };
let s = self.sym_of(*part);
let (tn, tf) = (s.term(near).unwrap(), s.term(far).unwrap());
let half = s.bb.0.abs().max(s.bb.2.abs()).max(1.0);
let wt = tw(&d.parts[*part].r).max(tw(&d.parts[*part].value));
let (mut l, mut r) = (half, half + 0.8 + wt);
if let Far::Label(m) = to { let len = lab_len(&d.nets[*m].name) as f64; if dir > 0 { r = r.max(len) } else { l = l.max(len) } }
let jx = next_x(edge, cur, l, r);
if started { cv.wire((cur, y), (jx, y), root_seen); }
started = true;
let a = (jx, y + 2 * v);
let (sx, sy, rr) = seat(&s, tn, a, (0, -v));
let b = term_at(&s, tf, sx, sy, rr);
cv.wire((jx, y), a, self.seen(*part, near));
self.put(cv, *part, sx, sy, rr, TextAt::Right, &[]);
let e = (b.0, b.1 + 2 * v);
let fs = self.seen(*part, far);
cv.wire(b, e, fs);
match to {
Far::Rail(n) => self.rail(cv, *n, e, (0, v)),
Far::Unknown => self.unknown(cv, e, (0, v), false),
Far::Label(m) => { let f = (e.0 + dir * lab_len(&d.nets[*m].name), e.1); cv.wire(e, f, fs); cv.label(d, *m, f.0, f.1, (-dir, 0)); self.count("labels"); }
}
edge = (jx as f64) + if dir > 0 { r } else { -l };
cur = jx;
}
Item::Tap { part, pad } => {
let s = self.sym_of(*part);
let half = s.bb.0.abs().max(s.bb.2.abs()).max(1.0);
let wt = tw(&d.parts[*part].r).max(tw(&d.parts[*part].value));
let jx = next_x(edge, cur, half, half + 0.8 + wt);
if started { cv.wire((cur, y), (jx, y), root_seen); }
started = true;
let a = (jx, y - 2);
let (sx, sy, rr) = seat(&s, 0, a, (0, 1));
cv.wire((jx, y), a, self.seen(*part, pad));
self.put(cv, *part, sx, sy, rr, TextAt::Right, &[]);
edge = (jx as f64) + if dir > 0 { half + 0.8 + wt } else { -half };
cur = jx;
}
Item::Series { part, near, far, next } => {
let s = self.sym_of(*part);
let (tn, tf) = (s.term(near).unwrap(), s.term(far).unwrap());
let len = (s.terms[tn].x - s.terms[tf].x).abs() + (s.terms[tn].y - s.terms[tf].y).abs();
let over = ((tw(&d.parts[*part].r) + 1.0 + tw(&d.parts[*part].value) - len as f64) / 2.0).max(0.0);
let nx = next_x(edge, cur, over, over);
let knee = nx - 2 * dir;
if !started {} else if (knee - cur) * dir > 0 { cv.wire((cur, y), (knee, y), root_seen); cv.wire((knee, y), (nx, y), self.seen(*part, near)); } else { cv.wire((cur, y), (nx, y), self.seen(*part, near)); }
let (sx, sy, rr) = seat(&s, tn, (nx, y), (-dir, 0));
let b = term_at(&s, tf, sx, sy, rr);
self.put(cv, *part, sx, sy, rr, TextAt::Above, &[]);
self.draw_chain(cv, next, b.0, y, dir, over.ceil() as i32 + 1);
return;
}
}
}
match &c.end {
End::None => {}
End::Label(n) => { let f = cur + dir * lab_len(&d.nets[*n].name).max(((edge - cur as f64) * dir as f64).ceil() as i32); let f = if f.rem_euclid(2) != 0 { f + dir } else { f }; cv.wire((cur, y), (f, y), root_seen); cv.label(d, *n, f, y, (-dir, 0)); self.count("labels"); }
End::Solo(n) => { let f = cur + dir * lab_len(&d.nets[*n].name); cv.wire((cur, y), (f, y), root_seen); cv.label(d, *n, f, y, (-dir, 0)); self.unknown(cv, (f, y), (dir, 0), false); self.count("labels"); }
End::Rail(n) => { let f = cur + dir * if c.items.is_empty() { (lead + lead % 2).max(2) } else { 2 }; cv.wire((cur, y), (f, y), root_seen); self.rail(cv, *n, (f, y), (dir, 0)); }
End::Unknown(supply) => { let f = cur + dir * if c.items.is_empty() { (lead + lead % 2).max(2) } else { 2 }; cv.wire((cur, y), (f, y), false); self.unknown(cv, (f, y), (dir, 0), *supply); }
End::Nc => { cv.ncs.push((cur, y)); cv.boxes.push((cur as f64 - 0.6, y as f64 - 0.6, cur as f64 + 0.6, y as f64 + 0.6)); self.count("noConnects"); }
}
}
/// What one terminal of an anchor starts: nothing known, a no-connect, or a strip on its net.
fn term_chain(&mut self, pi: usize, ti: usize, t: &Term, items: bool, taken: &mut Vec<usize>) -> Chain {
let from = Some((pi, t.nums[0].clone()));
let bare = |end: End| Chain { net: None, from: from.clone(), items: vec![], end, lead_label: false, lead_rail: false };
if t.nums.iter().any(|n| self.d.nc.contains(&(pi, n.clone()))) { return bare(End::Nc); }
match self.d.net_of(pi, &t.nums[0]) {
None => bare(End::Unknown(t.etype == "power_in")),
Some(n) => {
let covered: Vec<(usize, String)> = t.nums.iter().map(|k| (pi, k.clone())).collect();
if items { self.plan(n, from, &covered, Some((pi, ti)), 0, PIN_STRIP, taken) }
else {
let others = self.d.nets[n].pins.iter().any(|p| !covered.contains(p));
let end = if self.d.nets[n].rail != Rail::No { End::Rail(n) } else if others { End::Label(n) } else { End::Solo(n) };
Chain { net: Some(n), from, items: vec![], end, lead_label: false, lead_rail: false }
}
}
}
}
/// Transistors in a column. Where one's lower lead and the next one's upper lead are the same net and no
/// other transistor of the block is on it that way, the two are drawn one above the other and wired, and
/// the net's own parts (a diode, a test point, a snubber) run right from the node between them: a
/// half-bridge reads as one, not as two transistors with the phase net named on each.
fn stacks(&mut self, block: &[usize]) -> Vec<Cv> {
let d = self.d;
let lead_of = |pi: usize, dir: (i32, i32)| -> Option<Term> { d.parts[pi].sym.as_ref()?.terms.iter().find(|t| t.dir == dir).cloned() };
let vert: Vec<usize> = block.iter().copied().filter(|&i| d.parts[i].role == Role::Anchor && d.parts[i].sym.as_ref().map(|s| s.terms.iter().filter(|t| t.dir == (0, -1)).count() == 1 && s.terms.iter().filter(|t| t.dir == (0, 1)).count() == 1 && !s.terms.iter().any(|t| t.dir == (1, 0))).unwrap_or(false)).collect();
let end_net = |pi: usize, dir: (i32, i32)| -> Option<usize> { let n = d.net_of(pi, &lead_of(pi, dir)?.nums[0])?; (d.nets[n].rail == Rail::No).then_some(n) };
let mut next: HashMap<usize, usize> = HashMap::new();
let mut under: HashSet<usize> = HashSet::new();
for &x in &vert {
let Some(n) = end_net(x, (0, 1)) else { continue };
let below: Vec<usize> = vert.iter().copied().filter(|&y| y != x && end_net(y, (0, -1)) == Some(n)).collect();
let above = vert.iter().filter(|&&z| end_net(z, (0, 1)) == Some(n)).count();
if below.len() == 1 && above == 1 && !under.contains(&below[0]) && next.get(&below[0]) != Some(&x) { next.insert(x, below[0]); under.insert(below[0]); }
}
let mut out = vec![];
for &head in vert.iter().filter(|x| next.contains_key(x) && !under.contains(x)) {
let mut seq = vec![head];
while let Some(&y) = next.get(seq.last().unwrap()) { if seq.contains(&y) { break; } seq.push(y); }
for &q in &seq { self.claimed.insert(q); }
let mut cv = self.cluster(seq[0], false, true);
let low = lead_of(seq[0], (0, 1)).unwrap();
let mut tip = (low.x, low.y);
for k in 1..seq.len() {
let (prev, this) = (seq[k - 1], seq[k]);
let (pl, tl) = (lead_of(prev, (0, 1)).unwrap(), lead_of(this, (0, -1)).unwrap());
let n = end_net(prev, (0, 1)).unwrap();
let covered: Vec<(usize, String)> = pl.nums.iter().map(|x| (prev, x.clone())).chain(tl.nums.iter().map(|x| (this, x.clone()))).collect();
let mut taken = vec![];
let mut chain = self.plan(n, Some((prev, pl.nums[0].clone())), &covered, None, 0, PIN_STRIP, &mut taken);
if chain.end == End::Solo(n) { chain.end = if d.nets[n].named { End::Label(n) } else { End::None }; }
// the node sits below whatever already hangs beside the upper transistor
let mut run = Cv::default();
self.draw_chain(&mut run, &chain, 0, 0, 1, 3);
let mut jy = tip.1 + 2;
while cv.clashes(&run, tip.0, jy, 0.6) { jy += 2; }
let j = (tip.0, jy);
cv.wire(tip, j, self.seen(prev, &pl.nums[0]));
cv.merge(&run, j.0, j.1);
let sub = self.cluster(this, true, k + 1 < seq.len());
let dx = tip.0 - tl.x;
let mut dy = j.1 + 2 - tl.y; dy += dy.rem_euclid(2);
while cv.clashes(&sub, dx, dy, 0.6) { dy += 2; }
cv.merge(&sub, dx, dy);
cv.wire(j, (tl.x + dx, tl.y + dy), self.seen(this, &tl.nums[0]));
let bl = lead_of(this, (0, 1)).unwrap();
tip = (bl.x + dx, bl.y + dy);
}
self.count("transistorStacks");
out.push(cv);
}
out
}
/// An IC, connector or other many-pin part with a strip at every pin. `skip_top`, `skip_bottom`: leave
/// that lead bare, for the caller to wire (a transistor in a column).
fn cluster(&mut self, pi: usize, skip_top: bool, skip_bottom: bool) -> Cv {
let d = self.d;
let fixed = d.parts[pi].sym.is_some();
// a box's pins get their rows here; until then a terminal carries its side and its group
let terms: Vec<Term> = match &d.parts[pi].sym {
Some(s) => s.terms.clone(),
None => { let b = d.parts[pi].boxed.as_ref().unwrap(); b.left.iter().map(|p| (p, -1)).chain(b.right.iter().map(|p| (p, 1))).map(|(p, side)| Term { nums: vec![p.num.clone()], name: p.name.clone(), etype: p.etype.clone(), x: side, y: p.y, dir: (side, 0) }).collect() }
};
let mut cv = Cv::default();
let mut rows: Vec<i32> = vec![0; terms.len()];
let mut strips: Vec<Option<Cv>> = vec![None; terms.len()];
for side in [-1, 1] {
let mut list: Vec<usize> = (0..terms.len()).filter(|&i| terms[i].dir == (side, 0)).collect();
if fixed { list.sort_by_key(|&i| terms[i].y); }
if fixed {
// Pins at the symbol's own pitch. Every pin's plain stub (its label, rail symbol or marker) fits
// the pitch. A strip with parts on it must clear the stubs of the pins around it and the strips
// already drawn, so it starts further out; when that takes it too far the pin keeps its label
// and the parts are drawn beside the symbol from the same label.
let keep = self.stats.clone();
let reach = |c: &Cv| { let b = c.bounds(); if side > 0 { b.2 } else { -b.0 } };
let mut stubs: Vec<(usize, Cv)> = list.iter().map(|&ti| { let mut t = vec![]; let c = self.term_chain(pi, ti, &terms[ti], false, &mut t); let mut sc = Cv::default(); self.draw_chain(&mut sc, &c, 0, 0, side, 0); (ti, sc) }).collect();
// a rail symbol or marker beside a labelled pin sits out past that label, so its text is clear of it
let mut leads: HashMap<usize, i32> = HashMap::new();
for k in 0..stubs.len() {
let (ti, sc) = (stubs[k].0, &stubs[k].1);
if !sc.labels.is_empty() { continue; }
let near = stubs.iter().filter(|(tj, o)| *tj != ti && (terms[*tj].y - terms[ti].y).abs() <= 2 && !o.labels.is_empty()).map(|(_, o)| reach(o)).fold(0.0, f64::max);
if near > 0.0 { leads.insert(ti, near.ceil() as i32 + 1); }
}
for (ti, sc) in stubs.iter_mut() { if let Some(l) = leads.get(ti) { let mut t = vec![]; let c = self.term_chain(pi, *ti, &terms[*ti], false, &mut t); *sc = Cv::default(); self.draw_chain(sc, &c, 0, 0, side, *l); } }
self.stats = keep;
let mut thick: Vec<(i32, Cv)> = vec![];
for &ti in &list {
let y = terms[ti].y;
let mut taken = vec![];
let chain = self.term_chain(pi, ti, &terms[ti], true, &mut taken);
let mut scv = Cv::default();
let keep = self.stats.clone();
let (mut lead, mut fits) = (leads.get(&ti).copied().unwrap_or(0), chain.items.is_empty());
for _ in 0..5 {
scv = Cv::default(); self.stats = keep.clone();
self.draw_chain(&mut scv, &chain, 0, 0, side, lead);
if fits { break; }
let hits: Vec<f64> = stubs.iter().filter(|(tj, sc)| *tj != ti && sc.clashes(&scv, 0, y - terms[*tj].y, 0.15)).map(|(_, sc)| reach(sc))
.chain(thick.iter().filter(|(py, pcv)| pcv.clashes(&scv, 0, y - py, 0.6)).map(|(_, pcv)| reach(pcv))).collect();
if hits.is_empty() { fits = true; break; }
lead = hits.iter().cloned().fold(0.0, f64::max).ceil() as i32 + 2;
if lead > 64 { break; }
}
if !fits {
self.stats = keep;
for t in taken.drain(..) { self.claimed.remove(&t); }
let c = self.term_chain(pi, ti, &terms[ti], false, &mut taken);
scv = Cv::default(); self.draw_chain(&mut scv, &c, 0, 0, side, leads.get(&ti).copied().unwrap_or(0));
self.count("stripsMovedToLabels");
} else if !chain.items.is_empty() { thick.push((y, scv.clone())); }
rows[ti] = y; strips[ti] = Some(scv);
}
continue;
}
let mut placed: Vec<(i32, bool, Cv)> = vec![];
let (mut y_next, mut group) = (0, list.first().map(|&i| terms[i].y).unwrap_or(0));
for ti in list {
// a box: the rows open up until each pin's strip has the room it needs
let mut taken = vec![];
let chain = self.term_chain(pi, ti, &terms[ti], true, &mut taken);
let mut scv = Cv::default();
self.draw_chain(&mut scv, &chain, 0, 0, side, 0);
if terms[ti].y != group { group = terms[ti].y; y_next += 2; }
let mut y = y_next;
let thin = chain.items.is_empty();
while placed.iter().any(|(py, thin2, pcv)| pcv.clashes(&scv, 0, y - py, if thin && *thin2 { 0.05 } else { 0.6 })) { y += 2; }
rows[ti] = y; y_next = y + 2;
placed.push((y, thin, scv.clone()));
strips[ti] = Some(scv);
}
}
let s = if fixed { d.parts[pi].sym.clone().unwrap() } else {
let b = d.parts[pi].boxed.as_ref().unwrap();
let at = |num: &str| rows[terms.iter().position(|t| t.nums[0] == num).unwrap()];
let rowed = |v: &[BoxPin]| -> Vec<BoxPin> { v.iter().map(|p| BoxPin { y: at(&p.num), ..p.clone() }).collect() };
let s = schsym::box_sym(&safe(&d.parts[pi].r), &d.parts[pi].r.chars().take_while(|c| c.is_ascii_alphabetic()).collect::<String>(), &rowed(&b.left), &rowed(&b.right), 0);
self.box_syms.insert(pi, s.clone());
s
};
for (ti, sc) in strips.iter().enumerate() { if let Some(sc) = sc { let t = &s.terms[s.term(&terms[ti].nums[0]).unwrap()]; cv.merge(sc, t.x, t.y); } }
// supply pins on the top edge with decoupling: one rail wire above the symbol per supply, its capacitors
// in a row on it clear of the other top pins, wired down to every pin of that supply
let tops: Vec<usize> = (0..terms.len()).filter(|&i| terms[i].dir == (0, -1)).collect();
let mut railed: HashSet<usize> = HashSet::new();
let mut level = 0;
for &ti in &tops {
let Some(n) = d.net_of(pi, &terms[ti].nums[0]) else { continue };
if railed.contains(&ti) || !self.attach.values().any(|a| a.1 == pi && a.2 == ti) { continue; }
let mut taken = vec![];
let chain = self.term_chain(pi, ti, &terms[ti], true, &mut taken);
if chain.items.is_empty() { continue; }
let mut group: Vec<usize> = tops.iter().copied().filter(|&k| d.net_of(pi, &terms[k].nums[0]) == Some(n) && terms[k].y == terms[ti].y).collect();
group.sort_by_key(|&k| terms[k].x);
let yk = terms[ti].y - 14 - 16 * level; level += 1;
for &k in &group { let sk = self.seen(pi, &terms[k].nums[0]); cv.wire((terms[k].x, terms[k].y), (terms[k].x, yk), sk); railed.insert(k); }
let st = self.seen(pi, &terms[ti].nums[0]);
for w in group.windows(2) { cv.wire((terms[w[0]].x, yk), (terms[w[1]].x, yk), st); }
let mean = group.iter().map(|&k| terms[k].x as f64).sum::<f64>() / group.len() as f64;
let dir = if mean <= (s.bb.0 + s.bb.2) / 2.0 { -1 } else { 1 };
let x0 = if dir < 0 { terms[group[0]].x } else { terms[*group.last().unwrap()].x };
let far = tops.iter().map(|&k| terms[k].x * dir).max().unwrap_or(0);
self.draw_chain(&mut cv, &chain, x0, yk, dir, far - x0 * dir + 2);
self.count("supplyRailsOverIc");
}
// other pins on the top and bottom edge of a library symbol: a rail symbol, a label up the wire, or the marker
let no_right = !terms.iter().any(|t| t.dir == (1, 0));
for (ti, t) in terms.iter().enumerate().filter(|(i, t)| t.dir.0 == 0 && !railed.contains(i) && !(skip_top && t.dir == (0, -1)) && !(skip_bottom && t.dir == (0, 1))) {
let mut taken = vec![];
let chain = self.term_chain(pi, ti, t, false, &mut taken);
let (p, v) = ((t.x, t.y), t.dir.1);
let seen = self.seen(pi, &t.nums[0]);
// a transistor's upper or lower lead with parts of its own on the net (a sense resistor): they run right from a short stub
if let (End::Label(n) | End::Solo(n), true) = (&chain.end, no_right) {
let covered: Vec<(usize, String)> = t.nums.iter().map(|k| (pi, k.clone())).collect();
let run = self.plan(*n, chain.from.clone(), &covered, None, 0, PIN_STRIP, &mut taken);
if !run.items.is_empty() { let j = (p.0, p.1 + 2 * v); cv.wire(p, j, seen); self.draw_chain(&mut cv, &run, j.0, j.1, 1, 3); continue; }
for x in taken.drain(..) { self.claimed.remove(&x); }
}
match chain.end {
End::Nc => { cv.ncs.push(p); self.count("noConnects"); }
End::Rail(n) => {
// beside another pin of this edge the name goes to the open side
let near = |dx: i32| terms.iter().any(|o| o.dir == t.dir && o.y == t.y && (o.x - t.x) * dx > 0 && (o.x - t.x).abs() <= 4);
let side = match (near(-1), near(1)) { (true, false) => 1, (false, true) => -1, _ => 0 };
let e = (p.0, p.1 + 2 * v); cv.wire(p, e, seen); self.rail_text(&mut cv, n, e, (0, v), side);
}
End::Unknown(sup) => { let e = (p.0, p.1 + 2 * v); cv.wire(p, e, false); self.unknown(&mut cv, e, (0, v), sup); }
End::Label(n) | End::Solo(n) => {
let e = (p.0, p.1 + v * lab_len(&d.nets[n].name)); cv.wire(p, e, seen); cv.label(d, n, e.0, e.1, (0, -v)); self.count("labels");
if chain.end == End::Solo(n) { self.unknown(&mut cv, e, (0, v), false); }
}
End::None => {}
}
}
let mut clear = cv.boxes.clone();
// a lead left for the caller to wire still keeps the text off its line
for t in terms.iter().filter(|t| (skip_top && t.dir == (0, -1)) || (skip_bottom && t.dir == (0, 1))) { clear.push((t.x as f64 - 0.5, t.y as f64 + if t.dir.1 < 0 { -40.0 } else { 0.0 }, t.x as f64 + 0.5, t.y as f64 + if t.dir.1 < 0 { 0.0 } else { 40.0 })); }
self.put(&mut cv, pi, 0, 0, 0, TextAt::Ic, &clear);
cv
}
/// Parts no pin strip took: runs that start at a label or a rail, and single parts with what is known at each end.
fn leftovers(&mut self, block: &[usize]) -> Vec<Cv> {
let d = self.d;
self.free = true;
let mut out = vec![];
let mut order: Vec<usize> = block.iter().copied().filter(|i| matches!(d.parts[*i].role, Role::Two | Role::One)).collect();
order.sort_by(|a, b| natural(&d.parts[*a].r, &d.parts[*b].r));
// runs on signal nets first, then rows on rails
for want_rail in [false, true] {
for &pi in &order {
if self.claimed.contains(&pi) { continue; }
let s = d.parts[pi].sym.as_ref().unwrap();
let nets: Vec<usize> = s.terms.iter().filter_map(|t| d.net_of(pi, &t.nums[0])).collect();
let pick = if !want_rail { nets.iter().copied().filter(|n| d.nets[*n].rail == Rail::No).max_by_key(|n| (self.takers(*n, None).len(), std::cmp::Reverse(*n))) }
// a part between rails goes on the supply's row (between two grounds, on the first)
else if nets.len() == s.terms.len() && nets.iter().all(|n| d.nets[*n].rail != Rail::No) { nets.iter().copied().find(|n| d.nets[*n].rail == Rail::Supply).or(nets.first().copied()) }
else { None };
let Some(n) = pick else { continue };
let mut taken = vec![];
let chain = self.plan(n, None, &[], None, 0, FREE_STRIP, &mut taken);
if chain.items.is_empty() { continue; }
let mut cv = Cv::default();
self.draw_chain(&mut cv, &chain, 0, 0, 1, 0);
self.count(if want_rail { "railRows" } else { "labelStrips" });
out.push(cv);
}
}
// single parts: each end shows its rail, or the marker
let mut cells: Vec<Cv> = vec![];
let mut zero: Vec<usize> = block.iter().copied().filter(|i| d.parts[*i].role == Role::Zero).collect();
zero.sort_by(|a, b| natural(&d.parts[*a].r, &d.parts[*b].r));
for &pi in order.iter().chain(zero.iter()) {
if self.claimed.contains(&pi) { continue; }
self.claimed.insert(pi);
if d.parts[pi].role == Role::Zero && d.parts[pi].sym.is_none() {
// a part whose pads the photos do not show: a box with no pins
let s = schsym::box_sym(&safe(&d.parts[pi].r), &d.parts[pi].r.chars().take_while(|c| c.is_ascii_alphabetic()).collect::<String>(), &[], &[], tw(&d.parts[pi].value).ceil() as i32 + 4);
self.box_syms.insert(pi, s);
}
let s = self.sym_of(pi);
let mut cv = Cv::default();
// stand a two-pin part upright with any ground at the bottom
let top = if s.terms.len() == 2 { let g = |t: &Term| d.net_of(pi, &t.nums[0]).map(|n| d.nets[n].rail == Rail::Ground).unwrap_or(false); if g(&s.terms[0]) && !g(&s.terms[1]) { 1 } else { 0 } } else { 0 };
let (sx, sy, r) = if s.terms.is_empty() { (0, 0, 0) } else { seat(&s, top, (0, 0), if s.terms.len() == 1 { (0, 1) } else { (0, -1) }) };
for (ti, t) in s.terms.iter().enumerate() {
let p = term_at(&s, ti, sx, sy, r); let v = rot(t.dir, r);
let e = (p.0 + 2 * v.0, p.1 + 2 * v.1);
match d.net_of(pi, &t.nums[0]) {
Some(n) if d.nets[n].rail != Rail::No => { cv.wire(p, e, self.seen(pi, &t.nums[0])); self.rail(&mut cv, n, e, v); }
Some(n) => { let f = (p.0 + v.0 * lab_len(&d.nets[n].name), p.1 + v.1 * lab_len(&d.nets[n].name)); cv.wire(p, f, self.seen(pi, &t.nums[0])); cv.label(d, n, f.0, f.1, (-v.0, -v.1)); self.count("labels"); }
None => { cv.wire(p, e, false); self.unknown(&mut cv, e, v, false); }
}
}
self.put(&mut cv, pi, sx, sy, r, if s.terms.is_empty() { TextAt::Above } else { TextAt::Right }, &[]);
if d.parts[pi].role == Role::Zero && d.parts[pi].kind != Kind::Hole && d.parts[pi].kind != Kind::Fid { let b = cv.bounds(); cv.text("no pads read from the photos", (b.0 + b.2) / 2.0, b.3 + 1.2, 1.016, false, 0); }
self.count("singleParts");
cells.push(cv);
}
// the single parts in rows
let mut row = Cv::default(); let mut x = 0;
for c in cells {
let b = c.bounds(); let w = (b.2 - b.0).ceil() as i32 + 3; let w = w + w % 2;
if x + w > FREE_STRIP && x > 0 { out.push(std::mem::take(&mut row)); x = 0; }
let dx = x - b.0.floor() as i32; row.merge(&c, dx + dx.rem_euclid(2), 0); x += w + 2;
}
if !row.boxes.is_empty() { out.push(row); }
self.free = false;
out
}
/// Every satellite goes to one anchor pin: on a signal net before a rail, an IC before a connector,
/// then the nearest on the board. Decoupling goes to the supply pin it sits beside.
fn attach_block(&mut self, block: &[usize]) {
let d = self.d;
self.attach.clear();
self.in_block = block.iter().copied().collect();
let mut anchors: HashMap<usize, Vec<(usize, usize, String)>> = HashMap::new(); // net to (part, terminal, pad)
for &pi in block.iter().filter(|i| d.parts[**i].role == Role::Anchor) {
let terms: Vec<Vec<String>> = match &d.parts[pi].sym { Some(s) => s.terms.iter().map(|t| t.nums.clone()).collect(), None => { let b = d.parts[pi].boxed.as_ref().unwrap(); b.left.iter().chain(b.right.iter()).map(|p| vec![p.num.clone()]).collect() } };
let dirs: Vec<(i32, i32)> = match &d.parts[pi].sym { Some(s) => s.terms.iter().map(|t| t.dir).collect(), None => vec![(1, 0); terms.len()] };
let mut top_rails: HashSet<usize> = HashSet::new();
for (ti, nums) in terms.iter().enumerate() {
let Some(n) = d.net_of(pi, &nums[0]) else { continue };
// a side pin starts a strip; the first top pin of each supply takes that supply's decoupling
if dirs[ti].0 != 0 || (dirs[ti] == (0, -1) && d.nets[n].rail == Rail::Supply && top_rails.insert(n)) { anchors.entry(n).or_default().push((pi, ti, nums[0].clone())); }
}
}
for &pi in block.iter().filter(|i| matches!(d.parts[**i].role, Role::Two | Role::One)) {
let s = d.parts[pi].sym.as_ref().unwrap();
let mut best: Option<((i32, i32, i64), String, usize, usize)> = None;
// a part with a signal on one pin belongs to that signal's run (resistor then LED then rail), never to the rail on its other pin
let on_signal = s.terms.iter().any(|t| d.net_of(pi, &t.nums[0]).map(|n| d.nets[n].rail == Rail::No).unwrap_or(false));
for t in &s.terms {
let pad = &t.nums[0];
let Some(n) = d.net_of(pi, pad) else { continue };
let rank = match d.nets[n].rail { Rail::No => 0, Rail::Supply => 1, Rail::Ground => 2 };
// ground alone never places a part: a capacitor to ground belongs to the net on its other pin
if (rank == 2 && s.terms.len() == 2) || (rank > 0 && on_signal) { continue; }
let here = d.pos_of(pi, pad);
for (a, ti, apad) in anchors.get(&n).into_iter().flatten() {
let q = d.pos_of(*a, apad);
let dist = (((here.0 - q.0).powi(2) + (here.1 - q.1).powi(2)).sqrt() * 100.0) as i64;
let key = (rank, if d.parts[*a].kind == Kind::Conn { 1 } else { 0 }, dist);
if best.as_ref().map(|b| key < b.0).unwrap_or(true) { best = Some((key, pad.clone(), *a, *ti)); }
}
}
if let Some((_, pad, a, ti)) = best { self.attach.insert(pi, (pad, a, ti)); }
}
}
}
// ---------------------------------------------------------------------------------------------
// Sheets
// ---------------------------------------------------------------------------------------------
struct Sheet { file: String, title: String, paper: &'static str, cv: Cv, block: usize }
/// Paper sizes in steps: (name, width, height, right edge of the block frame, bottom edge above the title block).
const PAPERS: [(&str, f64, f64, i32, i32); 2] = [("A4", 297.0, 210.0, 224, 128), ("A3", 420.0, 297.0, 320, 196)];
const FRAME: i32 = 10;
/// Each group goes to the highest free spot that takes it, leftmost first (a skyline): a tall cluster
/// does not leave the room beside it empty. A group that cannot go on this sheet starts the next.
fn pack(groups: &[Cv], w: i32, h: i32) -> (Vec<Vec<(usize, i32, i32)>>, bool) {
let mut sheets: Vec<Vec<(usize, i32, i32)>> = vec![vec![]];
let mut sky = vec![0i32; w.max(1) as usize];
let mut fits = true;
for (k, g) in groups.iter().enumerate() {
let b = g.bounds();
let (gw, gh) = (((b.2 - b.0).ceil() as i32 + 6).min(w), (b.3 - b.1).ceil() as i32 + 4);
if (b.2 - b.0).ceil() as i32 + 2 > w || gh - 2 > h { fits = false; }
let spot = |sky: &[i32]| -> Option<(i32, i32)> {
(0..=(w - gw)).step_by(2).map(|x| (x, sky[x as usize..(x + gw) as usize].iter().copied().max().unwrap_or(0))).filter(|(_, y)| y + gh - 2 <= h).min_by_key(|(x, y)| (*y, *x))
};
let (x, y) = match spot(&sky) { Some(p) => p, None => { if !sheets.last().unwrap().is_empty() { sheets.push(vec![]); sky = vec![0; w.max(1) as usize]; } spot(&sky).unwrap_or((0, 0)) } };
for c in &mut sky[x as usize..(x + gw) as usize] { *c = y + gh; }
sheets.last_mut().unwrap().push((k, x, y));
}
(sheets, fits)
}
fn place(groups: &[Cv], spots: &[(usize, i32, i32)], ox: i32, oy: i32) -> Cv {
let mut cv = Cv::default();
for (k, x, y) in spots {
let b = groups[*k].bounds();
let (dx, dy) = (ox + x - b.0.floor() as i32, oy + y - b.1.floor() as i32);
cv.merge(&groups[*k], dx + dx.rem_euclid(2), dy + dy.rem_euclid(2));
}
cv
}
pub struct Project { pub files: Vec<(String, String)>, pub notes: Vec<String>, pub stats: Value, /** where the drawn sheets and nets.json disagree (none, or the generator has a fault) */ pub differences: Vec<String> }
/// The connections the drawn sheets hold, worked out from their geometry alone (wire ends, pins, labels by
/// name, rail symbols by rail), against nets.json: every net's pins joined to each other and to nothing
/// else, every pin of unknown net joined to no other pin. KiCad's own netlist is the gate (`check
/// schematic`); this runs at every build, with or without KiCad, so a drawing fault never passes quietly.
fn differences(d: &Design, sheets: &[Sheet], syms: &BTreeMap<String, Sym>, box_syms: &HashMap<usize, Sym>, hier: &HashMap<usize, BTreeSet<usize>>) -> Vec<String> {
let mut parent: Vec<usize> = vec![];
fn find(p: &mut Vec<usize>, mut i: usize) -> usize { while p[i] != i { p[i] = p[p[i]]; i = p[i]; } i }
#[derive(PartialEq, Eq, Hash, Clone, Copy)]
enum Key { Pt(usize, i32, i32), Lab(usize, usize), Net(usize) }
let mut ids: HashMap<Key, usize> = HashMap::new();
let mut node = |k: Key, parent: &mut Vec<usize>| -> usize { *ids.entry(k).or_insert_with(|| { parent.push(parent.len()); parent.len() - 1 }) };
let mut join = |a: usize, b: usize, parent: &mut Vec<usize>| { let (x, y) = (find(parent, a), find(parent, b)); parent[x] = y; };
let mut pins: Vec<((usize, String), usize)> = vec![];
for (si, s) in sheets.iter().enumerate() {
for w in &s.cv.wires { let (a, b) = (node(Key::Pt(si, w.a.0, w.a.1), &mut parent), node(Key::Pt(si, w.b.0, w.b.1), &mut parent)); join(a, b, &mut parent); }
for l in &s.cv.labels {
let across = hier.get(&l.net).map(|v| v.contains(&si) && v.len() > 1).unwrap_or(false);
let (a, b) = (node(Key::Pt(si, l.x, l.y), &mut parent), node(if across { Key::Net(l.net) } else { Key::Lab(si, l.net) }, &mut parent)); join(a, b, &mut parent);
}
let mut ends: Vec<(i32, i32)> = s.cv.wires.iter().flat_map(|w| [w.a, w.b]).chain(s.cv.labels.iter().map(|l| (l.x, l.y))).collect();
for i in &s.cv.insts {
let sym = match i.part { Some(pi) => d.parts[pi].sym.as_ref().or_else(|| box_syms.get(&pi)).unwrap(), None => &syms[&i.sym] };
for t in 0..sym.terms.len() {
let p = term_at(sym, t, i.x, i.y, i.rot); ends.push(p);
let a = node(Key::Pt(si, p.0, p.1), &mut parent);
if let Some(n) = i.rail { let b = node(Key::Net(n), &mut parent); join(a, b, &mut parent); }
if let Some(pi) = i.part { for num in &sym.terms[t].nums { pins.push(((pi, num.clone()), a)); } }
}
}
// an end that lands inside another wire joins it: the way two nets would short by accident
for p in ends { for w in &s.cv.wires {
let inside = (w.a.0 == w.b.0 && p.0 == w.a.0 && p.1 > w.a.1.min(w.b.1) && p.1 < w.a.1.max(w.b.1)) || (w.a.1 == w.b.1 && p.1 == w.a.1 && p.0 > w.a.0.min(w.b.0) && p.0 < w.a.0.max(w.b.0));
if inside { let (a, b) = (node(Key::Pt(si, p.0, p.1), &mut parent), node(Key::Pt(si, w.a.0, w.a.1), &mut parent)); join(a, b, &mut parent); }
} }
}
let mut out = vec![];
let group: HashMap<(usize, String), usize> = pins.iter().map(|(k, n)| (k.clone(), find(&mut parent, *n))).collect();
let mut members: HashMap<usize, Vec<&(usize, String)>> = HashMap::new();
for (k, g) in &group { members.entry(*g).or_default().push(k); }
let name = |p: &(usize, String)| format!("{}.{}", d.parts[p.0].r, p.1);
for (pi, part) in d.parts.iter().enumerate() { for pad in &part.pads { if !group.contains_key(&(pi, pad.clone())) { out.push(format!("{}.{pad} is not on any sheet", part.r)); } } }
for n in &d.nets {
let gs: BTreeSet<usize> = n.pins.iter().filter_map(|p| group.get(p).copied()).collect();
if gs.len() > 1 { out.push(format!("net {}: drawn as {} separate pieces", n.name, gs.len())); }
for g in gs { for m in &members[&g] { if d.pin_net.get(*m).map(|x| d.nets[*x].name != n.name).unwrap_or(true) { out.push(format!("net {}: the sheet also joins {} to it", n.name, name(m))); } } }
}
for (p, g) in &group { if !d.pin_net.contains_key(p) && members[g].len() > 1 { out.push(format!("{} has no net in nets.json and the sheet joins it to {}", name(p), members[g].iter().filter(|m| **m != p).map(|m| name(m)).take(4).collect::<Vec<_>>().join(", "))); } }
out.sort(); out.dedup();
out
}
fn mm(v: f64) -> String { let s = format!("{:.3}", v * STEP); s.trim_end_matches('0').trim_end_matches('.').to_string() }
fn font(size: f64, bold: bool) -> String { format!("(font (size {size} {size}){})", if bold { " (thickness 0.381) (bold yes)" } else { "" }) }
struct Writer<'a> { d: &'a Design, root: String, virt: BTreeMap<String, usize>, hier: HashMap<usize, BTreeSet<usize>> }
impl<'a> Writer<'a> {
fn items(&mut self, cv: &Cv, syms: &BTreeMap<String, Sym>, box_syms: &HashMap<usize, Sym>, path: &str, seed: &str, sheet: Option<usize>) -> String {
let d = self.d;
let mut o = String::new();
// junction dots where three or more ends meet
let mut ends: HashMap<(i32, i32), usize> = HashMap::new();
for w in &cv.wires { *ends.entry(w.a).or_default() += 1; *ends.entry(w.b).or_default() += 1; }
for i in &cv.insts {
let s = match i.part { Some(pi) => d.parts[pi].sym.as_ref().or_else(|| box_syms.get(&pi)).unwrap(), None => &syms[&i.sym] };
for t in 0..s.terms.len() { *ends.entry(term_at(s, t, i.x, i.y, i.rot)).or_default() += 1; }
}
let mut dots: Vec<(i32, i32)> = ends.iter().filter(|(_, n)| **n >= 3).map(|(p, _)| *p).collect(); dots.sort();
for (k, p) in dots.iter().enumerate() { o += &format!("\t(junction (at {} {}) (diameter 0.9144) (color 0 0 0 0) (uuid \"{}\"))\n", mm(p.0 as f64), mm(p.1 as f64), uuid(&format!("{seed}/j{k}"))); }
for (k, p) in cv.ncs.iter().enumerate() { o += &format!("\t(no_connect (at {} {}) (uuid \"{}\"))\n", mm(p.0 as f64), mm(p.1 as f64), uuid(&format!("{seed}/nc{k}"))); }
for (k, w) in cv.wires.iter().enumerate() {
o += &format!("\t(wire (pts (xy {} {}) (xy {} {})) (stroke (width 0.1524) (type {})) (uuid \"{}\"))\n", mm(w.a.0 as f64), mm(w.a.1 as f64), mm(w.b.0 as f64), mm(w.b.1 as f64), if w.seen { "solid" } else { "dash" }, uuid(&format!("{seed}/w{k}")));
}
for (k, l) in cv.labels.iter().enumerate() {
let (ang, just) = match l.run { (1, 0) => (0, "left"), (-1, 0) => (180, "right"), (0, -1) => (90, "left"), _ => (270, "right") };
let hier = sheet.map(|s| self.hier.get(&l.net).map(|v| v.contains(&s) && v.len() > 1).unwrap_or(false)).unwrap_or(false);
if hier { o += &format!("\t(hierarchical_label \"{}\" (shape passive) (at {} {} {ang}) (effects {} (justify {just} bottom)) (uuid \"{}\"))\n", esc(&d.nets[l.net].name), mm(l.x as f64), mm(l.y as f64), font(1.27, false), uuid(&format!("{seed}/h{k}"))); }
else { o += &format!("\t(label \"{}\" (at {} {} {ang}) (effects {} (justify {just} bottom)) (uuid \"{}\"))\n", esc(&d.nets[l.net].name), mm(l.x as f64), mm(l.y as f64), font(1.27, false), uuid(&format!("{seed}/l{k}"))); }
}
for (k, t) in cv.texts.iter().enumerate() {
o += &format!("\t(text \"{}\" (exclude_from_sim no) (at {} {} 0) (effects {}{}) (uuid \"{}\"))\n", esc(&t.s), mm(t.x), mm(t.y), font(t.size, t.bold), match t.just { -1 => " (justify left)", 1 => " (justify right)", _ => "" }, uuid(&format!("{seed}/t{k}")));
}
for i in &cv.insts {
let s = match i.part { Some(pi) => d.parts[pi].sym.as_ref().or_else(|| box_syms.get(&pi)).unwrap(), None => &syms[&i.sym] };
let refd = match i.part { Some(pi) => d.parts[pi].r.clone(), None => { let n = self.virt.entry(s.prefix.clone()).or_default(); *n += 1; format!("{}{:04}", s.prefix, n) } };
let ang = if i.rot == 90 || i.rot == 270 { 90 } else { 0 };
let (x, y) = (mm(i.x as f64), mm(i.y as f64));
let real = i.part.is_some();
o += &format!("\t(symbol (lib_id \"{}\") (at {x} {y} {}) (unit 1) (exclude_from_sim {}) (in_bom {}) (on_board {}) (dnp no) (uuid \"{}\")\n", s.id(), i.rot, if real { "no" } else { "yes" }, if real { "yes" } else { "no" }, if real { "yes" } else { "no" }, uuid(&format!("{}/sym/{refd}", d.name)));
let shown = |at: Option<(f64, f64)>| match at { Some(p) => format!("(at {} {} {ang}) (effects {})", mm(p.0), mm(p.1), font(1.27, false)), None => format!("(at {x} {y} 0) (effects {} (hide yes))", font(1.27, false)) };
o += &format!("\t\t(property \"Reference\" \"{}\" {})\n", esc(&refd), shown(if real { i.ref_at } else { None }));
o += &format!("\t\t(property \"Value\" \"{}\" {})\n", esc(&i.value), shown(i.val_at));
if let Some(pi) = i.part {
// hidden fields stacked below the body
let b = body_box(s, i.x, i.y, i.rot);
for (k, (name, val)) in d.parts[pi].fields.iter().enumerate() { o += &format!("\t\t(property \"{}\" \"{}\" (at {} {} 0) (effects {} (hide yes)))\n", name, esc(val), mm((b.0 + b.2) / 2.0), mm(b.3 + 2.0 + 2.0 * k as f64), font(1.27, false)); }
} else { o += &format!("\t\t(property \"Footprint\" \"\" (at {x} {y} 0) (effects {} (hide yes)))\n\t\t(property \"Datasheet\" \"~\" (at {x} {y} 0) (effects {} (hide yes)))\n", font(1.27, false), font(1.27, false)); }
for n in s.terms.iter().flat_map(|t| t.nums.iter()) { o += &format!("\t\t(pin \"{}\" (uuid \"{}\"))\n", esc(n), uuid(&format!("{}/pin/{refd}/{n}", d.name))); }
o += &format!("\t\t(instances (project \"{}\" (path \"{path}\" (reference \"{}\") (unit 1))))\n\t)\n", esc(&d.name), esc(&refd));
}
o
}
fn file(&self, uid: &str, paper: &str, title: &str, sheet_no: usize, of: usize, libs: &str, body: &str, tail: &str) -> String {
format!("(kicad_sch\n\t(version 20231120)\n\t(generator \"adom-aiflow-reverse\")\n\t(generator_version \"8.0\")\n\t(uuid \"{uid}\")\n\t(paper \"{paper}\")\n\t(title_block (title \"{}\") (comment 1 \"Solid wire: seen in copper. Dashed: inferred. ?: net not known.\") (comment 2 \"Reverse engineered from photos. Sheet {sheet_no} of {of}.\"))\n\t(lib_symbols\n{libs}\t)\n{body}{tail})\n", esc(title))
}
}
fn lib_block(names: &BTreeSet<String>, syms: &BTreeMap<String, Sym>) -> String { names.iter().map(|n| format!("\t\t{}\n", syms[n].text(true))).collect() }
/// Lay the whole design out and write every sheet.
pub fn project(d: &Design) -> Project {
let mut lay = Lay { d, syms: BTreeMap::new(), box_syms: HashMap::new(), claimed: HashSet::new(), attach: HashMap::new(), in_block: HashSet::new(), free: false, stats: BTreeMap::new() };
let mut sheets: Vec<Sheet> = vec![];
let mut notes = d.notes.clone();
for (bi, b) in d.blocks.iter().enumerate() {
lay.attach_block(&b.parts);
let mut anchors: Vec<usize> = b.parts.iter().copied().filter(|i| d.parts[*i].role == Role::Anchor).collect();
// ICs first, biggest first, then connectors: each takes the parts nearest its pins
anchors.sort_by(|x, y| { let k = |i: &usize| (d.parts[*i].kind == Kind::Conn, std::cmp::Reverse(d.parts[*i].pads.len())); k(x).cmp(&k(y)).then(natural(&d.parts[*x].r, &d.parts[*y].r)) });
let mut groups: Vec<Cv> = lay.stacks(&b.parts);
anchors.retain(|pi| !lay.claimed.contains(pi));
groups.extend(anchors.iter().map(|&pi| { lay.claimed.insert(pi); lay.cluster(pi, false, false) }));
groups.extend(lay.leftovers(&b.parts));
// the smallest paper that takes the block on one sheet; A3 sheets after that
let mut chosen = None;
for (paper, _, _, right, bottom) in PAPERS {
let (w, h) = (right - FRAME - 6, bottom - FRAME - 10);
let (p, fits) = pack(&groups, w, h);
if (p.len() == 1 && fits) || paper == "A3" { chosen = Some((paper, p)); break; }
}
let (paper, p) = chosen.unwrap();
let n = p.len();
for (k, spots) in p.iter().enumerate() {
let cv = place(&groups, spots, FRAME + 4, FRAME + 8);
let file = format!("{}-{}{}.kicad_sch", d.name, b.id, if n > 1 { format!("-{}", k + 1) } else { String::new() });
sheets.push(Sheet { file, title: if n > 1 { format!("{} ({} OF {n})", b.name, k + 1) } else { b.name.clone() }, paper, cv, block: bi });
}
if n > 1 { notes.push(format!("block {} needs {n} A3 sheets", b.name)); }
}
for pi in 0..d.parts.len() { if !lay.claimed.contains(&pi) { notes.push(format!("{} was not drawn (no block holds it)", d.parts[pi].r)); } }
// which nets cross sheets: those get hierarchical labels and a pin on the root sheet
let mut on: HashMap<usize, BTreeSet<usize>> = HashMap::new();
for (si, s) in sheets.iter().enumerate() { for l in &s.cv.labels { on.entry(l.net).or_default().insert(si); } }
let hier: HashMap<usize, BTreeSet<usize>> = on.into_iter().filter(|(n, v)| v.len() > 1 && d.nets[*n].rail == Rail::No).collect();
let root_uuid = uuid(&format!("sch/{}", d.name));
let mut w = Writer { d, root: root_uuid.clone(), virt: BTreeMap::new(), hier };
let total = sheets.len() + 1;
let mut files = vec![];
let mut all_syms: BTreeSet<String> = BTreeSet::new();
let sheet_uuid = |f: &str| uuid(&format!("sch/{}/sheet/{f}", d.name));
for (si, s) in sheets.iter().enumerate() {
let (_, _, _, right, bottom) = PAPERS.iter().find(|p| p.0 == s.paper).copied().unwrap();
let mut body = format!("\t(rectangle (start {} {}) (end {} {}) (stroke (width 0.3) (type solid)) (fill (type none)) (uuid \"{}\"))\n", mm(FRAME as f64), mm(FRAME as f64), mm(right as f64), mm(bottom as f64), uuid(&format!("{}/frame", s.file)));
body += &format!("\t(text \"{}\" (exclude_from_sim no) (at {} {} 0) (effects {}) (uuid \"{}\"))\n", esc(&s.title), mm((FRAME + right) as f64 / 2.0), mm(FRAME as f64 + 3.0), font(1.905, true), uuid(&format!("{}/title", s.file)));
body += &w.items(&s.cv, &lay.syms, &lay.box_syms, &format!("/{root_uuid}/{}", sheet_uuid(&s.file)), &s.file, Some(si));
let mut names: BTreeSet<String> = s.cv.insts.iter().map(|i| i.sym.clone()).collect();
for i in &s.cv.insts { if let Some(pi) = i.part { if let Some(bs) = lay.box_syms.get(&pi) { lay.syms.entry(bs.name.clone()).or_insert(bs.clone()); names.insert(bs.name.clone()); } } }
all_syms.extend(names.iter().cloned());
files.push((s.file.clone(), w.file(&uuid(&format!("sch/{}/file/{}", d.name, s.file)), s.paper, &format!("{}: {}", d.name, s.title), si + 2, total, &lib_block(&names, &lay.syms), &body, "")));
let b = s.cv.bounds();
if b.2 > right as f64 || b.3 > bottom as f64 { notes.push(format!("sheet {} does not fit its frame", s.file)); }
}
// ---- root sheet: hierarchy, legend, rail flags ----
let mut rcv = Cv::default();
let mut rbody = String::new();
let (paper, right, bottom) = ("A3", PAPERS[1].3, PAPERS[1].4);
let split = bottom - 44;
// sheet symbols on shelves
let (mut x, mut y, mut row_h) = (FRAME + 6, FRAME + 12, 0);
for (si, s) in sheets.iter().enumerate() {
let mut pins: Vec<usize> = w.hier.iter().filter(|(_, v)| v.contains(&si)).map(|(n, _)| *n).collect();
pins.sort_by(|a, b| natural(&d.nets[*a].name, &d.nets[*b].name));
let half = (pins.len() + 1) / 2;
let longest = |v: &[usize]| v.iter().map(|n| lab_len(&d.nets[*n].name)).max().unwrap_or(0);
let (ll, lr) = (longest(&pins[..half]), longest(&pins[half..]));
let name_w = tw(&s.title).max(tw(&s.file)).ceil() as i32 + 2;
let bw = { let v = (ll + lr + 4).max(name_w).max(24); v + v % 2 };
let bh = (half.max(1) as i32 + 1) * 2;
let total_w = ll + bw + lr + 4;
if x + total_w > right - 4 && x > FRAME + 6 { x = FRAME + 6; y += row_h + 10; row_h = 0; }
let (bx, by) = (x + ll + 2, y);
let su = sheet_uuid(&s.file);
rbody += &format!("\t(sheet (at {} {}) (size {} {}) (fields_autoplaced yes) (stroke (width 0.1524) (type solid)) (fill (color 0 0 0 0.0000)) (uuid \"{su}\")\n", mm(bx as f64), mm(by as f64), mm(bw as f64), mm(bh as f64));
rbody += &format!("\t\t(property \"Sheetname\" \"{}\" (at {} {} 0) (effects {} (justify left bottom)))\n\t\t(property \"Sheetfile\" \"{}\" (at {} {} 0) (effects {} (justify left top)))\n", esc(&s.title), mm(bx as f64), mm(by as f64 - 0.5), font(1.27, false), esc(&s.file), mm(bx as f64), mm((by + bh) as f64 + 0.5), font(1.27, false));
for (k, n) in pins.iter().enumerate() {
let left = k < half;
let (px, py) = (if left { bx } else { bx + bw }, by + 2 + 2 * (if left { k } else { k - half }) as i32);
rbody += &format!("\t\t(pin \"{}\" passive (at {} {} {}) (effects {} (justify {})) (uuid \"{}\"))\n", esc(&d.nets[*n].name), mm(px as f64), mm(py as f64), if left { 180 } else { 0 }, font(1.27, false), if left { "left" } else { "right" }, uuid(&format!("{}/pin/{}", s.file, d.nets[*n].name)));
let len = lab_len(&d.nets[*n].name); let dir = if left { -1 } else { 1 };
rcv.wire((px, py), (px + dir * len, py), true);
rcv.label(d, *n, px + dir * len, py, (-dir, 0));
}
rbody += &format!("\t\t(instances (project \"{}\" (path \"/{root_uuid}\" (page \"{}\"))))\n\t)\n", esc(&d.name), si + 2);
rcv.boxes.push((bx as f64, by as f64 - 2.0, (bx + bw) as f64, (by + bh) as f64 + 2.0));
x += total_w + 6; row_h = row_h.max(bh);
if y + bh > split - 2 { notes.push("the hierarchy does not fit the root sheet".into()); }
}
// legend and rail flags
let ly = split + 8;
let legend: [(&str, &str); 5] = [("solid", "Solid wire: the pad it leads to was seen joined to this net's copper in the photos."), ("dash", "Dashed wire: the join is inferred (a datasheet or the circuit), not seen. Tests in tests.json settle it."),
("unknown", "? marker: the net of this pin is not known. It is not a no-connect."), ("nc", "X: no-connect, used only where the run holds evidence the pin is unused."), ("", "Every symbol carries hidden fields: Evidence (how the part is known), Test (its test id) and Marking (as read from the photo).")];
let lx = FRAME + 6;
for (k, (kind, text)) in legend.iter().enumerate() {
let yy = (ly + 4 * k as i32) as f64; let id = uuid(&format!("{}/legend{k}", d.name));
match *kind {
"solid" | "dash" => rbody += &format!("\t(polyline (pts (xy {} {}) (xy {} {})) (stroke (width 0.1524) (type {}) (color 0 132 0 1)) (uuid \"{id}\"))\n", mm(lx as f64), mm(yy), mm(lx as f64 + 10.0), mm(yy), if *kind == "solid" { "solid" } else { "dash" }),
"unknown" => { rbody += &format!("\t(circle (center {} {}) (radius 1.27) (stroke (width 0.1524) (type solid)) (fill (type none)) (uuid \"{id}\"))\n", mm(lx as f64 + 5.0), mm(yy)); rcv.text("?", lx as f64 + 5.0, yy, 1.27, false, 0); }
"nc" => { for (q, (a, b)) in [((-0.5, -0.5), (0.5, 0.5)), ((-0.5, 0.5), (0.5, -0.5))].iter().enumerate() { rbody += &format!("\t(polyline (pts (xy {} {}) (xy {} {})) (stroke (width 0.1524) (type solid)) (uuid \"{}\"))\n", mm(lx as f64 + 5.0 + a.0), mm(yy + a.1), mm(lx as f64 + 5.0 + b.0), mm(yy + b.1), uuid(&format!("{}/legendx{q}", d.name))); } }
_ => {}
}
rcv.text(text, lx as f64 + 13.0, yy, 1.27, false, -1);
}
for (k, text) in d.carried.iter().enumerate() { rcv.text(text, lx as f64, (ly + 4 * (legend.len() + k) as i32) as f64, 1.27, true, -1); }
// every rail is fed from somewhere the photos do not name: one PWR_FLAG each keeps the ERC honest about the rest
let driven = |n: usize| d.nets[n].pins.iter().any(|(pi, pad)| { let et = |s: &Sym| s.term(pad).map(|t| s.terms[t].etype == "power_out").unwrap_or(false); d.parts[*pi].sym.as_ref().map(|s| et(s)).unwrap_or(false) || lay.box_syms.get(pi).map(|s| et(s)).unwrap_or(false) });
let rails: Vec<usize> = (0..d.nets.len()).filter(|n| d.nets[*n].rail != Rail::No && !d.nets[*n].pins.is_empty() && !driven(*n)).collect();
let fx0 = right - 6 - 12 * rails.len().min(12) as i32;
for (k, n) in rails.iter().enumerate() {
let (fx, fy) = (fx0 + 12 * (k % 12) as i32 + (fx0 % 2), ly + 6 + 14 * (k / 12) as i32);
let fs = schsym::flag_sym();
let (sx, sy, r) = seat(&fs, 0, (fx, fy), (0, 1));
rcv.boxes.push(body_box(&fs, sx, sy, r));
let name = lay.reg(fs);
rcv.insts.push(Inst { sym: name, x: sx, y: sy, rot: r, part: None, rail: None, value: "PWR_FLAG".into(), ref_at: None, val_at: Some((fx as f64, fy as f64 - 3.2)) });
rcv.wire((fx, fy), (fx, fy + 2), true);
lay.rail(&mut rcv, *n, (fx, fy + 2), (0, 1));
}
if !rails.is_empty() { rcv.text("PWR_FLAG on each rail: where a rail is fed from is not read from the photos.", (right - 6) as f64, ly as f64, 1.27, false, 1); }
for (k, (a, b, title)) in [((FRAME, FRAME), (right, split), "HIERARCHY"), ((FRAME, split), (right, bottom), "EVIDENCE LEGEND AND RAIL FLAGS")].iter().enumerate() {
rbody += &format!("\t(rectangle (start {} {}) (end {} {}) (stroke (width 0.3) (type solid)) (fill (type none)) (uuid \"{}\"))\n", mm(a.0 as f64), mm(a.1 as f64), mm(b.0 as f64), mm(b.1 as f64), uuid(&format!("{}/rootframe{k}", d.name)));
rbody += &format!("\t(text \"{title}\" (exclude_from_sim no) (at {} {} 0) (effects {}) (uuid \"{}\"))\n", mm((a.0 + b.0) as f64 / 2.0), mm(a.1 as f64 + 3.0), font(1.905, true), uuid(&format!("{}/roottitle{k}", d.name)));
}
rbody += &w.items(&rcv, &lay.syms, &lay.box_syms, &format!("/{root_uuid}"), "root", None);
let names: BTreeSet<String> = rcv.insts.iter().map(|i| i.sym.clone()).collect();
all_syms.extend(names.iter().cloned());
let mut tail = "\t(sheet_instances (path \"/\" (page \"1\")))\n".to_string();
let _ = &mut tail;
files.insert(0, (format!("{}.kicad_sch", d.name), w.file(&root_uuid, paper, &format!("{} (reverse engineered from photos)", d.name), 1, total, &lib_block(&names, &lay.syms), &rbody, &tail)));
// the project library every embedded symbol is filed under, and its table
files.push((format!("{}.kicad_sym", schsym::LIB), format!("(kicad_symbol_lib (version 20231120) (generator \"adom-aiflow-reverse\") (generator_version \"8.0\")\n{})\n", all_syms.iter().map(|n| format!("\t{}\n", lay.syms[n].text(false))).collect::<String>())));
files.push(("sym-lib-table".into(), format!("(sym_lib_table\n (version 7)\n (lib (name \"{0}\")(type \"KiCad\")(uri \"${{KIPRJMOD}}/{0}.kicad_sym\")(options \"\")(descr \"Symbols of the reverse engineered schematic\"))\n)\n", schsym::LIB)));
// the footprints: the run's own cache (the files the board was built from) and the generated ones
let mut table = String::from("(fp_lib_table\n (version 7)\n");
let mut libs: BTreeSet<String> = d.parts.iter().filter_map(|p| p.fields[0].1.split_once(':').map(|x| x.0.to_string())).collect();
libs.remove("generated");
for l in &libs { table += &format!(" (lib (name \"{l}\")(type \"KiCad\")(uri \"${{KIPRJMOD}}/../reverse-flow/lib/fp/{l}\")(options \"\")(descr \"Footprints this run was built from\"))\n"); }
if !d.generated.is_empty() { table += " (lib (name \"generated\")(type \"KiCad\")(uri \"${KIPRJMOD}/generated.pretty\")(options \"\")(descr \"Footprints drawn from the photos\"))\n"; }
files.push(("fp-lib-table".into(), table + ")\n"));
for (n, t) in &d.generated { files.push((format!("generated.pretty/{n}.kicad_mod"), t.clone())); }
let _ = &w.root;
let mut stats = json!({"sheets": total, "parts": d.parts.len(), "nets": d.nets.len(), "wires": sheets.iter().map(|s| s.cv.wires.len()).sum::<usize>(), "wiresSeen": sheets.iter().map(|s| s.cv.wires.iter().filter(|w| w.seen).count()).sum::<usize>(),
"symbolsBoxed": d.parts.iter().filter(|p| p.sym.is_none() && !p.pads.is_empty()).count(), "hierarchicalNets": w.hier.len(), "blocks": sheets.iter().map(|s| s.block).collect::<BTreeSet<_>>().len()});
for (k, v) in &lay.stats { stats[*k] = json!(v); }
let differences = differences(d, &sheets, &lay.syms, &lay.box_syms, &w.hier);
Project { files, notes, stats, differences }
}
/// Generate the schematic of a run into its kicad folder. Returns hints for the AI.
pub fn write(flow: &Path, kdir: &Path, name: &str) -> Result<Vec<String>, String> {
let parts_doc = crate::items::read_json(&flow.join("parts.json"))?;
let parts: Vec<Value> = parts_doc["parts"].as_array().cloned().unwrap_or_default();
let blocks = crate::items::read_json(&flow.join("blocks.json")).unwrap_or_else(|_| json!({"blocks": [{"id": "all", "name": "All parts", "parts": parts.iter().map(|p| p["ref"].clone()).collect::<Vec<_>>()}]}));
let nets = crate::items::read_json(&flow.join("nets.json")).unwrap_or(json!({"nets": []}));
let ev = crate::items::read_json(&flow.join("net-evidence.json")).unwrap_or(Value::Null);
let mut libs = Libs::new(&flow.join("lib").join("sym"));
let mut d = Design::load(&kicad_out::cache_dir(flow), &mut libs, &parts, &blocks, &nets, &ev, name);
d.carried = crate::notread::faces(flow).iter().map(|f| format!("{} Pins on that face end in a ? marker.", crate::notread::required(f).replace('`', "'"))).collect();
let p = project(&d);
// sheets of an earlier build that this one no longer writes
if let Ok(rd) = std::fs::read_dir(kdir) {
for e in rd.flatten() { let f = e.file_name().to_string_lossy().to_string(); if f.starts_with(&format!("{name}-")) && f.ends_with(".kicad_sch") && !p.files.iter().any(|(n, _)| *n == f) { let _ = std::fs::remove_file(e.path()); } }
}
for (f, text) in &p.files { let path = kdir.join(f); if let Some(dir) = path.parent() { std::fs::create_dir_all(dir).ok(); } std::fs::write(&path, text).map_err(|e| format!("{f}: {e}"))?; }
let mut h = vec![format!("schematic: {} sheets ({} hierarchical nets), {} wires ({} seen in copper, the rest dashed), {} pins of unknown net marked ?, {} parts as boxes", p.stats["sheets"], p.stats["hierarchicalNets"], p.stats["wires"], p.stats["wiresSeen"], p.stats["unknownPins"].as_u64().unwrap_or(0), p.stats["symbolsBoxed"])];
if !p.differences.is_empty() { h.push(format!("PROBLEM: the sheets as drawn do not hold the connections of nets.json ({} differences; a fault in the generator, not in the run): {}", p.differences.len(), p.differences.iter().take(6).cloned().collect::<Vec<_>>().join("; "))); }
h.extend(p.notes.iter().take(12).map(|n| format!("schematic: {n}")));
if p.notes.len() > 12 { h.push(format!("schematic: {} more notes", p.notes.len() - 12)); }
h.push("Run `check schematic` (ERC, netlist against nets.json, sheet style). Name an identified IC's library symbol in parts.json `symbol`, or its pin names in `pins`, and build again.".into());
Ok(h)
}
#[cfg(test)]
mod tests {
use super::*;
struct Tmp(std::path::PathBuf);
impl Tmp {
fn new() -> Self {
static NEXT: std::sync::atomic::AtomicUsize = std::sync::atomic::AtomicUsize::new(0);
let p = std::env::temp_dir().join(format!("reverse-sch-{}-{}", std::process::id(), NEXT.fetch_add(1, std::sync::atomic::Ordering::Relaxed)));
std::fs::create_dir_all(&p).unwrap(); Tmp(p)
}
fn put(&self, rel: &str, text: &str) { let p = self.0.join(rel); std::fs::create_dir_all(p.parent().unwrap()).unwrap(); std::fs::write(p, text).unwrap(); }
}
impl Drop for Tmp { fn drop(&mut self) { let _ = std::fs::remove_dir_all(&self.0); } }
fn fp(name: &str, pads: &[&str]) -> String { format!("(footprint \"{name}\" (layer \"F.Cu\")\n{})", pads.iter().enumerate().map(|(k, p)| format!(" (pad \"{p}\" smd rect (at {} 0) (size 0.5 0.5) (layers \"F.Cu\"))\n", k)).collect::<String>()) }
fn two(name: &str, horizontal: bool) -> String {
let (a, b) = if horizontal { ("(at -3.81 0 0)", "(at 3.81 0 180)") } else { ("(at 0 3.81 270)", "(at 0 -3.81 90)") };
format!("(symbol \"{name}\" (property \"Reference\" \"{}\" (at 0 0 0)) (symbol \"{name}_0_1\" (rectangle (start -1.016 -1.016) (end 1.016 1.016) (stroke (width 0.254) (type default)) (fill (type none))) (polyline (pts (xy -1 0) (xy 1 0)) (stroke (width 0) (type default)) (fill (type none)))) (symbol \"{name}_1_1\" (pin passive line {a} (length 1.27) (name \"K\" (effects (font (size 1.27 1.27)))) (number \"1\" (effects (font (size 1.27 1.27))))) (pin passive line {b} (length 1.27) (name \"A\" (effects (font (size 1.27 1.27)))) (number \"2\" (effects (font (size 1.27 1.27)))))))", &name[..1])
}
/// A run folder with the footprints and library symbols a small board needs, so no test asks KiCad for anything.
fn bench() -> Tmp {
let t = Tmp::new();
t.put("fp/T/IC8.kicad_mod", &fp("IC8", &["1", "2", "3", "4", "5", "6", "7", "8"]));
t.put("fp/T/P2.kicad_mod", &fp("P2", &["1", "2"]));
t.put("fp/T/J3.kicad_mod", &fp("J3", &["1", "2", "3"]));
for n in ["R", "C"] { t.put(&format!("sym/Device/{n}.kicad_sym"), &two(n, false)); }
t.put("sym/Device/LED.kicad_sym", &two("LED", true));
t.put("sym/Connector_Generic/Conn_01x03.kicad_sym", "(symbol \"Conn_01x03\" (property \"Reference\" \"J\" (at 0 5 0)) (symbol \"Conn_01x03_1_1\" (rectangle (start -1.27 3.81) (end 1.27 -3.81) (stroke (width 0.254) (type default)) (fill (type background))) (pin passive line (at -5.08 2.54 0) (length 3.81) (name \"Pin_1\" (effects (font (size 1.27 1.27)))) (number \"1\" (effects (font (size 1.27 1.27))))) (pin passive line (at -5.08 0 0) (length 3.81) (name \"Pin_2\" (effects (font (size 1.27 1.27)))) (number \"2\" (effects (font (size 1.27 1.27))))) (pin passive line (at -5.08 -2.54 0) (length 3.81) (name \"Pin_3\" (effects (font (size 1.27 1.27)))) (number \"3\" (effects (font (size 1.27 1.27)))))))");
t.put("sym/Connector/TestPoint.kicad_sym", "(symbol \"TestPoint\" (property \"Reference\" \"TP\" (at 0 5 0)) (symbol \"TestPoint_0_1\" (circle (center 0 3.302) (radius 0.762) (stroke (width 0) (type default)) (fill (type none)))) (symbol \"TestPoint_1_1\" (pin passive line (at 0 0 90) (length 2.54) (name \"1\" (effects (font (size 1.27 1.27)))) (number \"1\" (effects (font (size 1.27 1.27)))))))");
t
}
fn part(r: &str, fp: &str, value: &str, x: f64) -> Value { json!({"ref": r, "footprint": fp, "value": value, "marking": "", "function": "test part", "package": "", "tag": "MEASURED", "tests": [format!("T-{r}")], "pos_mm": [x, 100.0], "rot": 0, "side": "top"}) }
fn board() -> (Vec<Value>, Value, Value) {
let mut parts = vec![part("U1", "T:IC8", "unknown IC", 110.0), part("R1", "T:P2", "10.0kΩ", 108.0), part("R2", "T:P2", "330Ω", 112.0), part("C1", "T:P2", "100nF", 110.5), part("C2", "T:P2", "", 130.0),
part("LED1", "T:P2", "LED", 114.0), part("J1", "T:J3", "1x3", 120.0), part("TP1", "generated:lug", "GND", 125.0)];
parts[7]["lug"] = json!({"pad_mm": 3.0, "drill_mm": 1.5});
let nets = json!({"nets": [
{"name": "+3V3", "tag": "INFERRED", "pins": ["U1.8", "C1.1", "R1.1", "LED1.2"]},
{"name": "GND", "tag": "MEASURED", "pins": ["U1.4", "C1.2", "J1.3", "TP1.1", "C2.2", "PHOTO1.M1"]},
{"name": "SIG", "tag": "INFERRED", "pins": ["U1.1", "R1.2", "J1.1"]},
{"name": "LED_K", "tag": "INFERRED", "pins": ["U1.2", "R2.1"]},
{"name": "LED_A", "tag": "INFERRED", "pins": ["R2.2", "LED1.1"]},
{"name": "N001", "nameSource": "none: unnamed", "tag": "INFERRED", "pins": ["U1.5"]}]});
(parts, json!({"blocks": [{"id": "core", "name": "Core", "parts": ["U1", "R1", "R2", "C1", "LED1", "TP1"]}, {"id": "io", "name": "Connector and spare", "parts": ["J1", "C2"]}]}), nets)
}
fn make(parts: &[Value], blocks: &Value, nets: &Value, t: &Tmp) -> (Design, Project) {
let mut libs = Libs::new(&t.0.join("sym"));
let ev = json!({"nets": [{"net": "GND", "pins": {"U1.4": "copper", "C1.2": "no copper seen"}}]});
let d = Design::load(&t.0.join("fp"), &mut libs, parts, blocks, nets, &ev, "bench");
let p = project(&d);
(d, p)
}
#[test]
fn the_drawn_sheets_hold_exactly_the_nets() {
let t = bench();
let (parts, blocks, nets) = board();
let (d, p) = make(&parts, &blocks, &nets, &t);
assert!(d.notes.is_empty(), "{:?}", d.notes);
assert!(p.differences.is_empty(), "{:?}", p.differences);
let names: Vec<&str> = p.files.iter().map(|f| f.0.as_str()).collect();
for want in ["bench.kicad_sch", "bench-core.kicad_sch", "bench-io.kicad_sch", "RE.kicad_sym", "sym-lib-table", "fp-lib-table"] { assert!(names.contains(&want), "{want} in {names:?}"); }
for (f, text) in p.files.iter().filter(|f| f.0.ends_with(".kicad_sch") || f.0.ends_with(".kicad_sym")) { assert!(crate::sexp::parse(text).is_ok(), "{f} is one well-formed list"); }
let get = |n: &str| &p.files.iter().find(|f| f.0 == n).unwrap().1;
let (root, core, io) = (get("bench.kicad_sch"), get("bench-core.kicad_sch"), get("bench-io.kicad_sch"));
// SIG and the rails cross the two blocks: SIG by a hierarchical label and a pin on each sheet symbol, the rails by their symbols
assert_eq!(root.matches("(pin \"SIG\" passive").count(), 2);
assert!(core.contains("(hierarchical_label \"SIG\"") && io.contains("(hierarchical_label \"SIG\""));
assert!(!root.contains("(pin \"GND\"") && core.contains("RE:PWR_GND") && io.contains("RE:PWR_GND"));
assert!(root.contains("\"HIERARCHY\"") && root.contains("EVIDENCE LEGEND") && root.contains("RE:PWR_FLAG"));
assert!(core.contains("(text \"CORE\"") && io.contains("(text \"CONNECTOR AND SPARE\""), "block titles in uppercase");
// real shapes, the values as the sheet rules write them, and the evidence fields
assert!(core.contains("(lib_id \"RE:R\")") && core.contains("(lib_id \"RE:LED\")") && core.contains("(lib_id \"RE:C\")") && core.contains("(lib_id \"RE:TestPoint\")"));
assert!(core.contains("\"Value\" \"10.0K\"") && core.contains("\"Value\" \"100n\"") && core.contains("\"Value\" \"330R\"") && io.contains("\"Value\" \"?\""));
assert!(core.contains("(property \"Evidence\" \"MEASURED\"") && core.contains("(property \"Test\" \"T-R1\""));
assert!(core.contains("\"Footprint\" \"generated:Lug_3.0mm_Drill1.50mm\""), "a generated footprint is named as the library files it");
assert!(names.contains(&"generated.pretty/Lug_3.0mm_Drill1.50mm.kicad_mod"));
// pins of unknown net: a marker each, never a no-connect; the one-pin unnamed net is unknown too
assert!(!core.contains("(no_connect") && !io.contains("(no_connect"));
assert_eq!(core.matches("(lib_id \"RE:Unknown_").count(), 4, "U1.3, U1.5, U1.6, U1.7");
assert_eq!(io.matches("(lib_id \"RE:Unknown_").count(), 2, "J1.2 and C2.1");
// evidence: the pad seen on copper has a solid wire, the others are dashed
assert!(core.contains("(type solid)") && core.contains("(type dash)"));
assert_eq!(p.stats["sheets"], 3);
}
#[test]
fn the_generated_sheets_pass_the_sheet_rules() {
let t = bench();
let (parts, blocks, nets) = board();
let (_, p) = make(&parts, &blocks, &nets, &t);
let (mut errors, mut warnings) = (vec![], vec![]);
let mut at_ic = 0;
for (f, text) in p.files.iter().filter(|f| f.0.ends_with(".kicad_sch")) {
let v = crate::schcheck::style(text, f, &BTreeSet::new(), &mut errors, &mut warnings).unwrap();
at_ic += v["decouplingAtIc"].as_u64().unwrap();
}
assert!(errors.is_empty(), "{errors:#?}");
assert_eq!(at_ic, 1, "C1 hangs on U1's supply pin");
}
#[test]
fn a_no_connect_needs_the_run_to_say_so() {
let t = bench();
let (parts, blocks, mut nets) = board();
nets["noConnect"] = json!(["U1.3", {"pin": "U1.6", "why": "datasheet: NC"}]);
let (_, p) = make(&parts, &blocks, &nets, &t);
let core = &p.files.iter().find(|f| f.0 == "bench-core.kicad_sch").unwrap().1;
assert_eq!(core.matches("(no_connect").count(), 2);
assert_eq!(core.matches("(lib_id \"RE:Unknown_").count(), 2);
assert!(p.differences.is_empty(), "{:?}", p.differences);
}
#[test]
fn named_pins_group_a_box_by_function_and_a_wrong_symbol_is_refused() {
let t = bench();
let (mut parts, blocks, nets) = board();
parts[0]["pins"] = json!({"1": "IN", "2": {"name": "OUT", "type": "output"}, "3": "ADJ", "4": "GND", "5": "NC", "6": "EN", "7": "FB", "8": "VIN"});
parts[6]["symbol"] = json!("Device:R");
let (d, p) = make(&parts, &blocks, &nets, &t);
assert!(d.notes.iter().any(|n| n.starts_with("J1: Device:R not used")), "{:?}", d.notes);
assert!(p.differences.is_empty(), "{:?}", p.differences);
let lib = &p.files.iter().find(|f| f.0 == "RE.kicad_sym").unwrap().1;
let u1 = crate::schsym::extract(lib, "U1").unwrap();
let y = |name: &str| -> f64 { let i = u1.find(&format!("(name \"{name}\"")).unwrap(); let at = u1[..i].rfind("(at ").unwrap(); u1[at + 4..].split_whitespace().nth(1).unwrap().parse().unwrap() };
let x = |name: &str| -> f64 { let i = u1.find(&format!("(name \"{name}\"")).unwrap(); let at = u1[..i].rfind("(at ").unwrap(); u1[at + 4..].split_whitespace().next().unwrap().parse().unwrap() };
assert!(y("VIN") > y("IN") && y("IN") > y("GND"), "supply on top, ground at the bottom (library y is up)");
assert!(x("OUT") > 0.0 && x("VIN") < 0.0, "outputs on the right");
assert!(u1.contains("(pin power_in line") && u1.contains("(pin output line"), "pin types where the run gives them");
}
#[test]
fn values_and_rails_read_as_the_rules_write_them() {
assert_eq!(sheet_value(Kind::C, "100nF"), "100n");
assert_eq!(sheet_value(Kind::C, "10 µF"), "10u");
assert_eq!(sheet_value(Kind::C, ""), "?");
assert_eq!(sheet_value(Kind::R, "2mΩ"), "0R002");
assert_eq!(sheet_value(Kind::R, "22Ω"), "22R");
assert_eq!(sheet_value(Kind::R, "2.00kΩ"), "2.00K");
assert_eq!(sheet_value(Kind::L, "100µH"), "100uH");
assert_eq!(sheet_value(Kind::Q, "N-MOSFET (marking unreadable)"), "N-MOSFET");
for (n, r) in [("+3V3", Rail::Supply), ("GND", Rail::Ground), ("AGND", Rail::Ground), ("VIN_LOGIC", Rail::Supply), ("CM5_3.3V", Rail::Supply), ("VBUS_EN", Rail::No), ("VSENSE", Rail::No), ("SD_VDD_OVERRIDE", Rail::No), ("GPIO5", Rail::No)] { assert_eq!(rail_of(n), r, "{n}"); }
assert_eq!(pad_numbers(&fp("x", &["10", "2", "1", "2", "MP", ""])), vec!["1", "2", "10", "MP"]);
}
#[test]
fn a_long_row_of_capacitors_wraps_and_every_sheet_is_a3_at_most() {
let t = bench();
let mut parts = vec![part("J1", "T:J3", "1x3", 100.0)];
let mut vin = vec!["J1.1".to_string()]; let mut gnd = vec!["J1.3".to_string()];
for k in 1..=60 { parts.push(part(&format!("C{k}"), "T:P2", "", 100.0 + k as f64)); vin.push(format!("C{k}.1")); gnd.push(format!("C{k}.2")); }
let nets = json!({"nets": [{"name": "VIN", "tag": "INFERRED", "pins": vin}, {"name": "GND", "tag": "INFERRED", "pins": gnd}]});
let blocks = json!({"blocks": [{"id": "in", "name": "Power entry", "parts": parts.iter().map(|p| p["ref"].clone()).collect::<Vec<_>>()}]});
let (_, p) = make(&parts, &blocks, &nets, &t);
assert!(p.differences.is_empty(), "{:?}", p.differences);
assert!(p.stats["railRows"].as_u64().unwrap() >= 2, "{}", p.stats);
for (f, text) in p.files.iter().filter(|f| f.0.ends_with(".kicad_sch")) { assert!(text.contains("(paper \"A4\")") || text.contains("(paper \"A3\")"), "{f}"); }
assert!(!p.notes.iter().any(|n| n.contains("does not fit")), "{:?}", p.notes);
}
}