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.
← Commit history
1.0.29: NATIVE (Rust) build. 1.0.29: merges Astra PR #19: KiCad's PCB_FIELD_T refusal is classified as native_field_update_unsupported (kicadVersion, retryable:false) after a confirmed rollback; doc records the 10.0.5 limitation (wiki #104).
3 files changed
+259−4
rust/Cargo.lock+3−3@@ -244,7 +244,7 @@ dependencies = [ [[package]] name = "kicad-bridge"-version = "1.0.28"+version = "1.0.29" dependencies = [ "kicad-core", "kicad-platform",@@ -255,7 +255,7 @@ dependencies = [ [[package]] name = "kicad-core"-version = "1.0.28"+version = "1.0.29" dependencies = [ "kicad-ipc-rs", "kicad-platform",@@ -281,7 +281,7 @@ dependencies = [ [[package]] name = "kicad-platform"-version = "1.0.28"+version = "1.0.29" dependencies = [ "serde", "serde_json",
rust/Cargo.toml+1−1@@ -3,7 +3,7 @@ resolver = "2" members = ["crates/*"] [workspace.package]-version = "1.0.28"+version = "1.0.29" edition = "2021" license = "MIT" publish = false
rust/crates/kicad-core/src/ipc/silk_fields.rs.origadded+255@@ -0,0 +1,255 @@+use super::*;+use crate::silk_fields as fields;+use kicad_ipc_rs::FieldItem;++fn id(item: &EditablePcbItem) -> Option<&str> {+ match item {+ EditablePcbItem::Field(f) => f+ .proto()+ .text+ .as_ref()?+ .id+ .as_ref()+ .map(|x| x.value.as_str()),+ _ => None,+ }+}+fn apply_geometry(mut field: FieldItem, e: &fields::Edit) -> RResult<EditablePcbItem> {+ let f = field.proto_mut();+ if f.name != e.name {+ return Err(RoutingError::new(+ "field_identity_mismatch",+ "Native field name does not match saved reference/value property",+ ));+ }+ f.visible = e.visible;+ let b = f+ .text+ .as_mut()+ .ok_or_else(|| RoutingError::new("field_unavailable", "Native field has no board text"))?;+ if b.locked == 2 || b.knockout {+ return Err(RoutingError::new(+ "field_locked_or_knockout",+ "Locked/knockout fields are not supported",+ ));+ }+ b.layer = BoardLayerInfo::id_from_name(&e.text.layer).unwrap();+ let t = b+ .text+ .as_mut()+ .ok_or_else(|| RoutingError::new("field_unavailable", "Native field has no text"))?;+ if t.text != e.text.text {+ return Err(RoutingError::new(+ "field_identity_mismatch",+ "Native field text differs from the candidate",+ ));+ }+ let p = t.position.get_or_insert_with(Default::default);+ p.x_nm = nm(e.text.x);+ p.y_nm = nm(e.text.y);+ let a = t.attributes.get_or_insert_with(Default::default);+ if a.bold+ || a.italic+ || a.underlined+ || (!a.font_name.is_empty() && a.font_name != "KiCad Font")+ {+ return Err(RoutingError::new(+ "field_style_unsupported",+ "Only plain native stroke-font fields are supported",+ ));+ }+ a.horizontal_alignment = match e.text.align.as_str() {+ "left" => 1,+ "right" => 3,+ _ => 2,+ };+ a.vertical_alignment = 2;+ a.angle.get_or_insert_with(Default::default).value_degrees = e.text.rotation;+ a.line_spacing = 1.;+ a.stroke_width.get_or_insert_with(Default::default).value_nm = nm(e.text.stroke);+ let size = a.size.get_or_insert_with(Default::default);+ size.x_nm = nm(e.text.width);+ size.y_nm = nm(e.text.height);+ a.mirrored = e.text.mirrored;+ a.multiline = t.text.contains('\n');+ Ok(EditablePcbItem::Field(field))+}++pub fn silk_fields_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 fields",+ ));+ }+ let s = connect(ctx, args)?;+ let (text, _, revision) = s.snapshot()?;+ check_revision(args, &revision)?;+ let plan = fields::prepare(&text, args)?;+ let ids: Vec<_> = plan.edits.iter().map(|e| e.text.id.clone()).collect();+ let native = s+ .client+ .get_editable_items_by_id(ids.clone())+ .map_err(map_err)?;+ let mut updates = Vec::new();+ for e in &plan.edits {+ let f = native+ .iter()+ .find(|f| id(f) == Some(e.text.id.as_str()))+ .ok_or_else(|| {+ RoutingError::new(+ "native_field_unavailable",+ "KiCad did not return the exact nested field UUID; no whole-footprint fallback",+ )+ })?;+ let EditablePcbItem::Field(f) = f else {+ unreachable!()+ };+ updates.push(apply_geometry(f.clone(), e)?);+ }+ 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(a) = v["violations"].as_array_mut() {+ for x in a {+ x["severity"] = json!("error");+ }+ }+ v+ };+ let added = new_errors(&all(before), &all(after.clone()))?;+ if !added.is_empty() {+ return Err(+ RoutingError::new("drc_rejected", "Field geometry adds native DRC findings")+ .with("violations", json!(added))+ .with("mutated", json!(false)),+ );+ }+ if args["dryRun"] == true {+ return Ok(+ json!({"success":true,"mutated":false,"revision":revision,"drc":after,"updatedIds":ids}),+ );+ }+ check_revision(args, &s.revision()?)?;+ transaction(&s, "Adom reference/value fields", || {+ let reply = s.client.update_editable_items(updates.clone())?;+ let actual: HashSet<_> = reply.iter().filter_map(id).collect();+ if actual != ids.iter().map(String::as_str).collect() {+ return Err(IpcFailure::from(+ "Native field update did not preserve all field UUIDs".to_string(),+ ));+ }+ Ok(())+ })?;+ let mut out = json!({"success":true,"committed":true,"mutated":true,"undoSteps":1,"updatedIds":ids,"footprintIds":plan.edits.iter().map(|e|&e.footprint_id).collect::<Vec<_>>(),"saved":false,"drc":after,"_hint":"Only nested reference/value fields were updated; no whole footprint replacement. Inspect postCommit errors before any retry, then refresh the exact linked views and inspect the board."});+ match s.client.get_editable_items_by_id(ids.clone()) {+ Ok(actual) => {+ let matches = updates.iter().all(|want| {+ actual.iter().any(|got| {+ id(got) == id(want) && got.clone().into_any() == want.clone().into_any()+ })+ });+ out["fieldsVerified"] = json!(matches);+ if !matches {+ out["success"] = json!(false);+ out["postCommitFieldError"]=json!("Native field readback differs from requested geometry or preserved content; inspect before retrying");+ }+ }+ Err(e) => {+ out["success"] = json!(false);+ out["postCommitFieldError"] = json!(e.to_string());+ }+ }+ match s.snapshot() {+ Ok((current, _, rev)) => {+ out["revision"] = json!(rev);+ let set = ids.iter().cloned().collect();+ let preserved = match (+ fields::preserved_geometry(&text, &set),+ fields::preserved_geometry(¤t, &set),+ ) {+ (Ok(before), Ok(after)) => before == after,+ _ => false,+ };+ out["otherBoardDataVerified"] = json!(preserved);+ if !preserved {+ out["success"] = json!(false);+ out["postCommitPreservationError"]=json!("Board data outside the requested fields changed; this may also indicate concurrent editing");+ }+ }+ Err(e) => {+ out["success"] = json!(false);+ out["postCommitReadbackError"] = json!(e.message);+ }+ }+ 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 && out["success"] == 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)+}++#[cfg(test)]+mod tests {+ use super::*;+ #[test]+ fn nested_field_uuid_and_content_survive_geometry_update() {+ let mut f = FieldItem::from_proto(Default::default());+ let p = f.proto_mut();+ p.name = "Reference".into();+ p.id = Some(Default::default());+ p.visible = true;+ let b = p.text.get_or_insert_with(Default::default);+ b.id.get_or_insert_with(Default::default).value = "field-id".into();+ b.locked = 1;+ let text = b.text.get_or_insert_with(Default::default);+ text.text = "R1".into();+ text.hyperlink = "preserve-me".into();+ let e=fields::Edit{text:crate::silk_text::Text::parse(&json!({"text":"R1","layer":"B.SilkS","x":12.,"y":19.,"rotation":90.,"height":0.7,"width":0.6,"stroke":0.1,"align":"right"}),"field-id".into()).unwrap(),visible:false,footprint_id:"parent".into(),name:"Reference".into()};+ let result = apply_geometry(f, &e).unwrap();+ assert_eq!(id(&result), Some("field-id"));+ let EditablePcbItem::Field(field) = result else {+ panic!("not a field")+ };+ let p = field.proto();+ assert_eq!(p.name, "Reference");+ assert!(!p.visible);+ assert_eq!(p.id.as_ref().unwrap().id, 0);+ let b = p.text.as_ref().unwrap();+ let t = b.text.as_ref().unwrap();+ assert_eq!(t.text, "R1");+ assert_eq!(t.hyperlink, "preserve-me");+ assert_eq!(t.position.as_ref().unwrap().x_nm, 12_000_000);+ assert_eq!(b.layer, BoardLayerInfo::id_from_name("B.SilkS").unwrap());+ let a = t.attributes.as_ref().unwrap();+ assert_eq!(a.horizontal_alignment, 3);+ assert!(a.mirrored);+ assert_eq!(a.size.as_ref().unwrap().y_nm, 700_000);+ let mut bold = field.clone();+ bold.proto_mut()+ .text+ .as_mut()+ .unwrap()+ .text+ .as_mut()+ .unwrap()+ .attributes+ .as_mut()+ .unwrap()+ .bold = true;+ assert!(apply_geometry(bold, &e).is_err());+ }+}