main
John Lauer STM32G431CBT6: datasheet-built LQFP48 model (DS12589 Rev 2 Table 94) with ST's real top marking STM32G431 / CBT6 / Y WW… 751ee87
#!/usr/bin/env python3
"""Datasheet-built gull-wing package (SOIC / QFP) in the Adom/KiCad frame: Z up, seated on z=0, centred,
pin 1 where the KiCad footprint puts pad 1 (top left, x<0 y>0 in the Y-up model frame), pads numbered
counter-clockwise seen from the top.
usage: build_gullwing.py params.json out.step
params: name, kind ("dual"|"quad"), body_x, body_y (max body size at the parting line, x = across the
pin rows for dual), span_x, span_y (lead tip to tip; dual uses span_x), a1 (standoff), top (overall
height), zp (lead centre height), c (lead thickness), b (lead width), e (pitch), L (foot length tip to the
outer face of the vertical leg = gauge-plane length with square bends), n_side (pins per row/side),
inset_bot, inset_top (draft insets of the bottom/top faces), dot {x, y, d, depth} or null.
Square lead bends and plain drafted body faces; every number is cited in the provenance record."""
import sys, json, math, cadquery as cq
P = json.load(open(sys.argv[1])); out = sys.argv[2]
bx, by, a1, top, zp, c, b, e, L = (P[k] for k in ("body_x", "body_y", "a1", "top", "zp", "c", "b", "e", "L"))
ib, it = P["inset_bot"], P["inset_top"]
def rect(z, w, l):
    return cq.Wire.makePolygon([cq.Vector(-w/2, -l/2, z), cq.Vector(w/2, -l/2, z), cq.Vector(w/2, l/2, z), cq.Vector(-w/2, l/2, z)], close=True)
body = cq.Solid.makeLoft([rect(a1, bx - 2*ib, by - 2*ib), rect(zp, bx, by)]).fuse(
       cq.Solid.makeLoft([rect(zp, bx, by), rect(top, bx - 2*it, by - 2*it)])).clean()
dot = None
if P.get("dot"):
    d = P["dot"]
    cut = cq.Solid.makeCylinder(d["d"]/2, d["depth"] + 0.1, cq.Vector(d["x"], d["y"], top - d["depth"]))
    body = body.cut(cut).clean()
    dot = cq.Solid.makeCylinder(d["d"]/2, d["depth"] - 0.01, cq.Vector(d["x"], d["y"], top - d["depth"]))
def lead_x(half_body, tip, y):
    """a lead on the -x side running along -x, centred at y"""
    xt = tip; xv = tip - L          # outer face of the vertical leg (positive distances)
    xin = half_body - 0.15
    lt, lb = zp + c/2, zp - c/2
    pts = [(xin, lb), (xin, lt), (xv, lt), (xv, c), (xt, c), (xt, 0), (xv - c, 0), (xv - c, lb)]
    # build on +x then mirror: profile in XZ
    prof = cq.Workplane("XZ").polyline([(-x, z) for x, z in pts]).close().extrude(b/2, both=True).val()
    return prof.translate(cq.Vector(0, y, 0))
def rotz(s, deg):
    return s.rotate(cq.Vector(0, 0, 0), cq.Vector(0, 0, 1), deg)
n = P["n_side"]; offs = [((n - 1)/2 - i) * e for i in range(n)]   # +y first (pin 1 at the top of the left row)
leads = []   # (pin number, solid)
num = 1
if P["kind"] == "dual":
    for y in offs:                                   # left row, top to bottom: 1..n
        leads.append((num, lead_x(bx/2, P["span_x"]/2, y))); num += 1
    for y in reversed(offs):                         # right row, bottom to top: n+1..2n
        leads.append((num, rotz(lead_x(bx/2, P["span_x"]/2, -y), 180))); num += 1
else:
    # left side (pins 1..n top to bottom), bottom side (left to right), right side (bottom to top), top side (right to left)
    for side, ang in enumerate((0, 90, 180, 270)):
        half = (bx if side % 2 == 0 else by)/2; tip = (P["span_x"] if side % 2 == 0 else P["span_y"])/2
        for y in offs:
            leads.append((num, rotz(lead_x(half, tip, y), ang))); num += 1
asm = cq.Assembly(name=P["name"])
asm.add(body, name="body", color=cq.Color(0.148, 0.145, 0.145))
if dot is not None:
    asm.add(dot, name="pin1_dimple", color=cq.Color(0.33, 0.32, 0.31))
for k, s in leads:
    asm.add(s, name=f"lead_{k}", color=cq.Color(0.824, 0.820, 0.781))
asm.save(out, "STEP")
allc = cq.Compound.makeCompound([body] + [s for _, s in leads]); bb = allc.BoundingBox()
p1 = leads[0][1].BoundingBox()
print(json.dumps(dict(bbox=[round(v, 3) for v in (bb.xmin, bb.xmax, bb.ymin, bb.ymax, bb.zmin, bb.zmax)],
                      pin1_centre=[round((p1.xmin + p1.xmax)/2, 3), round((p1.ymin + p1.ymax)/2, 3)], leads=len(leads))))