main
John Lauer README, license, states, models, verification log and provenance ef5470d 20h ago
#!/usr/bin/env python3
"""Build the 150060VS75000 (Wurth WL-SMCW 0603) 3D model from the datasheet as an AP214 STEP.

No CAD kernel needed: every solid is a planar-faced prism (an extruded or lofted polygon), written
straight to STEP with colours (STYLED_ITEM per solid). Adom frame: Z-up, millimetres, seated on z = 0,
origin = footprint origin (pad-pattern centre), pin 1 (cathode) toward -X.

Dimensions: Wurth Elektronik datasheet 150060VS75000, revision 002.009 (2019-02-26), p.1 "Dimensions":
  body 1.6 +/-0.1 x 0.8 +/-0.1, total height 0.7 +/-0.1, substrate 0.3 +/-0.1, lens 1.1 +/-0.1 long,
  bottom terminals 0.7 typ wide with a 0.7 +/-0.1 gap, castellation R 0.175 typ, polarity marks.
Appearance (colours, wrap-around gold ends, green underside triangle): the Wurth WL-SMCW 0603 product
photo used on distributor listings. See provenance/3d-model.json.

usage: build_step.py OUT.step [--lens off|on] [--segs 12] [--no-die] [--no-marks] [--name NAME]
"""
import argparse, math

COL = {
    "substrate": (0.90, 0.90, 0.88),
    "terminal": (0.85, 0.68, 0.35),
    "mark": (0.10, 0.65, 0.25),
    "die": (0.15, 0.15, 0.17),
    "lens_off": (0.86, 0.92, 0.93),   # water-clear epoxy, unlit
    "lens_on": (0.62, 0.95, 0.10),    # 570 nm dominant (bright green), lit
}

L, W = 1.6, 0.8          # body length (x), width (y)
H_SUB = 0.30             # substrate top
H_TOT = 0.70             # lens top
LENS_L = 1.10            # lens length at its base
LENS_TOP_INSET = 0.02    # draft seen on p.1 side and end views
END = (L - LENS_L) / 2   # 0.25 mm end blocks (gold wrap-around terminals)
PAD_GAP = 0.70           # bottom terminal gap
PAD_W = 0.70             # bottom terminal width (typ)
R_CAST = 0.175
PLATE = 0.02             # bottom pad / plating thickness
TOP_PLATE = 0.01         # terminal stands proud of the substrate top


def end_block_outline(sign, segs):
    """CCW outline of one end block (x from 0.55 to 0.8 on the +X end), castellation notch at the end face."""
    xi, xo = LENS_L / 2, L / 2
    pts = [(xi, -W / 2), (xo, -W / 2), (xo, -R_CAST)]
    for k in range(1, segs):
        t = -math.pi / 2 + math.pi * k / segs
        pts.append((xo - R_CAST * math.cos(t), R_CAST * math.sin(t)))
    pts += [(xo, R_CAST), (xo, W / 2), (xi, W / 2)]
    if sign < 0:
        pts = [(-x, y) for x, y in pts][::-1]
    return pts


def rect(x0, y0, x1, y1):
    return [(x0, y0), (x1, y0), (x1, y1), (x0, y1)]


class Step:
    def __init__(self):
        self.lines = []
        self.n = 0
        self.vcache = {}
        self.dcache = {}

    def add(self, s):
        self.n += 1
        self.lines.append("#%d = %s;" % (self.n, s))
        return "#%d" % self.n

    def pt(self, p):
        return self.add("CARTESIAN_POINT('',(%s))" % ",".join("%.6f" % c for c in p))

    def dir(self, d):
        k = tuple(round(c, 9) for c in d)
        if k not in self.dcache:
            self.dcache[k] = self.add("DIRECTION('',(%s))" % ",".join("%.9f" % c for c in d))
        return self.dcache[k]


def sub(a, b): return (a[0] - b[0], a[1] - b[1], a[2] - b[2])
def cross(a, b): return (a[1] * b[2] - a[2] * b[1], a[2] * b[0] - a[0] * b[2], a[0] * b[1] - a[1] * b[0])
def norm(a):
    m = math.sqrt(sum(c * c for c in a)); return tuple(c / m for c in a)


def newell(poly):
    n = [0.0, 0.0, 0.0]
    for i, p in enumerate(poly):
        q = poly[(i + 1) % len(poly)]
        n[0] += (p[1] - q[1]) * (p[2] + q[2]); n[1] += (p[2] - q[2]) * (p[0] + q[0]); n[2] += (p[0] - q[0]) * (p[1] + q[1])
    return norm(n)


