← 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

John Lauer ·ef5a17af4f ·27d ago ·parent 120a639
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(()) }