← Commit history

0.9.307: Tour verification (John: 'tool your bridge code to definitively know that the correct thing is showing'): every beat now MEASURES, after its raise, that its own window is the top of AD's z-order, that the live title carries the expected document, that the GL canvas is rendered (kicad_ui canvas probe) and that two captures 0.8 s apart are identical (settled). The result rides on the step and on progress as ; motion runs only on a verified window, otherwise the step says exactly which fact failed and which window was actually on top. Zoom discipline: 2D beats start and end on extents with two wheel clicks in and back out; 3D beats zoom one menu step in and out and end on Zoom to Fit while staying isometric.

John Lauer ·5113339ec3 ·1mo ago ·parent 85b01db
3 files changed +83−12
BRIDGE_VERSION+1−1
@@ -1 +1 @@-0.9.306\ No newline at end of file+0.9.307\ No newline at end of file
bridge.json+1−1
@@ -2,7 +2,7 @@   "manifest_version": 1,   "name": "kicad",   "displayName": "KiCad EDA",-  "version": "0.9.306",+  "version": "0.9.307",   "description": "Reverse bridge for KiCad \u2014 board/schematic introspection, lint via kicad-cli, plugin install, multi-instance probe, in-process DRC.",   "homepage": "https://wiki.adom.inc/adom/adom-bridge",   "author": "Adom Inc.",
handlers/demo.py+81−10
@@ -628,6 +628,68 @@ def _job_update(**kw):         _JOB.update(kw)  +def _verify_beat(hwnd: int | None, expected_title: str, tries: int = 2) -> dict:+    """MEASURE, never assume (John, 2026-09-03: "you have to tool your bridge+    code to definitively know that the correct thing is showing"). Four facts+    per beat, each read from the desktop:+      front   : AD's z-order says our hwnd is the top window+      title   : the window's live title carries the document we expect+      painted : the GL canvas is not a uniform blank (kicad_ui canvas probe)+      stable  : two captures 0.8 s apart are byte-identical (rendering settled)+    One extra raise attempt if it is not in front. Motion and the frame are+    gated on the result; the result rides on the step so nobody has to guess."""+    import time as _t, hashlib as _h+    v = {"hwnd": hwnd, "expected": expected_title, "front": False, "title": False,+         "painted": None, "stable": None, "frontTitle": None, "attempts": 0}+    if not hwnd:+        v["why"] = "no window handle for this beat"+        return v+    try:+        from handlers import kicad_windows, win_focus, ad_client+        from handlers.kicad_ui import _canvas_probe, _screenshot_hwnd+    except Exception as e:  # pylint: disable=broad-except+        v["why"] = f"helpers unavailable: {e}"+        return v+    for attempt in range(tries):+        v["attempts"] = attempt + 1+        rows = [r for r in (kicad_windows.find(all_windows=True, fresh=True) or []) if r.get("z") is not None]+        top = sorted(rows, key=lambda r: r["z"])[0] if rows else {}+        v["frontTitle"] = top.get("title")+        v["front"] = int(top.get("hwnd") or 0) == int(hwnd)+        mine = next((r for r in rows if int(r.get("hwnd") or 0) == int(hwnd)), None)+        live_title = (mine or {}).get("title") or ""+        v["liveTitle"] = live_title+        v["title"] = expected_title.lower() in live_title.lower() if expected_title else bool(live_title)+        if v["front"]:+            break+        try:                                     # one more ask, through AD, then measure again+            win_focus.allow_activation(int(hwnd))+            ad_client.call("desktop_bring_to_front", {"hwnd": int(hwnd),+                           "reason": f"KiCad tour: bring the {expected_title or 'beat'} window forward, once"}, timeout=8)+        except Exception:  # pylint: disable=broad-except+            pass+        _t.sleep(0.6)+    try:+        probe = _canvas_probe(int(hwnd)) or {}+        v["painted"] = bool(probe.get("rendered")) if probe.get("checked") else None+        v["canvas"] = {k: probe.get(k) for k in ("checked", "rendered", "distinctColors", "region") if k in probe}+    except Exception:  # pylint: disable=broad-except+        v["painted"] = None+    try:+        def _digest():+            r = _screenshot_hwnd(int(hwnd), label="verify", max_dim=480)+            pth = r.get("fullPath") or r.get("savedTo")+            return _h.sha1(open(pth, "rb").read()).hexdigest() if pth else None+        a = _digest(); _t.sleep(0.8); b = _digest()+        v["stable"] = bool(a and b and a == b)+        if not v["stable"]:+            _t.sleep(1.2); c = _digest(); v["stable"] = bool(b and c and b == c)+    except Exception:  # pylint: disable=broad-except+        v["stable"] = None+    v["ok"] = bool(v["front"] and v["title"] and v["painted"] is not False)+    return v++ def _foreground_once(window_title: str, hwnd: int | None = None) -> bool:     """Bring the window this beat opened to the front ONCE, then hands off. @@ -1321,15 +1383,23 @@ def _run_demo_background(kicad_info: dict) -> None:             if r.get("ok"):                 r["foregrounded"] = _foreground_once(r.get("window") or r.get("windowTitle") or "",                                                      hwnd=r.get("hwnd"))-                # Motion for the recording (John): only while OUR window is in-                # front, and it stops at the next gesture on STOP or a switch away.-                try:-                    from handlers import demo_motion-                    kind = "3d" if s_ in ("footprint3d", "board3d") else "2d"-                    target = r.get("hwnd") or _hwnd_for_title(r.get("window") or "")-                    r["motion"] = demo_motion.choreograph(target, kind, abort=lambda: _EXIT["now"])-                except Exception as e:  # pylint: disable=broad-except-                    r["motion"] = {"error": f"{type(e).__name__}: {e}"}+                # MEASURE before moving: the right window, in front, painted, settled.+                target = r.get("hwnd") or _hwnd_for_title(r.get("window") or "")+                r["verified"] = _verify_beat(target, r.get("window") or "")+                _job_update(verified=r["verified"])+                if not r["verified"].get("ok"):+                    r["motion"] = {"skipped": f"not verified: front={r['verified'].get('front')} "+                                              f"title={r['verified'].get('title')} painted={r['verified'].get('painted')} "+                                              f"(top window: {r['verified'].get('frontTitle')})"}+                else:+                    # Motion for the recording (John): only on a verified window, and it+                    # stops at the next gesture on STOP or a switch away.+                    try:+                        from handlers import demo_motion+                        kind = "3d" if s_ in ("footprint3d", "board3d") else "2d"+                        r["motion"] = demo_motion.choreograph(target, kind, abort=lambda: _EXIT["now"])+                    except Exception as e:  # pylint: disable=broad-except+                        r["motion"] = {"error": f"{type(e).__name__}: {e}"}             if _EXIT["now"]:                 exited = True                 break@@ -1393,6 +1463,7 @@ def _run_demo_background(kicad_info: dict) -> None:                                    "foregrounded": r.get("foregrounded"),                                    "motion": r.get("motion"),                                    "hwnd": r.get("hwnd"), "window": r.get("window"),+                                   "verified": r.get("verified"),                                    "failReason": (None if r.get("ok") else {                                        "handlerSuccess": bool((r.get("raw") or {}).get("success")),                                        "error": str((r.get("raw") or {}).get("error") or "")[:200],@@ -1442,7 +1513,7 @@ def handle_demo(kicad_info: dict, args: dict) -> dict:         out = {"success": True, **{k: snap.get(k) for k in                ("done", "step", "stepLabel", "beatIndex", "beatCount",                 "percent", "elapsedSec", "estimatedSec", "error",-                "panel", "muted", "muteSource", "lastPress", "paused", "stopping")}}+                "panel", "muted", "muteSource", "lastPress", "paused", "stopping", "verified")}}         # 0.9.199 (wiki issue #39, John): `active` used to be a BOOLEAN here and         # an ARRAY in kicad_progress - the same key, two types, from one bridge.         # A renderer written as `if (resp.active)` then treats a finished job as