app
KiCad - the KiCad Bridge
Public Made by Adomby adom
Drive your own KiCad from an AI, in the background: open every editor, read the whole board as data (the EDA Bridge Standard board model), stack-up, part heights, netlists, place, route, pour, DRC and exports.
master
John Lauer
1.0.61: Every Konnect v0.13.0 capability, 211 verbs (was 133; docs/konnect-gap.md maps all 228 Konnect tools). Schemati…
0009b41
14d ago
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474
//! Schematic analysis on one sheet: what is connected at a point, the connectivity and
//! layout check (dangling wire ends, floating labels, unconnected pins, single-pin nets,
//! distinct names shorted together, overlapping symbols and text), and the near-miss wire
//! end fixer. Everything here reads `sch_model`'s model; only `apply_moves` edits a file.
use serde::Serialize;
use serde_json::{json, Value};
use crate::schematic::{Node, Schematic};
use crate::sch_model::{dist, on_interior, on_segment, overlap, r4, same, BBox, Connectivity, Item, Pt, SheetModel};
#[derive(Clone, Debug, Serialize)]
pub struct Finding {
pub check: &'static str,
/// error | warning | info
pub severity: &'static str,
#[serde(skip_serializing_if = "Option::is_none")]
pub at: Option<[f64; 2]>,
#[serde(skip_serializing_if = "Option::is_none")]
pub uuid: Option<String>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub refs: Vec<String>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub nets: Vec<String>,
pub message: String,
pub fix: String,
}
/// `U1.3 EN`, or `R1.2` when the pin has no name of its own.
fn pin_label(p: &crate::sch_model::Pin) -> String {
if p.name.is_empty() || p.name == "~" || p.name == p.number {
format!("{}.{}", p.reference, p.number)
} else {
format!("{}.{} {}", p.reference, p.number, p.name)
}
}
fn pt(p: Pt) -> Option<[f64; 2]> {
Some([r4(p.0), r4(p.1)])
}
fn fmt(p: Pt) -> String {
format!("({}, {})", crate::schematic::fmt_num(r4(p.0)), crate::schematic::fmt_num(r4(p.1)))
}
/// Is a wire end at `p` (of segment `seg`) terminated by anything other than itself?
fn end_terminated(m: &SheetModel, seg: usize, p: Pt) -> bool {
m.wires.iter().enumerate().any(|(j, w)| j != seg && (same(w.a, p) || same(w.b, p) || on_interior(p, w.a, w.b)))
|| m.pins.iter().any(|x| same(x.at, p))
|| m.labels.iter().any(|x| same(x.at, p))
|| m.sheet_pins().any(|x| same(x.at, p))
|| m.junctions.iter().any(|x| same(x.at, p))
|| m.no_connects.iter().any(|x| same(x.at, p))
|| m.buses.iter().any(|b| on_segment(p, b.a, b.b))
}
/// Every wire end nothing terminates: (segment index, end point, the other end).
pub fn dangling_ends(m: &SheetModel) -> Vec<(usize, Pt, Pt)> {
let mut out = Vec::new();
for (i, w) in m.wires.iter().enumerate() {
for (p, other) in [(w.a, w.b), (w.b, w.a)] {
if !end_terminated(m, i, p) {
out.push((i, p, other));
}
}
}
out
}
/// A point item physically touches a wire end, or mid-wire through a junction dot (pins and
/// sheet pins), or anywhere along a wire (labels).
fn on_wire(m: &SheetModel, p: Pt, anywhere: bool) -> bool {
m.wires.iter().any(|w| same(w.a, p) || same(w.b, p) || ((anywhere || m.junctions.iter().any(|j| same(j.at, p))) && on_segment(p, w.a, w.b)))
}
/// The nearest connectable thing to `p` within `tol` (not on wire segment `skip`):
/// (point, description).
pub fn nearest_target(m: &SheetModel, p: Pt, tol: f64, skip_uuid: &str) -> Option<(Pt, String)> {
let mut best: Option<(f64, Pt, String)> = None;
let mut consider = |q: Pt, what: String| {
let d = dist(p, q);
if d > 1e-6 && d <= tol && best.as_ref().map_or(true, |b| d < b.0) {
best = Some((d, q, what));
}
};
for x in &m.pins {
consider(x.at, if x.power_symbol { format!("power symbol {} ({})", x.reference, x.power_net.clone().unwrap_or_default()) } else { format!("pin {}.{}", x.reference, x.number) });
}
for x in &m.labels {
consider(x.at, format!("{} label {}", x.kind, x.text));
}
for x in m.sheet_pins() {
consider(x.at, format!("sheet pin {} of {}", x.name, x.sheet_name));
}
for w in m.wires.iter().filter(|w| w.uuid != skip_uuid) {
consider(w.a, format!("wire end {}", w.uuid));
consider(w.b, format!("wire end {}", w.uuid));
}
best.map(|(_, q, w)| (q, w))
}
#[derive(Clone, Debug, Default)]
pub struct CheckOpts {
pub overlaps: bool,
}
pub struct CheckReport {
pub findings: Vec<Finding>,
/// Nets that carry a global, hierarchical or power name: single-pin judgement needs the
/// other sheets, so they are not flagged here.
pub cross_sheet_skipped: usize,
}
pub fn check(m: &SheetModel, c: &Connectivity, opts: &CheckOpts) -> CheckReport {
let mut f: Vec<Finding> = Vec::new();
// dangling wire ends
for (seg, p, _) in dangling_ends(m) {
let w = &m.wires[seg];
let near = nearest_target(m, p, 1.27, &w.uuid);
let (message, fix) = match &near {
Some((q, what)) => (
format!("Wire end at {} connects to nothing; {what} is {} mm away at {}", fmt(p), crate::schematic::fmt_num(r4(dist(p, *q))), fmt(*q)),
"A near miss: kicad_sch_fix_connectivity {dryRun:false} snaps it onto that point (tolerance 0.5 mm by default; pass tolerance to widen).".to_string(),
),
None => (
format!("Wire end at {} connects to nothing", fmt(p)),
"Extend the wire onto the pin or label it should reach, or delete the stub with kicad_sch_delete {uuid}.".to_string(),
),
};
f.push(Finding { check: "dangling_wire_end", severity: "warning", at: pt(p), uuid: Some(w.uuid.clone()), refs: vec![], nets: vec![], message, fix });
}
// floating labels
for l in &m.labels {
let touches = on_wire(m, l.at, true) || m.pins.iter().any(|p| same(p.at, l.at)) || m.sheet_pins().any(|s| same(s.at, l.at));
if !touches {
f.push(Finding {
check: "floating_label",
severity: "error",
at: pt(l.at),
uuid: Some(l.uuid.clone()),
refs: vec![],
nets: vec![l.text.clone()],
message: format!("{} label {} at {} touches no wire or pin, so it names nothing", l.kind, l.text, fmt(l.at)),
fix: "A label connects only where its anchor sits on a wire or a pin end: move it there (delete with kicad_sch_delete and re-add with kicad_sch_label at the wire point), or delete it.".into(),
});
}
}
// unconnected pins (not passive, not power symbols, not hidden power pins), and flags on wired pins
for (i, p) in m.pins.iter().enumerate() {
if p.power_symbol || p.hidden {
continue;
}
let flagged = m.no_connects.iter().any(|n| same(n.at, p.at));
let attached = on_wire(m, p.at, false) || m.labels.iter().any(|l| same(l.at, p.at)) || m.sheet_pins().any(|s| same(s.at, p.at)) || m.pins.iter().enumerate().any(|(k, q)| k != i && same(q.at, p.at));
if flagged && attached {
f.push(Finding {
check: "no_connect_connected",
severity: "warning",
at: pt(p.at),
uuid: None,
refs: vec![p.reference.clone()],
nets: vec![],
message: format!("{} is wired but also carries a no-connect flag", pin_label(p)),
fix: "Delete the no-connect flag (kicad_sch_delete with its uuid from kicad_sch_trace_point at the pin) or the wire, whichever is wrong.".into(),
});
continue;
}
if flagged || attached || p.electrical == "passive" || p.electrical == "no_connect" {
continue;
}
f.push(Finding {
check: "unconnected_pin",
severity: "error",
at: pt(p.at),
uuid: Some(p.symbol_uuid.clone()),
refs: vec![p.reference.clone()],
nets: vec![],
message: format!("{} ({}) is not connected", pin_label(p), p.electrical),
fix: if p.electrical == "input" || p.electrical == "power_in" {
"An open input floats: wire it to its net (kicad_sch_wire to the pin point, at), tie it high or low, or put a no-connect flag on it if the datasheet allows (kicad_sch_no_connect {pin}).".into()
} else {
"Wire it (kicad_sch_wire) or flag it intentionally open (kicad_sch_no_connect {pin:\"Ref.Pin\"}).".into()
},
});
}
// single-pin nets and distinct names shorted together
let mut cross = 0;
for (gi, g) in c.groups.iter().enumerate() {
let part_pins: Vec<_> = g.part_pins(m).collect();
let has_wire_or_label = g.has(|i| matches!(i, Item::Wire(_) | Item::Label(_)));
let flagged = g.has(|i| matches!(i, Item::NoConnect(_)));
if part_pins.len() == 1 && has_wire_or_label && !flagged {
if g.leaves_sheet() {
cross += 1;
} else {
let p = part_pins[0];
f.push(Finding {
check: "single_pin_net",
severity: "warning",
at: pt(p.at),
uuid: None,
refs: vec![p.reference.clone()],
nets: vec![c.net_name(m, gi)],
message: format!("Net {} reaches only {}: a missing counterpart, an orphan label or a stub", c.net_name(m, gi), pin_label(p)),
fix: "Wire the other end, or if the pin is meant to be open replace the stub with a no-connect flag (kicad_sch_no_connect).".into(),
});
}
}
let names: Vec<&(String, String)> = g.names.iter().filter(|(k, _)| k != "sheet_pin").collect();
let mut distinct: Vec<&str> = names.iter().map(|(_, t)| t.as_str()).collect();
distinct.sort();
distinct.dedup();
if distinct.len() >= 2 {
let rails: Vec<&str> = {
let mut v: Vec<&str> = names.iter().filter(|(k, _)| k == "power" || k == "global").map(|(_, t)| t.as_str()).collect();
v.sort();
v.dedup();
v
};
let hard = rails.len() >= 2;
let at = g.items.iter().find_map(|i| if let Item::Label(k) = i { Some(m.labels[*k].at) } else { None }).or_else(|| part_pins.first().map(|p| p.at));
f.push(Finding {
check: "shorted_nets",
severity: if hard { "error" } else { "warning" },
at: at.and_then(pt),
uuid: None,
refs: part_pins.iter().map(|p| format!("{}.{}", p.reference, p.number)).collect(),
nets: distinct.iter().map(|s| s.to_string()).collect(),
message: format!(
"One net carries {} names: {}{}",
distinct.len(),
names.iter().map(|(k, t)| format!("{t} ({k})")).collect::<Vec<_>>().join(", "),
if hard { "; two power or global rails are shorted" } else { "; eeschema keeps one and warns (multiple_net_names)" }
),
fix: if hard {
"Two rails meet on one wire: find the wire joining them (kicad_sch_trace_point at the location) and break it, unless they really are one rail, then keep one name.".into()
} else {
"If they are meant to be one net keep one name (a hierarchical label beside a power symbol is common and harmless); otherwise separate the wires.".into()
},
});
}
}
if opts.overlaps {
overlaps(m, &mut f);
}
CheckReport { findings: f, cross_sheet_skipped: cross }
}
fn bb_json(b: BBox) -> Value {
json!([r4(b.0), r4(b.1), r4(b.2), r4(b.3)])
}
fn overlaps(m: &SheetModel, f: &mut Vec<Finding>) {
let syms: Vec<(&str, BBox, bool)> = m.symbols.iter().filter_map(|s| s.bbox.map(|b| (s.reference.as_str(), b, s.is_power))).collect();
for i in 0..syms.len() {
for j in (i + 1)..syms.len() {
let (a, b) = (&syms[i], &syms[j]);
if a.0 == b.0 {
continue; // units of one part
}
if let Some(o) = overlap(a.1, b.1, 0.25) {
f.push(Finding {
check: "overlap",
severity: if a.2 || b.2 { "info" } else { "warning" },
at: Some([r4((o.0 + o.2) / 2.0), r4((o.1 + o.3) / 2.0)]),
uuid: None,
refs: vec![a.0.to_string(), b.0.to_string()],
nets: vec![],
message: format!("{} and {} overlap (bodies and pins share {} x {} mm)", a.0, b.0, crate::schematic::fmt_num(r4(o.2 - o.0)), crate::schematic::fmt_num(r4(o.3 - o.1))),
fix: format!("Move one of them clear: kicad_sch_move {{reference:\"{}\", to:[x, y]}}; bodies span {} and {}.", b.0, bb_json(a.1), bb_json(b.1)),
});
}
}
}
// labels and free text over symbol bodies
let mut texts: Vec<(String, BBox, Option<String>, &'static str, String)> = Vec::new();
for l in &m.labels {
let pad = if l.kind == "local" { 0.0 } else { l.size * 1.5 };
let bb = crate::sch_model::text_bbox(l.at, &l.text, l.size, l.rotation, false, pad);
texts.push((format!("{} label {}", l.kind, l.text), bb, None, "label", l.uuid.clone()));
}
for t in &m.texts {
let what = match &t.owner {
Some(o) => format!("field {:?} of {o}", t.text),
None => format!("text {:?}", t.text),
};
texts.push((what, t.bbox, t.owner.clone(), if t.owner.is_some() { "field" } else { "text" }, t.uuid.clone()));
}
for (what, bb, owner, kind, uuid) in &texts {
for (r, sb, is_power) in &syms {
if owner.as_deref() == Some(*r) || *is_power {
continue;
}
if let Some(o) = overlap(*bb, *sb, 0.15) {
f.push(Finding {
check: "overlap",
severity: if *kind == "field" { "info" } else { "warning" },
at: Some([r4((o.0 + o.2) / 2.0), r4((o.1 + o.3) / 2.0)]),
uuid: Some(uuid.clone()),
refs: vec![r.to_string()],
nets: vec![],
message: format!("{what} runs over the body of {r}"),
fix: "Move the text clear of the symbol, or move the symbol (kicad_sch_move).".into(),
});
}
}
}
for i in 0..texts.len() {
for j in (i + 1)..texts.len() {
let (a, b) = (&texts[i], &texts[j]);
if a.2.is_some() && a.2 == b.2 {
continue;
}
if let Some(o) = overlap(a.1, b.1, 0.15) {
f.push(Finding {
check: "overlap",
severity: "info",
at: Some([r4((o.0 + o.2) / 2.0), r4((o.1 + o.3) / 2.0)]),
uuid: None,
refs: [a.2.clone(), b.2.clone()].into_iter().flatten().collect(),
nets: vec![],
message: format!("{} and {} overlap", a.0, b.0),
fix: "Move one of the two texts so both read.".into(),
});
}
}
}
}
// ---------------------------------------------------------------------------
// Trace from a point
// ---------------------------------------------------------------------------
/// Everything electrically connected at `at`: the items under the point and, for each net
/// they belong to, the whole net.
pub fn trace_point(m: &SheetModel, c: &Connectivity, at: Pt, tol: f64) -> Value {
let near = |p: Pt| dist(p, at) <= tol;
let mut here: Vec<Item> = Vec::new();
for (i, w) in m.wires.iter().enumerate() {
let seg_hit = {
// distance from the point to the segment
let (dx, dy) = (w.b.0 - w.a.0, w.b.1 - w.a.1);
let l2 = dx * dx + dy * dy;
let t = if l2 == 0.0 { 0.0 } else { (((at.0 - w.a.0) * dx + (at.1 - w.a.1) * dy) / l2).clamp(0.0, 1.0) };
dist(at, (w.a.0 + t * dx, w.a.1 + t * dy)) <= tol
};
if seg_hit {
here.push(Item::Wire(i));
}
}
here.extend(m.pins.iter().enumerate().filter(|(_, p)| near(p.at)).map(|(i, _)| Item::Pin(i)));
here.extend(m.labels.iter().enumerate().filter(|(_, p)| near(p.at)).map(|(i, _)| Item::Label(i)));
let sps: Vec<_> = m.sheet_pins().collect();
here.extend(sps.iter().enumerate().filter(|(_, p)| near(p.at)).map(|(i, _)| Item::SheetPin(i)));
here.extend(m.junctions.iter().enumerate().filter(|(_, p)| near(p.at)).map(|(i, _)| Item::Junction(i)));
here.extend(m.no_connects.iter().enumerate().filter(|(_, p)| near(p.at)).map(|(i, _)| Item::NoConnect(i)));
let item_json = |it: &Item| -> Value {
match *it {
Item::Wire(i) => json!({"type": "wire", "uuid": m.wires[i].uuid, "from": [m.wires[i].a.0, m.wires[i].a.1], "to": [m.wires[i].b.0, m.wires[i].b.1]}),
Item::Pin(i) => {
let p = &m.pins[i];
if p.power_symbol {
json!({"type": "power_symbol", "reference": p.reference, "net": p.power_net, "at": [p.at.0, p.at.1], "uuid": p.symbol_uuid})
} else {
json!({"type": "pin", "pin": format!("{}.{}", p.reference, p.number), "name": p.name, "electrical": p.electrical, "at": [p.at.0, p.at.1], "symbolUuid": p.symbol_uuid})
}
}
Item::Label(i) => json!({"type": "label", "kind": m.labels[i].kind, "text": m.labels[i].text, "at": [m.labels[i].at.0, m.labels[i].at.1], "uuid": m.labels[i].uuid}),
Item::SheetPin(i) => json!({"type": "sheet_pin", "name": sps[i].name, "sheet": sps[i].sheet_name, "shape": sps[i].shape, "at": [sps[i].at.0, sps[i].at.1], "uuid": sps[i].uuid}),
Item::Junction(i) => json!({"type": "junction", "at": [m.junctions[i].at.0, m.junctions[i].at.1], "uuid": m.junctions[i].uuid}),
Item::NoConnect(i) => json!({"type": "no_connect", "at": [m.no_connects[i].at.0, m.no_connects[i].at.1], "uuid": m.no_connects[i].uuid}),
}
};
let mut groups: Vec<usize> = here.iter().filter_map(|i| c.of.get(i).copied()).collect();
groups.sort();
groups.dedup();
let nets: Vec<Value> = groups
.iter()
.map(|&g| {
let grp = &c.groups[g];
let pins: Vec<Value> = grp.part_pins(m).map(|p| json!({"pin": format!("{}.{}", p.reference, p.number), "name": p.name, "electrical": p.electrical, "at": [p.at.0, p.at.1]})).collect();
let labels: Vec<Value> = grp.items.iter().filter_map(|i| if let Item::Label(k) = i { Some(json!({"kind": m.labels[*k].kind, "text": m.labels[*k].text, "at": [m.labels[*k].at.0, m.labels[*k].at.1], "uuid": m.labels[*k].uuid})) } else { None }).collect();
let power: Vec<String> = grp.pins(m).filter_map(|p| p.power_net.clone()).collect::<std::collections::BTreeSet<_>>().into_iter().collect();
let wires: Vec<String> = grp.items.iter().filter_map(|i| if let Item::Wire(k) = i { Some(m.wires[*k].uuid.clone()) } else { None }).collect::<std::collections::BTreeSet<_>>().into_iter().collect();
let sheet_pins: Vec<Value> = grp.items.iter().filter_map(|i| if let Item::SheetPin(k) = i { Some(json!({"name": sps[*k].name, "sheet": sps[*k].sheet_name})) } else { None }).collect();
let junctions = grp.items.iter().filter(|i| matches!(i, Item::Junction(_))).count();
let ncs = grp.items.iter().filter(|i| matches!(i, Item::NoConnect(_))).count();
json!({
"net": c.net_name(m, g),
"named": grp.best_name().is_some(),
"leavesSheet": grp.leaves_sheet(),
"pins": pins,
"labels": labels,
"powerSymbols": power,
"sheetPins": sheet_pins,
"wires": wires,
"junctionCount": junctions,
"noConnectCount": ncs,
})
})
.collect();
let crossing = {
let wires_here: Vec<usize> = here.iter().filter_map(|i| if let Item::Wire(k) = i { Some(*k) } else { None }).collect();
let gs: std::collections::BTreeSet<usize> = wires_here.iter().filter_map(|k| c.of.get(&Item::Wire(*k)).copied()).collect();
gs.len() >= 2
};
json!({
"at": [at.0, at.1],
"tolerance": tol,
"itemsHere": here.iter().map(item_json).collect::<Vec<_>>(),
"netCount": nets.len(),
"nets": nets,
"crossingWithoutJunction": crossing,
})
}
// ---------------------------------------------------------------------------
// Near-miss fixer
// ---------------------------------------------------------------------------
#[derive(Clone, Debug, Serialize)]
pub struct Move {
#[serde(rename = "wireUuid")]
pub wire_uuid: String,
pub from: [f64; 2],
pub to: [f64; 2],
pub distance: f64,
pub target: String,
/// The snapped wire is no longer horizontal or vertical.
pub diagonal: bool,
}
/// Dangling wire ends within `tol` of a pin, label, sheet pin or another wire's end.
pub fn plan_fixes(m: &SheetModel, tol: f64) -> (Vec<Move>, Vec<Value>) {
let mut moves = Vec::new();
let mut left = Vec::new();
for (seg, p, other) in dangling_ends(m) {
let w = &m.wires[seg];
match nearest_target(m, p, tol, &w.uuid) {
Some((q, what)) => {
let diagonal = (q.0 - other.0).abs() > 1e-6 && (q.1 - other.1).abs() > 1e-6;
moves.push(Move { wire_uuid: w.uuid.clone(), from: [r4(p.0), r4(p.1)], to: [r4(q.0), r4(q.1)], distance: r4(dist(p, q)), target: what, diagonal });
}
None => left.push(json!({"wireUuid": w.uuid, "at": [r4(p.0), r4(p.1)], "why": format!("nothing connectable within {tol} mm")})),
}
}
(moves, left)
}
/// Rewrite the matching `(xy ..)` of each wire. Returns how many points moved.
pub fn apply_moves(sch: &mut Schematic, moves: &[Move]) -> usize {
let mut n = 0;
for mv in moves {
let Some(idx) = sch.find_uuid_index(&mv.wire_uuid) else { continue };
let node = &mut sch.doc.root.children_mut().expect("list")[idx];
let Some(pts) = node.child_mut("pts") else { continue };
for xy in pts.children_mut().expect("list").iter_mut().filter(|c| c.head() == Some("xy")) {
let (Some(x), Some(y)) = (xy.num(0), xy.num(1)) else { continue };
if same((x, y), (mv.from[0], mv.from[1])) {
xy.set_arg(0, Node::number(mv.to[0]));
xy.set_arg(1, Node::number(mv.to[1]));
n += 1;
break;
}
}
}
n
}