Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016X4CAujMuib2tDqt9JRg6X
132 lines
6.2 KiB
Python
132 lines
6.2 KiB
Python
"""Cobre de las EinarBoard en blanco sobre noche (21-oct): portada (V5.6) y miniaturas por versión.
|
|
Lo nuevo de cada revisión va en blanco sólido; el resto del cobre en blanco tenue.
|
|
Uso: python3 tarjetas_blanco.py DIR_JSON DIR_SALIDA (pcb_v55/v56/v57/v6.json, de leer_pcb.py)
|
|
Genera SVG; la conversión a WebP la hace rasterizar.py."""
|
|
import json, os, re, sys
|
|
|
|
SRC, OUT = sys.argv[1], sys.argv[2]
|
|
D = {v: json.load(open(os.path.join(SRC, f'pcb_{v}.json'))) for v in ('v55', 'v56', 'v57', 'v6')}
|
|
|
|
|
|
def nets(d):
|
|
return {p[7] for p in d['pads'] if p[7]} | {s[6] for s in d['segments'] if s[6]}
|
|
|
|
|
|
def ok(n):
|
|
return n and n != 'GND' and not n.startswith('unconnected')
|
|
|
|
|
|
NUEVO = {'v55': set(),
|
|
'v56': {n for n in nets(D['v56']) - nets(D['v55']) if ok(n)},
|
|
'v57': {n for n in nets(D['v57']) - nets(D['v56']) if ok(n)},
|
|
'v6': {n for n in nets(D['v6']) - nets(D['v57'])
|
|
if ok(n) and re.search(r'GNSS|MODEM|4G|ANT|RF_|NETLIGHT|1V8|F9P|LTE', n)}}
|
|
|
|
|
|
def contorno(d):
|
|
xs, ys = [], []
|
|
for e in d['edges']:
|
|
for k in e[1:]:
|
|
if isinstance(k, list) and k and k[0] in ('start', 'end'):
|
|
xs.append(float(k[1])); ys.append(float(k[2]))
|
|
return min(xs), min(ys), max(xs) - min(xs), max(ys) - min(ys)
|
|
|
|
|
|
# opacidades: (zonas, B.Cu, capas internas, F.Cu, pads, vías) y lo nuevo (F.Cu, internas, pads)
|
|
TEMAS = {
|
|
'mini': dict(fondo='#111A2C', z=0.035, b=0.12, i=0.08, f=0.28, pad=0.38, via=0.32, nf=1.0, ni=0.55, npad=0.95, min_w=0.55),
|
|
'portada': dict(fondo='#0E1524', z=0.03, b=0.10, i=0.07, f=0.22, pad=0.30, via=0.26, nf=0.95, ni=0.5, npad=0.9, min_w=0.8),
|
|
}
|
|
|
|
|
|
def svg(d, hl, crop, size, tema):
|
|
T = TEMAS[tema]
|
|
x0, y0, W, H = crop
|
|
OW, OH = size
|
|
s = max(OW / W, OH / H) # llena el recuadro (la portada recorta arriba y abajo)
|
|
ox, oy = (OW - W * s) / 2, (OH - H * s) / 2
|
|
X = lambda x: ox + (x - x0) * s
|
|
Y = lambda y: oy + (y - y0) * s
|
|
o = [f'<svg xmlns="http://www.w3.org/2000/svg" width="{OW}" height="{OH}" viewBox="0 0 {OW} {OH}">',
|
|
f'<rect width="{OW}" height="{OH}" fill="{T["fondo"]}"/>']
|
|
|
|
def grupo(op, partes):
|
|
if partes:
|
|
o.append(f'<g opacity="{op}" fill="#fff" stroke="#fff">' + ''.join(partes) + '</g>')
|
|
|
|
def lineas(capas, solo=None, sin=None):
|
|
r = []
|
|
for x1, y1, x2, y2, w, l, n in d['segments']:
|
|
if l not in capas or (solo is not None and n not in solo) or (sin is not None and n in sin):
|
|
continue
|
|
r.append(f'<line x1="{X(x1):.1f}" y1="{Y(y1):.1f}" x2="{X(x2):.1f}" y2="{Y(y2):.1f}" '
|
|
f'stroke-width="{max(w * s, T["min_w"]):.2f}" stroke-linecap="round" fill="none"/>')
|
|
return r
|
|
|
|
def pads(solo=None, sin=None):
|
|
r = []
|
|
for ax, ay, w, h, prot, shape, typ, n, ref, layer, lays in d['pads']:
|
|
if not any(l in ('F.Cu', '*.Cu') for l in lays):
|
|
continue
|
|
if (solo is not None and n not in solo) or (sin is not None and n in sin):
|
|
continue
|
|
cx, cy, ww, hh = X(ax), Y(ay), w * s, h * s
|
|
if shape == 'circle':
|
|
r.append(f'<circle cx="{cx:.1f}" cy="{cy:.1f}" r="{ww / 2:.1f}" stroke="none"/>')
|
|
else:
|
|
rr = min(ww, hh) / 2 if shape == 'oval' else (min(ww, hh) * 0.18 if shape == 'roundrect' else 0)
|
|
r.append(f'<rect x="{cx - ww / 2:.1f}" y="{cy - hh / 2:.1f}" width="{ww:.1f}" height="{hh:.1f}" rx="{rr:.1f}" '
|
|
f'stroke="none" transform="rotate({-prot:.0f} {cx:.1f} {cy:.1f})"/>')
|
|
return r
|
|
|
|
def vias(solo=None, sin=None):
|
|
return [f'<circle cx="{X(x):.1f}" cy="{Y(y):.1f}" r="{sz * s / 2:.1f}" stroke="none"/>'
|
|
for x, y, sz, n in d['vias'] if not ((solo is not None and n not in solo) or (sin is not None and n in sin))]
|
|
|
|
zonas = []
|
|
for lay, net, pts in d['zones']:
|
|
if lay in ('F.Cu', 'B.Cu') and pts:
|
|
zonas.append('<path d="M' + ' L'.join(f'{X(x):.1f},{Y(y):.1f}' for x, y in pts) + 'Z" '
|
|
f'stroke-width="{0.12 * s:.2f}" fill-opacity="0.6"/>')
|
|
grupo(T['z'], zonas)
|
|
grupo(T['b'], lineas({'B.Cu'}, sin=hl))
|
|
grupo(T['i'], lineas({'In1.Cu', 'In2.Cu'}, sin=hl))
|
|
grupo(T['f'], lineas({'F.Cu'}, sin=hl))
|
|
grupo(T['pad'], pads(sin=hl))
|
|
grupo(T['via'], vias(sin=hl))
|
|
if hl:
|
|
grupo(T['ni'], lineas({'B.Cu', 'In1.Cu', 'In2.Cu'}, solo=hl))
|
|
grupo(T['nf'], lineas({'F.Cu'}, solo=hl))
|
|
grupo(T['npad'], pads(solo=hl) + vias(solo=hl))
|
|
# barrenos: del color del fondo, encima de todo
|
|
hoyos = [f'<circle cx="{X(x):.1f}" cy="{Y(y):.1f}" r="{sz * s / 4.2:.1f}"/>' for x, y, sz, n in d['vias']]
|
|
for ax, ay, w, h, prot, shape, typ, n, ref, layer, lays in d['pads']:
|
|
if typ == 'thru_hole':
|
|
hoyos.append(f'<circle cx="{X(ax):.1f}" cy="{Y(ay):.1f}" r="{min(w, h) * s * 0.28:.1f}"/>')
|
|
o.append(f'<g fill="{T["fondo"]}">' + ''.join(hoyos) + '</g>')
|
|
# contorno de la tarjeta (solo en miniaturas)
|
|
if tema == 'mini':
|
|
bx, by, bw, bh = contorno(d)
|
|
o.append(f'<rect x="{X(bx):.1f}" y="{Y(by):.1f}" width="{bw * s:.1f}" height="{bh * s:.1f}" rx="{1.2 * s:.1f}" '
|
|
f'fill="none" stroke="#fff" stroke-opacity="0.35" stroke-width="{max(0.35 * s, 1):.2f}"/>')
|
|
o.append('</svg>')
|
|
return '\n'.join(o)
|
|
|
|
|
|
os.makedirs(OUT, exist_ok=True)
|
|
for v, d in D.items():
|
|
bx, by, bw, bh = contorno(d)
|
|
m = 2.0
|
|
open(os.path.join(OUT, f'{v}.svg'), 'w').write(svg(d, NUEVO[v], (bx - m, by - m, bw + 2 * m, bh + 2 * m), (420, 300), 'mini'))
|
|
print(v, 'nuevo:', len(NUEVO[v]), 'redes; contorno', round(bw, 1), 'x', round(bh, 1), 'mm')
|
|
|
|
# portada: franja de la V5.6 donde más cobre nuevo hay (ventana de 20 mm de alto)
|
|
d = D['v56']; hl = NUEVO['v56']
|
|
bx, by, bw, bh = contorno(d)
|
|
W = bw - 16; H = W * 400 / 2400
|
|
mejor = max((sum(abs(s[2] - s[0]) + abs(s[3] - s[1]) for s in d['segments'] if s[6] in hl and y <= (s[1] + s[3]) / 2 <= y + H), y)
|
|
for y in [by + 1 + k * 0.5 for k in range(int((bh - H - 2) / 0.5))])
|
|
y0 = mejor[1]
|
|
print('portada: y0 =', round(y0, 1), 'mm; mm de cobre nuevo en la franja:', round(mejor[0], 1))
|
|
open(os.path.join(OUT, 'portada-v56.svg'), 'w').write(svg(d, hl, (bx + 8, y0, W, H), (2400, 400), 'portada'))
|