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.54: 1.0.54. Wiki #119: back-side footprints written by other tools are read the way KiCad reads them. A footprint o…
4f34c0c
1mo ago
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124
use super::*;
use crate::silk_graphics as graphics;
pub fn silk_graphics_batch(ctx: &Ctx, args: &Value) -> RResult<Value> {
if args["expectedRevision"]
.as_str()
.filter(|s| !s.is_empty())
.is_none()
{
return Err(RoutingError::new(
"revision_required",
"Read the board revision before editing silk graphics",
));
}
let s = connect(ctx, args)?;
let (text, _, revision) = s.snapshot()?;
check_revision(args, &revision)?;
let plan = graphics::prepare(&text, args)?;
let before = drc_snapshot(ctx.kicad_cli.as_deref(), &s.source_path(), &text)?;
let after = drc_snapshot(ctx.kicad_cli.as_deref(), &s.source_path(), &plan.candidate)?;
let all = |mut v: Value| {
if let Some(rows) = v["violations"].as_array_mut() {
for row in rows {
row["severity"] = json!("error");
}
}
v
};
let added = new_errors(&all(before), &all(after.clone()))?;
if !added.is_empty() {
return Err(RoutingError::new(
"drc_rejected",
"Silk graphics add native DRC findings; no live edit",
)
.with("_hint", json!(crate::ipc::silk_drc_hint(&added)))
.with("violations", json!(added))
.with("mutated", json!(false)));
}
if args["dryRun"] == true {
return Ok(json!({"success":true,"mutated":false,"drc":after,"revision":revision}));
}
let encode = |g: &graphics::Graphic| prost_types::Any {
type_url: graphics::TYPE_URL.into(),
value: g.bytes(BoardLayerInfo::id_from_name(&g.layer).unwrap()),
};
let created: Vec<_> = plan.create.iter().map(|g| g.id.clone()).collect();
let updated: Vec<_> = plan.update.iter().map(|g| g.id.clone()).collect();
let verify = |rows: Vec<prost_types::Any>, wanted: &[String]| -> Result<(), IpcFailure> {
let actual: HashSet<_> = rows.iter().filter_map(|a| graphics::id(&a.value)).collect();
if actual != wanted.iter().cloned().collect() {
return Err(IpcFailure::from(
"Native graphics reply did not preserve requested UUIDs".to_string(),
));
}
Ok(())
};
check_revision(args, &s.revision()?)?;
transaction(&s, "Adom silkscreen graphics", || {
if !plan.remove.is_empty() {
s.client.delete_items(plan.remove.clone())?;
}
if !plan.update.is_empty() {
verify(
s.client
.update_items(plan.update.iter().map(&encode).collect())?,
&updated,
)?;
}
if !plan.create.is_empty() {
verify(
s.client
.create_items(plan.create.iter().map(&encode).collect(), None)?,
&created,
)?;
}
Ok(())
})?;
let mut out = json!({"success":true,"committed":true,"mutated":true,"undoSteps":1,"createdIds":created,"updatedIds":updated,"removedIds":plan.remove,"drc":after,"saved":false,"source":"live-editor","_hint":"Inspect every postCommit error before retrying. Refresh the linked 3D viewer after edits. Coordinates are board-frame on either side, not mirrored screen coordinates."});
match s
.client
.get_items_raw_by_type_codes(vec![PcbObjectTypeCode::new_shape().code])
{
Ok(rows) => {
let live: HashSet<_> = rows.iter().filter_map(|a| graphics::id(&a.value)).collect();
let remaining: Vec<_> = plan.remove.iter().filter(|id| live.contains(*id)).collect();
let missing: Vec<_> = created
.iter()
.chain(&updated)
.filter(|id| !live.contains(*id))
.collect();
out["deletionVerified"] = json!(remaining.is_empty());
if !remaining.is_empty() || !missing.is_empty() {
out["success"] = json!(false);
out["postCommitReadbackError"] =
json!({"remainingRemovedIds":remaining,"missingLiveIds":missing});
}
}
Err(e) => {
out["success"] = json!(false);
out["postCommitReadbackError"] = json!(e.to_string());
}
}
match s.revision() {
Ok(rev) => out["revision"] = json!(rev),
Err(e) => {
out["success"] = json!(false);
out["postCommitRevisionError"] = json!(e.message);
}
}
let ids = created.iter().chain(&updated).cloned().collect::<Vec<_>>();
if !ids.is_empty() {
match s.client.get_item_bounding_boxes(ids,false){Ok(rows)=>out["bounds"]=json!(rows.iter().map(|b|json!({"id":b.item_id,"boxMm":[b.x_nm as f64/1e6,b.y_nm as f64/1e6,(b.x_nm+b.width_nm) as f64/1e6,(b.y_nm+b.height_nm) as f64/1e6]})).collect::<Vec<_>>()),Err(e)=>{out["success"]=json!(false);out["postCommitBoundsError"]=json!(e.to_string());}}
}
if args["save"] == true {
match s.client.save_document() {
Ok(()) => out["saved"] = json!(true),
Err(e) => {
out["success"] = json!(false);
out["postCommitSaveError"] = json!(e.to_string());
}
}
}
Ok(out)
}