main
John Lauer 3D model from KiCad's own package generator at the datasheet's values: radiused gull-wing leads in matte tin, reviewed… 9ad862b
#!/usr/bin/env python3
"""Run KiCad's own gull-wing generator (kicad-packages3D-generator, GW_QFP_SOIC_SSOP_TSSOP_SOT/make_gw,
CadQuery) on a datasheet parameter set, tuning the generator's flat-foot length L until the foot measured
to the 0.25 mm gauge plane (JEDEC L) equals the datasheet value.
usage: kgen.py base_model_key overrides.json out.step   (overrides: generator params + "gauge_L" target)
Prints JSON: params used, measured gauge L, bbox. The STEP holds three named parts: body, pins, mark
(KiCad's own colours), exported like the generator does (one assembly)."""
import sys, os, json, copy
REPO = os.path.expanduser("~/project/kicad-gen/kicad-packages3D-generator")
sys.path.insert(0, REPO)
import yaml, cadquery as cq
from OCP.BRepAlgoAPI import BRepAlgoAPI_Section
from OCP.gp import gp_Pln, gp_Pnt, gp_Dir
from GW_QFP_SOIC_SSOP_TSSOP_SOT.gw_qfp_soic_ssop_tssop_sot import make_gw
from _tools import shaderColors
key, ov, out = sys.argv[1], json.load(open(sys.argv[2])), sys.argv[3]
LEAD_Y = ov.pop("lead_y", None)
base = yaml.safe_load(open(os.path.join(REPO, "GW_QFP_SOIC_SSOP_TSSOP_SOT/cq_parameters.yaml")))[key]
P = copy.deepcopy(base); target = ov.pop("gauge_L", None); P.update(ov)
def build(p):
    body, pins, mark = make_gw(p)
    r = p["rotation"]
    return [s.rotate((0, 0, 0), (0, 0, 1), r) for s in (body, pins, mark)]
def gauge_L(pins, y=None):
    """JEDEC L: lead tip to where the lead's lower outer surface crosses z = 0.25, on a section through
    one lead's centre line, on the -x row."""
    s = pins.val() if hasattr(pins, "val") else pins
    bb = s.BoundingBox()
    # find a lead centre on the -x row: section at x just inside the tip, take the first foot's y range
    if y is None:
        # the lead nearest y = 0 on the -x row: cluster the foot section's edges into leads, take a centre
        sx = BRepAlgoAPI_Section(s.wrapped, gp_Pln(gp_Pnt(bb.xmin + 0.05, 0, 0), gp_Dir(1, 0, 0))); sx.Build()
        ys = sorted(set(round(v.Y, 4) for v in cq.Shape.cast(sx.Shape()).Vertices()))
        leads, cur = [], [ys[0]]
        for v in ys[1:]:
            if v - cur[-1] > 0.05: leads.append(cur); cur = [v]
            else: cur.append(v)
        leads.append(cur)
        y = sorted(((l[0] + l[-1]) / 2 for l in leads), key=abs)[0]
    sec = BRepAlgoAPI_Section(s.wrapped, gp_Pln(gp_Pnt(0, y, 0), gp_Dir(0, 1, 0))); sec.Build()
    prof = cq.Shape.cast(sec.Shape())
    sec2 = BRepAlgoAPI_Section(prof.wrapped, gp_Pln(gp_Pnt(0, 0, 0.25), gp_Dir(0, 0, 1))); sec2.Build()
    xs = sorted(v.X for v in cq.Shape.cast(sec2.Shape()).Vertices() if v.X < 0)
    tip = prof.BoundingBox().xmin
    return round(xs[0] - tip, 4), round(y, 3)
parts = build(P)
L_meas, ylead = gauge_L(parts[1], LEAD_Y)
if target is not None:
    # the generator's L is the flat foot; the gauge-plane L grows with it 1:1, so a few secant steps do
    for _ in range(6):
        if abs(L_meas - target) < 0.0005: break
        P["L"] = P.get("L") or ((P["E"] - P["E1"]) / 2 - (P["S"] + P["R1"]))
        P["S"] = None; P["L"] = round(P["L"] + (target - L_meas), 5); P.pop("S", None)
        parts = build(P); L_meas, ylead = gauge_L(parts[1], LEAD_Y)
cols = {k: shaderColors.named_colors[P[k]].getDiffuseFloat() for k in ("body_color_key", "pin_color_key", "mark_color_key")}
asm = cq.Assembly(name=P["model_name"])
for name, s, ck in zip(("body", "pins", "mark"), parts, ("body_color_key", "pin_color_key", "mark_color_key")):
    c = cols[ck]; asm.add(s, name=name, color=cq.Color(c[0], c[1], c[2]))
asm.save(out, "STEP")
allb = cq.Compound.makeCompound([p.val() if hasattr(p, "val") else p for p in parts]).BoundingBox()
print(json.dumps(dict(model=P["model_name"], params={k: P[k] for k in sorted(P) if k not in ("source",)}, gauge_L=L_meas, lead_y=ylead,
                      bbox=[round(v, 3) for v in (allb.xmin, allb.xmax, allb.ymin, allb.ymax, allb.zmin, allb.zmax)], colours=cols)))