component
LW A6SG-V2BA-FK0PN0
Public Unreviewedby adonis
ams OSRAM Standard LEDs - SMD, White SIDELED®, side-emitting. Datasheet-verified pinout, KiCad symbol/footprint/3D.
main
adonis
LW A6SG-V2BA-FK0PN0 SIDELED: datasheet-derived symbol, footprint, 3D model, viewers and generators
86e4c6a
1mo ago
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342
#!/usr/bin/env python3
"""
Emit an AP214 STEP model of the ams OSRAM SIDELED(R) package (LW A6SG).
Every dimension below is cited to "LW A6SG binning FK0PN0", version 1.1,
2020-06-01 -- page numbers in the comments. Anything the datasheet does not
dimension is marked REPRESENTATIVE and is called out in the README too.
Frame: X right, Y front(-)/back(+), Z up. Board top plane at Z = 0, so the
model drops straight onto the footprint origin.
"""
import math
# ---------------------------------------------------------------- dimensions
BODY_W = 4.00 # p.14 top view, 4 +/-0.2
BODY_H = 3.97 # p.14 front view, 3.97 +0.23/-0.17
BODY_D = 2.85 # p.14 side view, (2.85)
ENVELOPE_D = 3.63 # p.14 top view, 3.63 +/-0.2 (body + lens dome)
STANDOFF = 0.80 # p.14 side view, (0.8) -- body underside above board
LENS_DIA = 2.40 # p.14 front view, (2.4)
OPTIC_Z_REL = 2.52 # p.14 front view, 2.52 +0.28/-0.12 above body underside
LEAD_W = 1.00 # p.14 front view, 1 +/-0.1
LEAD_PITCH = 2.54 # p.14 front view, "2.54 spacing"
PAD_PITCH = 2.50 # p.15 recommended solder pad: 3.7 outer span - 1.2 pad
PAD_LEN = 3.00 # p.15 recommended solder pad, 3.0 (0.118)
LEAD_T = 0.20 # REPRESENTATIVE: lead frame stock thickness
CHAMFER = 0.50 # REPRESENTATIVE: size of the cathode-marking chamfer
FOOT_FRONT = -1.00 # REPRESENTATIVE: foot stops short of the lens face
FOOT_BACK = 1.50 # foot back edge, flush with the 3.0 mm land
Y_FRONT = -ENVELOPE_D / 2.0 # -1.815, lens tip
Y_FACE = Y_FRONT + (ENVELOPE_D - BODY_D) # -1.035, body front face
Y_BACK = Y_FRONT + ENVELOPE_D # +1.815, body back face
Z_BOT = STANDOFF # 0.80
Z_TOP = STANDOFF + BODY_H # 4.77
OPTIC_Z = STANDOFF + OPTIC_Z_REL # 3.32
DOME_H = ENVELOPE_D - BODY_D # 0.78 protrusion
DOME_R = ((LENS_DIA / 2.0) ** 2 + DOME_H ** 2) / (2.0 * DOME_H) # fitted cap
COL_BODY = (0.930, 0.930, 0.915) # white SMT housing (p.1 "white SMT package")
COL_LEAD = (0.752, 0.762, 0.780) # tin-plated lead frame
COL_LENS = (0.980, 0.958, 0.862) # "colored diffused silicone resin" (p.1)
# ------------------------------------------------------------------- meshing
def tri_normal(a, b, c):
u = (b[0] - a[0], b[1] - a[1], b[2] - a[2])
v = (c[0] - a[0], c[1] - a[1], c[2] - a[2])
return (u[1] * v[2] - u[2] * v[1],
u[2] * v[0] - u[0] * v[2],
u[0] * v[1] - u[1] * v[0])
def quad(a, b, c, d):
"""Two triangles, winding preserved."""
return [(a, b, c), (a, c, d)]
def ear_clip(poly):
"""Triangulate a simple CCW polygon of 2D points; yields index triples."""
idx = list(range(len(poly)))
def area2(i, j, k):
(ax, ay), (bx, by), (cx, cy) = poly[i], poly[j], poly[k]
return (bx - ax) * (cy - ay) - (by - ay) * (cx - ax)
def inside(i, j, k, p):
d1, d2, d3 = area2(i, j, p), area2(j, k, p), area2(k, i, p)
neg = d1 < 0 or d2 < 0 or d3 < 0
pos = d1 > 0 or d2 > 0 or d3 > 0
return not (neg and pos)
out = []
guard = 0
while len(idx) > 3 and guard < 5000:
guard += 1
for n in range(len(idx)):
i, j, k = idx[n - 1], idx[n], idx[(n + 1) % len(idx)]
if area2(i, j, k) <= 1e-12:
continue
if any(inside(i, j, k, m) for m in idx if m not in (i, j, k)):
continue
out.append((i, j, k))
idx.pop(n)
break
else:
break
if len(idx) == 3:
out.append(tuple(idx))
return out
def extrude(profile, lo, hi, axis):
"""Extrude a CCW 2D profile between two planes normal to `axis`."""
def place(p, t):
if axis == 'x':
return (t, p[0], p[1])
if axis == 'y':
return (p[0], t, p[1])
return (p[0], p[1], t)
tris, n = [], len(profile)
for a, b, c in ear_clip(profile):
tris.append((place(profile[c], hi), place(profile[b], hi), place(profile[a], hi)))
tris.append((place(profile[a], lo), place(profile[b], lo), place(profile[c], lo)))
for i in range(n):
p, q = profile[i], profile[(i + 1) % n]
tris += quad(place(p, lo), place(q, lo), place(q, hi), place(p, hi))
# Extruding a CCW profile along -axis flips every face; fix in one place.
probe = sum(tri_normal(*t)[{'x': 0, 'y': 1, 'z': 2}[axis]] for t in tris[:2])
if probe < 0:
tris = [(t[0], t[2], t[1]) for t in tris]
return tris
def box(x0, x1, y0, y1, z0, z1):
return extrude([(x0, y0), (x1, y0), (x1, y1), (x0, y1)], z0, z1, 'z')
def dome(cx, cz, y_base, radius, height, segs=32, rings=8):
"""Spherical cap opening toward -Y, closed with a flat base disc."""
base_r = math.sqrt(max(radius * radius - (radius - height) ** 2, 0.0))
y_c = y_base + radius - height # sphere centre, behind the face
# polar angle 0 = tip (-Y), grows toward the rim
a_rim = math.acos(max(-1.0, min(1.0, (y_c - y_base) / radius)))
def surf(ring, seg):
a = a_rim * ring / rings
phi = 2.0 * math.pi * seg / segs
r = radius * math.sin(a)
return (cx + r * math.cos(phi), y_c - radius * math.cos(a), cz + r * math.sin(phi))
tris = []
tip = (cx, y_c - radius, cz)
for s in range(segs):
tris.append((tip, surf(1, s + 1), surf(1, s)))
for ring in range(1, rings):
for s in range(segs):
tris += quad(surf(ring, s), surf(ring, s + 1),
surf(ring + 1, s + 1), surf(ring + 1, s))
centre = (cx, y_base, cz)
for s in range(segs):
p = (cx + base_r * math.cos(2 * math.pi * s / segs), y_base,
cz + base_r * math.sin(2 * math.pi * s / segs))
q = (cx + base_r * math.cos(2 * math.pi * (s + 1) / segs), y_base,
cz + base_r * math.sin(2 * math.pi * (s + 1) / segs))
tris.append((centre, p, q))
return tris
# ------------------------------------------------------------------ the part
def build():
# Housing: rectangular prism with one vertical edge chamfered on the
# cathode (-X) side -- the datasheet's "Cathode marking" callout (p.14).
housing = [
(-BODY_W / 2, Y_FACE + CHAMFER),
(-BODY_W / 2 + CHAMFER, Y_FACE),
(BODY_W / 2, Y_FACE),
(BODY_W / 2, Y_BACK),
(-BODY_W / 2, Y_BACK),
]
solids = [('housing', COL_BODY, extrude(housing, Z_BOT, Z_TOP, 'z'))]
# Gull-wing leads: foot on the land pattern, riser up into the housing.
lead = [
(FOOT_FRONT, 0.0), (FOOT_BACK, 0.0), (FOOT_BACK, Z_BOT + 0.2),
(FOOT_BACK - LEAD_T, Z_BOT + 0.2), (FOOT_BACK - LEAD_T, LEAD_T),
(FOOT_FRONT, LEAD_T),
]
for sign, name in ((-1, 'lead_cathode'), (1, 'lead_anode')):
cx = sign * PAD_PITCH / 2.0
solids.append((name, COL_LEAD,
extrude(lead, cx - LEAD_W / 2, cx + LEAD_W / 2, 'x')))
solids.append(('lens', COL_LENS,
dome(0.0, OPTIC_Z, Y_FACE, DOME_R, DOME_H)))
# KiCad inverts Y between footprint space and 3D-model space, so mirror the
# part in Y here: the lens then lands on the footprint's -Y edge, where the
# .kicad_mod draws the emission arrow. Mirroring flips handedness, so the
# winding of every triangle is reversed to keep normals pointing outward.
return [(name, colour, [(mirror(t[0]), mirror(t[2]), mirror(t[1]))
for t in tris])
for name, colour, tris in solids]
def mirror(p):
return (p[0], -p[1], p[2])
# --------------------------------------------------------------- STEP writer
class Step:
def __init__(self):
self.lines, self.n = [], 0
def add(self, body):
self.n += 1
self.lines.append("#%d = %s;" % (self.n, body))
return self.n
def pt(self, p):
return self.add("CARTESIAN_POINT('',(%s))" % ",".join("%.6f" % v for v in p))
def dr(self, d):
return self.add("DIRECTION('',(%s))" % ",".join("%.6f" % v for v in d))
def ax2(self, origin, axis, ref):
return self.add("AXIS2_PLACEMENT_3D('',#%d,#%d,#%d)"
% (self.pt(origin), self.dr(axis), self.dr(ref)))
def norm(v):
m = math.sqrt(sum(c * c for c in v))
return tuple(c / m for c in v) if m > 1e-12 else (0.0, 0.0, 1.0)
def solid_brep(s, tris, label):
"""Triangles -> MANIFOLD_SOLID_BREP with shared vertices and edges."""
vmap, verts = {}, []
for t in tris:
for p in t:
key = tuple(round(c, 6) for c in p)
if key not in vmap:
vmap[key] = len(verts)
verts.append(key)
vpoint = [s.add("VERTEX_POINT('',#%d)" % s.pt(v)) for v in verts]
edges = {}
def edge(i, j):
key = (i, j) if i < j else (j, i)
if key not in edges:
a, b = verts[key[0]], verts[key[1]]
d = norm((b[0] - a[0], b[1] - a[1], b[2] - a[2]))
vec = s.add("VECTOR('',#%d,1.000000)" % s.dr(d))
line = s.add("LINE('',#%d,#%d)" % (s.pt(a), vec))
edges[key] = s.add("EDGE_CURVE('',#%d,#%d,#%d,.T.)"
% (vpoint[key[0]], vpoint[key[1]], line))
return edges[key], (i < j)
faces = []
for t in tris:
idx = [vmap[tuple(round(c, 6) for c in p)] for p in t]
if len(set(idx)) < 3:
continue
n = tri_normal(*[verts[i] for i in idx])
if math.sqrt(sum(c * c for c in n)) < 1e-12:
continue
n = norm(n)
a, b = verts[idx[0]], verts[idx[1]]
ref = norm((b[0] - a[0], b[1] - a[1], b[2] - a[2]))
# Gram-Schmidt so the reference direction is exactly in-plane.
dot = sum(ref[i] * n[i] for i in range(3))
ref = norm(tuple(ref[i] - dot * n[i] for i in range(3)))
plane = s.add("PLANE('',#%d)" % s.ax2(a, n, ref))
oriented = []
for k in range(3):
ec, fwd = edge(idx[k], idx[(k + 1) % 3])
oriented.append(s.add("ORIENTED_EDGE('',*,*,#%d,%s)"
% (ec, ".T." if fwd else ".F.")))
loop = s.add("EDGE_LOOP('',(%s))" % ",".join("#%d" % o for o in oriented))
bound = s.add("FACE_OUTER_BOUND('',#%d,.T.)" % loop)
faces.append(s.add("ADVANCED_FACE('',(#%d),#%d,.T.)" % (bound, plane)))
shell = s.add("CLOSED_SHELL('',(%s))" % ",".join("#%d" % f for f in faces))
return s.add("MANIFOLD_SOLID_BREP('%s',#%d)" % (label, shell))
def write(path, solids):
s = Step()
ctx_axis = s.ax2((0.0, 0.0, 0.0), (0.0, 0.0, 1.0), (1.0, 0.0, 0.0))
length = s.add("( LENGTH_UNIT() NAMED_UNIT(*) SI_UNIT(.MILLI.,.METRE.) )")
angle = s.add("( NAMED_UNIT(*) PLANE_ANGLE_UNIT() SI_UNIT($,.RADIAN.) )")
solid_a = s.add("( NAMED_UNIT(*) SI_UNIT($,.STERADIAN.) SOLID_ANGLE_UNIT() )")
tol = s.add("UNCERTAINTY_MEASURE_WITH_UNIT(LENGTH_MEASURE(1.E-07),#%d,"
"'distance_accuracy_value','confusion accuracy')" % length)
ctx = s.add("( GEOMETRIC_REPRESENTATION_CONTEXT(3) "
"GLOBAL_UNCERTAINTY_ASSIGNED_CONTEXT((#%d)) "
"GLOBAL_UNIT_ASSIGNED_CONTEXT((#%d,#%d,#%d)) "
"REPRESENTATION_CONTEXT('','3D') )" % (tol, length, angle, solid_a))
breps, styled = [], []
for label, colour, tris in solids:
brep = solid_brep(s, tris, label)
breps.append(brep)
rgb = s.add("COLOUR_RGB('',%.6f,%.6f,%.6f)" % colour)
fill = s.add("FILL_AREA_STYLE_COLOUR('',#%d)" % rgb)
area = s.add("FILL_AREA_STYLE('',(#%d))" % fill)
sfill = s.add("SURFACE_STYLE_FILL_AREA(#%d)" % area)
side = s.add("SURFACE_SIDE_STYLE('',(#%d))" % sfill)
usage = s.add("SURFACE_STYLE_USAGE(.BOTH.,#%d)" % side)
psa = s.add("PRESENTATION_STYLE_ASSIGNMENT((#%d))" % usage)
styled.append(s.add("STYLED_ITEM('colour',(#%d),#%d)" % (psa, brep)))
shape = s.add("ADVANCED_BREP_SHAPE_REPRESENTATION('%s',(#%d,%s),#%d)"
% ('LW_A6SG', ctx_axis,
",".join("#%d" % b for b in breps), ctx))
s.add("MECHANICAL_DESIGN_GEOMETRIC_PRESENTATION_REPRESENTATION('',(%s),#%d)"
% (",".join("#%d" % x for x in styled), ctx))
app = s.add("APPLICATION_CONTEXT('core data for automotive mechanical "
"design processes')")
s.add("APPLICATION_PROTOCOL_DEFINITION('international standard',"
"'automotive_design',2000,#%d)" % app)
pctx = s.add("PRODUCT_CONTEXT('',#%d,'mechanical')" % app)
prod = s.add("PRODUCT('LW_A6SG-V2BA-FK0PN0','LW A6SG-V2BA-FK0PN0',"
"'ams OSRAM SIDELED white LED',(#%d))" % pctx)
pdf = s.add("PRODUCT_DEFINITION_FORMATION('','',#%d)" % prod)
pdctx = s.add("PRODUCT_DEFINITION_CONTEXT('part definition',#%d,'design')" % app)
pdef = s.add("PRODUCT_DEFINITION('design','',#%d,#%d)" % (pdf, pdctx))
pds = s.add("PRODUCT_DEFINITION_SHAPE('','',#%d)" % pdef)
s.add("SHAPE_DEFINITION_REPRESENTATION(#%d,#%d)" % (pds, shape))
header = (
"ISO-10303-21;\n"
"HEADER;\n"
"FILE_DESCRIPTION(('ams OSRAM SIDELED LW A6SG-V2BA-FK0PN0'),'2;1');\n"
"FILE_NAME('lw-a6sg-v2ba-fk0pn0.step','2026-08-27T00:00:00',"
"('Adom'),('Adom'),'gen_step.py','Adom component pipeline','');\n"
"FILE_SCHEMA(('AUTOMOTIVE_DESIGN { 1 0 10303 214 1 1 1 1 }'));\n"
"ENDSEC;\n"
"DATA;\n")
with open(path, 'w') as fh:
fh.write(header)
fh.write("\n".join(s.lines))
fh.write("\nENDSEC;\nEND-ISO-10303-21;\n")
return s.n
if __name__ == '__main__':
import sys
out = sys.argv[1] if len(sys.argv) > 1 else 'lw-a6sg-v2ba-fk0pn0.step'
solids = build()
n = write(out, solids)
print("OK: %s (%d solids, %d triangles, %d entities)"
% (out, len(solids), sum(len(t) for _, _, t in solids), n))
print(" envelope X %.2f Y %.2f Z %.2f dome R %.3f"
% (BODY_W, ENVELOPE_D, Z_TOP, DOME_R))