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.33: NATIVE (Rust) build. 1.0.33 (wiki #109): model check and 3D show verbs report the 3D viewer's model-visibility toggles and say when a hidden class exists on the board; new kicad_3d_viewer_settings; failed kicad_close lists dialogsSeen; show verbs wait for the file's title before capturing.
1 file changed
+156
rust/crates/kicad-core/src/model_check.rs+156@@ -631,6 +631,114 @@ pub fn check_footprint(ctx: &Ctx, library: &str, footprint: &str, project_dir: O } } +/// The 3D viewer's model-visibility toggles (wiki #109): `<config>/3d_viewer.json`,+/// `render.show_footprints_*`. They hide whole CLASSES of footprint bodies in the viewer+/// AND in `kicad-cli pcb render`, so a board whose every model file resolves can still+/// render as pads only. `insert` is KiCad's name for SMD, `normal` for through-hole.+pub const VIEWER_FLAGS: &[(&str, &str, &str)] = &[+ ("smd", "show_footprints_insert", "SMD models"),+ ("throughHole", "show_footprints_normal", "through-hole models"),+ ("virtual", "show_footprints_virtual", "virtual (other) models"),+ ("notInPosFile", "show_footprints_not_in_posfile", "models not in the position file"),+ ("dnp", "show_footprints_dnp", "models marked DNP"),+];++pub fn viewer_settings_path(ctx: &Ctx) -> Option<std::path::PathBuf> {+ ctx.config_dir.as_ref().map(|d| d.join("3d_viewer.json"))+}++/// `{settingsFile, flags:{smd,throughHole,virtual,notInPosFile,dnp}, known}`. A missing+/// file or key reads as KiCad's default (true, except dnp which defaults false).+pub fn viewer_visibility(ctx: &Ctx) -> Value {+ let path = viewer_settings_path(ctx);+ let doc: Value = path.as_ref().and_then(|p| std::fs::read_to_string(p).ok()).and_then(|t| serde_json::from_str(&t).ok()).unwrap_or(Value::Null);+ let mut flags = serde_json::Map::new();+ for (key, json_key, _) in VIEWER_FLAGS {+ let default = *key != "dnp";+ flags.insert((*key).to_string(), json!(doc["render"][*json_key].as_bool().unwrap_or(default)));+ }+ json!({"settingsFile": path.as_ref().map(|p| norm(p)), "known": !doc.is_null(), "flags": flags})+}++/// Which viewer class a footprint's body belongs to, from its `(attr ...)` tokens.+pub fn viewer_classes(attrs: &[String]) -> Vec<&'static str> {+ let has = |t: &str| attrs.iter().any(|a| a == t);+ let mut out = vec![if has("smd") { "smd" } else if has("through_hole") { "throughHole" } else { "virtual" }];+ if has("exclude_from_pos_files") { out.push("notInPosFile"); }+ if has("dnp") { out.push("dnp"); }+ out+}++/// Set the viewer's model-visibility toggles (wiki #109 ask 2). File logic only: the caller+/// refuses while KiCad runs, because KiCad rewrites 3d_viewer.json from memory when it exits.+/// Every other key in the file is preserved; the previous file is kept beside it as+/// `3d_viewer.json.adom.bak`; the write goes through a temp file and a rename.+pub fn set_viewer_visibility(ctx: &Ctx, args: &Value) -> Value {+ let Some(path) = viewer_settings_path(ctx) else {+ return json!({"success": false, "errorCode": "not_installed", "error": "No KiCad config directory detected, so there is no 3d_viewer.json to edit."});+ };+ let before = viewer_visibility(ctx);+ let mut wanted: Vec<(&str, &str, bool)> = Vec::new();+ for (key, json_key, _) in VIEWER_FLAGS {+ match args.get(*key) {+ None | Some(Value::Null) => {}+ Some(Value::Bool(b)) => wanted.push((key, json_key, *b)),+ Some(_) => return json!({"success": false, "errorCode": "bad_arg", "error": format!("{key} must be true or false"), "mutated": false}),+ }+ }+ if wanted.is_empty() {+ return json!({"success": true, "mutated": false, "viewer": before,+ "_hint": "Read only. Pass smd, throughHole, virtual, notInPosFile or dnp (true|false) to change a toggle; KiCad must be closed while the file is written, then reopen the viewer."});+ }+ let mut doc: Value = std::fs::read_to_string(&path).ok().and_then(|t| serde_json::from_str(&t).ok()).unwrap_or_else(|| json!({}));+ if !doc.is_object() {+ doc = json!({});+ }+ if !doc["render"].is_object() {+ doc["render"] = json!({});+ }+ let mut changed = serde_json::Map::new();+ for (key, json_key, value) in &wanted {+ if doc["render"][*json_key].as_bool() != Some(*value) {+ changed.insert((*key).to_string(), json!(value));+ }+ doc["render"][*json_key] = json!(value);+ }+ if changed.is_empty() {+ return json!({"success": true, "mutated": false, "viewer": before, "_hint": "Every requested toggle already had that value; nothing was written."});+ }+ if path.is_file() {+ let _ = std::fs::copy(&path, path.with_extension("json.adom.bak"));+ } else if let Some(dir) = path.parent() {+ let _ = std::fs::create_dir_all(dir);+ }+ let tmp = path.with_extension("json.adom.tmp");+ let body = serde_json::to_string_pretty(&doc).unwrap_or_default() + "\n";+ if let Err(e) = std::fs::write(&tmp, body).and_then(|_| std::fs::rename(&tmp, &path)) {+ let _ = std::fs::remove_file(&tmp);+ return json!({"success": false, "errorCode": "settings_write_failed", "error": format!("cannot write {}: {e}", path.display()), "mutated": false});+ }+ json!({"success": true, "mutated": true, "changed": changed, "before": before["flags"], "viewer": viewer_visibility(ctx),+ "backup": norm(&path.with_extension("json.adom.bak")),+ "_hint": "Written. The 3D viewer and kicad-cli pcb render read this file when they start: reopen the board and its viewer (kicad_show_3d_board) to see the bodies."})+}++/// The sentence for a reply when the viewer hides a class that exists on the board.+pub fn viewer_hint(visibility: &Value, counts: &serde_json::Map<String, Value>) -> Option<String> {+ let mut parts = Vec::new();+ for (key, _, label) in VIEWER_FLAGS {+ let n = counts.get(*key).and_then(Value::as_u64).unwrap_or(0);+ if n > 0 && visibility["flags"][*key] == json!(false) {+ parts.push(format!("{label} are hidden and the board has {n} such footprint(s)"));+ }+ }+ if parts.is_empty() {+ return None;+ }+ Some(format!("The 3D viewer settings hide bodies that exist: {}. They will not render in the viewer or in kicad-cli pcb render even though the model files resolve. kicad_3d_viewer_settings shows and sets these toggles (KiCad must be closed while it writes).", parts.join("; ")))+}++ /// kicad_model_check {boardPath}. pub fn check_board(ctx: &Ctx, board_path: &str, limit: usize) -> Value { let p = Path::new(board_path);@@ -647,6 +755,7 @@ pub fn check_board(ctx: &Ctx, board_path: &str, limit: usize) -> Value { let project_dir = p.parent().map(norm).unwrap_or_default(); let mut per = Vec::new(); let mut bindings = Vec::new();+ let mut class_counts: serde_json::Map<String, Value> = serde_json::Map::new(); let (mut footprints, mut with_model, mut no_model_expected_n, mut missing, mut unresolved, mut nwarn) = (0u64, 0u64, 0u64, 0u64, 0u64, 0u64); for fp in tree.find_all("footprint") { let fp_id = fp.atom_at(1).unwrap_or("?").to_string();@@ -660,6 +769,12 @@ pub fn check_board(ctx: &Ctx, board_path: &str, limit: usize) -> Value { } let rows = model_rows(fp, ctx, Some(&project_dir)); let a = attrs(fp);+ if !rows.is_empty() {+ for c in viewer_classes(&a) {+ let n = class_counts.get(c).and_then(Value::as_u64).unwrap_or(0) + 1;+ class_counts.insert(c.to_string(), json!(n));+ }+ } bindings.push(json!({ "itemId": fp.find("uuid").and_then(|x| x.atom_at(1)), "reference": reference,@@ -690,6 +805,8 @@ pub fn check_board(ctx: &Ctx, board_path: &str, limit: usize) -> Value { per.push(json!({"reference": reference, "footprint": fp_id, "warnings": w, "models": models})); } }+ let viewer = viewer_visibility(ctx);+ let viewer_note = viewer_hint(&viewer, &class_counts); json!({ "success": true, "board": norm(p),@@ -699,6 +816,9 @@ pub fn check_board(ctx: &Ctx, board_path: &str, limit: usize) -> Value { "missingModel": missing, "unresolvedModel": unresolved, "warnings": nwarn}, "footprintsWithWarnings": per, "modelBindings": bindings,+ "viewer": viewer,+ "footprintsWithModelByViewerClass": class_counts,+ "_viewerHint": viewer_note, "bindingsComplete": true, "evidenceSource": "saved-board-file", "unsavedEditorStateIncluded": false,@@ -738,6 +858,42 @@ pub fn handle(ctx: &Ctx, args: &Value) -> Value { mod tests { use super::*; + // Wiki #109: every model resolved and the chips were still invisible, because the+ // viewer's own "SMD Models" toggle was off.+ #[test]+ fn viewer_toggles_are_read_classed_hinted_and_set() {+ let dir = std::env::temp_dir().join(format!("kb109-{}", std::process::id()));+ std::fs::create_dir_all(&dir).unwrap();+ std::fs::write(dir.join("3d_viewer.json"), r#"{"camera":{"keep":1},"render":{"show_footprints_insert":false,"show_zones":true}}"#).unwrap();+ let ctx = Ctx { config_dir: Some(dir.clone()), ..Default::default() };+ let v = viewer_visibility(&ctx);+ assert_eq!(v["flags"]["smd"], json!(false));+ assert_eq!(v["flags"]["throughHole"], json!(true), "a missing key reads as KiCad's default");+ assert_eq!(v["flags"]["dnp"], json!(false));+ assert_eq!(viewer_classes(&["smd".into(), "dnp".into()]), vec!["smd", "dnp"]);+ assert_eq!(viewer_classes(&["through_hole".into()]), vec!["throughHole"]);+ assert_eq!(viewer_classes(&[]), vec!["virtual"]);+ let mut counts = serde_json::Map::new();+ counts.insert("smd".into(), json!(3));+ counts.insert("throughHole".into(), json!(2));+ let hint = viewer_hint(&v, &counts).unwrap();+ assert!(hint.contains("SMD models are hidden") && hint.contains("3 such"), "{hint}");+ assert!(!hint.contains("through-hole"), "a visible class is not mentioned: {hint}");+ // Read-only call, then a real set that preserves every other key and leaves a backup.+ assert_eq!(set_viewer_visibility(&ctx, &json!({}))["mutated"], json!(false));+ assert_eq!(set_viewer_visibility(&ctx, &json!({"smd": "yes"}))["errorCode"], json!("bad_arg"));+ let r = set_viewer_visibility(&ctx, &json!({"smd": true}));+ assert_eq!(r["mutated"], json!(true), "{r}");+ assert_eq!(r["viewer"]["flags"]["smd"], json!(true));+ let doc: Value = serde_json::from_str(&std::fs::read_to_string(dir.join("3d_viewer.json")).unwrap()).unwrap();+ assert_eq!(doc["camera"]["keep"], json!(1));+ assert_eq!(doc["render"]["show_zones"], json!(true));+ assert!(dir.join("3d_viewer.json.adom.bak").is_file());+ assert!(viewer_hint(&viewer_visibility(&ctx), &counts).is_none());+ assert_eq!(set_viewer_visibility(&ctx, &json!({"smd": true}))["mutated"], json!(false), "idempotent");+ let _ = std::fs::remove_dir_all(&dir);+ }+ const STEP: &[u8] = b"ISO-10303-21;\nHEADER;ENDSEC;\nDATA;\n#1=MANIFOLD_SOLID_BREP('x',#2);\n#2=CLOSED_SHELL('',());\nENDSEC;\nEND-ISO-10303-21;\n"; struct Fixture {