← Commit history

0.9.306: Tour 3D motion is now deterministic: KiCad's own View menu commands (Rotate X/Z by 10 degrees, Zoom In/Out, Zoom to Fit) resolved from the live menu bar and posted as WM_COMMAND, so the isometric and the orbit are the same on every DPI and need no mouse or foreground; mouse drags remain only as the fallback (they flipped the chip viewer and did nothing in the board viewer on 2026-09-03). Each step in the result now carries the window handle and title it drove.

John Lauer ·85b01dbd6f ·1mo ago ·parent 1f041d8
1 file changed +69
handlers/demo_motion.py+69
@@ -120,6 +120,67 @@ def _canvas_center(hwnd: int):     return int(l + (r - l) * 0.62), int(t + (b - t) * 0.55), (r - l), (b - t)  +def _menu_ids(hwnd: int) -> dict:+    """The 3D viewer's View-menu command ids, resolved from the live menu bar+    (labels are stable across KiCad 8/9/10; ids are not). Empty when the walk+    fails (then the caller falls back to mouse drags)."""+    try:+        from handlers import win_menu+    except Exception:  # pylint: disable=broad-except+        return {}+    ids = {}+    for key, words in {"fit": ("zoom", "fit"), "zoom_in": ("zoom", "in"), "zoom_out": ("zoom", "out"),+                       "rotx_cw": ("rotate x", "clockwise"), "rotx_ccw": ("rotate x", "counter"),+                       "rotz_cw": ("rotate z", "clockwise"), "rotz_ccw": ("rotate z", "counter")}.items():+        try:+            cid, _label = win_menu.find_menu_command(hwnd, *words)+            if cid:+                ids[key] = int(cid)+        except Exception:  # pylint: disable=broad-except+            pass+    # "zoom in" also matches "Zoom In" only if "counter" did not swallow "clockwise" ids: guard the pairs+    if ids.get("rotx_cw") and ids.get("rotx_cw") == ids.get("rotx_ccw"):+        ids.pop("rotx_ccw", None)+    if ids.get("rotz_cw") and ids.get("rotz_cw") == ids.get("rotz_ccw"):+        ids.pop("rotz_ccw", None)+    return ids+++def choreograph_3d_menu(hwnd: int, abort=None, log=None) -> dict | None:+    """Deterministic 3D motion through KiCad's own View menu (WM_COMMAND, 10+    degrees per rotate, no mouse, no foreground needed): mouse drags over-rotated+    the chip viewer and did nothing in the board viewer on 2026-09-03 because+    drag sensitivity and the window under the cursor are not ours to control.+    Returns None when the menu could not be resolved (caller falls back)."""+    from handlers import win_menu+    ids = _menu_ids(hwnd)+    if not all(k in ids for k in ("rotx_cw", "rotz_cw", "zoom_in", "zoom_out")):+        return None+    rep = {"kind": "3d", "method": "menu", "gestures": 0, "aborted": None, "ids": ids}+    plan = ([("fit", "fit", 1, 0.6)] if "fit" in ids else []) + [+        ("tilt", "rotx_cw", 3, 0.28),          # ~30 degrees toward isometric+        ("turn", "rotz_cw", 4, 0.28),          # ~40 degrees of yaw+        ("zoom-in", "zoom_in", 2, 0.35),+        ("orbit", "rotz_cw", 6, 0.32),         # a slow 60 degree orbit+        ("zoom-out", "zoom_out", 2, 0.35),+        ("orbit-back", "rotz_ccw" if "rotz_ccw" in ids else "rotz_cw", 4, 0.32),+    ]+    for name, key, reps, dt in plan:+        for _ in range(reps):+            if abort and abort():+                rep["aborted"] = f"stop requested during {name}"+                return rep+            if not win_menu.invoke_menu_command(hwnd, ids[key]):+                rep["aborted"] = f"menu command refused during {name}"+                return rep+            rep["gestures"] += 1+            time.sleep(dt)+        if log:+            log(f"motion 3d(menu): {name}")+        time.sleep(0.4)+    return rep++ def choreograph(hwnd: int, kind: str, abort=None, log=None) -> dict:     """Run the beat's motion on `hwnd`. kind: '2d' | '3d'. Returns a report;     never raises. Stops the instant the window is not in front or abort()."""@@ -135,6 +196,14 @@ def choreograph(hwnd: int, kind: str, abort=None, log=None) -> dict:             rep["aborted"] = f"window not in front before {name} (user switched away)"; return False         return True +    if kind == "3d":+        try:+            time.sleep(1.2)                    # let the STEP bodies load before the first turn+            menu_rep = choreograph_3d_menu(hwnd, abort=abort, log=log)+            if menu_rep is not None:+                return menu_rep+        except Exception as e:  # pylint: disable=broad-except+            rep["menuError"] = f"{type(e).__name__}: {e}"     try:         cx, cy, w, h = _canvas_center(hwnd)         dx, dy = int(w * 0.10), int(h * 0.08)