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
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.
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)