"""Load footprints and symbols from the installed KiCad libraries.""" import math import os from sexpr import parse, find, findall, QStr KICAD = os.environ.get('KICAD_SHARE', '/snap/kicad/current/usr/share/kicad') FP_DIR = os.path.join(KICAD, 'footprints') SYM_DIR = os.path.join(KICAD, 'symbols') def load_footprint(lib_id, min_drill=None, min_ring=0.2, solid_thermal_pads=False): lib, name = lib_id.split(':', 1) path = os.path.join(FP_DIR, lib + '.pretty', name + '.kicad_mod') with open(path) as f: fp = parse(f.read()) if solid_thermal_pads: _solidify_thermal_pads(fp) if min_drill: _enforce_min_drill(fp, min_drill, min_ring) return fp def _solidify_thermal_pads(fp): """Remove plated holes that sit inside a cluster of same-numbered SMD pads (thermal vias in an exposed pad) and add one copper-only pad over the cluster, so the copper stays continuous under the solder-mask/paste windows but no hole is left in a pad. Fabs flag holes in pads (solder wicks into them) and charge for resin filling.""" import copy pads = findall(fp, 'pad') by_num = {} for p in pads: by_num.setdefault(str(p[1]), []).append(p) for num, group in by_num.items(): smd = [p for p in group if str(p[2]) == 'smd'] tht = [p for p in group if str(p[2]) == 'thru_hole'] if len(smd) < 2 or not tht: continue boxes = [] for p in smd: at, size = find(p, 'at'), find(p, 'size') x, y, w, h = float(at[1]), float(at[2]), float(size[1]), float(size[2]) boxes.append((x - w / 2, y - h / 2, x + w / 2, y + h / 2)) x0, y0 = min(b[0] for b in boxes), min(b[1] for b in boxes) x1, y1 = max(b[2] for b in boxes), max(b[3] for b in boxes) inside = [p for p in tht if x0 - 0.5 <= float(find(p, 'at')[1]) <= x1 + 0.5 and y0 - 0.5 <= float(find(p, 'at')[2]) <= y1 + 0.5] if not inside: continue for p in inside: fp.remove(p) solid = copy.deepcopy(smd[0]) solid[:] = [v for v in solid if not (isinstance(v, list) and v and v[0] in ('uuid', 'roundrect_rratio'))] solid[3] = 'rect' at, size, layers = find(solid, 'at'), find(solid, 'size'), find(solid, 'layers') at[1:] = [f'{(x0 + x1) / 2:g}', f'{(y0 + y1) / 2:g}'] size[1:] = [f'{x1 - x0:g}', f'{y1 - y0:g}'] layers[1:] = [QStr('F.Cu')] fp.insert(fp.index(smd[0]), solid) def _enforce_min_drill(fp, min_drill, min_ring): """Grow round plated holes below min_drill (and their copper ring) so the board fits the fab's process.""" for p in findall(fp, 'pad'): if str(p[2]) != 'thru_hole': continue drill = find(p, 'drill') if not drill or len(drill) < 2 or isinstance(drill[1], list) or str(drill[1]) == 'oval': continue if float(drill[1]) >= min_drill - 1e-9: continue drill[1] = f'{min_drill:g}' size = find(p, 'size') need = min_drill + 2 * min_ring for i in (1, 2): if float(size[i]) < need: size[i] = f'{need:g}' def fp_pads(fp): """Pads as dicts in footprint-local coords (KiCad: y down).""" pads = [] for p in findall(fp, 'pad'): num = str(p[1]) kind = p[2] # smd / thru_hole / np_thru_hole / connect shape = p[3] at = find(p, 'at') x, y = float(at[1]), float(at[2]) rot = float(at[3]) if len(at) > 3 else 0.0 size = find(p, 'size') w, h = float(size[1]), float(size[2]) layers = [str(l) for l in find(p, 'layers')[1:]] drill = find(p, 'drill') d = None if drill: vals = [v for v in drill[1:] if not isinstance(v, list)] if vals and vals[0] == 'oval': d = (float(vals[1]), float(vals[2]) if len(vals) > 2 else float(vals[1])) elif vals: d = (float(vals[0]), float(vals[0])) pads.append(dict(num=num, kind=kind, shape=shape, x=x, y=y, rot=rot, w=w, h=h, layers=layers, drill=d, node=p)) return pads def fp_courtyard(fp): """Bounding box (x0,y0,x1,y1) of the front/back courtyard lines/rects.""" xs, ys = [], [] for kind in ('fp_line', 'fp_rect', 'fp_circle', 'fp_arc', 'fp_poly'): for g in findall(fp, kind): layer = find(g, 'layer') if not layer or 'CrtYd' not in str(layer[1]): continue for key in ('start', 'end', 'mid', 'center'): pt = find(g, key) if pt: xs.append(float(pt[1])); ys.append(float(pt[2])) pts = find(g, 'pts') if pts: for xy in findall(pts, 'xy'): xs.append(float(xy[1])); ys.append(float(xy[2])) if kind == 'fp_circle': c = find(g, 'center'); e = find(g, 'end') r = math.hypot(float(e[1]) - float(c[1]), float(e[2]) - float(c[2])) xs += [float(c[1]) - r, float(c[1]) + r] ys += [float(c[2]) - r, float(c[2]) + r] if not xs: return None return (min(xs), min(ys), max(xs), max(ys)) _symlib_cache = {} def load_symbol(lib_id): """Return the symbol definition list (with 'extends' resolved).""" lib, name = lib_id.split(':', 1) if lib not in _symlib_cache: with open(os.path.join(SYM_DIR, lib + '.kicad_sym')) as f: _symlib_cache[lib] = parse(f.read()) root = _symlib_cache[lib] syms = {str(s[1]): s for s in findall(root, 'symbol')} s = syms[name] ext = find(s, 'extends') if ext: base = load_symbol(lib + ':' + str(ext[1])) # copy base, take name + properties from derived import copy merged = copy.deepcopy(base) merged[1] = QStr(name) # rename sub-units for sub in findall(merged, 'symbol'): sub[1] = QStr(str(sub[1]).replace(str(ext[1]), name, 1)) # properties: inherited from the base, overridden/extended by the derived symbol base_props = findall(merged, 'property') merged = [x for x in merged if not (isinstance(x, list) and x and x[0] == 'property')] derived = {str(p[1]): p for p in findall(s, 'property')} props = [] for p in base_props: props.append(derived.pop(str(p[1]), p)) props += list(derived.values()) # insert properties after the symbol-level flags idx = 2 while idx < len(merged) and isinstance(merged[idx], list) and merged[idx][0] in ('pin_names', 'pin_numbers', 'exclude_from_sim', 'in_bom', 'on_board'): idx += 1 merged = merged[:idx] + props + merged[idx:] return merged return s def sym_pins(sym): """Pins (number, name, type, x, y, angle, length) in symbol coords (y up).""" pins = [] for unit in findall(sym, 'symbol'): for p in findall(unit, 'pin'): at = find(p, 'at') length = float(find(p, 'length')[1]) name = find(p, 'name') number = find(p, 'number') pins.append(dict(type=str(p[1]), style=str(p[2]), x=float(at[1]), y=float(at[2]), angle=float(at[3]) if len(at) > 3 else 0.0, length=length, name=str(name[1]), num=str(number[1]), hidden=('hide' in [str(v) for v in p if not isinstance(v, list)]), unit=str(unit[1]))) return pins