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.326: Tour: all editor and viewer windows at 95 percent of the work area, centered; 2D motion aims at the featured part (position parsed from the board and schematic) and checks the canvas after each gesture; 3D motion re-frames when the board is clipped or tiny
1 file changed
+46−10
handlers/demo_motion.py+46−10@@ -146,7 +146,7 @@ def _menu_ids(hwnd: int) -> dict: return ids -def choreograph_3d_menu(hwnd: int, abort=None, log=None) -> dict | None:+def choreograph_3d_menu(hwnd: int, abort=None, log=None, measure=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@@ -180,10 +180,26 @@ def choreograph_3d_menu(hwnd: int, abort=None, log=None) -> dict | None: if log: log(f"motion 3d(menu): {name}") time.sleep(0.4)+ # Framing check (John, 2026-09-05): after every step look at the canvas.+ # Board clipped on two opposite edges -> one zoom-out; board tiny -> one+ # zoom-in. Recorded so the run says what it corrected.+ if measure and name not in ("extents",):+ try:+ m = measure() or {}+ t = set(m.get("touches") or [])+ if ({"left", "right"} <= t or {"top", "bottom"} <= t) and "zoom_out" in ids:+ win_menu.invoke_menu_command(hwnd, ids["zoom_out"]); time.sleep(0.5)+ rep.setdefault("framed", []).append(f"{name}: clipped {sorted(t)} -> zoom out")+ elif (m.get("coverage") or 0) < 0.10 and m.get("bbox") and "zoom_in" in ids:+ win_menu.invoke_menu_command(hwnd, ids["zoom_in"]); time.sleep(0.5)+ rep.setdefault("framed", []).append(f"{name}: coverage {m.get('coverage')} -> zoom in")+ rep["lastCoverage"] = m.get("coverage")+ except Exception: # pylint: disable=broad-except+ pass return rep -def choreograph(hwnd: int, kind: str, abort=None, log=None, check=None) -> dict:+def choreograph(hwnd: int, kind: str, abort=None, log=None, check=None, measure=None, focus=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().""" rep = {"kind": kind, "gestures": 0, "aborted": None}@@ -207,7 +223,7 @@ def choreograph(hwnd: int, kind: str, abort=None, log=None, check=None) -> dict: 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)+ menu_rep = choreograph_3d_menu(hwnd, abort=abort, log=log, measure=measure) if menu_rep is not None: return menu_rep except Exception as e: # pylint: disable=broad-except@@ -236,13 +252,33 @@ def choreograph(hwnd: int, kind: str, abort=None, log=None, check=None) -> dict: # smart about how close". Extents first, two wheel clicks in (about # 1.7x), a short pan, back out, and END on extents so the frame the # user is left with is the whole thing.+ # John, 2026-09-05: "be smart about your zooming/panning cuz you still+ # don't end up showing important stuff". Aim at the featured part: after+ # extents, measure where the drawing sits on the canvas and map the+ # part's fraction (from the design files) onto it; wheel-zoom AT that+ # point (KiCad zooms about the cursor), pan it to the centre, hold,+ # zoom back out, extents.+ aim = {"x": cx, "y": cy}+ def _aim():+ key(VK_HOME); time.sleep(0.7)+ if measure and focus:+ try:+ m = measure() or {}+ bb = m.get("bbox")+ if bb:+ aim["x"] = int(bb[0] + float(focus[0]) * (bb[2] - bb[0]))+ aim["y"] = int(bb[1] + float(focus[1]) * (bb[3] - bb[1]))+ rep["aim"] = {"x": aim["x"], "y": aim["y"], "bbox": bb, "coverage": m.get("coverage")}+ except Exception: # pylint: disable=broad-except+ pass plan = [- ("extents", lambda: key(VK_HOME)),- ("zoom-in", lambda: wheel(2, cx, cy)),- ("pan-right", lambda: drag("middle", cx, cy, cx - dx, cy, steps=16)),- ("pan-down", lambda: drag("middle", cx, cy, cx, cy - dy, steps=16)),- ("pan-back", lambda: drag("middle", cx, cy, cx + dx, cy + dy, steps=16)),- ("zoom-out", lambda: wheel(-2, cx, cy)),+ ("extents", _aim),+ ("zoom-in", lambda: wheel(2, aim["x"], aim["y"])),+ ("centre", lambda: drag("middle", aim["x"], aim["y"], cx, cy, steps=18)),+ ("zoom-in-2", lambda: wheel(1, cx, cy)),+ ("pan-look", lambda: drag("middle", cx, cy, cx - dx, cy - dy, steps=14)),+ ("pan-back", lambda: drag("middle", cx - dx, cy - dy, cx, cy, steps=14)),+ ("zoom-out", lambda: wheel(-3, cx, cy)), ("extents-2", lambda: key(VK_HOME)), ] for name, fn in plan:@@ -257,7 +293,7 @@ def choreograph(hwnd: int, kind: str, abort=None, log=None, check=None) -> dict: # that content goes off the screen ... analyze your motion". After # every 2D gesture that can empty the view, look at the canvas; a # blank one means we panned off the content: recenter on extents.- if check and kind != "3d" and name.startswith(("pan", "zoom-in")):+ if check and kind != "3d" and name.startswith(("pan", "zoom-in", "centre")): try: if not check(): key(VK_HOME); time.sleep(0.45)