"""Ground pour: raster fill on the router grid -> fractured polygons for KiCad. Cells are squares of side G centred on grid points. A cell may be filled when no foreign copper is within the pour clearance + half the cell diagonal; that radius map already exists in the router (radius for a 0.2 mm track = 0.38 mm). """ from collections import deque import router as R def fill_mask(rt, net, layer, r_map, open_cells=2): k = rt.nid(net) nx, ny = rt.nx, rt.ny m = rt.maps[r_map][layer] mask = bytearray(nx * ny) for i in range(nx * ny): v = m[i] if v == -1 or v == k: mask[i] = 1 # morphological opening (erode then dilate, 4-neighbourhood) removes slivers for _ in range(open_cells): mask = _erode(mask, nx, ny) for _ in range(open_cells): mask = _dilate(mask, nx, ny, limit=m, k=k) return mask def _erode(mask, nx, ny): out = bytearray(len(mask)) for iy in range(1, ny - 1): b = iy * nx for ix in range(1, nx - 1): i = b + ix if mask[i] and mask[i - 1] and mask[i + 1] and mask[i - nx] and mask[i + nx]: out[i] = 1 return out def _dilate(mask, nx, ny, limit, k): out = bytearray(mask) for iy in range(1, ny - 1): b = iy * nx for ix in range(1, nx - 1): i = b + ix if not mask[i] and (mask[i - 1] or mask[i + 1] or mask[i - nx] or mask[i + nx]): v = limit[i] if v == -1 or v == k: out[i] = 1 return out def keep_connected(mask, nx, ny, anchors): """Keep only 4-connected components that contain an anchor cell index.""" keep = bytearray(len(mask)) q = deque(i for i in anchors if mask[i]) for i in q: keep[i] = 1 while q: i = q.popleft() for j in (i - 1, i + 1, i - nx, i + nx): if 0 <= j < len(mask) and mask[j] and not keep[j]: keep[j] = 1 q.append(j) return keep def contours(mask, nx, ny): """Boundary loops of the union of filled cells, in corner coordinates (cx, cy) where cell (ix, iy) spans corners (ix..ix+1, iy..iy+1). Filled area on the left.""" edges = {} def add(a, b): edges.setdefault(a, []).append(b) for iy in range(ny): b = iy * nx for ix in range(nx): if not mask[b + ix]: continue # neighbours (outside = empty) up = iy > 0 and mask[b + ix - nx] dn = iy < ny - 1 and mask[b + ix + nx] lf = ix > 0 and mask[b + ix - 1] rt_ = ix < nx - 1 and mask[b + ix + 1] # traverse cell boundary counter-clockwise in screen coords (y down): filled on left if not up: add((ix + 1, iy), (ix, iy)) if not lf: add((ix, iy), (ix, iy + 1)) if not dn: add((ix, iy + 1), (ix + 1, iy + 1)) if not rt_: add((ix + 1, iy + 1), (ix + 1, iy)) loops = [] while edges: start = next(iter(edges)) loop = [start] cur = start prev_dir = None while True: outs = edges[cur] if len(outs) == 1: nxt = outs.pop() else: # pinch point: prefer the turn that keeps the filled region on the left (turn left first) best = None for cand in outs: d = (cand[0] - cur[0], cand[1] - cur[1]) if prev_dir is None: best = cand break # left turn in y-down coords: (dx,dy) -> (dy,-dx) if d == (prev_dir[1], -prev_dir[0]): best = cand break if best is None: best = outs[0] outs.remove(best) nxt = best if not outs: del edges[cur] prev_dir = (nxt[0] - cur[0], nxt[1] - cur[1]) cur = nxt if cur == start: break loop.append(cur) loops.append(_simplify(loop)) return loops def _simplify(loop): out = [] n = len(loop) for i in range(n): a, b, c = loop[i - 1], loop[i], loop[(i + 1) % n] if (b[0] - a[0]) * (c[1] - b[1]) - (b[1] - a[1]) * (c[0] - b[0]) != 0: out.append(b) return out def area2(loop): s = 0 for i in range(len(loop)): x1, y1 = loop[i - 1] x2, y2 = loop[i] s += x1 * y2 - x2 * y1 return s def point_in(loop, x, y): inside = False n = len(loop) for i in range(n): x1, y1 = loop[i - 1] x2, y2 = loop[i] if (y1 > y) != (y2 > y): xi = x1 + (y - y1) * (x2 - x1) / (y2 - y1) if xi > x: inside = not inside return inside def fracture(outer, holes): """Merge holes into the outer ring with zero-width bridges (KiCad's fractured form). Rectilinear integer loops. Holes are bridged rightwards to the nearest vertical edge.""" ring = list(outer) for hole in sorted(holes, key=lambda h: -max(p[0] for p in h)): xh = max(p[0] for p in hole) vi = min((i for i in range(len(hole)) if hole[i][0] == xh), key=lambda i: hole[i][1]) V = hole[vi] best = None n = len(ring) for i in range(n): A, B = ring[i], ring[(i + 1) % n] if A[0] == B[0] and A[0] > V[0] and min(A[1], B[1]) <= V[1] <= max(A[1], B[1]): if best is None or A[0] < best[0]: best = (A[0], i) if best is None: continue x, i = best P = (x, V[1]) seq = hole[vi:] + hole[:vi] ring = ring[:i + 1] + [P] + seq + [V, P] + ring[i + 1:] out = [] for p in ring: if not out or out[-1] != p: out.append(p) if len(out) > 1 and out[0] == out[-1]: out.pop() return out def polygons(mask, nx, ny): loops = contours(mask, nx, ny) outers, holes = [], [] for lp in loops: if len(lp) < 4: continue (outers if area2(lp) < 0 else holes).append(lp) result = [] owner = {} for h in holes: x, y = h[0][0] + 1e-3, h[0][1] + 1e-3 cands = [o for o in outers if point_in(o, x, y)] if cands: owner[id(h)] = min(cands, key=lambda o: abs(area2(o))) for o in outers: hs = [h for h in holes if owner.get(id(h)) is o] result.append(fracture(o, hs)) return result def to_mm(poly, G): return [((cx - 0.5) * G, (cy - 0.5) * G) for (cx, cy) in poly]