molecule
AM/FM Receiver Molecule
Public Made by Adomby adom
USB-powered AM/FM receiver on an Adom Molecule: discrete BJT and op-amp superhet (10.7 MHz and 455 kHz IFs), STM32G0B1 tuning, self-calibration and USB audio over one USB-C cable. 96 x 96 mm pin grid, 4 layers, KiCad 10.
main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496
"""Copper-geometry helpers: clearance-checked via fanout, GND stitching, silkscreen reference placement."""
import math
import pcbnew
def mm(v):
return pcbnew.FromMM(v)
def tomm(v):
return pcbnew.ToMM(v)
def rect_dist(px, py, r):
"""Distance from point to axis-aligned rect (0 if inside)."""
x0, y0, x1, y1 = r
dx = max(x0 - px, 0.0, px - x1)
dy = max(y0 - py, 0.0, py - y1)
return math.hypot(dx, dy)
def seg_point_dist(ax, ay, bx, by, px, py):
vx, vy = bx - ax, by - ay
L2 = vx * vx + vy * vy
t = 0.0 if L2 == 0 else max(0.0, min(1.0, ((px - ax) * vx + (py - ay) * vy) / L2))
cx, cy = ax + t * vx, ay + t * vy
return math.hypot(px - cx, py - cy)
def seg_rect_dist(ax, ay, bx, by, r, steps=None):
L = math.hypot(bx - ax, by - ay)
n = max(2, int(L / 0.05) + 1) if steps is None else steps
best = 1e9
for i in range(n + 1):
t = i / n
d = rect_dist(ax + t * (bx - ax), ay + t * (by - ay), r)
if d < best:
best = d
return best
def seg_seg_dist(a, b, c, d):
return min(seg_point_dist(a[0], a[1], b[0], b[1], c[0], c[1]),
seg_point_dist(a[0], a[1], b[0], b[1], d[0], d[1]),
seg_point_dist(c[0], c[1], d[0], d[1], a[0], a[1]),
seg_point_dist(c[0], c[1], d[0], d[1], b[0], b[1])) if not segs_cross(a, b, c, d) else 0.0
def segs_cross(a, b, c, d):
def orient(p, q, r):
return (q[0] - p[0]) * (r[1] - p[1]) - (q[1] - p[1]) * (r[0] - p[0])
o1, o2, o3, o4 = orient(a, b, c), orient(a, b, d), orient(c, d, a), orient(c, d, b)
return (o1 * o2 < 0) and (o3 * o4 < 0)
class CopperMap:
"""Snapshot of copper on the outer layers for clearance queries (mm units)."""
def __init__(self, board):
self.board = board
self.pads = [] # (netcode, layers{'F','B'}, rect, pad)
self.tracks = [] # (netcode, layer, (ax,ay), (bx,by), halfwidth)
self.vias = [] # (netcode, x, y, r, drill)
for fp in board.GetFootprints():
for p in fp.Pads():
bb = p.GetBoundingBox()
r = (tomm(bb.GetLeft()), tomm(bb.GetTop()), tomm(bb.GetRight()), tomm(bb.GetBottom()))
layers = set()
if p.IsOnLayer(pcbnew.F_Cu):
layers.add('F')
if p.IsOnLayer(pcbnew.B_Cu):
layers.add('B')
if p.GetAttribute() in (pcbnew.PAD_ATTRIB_PTH, pcbnew.PAD_ATTRIB_NPTH):
layers |= {'F', 'B'}
self.pads.append((p.GetNetCode(), layers, r, p))
for t in board.GetTracks():
if t.GetClass() == 'PCB_VIA':
pos = t.GetPosition()
self.vias.append((t.GetNetCode(), tomm(pos.x), tomm(pos.y), tomm(t.GetWidth(pcbnew.F_Cu)) / 2,
tomm(t.GetDrillValue())))
else:
a, b = t.GetStart(), t.GetEnd()
lay = 'F' if t.GetLayer() == pcbnew.F_Cu else ('B' if t.GetLayer() == pcbnew.B_Cu else 'I')
self.tracks.append((t.GetNetCode(), lay, (tomm(a.x), tomm(a.y)), (tomm(b.x), tomm(b.y)),
tomm(t.GetWidth()) / 2))
self.courtyards = []
for fp in board.GetFootprints():
cy = fp.GetCourtyard(pcbnew.F_CrtYd)
if cy.OutlineCount():
bb = cy.BBox()
self.courtyards.append((fp.GetReference(), (tomm(bb.GetLeft()), tomm(bb.GetTop()),
tomm(bb.GetRight()), tomm(bb.GetBottom()))))
def via_ok(self, net, x, y, r, drill, clr, own_pad=None, own_clr=0.1, layers=('F', 'B')):
for (n, lays, rect, p) in self.pads:
if not (lays & set(layers)):
continue
d = rect_dist(x, y, rect)
if p is own_pad:
if d < r + own_clr:
return False
continue
if n == net and n != 0:
if d < r + 0.05:
return False # never on top of another same-net pad
continue
if d < r + clr:
return False
for (n, lay, a, b, hw) in self.tracks:
if lay not in layers:
continue
if n == net:
continue
if seg_point_dist(a[0], a[1], b[0], b[1], x, y) < r + hw + clr:
return False
for (n, vx, vy, vr, vd) in self.vias:
dist = math.hypot(vx - x, vy - y)
if dist < drill / 2 + vd / 2 + 0.3: # hole-to-hole
return False
if n != net and dist < r + vr + clr:
return False
return True
def track_ok(self, net, a, b, hw, clr, own_pad=None, layer='F'):
for (n, lays, rect, p) in self.pads:
if layer not in lays or p is own_pad or n == net:
continue
if seg_rect_dist(a[0], a[1], b[0], b[1], rect) < hw + clr:
return False
for (n, lay, c, d, thw) in self.tracks:
if lay != layer or n == net:
continue
if seg_seg_dist(a, b, c, d) < hw + thw + clr:
return False
for (n, vx, vy, vr, vd) in self.vias:
if n == net:
continue
if seg_point_dist(a[0], a[1], b[0], b[1], vx, vy) < hw + vr + clr:
return False
return True
def in_courtyard(self, x, y, exclude=None):
for ref, r in self.courtyards:
if ref == exclude:
continue
if r[0] <= x <= r[2] and r[1] <= y <= r[3]:
return True
return False
def add_via(board, net, x, y, dia=0.6, drill=0.3, locked=False):
v = pcbnew.PCB_VIA(board)
v.SetPosition(pcbnew.VECTOR2I(mm(x), mm(y)))
v.SetWidth(mm(dia))
v.SetDrill(mm(drill))
v.SetViaType(pcbnew.VIATYPE_THROUGH)
v.SetLayerPair(pcbnew.F_Cu, pcbnew.B_Cu)
v.SetNetCode(net)
v.SetLocked(locked)
board.Add(v)
return v
def add_track(board, net, a, b, width, layer=pcbnew.F_Cu, locked=False):
t = pcbnew.PCB_TRACK(board)
t.SetStart(pcbnew.VECTOR2I(mm(a[0]), mm(a[1])))
t.SetEnd(pcbnew.VECTOR2I(mm(b[0]), mm(b[1])))
t.SetWidth(mm(width))
t.SetLayer(layer)
t.SetNetCode(net)
t.SetLocked(locked)
board.Add(t)
return t
def zone_contains(board, netname, layer, x, y, margin=0.0):
"""True if (x, y) lies inside a zone outline of `netname` on `layer` (outline, not fill)."""
for z in board.Zones():
if z.GetNetname() != netname or not z.IsOnLayer(layer):
continue
ol = z.Outline()
pts = [(x, y)] if margin == 0 else [(x + margin * math.cos(a), y + margin * math.sin(a))
for a in [k * math.pi / 4 for k in range(8)]] + [(x, y)]
if all(ol.Contains(pcbnew.VECTOR2I(mm(px), mm(py))) for px, py in pts):
return True
return False
def ep_stubs(board, width=0.25, log=print):
"""QFN pins on the exposed pad's net (VSS) get a locked stub straight in to the exposed pad."""
n = 0
for fp in board.GetFootprints():
smd = [q for q in fp.Pads() if q.GetAttribute() == pcbnew.PAD_ATTRIB_SMD]
eps = [q for q in smd if tomm(max(q.GetBoundingBox().GetWidth(), q.GetBoundingBox().GetHeight())) > 3.0
and q.IsOnLayer(pcbnew.F_Cu)]
if not eps:
continue
ep = eps[0]
c = ep.GetPosition()
ex, ey = tomm(c.x), tomm(c.y)
half = tomm(ep.GetBoundingBox().GetWidth()) / 2
for q in smd:
if q.GetNetCode() != ep.GetNetCode() or not q.IsOnLayer(pcbnew.F_Cu):
continue
if tomm(max(q.GetBoundingBox().GetWidth(), q.GetBoundingBox().GetHeight())) > 3.0:
continue # the exposed pad itself (front or back copper)
p = q.GetPosition()
px, py = tomm(p.x), tomm(p.y)
dx, dy = ex - px, ey - py
if abs(dx) >= abs(dy): # side pin: go straight in along x
tx, ty = ex - math.copysign(half - 0.3, dx), py
else:
tx, ty = px, ey - math.copysign(half - 0.3, dy)
add_track(board, q.GetNetCode(), (px, py), (tx, ty), min(width, 0.9 * tomm(min(q.GetBoundingBox().GetWidth(), q.GetBoundingBox().GetHeight()))), locked=True)
n += 1
log(' exposed-pad stubs: %d' % n)
return n
def plane_fanout(board, plane_nets, via_dia=0.6, drill=0.3, clr=0.19, width=0.3, log=print):
"""Give every SMD pad of a plane net its own via into the plane (locked pre-route)."""
cm = CopperMap(board)
added, failed = 0, []
plane_layer = {'GND': pcbnew.In1_Cu, '+5VA': pcbnew.In2_Cu, '+3V3': pcbnew.In2_Cu}
pads = []
for fp in board.GetFootprints():
c = fp.GetPosition()
for p in fp.Pads():
if p.GetNetname() not in plane_nets or p.GetAttribute() != pcbnew.PAD_ATTRIB_SMD:
continue
if not p.IsOnLayer(pcbnew.F_Cu):
continue
pads.append((fp, p, (tomm(c.x), tomm(c.y))))
# Escape lanes: on fine-pitch packages (16+ SMD pads) keep a 1.5 mm strip straight out from every pad free
# of other nets' fan-out vias and necks, or a signal pin squeezed between two plane pins can't get out.
lanes = {}
for fp in board.GetFootprints():
smd = [q for q in fp.Pads() if q.GetAttribute() == pcbnew.PAD_ATTRIB_SMD]
if len(smd) < 16:
continue
c = fp.GetPosition()
fcx, fcy = tomm(c.x), tomm(c.y)
ep_nets = {q.GetNetCode() for q in smd if tomm(max(q.GetBoundingBox().GetWidth(), q.GetBoundingBox().GetHeight())) > 3.0}
for q in smd:
bb = q.GetBoundingBox()
x0, y0, x1, y1 = tomm(bb.GetLeft()), tomm(bb.GetTop()), tomm(bb.GetRight()), tomm(bb.GetBottom())
if max(x1 - x0, y1 - y0) > 3.0: # exposed pad
continue
if q.GetNetCode() <= 0 or q.GetNetCode() in ep_nets:
continue # unused pin, or tied straight in to the exposed pad
ox, oy = (x0 + x1) / 2 - fcx, (y0 + y1) / 2 - fcy
if abs(ox) >= abs(oy):
x0, x1 = (x0, x1 + 1.5) if ox > 0 else (x0 - 1.5, x1)
else:
y0, y1 = (y0, y1 + 1.5) if oy > 0 else (y0 - 1.5, y1)
lanes.setdefault(fp.GetReference(), []).append((q.GetNetCode(), x0 - 0.05, y0 - 0.05, x1 + 0.05, y1 + 0.05))
def in_lane(ref, net, x, y, r):
for n_, x0, y0, x1, y1 in lanes.get(ref, ()):
if n_ == net:
continue
dx, dy = max(x0 - x, 0.0, x - x1), max(y0 - y, 0.0, y - y1)
if dx * dx + dy * dy < r * r:
return True
return False
for fp, p, (cx, cy) in pads:
net = p.GetNetCode()
netname = p.GetNetname()
pos = p.GetPosition()
px, py = tomm(pos.x), tomm(pos.y)
bb = p.GetBoundingBox()
hw, hh = tomm(bb.GetWidth()) / 2, tomm(bb.GetHeight()) / 2
rv = via_dia / 2
tw = min(width, 0.85 * min(2 * hw, 2 * hh)) # thinner neck for fine-pitch pins
# outward direction from footprint centre
ox, oy = px - cx, py - cy
on = math.hypot(ox, oy)
cands = []
for k in range(16):
a = 2 * math.pi * k / 16
dx, dy = math.cos(a), math.sin(a)
ext = abs(dx) * hw + abs(dy) * hh # pad half-extent along direction (rect approx)
for step in range(8):
d = ext + rv + 0.12 + step * 0.2
vx, vy = px + dx * d, py + dy * d
score = d
if on > 1e-6:
score -= 0.35 * (dx * ox + dy * oy) / on # prefer outward
cands.append((score, vx, vy))
cands.sort()
ok = False
for score, vx, vy in cands:
vx, vy = round(vx / 0.05) * 0.05, round(vy / 0.05) * 0.05
if not zone_contains(board, netname, plane_layer[netname], vx, vy, margin=0.6):
continue
ref = fp.GetReference()
if ref in lanes and (in_lane(ref, net, vx, vy, rv + clr) or any(
in_lane(ref, net, px + (vx - px) * t, py + (vy - py) * t, tw / 2 + clr) for t in (0.5, 0.75, 1.0))):
continue
if not cm.via_ok(net, vx, vy, rv, drill, clr, own_pad=p):
continue
if not cm.track_ok(net, (px, py), (vx, vy), tw / 2, clr, own_pad=p):
continue
add_via(board, net, vx, vy, via_dia, drill, locked=True)
add_track(board, net, (px, py), (vx, vy), tw, locked=True)
cm.vias.append((net, vx, vy, rv, drill))
cm.tracks.append((net, 'F', (px, py), (vx, vy), tw / 2))
added += 1
ok = True
break
if not ok:
failed.append('%s.%s(%s)' % (fp.GetReference(), p.GetNumber(), netname))
log(' plane fanout: %d vias added, %d pads left to the router: %s' % (added, len(failed), ' '.join(failed)))
return added, failed
def stitch_gnd(board, W, H, OX, OY, pitch=2.5, via_dia=0.6, drill=0.3, clr=0.25, edge=1.0, log=print):
"""GND stitching vias on a grid wherever they clear every other-net copper feature."""
cm = CopperMap(board)
gnd = board.FindNet('GND').GetNetCode()
added = 0
y = OY + edge + 0.5
while y < OY + H - edge:
x = OX + edge + 0.5
while x < OX + W - edge:
if not cm.in_courtyard(x, y) and cm.via_ok(gnd, x, y, via_dia / 2, drill, clr, layers=('F', 'B')):
# keep clear of same-net pads too (no via under a pad)
add_via(board, gnd, x, y, via_dia, drill)
cm.vias.append((gnd, x, y, via_dia / 2, drill))
added += 1
x += pitch
y += pitch
log(' stitching vias: %d' % added)
return added
def place_silk_refs(board, W, H, OX, OY, log=print):
"""Put each reference where it hits no pad, no other silk and stays on the board; else hide it."""
pads = []
for fp in board.GetFootprints():
for p in fp.Pads():
bb = p.GetBoundingBox()
pads.append((tomm(bb.GetLeft()) - 0.15, tomm(bb.GetTop()) - 0.15,
tomm(bb.GetRight()) + 0.15, tomm(bb.GetBottom()) + 0.15))
silk = []
for fp in board.GetFootprints():
for g in fp.GraphicalItems():
if g.GetLayer() == pcbnew.F_SilkS and g.GetClass() != 'PCB_TEXT':
bb = g.GetBoundingBox()
silk.append((tomm(bb.GetLeft()) - 0.1, tomm(bb.GetTop()) - 0.1,
tomm(bb.GetRight()) + 0.1, tomm(bb.GetBottom()) + 0.1))
for d in board.GetDrawings():
if d.GetLayer() == pcbnew.F_SilkS:
bb = d.GetBoundingBox()
silk.append((tomm(bb.GetLeft()) - 0.1, tomm(bb.GetTop()) - 0.1,
tomm(bb.GetRight()) + 0.1, tomm(bb.GetBottom()) + 0.1))
for fp in board.GetFootprints(): # visible value fields (machine contact labels)
v = fp.Value()
if v.IsVisible() and v.GetLayer() == pcbnew.F_SilkS:
bb = v.GetBoundingBox()
silk.append((tomm(bb.GetLeft()) - 0.15, tomm(bb.GetTop()) - 0.15,
tomm(bb.GetRight()) + 0.15, tomm(bb.GetBottom()) + 0.15))
def ov(a, b):
return not (a[2] <= b[0] or a[0] >= b[2] or a[3] <= b[1] or a[1] >= b[3])
placed, hidden = 0, []
fps = sorted(board.GetFootprints(), key=lambda f: -tomm(f.GetBoundingBox(False).GetWidth()))
for fp in fps:
ref = fp.Reference()
if not ref.IsVisible() or ref.GetLayer() != pcbnew.F_SilkS:
continue
cy = fp.GetCourtyard(pcbnew.F_CrtYd)
bb = cy.BBox() if cy.OutlineCount() else fp.GetBoundingBox(False)
x0, y0, x1, y1 = tomm(bb.GetLeft()), tomm(bb.GetTop()), tomm(bb.GetRight()), tomm(bb.GetBottom())
cx, cy_ = (x0 + x1) / 2, (y0 + y1) / 2
n = len(fp.GetReference())
tw, th = 0.85 * n * 0.7 + 0.2, 0.7 + 0.2
cands = []
for ang, (tx, ty) in ((0, (cx, y0 - th / 2)), (0, (cx, y1 + th / 2)),
(0, (x1 + tw / 2, cy_)), (0, (x0 - tw / 2, cy_)),
(90, (x1 + th / 2, cy_)), (90, (x0 - th / 2, cy_)),
(0, (x1 + tw / 2, y0)), (0, (x0 - tw / 2, y0)),
(0, (x1 + tw / 2, y1)), (0, (x0 - tw / 2, y1)),
(0, (cx, y0 - th / 2 - 0.6)), (0, (cx, y1 + th / 2 + 0.6))):
w_, h_ = (tw, th) if ang == 0 else (th, tw)
box = (tx - w_ / 2, ty - h_ / 2, tx + w_ / 2, ty + h_ / 2)
cands.append((ang, tx, ty, box))
done = False
for ang, tx, ty, box in cands:
if box[0] < OX + 0.4 or box[2] > OX + W - 0.4 or box[1] < OY + 0.4 or box[3] > OY + H - 0.4:
continue
if any(ov(box, p) for p in pads) or any(ov(box, s) for s in silk):
continue
ref.SetTextAngleDegrees(ang)
ref.SetPosition(pcbnew.VECTOR2I(mm(tx), mm(ty)))
silk.append(box)
placed += 1
done = True
break
if not done:
ref.SetLayer(pcbnew.F_Fab) # keep it on the assembly drawing instead
hidden.append(fp.GetReference())
log(' silk references: %d placed, %d moved to F.Fab' % (placed, len(hidden)))
return placed, hidden
def place_board_texts(board, W, H, OX, OY, log=print):
"""Nudge free-standing silkscreen texts off pads/courtyards (spiral search); drop to F.Fab if no room."""
obst = []
for fp in board.GetFootprints():
for p in fp.Pads():
bb = p.GetBoundingBox()
obst.append((tomm(bb.GetLeft()) - 0.2, tomm(bb.GetTop()) - 0.2,
tomm(bb.GetRight()) + 0.2, tomm(bb.GetBottom()) + 0.2))
cy = fp.GetCourtyard(pcbnew.F_CrtYd)
if cy.OutlineCount():
bb = cy.BBox()
obst.append((tomm(bb.GetLeft()), tomm(bb.GetTop()), tomm(bb.GetRight()), tomm(bb.GetBottom())))
v = fp.Value()
if v.IsVisible() and v.GetLayer() == pcbnew.F_SilkS:
bb = v.GetBoundingBox()
obst.append((tomm(bb.GetLeft()) - 0.2, tomm(bb.GetTop()) - 0.2,
tomm(bb.GetRight()) + 0.2, tomm(bb.GetBottom()) + 0.2))
def ov(a, b):
return not (a[2] <= b[0] or a[0] >= b[2] or a[3] <= b[1] or a[1] >= b[3])
back_obst = []
for fp in board.GetFootprints():
for p in fp.Pads():
if p.IsOnLayer(pcbnew.B_Cu) or p.GetAttribute() != pcbnew.PAD_ATTRIB_SMD:
bb = p.GetBoundingBox()
back_obst.append((tomm(bb.GetLeft()) - 0.3, tomm(bb.GetTop()) - 0.3,
tomm(bb.GetRight()) + 0.3, tomm(bb.GetBottom()) + 0.3))
moved = dropped = 0
placed = []
for d in list(board.GetDrawings()):
if d.GetClass() != 'PCB_TEXT' or d.GetLayer() not in (pcbnew.F_SilkS, pcbnew.B_SilkS):
continue
obst_here = obst if d.GetLayer() == pcbnew.F_SilkS else back_obst
pos = d.GetPosition()
x0, y0 = tomm(pos.x), tomm(pos.y)
size0, thick0 = d.GetTextSize(), d.GetTextThickness()
best = None
# full size close by first; then shrink (down to 0.8 mm text) and search further before giving up
for scale, reach in ((1.0, 20), (0.8, 40), (0.65, 60)):
if scale != 1.0:
if tomm(size0.y) * scale < 0.8:
break
d.SetTextSize(pcbnew.VECTOR2I(int(size0.x * scale), int(size0.y * scale)))
d.SetTextThickness(max(mm(0.12), int(thick0 * scale)))
bb = d.GetBoundingBox()
w, h = tomm(bb.GetWidth()) + 0.3, tomm(bb.GetHeight()) + 0.3
for r in [0] + [0.5 * k for k in range(1, reach + 1)]:
n = 1 if r == 0 else max(8, int(r * 6))
for k in range(n):
a = 2 * math.pi * k / n
x, y = x0 + r * math.cos(a), y0 + r * math.sin(a)
box = (x - w / 2, y - h / 2, x + w / 2, y + h / 2)
if box[0] < OX + 0.5 or box[2] > OX + W - 0.5 or box[1] < OY + 0.5 or box[3] > OY + H - 0.5:
continue
if any(ov(box, o) for o in obst_here) or any(ov(box, o) for o in placed):
continue
best = (x, y, box)
break
if best:
break
if best:
break
if not best:
d.SetTextSize(size0)
d.SetTextThickness(thick0)
if best:
if best[0] != x0 or best[1] != y0:
moved += 1
d.SetPosition(pcbnew.VECTOR2I(mm(best[0]), mm(best[1])))
placed.append(best[2])
else:
d.SetLayer(pcbnew.F_Fab if d.GetLayer() == pcbnew.F_SilkS else pcbnew.B_Fab)
dropped += 1
log(' board texts: %d moved, %d dropped to Fab' % (moved, dropped))
def trim_edge_silk(board, OX, OY, W, H, margin=0.25, log=print):
"""Remove footprint silkscreen strokes that reach past the board edge (edge-mount parts)."""
n = 0
for fp in board.GetFootprints():
for g in list(fp.GraphicalItems()):
if g.GetLayer() not in (pcbnew.F_SilkS, pcbnew.B_SilkS) or g.GetClass() == 'PCB_TEXT':
continue
bb = g.GetBoundingBox()
if (tomm(bb.GetLeft()) < OX + margin or tomm(bb.GetRight()) > OX + W - margin or
tomm(bb.GetTop()) < OY + margin or tomm(bb.GetBottom()) > OY + H - margin):
fp.Remove(g)
n += 1
log(' trimmed %d silkscreen strokes at the board edge' % n)