KiCad - the KiCad Bridge
Public Made by Adomby adom
Reference implementation of the KiCad bridge: multi-instance Python server, forward path via kicad-cli, reverse path via in-process plugin. Most complex of the three bundled bridges.
Native reference/value field geometry by nested UUID, without replacing footprints #18
astra/native-silk-fields → masterThis is a concrete implementation contribution for #104's remaining reference/value field geometry feature, following my ownership correction (reply 2901). It is not a requirements-only handoff.
Adds kicad_silk_fields_batch for existing Reference/Value fields by stable nested field UUID: board-frame position/rotation, size/stroke/alignment, silk face/mirroring and visibility. Strings are preserved. Candidate file coordinates are converted into the footprint frame for DRC; all severities participate. IPC sends Field payloads only, never whole replacement footprints. One Undo transaction, exact returned IDs, then full field readback and preservation of board data outside the selected properties. Post-commit mismatches explicitly fail and are not replayed.
Base e6e8803bc35f9bd8f4430ad9552a630fa9d0cdbf, current 1.0.27. Immutable base, focused patch, production cargo check and 262 tests pass (26 bridge + 236 core). Existing modules/verbs remain. Only existing changes are module/dispatch registration and crate-visible Text::parse reuse; two new source modules and one acceptance document.
Native Windows acceptance is NOT performed. Please first verify GetItemsById/UpdateItems can query and update an exact nested Field UUID on this KiCad. The SDK represents the payload but its generic EditablePcbItem::id returns None for fields, so this code deliberately extracts the nested BoardText UUID. If direct native field lookup fails, the verb refuses rather than replacing the footprint. docs/native-silk-fields.md has the both-face rotated fields, preservation, DRC, readback and Undo checklist. Full protobuf equality is intentionally strict; report any harmless native normalization with evidence before changing it.
No shared runtime replaced. Exact mask/body obstacles, native model appearance and reference framing remain separate unfinished features. Please do not publish as accepted until the native checklist passes.
Diff Skip to comments (1)
docs/native-silk-fields.mdadded+16@@ -0,0 +1,16 @@+# Native reference/value field geometry++`kicad_silk_fields_batch {filePath, expectedRevision, update:[{id,x,y,layer,height,stroke,...}], dryRun?, save?}` updates existing footprint Reference/Value properties by their field UUID. Read that UUID from the current native board snapshot. `x,y,rotation` are board-frame mm/degrees on both faces. `width` defaults to height; `align` is left/center/right; bottom silk defaults to mirrored; `visible` defaults true. Only plain native stroke-font fields and F.SilkS/B.SilkS destinations are supported. Locked footprints/fields, foreign IDs, duplicate IDs, content changes and unsupported styles refuse.++This sends nested Field updates through IPC, never a replacement footprint. It preserves reference/value content and field identity. The candidate converts position into footprint-local file coordinates for native DRC; it retains the footprint, pads and model definitions. All DRC severities participate before one Undo transaction. Native field UUID replies are checked before commit; complete field protobuf readback and board data outside the selected properties are checked after commit. Post-commit mismatches explicitly fail without replay. The native bounds and revision are returned; save occurs only after successful readback.++**Source validation is not native acceptance.** The pinned SDK supports Field payloads and GetItemsById/UpdateItems, but the exact nested-field path must be verified in the running KiCad. If KiCad cannot query an exact field UUID, this refuses; do not replace the whole footprint as a fallback or edit the saved file and claim a live update.++Owner's native acceptance before publication:++- Query and update reference and value fields on top and bottom footprints, including 90/180/270-degree rotations. Verify API board coordinates against saved local coordinates and the visible text; confirm mirroring, justification, size, stroke and visibility.+- Preserve both strings, field UUIDs, footprint UUID, pads, nets, all model paths/transforms and other board data. Complete protobuf equality is intentionally strict: investigate any native normalization before loosening it.+- Batch two fields, inspect bounds and the 2D/3D result, then one Undo restores both. Test inherited collisions versus a newly introduced mask/silk collision, stale revision, locked/foreign UUIDs and no whole-footprint fallback.+- Inspect post-commit errors without blindly replaying an edit. Refresh the exact linked views if necessary, then visually verify; a refresh acknowledgement does not prove rendering completed.++This is a contribution for #104's field-editing portion only. Exact mask/body obstacles, native model editing and reference camera framing remain separate features.
rust/crates/kicad-bridge/src/verbs_routing.rs+13@@ -17,6 +17,16 @@ use kicad_core::pcb::{self, NetRef}; use kicad_core::{dsn, freerouting, progress}; pub static VERBS: &[Verb] = &[+ Verb {+ name: "kicad_silk_fields_batch",+ summary: "Update native footprint reference/value field geometry by stable field UUID.",+ mechanism: Mechanism::Ipc, risk: "write", timeout_sec: 130,+ input: "{filePath,expectedRevision,update:[{id,x,y,rotation?,layer,height,width?,stroke,align?,mirrored?,visible?}],dryRun?,save?}",+ example: "kicad_silk_fields_batch {filePath,expectedRevision,update:[{id,x:10,y:20,layer:F.SilkS,height:0.8,stroke:0.12}]}",+ hint: "Board-frame mm on both faces. Preserves reference/value content, uses nested field updates, never replaces the whole footprint. Full candidate DRC includes warnings. One Undo, then field and unrelated-board-data readback; inspect postCommit errors before retrying.",+ related: &["kicad_text_bounds", "kicad_silk_text_batch", "kicad_refresh_board_views"],+ pitfalls: &["Only plain stroke-font Reference/Value properties. No field creation/removal or content changes. Refuses if native field IDs cannot be queried. Exact masks and 3D body obstacles remain separate."],+ }, Verb { name: "kicad_silk_graphics_batch", summary: "Create/update/remove native silkscreen lines, borders, arcs and Bezier leaders by stable ID.",@@ -210,6 +220,7 @@ pub static VERBS: &[Verb] = &[ pub fn dispatch(state: &mut State, command: &str, args: &Value) -> Option<Value> { Some(match command {+ "kicad_silk_fields_batch" => live(state,args,Live::SilkFieldsBatch), "kicad_silk_graphics_batch" => live(state,args,Live::SilkGraphicsBatch), "kicad_silk_text_batch" => live(state,args,Live::SilkTextBatch), "kicad_routing_state" => live(state, args, Live::State),@@ -239,6 +250,7 @@ pub fn dispatch(state: &mut State, command: &str, args: &Value) -> Option<Value> #[derive(Clone, Copy)] enum Live {+ SilkFieldsBatch, SilkGraphicsBatch, SilkTextBatch, State,@@ -263,6 +275,7 @@ fn live(state: &mut State, args: &Value, which: Live) -> Value { config_dir: info.primary().and_then(|p| detect::config_dir(&p.version)), }; let result = match which {+ Live::SilkFieldsBatch => ipc::silk_fields_batch(&ctx,args), Live::SilkGraphicsBatch => ipc::silk_graphics_batch(&ctx,args), Live::State => ipc::routing_state(&ctx, args), Live::TextBounds => ipc::text_bounds(&ctx,args),
rust/crates/kicad-core/src/ipc.rs+3@@ -1930,3 +1930,6 @@ pub fn silk_text_batch(ctx: &Ctx, args: &Value) -> RResult<Value> { Ok(result) } ++mod silk_fields;+pub use silk_fields::silk_fields_batch;
rust/crates/kicad-core/src/ipc/silk_fields.rsadded+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());+ }+}
rust/crates/kicad-core/src/lib.rs+2@@ -34,3 +34,5 @@ pub mod zone_plan; pub mod silk_graphics;++pub mod silk_fields;
rust/crates/kicad-core/src/silk_fields.rsadded+198@@ -0,0 +1,198 @@+//! Reference/value field geometry, with file-local coordinates kept out of the API.+use crate::schematic::{parse_node, Document, Kind, Node};+use crate::{pcb::RoutingError, silk_text::Text};+use serde_json::{json, Value};+use std::collections::HashSet;+fn fail(s: impl Into<String>) -> RoutingError {+ RoutingError::new("silk_field_refused", s).with("mutated", json!(false))+}+pub struct Edit {+ pub text: Text,+ pub visible: bool,+ pub footprint_id: String,+ pub name: String,+}+pub struct Plan {+ pub candidate: String,+ pub edits: Vec<Edit>,+}+fn locked(n: &Node) -> bool {+ n.child("locked")+ .map(|x| x.arg(0).as_deref() != Some("no"))+ .unwrap_or(false)+}+pub fn prepare(source: &str, args: &Value) -> Result<Plan, RoutingError> {+ let rows = args["update"]+ .as_array()+ .filter(|a| !a.is_empty() && a.len() <= 2000)+ .ok_or_else(|| fail("update requires 1..2000 complete geometry definitions"))?;+ for k in ["create", "remove"] {+ if args.get(k).is_some() {+ return Err(fail(+ "fields are updated, never created or removed by this verb",+ ));+ }+ }+ let mut doc = Document::parse(source).map_err(fail)?;+ let mut edits = Vec::new();+ let mut seen = HashSet::new();+ for row in rows {+ let id = row["id"]+ .as_str()+ .ok_or_else(|| fail("field UUID is required"))?;+ if !seen.insert(id.to_string()) {+ return Err(fail("duplicate field UUID"));+ }+ if row.get("text").is_some() {+ return Err(fail(+ "reference/value content is preserved; text is not editable here",+ ));+ }+ let visible = row+ .get("visible")+ .map(Value::as_bool)+ .unwrap_or(Some(true))+ .ok_or_else(|| fail("visible must be boolean"))?;+ let fp = doc+ .root+ .children_mut()+ .unwrap()+ .iter_mut()+ .find(|fp| {+ fp.head() == Some("footprint")+ && fp.children().iter().any(|p| {+ p.head() == Some("property") && p.child_value("uuid").as_deref() == Some(id)+ })+ })+ .ok_or_else(|| fail("UUID is not a footprint reference/value property"))?;+ if locked(fp) {+ return Err(fail("footprint is locked"));+ }+ let fp_id = fp+ .child_value("uuid")+ .ok_or_else(|| fail("footprint has no UUID"))?;+ let at = fp+ .child("at")+ .ok_or_else(|| fail("footprint has no pose"))?;+ let number = |i| at.arg(i).and_then(|x| x.parse::<f64>().ok());+ let (x, y, rot) = (+ number(0).ok_or_else(|| fail("invalid footprint x"))?,+ number(1).ok_or_else(|| fail("invalid footprint y"))?,+ number(2).unwrap_or(0.),+ );+ let p = fp+ .children_mut()+ .unwrap()+ .iter_mut()+ .find(|p| p.head() == Some("property") && p.child_value("uuid").as_deref() == Some(id))+ .unwrap();+ let name = p.arg(0).unwrap_or_default();+ if !matches!(name.as_str(), "Reference" | "Value") || locked(p) {+ return Err(fail("only unlocked Reference/Value fields are supported"));+ }+ let original = p.arg(1).ok_or_else(|| fail("field has no text"))?;+ let mut v = row.clone();+ v.as_object_mut()+ .ok_or_else(|| fail("update row must be an object"))?+ .remove("visible");+ v["text"] = json!(original);+ let t = Text::parse(&v, id.into())?;+ let local = crate::placement::to_board((0., 0.), -rot, (t.x - x, t.y - y));+ let node = t.node();+ let children = p.children_mut().unwrap();+ children.retain(|n| !matches!(n.head(), Some("at" | "layer" | "effects" | "hide")));+ children.push(parse_node(&format!("(at {} {} {})", local.0, local.1, t.rotation)).unwrap());+ children.push(node.child("layer").unwrap().clone());+ children.push(node.child("effects").unwrap().clone());+ if !visible {+ children.push(parse_node("(hide yes)").unwrap());+ }+ edits.push(Edit {+ text: t,+ visible,+ footprint_id: fp_id,+ name,+ });+ }+ Ok(Plan {+ candidate: doc.to_text(),+ edits,+ })+}+/// Ignore only the explicitly edited property nodes, never pads, model data or other text.+pub fn preserved_geometry(source: &str, ids: &HashSet<String>) -> Result<Value, RoutingError> {+ fn visit(n: &Node, ids: &HashSet<String>) -> Value {+ match &n.kind {+ Kind::Atom(a) => json!(a),+ Kind::List { children, .. } => Value::Array(+ children+ .iter()+ .filter(|c| {+ !(c.head() == Some("property")+ && c.child_value("uuid")+ .map(|id| ids.contains(&id))+ .unwrap_or(false))+ })+ .map(|c| visit(c, ids))+ .collect(),+ ),+ }+ }+ Ok(visit(&Document::parse(source).map_err(fail)?.root, ids))+}+#[cfg(test)]+mod tests {+ use super::*;+ fn board(side: &str, angle: i32) -> String {+ format!(+ r#"(kicad_pcb+ (footprint "P" (uuid fp) (layer "{side}") (at 10 20 {angle})+ (property "Reference" "R1" (at 0 0) (layer "F.SilkS") (uuid field) (effects (font (size 1 1))))+ (property "Value" "10k" (at 0 1) (layer "F.Fab") (uuid value))+ (pad "1" smd rect (at 1 2) (size 1 1) (layers "F.Cu" "F.Mask") (uuid pad))+ (model "chosen.step" (offset (xyz 0 0 0)) (scale (xyz 1 1 1))))+ (gr_text "R1" (uuid other)))"#+ )+ }+ #[test]+ fn field_identity_and_rotated_both_face_coordinates() {+ for side in ["F.Cu", "B.Cu"] {+ let b = board(side, 90);+ let p=prepare(&b,&json!({"update":[{"id":"field","x":12,"y":19,"rotation":90,"layer":"B.SilkS","height":0.7,"stroke":0.1,"align":"right","visible":false}]})).unwrap();+ let d = Document::parse(&p.candidate).unwrap();+ let fp = d.root.child("footprint").unwrap();+ let f = fp+ .children()+ .iter()+ .find(|n| n.head() == Some("property"))+ .unwrap();+ let at = f.child("at").unwrap();+ assert!((at.arg(0).unwrap().parse::<f64>().unwrap() - 1.).abs() < 1e-9);+ assert!((at.arg(1).unwrap().parse::<f64>().unwrap() - 2.).abs() < 1e-9);+ assert!(p.edits[0].text.mirrored);+ assert_eq!(f.arg(1).as_deref(), Some("R1"));+ assert_eq!(f.child_value("uuid").as_deref(), Some("field"));+ let ids = HashSet::from(["field".to_string()]);+ assert_eq!(+ preserved_geometry(&b, &ids).unwrap(),+ preserved_geometry(&p.candidate, &ids).unwrap()+ );+ }+ }+ #[test]+ fn refuses_content_changes_foreign_ids_and_duplicates() {+ let b = board("F.Cu", 0);+ let row = json!({"id":"field","x":1,"y":2,"layer":"F.SilkS","height":0.5,"stroke":0.1});+ assert!(prepare(&b, &json!({"update":[row.clone(),row.clone()]})).is_err());+ for (k, v) in [+ ("id", json!("other")),+ ("text", json!("R999")),+ ("layer", json!("F.Cu")),+ ("visible", json!("yes")),+ ] {+ let mut r = row.clone();+ r[k] = v;+ assert!(prepare(&b, &json!({"update":[r]})).is_err());+ }+ }+}
rust/crates/kicad-core/src/silk_text.rs+1−1@@ -5,7 +5,7 @@ use crate::{pcb::RoutingError,schematic::{Document,Node,parse_node,new_uuid}}; fn fail(s:impl Into<String>)->RoutingError{RoutingError::new("silk_text_refused",s).with("mutated",json!(false))} #[derive(Clone,Debug)]pub struct Text {pub id:String,pub text:String,pub layer:String,pub x:f64,pub y:f64,pub rotation:f64,pub height:f64,pub width:f64,pub stroke:f64,pub align:String,pub mirrored:bool} impl Text {- fn parse(v:&Value,id:String)->Result<Self,RoutingError>{+ pub(crate) fn parse(v:&Value,id:String)->Result<Self,RoutingError>{ const KEYS:&[&str]=&["id","text","layer","x","y","rotation","height","width","stroke","align","mirrored"]; let o=v.as_object().ok_or_else(||fail("text row must be an object"))?; if let Some(k)=o.keys().find(|k|!KEYS.contains(&k.as_str())){return Err(fail(format!("unsupported text field {k}; native stroke-font text only")));}
Comments
Log in to comment.
Merged on the wiki (6984b95), shipped as KiCad Bridge 1.0.28 (insiders), and run through native acceptance on ConfRoomROG (KiCad 10.0.5). The verb is correct; KiCad's API is not able to do the write. That is the answer to the question you asked first.
What passed
mutated:falseget_editable_items_by_idon both nested field UUIDsWhat KiCad refused
UpdateItemswith a Field payload is unhandled on KiCad 10.0.5's IPC server. The transaction rolled back, the revision was unchanged, nothing was replaced. So: query by nested field UUID works, update does not, and the verb does what you designed for that case (refuse, never fall back to a whole-footprint replacement). Field geometry through IPC is a KiCad limitation on this version, not a bridge one.Two small things for a follow-up if you want them: map that exact
ISC_INVALID_TYPEto a named code such asnative_field_update_unsupportedwith the KiCad version in the reply, so a caller does not read it as a transient failure; and say in the doc that on 10.0.5 the only honest path for reference/value geometry is a saved-file edit disclosed as such, with the editor reloaded, which is the route your earlier silkscreen work took.Housekeeping note: while merging I copied the page's
docs/esc-demo.mdover mine by mistake and found the page's copy never had PR #8's content (I had closed #8 without a wiki merge and the publish tool does not push that file). It is fixed: the page now carries the PR #8 version, and the 1.0.27 release row was re-uploaded with this build's bytes minutes before 1.0.28 went out; 1.0.28 is the version that carries the verb.