app
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.
← Commit history
tour fixes from the first native run: stop verbs bypass the ab backoff and timeouts do not start it; per-gesture foreground tolerance; zoom-to-fit repaint before failing a beat on a blank canvas
3 files changed
+40−4
rust/crates/kicad-bridge/src/verbs_demo.rs+20−1@@ -674,7 +674,26 @@ fn verify_beat(hwnd: Option<u64>, expected_title: &str) -> Value { } sleep_ms(600); }- let probe = canvas_probe(hwnd);+ let mut probe = canvas_probe(hwnd);+ if is_true(&probe, "checked") && !is_true(&probe, "rendered") {+ // A loaded Footprint or Symbol Editor can sit on an unpainted GL canvas until the+ // view changes (the 0.9.202 blank-frame class; seen on the tour's footprint beat,+ // ConfRoomROG 2026-09-11). Zoom to fit through the native View menu (background,+ // no input) and measure again.+ let fit = kicad_platform::native().menu_tree(hwnd).ok().and_then(|items| {+ items.into_iter().find(|it| {+ let last = it.path.last().map(|p| p.to_lowercase()).unwrap_or_default();+ last.contains("zoom to fit") || last == "fit" || last.contains("fit to")+ })+ });+ if let Some(it) = fit {+ if kicad_platform::native().menu_invoke(hwnd, it.id).is_ok() {+ v["repaint"] = json!({"via": "menu", "item": it.path.join(" > "), "id": it.id});+ sleep_ms(1500);+ probe = canvas_probe(hwnd);+ }+ }+ } v["painted"] = if is_true(&probe, "checked") { json!(is_true(&probe, "rendered")) } else { Value::Null }; if probe.is_object() { let mut c = json!({});
rust/crates/kicad-core/src/ab.rs+9−2@@ -292,7 +292,10 @@ pub fn call(command: &str, args: Value, timeout: Duration) -> Result<AbReply, Ab let Some(base) = base_url() else { return Err(AbError::Unavailable("ADOM_DIRECT_API_URL is not set and ~/.adom/direct-api-port is missing (inbound-only spawn?)".into())); };- if in_backoff() {+ // Cleanup and stop verbs are never skipped: a swallowed desktop_record_stop leaves the+ // recorder running and a zero-byte file (ConfRoomROG, 2026-09-11).+ let critical = matches!(command, "desktop_record_stop" | "desktop_record_window_stop" | "desktop_caption_hide" | "notify_dismiss" | "desktop_demo_panel" | "desktop_set_window_bounds");+ if in_backoff() && !critical { return Err(AbError::Unavailable("direct API failed a moment ago; backing off".into())); } let mut args = match args {@@ -312,7 +315,11 @@ pub fn call(command: &str, args: Value, timeout: Duration) -> Result<AbReply, Ab let text = match http_post(&format!("{base}/command"), &headers, &body, timeout) { Ok(t) => t, Err(e) => {- note_failure();+ // A timeout means ab is busy (a 30 s screenshot, a recorder finalising), not gone:+ // only a connection-level failure starts the backoff.+ if !e.contains("timeout") && !e.contains("timed out") {+ note_failure();+ } return Err(AbError::Unavailable(e)); } };
rust/crates/kicad-platform/src/win/tour_input.rs+11−1@@ -32,9 +32,19 @@ use super::{focus, from_wide, wide}; pub const NOT_FOREGROUND: &str = "window is not foreground: refusing synthetic input"; fn require_foreground(h: u64) -> Result<(), String> {- if h == 0 || focus::foreground() != h {+ if h == 0 { return Err(NOT_FOREGROUND.into()); }+ // A caption or the demo panel can hold the foreground for a frame or two right when a+ // gesture starts; give the OS up to 600 ms to agree before refusing (the Python gate+ // waited 1.5 s once per beat; this is the per-gesture version).+ let deadline = std::time::Instant::now() + std::time::Duration::from_millis(600);+ while focus::foreground() != h {+ if std::time::Instant::now() >= deadline {+ return Err(NOT_FOREGROUND.into());+ }+ std::thread::sleep(std::time::Duration::from_millis(40));+ } Ok(()) }