main
John Lauer MCP2562FD-E/SN component page: batch 2 2069f2d 2d ago
#!/usr/bin/env python3
"""Build a Microchip SN (SOIC-8, 3.90 mm body) gull-wing model from the package drawing values
(DS20005284B p.23-24, drawing C04-00057-SN Rev L), in KiCad's 3D frame: Z up, seated on z = 0, centred on the
footprint origin, pins along x, pin 1 at (-x, +y) (footprint pad 1 at (-x, -y) in KiCad's Y-down footprint).
Bend radii, lead angle, mold draft and the pin-1 dimple follow KiCad's own SOIC-8 library model so the leads keep
radiused gull-wing bends; only the flat foot is longer, so the gauge-plane foot L lands inside 0.40 .. 1.27.
usage: build_soic8.py out.step [params.json]   (writes a two-part assembly: body (black epoxy), pins (tin))"""
import sys, json, math
import cadquery as cq

P = dict(
    N=8, e=1.27, E=6.00, E1=3.90, D=4.90, A=1.75, A1=0.10,
    b=0.41, c=0.20,                 # lead width / thickness (drawing: b 0.31..0.51, c 0.17..0.25)
    parting=0.825,                  # parting line height (KiCad library model; not printed)
    top_inset=0.19, bot_inset=0.115,  # mold draft insets per side (KiCad model: 11.6 deg top, 9.0 deg bottom; drawing 5..15)
    r_in=0.10,                      # inner bend radius (drawing R, R1 >= 0.07; KiCad 0.10)
    lead_angle=9.3,                 # rise off vertical, degrees (KiCad model)
    z_shoulder=0.925,               # lead centreline height where it leaves the body (KiCad model)
    foot_flat=0.39,                 # flat bottom length at z = 0, tuned so gauge-plane L = target
    pin1=dict(x=-1.11, y=1.61, d=0.60, depth=0.05),
    detail="full",                  # full | low (bends as 3 straight segments) | superlow (1 segment, undrafted body)
)
if len(sys.argv) > 2:
    P.update(json.load(open(sys.argv[2])))

def lead_profile_path(sign):
    """centreline of one lead in the XZ plane, from inside the body to the tip, for the -x row (sign=-1)."""
    c, r = P["c"], P["r_in"] + P["c"] / 2          # centreline radius of both bends
    tip = P["E"] / 2
    zc_foot = c / 2
    a = math.radians(P["lead_angle"])
    # bottom bend: horizontal foot (going outward) <- rise.  Work outward in |x|.
    x_fb = tip - P["foot_flat"]                     # where the flat (centreline) foot starts, |x|
    # bottom bend centre above the foot start
    cb = (x_fb, zc_foot + r)
    # rise direction (going up and inward): from bottom bend end
    # point on bottom arc where tangent equals rise direction: angle from centre
    pb_end = (cb[0] - r * math.cos(a), cb[1] - r * math.sin(a))
    # top bend: centre below shoulder centreline
    ct_z = P["z_shoulder"] - r
    pt_start_z = ct_z + r * math.sin(a)
    # rise line from pb_end going up-inward with angle a off vertical
    dz = pt_start_z - pb_end[1]
    pt_start = (pb_end[0] - dz * math.tan(a), pt_start_z)
    ct = (pt_start[0] - r * math.cos(a), ct_z)
    x_sh = ct[0]                                     # shoulder end (|x|) where top bend begins
    inner = P["E1"] / 2 - 0.35                       # start well inside the body
    pts = dict(inner=inner, x_sh=x_sh, ct=ct, pt_start=pt_start, pb_end=pb_end, cb=cb, x_fb=x_fb, tip=tip, r=r)
    return pts

def arc(wp, c, r, t0, t1, s, top):
    """bend from angle t0 to t1: top bend points (c.x + r sin t, c.z + r cos t); bottom (c.x - r cos t, c.z - r sin t).
    full detail = true arc; low = 3 chords; superlow = 1 chord."""
    pt = (lambda t: (c[0] + r * math.sin(t), c[1] + r * math.cos(t))) if top else (lambda t: (c[0] - r * math.cos(t), c[1] - r * math.sin(t)))
    if P["detail"] == "full":
        m, e = pt((t0 + t1) / 2), pt(t1)
        return wp.threePointArc((s * m[0], m[1]), (s * e[0], e[1]))
    n = 3 if P["detail"] == "low" else 1
    for i in range(1, n + 1):
        q = pt(t0 + (t1 - t0) * i / n); wp = wp.lineTo(s * q[0], q[1])
    return wp