def solid(st, name, bottom, top, z0, z1):
    """Loft polygon `bottom` at z0 to polygon `top` at z1 (same vertex count, both CCW from +Z, planar sides)."""
    nb = len(bottom)
    V = [(x, y, z0) for x, y in bottom] + [(x, y, z1) for x, y in top]
    faces = [list(range(nb))[::-1], [nb + i for i in range(nb)]]
    for i in range(nb):
        j = (i + 1) % nb
        faces.append([i, j, nb + j, nb + i])
    vp = {}
    for i, p in enumerate(V):
        vp[i] = st.add("VERTEX_POINT('',%s)" % st.pt(p))
    edges = {}

    def edge(a, b):
        k = (min(a, b), max(a, b))
        if k not in edges:
            p, q = V[k[0]], V[k[1]]
            d = sub(q, p); ln = math.sqrt(sum(c * c for c in d))
            line = st.add("LINE('',%s,%s)" % (st.pt(p), st.add("VECTOR('',%s,%.6f)" % (st.dir(norm(d)), ln))))
            edges[k] = st.add("EDGE_CURVE('',%s,%s,%s,.T.)" % (vp[k[0]], vp[k[1]], line))
        return edges[k], (a, b) == (min(a, b), max(a, b))

    fids = []
    for f in faces:
        poly = [V[i] for i in f]
        n = newell(poly)
        ref = norm(sub(poly[1], poly[0]))
        ax = st.add("AXIS2_PLACEMENT_3D('',%s,%s,%s)" % (st.pt(poly[0]), st.dir(n), st.dir(ref)))
        plane = st.add("PLANE('',%s)" % ax)
        oes = []
        for i in range(len(f)):
            e, fwd = edge(f[i], f[(i + 1) % len(f)])
            oes.append(st.add("ORIENTED_EDGE('',*,*,%s,%s)" % (e, ".T." if fwd else ".F.")))
        loop = st.add("EDGE_LOOP('',(%s))" % ",".join(oes))
        bound = st.add("FACE_OUTER_BOUND('',%s,.T.)" % loop)
        fids.append(st.add("ADVANCED_FACE('',(%s),%s,.T.)" % (bound, plane)))
    shell = st.add("CLOSED_SHELL('',(%s))" % ",".join(fids))
    return st.add("MANIFOLD_SOLID_BREP('%s',%s)" % (name, shell))


