← Commit history

0.9.297: Tour: motion on every beat for the recording (fit, wheel zoom in/out, middle-drag pans in the 2D editors; Home, an isometric left-drag and three orbits with zoom in the 3D viewers), sent only while the beat's window is confirmed in front and aborted at the next gesture on STOP or when the user switches away. The board-in-3D beat now closes the chip's 3D viewer first and picks the viewer owned by the PCB Editor (the last beat was showing the chip's viewer by title match). STOP from the remote's X now clears the caption and ends the tour within the beat, audio already off. Per-beat motion reports ride on the result.

John Lauer ·3bd8e4c603 ·1mo ago ·parent ce1759c
3 files changed +95−13
BRIDGE_VERSION+1−1
@@ -1 +1 @@-0.9.296\ No newline at end of file+0.9.297\ 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.296",+  "version": "0.9.297",   "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+93−11
@@ -505,7 +505,9 @@ def _run_step(kicad_info: dict, step: str, prep: dict) -> dict:     if step == "footprint3d":         import time; time.sleep(2)         r = handle_open_3d_viewer(kicad_info, {"editor": "footprint"})+        time.sleep(2)         return {"ok": bool(r.get("success")), "window": "3D Viewer",+                "hwnd": _viewer_3d_owned_by("Footprint Editor"),                 "title": "3/6 · The chip in 3D",                 "say": ("And the same chip in three dimensions" +                         (", the LQFP-48 body from KiCad's own 3D library sitting on its pads."@@ -547,8 +549,12 @@ def _run_step(kicad_info: dict, step: str, prep: dict) -> dict:         except Exception:             pass         time.sleep(2)+        _close_hwnd(_viewer_3d_owned_by("Footprint Editor"))   # the chip's viewer must not win the title match+        time.sleep(1)         r = handle_open_3d_viewer(kicad_info, {"editor": "pcb"})-        return {"ok": bool(r.get("success")), "window": "3D Viewer",+        time.sleep(3)+        v = _viewer_3d_owned_by("PCB Editor")+        return {"ok": bool(r.get("success")) and bool(v), "window": "3D Viewer", "hwnd": v,                 "title": "6/6 · The board in 3D",                 "say": ("And the finished ESC in 3D. This is the payoff: from one symbol to a rendered "                         "assembly, every step driven on your own machine, and each window brought to "@@ -585,7 +591,7 @@ def _job_update(**kw):         _JOB.update(kw)  -def _foreground_once(window_title: str) -> bool:+def _foreground_once(window_title: str, hwnd: int | None = None) -> bool:     """Bring the window this beat opened to the front ONCE, then hands off.      John, 2026-09-03: the first version called SetForegroundWindow from the@@ -602,10 +608,11 @@ def _foreground_once(window_title: str) -> bool:     try:         from handlers.kicad_ui import _find_all_kicad_windows         from handlers import win_focus, ad_client, kicad_windows-        target = None-        for w in _find_all_kicad_windows():-            if window_title.lower() in (w.get("title") or "").lower():-                target = int(w["hwnd"]); break+        target = int(hwnd) if hwnd else None+        if not target:+            for w in _find_all_kicad_windows():+                if window_title.lower() in (w.get("title") or "").lower():+                    target = int(w["hwnd"]); break         if not target:             return False         try:@@ -638,7 +645,46 @@ def _foreground_once(window_title: str) -> bool:   -def _beat_shot(kicad_info: dict, step_label: str, window_title: str) -> dict | None:+def _hwnd_for_title(window_title: str) -> int | None:+    try:+        from handlers.kicad_ui import _find_all_kicad_windows+        for w in _find_all_kicad_windows():+            if window_title.lower() in (w.get("title") or "").lower():+                return int(w["hwnd"])+    except Exception:  # pylint: disable=broad-except+        pass+    return None+++def _viewer_3d_owned_by(editor_title: str) -> int | None:+    """The '3D Viewer' whose OWNER is the given editor frame (Footprint+    Editor vs PCB Editor). John, 2026-09-03: the last beat showed the chip's+    viewer instead of the board's because both are titled '3D Viewer' and the+    first title match won."""+    try:+        from handlers.kicad_ui import _find_all_kicad_windows+        rows = _find_all_kicad_windows()+        owners = [int(w["hwnd"]) for w in rows if editor_title.lower() in (w.get("title") or "").lower()]+        for w in rows:+            if "3d viewer" in (w.get("title") or "").lower() and int(w.get("ownerHwnd") or 0) in owners:+                return int(w["hwnd"])+    except Exception:  # pylint: disable=broad-except+        pass+    return None+++def _close_hwnd(hwnd: int | None) -> None:+    if not hwnd:+        return+    try:+        from handlers import ad_client+        ad_client.call("desktop_close_window", {"hwnd": int(hwnd),+                                                "reason": "KiCad tour: close the chip's 3D viewer before showing the board's"}, timeout=10)+    except Exception:  # pylint: disable=broad-except+        pass+++def _beat_shot(kicad_info: dict, step_label: str, window_title: str, hwnd: int | None = None) -> dict | None:     """The per-step EVIDENCE frame for the web-control page (#25, John 2026-08-16:     the page's own guess-snaps race the tour and shoot the wrong window — only     the bridge knows the moment a step completes and which window is its@@ -649,7 +695,7 @@ def _beat_shot(kicad_info: dict, step_label: str, window_title: str) -> dict | N         import base64 as _b64         from handlers.kicad_ui import _find_all_kicad_windows, _screenshot_hwnd         for w in _find_all_kicad_windows():-            if window_title.lower() in (w.get("title") or "").lower():+            if (hwnd and int(w["hwnd"]) == int(hwnd)) or (not hwnd and window_title.lower() in (w.get("title") or "").lower()):                 shot = _screenshot_hwnd(w["hwnd"], label="stepshot", max_dim=640)                 p = shot.get("fullPath") or shot.get("savedTo")                 if shot.get("success") and p and p.lower().endswith(".png"):@@ -898,6 +944,13 @@ def on_panel_event(ev: dict) -> dict:         return {"ignored": b}     if cmd in ("pause", "exit"):         _stop_narration()           # the user's hand: silence within the beat+    if cmd == "exit":+        # John, 2026-09-03: on the panel's X "exit immediately, remove the+        # captions and stop the audio". Audio is already dead; clear the+        # caption now; the beat loop, the narration wait and the motion all+        # poll _EXIT and bail at their next gesture.+        _EXIT["now"] = True+        _caption_clear()     set_demo_control(cmd)     _job_update(lastPress={"button": b, "at": at, "via": "callback"})     return {"queued": cmd}@@ -987,7 +1040,18 @@ def _drain_ctrl() -> str | None:     return None  +def _caption_clear() -> None:+    try:+        from handlers import ad_client+        ad_client.call("desktop_caption", {"action": "hide", "id": _CAPTION_ID,+                                           "reason": "The user stopped the KiCad tour; clear its caption"}, timeout=6)+    except Exception:  # pylint: disable=broad-except+        pass++ def _caption(text: str, duration_ms: int = 60000) -> None:+    if _EXIT["now"]:+        return     """Paint/replace the demo caption on the user's screen. Best-effort: a     caption must never be able to fail a beat."""     try:@@ -1025,6 +1089,7 @@ def _run_demo_background(kicad_info: dict) -> None:     # (it would fight that single raise), and no push-to-back of the demo's     # windows. Every other verb keeps the background contract.     done_weight = 0.0+    _EXIT["now"] = False     try:         _panel_show(0, "playing")         _caption(_BEAT_CAPTIONS["prepare"], duration_ms=_BEAT_CAPTION_MS["prepare"])@@ -1105,10 +1170,26 @@ def _run_demo_background(kicad_info: dict) -> None:                       file=_sys.stderr)             r["step"] = s_             results.append(r)+            if _EXIT["now"]:+                exited = True+                break             if r.get("ok"):-                r["foregrounded"] = _foreground_once(r.get("window") or r.get("windowTitle") or "")+                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}"}+            if _EXIT["now"]:+                exited = True+                break             shot = _beat_shot(kicad_info, f"Opening {s_.replace('_', ' ')}",-                              r.get("window") or r.get("windowTitle") or "")+                              r.get("window") or r.get("windowTitle") or "", hwnd=r.get("hwnd"))             if shot:                 with _JOB_LOCK:                     _JOB.setdefault("pendingShots", []).append(shot)@@ -1120,7 +1201,7 @@ def _run_demo_background(kicad_info: dict) -> None:                 if not pr or pr.poll() is not None:                     break                 cmd = _drain_ctrl()-                if cmd == "exit":+                if cmd == "exit" or _EXIT["now"]:                     exited = True                     break                 if cmd:@@ -1165,6 +1246,7 @@ def _run_demo_background(kicad_info: dict) -> None:                         "steps": [{"step": r["step"], "title": r.get("title"),                                    "ok": r.get("ok"), "pointOut": r.get("pointOut"),                                    "foregrounded": r.get("foregrounded"),+                                   "motion": r.get("motion"),                                    "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],