← Commit history

0.9.340: land the shipped runtime source for handlers/kicad_ui.py (tour fixes 0.9.318-0.9.339 never reached master, issue #79)

John Lauer ·6745e30a88 ·1mo ago ·parent a7aebbf
1 file changed +78
handlers/kicad_ui.py+78
@@ -573,6 +573,84 @@ def _canvas_probe(hwnd: int) -> dict | None:         return None  +def _canvas_content(hwnd: int) -> dict | None:+    """Where the DRAWN content sits on the GL canvas, in SCREEN pixels.++    Same GDI grab as _canvas_probe. Background = the most common sampled colour+    on the canvas; content = samples that differ from it by more than a small+    tolerance. Returns {canvas:(l,t,r,b), bbox:(l,t,r,b)|None, coverage:0..1,+    touches:[edges]} so a demo can aim its zoom at the part it is talking about+    and notice when a pan or a rotation pushed the content off the view+    (John, 2026-09-05: "don't be dumb about your zooming or panning")."""+    try:+        rect = _get_window_rect(hwnd)+        width, height = rect["width"], rect["height"]+        if width <= 0 or height <= 0:+            return None+        hdc_screen = user32.GetDC(hwnd)+        hdc_mem = gdi32.CreateCompatibleDC(hdc_screen)+        hbmp = gdi32.CreateCompatibleBitmap(hdc_screen, width, height)+        old_bmp = gdi32.SelectObject(hdc_mem, hbmp)+        user32.PrintWindow(hwnd, hdc_mem, PW_RENDERFULLCONTENT)+        class _BIH(ctypes.Structure):+            _fields_ = [("biSize", ctypes.wintypes.DWORD), ("biWidth", ctypes.wintypes.LONG),+                        ("biHeight", ctypes.wintypes.LONG), ("biPlanes", ctypes.wintypes.WORD),+                        ("biBitCount", ctypes.wintypes.WORD), ("biCompression", ctypes.wintypes.DWORD),+                        ("biSizeImage", ctypes.wintypes.DWORD), ("biXPelsPerMeter", ctypes.wintypes.LONG),+                        ("biYPelsPerMeter", ctypes.wintypes.LONG), ("biClrUsed", ctypes.wintypes.DWORD),+                        ("biClrImportant", ctypes.wintypes.DWORD)]+        bmi = _BIH(); bmi.biSize = ctypes.sizeof(_BIH)+        bmi.biWidth, bmi.biHeight, bmi.biPlanes = width, -height, 1     # top-down rows+        bmi.biBitCount, bmi.biCompression = 32, BI_RGB+        pixel_buf = ctypes.create_string_buffer(width * height * 4)+        gdi32.GetDIBits(hdc_mem, hbmp, 0, height, pixel_buf, ctypes.byref(bmi), DIB_RGB_COLORS)+        buf = pixel_buf.raw+        gdi32.SelectObject(hdc_mem, old_bmp); gdi32.DeleteObject(hbmp); gdi32.DeleteDC(hdc_mem)+        user32.ReleaseDC(hwnd, hdc_screen)+        gl = _gl_canvas_rect(hwnd)+        if not gl:+            return None+        wl, wt = rect.get("x", rect.get("left", 0)), rect.get("y", rect.get("top", 0))+        l, t, r, b = gl                                  # relative to the window box+        ins_x, ins_y = int((r - l) * 0.03), int((b - t) * 0.03)+        x0, x1, y0, y1 = max(0, l + ins_x), min(width, r - ins_x), max(0, t + ins_y), min(height, b - ins_y)+        if x1 <= x0 or y1 <= y0:+            return None+        n = 140+        sx, sy = max(1, (x1 - x0) // n), max(1, (y1 - y0) // n)+        samples = []+        for y in range(y0, y1, sy):+            row = y * width * 4+            for x in range(x0, x1, sx):+                o = row + x * 4+                samples.append((x, y, buf[o], buf[o + 1], buf[o + 2]))+        if not samples:+            return None+        from collections import Counter+        q = Counter(((bb >> 4, g >> 4, rr >> 4) for _, _, bb, g, rr in samples))+        bg = q.most_common(1)[0][0]+        def is_bg(bb, g, rr):+            return abs((bb >> 4) - bg[0]) <= 1 and abs((g >> 4) - bg[1]) <= 1 and abs((rr >> 4) - bg[2]) <= 1+        # grid dots are sparse single samples: ignore samples with no content neighbour+        cont = [(x, y) for x, y, bb, g, rr in samples if not is_bg(bb, g, rr)]+        cov = len(cont) / len(samples)+        if not cont:+            return {"canvas": (wl + l, wt + t, wl + r, wt + b), "bbox": None, "coverage": 0.0, "touches": []}+        xs = sorted(x for x, _ in cont); ys = sorted(y for _, y in cont)+        # trim 2% outliers (stray grid/axis pixels) before taking the box+        k = max(0, int(len(xs) * 0.02))+        bx0, bx1, by0, by1 = xs[k], xs[-1 - k], ys[k], ys[-1 - k]+        touches = []+        if bx0 <= x0 + 2 * sx: touches.append("left")+        if bx1 >= x1 - 2 * sx: touches.append("right")+        if by0 <= y0 + 2 * sy: touches.append("top")+        if by1 >= y1 - 2 * sy: touches.append("bottom")+        return {"canvas": (wl + l, wt + t, wl + r, wt + b),+                "bbox": (wl + bx0, wt + by0, wl + bx1, wt + by1), "coverage": round(cov, 3), "touches": touches}+    except Exception:  # pylint: disable=broad-except+        return None++ def _screenshot_hwnd(hwnd: int, label: str = "", max_dim: int = 1568) -> dict:     """Capture a window by HWND. AD core first, local PrintWindow as fallback.