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
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685
//! Sheet operations on a project: add, edit, move, delete and duplicate a hierarchical
//! sheet, and add, edit, delete, import and validate its pins.
//!
//! A `Workspace` loads the project's whole sheet tree (root found through the `.kicad_pro`
//! beside the file, or the file itself when there is none) so a sheet added to a sub-sheet
//! gets one instance entry per use of that sub-sheet, page numbers continue after the
//! highest one in the design, and a duplicated sheet's parts get references nobody else
//! uses. Operations change documents in memory and queue new files; `Workspace::commit`
//! writes them (a `.bak` beside each changed file, new files never overwrite).
use std::collections::BTreeSet;
use std::path::{Path, PathBuf};
use serde_json::{json, Value};
use crate::schematic::{fmt_num, is_symbol_instance, pretty, Node, Schematic};
use crate::sch_hier::{self as hier, Hierarchy, Paper, RefAllocator, Side, TitleEdit};
use crate::sch_model::{self, SheetRef};
pub struct Workspace {
pub h: Hierarchy,
/// The document being edited (index into `h.docs`).
pub target: usize,
pub file: PathBuf,
/// Files to create: (path, text).
pub new_files: Vec<(PathBuf, String)>,
pub notes: Vec<String>,
}
#[derive(Debug)]
pub struct Written {
pub changed: Vec<(PathBuf, PathBuf)>,
pub created: Vec<PathBuf>,
}
fn snap(v: f64) -> f64 {
(v / 1.27).round() * 1.27
}
fn r(v: f64) -> f64 {
(v * 1e4).round() / 1e4 + 0.0
}
impl Workspace {
/// Open `file` inside its project. `root` overrides the project root sheet.
pub fn open(file: &Path, root: Option<&Path>) -> Result<Workspace, String> {
let root_path = root.map(Path::to_path_buf).or_else(|| hier::project_root_for(file)).unwrap_or_else(|| file.to_path_buf());
let mut notes = Vec::new();
let mut h = Hierarchy::load(&root_path)?;
let target = match h.doc_of(file) {
Some(t) if h.paths_of(file).len() > 0 => t,
_ => {
if root.is_some() {
return Err(format!("{} is not a sheet of the hierarchy under {}", file.display(), root_path.display()));
}
notes.push(format!("{} is not reachable from the project root {}; it was edited as its own root.", file.display(), root_path.display()));
h = Hierarchy::load(file)?;
0
}
};
Ok(Workspace { h, target, file: file.to_path_buf(), new_files: Vec::new(), notes })
}
pub fn sch(&self) -> &Schematic {
&self.h.docs[self.target].1
}
pub fn sch_mut(&mut self) -> &mut Schematic {
self.h.changed.insert(self.target);
&mut self.h.docs[self.target].1
}
/// Instance paths of the edited file.
pub fn paths(&self) -> Vec<String> {
let p = self.h.paths_of(&self.file);
if p.is_empty() { vec![format!("/{}", self.sch().root_uuid().unwrap_or_default())] } else { p }
}
/// Every file this commit would touch: (path, is new).
pub fn plan(&self) -> Vec<(PathBuf, bool)> {
let mut v: Vec<(PathBuf, bool)> = self.h.changed.iter().map(|&d| (self.h.docs[d].0.clone(), false)).collect();
v.extend(self.new_files.iter().map(|(p, _)| (p.clone(), true)));
v
}
pub fn commit(&self) -> Result<Written, String> {
for (p, _) in &self.new_files {
if p.exists() {
return Err(format!("{} already exists; nothing was written", p.display()));
}
}
let mut created = Vec::new();
for (p, text) in &self.new_files {
if let Some(d) = p.parent() {
std::fs::create_dir_all(d).map_err(|e| format!("cannot create {}: {e}", d.display()))?;
}
crate::libraries::write_atomic(p, text.as_bytes())?;
created.push(p.clone());
}
let mut changed = Vec::new();
for &d in &self.h.changed {
let (p, s) = &self.h.docs[d];
let bak = s.save_with_backup(p)?;
changed.push((p.clone(), bak));
}
Ok(Written { changed, created })
}
fn sheets(&self) -> Vec<SheetRef> {
self.sch().doc.root.children().iter().filter(|n| n.head() == Some("sheet")).map(sch_model::sheet_ref).collect()
}
fn sheet_index(&self, key: &str) -> Result<usize, String> {
hier::find_sheet(self.sch(), key)
}
fn sheet_at(&self, idx: usize) -> SheetRef {
sch_model::sheet_ref(&self.sch().doc.root.children()[idx])
}
fn alloc(&self) -> RefAllocator {
RefAllocator { used: self.h.references() }
}
/// A free spot right of the sheets already on this page.
fn free_spot(&self, size: (f64, f64)) -> (f64, f64) {
let sheets = self.sheets();
if sheets.is_empty() {
return (25.4, 25.4);
}
let right = sheets.iter().map(|s| s.at.0 + s.size.0).fold(0.0, f64::max);
let top = sheets.iter().map(|s| s.at.1).fold(f64::MAX, f64::min);
let paper = Paper::of(self.sch()).size_mm();
let x = snap(right + 12.7);
if x + size.0 < paper.0 - 10.0 {
(r(x), r(snap(top)))
} else {
let bottom = sheets.iter().map(|s| s.at.1 + s.size.1).fold(0.0, f64::max);
(25.4, r(snap(bottom + 12.7)))
}
}
// -----------------------------------------------------------------------
// sheets
// -----------------------------------------------------------------------
pub fn add_sheet(&mut self, name: &str, file: &str, at: Option<(f64, f64)>, size: Option<(f64, f64)>, import_pins: bool) -> Result<Value, String> {
let name = name.trim();
if name.is_empty() {
return Err("a sheet needs a name".into());
}
if self.sheets().iter().any(|s| s.name.eq_ignore_ascii_case(name)) {
return Err(format!("this sheet already has a sheet named {name}; sheet names must be unique on a page"));
}
let file = file.trim().replace('\\', "/");
if !file.to_ascii_lowercase().ends_with(".kicad_sch") {
return Err(format!("file {file} must end in .kicad_sch"));
}
let child = hier::resolve_child(&self.file, &file);
let ancestors: Vec<PathBuf> = self.ancestor_files();
if ancestors.iter().any(|a| same_file(a, &child)) {
return Err(format!("{file} is this sheet or one of its parents: a sheet cannot contain itself"));
}
let size = size.unwrap_or((25.4, 20.32));
let at = at.map(|(x, y)| (r(snap(x)), r(snap(y)))).unwrap_or_else(|| self.free_spot(size));
let project = self.h.project_name();
let parent_paths = self.paths();
let mut page = self.h.max_page();
let pages: Vec<(String, String)> = parent_paths
.iter()
.map(|p| {
page += 1;
(p.clone(), page.to_string())
})
.collect();
let (mut node, uuid) = hier::sheet_node(name, &file, at, size, &project, &pages);
let mut created = false;
let mut labels = Vec::new();
let mut renames = Vec::new();
if child.is_file() {
let mut cs = Schematic::load(&child)?;
labels = hier::hier_labels(&cs);
// symbols in a linked file need an instance entry for each new use
let mut alloc = self.alloc();
let mut changed = false;
for (p, _) in &pages {
let new_path = format!("{p}/{uuid}");
let rn = ensure_symbol_instances(&mut cs, &new_path, None, &project, &mut alloc);
changed |= !rn.is_empty() || cs.doc.root.children().iter().any(|n| is_symbol_instance(n));
renames.extend(rn);
}
if changed {
let d = match self.h.doc_of(&child) {
Some(d) => {
self.h.docs[d].1 = cs;
d
}
None => {
self.h.docs.push((child.clone(), cs));
self.h.docs.len() - 1
}
};
self.h.changed.insert(d);
}
} else {
let paper = Paper::of(self.sch());
let (text, _) = hier::blank_sheet(&paper, &TitleEdit { title: Some(name.to_string()), ..Default::default() });
self.new_files.push((child.clone(), text));
created = true;
}
let mut pins = Vec::new();
if import_pins && !labels.is_empty() {
let mut sref = sch_model::sheet_ref(&node);
for (lname, shape, _) in &labels {
let side = hier::default_side(shape);
let spot = hier::pin_spot(&sref, side, None)?;
let (pin, puuid) = hier::sheet_pin_node(lname, shape, spot, side);
hier::push_sheet_pin(&mut node, pin);
sref = sch_model::sheet_ref(&node);
pins.push(json!({"name": lname, "shape": shape, "side": side.name(), "at": [spot.0, spot.1], "uuid": puuid}));
}
}
hier::insert_top(self.sch_mut(), node);
Ok(json!({
"uuid": uuid,
"name": name,
"file": file,
"childPath": child.to_string_lossy().replace('\\', "/"),
"childCreated": created,
"at": [at.0, at.1],
"size": [size.0, size.1],
"pages": pages.iter().map(|(p, pg)| json!({"path": format!("{p}/{uuid}"), "page": pg})).collect::<Vec<_>>(),
"pinsAdded": pins,
"hierarchicalLabelsInChild": labels.len(),
"renamed": renames.iter().map(|(a, b)| json!({"from": a, "to": b})).collect::<Vec<_>>(),
}))
}
fn ancestor_files(&self) -> Vec<PathBuf> {
let mut out = vec![self.file.clone()];
for (i, inst) in self.h.instances.iter().enumerate() {
if inst.doc.is_some() && same_file(&inst.file, &self.file) {
let mut p = inst.parent;
while let Some(k) = p {
out.push(self.h.instances[k].file.clone());
p = self.h.instances[k].parent;
}
let _ = i;
}
}
out
}
pub fn edit_sheet(&mut self, key: &str, new_name: Option<&str>, file: Option<&str>, size: Option<(f64, f64)>, at: Option<(f64, f64)>) -> Result<Value, String> {
let idx = self.sheet_index(key)?;
let before = self.sheet_at(idx);
let mut changes = Vec::new();
if let Some(n) = new_name.map(str::trim).filter(|n| !n.is_empty()) {
if n != before.name && self.sheets().iter().any(|s| s.uuid != before.uuid && s.name.eq_ignore_ascii_case(n)) {
return Err(format!("another sheet on this page is already named {n}"));
}
let node = &mut self.sch_mut().doc.root.children_mut().expect("list")[idx];
hier::set_sheet_field(node, "Sheetname", n);
changes.push(format!("name {} -> {n}", before.name));
}
let mut child_created = false;
if let Some(f) = file.map(|f| f.trim().replace('\\', "/")).filter(|f| !f.is_empty()) {
if !f.to_ascii_lowercase().ends_with(".kicad_sch") {
return Err(format!("file {f} must end in .kicad_sch"));
}
let child = hier::resolve_child(&self.file, &f);
if self.ancestor_files().iter().any(|a| same_file(a, &child)) {
return Err(format!("{f} is this sheet or one of its parents: a sheet cannot contain itself"));
}
if !child.is_file() {
let paper = Paper::of(self.sch());
let (text, _) = hier::blank_sheet(&paper, &TitleEdit { title: Some(before.name.clone()), ..Default::default() });
self.new_files.push((child.clone(), text));
child_created = true;
}
let node = &mut self.sch_mut().doc.root.children_mut().expect("list")[idx];
hier::set_sheet_field(node, "Sheetfile", &f);
changes.push(format!("file {} -> {f}{}", before.file, if child_created { " (created blank)" } else { "" }));
}
if let Some((w, h)) = size {
let node = &mut self.sch_mut().doc.root.children_mut().expect("list")[idx];
hier::resize_sheet_node(node, r(w), r(h))?;
changes.push(format!("size {}x{} -> {}x{}", fmt_num(before.size.0), fmt_num(before.size.1), fmt_num(r(w)), fmt_num(r(h))));
}
if let Some((x, y)) = at {
let cur = self.sheet_at(idx).at;
let node = &mut self.sch_mut().doc.root.children_mut().expect("list")[idx];
hier::move_sheet_node(node, x - cur.0, y - cur.1);
changes.push(format!("at ({}, {}) -> ({}, {})", fmt_num(cur.0), fmt_num(cur.1), fmt_num(x), fmt_num(y)));
}
if changes.is_empty() {
return Err("nothing to change: pass newName, file, size or at".into());
}
let after = self.sheet_at(idx);
Ok(json!({"uuid": after.uuid, "name": after.name, "file": after.file, "at": [after.at.0, after.at.1], "size": [after.size.0, after.size.1], "changes": changes, "childCreated": child_created}))
}
pub fn move_sheet(&mut self, key: &str, to: Option<(f64, f64)>, by: Option<(f64, f64)>) -> Result<Value, String> {
let idx = self.sheet_index(key)?;
let s = self.sheet_at(idx);
let (dx, dy) = match (to, by) {
(Some(t), _) => (t.0 - s.at.0, t.1 - s.at.1),
(None, Some(b)) => b,
_ => return Err("pass to:[x, y] (new top-left corner) or by:[dx, dy]".into()),
};
let node = &mut self.sch_mut().doc.root.children_mut().expect("list")[idx];
hier::move_sheet_node(node, dx, dy);
let after = self.sheet_at(idx);
Ok(json!({"uuid": s.uuid, "name": s.name, "from": [s.at.0, s.at.1], "to": [after.at.0, after.at.1], "pinsMoved": s.pins.len(), "note": "Wires attached to the sheet pins do not follow; redraw them or run kicad_sch_check to find the ends left behind."}))
}
pub fn delete_sheet(&mut self, key: &str) -> Result<Value, String> {
let idx = self.sheet_index(key)?;
let s = self.sheet_at(idx);
self.sch_mut().doc.root.children_mut().expect("list").remove(idx);
let child = hier::resolve_child(&self.file, &s.file);
let still_used = self.h.instances.iter().filter(|i| i.sheet_uuid.as_deref() != Some(s.uuid.as_str()) && i.doc.is_some() && same_file(&i.file, &child)).count();
Ok(json!({
"uuid": s.uuid,
"name": s.name,
"file": s.file,
"childFileKept": child.to_string_lossy().replace('\\', "/"),
"childStillUsedElsewhere": still_used,
"pinsRemoved": s.pins.len(),
}))
}
pub fn duplicate_sheet(&mut self, key: &str, new_name: Option<&str>, file: Option<&str>, at: Option<(f64, f64)>) -> Result<Value, String> {
let idx = self.sheet_index(key)?;
let src = self.sheet_at(idx);
let src_child = hier::resolve_child(&self.file, &src.file);
if !src_child.is_file() {
return Err(format!("the sheet's file {} does not exist; nothing to duplicate", src.file));
}
let name = match new_name.map(str::trim).filter(|n| !n.is_empty()) {
Some(n) => n.to_string(),
None => {
let mut k = 2;
loop {
let cand = format!("{}{k}", src.name);
if !self.sheets().iter().any(|s| s.name.eq_ignore_ascii_case(&cand)) {
break cand;
}
k += 1;
}
}
};
if self.sheets().iter().any(|s| s.name.eq_ignore_ascii_case(&name)) {
return Err(format!("this page already has a sheet named {name}"));
}
let dir = src_child.parent().unwrap_or(Path::new(".")).to_path_buf();
let stem = src_child.file_stem().map(|s| s.to_string_lossy().to_string()).unwrap_or_else(|| "sheet".into());
let file_rel = match file.map(|f| f.trim().replace('\\', "/")).filter(|f| !f.is_empty()) {
Some(f) => f,
None => {
let fname = hier::free_copy_name(&dir, &stem);
let parent_dir = self.file.parent().unwrap_or(Path::new("."));
match dir.strip_prefix(parent_dir) {
Ok(rel) if rel.as_os_str().is_empty() => fname,
Ok(rel) => format!("{}/{fname}", rel.to_string_lossy().replace('\\', "/")),
Err(_) => dir.join(&fname).to_string_lossy().replace('\\', "/"),
}
}
};
let new_child = hier::resolve_child(&self.file, &file_rel);
if new_child.exists() || self.new_files.iter().any(|(p, _)| same_file(p, &new_child)) {
return Err(format!("{file_rel} already exists; pass another file name"));
}
let size = src.size;
let at = at.unwrap_or((src.at.0 + src.size.0 + 12.7, src.at.1));
let at = (r(snap(at.0)), r(snap(at.1)));
let project = self.h.project_name();
let parent_paths = self.paths();
let mut page = self.h.max_page();
let pages: Vec<(String, String)> = parent_paths
.iter()
.map(|p| {
page += 1;
(p.clone(), page.to_string())
})
.collect();
let (mut node, uuid) = hier::sheet_node(&name, &file_rel, at, size, &project, &pages);
// same pins as the source, same shapes, same edge offsets
for p in &src.pins {
let side = Side::of(&src, p.at);
let spot = (r(p.at.0 - src.at.0 + at.0), r(p.at.1 - src.at.1 + at.1));
let (pin, _) = hier::sheet_pin_node(&p.name, &p.shape, spot, side);
hier::push_sheet_pin(&mut node, pin);
}
// the copy: fresh uuids, its own instance entries, references nobody else uses
let mut copy = Schematic::load(&src_child)?;
let old_path = format!("{}/{}", parent_paths[0], src.uuid);
hier::refresh_uuids(&mut copy);
let mut alloc = self.alloc();
let mut renames = Vec::new();
for (i, (p, _)) in pages.iter().enumerate() {
let new_path = format!("{p}/{uuid}");
if i == 0 {
strip_symbol_instances(&mut copy);
}
renames.extend(ensure_symbol_instances(&mut copy, &new_path, Some(&old_path), &project, &mut alloc));
}
// sheets nested in the copy point at the same child files: those files gain instances
let nested: Vec<SheetRef> = copy.doc.root.children().iter().filter(|n| n.head() == Some("sheet")).map(sch_model::sheet_ref).collect();
let mut nested_note = Vec::new();
if !nested.is_empty() {
for (p, _) in &pages {
let copy_path = format!("{p}/{uuid}");
hier::retarget_sheet_instances(&mut copy, ©_path, &project, &mut page);
}
for ns in &nested {
let gfile = hier::resolve_child(&new_child, &ns.file);
nested_note.push(format!("{} ({}) is shared with the original, not copied", ns.name, ns.file));
if let Some(d) = self.h.doc_of(&gfile) {
for (p, _) in &pages {
let gpath = format!("{p}/{uuid}/{}", ns.uuid);
let rn = ensure_symbol_instances(&mut self.h.docs[d].1, &gpath, None, &project, &mut alloc);
renames.extend(rn);
}
self.h.changed.insert(d);
}
}
}
self.new_files.push((new_child.clone(), copy.to_text()));
hier::insert_top(self.sch_mut(), node);
renames.sort();
renames.dedup();
Ok(json!({
"uuid": uuid,
"name": name,
"file": file_rel,
"childPath": new_child.to_string_lossy().replace('\\', "/"),
"from": {"uuid": src.uuid, "name": src.name, "file": src.file},
"at": [at.0, at.1],
"size": [size.0, size.1],
"pages": pages.iter().map(|(p, pg)| json!({"path": format!("{p}/{uuid}"), "page": pg})).collect::<Vec<_>>(),
"pinsCopied": src.pins.len(),
"renamed": renames.iter().map(|(a, b)| json!({"from": a, "to": b})).collect::<Vec<_>>(),
"sharedNestedSheets": nested_note,
}))
}
// -----------------------------------------------------------------------
// sheet pins
// -----------------------------------------------------------------------
fn child_labels(&self, s: &SheetRef) -> Result<Vec<(String, String, String)>, String> {
let child = hier::resolve_child(&self.file, &s.file);
if let Some(d) = self.h.doc_of(&child) {
return Ok(hier::hier_labels(&self.h.docs[d].1));
}
if let Some((_, text)) = self.new_files.iter().find(|(p, _)| same_file(p, &child)) {
return Ok(hier::hier_labels(&Schematic::parse(text)?));
}
if !child.is_file() {
return Err(format!("the sheet's file {} does not exist", s.file));
}
Ok(hier::hier_labels(&Schematic::load(&child)?))
}
pub fn add_pin(&mut self, key: &str, name: &str, shape: &str, side: Option<Side>, at: Option<(f64, f64)>) -> Result<Value, String> {
let idx = self.sheet_index(key)?;
let s = self.sheet_at(idx);
let name = name.trim();
if name.is_empty() {
return Err("a sheet pin needs a name (the hierarchical label it meets inside)".into());
}
let shape = shape.trim().to_ascii_lowercase();
if !hier::PIN_SHAPES.contains(&shape.as_str()) {
return Err(format!("shape {shape} is not one of {}", hier::PIN_SHAPES.join(", ")));
}
if s.pins.iter().any(|p| p.name == name) {
return Err(format!("sheet {} already has a pin named {name}", s.name));
}
let side = side.or_else(|| at.map(|p| Side::of(&s, p))).unwrap_or_else(|| hier::default_side(&shape));
let spot = hier::pin_spot(&s, side, at)?;
let (pin, uuid) = hier::sheet_pin_node(name, &shape, spot, side);
let node = &mut self.sch_mut().doc.root.children_mut().expect("list")[idx];
hier::push_sheet_pin(node, pin);
let has_label = self.child_labels(&s).map(|l| l.iter().any(|(n, _, _)| n == name)).unwrap_or(false);
Ok(json!({"sheet": s.name, "name": name, "shape": shape, "side": side.name(), "at": [spot.0, spot.1], "uuid": uuid, "childHasLabel": has_label}))
}
fn pin_index(&self, sheet_idx: usize, pin: &str) -> Result<usize, String> {
let node = &self.sch().doc.root.children()[sheet_idx];
let hits: Vec<usize> = node.children().iter().enumerate().filter(|(_, c)| c.head() == Some("pin") && (c.arg(0).as_deref() == Some(pin) || c.child_value("uuid").as_deref() == Some(pin))).map(|(i, _)| i).collect();
match hits.len() {
1 => Ok(hits[0]),
0 => Err(format!("sheet has no pin {pin} (pins: {})", node.children().iter().filter(|c| c.head() == Some("pin")).filter_map(|c| c.arg(0)).collect::<Vec<_>>().join(", "))),
_ => Err(format!("{} pins are named {pin}; pass the pin's uuid", hits.len())),
}
}
pub fn edit_pin(&mut self, key: &str, pin: &str, new_name: Option<&str>, shape: Option<&str>, side: Option<Side>, at: Option<(f64, f64)>) -> Result<Value, String> {
let idx = self.sheet_index(key)?;
let s = self.sheet_at(idx);
let pi = self.pin_index(idx, pin)?;
let mut changes = Vec::new();
let old = s.pins.iter().find(|p| p.name == pin || p.uuid == pin).cloned().ok_or("pin vanished")?;
if let Some(n) = new_name.map(str::trim).filter(|n| !n.is_empty() && *n != old.name) {
if s.pins.iter().any(|p| p.name == n) {
return Err(format!("sheet {} already has a pin named {n}", s.name));
}
let node = &mut self.sch_mut().doc.root.children_mut().expect("list")[idx].children_mut().expect("list")[pi];
node.set_arg(0, Node::quoted(n));
changes.push(format!("name {} -> {n}", old.name));
}
if let Some(sh) = shape.map(|x| x.trim().to_ascii_lowercase()) {
if !hier::PIN_SHAPES.contains(&sh.as_str()) {
return Err(format!("shape {sh} is not one of {}", hier::PIN_SHAPES.join(", ")));
}
let node = &mut self.sch_mut().doc.root.children_mut().expect("list")[idx].children_mut().expect("list")[pi];
node.set_arg(1, Node::atom(sh.clone()));
changes.push(format!("shape {} -> {sh}", old.shape));
}
if side.is_some() || at.is_some() {
let mut others = s.clone();
others.pins.retain(|p| p.uuid != old.uuid);
let side = side.or_else(|| at.map(|p| Side::of(&s, p))).unwrap_or_else(|| Side::of(&s, old.at));
let spot = hier::pin_spot(&others, side, at)?;
let (angle, _) = match side {
Side::Left => (180.0, ""),
Side::Right => (0.0, ""),
Side::Top => (90.0, ""),
Side::Bottom => (270.0, ""),
};
let justify = match side {
Side::Left | Side::Bottom => "left",
_ => "right",
};
let node = &mut self.sch_mut().doc.root.children_mut().expect("list")[idx].children_mut().expect("list")[pi];
if let Some(a) = node.child_mut("at") {
a.set_arg(0, Node::number(spot.0));
a.set_arg(1, Node::number(spot.1));
a.set_arg(2, Node::number(angle));
}
if let Some(j) = node.child_mut("effects").and_then(|e| e.child_mut("justify")) {
j.set_arg(0, Node::atom(justify));
}
changes.push(format!("moved to the {} edge at ({}, {})", side.name(), fmt_num(spot.0), fmt_num(spot.1)));
}
if changes.is_empty() {
return Err("nothing to change: pass newName, shape, side or at".into());
}
Ok(json!({"sheet": s.name, "pin": old.name, "uuid": old.uuid, "changes": changes}))
}
pub fn delete_pin(&mut self, key: &str, pin: &str) -> Result<Value, String> {
let idx = self.sheet_index(key)?;
let s = self.sheet_at(idx);
let pi = self.pin_index(idx, pin)?;
let removed = self.sch_mut().doc.root.children_mut().expect("list")[idx].children_mut().expect("list").remove(pi);
Ok(json!({"sheet": s.name, "name": removed.arg(0), "uuid": removed.child_value("uuid"), "note": "A wire that ended on the pin now dangles; kicad_sch_check lists it."}))
}
/// Pins for every hierarchical label in the child that the sheet lacks.
pub fn import_pins(&mut self, key: &str) -> Result<Value, String> {
let idx = self.sheet_index(key)?;
let mut s = self.sheet_at(idx);
let labels = self.child_labels(&s)?;
let mut added = Vec::new();
let missing: Vec<(String, String)> = labels.iter().filter(|(n, _, _)| !s.pins.iter().any(|p| p.name == *n)).map(|(n, sh, _)| (n.clone(), sh.clone())).collect();
for (name, shape) in &missing {
let side = hier::default_side(shape);
let spot = hier::pin_spot(&s, side, None)?;
let (pin, uuid) = hier::sheet_pin_node(name, shape, spot, side);
let node = &mut self.sch_mut().doc.root.children_mut().expect("list")[idx];
hier::push_sheet_pin(node, pin);
s = self.sheet_at(idx);
added.push(json!({"name": name, "shape": shape, "side": side.name(), "at": [spot.0, spot.1], "uuid": uuid}));
}
Ok(json!({"sheet": s.name, "labelsInChild": labels.len(), "added": added, "alreadyPresent": labels.len() - added.len()}))
}
pub fn validate_pins(&self, key: Option<&str>) -> Result<Value, String> {
let sheets: Vec<SheetRef> = match key {
Some(k) => vec![self.sheet_at(self.sheet_index(k)?)],
None => self.sheets(),
};
let mut out = Vec::new();
let mut problems = 0;
for s in &sheets {
match self.child_labels(s) {
Ok(labels) => {
let (no_pin, no_label, mismatch) = hier::pin_label_diff(s, &labels);
problems += no_pin.len() + no_label.len() + mismatch.len();
out.push(json!({"sheet": s.name, "uuid": s.uuid, "file": s.file, "ok": no_pin.is_empty() && no_label.is_empty() && mismatch.is_empty(), "labelsWithoutPin": no_pin, "pinsWithoutLabel": no_label, "shapeMismatch": mismatch}));
}
Err(e) => {
problems += 1;
out.push(json!({"sheet": s.name, "uuid": s.uuid, "file": s.file, "ok": false, "error": e}));
}
}
}
Ok(json!({"ok": problems == 0, "problems": problems, "sheets": out}))
}
pub fn list_pins(&self, key: &str) -> Result<Value, String> {
let s = self.sheet_at(self.sheet_index(key)?);
Ok(json!({"sheet": s.name, "uuid": s.uuid, "file": s.file, "at": [s.at.0, s.at.1], "size": [s.size.0, s.size.1], "pins": s.pins.iter().map(|p| json!({"name": p.name, "shape": p.shape, "side": Side::of(&s, p.at).name(), "at": [p.at.0, p.at.1], "uuid": p.uuid})).collect::<Vec<_>>()}))
}
}
fn same_file(a: &Path, b: &Path) -> bool {
let ca = std::fs::canonicalize(a).unwrap_or_else(|_| a.to_path_buf());
let cb = std::fs::canonicalize(b).unwrap_or_else(|_| b.to_path_buf());
ca == cb
}
/// Remove every symbol's instances block (a copied file starts clean).
pub fn strip_symbol_instances(sch: &mut Schematic) {
for n in sch.doc.root.children_mut().expect("list").iter_mut().filter(|n| is_symbol_instance(n)) {
let c = n.children_mut().expect("list");
if let Some(i) = c.iter().position(|x| x.head() == Some("instances")) {
c.remove(i);
}
}
}
/// Give every symbol an instance entry for `path` when it lacks one. The reference is the one
/// it has under `from` (else its Reference field); a reference already used in the design is
/// replaced by the next free one (units of one part stay together). Returns the renames.
pub fn ensure_symbol_instances(sch: &mut Schematic, path: &str, from: Option<&str>, project: &str, alloc: &mut RefAllocator) -> Vec<(String, String)> {
let mut renames = Vec::new();
let mut chosen: std::collections::HashMap<String, String> = Default::default();
let mut claimed: BTreeSet<String> = BTreeSet::new();
for n in sch.doc.root.children_mut().expect("list").iter_mut().filter(|n| is_symbol_instance(n)) {
let has = n.child("instances").map_or(false, |i| i.children().iter().any(|p| p.head() == Some("project") && p.children().iter().any(|q| q.head() == Some("path") && q.arg(0).as_deref() == Some(path))));
if has {
continue;
}
let base = sch_model::instance_reference(n, from);
let reference = if base.starts_with('#') || base.ends_with('?') {
base.clone()
} else if let Some(r) = chosen.get(&base) {
r.clone()
} else {
let r = if alloc.used.contains(&base) && !claimed.contains(&base) { alloc.next(&base) } else {
alloc.used.insert(base.clone());
base.clone()
};
claimed.insert(r.clone());
chosen.insert(base.clone(), r.clone());
r
};
if reference != base {
renames.push((base.clone(), reference.clone()));
}
let unit = n.child("unit").and_then(|u| u.arg(0)).unwrap_or_else(|| "1".into());
let entry = Node::list("path", vec![Node::quoted(path), Node::list("reference", vec![Node::quoted(&reference)]), Node::list("unit", vec![Node::atom(unit)])]);
let children = n.children_mut().expect("list");
match children.iter_mut().find(|c| c.head() == Some("instances")) {
Some(insts) => {
let ic = insts.children_mut().expect("list");
if let Some(proj) = ic.iter_mut().find(|p| p.head() == Some("project")) {
let mut e = entry;
pretty(&mut e, 4);
proj.children_mut().expect("list").push(e);
} else {
let mut proj = Node::list("project", vec![Node::quoted(project), entry]);
pretty(&mut proj, 3);
ic.push(proj);
}
}
None => {
let mut inst = Node::list("instances", vec![Node::list("project", vec![Node::quoted(project), entry])]);
pretty(&mut inst, 2);
children.push(inst);
}
}
// a symbol used once shows its instance reference in the Reference field too
if let Some(p) = children.iter_mut().find(|c| c.head() == Some("property") && c.arg(0).as_deref() == Some("Reference")) {
if p.arg(1).as_deref() != Some(reference.as_str()) && from.is_some() {
p.set_arg(1, Node::quoted(&reference));
}
}
}
renames.sort();
renames.dedup();
renames
}