def make_lead(y, sign):
    q = lead_profile_path(sign)
    r, a = q["r"], math.radians(P["lead_angle"])
    s = sign
    zc = P["z_shoulder"]
    # build path in XZ (workplane XZ: local x = global x, local y = global z)
    wp = cq.Workplane("XZ").moveTo(s * q["inner"], zc).lineTo(s * q["x_sh"], zc)
    # top bend: from (x_sh, zc) to pt_start, centre ct; mid angle
    m = math.radians(90 - P["lead_angle"]) / 2 + math.radians(P["lead_angle"])
    mid_t = (q["ct"][0] + r * math.sin(math.pi / 2 - (math.pi / 2 - a) / 2) , q["ct"][1] + r * math.cos(math.pi / 2 - (math.pi / 2 - a) / 2))
    # parametric: top-bend arc points at angle phi from +z axis rotating outward: (ct.x + r sin phi, ct.z + r cos phi), phi 0 -> 90-a
    phi_end = math.pi / 2 - a
    pm = (q["ct"][0] + r * math.sin(phi_end / 2), q["ct"][1] + r * math.cos(phi_end / 2))
    pe = (q["ct"][0] + r * math.sin(phi_end), q["ct"][1] + r * math.cos(phi_end))
    wp = arc(wp, q["ct"], r, 0.0, phi_end, s, top=True)
    wp = wp.lineTo(s * q["pb_end"][0], q["pb_end"][1])
    # bottom bend: centre cb, from pb_end (angle a from -x... ) to (x_fb, c/2)
    cb = q["cb"]
    # points on bottom arc: (cb.x - r cos t, cb.z - r sin t), t from a -> 90deg
    t_mid = (a + math.pi / 2) / 2
    pm2 = (cb[0] - r * math.cos(t_mid), cb[1] - r * math.sin(t_mid))
    wp = arc(wp, cb, r, a, math.pi / 2, s, top=False)
    wp = wp.lineTo(s * q["tip"], P["c"] / 2)
    path = wp.wire().val()
    # profile: rectangle b (along y) x c (normal to path) at the path start, plane normal = path tangent (x)
    start = cq.Vector(s * q["inner"], y, zc)
    prof = cq.Workplane(cq.Plane(origin=start, xDir=(0, 1, 0), normal=(s, 0, 0))).rect(P["b"], P["c"])
    return prof.sweep(cq.Workplane().add(path), transition="round" if P["detail"] == "full" else "right").val()

def body():
    E1, D, A, A1, pz = P["E1"], P["D"], P["A"], P["A1"], P["parting"]
    if P["detail"] == "superlow":
        b = cq.Workplane("XY").workplane(offset=A1).rect(E1, D).extrude(A - A1)
        p1 = P["pin1"]
        dimple = cq.Workplane("XY").workplane(offset=A - p1["depth"]).center(p1["x"], p1["y"]).circle(p1["d"] / 2).extrude(p1["depth"] + 0.01)
        return b.cut(dimple).val()
    lo = cq.Workplane("XY").workplane(offset=A1).rect(E1 - 2 * P["bot_inset"], D - 2 * P["bot_inset"]) \
        .workplane(offset=pz - A1).rect(E1, D).loft()
    hi = cq.Workplane("XY").workplane(offset=pz).rect(E1, D) \
        .workplane(offset=A - pz).rect(E1 - 2 * P["top_inset"], D - 2 * P["top_inset"]).loft()
    b = lo.union(hi)
    p1 = P["pin1"]
    dimple = cq.Workplane("XY").workplane(offset=A - p1["depth"]).center(p1["x"], p1["y"]).circle(p1["d"] / 2).extrude(p1["depth"] + 0.01)
    return b.cut(dimple).val()

def main(out):
    n2 = P["N"] // 2
    ys = [(n2 - 1) / 2 * P["e"] - i * P["e"] for i in range(n2)]   # +y .. -y (pin 1 at +y)
    leads = []
    for i, y in enumerate(ys):
        leads.append(make_lead(y, -1))      # pins 1..4 on -x, pin 1 at +y
    for i, y in enumerate(reversed(ys)):
        leads.append(make_lead(y, +1))      # pins 5..8 on +x, pin 5 at -y, pin 8 at +y
    bd = body()
    # trim the lead stubs that sit inside the body so body and pins do not overlap
    pins = cq.Compound.makeCompound([l.cut(bd) for l in leads])
    asm = cq.Assembly(name="SOIC-8_SN")
    asm.add(bd, name="body", color=cq.Color(0.1, 0.1, 0.1))
    asm.add(pins, name="pins", color=cq.Color(0.82, 0.82, 0.80))
    asm.save(out, exportType="STEP")
    print(json.dumps({"out": out, "params": P}, default=str))

if __name__ == "__main__":
    main(sys.argv[1])