pixalynx/pixal-gps

These files define two separate printed circuit boards: a small, two-layer battery cartridge with protection circuitry and contact pads for a pouch cell, and a larger, two-layer wireless charging dock featuring USB-C input, a resonant coil, and wireless power transmission components.

Version
0.1.2
License
unset
Stars
0

scripts/gen-gnd-stubs.py

#!/usr/bin/env python3
"""Generate routing/gnd-stubs.json for the tracker: one plane via next to every ground land.

Reads the last build (dist/tracker/circuit.json) for the exact land positions, picks a via
spot 0.45 mm beyond the land in the direction that points away from the component centre
(falling back to the other three directions), rejects spots that come closer than CLEAR to
any other land/hole on any layer, an earlier stub via, or the board edge, and writes the
board-frame via coordinates keyed by port selector. The board file converts them into
component-local pcbPath points (lib/geometry.ts) and draws a 0.25 mm stub + via.
"""
import json, sys, math
src = sys.argv[1] if len(sys.argv) > 1 else 'dist/tracker/circuit.json'
out = sys.argv[2] if len(sys.argv) > 2 else 'routing/gnd-stubs.json'
NET = sys.argv[3] if len(sys.argv) > 3 else 'GND'
VIA_D = 0.4; CLEAR = 0.18; STEP = 0.45
d = json.load(open(src))
board = next(e for e in d if e['type'] == 'pcb_board')
bw, bh = board['width'], board['height']; bx, by = board['center']['x'], board['center']['y']
comps = {c['pcb_component_id']: c for c in d if c['type'] == 'pcb_component'}
sname = {c['source_component_id']: c['name'] for c in d if c['type'] == 'source_component'}
sports = {p['source_port_id']: p for p in d if p['type'] == 'source_port'}
nets = {n['source_net_id']: n for n in d if n['type'] == 'source_net'}
gnd_ids = [nid for nid, n in nets.items() if n['name'] == NET]
gnd_key = None
for t in d:
    if t['type'] == 'source_trace' and any(n in t.get('connected_source_net_ids', []) for n in gnd_ids):
        for pid in t.get('connected_source_port_ids', []):
            gnd_key = sports[pid].get('subcircuit_connectivity_map_key'); break
    if gnd_key: break
gnd_ports = {pid for pid, p in sports.items() if p.get('subcircuit_connectivity_map_key') == gnd_key}
pads = [e for e in d if e['type'] in ('pcb_smtpad', 'pcb_plated_hole', 'pcb_hole')]
def rect(e):
    if e['type'] == 'pcb_smtpad':
        if e['shape'] == 'circle': r = e['radius']; return (e['x'] - r, e['y'] - r, e['x'] + r, e['y'] + r)
        return (e['x'] - e['width'] / 2, e['y'] - e['height'] / 2, e['x'] + e['width'] / 2, e['y'] + e['height'] / 2)
    r = (e.get('outer_diameter') or e.get('hole_diameter') or e.get('diameter')) / 2
    return (e['x'] - r, e['y'] - r, e['x'] + r, e['y'] + r)
rects = [(rect(e), e) for e in pads]
def rdist(x, y, r):
    x0, y0, x1, y1 = r
    dx = max(x0 - x, 0, x - x1); dy = max(y0 - y, 0, y - y1)
    return math.hypot(dx, dy)
stubs = []; taken = []; skipped = []
pcb_ports = [p for p in d if p['type'] == 'pcb_port' and p.get('source_port_id') in gnd_ports]
for pp in pcb_ports:
    pad = next((e for e in pads if e['type'] == 'pcb_smtpad' and e.get('pcb_port_id') == pp['pcb_port_id']), None)
    if pad is None: continue
    comp = comps[pad['pcb_component_id']]; name = sname[comp['source_component_id']]
    sp = sports[pp['source_port_id']]
    if pad['shape'] == 'circle': w = h = pad['radius'] * 2
    else: w, h = pad['width'], pad['height']
    cx, cy = comp['center']['x'], comp['center']['y']
    dx, dy = pad['x'] - cx, pad['y'] - cy
    cands = []
    if abs(dx) >= abs(dy): cands = [(math.copysign(1, dx or 1), 0), (0, 1), (0, -1), (-math.copysign(1, dx or 1), 0)]
    else: cands = [(0, math.copysign(1, dy or 1)), (1, 0), (-1, 0), (0, -math.copysign(1, dy or 1))]
    placed = None
    for ux, uy in cands:
        for extra in (0, 0.15, 0.3):
            x = pad['x'] + ux * (w / 2 + STEP + extra); y = pad['y'] + uy * (h / 2 + STEP + extra)
            others = [(r, e) for r, e in rects if e is not pad]
            ok = abs(x - bx) <= bw / 2 - 0.45 and abs(y - by) <= bh / 2 - 0.45
            if ok:
                for r, e in others:
                    if rdist(x, y, r) < VIA_D / 2 + CLEAR: ok = False; break
            if ok:
                for (tx, ty) in taken:
                    if math.hypot(tx - x, ty - y) < VIA_D + CLEAR: ok = False; break
            if ok: placed = (round(x, 3), round(y, 3)); break
        if placed: break
    port_name = sp.get('name') or ''
    hints = pp.get('port_hints', [])
    pin_hint = next((h for h in hints if h.startswith('pin')), None) or next((f"pin{h}" for h in hints if h.isdigit()), None)
    sel = f"{name}.{port_name}" if port_name and not port_name.startswith('pin') else f"{name}.{pin_hint or port_name}"
    if placed:
        taken.append(placed)
        stubs.append({"port": sel, "component": name, "layer": pad['layer'], "pad": [round(pad['x'], 3), round(pad['y'], 3)], "via": [placed[0], placed[1]]})
    else:
        skipped.append(sel)
json.dump(stubs, open(out, 'w'), indent=1)
print(f"{len(stubs)} stubs written to {out}; skipped: {skipped}")