def build(out, lens="off", segs=12, die=True, marks=True, name="150060VS75000"):
    st = Step()
    parts = []  # (solid id, colour key)
    # substrate (white PCB) between the end blocks, bottom at the plating height
    parts.append((solid(st, "substrate", rect(-LENS_L / 2, -W / 2, LENS_L / 2, W / 2), rect(-LENS_L / 2, -W / 2, LENS_L / 2, W / 2), PLATE, H_SUB), "substrate"))
    for s, tag in ((-1, "K"), (1, "A")):
        o = end_block_outline(s, segs)
        parts.append((solid(st, "terminal_%s_end" % tag, o, o, 0.0, H_SUB + TOP_PLATE), "terminal"))
        x0, x1 = sorted((s * PAD_GAP / 2, s * LENS_L / 2))
        r = rect(x0, -PAD_W / 2, x1, PAD_W / 2)
        parts.append((solid(st, "terminal_%s_bottom" % tag, r, r, 0.0, PLATE), "terminal"))
    # lens: 1.1 x 0.8 at its base, slight draft to the top (p.1 side and end views)
    lb = rect(-LENS_L / 2, -W / 2, LENS_L / 2, W / 2)
    lt = rect(-LENS_L / 2 + LENS_TOP_INSET, -W / 2 + LENS_TOP_INSET, LENS_L / 2 - LENS_TOP_INSET, W / 2 - LENS_TOP_INSET)
    parts.append((solid(st, "lens", lb, lt, H_SUB, H_TOT), "lens_" + lens))
    if die:
        d = rect(-0.12, -0.12, 0.12, 0.12)
        parts.append((solid(st, "die", d, d, H_SUB, H_SUB + 0.10), "die"))
    if marks:
        # underside: triangle between the pads, tip toward pin 1 (cathode), p.1 bottom view
        tri = [(-0.16, 0.0), (0.16, -0.175), (0.16, 0.175)]
        parts.append((solid(st, "polarity_mark_bottom", tri, tri, PLATE * 0.4, PLATE), "mark"))
        # top: corner wedge on the pin-1 end, p.1 top view ("Polarity Mark")
        wedge = [(-L / 2, W / 2), (-LENS_L / 2, W / 2 - 0.10), (-LENS_L / 2, W / 2)]
        parts.append((solid(st, "polarity_mark_top", wedge, wedge, H_SUB + TOP_PLATE, H_SUB + TOP_PLATE + 0.005), "mark"))

    # product structure + units
    ctx_app = st.add("APPLICATION_CONTEXT('automotive_design')")
    st.add("APPLICATION_PROTOCOL_DEFINITION('international standard','automotive_design',2000,%s)" % ctx_app)
    pctx = st.add("PRODUCT_CONTEXT('',%s,'mechanical')" % ctx_app)
    prod = st.add("PRODUCT('%s','%s','',(%s))" % (name, name, pctx))
    st.add("PRODUCT_RELATED_PRODUCT_CATEGORY('part',$,(%s))" % prod)
    pdf = st.add("PRODUCT_DEFINITION_FORMATION('','',%s)" % prod)
    pdctx = st.add("PRODUCT_DEFINITION_CONTEXT('part definition',%s,'design')" % ctx_app)
    pd = st.add("PRODUCT_DEFINITION('design','',%s,%s)" % (pdf, pdctx))
    pds = st.add("PRODUCT_DEFINITION_SHAPE('','',%s)" % pd)
    lu = st.add("( LENGTH_UNIT() NAMED_UNIT(*) SI_UNIT(.MILLI.,.METRE.) )")
    au = st.add("( NAMED_UNIT(*) PLANE_ANGLE_UNIT() SI_UNIT($,.RADIAN.) )")
    su = st.add("( NAMED_UNIT(*) SI_UNIT($,.STERADIAN.) SOLID_ANGLE_UNIT() )")
    unc = st.add("UNCERTAINTY_MEASURE_WITH_UNIT(LENGTH_MEASURE(1.E-07),%s,'distance_accuracy_value','confusion accuracy')" % lu)
    gctx = st.add("( GEOMETRIC_REPRESENTATION_CONTEXT(3) GLOBAL_UNCERTAINTY_ASSIGNED_CONTEXT((%s)) GLOBAL_UNIT_ASSIGNED_CONTEXT((%s,%s,%s)) REPRESENTATION_CONTEXT('Context #1','3D Context with UNIT and UNCERTAINTY') )" % (unc, lu, au, su))
    origin = st.add("AXIS2_PLACEMENT_3D('',%s,%s,%s)" % (st.pt((0, 0, 0)), st.dir((0, 0, 1)), st.dir((1, 0, 0))))
    rep = st.add("ADVANCED_BREP_SHAPE_REPRESENTATION('',(%s),%s)" % (",".join([origin] + [p[0] for p in parts]), gctx))
    st.add("SHAPE_DEFINITION_REPRESENTATION(%s,%s)" % (pds, rep))
    styled = []
    for sid, ck in parts:
        r, g, b = COL[ck]
        c = st.add("COLOUR_RGB('%s',%.4f,%.4f,%.4f)" % (ck, r, g, b))
        fa = st.add("FILL_AREA_STYLE('',(%s))" % st.add("FILL_AREA_STYLE_COLOUR('',%s)" % c))
        sss = st.add("SURFACE_SIDE_STYLE('',(%s))" % st.add("SURFACE_STYLE_FILL_AREA(%s)" % fa))
        psa = st.add("PRESENTATION_STYLE_ASSIGNMENT((%s))" % st.add("SURFACE_STYLE_USAGE(.BOTH.,%s)" % sss))
        styled.append(st.add("STYLED_ITEM('color',(%s),%s)" % (psa, sid)))
    st.add("MECHANICAL_DESIGN_GEOMETRIC_PRESENTATION_REPRESENTATION('',(%s),%s)" % (",".join(styled), gctx))

    hdr = ["ISO-10303-21;", "HEADER;",
           "FILE_DESCRIPTION(('%s, Wurth Elektronik WL-SMCW 0603 chip LED, built from datasheet rev 002.009 p.1'),'2;1');" % name,
           "FILE_NAME('%s','2026-10-21T00:00:00',('Adom'),('Adom'),'adom build_step.py 1.0','adom build_step.py 1.0','');" % out.split('/')[-1],
           "FILE_SCHEMA(('AUTOMOTIVE_DESIGN { 1 0 10303 214 1 1 1 1 }'));", "ENDSEC;", "DATA;"]
    open(out, "w").write("\n".join(hdr + st.lines + ["ENDSEC;", "END-ISO-10303-21;", ""]))
    return len(parts)


if __name__ == "__main__":
    ap = argparse.ArgumentParser()
    ap.add_argument("out"); ap.add_argument("--lens", default="off", choices=["off", "on"])
    ap.add_argument("--segs", type=int, default=12); ap.add_argument("--no-die", action="store_true")
    ap.add_argument("--no-marks", action="store_true"); ap.add_argument("--name", default="150060VS75000")
    a = ap.parse_args()
    n = build(a.out, a.lens, a.segs, not a.no_die, not a.no_marks, a.name)
    print("OK: %d solids -> %s" % (n, a.out))