- KiCad project generated by pcb/tools (design, route, schematic, fab outputs). - v0.4 fixes the v0.3 review findings: low-dropout 3.3 V regulator, OR'd 5 V supply, polyfuse, USB ESD protection, input clamps, ceramic decoupling at the drivers, A4988/DRV8825 jumper, ground stitching. DRC/ERC clean. - v0.3 (as ordered) kept in pcb/archive/v0.3. - FluidNC config.yaml for the board; rho hard limit on. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
214 lines
6.6 KiB
Python
214 lines
6.6 KiB
Python
"""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]
|