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…
baad156
14d ago
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711
//! Schematic edit, second half: the geometry and connectivity the wiring verbs need, on top
//! of the lossless document model in `schematic`.
//!
//! * symbol transforms: a library point or vector through a placed unit's angle and mirror
//! (the same order `Schematic::pin_points` uses, checked against KiCad's demo sheets);
//! * every pin of every placed unit with its sheet point, outward direction, name, number
//! and electrical type;
//! * wires, buses, labels, junctions, no-connects, bus entries, texts and sheets as plain
//! lists;
//! * KiCad's junction rule (three or more distinct exits at a point, a wire end on another
//! wire's interior counting as two), used to add dots after new wiring and to prune them
//! after a delete;
//! * the edit operations behind the newer kicad_sch_* verbs: multi-unit move with wire
//! stretch, rotate, region move, annotate, symbol replace and library refresh, field reset,
//! wire delete and split, label edit, junctions, stubs to a net, pin to pin routes, text,
//! buses and bus entries.
//!
//! Everything here is pure document work; the verb layer resolves libraries, runs ERC and
//! writes the file.
use std::collections::{BTreeMap, BTreeSet};
use std::path::{Path, PathBuf};
use serde_json::{json, Value};
use crate::schematic::{
fmt_num, font_node, is_symbol_instance, lib_symbol_pins, new_uuid, norm_rotation, pretty, project_name_for, property_node, quote, sub_unit, symbol_property, xy, Kind, Node, Schematic,
};
pub type Pt = (f64, f64);
/// Coordinates closer than this are the same point (KiCad stores nanometres; files carry
/// at most four decimals of a millimetre).
pub const EPS: f64 = 1e-3;
/// The schematic grid KiCad's connection points sit on.
pub const GRID: f64 = 1.27;
/// An operation error with a stable code for the verb layer.
#[derive(Clone, Debug)]
pub struct OpError {
pub code: &'static str,
pub msg: String,
}
impl OpError {
pub fn new(code: &'static str, msg: impl Into<String>) -> OpError {
OpError { code, msg: msg.into() }
}
}
impl From<String> for OpError {
fn from(msg: String) -> OpError {
OpError { code: "edit_failed", msg }
}
}
impl std::fmt::Display for OpError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}: {}", self.code, self.msg)
}
}
pub type R<T> = Result<T, OpError>;
pub fn same(a: Pt, b: Pt) -> bool {
(a.0 - b.0).abs() < EPS && (a.1 - b.1).abs() < EPS
}
pub fn r4(v: f64) -> f64 {
(v * 1e4).round() / 1e4 + 0.0
}
pub fn rp(p: Pt) -> Pt {
(r4(p.0), r4(p.1))
}
/// Round to the nearest 1.27 mm grid point.
pub fn snap(p: Pt) -> Pt {
(r4((p.0 / GRID).round() * GRID), r4((p.1 / GRID).round() * GRID))
}
/// `p` lies on the segment a-b (ends included).
pub fn on_segment(p: Pt, a: Pt, b: Pt) -> bool {
let (dx, dy) = (b.0 - a.0, b.1 - a.1);
let len = (dx * dx + dy * dy).sqrt();
if len < EPS {
return same(p, a);
}
let cross = ((p.0 - a.0) * dy - (p.1 - a.1) * dx) / len;
if cross.abs() > EPS {
return false;
}
let t = ((p.0 - a.0) * dx + (p.1 - a.1) * dy) / len;
t > -EPS && t < len + EPS
}
/// `p` lies on a-b but is neither end.
pub fn on_interior(p: Pt, a: Pt, b: Pt) -> bool {
!same(p, a) && !same(p, b) && on_segment(p, a, b)
}
pub fn in_box(p: Pt, bx: [f64; 4]) -> bool {
let (x0, x1) = (bx[0].min(bx[2]), bx[0].max(bx[2]));
let (y0, y1) = (bx[1].min(bx[3]), bx[1].max(bx[3]));
p.0 > x0 - EPS && p.0 < x1 + EPS && p.1 > y0 - EPS && p.1 < y1 + EPS
}
fn unit_dir(from: Pt, to: Pt) -> Pt {
let (dx, dy) = (to.0 - from.0, to.1 - from.1);
let l = (dx * dx + dy * dy).sqrt();
if l < EPS {
(0.0, 0.0)
} else {
(r4(dx / l), r4(dy / l))
}
}
/// "right", "left", "up", "down" (sheet view, y down) for an axis vector, else the angle.
pub fn dir_name(v: Pt) -> String {
if same(v, (1.0, 0.0)) {
"right".into()
} else if same(v, (-1.0, 0.0)) {
"left".into()
} else if same(v, (0.0, -1.0)) {
"up".into()
} else if same(v, (0.0, 1.0)) {
"down".into()
} else {
format!("{}deg", fmt_num((-v.1).atan2(v.0).to_degrees()))
}
}
pub fn pt_json(p: Pt) -> Value {
json!([r4(p.0), r4(p.1)])
}
fn at_of(n: &Node) -> Option<Pt> {
let a = n.child("at")?;
Some((a.num(0)?, a.num(1)?))
}
fn set_at_xy(n: &mut Node, p: Pt) {
if let Some(a) = n.child_mut("at") {
a.set_arg(0, Node::number(r4(p.0)));
a.set_arg(1, Node::number(r4(p.1)));
}
}
/// Shift every `(at x y ..)`, `(xy x y)`, `(start x y)`, `(end x y)` and `(center x y)` in a
/// top-level item by (dx, dy): a symbol instance's anchor and fields, a label and its
/// properties, a sheet and its pins, a text box.
pub fn shift_node(n: &mut Node, dx: f64, dy: f64) {
let Some(children) = n.children_mut() else { return };
for c in children.iter_mut() {
match c.head() {
Some("at") | Some("xy") | Some("start") | Some("end") | Some("center") | Some("mid") => {
if let (Some(x), Some(y)) = (c.num(0), c.num(1)) {
c.set_arg(0, Node::number(r4(x + dx)));
c.set_arg(1, Node::number(r4(y + dy)));
}
}
// An instance's `instances` block and stroke/effects carry no coordinates.
Some("effects") | Some("stroke") | Some("instances") | Some("size") => {}
_ => {
if c.is_list() {
shift_node(c, dx, dy);
}
}
}
}
}
/// A canonical, whitespace-free spelling of a node, to compare two library blocks.
pub fn canon(n: &Node) -> String {
match &n.kind {
Kind::Atom(s) => s.clone(),
Kind::List { children, .. } => format!("({})", children.iter().map(canon).collect::<Vec<_>>().join(" ")),
}
}
// ---------------------------------------------------------------------------
// Symbol transform and pins
// ---------------------------------------------------------------------------
/// A placed unit's placement: anchor, angle (counterclockwise as seen) and mirror.
#[derive(Clone, Copy, Debug)]
pub struct SymXf {
pub x: f64,
pub y: f64,
pub rot: f64,
pub mirror: Option<char>,
}
impl SymXf {
pub fn of(inst: &Node) -> SymXf {
let at = inst.child("at");
SymXf {
x: at.and_then(|a| a.num(0)).unwrap_or(0.0),
y: at.and_then(|a| a.num(1)).unwrap_or(0.0),
rot: at.and_then(|a| a.num(2)).unwrap_or(0.0),
mirror: inst.child_value("mirror").and_then(|m| m.chars().next()),
}
}
/// A library vector (y up) as a sheet vector (y down).
pub fn vec(&self, v: Pt) -> Pt {
let (s, c) = self.rot.to_radians().sin_cos();
let (mut qx, mut qy) = (v.0 * c - v.1 * s, v.0 * s + v.1 * c);
match self.mirror {
Some('x') => qy = -qy,
Some('y') => qx = -qx,
_ => {}
}
(r4(qx), r4(-qy))
}
/// A library point as a sheet point.
pub fn point(&self, p: Pt) -> Pt {
let v = self.vec(p);
rp((self.x + v.0, self.y + v.1))
}
}
/// Rotate a sheet vector (y down) counterclockwise as seen by `deg`.
pub fn rotate_sheet_vec(v: Pt, deg: f64) -> Pt {
let (s, c) = deg.to_radians().sin_cos();
let (ux, uy) = (v.0, -v.1);
let (rx, ry) = (ux * c - uy * s, ux * s + uy * c);
(r4(rx), r4(-ry))
}
/// One pin of a library symbol.
#[derive(Clone, Debug)]
pub struct LibPin {
pub number: String,
pub name: String,
pub etype: String,
pub at: Pt,
pub angle: f64,
pub length: f64,
pub unit: u32,
pub hidden: bool,
}
/// The pins a placed unit carries: shared (unit 0) and its own, body style `style` (or the
/// shared style 0). Library coordinates.
pub fn lib_unit_pins(lib: &Node, base: &str, unit: u32, style: u32) -> Vec<LibPin> {
let mut out = Vec::new();
for sub in lib.children().iter().filter(|n| n.head() == Some("symbol")) {
let Some(name) = sub.arg(0) else { continue };
let Some(u) = sub_unit(&name, base) else { continue };
let st: u32 = name.rsplit('_').next().and_then(|s| s.parse().ok()).unwrap_or(1);
if (u != 0 && u != unit) || (st != 0 && st != style) {
continue;
}
for pin in sub.children().iter().filter(|n| n.head() == Some("pin")) {
let (Some(num), Some(pat)) = (pin.child_value("number"), pin.child("at")) else { continue };
let hidden = pin.children().iter().any(|c| c.raw() == Some("hide")) || pin.child("hide").and_then(|h| h.arg(0)).as_deref() == Some("yes");
out.push(LibPin {
number: num,
name: pin.child_value("name").unwrap_or_default(),
etype: pin.arg(0).unwrap_or_default(),
at: (pat.num(0).unwrap_or(0.0), pat.num(1).unwrap_or(0.0)),
angle: pat.num(2).unwrap_or(0.0),
length: pin.child("length").and_then(|l| l.num(0)).unwrap_or(0.0),
unit: u,
hidden,
});
}
}
out
}
/// Number of units a library symbol defines (1 when it has only shared sub-symbols).
pub fn lib_unit_count(lib: &Node, base: &str) -> u32 {
lib.children()
.iter()
.filter(|n| n.head() == Some("symbol"))
.filter_map(|n| n.arg(0).and_then(|name| sub_unit(&name, base)))
.max()
.unwrap_or(1)
.max(1)
}
/// A pin of a placed unit, in sheet coordinates.
#[derive(Clone, Debug)]
pub struct PinInfo {
pub reference: String,
pub unit: u32,
pub number: String,
pub name: String,
pub etype: String,
pub at: Pt,
/// Unit vector from the pin's connection point away from the body (sheet, y down).
pub out: Pt,
pub length: f64,
pub symbol_uuid: String,
pub hidden: bool,
pub lib_id: String,
}
impl PinInfo {
pub fn spec(&self) -> String {
format!("{}.{}", self.reference, self.number)
}
}
/// The key of an instance's `lib_symbols` entry (lib_name wins over lib_id).
pub fn lib_key(inst: &Node) -> String {
inst.child_value("lib_name").or_else(|| inst.child_value("lib_id")).unwrap_or_default()
}
pub fn base_name(key: &str) -> String {
key.rsplit(':').next().unwrap_or(key).to_string()
}
fn inst_unit(inst: &Node) -> u32 {
inst.child("unit").and_then(|u| u.num(0)).unwrap_or(1.0) as u32
}
fn inst_style(inst: &Node) -> u32 {
inst.child("body_style").or_else(|| inst.child("convert")).and_then(|c| c.num(0)).map(|v| v as u32).unwrap_or(1)
}
/// A wire or bus segment.
#[derive(Clone, Debug)]
pub struct Seg {
pub uuid: String,
pub a: Pt,
pub b: Pt,
}
fn seg_of(n: &Node) -> Option<Seg> {
let pts = n.child("pts")?;
let xys: Vec<&Node> = pts.children().iter().filter(|c| c.head() == Some("xy")).collect();
if xys.len() < 2 {
return None;
}
Some(Seg { uuid: n.child_value("uuid").unwrap_or_default(), a: (xys[0].num(0)?, xys[0].num(1)?), b: (xys[1].num(0)?, xys[1].num(1)?) })
}
fn set_seg_end(n: &mut Node, end: usize, p: Pt) {
let Some(pts) = n.child_mut("pts") else { return };
let Some(children) = pts.children_mut() else { return };
if let Some(x) = children.iter_mut().filter(|c| c.head() == Some("xy")).nth(end) {
x.set_arg(0, Node::number(r4(p.0)));
x.set_arg(1, Node::number(r4(p.1)));
}
}
pub const LABEL_HEADS: &[&str] = &["label", "global_label", "hierarchical_label"];
/// A net label of any kind.
#[derive(Clone, Debug)]
pub struct LabelInfo {
pub kind: String,
pub text: String,
pub at: Pt,
pub rotation: f64,
pub uuid: String,
pub shape: Option<String>,
}
/// What touches one point: for the junction rule and the pin reports.
struct Conn {
wires: Vec<Seg>,
pin_pts: Vec<Pt>,
}
impl Conn {
/// KiCad's explicit-junction rule: three or more distinct exits meet at `p` (a wire end
/// gives one exit, a wire passing through gives two, every pin its own), and a wire is
/// one of them.
fn needed(&self, p: Pt) -> bool {
let mut dirs: Vec<Pt> = Vec::new();
let mut uniques = 0usize;
let mut wire = false;
let push = |d: Pt, dirs: &mut Vec<Pt>| {
if !dirs.iter().any(|e| same(*e, d)) {
dirs.push(d);
}
};
for s in &self.wires {
if same(p, s.a) {
wire = true;
push(unit_dir(s.a, s.b), &mut dirs);
} else if same(p, s.b) {
wire = true;
push(unit_dir(s.b, s.a), &mut dirs);
} else if on_interior(p, s.a, s.b) {
wire = true;
push(unit_dir(p, s.a), &mut dirs);
push(unit_dir(p, s.b), &mut dirs);
}
}
for q in &self.pin_pts {
if same(*q, p) {
uniques += 1;
}
}
wire && dirs.len() + uniques >= 3
}
}
// ---------------------------------------------------------------------------
// Schematic: queries
// ---------------------------------------------------------------------------
impl Schematic {
/// Top-level indices of every placed unit of `reference`.
pub fn unit_indices(&self, reference: &str) -> Vec<usize> {
self.top().iter().enumerate().filter(|(_, n)| is_symbol_instance(n) && symbol_property(n, "Reference").as_deref() == Some(reference)).map(|(i, _)| i).collect()
}
fn require_units(&self, reference: &str) -> R<Vec<usize>> {
let v = self.unit_indices(reference);
if v.is_empty() {
return Err(OpError::new("not_found", format!("no symbol with reference {reference} on this sheet")));
}
Ok(v)
}
/// The pins of one placed unit (top-level node).
pub fn instance_pins(&self, inst: &Node) -> R<Vec<PinInfo>> {
let key = lib_key(inst);
let reference = symbol_property(inst, "Reference").unwrap_or_default();
let lib = self.lib_symbol(&key).ok_or_else(|| OpError::new("lib_symbol_missing", format!("{reference}: its library symbol {key} is not in the sheet's lib_symbols")))?;
let xf = SymXf::of(inst);
let unit = inst_unit(inst);
let uuid = inst.child_value("uuid").unwrap_or_default();
let lib_id = inst.child_value("lib_id").unwrap_or_default();
Ok(lib_unit_pins(lib, &base_name(&key), unit, inst_style(inst))
.into_iter()
.map(|p| {
let a = p.angle.to_radians();
let out = xf.vec((-a.cos(), -a.sin()));
PinInfo {
reference: reference.clone(),
unit,
number: p.number,
name: p.name,
etype: p.etype,
at: xf.point(p.at),
out,
length: p.length,
symbol_uuid: uuid.clone(),
hidden: p.hidden,
lib_id: lib_id.clone(),
}
})
.collect())
}
/// Every pin of every placed unit of `reference`.
pub fn pins_of(&self, reference: &str) -> R<Vec<PinInfo>> {
let mut out = Vec::new();
for i in self.require_units(reference)? {
out.extend(self.instance_pins(&self.top()[i])?);
}
Ok(out)
}
/// Every pin on the sheet (units whose library symbol is missing are skipped).
pub fn all_pins(&self) -> Vec<PinInfo> {
self.top().iter().filter(|n| is_symbol_instance(n)).filter_map(|n| self.instance_pins(n).ok()).flatten().collect()
}
/// `U1.3` or `U1.PA10`: by number first, then by name (case-insensitive).
pub fn find_pin(&self, spec: &str) -> R<PinInfo> {
let (reference, want) = spec.trim().split_once('.').ok_or_else(|| OpError::new("bad_arg", format!("pin {spec} is not Ref.Pin (e.g. U1.12 or U1.PA10)")))?;
let pins = self.pins_of(reference)?;
if let Some(p) = pins.iter().find(|p| p.number == want) {
return Ok(p.clone());
}
if let Some(p) = pins.iter().find(|p| p.name.eq_ignore_ascii_case(want)) {
return Ok(p.clone());
}
Err(OpError::new("pin_not_found", format!("{reference} has no pin {want} (numbers: {})", pins.iter().map(|p| p.number.clone()).collect::<Vec<_>>().join(", "))))
}
pub fn segments(&self, head: &str) -> Vec<Seg> {
self.top().iter().filter(|n| n.head() == Some(head)).filter_map(seg_of).collect()
}
pub fn wires(&self) -> Vec<Seg> {
self.segments("wire")
}
pub fn labels(&self) -> Vec<LabelInfo> {
self.top()
.iter()
.filter(|n| n.head().map_or(false, |h| LABEL_HEADS.contains(&h)))
.map(|n| {
let at = n.child("at");
LabelInfo {
kind: n.head().unwrap_or("").to_string(),
text: n.arg(0).unwrap_or_default(),
at: (at.and_then(|a| a.num(0)).unwrap_or(0.0), at.and_then(|a| a.num(1)).unwrap_or(0.0)),
rotation: at.and_then(|a| a.num(2)).unwrap_or(0.0),
uuid: n.child_value("uuid").unwrap_or_default(),
shape: n.child_value("shape"),
}
})
.collect()
}
/// (uuid, point) of every top-level item with head `head` and an `(at x y)`.
pub fn point_items(&self, head: &str) -> Vec<(String, Pt)> {
self.top().iter().filter(|n| n.head() == Some(head)).filter_map(|n| Some((n.child_value("uuid").unwrap_or_default(), at_of(n)?))).collect()
}
pub fn junction_at(&self, p: Pt) -> Option<String> {
self.point_items("junction").into_iter().find(|(_, q)| same(*q, p)).map(|(u, _)| u)
}
fn conn(&self) -> Conn {
Conn { wires: self.wires(), pin_pts: self.all_pins().into_iter().map(|p| p.at).collect() }
}
/// True when KiCad would want a junction dot at `p` (ignoring any dot already there).
pub fn junction_needed(&self, p: Pt) -> bool {
self.conn().needed(p)
}
/// What touches a point: wires (end or interior), labels, no-connects, junctions, pins.
pub fn touching(&self, p: Pt) -> Value {
let wires: Vec<Value> = self
.wires()
.into_iter()
.filter(|s| on_segment(p, s.a, s.b))
.map(|s| json!({"uuid": s.uuid, "from": pt_json(s.a), "to": pt_json(s.b), "atEnd": same(p, s.a) || same(p, s.b)}))
.collect();
let labels: Vec<Value> = self.labels().into_iter().filter(|l| same(l.at, p)).map(|l| json!({"kind": l.kind, "text": l.text, "uuid": l.uuid})).collect();
json!({
"wires": wires,
"labels": labels,
"noConnect": self.point_items("no_connect").iter().any(|(_, q)| same(*q, p)),
"junction": self.junction_at(p),
})
}
/// The net names reachable from `p` through wires (shared ends, junctions on interiors,
/// labels anywhere on a wire), and the other pins on that copper. A label or power pin
/// sitting directly on `p` counts too.
pub fn net_at(&self, p: Pt, pins: &[PinInfo]) -> (Vec<String>, Vec<String>) {
let wires = self.wires();
let junctions: Vec<Pt> = self.point_items("junction").into_iter().map(|(_, q)| q).collect();
let labels = self.labels();
let mut names: BTreeSet<String> = BTreeSet::new();
let mut reached_pts: Vec<Pt> = vec![p];
let mut used = vec![false; wires.len()];
let mut frontier = vec![p];
while let Some(q) = frontier.pop() {
for (i, s) in wires.iter().enumerate() {
if used[i] {
continue;
}
let at_end = same(q, s.a) || same(q, s.b);
let mid = on_interior(q, s.a, s.b) && (junctions.iter().any(|j| same(*j, q)) || same(q, p));
if !(at_end || mid) {
continue;
}
used[i] = true;
for e in [s.a, s.b] {
if !reached_pts.iter().any(|r| same(*r, e)) {
reached_pts.push(e);
frontier.push(e);
}
}
for j in &junctions {
if on_interior(*j, s.a, s.b) && !reached_pts.iter().any(|r| same(*r, *j)) {
reached_pts.push(*j);
frontier.push(*j);
}
}
for l in &labels {
if on_segment(l.at, s.a, s.b) {
names.insert(l.text.clone());
}
}
}
}
for l in &labels {
if same(l.at, p) {
names.insert(l.text.clone());
}
}
let mut others = Vec::new();
for pin in pins {
if reached_pts.iter().any(|r| same(*r, pin.at)) || wires.iter().enumerate().any(|(i, s)| used[i] && on_interior(pin.at, s.a, s.b)) {
if pin.reference.starts_with('#') {
// power symbol: its Value is the net name
if let Some(inst) = self.top().iter().find(|n| n.child_value("uuid").as_deref() == Some(pin.symbol_uuid.as_str())) {
if inst.child_value("lib_id").map_or(false, |l| l.starts_with("power:")) || self.lib_symbol(&lib_key(inst)).map_or(false, |l| l.child("power").is_some()) {
if let Some(v) = symbol_property(inst, "Value") {
names.insert(v);
}
}
}
}
others.push(pin.spec());
}
}
(names.into_iter().collect(), others)
}
/// Every item kind as plain lists (for kicad_sch_items).
pub fn items(&self, kinds: Option<&[String]>) -> Value {
let want = |k: &str| kinds.map_or(true, |ks| ks.iter().any(|x| x == k));
let mut v = serde_json::Map::new();
let segs = |head: &str| -> Vec<Value> { self.segments(head).into_iter().map(|s| json!({"uuid": s.uuid, "from": pt_json(s.a), "to": pt_json(s.b)})).collect() };
if want("wires") {
v.insert("wires".into(), json!(segs("wire")));
}
if want("buses") {
v.insert("buses".into(), json!(segs("bus")));
}
if want("busEntries") {
let e: Vec<Value> = self
.top()
.iter()
.filter(|n| n.head() == Some("bus_entry"))
.filter_map(|n| {
let at = at_of(n)?;
let sz = n.child("size").map(|s| (s.num(0).unwrap_or(0.0), s.num(1).unwrap_or(0.0))).unwrap_or((2.54, 2.54));
Some(json!({"uuid": n.child_value("uuid"), "from": pt_json(at), "to": pt_json((at.0 + sz.0, at.1 + sz.1)), "size": pt_json(sz)}))
})
.collect();
v.insert("busEntries".into(), json!(e));
}
if want("labels") {
let l: Vec<Value> = self.labels().into_iter().map(|l| json!({"kind": l.kind, "text": l.text, "at": pt_json(l.at), "rotation": l.rotation, "shape": l.shape, "uuid": l.uuid})).collect();
v.insert("labels".into(), json!(l));
}
if want("powerSymbols") {
let p: Vec<Value> = self
.top()
.iter()
.filter(|n| is_symbol_instance(n) && (n.child_value("lib_id").map_or(false, |l| l.starts_with("power:")) || self.lib_symbol(&lib_key(n)).map_or(false, |l| l.child("power").is_some())))
.map(|n| json!({"reference": symbol_property(n, "Reference"), "net": symbol_property(n, "Value"), "libId": n.child_value("lib_id"), "at": at_of(n).map(pt_json), "uuid": n.child_value("uuid")}))
.collect();
v.insert("powerSymbols".into(), json!(p));
}
let pts = |head: &str| -> Vec<Value> { self.point_items(head).into_iter().map(|(u, p)| json!({"uuid": u, "at": pt_json(p)})).collect() };
if want("junctions") {
v.insert("junctions".into(), json!(pts("junction")));
}
if want("noConnects") {
v.insert("noConnects".into(), json!(pts("no_connect")));
}
if want("texts") {
let t: Vec<Value> = self
.top()
.iter()
.filter(|n| n.head() == Some("text"))
.map(|n| json!({"text": n.arg(0), "at": at_of(n).map(pt_json), "rotation": n.child("at").and_then(|a| a.num(2)).unwrap_or(0.0), "uuid": n.child_value("uuid")}))
.collect();
v.insert("texts".into(), json!(t));
}
if want("sheets") {
let s: Vec<Value> = self
.top()
.iter()
.filter(|n| n.head() == Some("sheet"))
.map(|n| {
let size = n.child("size").map(|s| json!([s.num(0), s.num(1)]));
let pins: Vec<Value> = n.children().iter().filter(|c| c.head() == Some("pin")).map(|c| json!({"name": c.arg(0), "shape": c.arg(1), "at": at_of(c).map(pt_json), "uuid": c.child_value("uuid")})).collect();
json!({"name": symbol_property(n, "Sheetname").or_else(|| symbol_property(n, "Sheet name")), "file": symbol_property(n, "Sheetfile").or_else(|| symbol_property(n, "Sheet file")), "at": at_of(n).map(pt_json), "size": size, "pins": pins, "uuid": n.child_value("uuid")})
})
.collect();
v.insert("sheets".into(), json!(s));
}
Value::Object(v)
}
/// Placed symbols grouped by reference with every unit and every field.
pub fn components(&self, filter: Option<&[String]>) -> Vec<Value> {
let mut order: Vec<String> = Vec::new();
let mut by_ref: BTreeMap<String, Vec<&Node>> = BTreeMap::new();
for n in self.top().iter().filter(|n| is_symbol_instance(n)) {
let r = symbol_property(n, "Reference").unwrap_or_default();
if let Some(f) = filter {
if !f.iter().any(|x| x == &r) {
continue;
}
}
// unannotated X? parts are separate parts, not units of one
let key = if r.trim().ends_with('?') { format!("{r}\u{0}{}", n.child_value("uuid").unwrap_or_default()) } else { r };
if !by_ref.contains_key(&key) {
order.push(key.clone());
}
by_ref.entry(key).or_default().push(n);
}
order
.iter()
.map(|r| {
let units = &by_ref[r];
let first = units[0];
let fields: Vec<Value> = first
.children()
.iter()
.filter(|c| c.head() == Some("property"))
.map(|c| {
let hidden = c.child("effects").map_or(false, |e| e.child("hide").map_or(false, |h| h.arg(0).as_deref() != Some("no")) || e.children().iter().any(|x| x.raw() == Some("hide")));
json!({"name": c.arg(0), "value": c.arg(1), "at": at_of(c).map(pt_json), "angle": c.child("at").and_then(|a| a.num(2)).unwrap_or(0.0), "hidden": hidden})
})
.collect();
let key = lib_key(first);
let unit_count = self.lib_symbol(&key).map(|l| lib_unit_count(l, &base_name(&key)));
let us: Vec<Value> = units
.iter()
.map(|u| {
let xf = SymXf::of(u);
json!({
"unit": inst_unit(u),
"uuid": u.child_value("uuid"),
"at": pt_json((xf.x, xf.y)),
"rotation": xf.rot,
"mirror": u.child_value("mirror"),
"inBom": u.child_value("in_bom").map(|v| v == "yes"),
"onBoard": u.child_value("on_board").map(|v| v == "yes"),
"dnp": u.child_value("dnp").map(|v| v == "yes"),
})
})
.collect();
json!({
"reference": symbol_property(first, "Reference"),
"value": symbol_property(first, "Value"),
"footprint": symbol_property(first, "Footprint"),
"datasheet": symbol_property(first, "Datasheet"),
"libId": first.child_value("lib_id"),
"libName": first.child_value("lib_name"),
"unitCount": unit_count,
"units": us,
"fields": fields,
})
})
.collect()
}
}
// ---------------------------------------------------------------------------
// Schematic: wiring edits
// ---------------------------------------------------------------------------
fn stroke() -> Node {
Node::list("stroke", vec![Node::list("width", vec![Node::number(0.0)]), Node::list("type", vec![Node::atom("default")])])
}
impl Schematic {
fn index_of_uuid(&self, uuid: &str) -> Option<usize> {
self.find_uuid_index(uuid)
}
/// One `(wire)` or `(bus)` per straight segment of `points`.
pub fn add_segments(&mut self, head: &str, points: &[Pt]) -> R<Vec<String>> {
if points.len() < 2 {
return Err(OpError::new("bad_arg", format!("a {head} needs at least two points")));
}
let mut uuids = Vec::new();
for s in points.windows(2) {
if same(s[0], s[1]) {
continue;
}
let uuid = new_uuid();
self.insert_item(Node::list(head, vec![Node::list("pts", vec![xy(rp(s[0])), xy(rp(s[1]))]), stroke(), Node::list("uuid", vec![Node::quoted(&uuid)])]));
uuids.push(uuid);
}
if uuids.is_empty() {
return Err(OpError::new("bad_arg", "all points are the same; nothing to draw"));
}
Ok(uuids)
}
pub fn add_junction(&mut self, p: Pt) -> String {
let uuid = new_uuid();
self.insert_item(Node::list(
"junction",
vec![
Node::list("at", vec![Node::number(r4(p.0)), Node::number(r4(p.1))]),
Node::list("diameter", vec![Node::number(0.0)]),
Node::list("color", vec![Node::atom("0"), Node::atom("0"), Node::atom("0"), Node::atom("0")]),
Node::list("uuid", vec![Node::quoted(&uuid)]),
],
));
uuid
}
/// Add a dot at every point in `candidates` that KiCad's rule wants and that has none.
pub fn auto_junctions(&mut self, candidates: &[Pt]) -> Vec<(String, Pt)> {
let conn = self.conn();
let mut done: Vec<Pt> = Vec::new();
let mut out = Vec::new();
for &p in candidates {
if done.iter().any(|d| same(*d, p)) {
continue;
}
done.push(p);
if self.junction_at(p).is_none() && conn.needed(p) {
out.push((self.add_junction(p), rp(p)));
}
}
out
}
/// The points worth testing after new wires were drawn: their ends, and every existing
/// wire end or pin that lies on one of them.
pub fn junction_candidates(&self, new_uuids: &[String]) -> Vec<Pt> {
let wires = self.wires();
let new: Vec<&Seg> = wires.iter().filter(|s| new_uuids.contains(&s.uuid)).collect();
let mut pts: Vec<Pt> = Vec::new();
for s in &new {
pts.push(s.a);
pts.push(s.b);
}
for w in wires.iter().filter(|s| !new_uuids.contains(&s.uuid)) {
for e in [w.a, w.b] {
if new.iter().any(|s| on_interior(e, s.a, s.b)) {
pts.push(e);
}
}
}
for p in self.all_pins() {
if new.iter().any(|s| on_interior(p.at, s.a, s.b)) {
pts.push(p.at);
}
}
pts
}
/// Draw a polyline and add the junctions it needs.
pub fn add_wire_auto(&mut self, points: &[Pt]) -> R<(Vec<String>, Vec<(String, Pt)>)> {
let uuids = self.add_segments("wire", points)?;
let cands = self.junction_candidates(&uuids);
let j = self.auto_junctions(&cands);
Ok((uuids, j))
}
/// Remove every junction at one of `points` (or on one of `segs`) that the rule no
/// longer wants. Returns the removed uuids.
pub fn prune_junctions(&mut self, points: &[Pt], segs: &[(Pt, Pt)]) -> Vec<String> {
let conn = self.conn();
let doomed: Vec<String> = self
.point_items("junction")
.into_iter()
.filter(|(_, p)| points.iter().any(|q| same(*q, *p)) || segs.iter().any(|(a, b)| on_segment(*p, *a, *b)))
.filter(|(_, p)| !conn.needed(*p))
.map(|(u, _)| u)
.collect();
for u in &doomed {
if let Some(i) = self.index_of_uuid(u) {
self.top_mut().remove(i);
}
}
doomed
}
/// Delete wires by uuid; returns their geometry and the junctions pruned.
pub fn delete_wires(&mut self, uuids: &[String]) -> R<(Vec<Seg>, Vec<String>)> {
let mut gone = Vec::new();
for u in uuids {
let i = self.index_of_uuid(u).ok_or_else(|| OpError::new("not_found", format!("no item with uuid {u} on this sheet")))?;
let n = &self.top()[i];
if n.head() != Some("wire") && n.head() != Some("bus") {
return Err(OpError::new("wrong_kind", format!("{u} is a {}, not a wire", n.head().unwrap_or("item"))));
}
let s = seg_of(n).ok_or_else(|| OpError::new("parse_failed", format!("wire {u} has no (pts ..)")))?;
self.top_mut().remove(i);
gone.push(s);
}
let pts: Vec<Pt> = gone.iter().flat_map(|s| [s.a, s.b]).collect();
let segs: Vec<(Pt, Pt)> = gone.iter().map(|s| (s.a, s.b)).collect();
let pruned = self.prune_junctions(&pts, &segs);
Ok((gone, pruned))
}
/// Wires whose two ends are `a` and `b` (either order).
pub fn wires_between(&self, a: Pt, b: Pt) -> Vec<String> {
self.wires().into_iter().filter(|s| (same(s.a, a) && same(s.b, b)) || (same(s.a, b) && same(s.b, a))).map(|s| s.uuid).collect()
}
/// Split the wire passing through `at` into two; optionally a junction at the cut.
pub fn split_wire(&mut self, at: Pt, uuid: Option<&str>, junction: bool) -> R<(String, String, Option<String>)> {
let wires = self.wires();
let hit: Vec<&Seg> = wires.iter().filter(|s| uuid.map_or(true, |u| s.uuid == u) && on_interior(at, s.a, s.b)).collect();
let s = match hit.len() {
0 => return Err(OpError::new("not_on_wire", format!("no wire{} passes through ({}, {}) away from its ends", uuid.map(|u| format!(" {u}")).unwrap_or_default(), fmt_num(at.0), fmt_num(at.1)))),
1 => hit[0].clone(),
_ => return Err(OpError::new("ambiguous", format!("{} wires pass through ({}, {}); pass uuid to pick one", hit.len(), fmt_num(at.0), fmt_num(at.1)))),
};
let i = self.index_of_uuid(&s.uuid).expect("found above");
let mut second = self.top()[i].clone();
set_seg_end(&mut self.top_mut()[i], 1, at);
let new = new_uuid();
set_seg_end(&mut second, 0, at);
if let Some(u) = second.child_mut("uuid") {
u.set_arg(0, Node::quoted(&new));
}
self.top_mut().insert(i + 1, second);
let j = if junction && self.junction_at(at).is_none() { Some(self.add_junction(at)) } else { None };
Ok((s.uuid, new, j))
}
pub fn add_no_connects(&mut self, pts: &[Pt]) -> Vec<String> {
pts.iter().map(|p| self.add_no_connect(rp(*p)).expect("infallible")).collect()
}
/// Remove top-level items of kind `head` at `p`; returns their uuids.
pub fn remove_point_items(&mut self, head: &str, p: Pt) -> Vec<String> {
let doomed: Vec<String> = self.point_items(head).into_iter().filter(|(_, q)| same(*q, p)).map(|(u, _)| u).collect();
for u in &doomed {
if let Some(i) = self.index_of_uuid(u) {
self.top_mut().remove(i);
}
}
doomed
}
/// Remove a top-level item by uuid, checking its kind.
pub fn remove_kind(&mut self, head: &str, uuid: &str) -> R<()> {
let i = self.index_of_uuid(uuid).ok_or_else(|| OpError::new("not_found", format!("no item with uuid {uuid} on this sheet")))?;
if self.top()[i].head() != Some(head) {
return Err(OpError::new("wrong_kind", format!("{uuid} is a {}, not a {head}", self.top()[i].head().unwrap_or("item"))));
}
self.top_mut().remove(i);
Ok(())
}
/// Free text: `(text "..." (exclude_from_sim no) (at x y r) (effects ..) (uuid ..))`.
pub fn add_text(&mut self, text: &str, at: Pt, rotation: f64, size: f64, justify: &[String], bold: bool, italic: bool) -> R<String> {
if text.is_empty() {
return Err(OpError::new("bad_arg", "text is required"));
}
let rot = norm_rotation(rotation).map_err(|e| OpError::new("bad_arg", e))?;
if !(size > 0.0 && size < 100.0) {
return Err(OpError::new("bad_arg", format!("size must be a positive height in mm (got {size})")));
}
for j in justify {
if !["left", "right", "top", "bottom", "mirror"].contains(&j.as_str()) {
return Err(OpError::new("bad_arg", format!("justify takes left, right, top, bottom (got {j})")));
}
}
let mut font = vec![Node::list("size", vec![Node::number(size), Node::number(size)])];
if bold {
font.push(Node::list("bold", vec![Node::atom("yes")]));
}
if italic {
font.push(Node::list("italic", vec![Node::atom("yes")]));
}
let mut effects = vec![Node::list("font", font)];
if !justify.is_empty() {
effects.push(Node::list("justify", justify.iter().map(|j| Node::atom(j.as_str())).collect()));
}
let uuid = new_uuid();
self.insert_item(Node::list(
"text",
vec![
Node::quoted(text),
Node::list("exclude_from_sim", vec![Node::atom("no")]),
Node::list("at", vec![Node::number(r4(at.0)), Node::number(r4(at.1)), Node::number(rot)]),
Node::list("effects", effects),
Node::list("uuid", vec![Node::quoted(&uuid)]),
],
));
Ok(uuid)
}
/// A 45-degree bus entry from `at` to `at + size` (size is (+-2.54, +-2.54) normally).
pub fn add_bus_entry(&mut self, at: Pt, size: Pt) -> R<String> {
if size.0.abs() < EPS || size.1.abs() < EPS {
return Err(OpError::new("bad_arg", "a bus entry needs a non-zero size in both x and y (e.g. [2.54, -2.54])"));
}
let uuid = new_uuid();
self.insert_item(Node::list(
"bus_entry",
vec![
Node::list("at", vec![Node::number(r4(at.0)), Node::number(r4(at.1))]),
Node::list("size", vec![Node::number(r4(size.0)), Node::number(r4(size.1))]),
stroke(),
Node::list("uuid", vec![Node::quoted(&uuid)]),
],
));
Ok(uuid)
}
}
/// Label rotation and justify for a label whose text runs along `dir` away from its point.
pub fn label_rotation_for(dir: Pt) -> f64 {
if dir.0 > 0.5 {
0.0
} else if dir.0 < -0.5 {
180.0
} else if dir.1 < -0.5 {
90.0
} else {
270.0
}
}
// ---------------------------------------------------------------------------
// Schematic: labels
// ---------------------------------------------------------------------------
impl Schematic {
/// Indices of labels by uuid, or by text (optionally only the one at `at`).
pub fn label_indices(&self, uuid: Option<&str>, text: Option<&str>, at: Option<Pt>) -> Vec<usize> {
self.top()
.iter()
.enumerate()
.filter(|(_, n)| n.head().map_or(false, |h| LABEL_HEADS.contains(&h)))
.filter(|(_, n)| uuid.map_or(true, |u| n.child_value("uuid").as_deref() == Some(u)))
.filter(|(_, n)| text.map_or(true, |t| n.arg(0).as_deref() == Some(t)))
.filter(|(_, n)| at.map_or(true, |p| at_of(n).map_or(false, |q| same(p, q))))
.map(|(i, _)| i)
.collect()
}
/// Set a label's angle and the justify KiCad pairs with it (left for 0 and 90, right for
/// 180 and 270; local labels keep `bottom`).
pub fn set_label_rotation(&mut self, idx: usize, rotation: f64) -> R<f64> {
let rot = norm_rotation(rotation).map_err(|e| OpError::new("bad_arg", e))?;
let n = &mut self.top_mut()[idx];
let local = n.head() == Some("label");
let old = n.child("at").and_then(|a| a.num(2)).unwrap_or(0.0);
if let Some(a) = n.child_mut("at") {
if a.children().len() > 3 {
a.set_arg(2, Node::number(rot));
} else if let Some(c) = a.children_mut() {
let mut r = Node::number(rot);
r.lead = " ".into();
c.push(r);
}
}
let side = if rot == 180.0 || rot == 270.0 { "right" } else { "left" };
let mut new_j = vec![Node::atom(side)];
if local {
new_j.push(Node::atom("bottom"));
}
if let Some(e) = n.child_mut("effects") {
let keep: Vec<Node> = e.child("justify").map(|j| j.children().iter().skip(1).filter(|c| !matches!(c.raw(), Some("left") | Some("right") | Some("bottom") | Some("top"))).cloned().collect()).unwrap_or_default();
new_j.extend(keep.into_iter().map(|mut c| {
c.lead.clear();
c
}));
let mut j = Node::list("justify", new_j);
let ch = e.children_mut().expect("list");
if let Some(slot) = ch.iter_mut().find(|c| c.head() == Some("justify")) {
let lead = slot.lead.clone();
pretty(&mut j, 0);
j.lead = lead;
*slot = j;
} else {
// effects sits at depth 2 for a top-level label
pretty(&mut j, 3);
ch.push(j);
if let Kind::List { close_lead, .. } = &mut e.kind {
if close_lead.is_empty() {
*close_lead = "\n\t\t".into();
}
}
}
}
Ok(old)
}
}
// ---------------------------------------------------------------------------
// Schematic: moving and rotating symbols
// ---------------------------------------------------------------------------
/// What a stretch did to the wires.
#[derive(Clone, Debug, Default)]
pub struct Stretch {
pub wires_moved: Vec<String>,
pub wires_added: Vec<String>,
pub junctions: Vec<(String, Pt)>,
pub flags_moved: Vec<String>,
}
impl Schematic {
/// Move every unit of `reference` by (dx, dy). With `stretch`, wire ends on the old pin
/// points follow (an H or V wire gets a dogleg so it stays H or V), and no-connect flags,
/// labels and junctions sitting on those pins move with them.
pub fn move_units(&mut self, reference: &str, d: Pt, stretch: bool) -> R<(Vec<String>, Stretch)> {
let idxs = self.require_units(reference)?;
let old_pins: Vec<Pt> = if stretch { self.pins_of(reference).map(|v| v.into_iter().map(|p| p.at).collect()).unwrap_or_default() } else { vec![] };
let mut uuids = Vec::new();
for &i in &idxs {
let n = &mut self.top_mut()[i];
shift_node(n, d.0, d.1);
uuids.push(n.child_value("uuid").unwrap_or_default());
}
let mut st = Stretch::default();
if stretch && !old_pins.is_empty() {
st = self.stretch_wires(&old_pins, d);
}
Ok((uuids, st))
}
/// Wire ends at any of `old` move by `d`; point items there move too.
pub fn stretch_wires(&mut self, old: &[Pt], d: Pt) -> Stretch {
let on = |p: Pt| old.iter().any(|q| same(*q, p));
let mut st = Stretch::default();
let mut new_segs: Vec<(Pt, Pt)> = Vec::new();
let mut touched_pts: Vec<Pt> = Vec::new();
let idxs: Vec<usize> = self.top().iter().enumerate().filter(|(_, n)| n.head() == Some("wire")).map(|(i, _)| i).collect();
for i in idxs {
let Some(s) = seg_of(&self.top()[i]) else { continue };
let (ma, mb) = (on(s.a), on(s.b));
if !ma && !mb {
continue;
}
st.wires_moved.push(s.uuid.clone());
let n = &mut self.top_mut()[i];
if ma && mb {
set_seg_end(n, 0, (s.a.0 + d.0, s.a.1 + d.1));
set_seg_end(n, 1, (s.b.0 + d.0, s.b.1 + d.1));
continue;
}
let (end, fixed, moving) = if ma { (0, s.b, s.a) } else { (1, s.a, s.b) };
let new = rp((moving.0 + d.0, moving.1 + d.1));
let horizontal = (fixed.1 - moving.1).abs() < EPS;
let vertical = (fixed.0 - moving.0).abs() < EPS;
let corner = if horizontal { rp((new.0, fixed.1)) } else if vertical { rp((fixed.0, new.1)) } else { new };
if same(corner, new) || same(corner, fixed) {
set_seg_end(n, end, new);
} else {
set_seg_end(n, end, corner);
new_segs.push((corner, new));
touched_pts.push(corner);
}
touched_pts.push(new);
}
for (a, b) in new_segs {
if let Ok(u) = self.add_segments("wire", &[a, b]) {
st.wires_added.extend(u);
}
}
// flags, labels and dots that sat on the pins
let heads = ["no_connect", "junction", "label", "global_label", "hierarchical_label"];
let idxs: Vec<usize> = self.top().iter().enumerate().filter(|(_, n)| n.head().map_or(false, |h| heads.contains(&h)) && at_of(n).map_or(false, on)).map(|(i, _)| i).collect();
for i in idxs {
let n = &mut self.top_mut()[i];
shift_node(n, d.0, d.1);
st.flags_moved.push(n.child_value("uuid").unwrap_or_default());
}
st.junctions = self.auto_junctions(&touched_pts);
st
}
/// Rotate every unit of `reference` to the absolute angle `angle` (the first unit's
/// delta is applied to every unit, each about its own anchor; fields turn with it).
pub fn rotate_units(&mut self, reference: &str, angle: f64) -> R<(f64, f64, Vec<String>)> {
let target = norm_rotation(angle).map_err(|e| OpError::new("bad_arg", e))?;
let idxs = self.require_units(reference)?;
let first = SymXf::of(&self.top()[idxs[0]]);
// seen delta: a mirrored unit's stored angle runs the other way
let delta = if first.mirror.is_some() { first.rot - target } else { target - first.rot };
let mut uuids = Vec::new();
for &i in &idxs {
let n = &mut self.top_mut()[i];
let xf = SymXf::of(n);
let new = (((if xf.mirror.is_some() { xf.rot - delta } else { xf.rot + delta }) % 360.0) + 360.0) % 360.0;
if let Some(a) = n.child_mut("at") {
if a.children().len() > 3 {
a.set_arg(2, Node::number(new));
} else if let Some(c) = a.children_mut() {
let mut r = Node::number(new);
r.lead = " ".into();
c.push(r);
}
}
for prop in n.children_mut().expect("list").iter_mut().filter(|c| c.head() == Some("property")) {
if let Some(p) = at_of(prop) {
let v = rotate_sheet_vec((p.0 - xf.x, p.1 - xf.y), delta);
set_at_xy(prop, (xf.x + v.0, xf.y + v.1));
}
}
uuids.push(n.child_value("uuid").unwrap_or_default());
}
Ok((first.rot, target, uuids))
}
/// Move everything inside a box by (dx, dy). Units by their anchor, wires by each end
/// inside (a wire crossing the edge stretches), labels, junctions, no-connects, texts,
/// bus entries and sheets by their point.
pub fn move_region(&mut self, bx: [f64; 4], d: Pt, wires: bool, labels: bool, junctions: bool) -> Value {
let mut moved: BTreeMap<&'static str, Vec<String>> = BTreeMap::new();
let n_items = self.top().len();
for i in 0..n_items {
let n = &self.top()[i];
let head = n.head().unwrap_or("").to_string();
let key: &'static str = match head.as_str() {
"symbol" if is_symbol_instance(n) => "symbols",
"wire" | "bus" if wires => {
let Some(s) = seg_of(n) else { continue };
let (ia, ib) = (in_box(s.a, bx), in_box(s.b, bx));
if !ia && !ib {
continue;
}
let n = &mut self.top_mut()[i];
if ia {
set_seg_end(n, 0, (s.a.0 + d.0, s.a.1 + d.1));
}
if ib {
set_seg_end(n, 1, (s.b.0 + d.0, s.b.1 + d.1));
}
moved.entry(if head == "bus" { "buses" } else { "wires" }).or_default().push(s.uuid);
continue;
}
"label" | "global_label" | "hierarchical_label" if labels => "labels",
"junction" if junctions => "junctions",
"no_connect" if junctions => "noConnects",
"bus_entry" if wires => "busEntries",
"text" if labels => "texts",
"sheet" => "sheets",
_ => continue,
};
let Some(p) = at_of(n) else { continue };
if !in_box(p, bx) {
continue;
}
let n = &mut self.top_mut()[i];
shift_node(n, d.0, d.1);
moved.entry(key).or_default().push(n.child_value("uuid").unwrap_or_default());
}
let mut v = serde_json::Map::new();
for (k, list) in moved {
v.insert(k.to_string(), json!(list));
}
Value::Object(v)
}
/// Put fields back at the library symbol's offsets through the unit's transform (and the
/// library's angle and justify). Returns the fields reset per unit.
pub fn reset_fields(&mut self, reference: &str, names: &[String]) -> R<Vec<String>> {
let idxs = self.require_units(reference)?;
let mut done = Vec::new();
for &i in &idxs {
let key = lib_key(&self.top()[i]);
let lib = self.lib_symbol(&key).ok_or_else(|| OpError::new("lib_symbol_missing", format!("{reference}: library symbol {key} is not in lib_symbols")))?.clone();
let n = &mut self.top_mut()[i];
let xf = SymXf::of(n);
let unit = inst_unit(n);
for prop in n.children_mut().expect("list").iter_mut().filter(|c| c.head() == Some("property")) {
let name = prop.arg(0).unwrap_or_default();
if !names.iter().any(|x| x == &name) {
continue;
}
let Some(lp) = lib.children().iter().find(|c| c.head() == Some("property") && c.arg(0).as_deref() == Some(name.as_str())) else { continue };
let Some(la) = lp.child("at") else { continue };
let off = (la.num(0).unwrap_or(0.0), la.num(1).unwrap_or(0.0));
let p = xf.point(off);
let angle = la.num(2).unwrap_or(0.0);
if let Some(a) = prop.child_mut("at") {
a.set_arg(0, Node::number(p.0));
a.set_arg(1, Node::number(p.1));
if a.children().len() > 3 {
a.set_arg(2, Node::number(angle));
}
}
// the library's justify, keeping the instance's font and hide
let lib_j = lp.child("effects").and_then(|e| e.child("justify")).cloned();
if let Some(e) = prop.child_mut("effects") {
let ch = e.children_mut().expect("list");
let pos = ch.iter().position(|c| c.head() == Some("justify"));
match (pos, lib_j) {
(Some(k), Some(mut j)) => {
j.lead = ch[k].lead.clone();
ch[k] = j;
}
(Some(k), None) => {
ch.remove(k);
}
(None, Some(mut j)) => {
pretty(&mut j, 4);
let at = ch.iter().position(|c| c.head() == Some("hide")).unwrap_or(ch.len());
ch.insert(at, j);
}
(None, None) => {}
}
}
done.push(format!("{unit}:{name}"));
}
}
Ok(done)
}
}
// ---------------------------------------------------------------------------
// Schematic: library-level edits
// ---------------------------------------------------------------------------
/// What happened to one placed unit's pin list.
#[derive(Clone, Debug, Default)]
pub struct PinSync {
pub added: Vec<String>,
pub removed: Vec<String>,
}
fn sync_instance_pins(inst: &mut Node, want: &[String]) -> PinSync {
let ch = inst.children_mut().expect("list");
let have: Vec<String> = ch.iter().filter(|c| c.head() == Some("pin")).filter_map(|c| c.arg(0)).collect();
let mut sync = PinSync::default();
for h in &have {
if !want.contains(h) {
sync.removed.push(h.clone());
}
}
ch.retain(|c| !(c.head() == Some("pin") && c.arg(0).map_or(false, |a| !want.contains(&a))));
let insert_at = ch.iter().rposition(|c| c.head() == Some("pin")).map(|i| i + 1).unwrap_or_else(|| ch.iter().position(|c| c.head() == Some("instances")).unwrap_or(ch.len()));
let mut k = insert_at;
for w in want {
if !have.contains(w) {
let mut p = Node::list("pin", vec![Node::quoted(w), Node::list("uuid", vec![Node::quoted(&new_uuid())])]);
pretty(&mut p, 2);
ch.insert(k, p);
k += 1;
sync.added.push(w.clone());
}
}
sync
}
/// The outcome of a replace.
#[derive(Clone, Debug)]
pub struct Replaced {
pub old_lib_id: String,
pub units: Vec<String>,
pub lib_symbol_added: bool,
pub pins_added: Vec<String>,
pub pins_removed: Vec<String>,
pub old_entry_pruned: bool,
}
impl Schematic {
/// Point every unit of `reference` at `new_lib_id`, keeping unit, fields and position.
/// Refuses (pins_lost) when a pin number the units carry would disappear, unless `force`.
pub fn replace_symbol(&mut self, reference: &str, new_lib_id: &str, block: Option<&Node>, force: bool) -> R<Replaced> {
let idxs = self.require_units(reference)?;
let old_key = lib_key(&self.top()[idxs[0]]);
let old_lib_id = self.top()[idxs[0]].child_value("lib_id").unwrap_or_default();
let mut added = false;
let new_entry = match self.lib_symbol(new_lib_id) {
Some(n) => n.clone(),
None => {
let b = block.ok_or_else(|| OpError::new("symbol_not_found", format!("no lib_symbols entry for {new_lib_id} and no library block supplied")))?;
let mut probe = b.clone();
probe.set_arg(0, Node::quoted(new_lib_id));
probe
}
};
let base = base_name(new_lib_id);
let units = lib_unit_count(&new_entry, &base);
let mut plan: Vec<(usize, Vec<String>)> = Vec::new();
let mut lost: Vec<String> = Vec::new();
let mut gained: Vec<String> = Vec::new();
for &i in &idxs {
let inst = &self.top()[i];
let u = inst_unit(inst);
if u > units {
return Err(OpError::new("unit_missing", format!("{reference} places unit {u} but {new_lib_id} has {units} unit(s)")));
}
let have: Vec<String> = inst.children().iter().filter(|c| c.head() == Some("pin")).filter_map(|c| c.arg(0)).collect();
let want = lib_symbol_pins(&new_entry, &base, u);
for h in &have {
if !want.contains(h) && !lost.contains(h) {
lost.push(h.clone());
}
}
for w in &want {
if !have.contains(w) && !gained.contains(w) {
gained.push(w.clone());
}
}
plan.push((i, want));
}
if !lost.is_empty() && !force {
return Err(OpError::new("pins_lost", format!("{new_lib_id} has no pin {} that {reference} carries now; wires on those pins would dangle (pass force:true to replace anyway)", lost.join(", "))));
}
if self.lib_symbol(new_lib_id).is_none() {
added = self.add_lib_symbol(new_lib_id, block.expect("checked"))?;
}
let mut uuids = Vec::new();
for (i, want) in plan {
let n = &mut self.top_mut()[i];
if let Some(l) = n.child_mut("lib_id") {
l.set_arg(0, Node::quoted(new_lib_id));
}
n.children_mut().expect("list").retain(|c| c.head() != Some("lib_name"));
sync_instance_pins(n, &want);
uuids.push(n.child_value("uuid").unwrap_or_default());
}
let pruned = old_key != new_lib_id && self.prune_lib_symbol(&old_key);
Ok(Replaced { old_lib_id, units: uuids, lib_symbol_added: added, pins_added: gained, pins_removed: lost, old_entry_pruned: pruned })
}
/// Drop a lib_symbols entry no placed unit uses. True when removed.
pub fn prune_lib_symbol(&mut self, key: &str) -> bool {
if self.top().iter().any(|n| is_symbol_instance(n) && lib_key(n) == key) {
return false;
}
let Some(ls) = self.doc.root.child_mut("lib_symbols") else { return false };
let ch = ls.children_mut().expect("list");
let before = ch.len();
ch.retain(|n| !(n.head() == Some("symbol") && n.arg(0).as_deref() == Some(key)));
ch.len() != before
}
/// lib_symbols keys in file order.
pub fn lib_symbol_keys(&self) -> Vec<String> {
self.doc.root.child("lib_symbols").map(|ls| ls.children().iter().filter(|n| n.head() == Some("symbol")).filter_map(|n| n.arg(0)).collect()).unwrap_or_default()
}
/// Replace the embedded definition `key` with `block` (renamed to `key`) when they
/// differ, and resync the pin lists of every unit that uses it. Returns None when the
/// definitions already match; refuses (pins_lost) when placed pins would vanish unless
/// `force`.
pub fn update_lib_symbol(&mut self, key: &str, block: &Node, force: bool) -> R<Option<PinSync>> {
let old = self.lib_symbol(key).ok_or_else(|| OpError::new("not_found", format!("{key} is not in lib_symbols")))?;
let mut entry = block.clone();
entry.set_arg(0, Node::quoted(key));
if canon(old) == canon(&entry) {
return Ok(None);
}
let base = base_name(key);
let users: Vec<usize> = self.top().iter().enumerate().filter(|(_, n)| is_symbol_instance(n) && lib_key(n) == key).map(|(i, _)| i).collect();
let mut lost: Vec<String> = Vec::new();
for &i in &users {
let inst = &self.top()[i];
let want = lib_symbol_pins(&entry, &base, inst_unit(inst));
for h in inst.children().iter().filter(|c| c.head() == Some("pin")).filter_map(|c| c.arg(0)) {
if !want.contains(&h) && !lost.contains(&h) {
lost.push(format!("{}.{h}", symbol_property(inst, "Reference").unwrap_or_default()));
}
}
}
if !lost.is_empty() && !force {
return Err(OpError::new("pins_lost", format!("the library's {key} drops pins placed on this sheet ({}); pass force:true to update anyway", lost.join(", "))));
}
pretty(&mut entry, 2);
let ls = self.doc.root.child_mut("lib_symbols").expect("entry exists");
let ch = ls.children_mut().expect("list");
let k = ch.iter().position(|n| n.head() == Some("symbol") && n.arg(0).as_deref() == Some(key)).expect("entry exists");
ch[k] = entry.clone();
let mut sync = PinSync::default();
for i in users {
let want = lib_symbol_pins(&entry, &base, inst_unit(&self.top()[i]));
let s = sync_instance_pins(&mut self.top_mut()[i], &want);
sync.added.extend(s.added);
sync.removed.extend(s.removed);
}
sync.added.sort();
sync.added.dedup();
sync.removed.sort();
sync.removed.dedup();
Ok(Some(sync))
}
}
// ---------------------------------------------------------------------------
// Annotation
// ---------------------------------------------------------------------------
/// `R12` -> ("R", Some(12)); `R?` -> ("R", None); `#PWR03` -> ("#PWR", Some(3)).
pub fn split_reference(r: &str) -> (String, Option<u32>) {
let t = r.trim();
if let Some(p) = t.strip_suffix('?') {
return (p.to_string(), None);
}
let digits = t.chars().rev().take_while(|c| c.is_ascii_digit()).count();
if digits == 0 {
return (t.to_string(), None);
}
let (p, n) = t.split_at(t.len() - digits);
(p.to_string(), n.parse().ok())
}
/// One planned renumbering.
#[derive(Clone, Debug, serde::Serialize)]
pub struct AnnotateStep {
pub uuid: String,
pub from: String,
pub to: String,
pub unit: u32,
pub at: [f64; 2],
#[serde(rename = "libId")]
pub lib_id: String,
}
impl Schematic {
/// Every reference on this sheet (Reference fields and instance records).
pub fn references_used(&self) -> BTreeSet<String> {
let mut out = BTreeSet::new();
for n in self.top().iter().filter(|n| is_symbol_instance(n)) {
if let Some(r) = symbol_property(n, "Reference") {
out.insert(r);
}
if let Some(inst) = n.child("instances") {
for proj in inst.children().iter().filter(|c| c.head() == Some("project")) {
for path in proj.children().iter().filter(|c| c.head() == Some("path")) {
if let Some(r) = path.child_value("reference") {
out.insert(r);
}
}
}
}
}
out
}
/// Plan numbers for every `X?` reference: the first number per prefix free in `used`
/// (references from every sheet of the project), in position order (x then y, or y then
/// x). The units of an unannotated multi-unit part are packed into one designator per
/// lib id and value, the way eeschema does.
pub fn annotate_plan(&self, used: &BTreeSet<String>, by_y: bool) -> Vec<AnnotateStep> {
let mut taken: BTreeMap<String, BTreeSet<u32>> = BTreeMap::new();
for r in used {
if let (p, Some(n)) = split_reference(r) {
taken.entry(p).or_default().insert(n);
}
}
let mut pending: Vec<(usize, String, Pt, u32, String, String)> = self
.top()
.iter()
.enumerate()
.filter(|(_, n)| is_symbol_instance(n))
.filter_map(|(i, n)| {
let r = symbol_property(n, "Reference")?;
if !r.trim().ends_with('?') {
return None;
}
let xf = SymXf::of(n);
Some((i, split_reference(&r).0, (xf.x, xf.y), inst_unit(n), n.child_value("lib_id").unwrap_or_default(), symbol_property(n, "Value").unwrap_or_default()))
})
.collect();
pending.sort_by(|a, b| {
let (ka, kb) = if by_y { ((a.2 .1, a.2 .0), (b.2 .1, b.2 .0)) } else { (a.2, b.2) };
ka.partial_cmp(&kb).unwrap_or(std::cmp::Ordering::Equal)
});
// open packages of multi-unit parts: (prefix, lib id, value) -> [(number, units used)]
let mut packages: BTreeMap<(String, String, String), Vec<(u32, BTreeSet<u32>, u32)>> = BTreeMap::new();
let mut out = Vec::new();
for (i, prefix, at, unit, lib_id, value) in pending {
let n = &self.top()[i];
let key = lib_key(n);
let units = self.lib_symbol(&key).map(|l| lib_unit_count(l, &base_name(&key))).unwrap_or(1);
let free = |taken: &mut BTreeMap<String, BTreeSet<u32>>| -> u32 {
let set = taken.entry(prefix.clone()).or_default();
let mut k = 1;
while set.contains(&k) {
k += 1;
}
set.insert(k);
k
};
let num = if units > 1 {
let pk = packages.entry((prefix.clone(), lib_id.clone(), value.clone())).or_default();
if let Some(slot) = pk.iter_mut().find(|(_, us, total)| !us.contains(&unit) && (us.len() as u32) < *total) {
slot.1.insert(unit);
slot.0
} else {
let k = free(&mut taken);
let mut s = BTreeSet::new();
s.insert(unit);
packages.get_mut(&(prefix.clone(), lib_id.clone(), value.clone())).expect("inserted").push((k, s, units));
k
}
} else {
free(&mut taken)
};
let to = if prefix.starts_with('#') { format!("{prefix}{num:02}") } else { format!("{prefix}{num}") };
out.push(AnnotateStep { uuid: n.child_value("uuid").unwrap_or_default(), from: symbol_property(n, "Reference").unwrap_or_default(), to, unit, at: [at.0, at.1], lib_id });
}
out
}
/// Set one placed unit's Reference field and every instance record's reference.
pub fn set_unit_reference(&mut self, uuid: &str, reference: &str) -> R<()> {
let i = self.find_uuid_index(uuid).ok_or_else(|| OpError::new("not_found", format!("no item with uuid {uuid}")))?;
let n = &mut self.top_mut()[i];
let ch = n.children_mut().expect("list");
if let Some(p) = ch.iter_mut().find(|c| c.head() == Some("property") && c.arg(0).as_deref() == Some("Reference")) {
p.set_arg(1, Node::quoted(reference));
}
if let Some(inst) = ch.iter_mut().find(|c| c.head() == Some("instances")) {
for proj in inst.children_mut().expect("list").iter_mut().filter(|c| c.head() == Some("project")) {
for path in proj.children_mut().expect("list").iter_mut().filter(|c| c.head() == Some("path")) {
if let Some(r) = path.child_mut("reference") {
r.set_arg(0, Node::quoted(reference));
}
}
}
}
Ok(())
}
}
/// The sheets of the project `sheet` belongs to: the root (`<project>.kicad_sch` beside the
/// `.kicad_pro`) and everything its `(sheet ..)` items reach, plus `sheet` itself.
pub fn project_sheets(sheet: &Path) -> Vec<PathBuf> {
let dir = sheet.parent().map(Path::to_path_buf).unwrap_or_default();
let mut out: Vec<PathBuf> = Vec::new();
let mut queue: Vec<PathBuf> = Vec::new();
if let Some(stem) = project_name_for(sheet) {
let root = dir.join(format!("{stem}.kicad_sch"));
if root.is_file() {
queue.push(root);
}
}
queue.push(sheet.to_path_buf());
while let Some(p) = queue.pop() {
let canon_p = std::fs::canonicalize(&p).unwrap_or_else(|_| p.clone());
if out.iter().any(|q| std::fs::canonicalize(q).unwrap_or_else(|_| q.clone()) == canon_p) {
continue;
}
let Ok(text) = std::fs::read_to_string(&p) else { continue };
out.push(p.clone());
let Ok(s) = Schematic::parse(&text) else { continue };
let base = p.parent().map(Path::to_path_buf).unwrap_or_default();
for n in s.top().iter().filter(|n| n.head() == Some("sheet")) {
if let Some(f) = symbol_property(n, "Sheetfile").or_else(|| symbol_property(n, "Sheet file")) {
queue.push(base.join(f.replace('\\', "/")));
}
}
}
out
}
// ---------------------------------------------------------------------------
// Small builders the verbs share
// ---------------------------------------------------------------------------
/// Two-point L route from `a` to `b`: straight when aligned, else the corner that leaves
/// `a` along its own axis (`a_out` horizontal -> go horizontal first). `first` forces "h"
/// or "v".
pub fn l_route(a: Pt, a_out: Pt, b: Pt, first: Option<&str>) -> Vec<Pt> {
if (a.0 - b.0).abs() < EPS || (a.1 - b.1).abs() < EPS {
return vec![a, b];
}
let h_first = match first {
Some("h") | Some("horizontal") => true,
Some("v") | Some("vertical") => false,
_ => a_out.0.abs() > 0.5,
};
let corner = if h_first { (b.0, a.1) } else { (a.0, b.1) };
vec![a, rp(corner), b]
}
/// A label node text for diagnostics.
pub fn describe_pt(p: Pt) -> String {
format!("({}, {})", fmt_num(p.0), fmt_num(p.1))
}
/// Quote helper re-exported for the verb layer.
pub fn q(s: &str) -> String {
quote(s)
}
/// A hidden property node (re-exported builder).
pub fn hidden_property(name: &str, value: &str, at: Pt) -> Node {
property_node(name, value, at, None, true)
}
/// The default font node (re-exported builder).
pub fn default_font() -> Node {
font_node()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn segment_geometry() {
assert!(on_interior((5.0, 0.0), (0.0, 0.0), (10.0, 0.0)));
assert!(!on_interior((0.0, 0.0), (0.0, 0.0), (10.0, 0.0)));
assert!(on_segment((0.0, 0.0), (0.0, 0.0), (10.0, 0.0)));
assert!(!on_segment((11.0, 0.0), (0.0, 0.0), (10.0, 0.0)));
assert!(!on_segment((5.0, 0.1), (0.0, 0.0), (10.0, 0.0)));
assert!(on_interior((2.54, 2.54), (0.0, 0.0), (5.08, 5.08)));
}
#[test]
fn transform_matches_kicad_order() {
let xf = SymXf { x: 100.0, y: 50.0, rot: 90.0, mirror: None };
// a pin 3.81 above the anchor in the library (y up) rotated 90 ccw lands to the left
assert_eq!(xf.point((0.0, 3.81)), (96.19, 50.0));
let m = SymXf { x: 100.0, y: 50.0, rot: 0.0, mirror: Some('x') };
assert_eq!(m.point((0.0, 3.81)), (100.0, 53.81));
assert_eq!(rotate_sheet_vec((1.0, 0.0), 90.0), (0.0, -1.0));
}
#[test]
fn references_split() {
assert_eq!(split_reference("R12"), ("R".to_string(), Some(12)));
assert_eq!(split_reference("R?"), ("R".to_string(), None));
assert_eq!(split_reference("#PWR03"), ("#PWR".to_string(), Some(3)));
}
#[test]
fn l_routes() {
assert_eq!(l_route((0.0, 0.0), (1.0, 0.0), (10.0, 5.0), None), vec![(0.0, 0.0), (10.0, 0.0), (10.0, 5.0)]);
assert_eq!(l_route((0.0, 0.0), (0.0, 1.0), (10.0, 5.0), None), vec![(0.0, 0.0), (0.0, 5.0), (10.0, 5.0)]);
assert_eq!(l_route((0.0, 0.0), (0.0, 1.0), (10.0, 0.0), None).len(), 2);
}
#[test]
fn snaps_to_grid() {
assert_eq!(snap((1.3, 2.5)), (1.27, 2.54));
}
}