← Commit history

LW A6SG-V2BA-FK0PN0 SIDELED: datasheet-derived symbol, footprint, 3D model, viewers and generators

adonis ·86e4c6a143 ·1mo ago ·parent 537790a
7 files changed +1255−22
page.json+116−22
@@ -1,8 +1,12 @@ {+  "schema_version": 1,+  "type": "component",+  "kind": "component",   "slug": "lw-a6sg-v2ba-fk0pn0",+  "title": "LW A6SG-V2BA-FK0PN0",+  "brief": "ams OSRAM Standard LEDs - SMD, White SIDELED\u00ae, side-emitting. Datasheet-verified pinout, KiCad symbol/footprint/3D.",   "version": "0.1.2",-  "type": "component",-  "description": "ams OSRAM Standard LEDs - SMD, White SIDELED®, side-emitting, 900-2240 mcd at IF = 20 mA. Datasheet-verified pinout, KiCad symbol/footprint/3D.",+  "license": "CC-BY-SA-4.0",   "tags": [     "led",     "sideled",@@ -11,6 +15,113 @@     "automotive",     "lw-a6sg-v2ba-fk0pn0"   ],+  "component": {+    "mpn": "LW A6SG-V2BA-FK0PN0",+    "manufacturer": "ams OSRAM",+    "package": "SIDELED\u00ae",+    "pin_count": 2,+    "category": "LED",+    "subcategory": "Standard LEDs - SMD, White SIDELED\u00ae",+    "datasheet": "docs/lw-a6sg-v2ba-fk0pn0-datasheet.pdf",+    "parts": {+      "symbol": "lw-a6sg-v2ba-fk0pn0.kicad_sym",+      "footprint": "lw-a6sg-v2ba-fk0pn0.kicad_mod",+      "model_3d": "lw-a6sg-v2ba-fk0pn0.glb",+      "model_3d_step": "lw-a6sg-v2ba-fk0pn0.step",+      "lbr": "lw-a6sg-v2ba-fk0pn0.lbr"+    },+    "distributor_links": {+      "mouser": "https://www.mouser.com/en/ProductDetail/ams-OSRAM/LW-A6SG-V2BA-FK0PN0?qs=vLWxofP3U2xiZuGh4CU2Qw%3D%3D",+      "digikey": "https://www.digikey.com/en/products/detail/ams-osram-ag/LW-A6SG-V2BA-FK0PN0-0-20-R33/13624925"+    },+    "pins": [+      {+        "number": "1",+        "name": "K",+        "type": "passive",+        "description": "Cathode. The datasheet's dimensional drawing (p.14) labels this lead 'Cathode' and puts the cathode marking on this side of the package; the chamfered body corner in the 3D model and the silkscreen triangle both mark it. Return the LED current here."+      },+      {+        "number": "2",+        "name": "A",+        "type": "passive",+        "description": "Anode. Drive at IF = 20 mA typical, 5 mA min to 50 mA max (p.3). VF is 2.90 V min / 3.00 V typ / 3.50 V max at 20 mA (p.4), so size the series resistor from VF max, not typ. Never connect to a fixed supply without current limiting."+      }+    ],+    "provenance": {+      "extractor": "adom component pipeline (Claude)",+      "datasheet": {+        "url": "docs/lw-a6sg-v2ba-fk0pn0-datasheet.pdf",+        "title": "LW A6SG binning FK0PN0",+        "version": "1.1",+        "dated": "2020-06-01",+        "pages": 25,+        "sha256": "93722427bd3f7bbb",+        "source_tier": "manufacturer",+        "retrieved": "2026-08-27"+      },+      "cad": "Generated from the manufacturer datasheet (land pattern p.15, package outline p.14)",+      "pins": {+        "total": 2,+        "described": 2,+        "cites_pages": true,+        "refuses_to_fabricate": true+      }+    },+    "viewers": {+      "symbol": "viewers/symbol.html",+      "footprint": "viewers/footprint.html"+    },+    "pin1": {+      "pad": "1",+      "at": [+        -1.25,+        0.0+      ],+      "corner": "left",+      "source": "datasheet-cathode-marking",+      "derived_from": "[email protected]",+      "mark": "drawn"+    }+  },+  "metadata": {+    "specs": {+      "Manufacturer": "ams OSRAM",+      "Package": "SIDELED\u00ae, 2-lead gull-wing, side-emitting",+      "Category": "LED",+      "Pins": "2",+      "Colour": "White, Cx = 0.33 / Cy = 0.33 (CIE 1931)",+      "Luminous intensity": "900 \u2026 2240 mcd at IF = 20 mA",+      "Forward voltage": "3.00 V typ, 3.50 V max at IF = 20 mA",+      "Forward current": "20 mA typ, 50 mA max (5 mA min)",+      "Viewing angle": "120\u00b0 (2\u03c6 at 50 % IV)",+      "Body size": "4.00 \u00d7 3.63 \u00d7 3.97 mm (0.80 mm standoff)"+    }+  },+  "sections": [+    "hero",+    "viewers",+    "readme",+    "breadcrumbs",+    "info"+  ],+  "discovery_triggers": [+    "lw-a6sg-v2ba-fk0pn0",+    "lw a6sg",+    "a6sg",+    "sideled",+    "osram sideled",+    "white sideled",+    "side emitting led",+    "side firing led",+    "q65113a1198",+    "light pipe led"+  ],+  "discovery_pitch": "LW A6SG-V2BA-FK0PN0 (ams OSRAM) white SIDELED - side-emitting LED with datasheet-verified pinout, land pattern, KiCad symbol/footprint and 3D model.",+  "author": {+    "name": "adonis",+    "email": "[email protected]"+  },   "dependencies": {},   "hero": {     "type": "image",@@ -24,28 +135,11 @@     "lw-a6sg-v2ba-fk0pn0.glb",     "lw-a6sg-v2ba-fk0pn0.step",     "lw-a6sg-v2ba-fk0pn0.lbr",+    "docs/hero.png",     "docs/lw-a6sg-v2ba-fk0pn0-datasheet.pdf",     "viewers/symbol.html",     "viewers/footprint.html",     "pads.json",     "skills/sideled-lw-a6sg/SKILL.md"-  ],-  "author": {-    "name": "adonis",-    "email": "[email protected]"-  },-  "kind": "component",-  "discovery_triggers": [-    "lw-a6sg-v2ba-fk0pn0",-    "lw a6sg",-    "a6sg",-    "sideled",-    "osram sideled",-    "white sideled",-    "side emitting led",-    "side firing led",-    "q65113a1198",-    "light pipe led"-  ],-  "discovery_pitch": "LW A6SG-V2BA-FK0PN0 (ams OSRAM) white SIDELED - side-emitting LED with datasheet-verified pinout, land pattern, KiCad symbol/footprint and 3D model."-}\ No newline at end of file+  ]+}
tools/gen_part.pyadded+654
@@ -0,0 +1,654 @@+#!/usr/bin/env python3+"""+Emit every non-3D artefact for the LW A6SG-V2BA-FK0PN0 component page from one+source of truth: KiCad symbol + footprint, the EAGLE library, pads.json,+page.json, package.json, README.md and the two interactive viewers.++Datasheet: "LW A6SG binning FK0PN0", ams OSRAM, version 1.1, 2020-06-01.+Page numbers in comments refer to that document.+"""+import json, os, re, textwrap++HERE = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))+SLUG = "lw-a6sg-v2ba-fk0pn0"+MPN = "LW A6SG-V2BA-FK0PN0"+SYM = "LW_A6SG-V2BA-FK0PN0"+FP = "LED_SIDELED_LW-A6SG"+MFR = "ams OSRAM"+PKG = "SIDELED®"+OWNER = "adonis"++# --------------------------------------------------------------- land pattern+# p.15 "Recommended Solder Pad": two 1.2 x 3.0 pads, 3.7 outer-to-outer.+PAD_W, PAD_H, PAD_SPAN = 1.20, 3.00, 3.70+PAD_X = (PAD_SPAN - PAD_W) / 2.0          # 1.25 -> pitch 2.5+BODY_W, BODY_D = 4.00, 3.63               # p.14 top view+LENS_DIA = 2.40                           # p.14 front view+CHAMFER = 0.50+SILK_X, SILK_Y = BODY_W / 2 + 0.10, BODY_D / 2 + 0.10+CRTY_X, CRTY_Y = BODY_W / 2 + 0.25, BODY_D / 2 + 0.25++PINS = [+    dict(number="1", name="K", type="passive", pad_x=-PAD_X,+         description=(+             "Cathode. The datasheet's dimensional drawing (p.14) labels this "+             "lead 'Cathode' and puts the cathode marking on this side of the "+             "package; the chamfered body corner in the 3D model and the "+             "silkscreen triangle both mark it. Return the LED current here.")),+    dict(number="2", name="A", type="passive", pad_x=+PAD_X,+         description=(+             "Anode. Drive at IF = 20 mA typical, 5 mA min to 50 mA max (p.3). "+             "VF is 2.90 V min / 3.00 V typ / 3.50 V max at 20 mA (p.4), so "+             "size the series resistor from VF max, not typ. Never connect to "+             "a fixed supply without current limiting.")),+]++SPECS = [+    ("Manufacturer", MFR),+    ("Package", "SIDELED®, 2-lead gull-wing, side-emitting"),+    ("Category", "LED"),+    ("Pins", "2"),+    ("Colour", "White, Cx = 0.33 / Cy = 0.33 (CIE 1931)"),+    ("Luminous intensity", "900 … 2240 mcd at IF = 20 mA"),+    ("Forward voltage", "3.00 V typ, 3.50 V max at IF = 20 mA"),+    ("Forward current", "20 mA typ, 50 mA max (5 mA min)"),+    ("Viewing angle", "120° (2φ at 50 % IV)"),+    ("Body size", "4.00 × 3.63 × 3.97 mm (0.80 mm standoff)"),+]++ELECTRICAL = [+    ("Forward voltage", "VF", "2.90", "3.00", "3.50", "V", "IF = 20 mA (p.4)"),+    ("Forward current", "IF", "5", "20", "50", "mA", "TS = 25 °C (p.3)"),+    ("Surge current", "IFS", "—", "—", "300", "mA",+     "t ≤ 10 µs, D = 0.005 (p.3)"),+    ("Reverse voltage", "VR", "—", "—", "5", "V", "TS = 25 °C (p.3)"),+    ("Reverse current", "IR", "—", "0.01", "10", "µA", "VR = 5 V (p.4)"),+    ("Luminous intensity", "IV", "900", "—", "2240", "mcd",+     "IF = 20 mA, bins V2…BA (p.2, p.5)"),+    ("Chromaticity", "Cx / Cy", "—", "0.33 / 0.33", "—", "—",+     "CIE 1931, bin group FK0PN0 (p.4)"),+    ("Thermal resistance", "RthJS", "—", "—", "220", "K/W",+     "junction to solder point, real (p.4)"),+    ("Thermal resistance", "RthJA", "—", "—", "380", "K/W",+     "junction to ambient, real (p.4)"),+    ("Junction temperature", "Tj", "—", "—", "125", "°C", "p.3"),+    ("Operating temperature", "Top", "-40", "—", "110", "°C", "p.3"),+]+++def w(rel, text):+    path = os.path.join(HERE, rel)+    os.makedirs(os.path.dirname(path), exist_ok=True)+    with open(path, "w") as fh:+        fh.write(text)+    print("  wrote %-42s %6d B" % (rel, len(text.encode())))+++# ------------------------------------------------------------------- symbol+def kicad_sym():+    def pin(p, x, ang, name):+        return (+            '    (pin passive line (at %.2f 0.00 %d) (length 2.54)\n'+            '      (name "%s" (effects (font (size 1.27 1.27))))\n'+            '      (number "%s" (effects (font (size 1.27 1.27))))\n'+            '    )' % (x, ang, name, p))++    stroke = '(stroke (width 0.254) (type default)) (fill (type %s))'+    body = [+        # cathode bar, then the anode triangle pointing into it+        '    (polyline (pts (xy -1.27 -1.27) (xy -1.27 1.27))\n      %s)'+        % (stroke % 'none'),+        '    (polyline (pts (xy 1.27 1.27) (xy 1.27 -1.27) (xy -1.27 0.00)'+        ' (xy 1.27 1.27))\n      %s)' % (stroke % 'none'),+    ]+    # two emission arrows, the LED convention+    for dx in (-0.55, 0.75):+        body.append(+            '    (polyline (pts (xy %.2f 1.60) (xy %.2f 2.70))\n      %s)'+            % (dx, dx + 1.10, stroke % 'none'))+        body.append(+            '    (polyline (pts (xy %.2f 2.70) (xy %.2f 2.24) (xy %.2f 2.34)'+            ' (xy %.2f 2.70))\n      %s)'+            % (dx + 1.10, dx + 1.02, dx + 0.64, dx + 1.10, stroke % 'outline'))+    return (+        '(kicad_symbol_lib (version 20211014) (generator adom_sideled)\n'+        '  (symbol "%s" (pin_names (offset 0.762) hide) (in_bom yes) (on_board yes)\n'+        '    (property "Reference" "D" (at 0 5.08 0) (effects (font (size 1.27 1.27))))\n'+        '    (property "Value" "%s" (at 0 -3.81 0) (effects (font (size 1.27 1.27))))\n'+        '    (property "Footprint" "%s:%s" (at 0 0 0) (effects (font (size 1.27 1.27)) hide))\n'+        '    (property "Datasheet" "docs/%s-datasheet.pdf" (at 0 0 0) (effects (font (size 1.27 1.27)) hide))\n'+        '    (property "Manufacturer" "%s" (at 0 0 0) (effects (font (size 1.27 1.27)) hide))\n'+        '    (property "Description" "White SIDELED, side-emitting, 900-2240 mcd at 20 mA" (at 0 0 0) (effects (font (size 1.27 1.27)) hide))\n'+        '    (symbol "%s_0_1"\n%s\n    )\n'+        '    (symbol "%s_1_1"\n%s\n    )\n'+        '  )\n)\n'+        % (SYM, MPN, SLUG, FP, SLUG, MFR, SYM, "\n".join(body), SYM,+           "\n".join([pin("1", -3.81, 0, "K"), pin("2", 3.81, 180, "A")])))+++# ---------------------------------------------------------------- footprint+def kicad_mod():+    def pad(n, x, name):+        return (+            '\t(pad "%s" smd roundrect\n'+            '\t\t(at %.3f 0)\n\t\t(size %.2f %.2f)\n'+            '\t\t(layers "F.Cu" "F.Paste" "F.Mask")\n'+            '\t\t(roundrect_rratio 0.25)\n'+            '\t\t(uuid "0000pad-%s")\n\t)' % (n, x, PAD_W, PAD_H, n))++    def line(layer, x1, y1, x2, y2, width):+        return ('\t(fp_line\n\t\t(start %.3f %.3f)\n\t\t(end %.3f %.3f)\n'+                '\t\t(stroke\n\t\t\t(width %.3f)\n\t\t\t(type solid)\n\t\t)\n'+                '\t\t(layer "%s")\n\t)' % (x1, y1, x2, y2, width, layer))++    def rect(layer, hx, hy, width):+        return "\n".join([+            line(layer, -hx, -hy, hx, -hy, width),+            line(layer, hx, -hy, hx, hy, width),+            line(layer, hx, hy, -hx, hy, width),+            line(layer, -hx, hy, -hx, -hy, width)])++    hx, hy = BODY_W / 2, BODY_D / 2+    fab = "\n".join([+        line("F.Fab", -hx, -hy + CHAMFER, -hx + CHAMFER, -hy, 0.10),+        line("F.Fab", -hx + CHAMFER, -hy, hx, -hy, 0.10),+        line("F.Fab", hx, -hy, hx, hy, 0.10),+        line("F.Fab", hx, hy, -hx, hy, 0.10),+        line("F.Fab", -hx, hy, -hx, -hy + CHAMFER, 0.10),+        # lens window on the emitting (-Y) face, and the emission arrow+        '\t(fp_circle\n\t\t(center 0 %.3f)\n\t\t(end %.3f %.3f)\n'+        '\t\t(stroke\n\t\t\t(width 0.100)\n\t\t\t(type solid)\n\t\t)\n'+        '\t\t(fill none)\n\t\t(layer "F.Fab")\n\t)' % (-hy, LENS_DIA / 2, -hy),+        line("F.Fab", 0, -hy - 0.35, 0, -hy - 1.15, 0.10),+        line("F.Fab", 0, -hy - 1.15, -0.30, -hy - 0.85, 0.10),+        line("F.Fab", 0, -hy - 1.15, 0.30, -hy - 0.85, 0.10),+    ])+    silk = rect("F.SilkS", SILK_X, SILK_Y, 0.12) + "\n" + (+        # cathode / pin-1 triangle, outside the body on the -X side+        '\t(fp_poly\n\t\t(pts\n\t\t\t(xy %.3f %.3f) (xy %.3f %.3f) (xy %.3f %.3f)\n\t\t)\n'+        '\t\t(stroke\n\t\t\t(width 0.050)\n\t\t\t(type solid)\n\t\t)\n'+        '\t\t(fill solid)\n\t\t(layer "F.SilkS")\n\t)'+        % (-PAD_X, -SILK_Y - 0.10, -PAD_X - 0.24, -SILK_Y - 0.53,+           -PAD_X + 0.24, -SILK_Y - 0.53))+    crtyd = rect("F.CrtYd", CRTY_X, CRTY_Y, 0.05)++    def prop(name, value, layer, y, hide=""):+        return ('\t(property "%s" "%s"\n\t\t(at 0 %.3f 0)\n\t\t(layer "%s")\n%s'+                '\t\t(uuid "0000prop-%s")\n\t\t(effects\n\t\t\t(font\n'+                '\t\t\t\t(size 1 1)\n\t\t\t\t(thickness 0.15)\n\t\t\t)\n\t\t)\n\t)'+                % (name, value, y, layer, hide, name.lower()))++    return (+        '(footprint "%s"\n'+        '\t(version 20240108)\n'+        '\t(generator "adom_sideled")\n'+        '\t(generator_version "9.0")\n'+        '\t(layer "F.Cu")\n'+        '\t(descr "ams OSRAM SIDELED, 2-lead side-emitting LED, 4.0 x 3.63 x 3.97 mm. '+        'Land pattern per the Recommended Solder Pad drawing, LW A6SG binning FK0PN0 v1.1 p.15: '+        '2 x (1.2 x 3.0) mm pads, 3.7 mm outer span. Emission is toward -Y.")\n'+        '\t(tags "LED SIDELED OSRAM side-emitting white")\n'+        '\t(attr smd)\n'+        '%s\n%s\n%s\n%s\n%s\n%s\n%s\n'+        '\t(embedded_fonts no)\n'+        '\t(model "${KIPRJMOD}/%s.step"\n'+        '\t\t(offset\n\t\t\t(xyz 0 0 0)\n\t\t)\n'+        '\t\t(scale\n\t\t\t(xyz 1 1 1)\n\t\t)\n'+        '\t\t(rotate\n\t\t\t(xyz 0 0 0)\n\t\t)\n\t)\n)\n'+        % (FP,+           prop("Reference", "D**", "F.SilkS", -3.10),+           prop("Value", MPN, "F.Fab", 3.10),+           prop("Datasheet", "docs/%s-datasheet.pdf" % SLUG, "F.Fab", 0,+                "\t\t(hide yes)\n"),+           silk, fab, crtyd,+           "\n".join([pad(p["number"], p["pad_x"], p["name"]) for p in PINS]),+           SLUG))+++# ------------------------------------------------------------------ EAGLE+def lbr():+    layers = "".join(+        '<layer number="%d" name="%s" color="%d" fill="%d" visible="%s" active="yes"/>\n'+        % t for t in [+            (1, "Top", 4, 1, "yes"), (16, "Bottom", 1, 1, "yes"),+            (17, "Pads", 2, 1, "yes"), (18, "Vias", 2, 1, "yes"),+            (20, "Dimension", 15, 1, "yes"), (21, "tPlace", 7, 1, "yes"),+            (22, "bPlace", 7, 1, "yes"), (23, "tOrigins", 15, 1, "yes"),+            (25, "tNames", 7, 1, "yes"), (27, "tValues", 7, 1, "yes"),+            (29, "tStop", 7, 3, "no"), (31, "tCream", 7, 4, "no"),+            (39, "tKeepout", 4, 11, "no"), (51, "tDocu", 7, 1, "yes"),+            (94, "Symbols", 4, 1, "yes"), (95, "Names", 7, 1, "yes"),+            (96, "Values", 7, 1, "yes"), (97, "Info", 7, 1, "yes")])+    # EAGLE's Y axis points the opposite way to KiCad's, hence the sign flips.+    smds = "".join(+        '  <smd name="%s" x="%.4f" y="0.0000" dx="%.4f" dy="%.4f" layer="1" roundness="25"/>\n'+        % (p["number"], p["pad_x"], PAD_W, PAD_H) for p in PINS)+    hx, hy = BODY_W / 2, BODY_D / 2+    wires = "".join(+        '  <wire x1="%.4f" y1="%.4f" x2="%.4f" y2="%.4f" width="%.4f" layer="%d"/>\n' % t+        for t in [+            (-SILK_X, -SILK_Y, SILK_X, -SILK_Y, 0.12, 21),+            (SILK_X, -SILK_Y, SILK_X, SILK_Y, 0.12, 21),+            (SILK_X, SILK_Y, -SILK_X, SILK_Y, 0.12, 21),+            (-SILK_X, SILK_Y, -SILK_X, -SILK_Y, 0.12, 21),+            (-hx, hy - CHAMFER, -hx + CHAMFER, hy, 0.10, 51),+            (-hx + CHAMFER, hy, hx, hy, 0.10, 51),+            (hx, hy, hx, -hy, 0.10, 51),+            (hx, -hy, -hx, -hy, 0.10, 51),+            (-hx, -hy, -hx, hy - CHAMFER, 0.10, 51)])+    return (+        '<?xml version="1.0" encoding="utf-8"?>\n'+        '<!DOCTYPE eagle SYSTEM "eagle.dtd">\n<eagle version="9.6.2">\n<drawing>\n'+        '<settings><setting alwaysvectorfont="no"/></settings>\n'+        '<grid distance="0.1" unitdist="inch" unit="inch" style="lines" multiple="1"'+        ' display="no" altdistance="0.01" altunitdist="inch" altunit="inch"/>\n'+        '<layers>\n%s</layers>\n<library>\n'+        '<description>%s &#8212; %s %s</description>\n'+        '<packages>\n<package name="%s">\n'+        '  <description>%s SIDELED, 2-lead side-emitting LED</description>\n'+        '%s%s'+        '  <circle x="0" y="%.4f" radius="%.4f" width="0.1" layer="51"/>\n'+        '  <text x="0" y="%.3f" size="1.27" layer="25" align="bottom-center">&gt;NAME</text>\n'+        '  <text x="0" y="%.3f" size="1.27" layer="27" align="top-center">&gt;VALUE</text>\n'+        '<!--adom-pin1--><polygon width="0.0500" layer="21">'+        '<vertex x="%.4f" y="%.4f"/><vertex x="%.4f" y="%.4f"/>'+        '<vertex x="%.4f" y="%.4f"/></polygon>\n'+        '</package>\n</packages>\n'+        '<symbols>\n<symbol name="%s">\n'+        '  <wire x1="-1.27" y1="-1.27" x2="-1.27" y2="1.27" width="0.254" layer="94"/>\n'+        '  <wire x1="1.27" y1="1.27" x2="1.27" y2="-1.27" width="0.254" layer="94"/>\n'+        '  <wire x1="1.27" y1="-1.27" x2="-1.27" y2="0" width="0.254" layer="94"/>\n'+        '  <wire x1="-1.27" y1="0" x2="1.27" y2="1.27" width="0.254" layer="94"/>\n'+        '  <wire x1="-0.55" y1="1.6" x2="0.55" y2="2.7" width="0.2" layer="94"/>\n'+        '  <wire x1="0.75" y1="1.6" x2="1.85" y2="2.7" width="0.2" layer="94"/>\n'+        '  <text x="0" y="5.08" size="1.27" layer="95" align="bottom-center">&gt;NAME</text>\n'+        '  <text x="0" y="-3.81" size="1.27" layer="96" align="top-center">&gt;VALUE</text>\n'+        '  <pin name="K" x="-3.81" y="0" length="short" direction="pas"/>\n'+        '  <pin name="A" x="3.81" y="0" length="short" direction="pas" rot="R180"/>\n'+        '</symbol>\n</symbols>\n'+        '<devicesets>\n<deviceset name="%s" prefix="D">\n'+        '  <description>%s white SIDELED, 900-2240 mcd at IF = 20 mA</description>\n'+        '  <gates><gate name="G$1" symbol="%s" x="0" y="0"/></gates>\n'+        '  <devices>\n    <device name="" package="%s">\n      <connects>\n'+        '        <connect gate="G$1" pin="K" pad="1"/>\n'+        '        <connect gate="G$1" pin="A" pad="2"/>\n'+        '      </connects>\n'+        '      <technologies><technology name=""/></technologies>\n'+        '    </device>\n  </devices>\n</deviceset>\n</devicesets>\n'+        '</library>\n</drawing>\n</eagle>\n'+        % (layers, MPN, MFR, PKG, FP, MFR, smds, wires,+           hy, LENS_DIA / 2, SILK_Y + 0.7, -SILK_Y - 0.7,+           -PAD_X, SILK_Y + 0.10, -PAD_X - 0.24, SILK_Y + 0.53,+           -PAD_X + 0.24, SILK_Y + 0.53,+           SYM, SYM, MFR, SYM, FP))+++# ------------------------------------------------------------------- viewers+TPL = open(os.path.join(HERE, "tools", "viewer.tpl.html")).read()+++def viewer(kind, bar, svg, title):+    pins = [dict(number=p["number"], name=p["name"], type=p["type"],+                 desc=p["description"]) for p in PINS]+    return (TPL+            .replace("@@TITLE@@", title)+            .replace("@@BAR@@", bar)+            .replace("@@SVG@@", svg)+            .replace("@@KIND@@", kind)+            .replace("@@MPN@@", "%s/%s" % (OWNER.upper(), SYM))+            .replace("@@PINS@@", json.dumps(pins, sort_keys=True)))+++def symbol_svg():+    S = 100.0   # px per mm+    def X(v): return v * S+    def Y(v): return -v * S     # SVG y grows downward, KiCad symbol y grows up+    out = []+    out.append("<polyline points='%.1f,%.1f %.1f,%.1f' fill='none' stroke='#e35d5d' stroke-width='4'/>"+               % (X(-1.27), Y(-1.27), X(-1.27), Y(1.27)))+    out.append("<polygon points='%.1f,%.1f %.1f,%.1f %.1f,%.1f' fill='#e35d5d22' stroke='#e35d5d' stroke-width='4'/>"+               % (X(1.27), Y(1.27), X(1.27), Y(-1.27), X(-1.27), Y(0)))+    for dx in (-0.55, 0.75):+        out.append("<polyline points='%.1f,%.1f %.1f,%.1f' fill='none' stroke='#dfe4ec' stroke-width='4'/>"+                   % (X(dx), Y(1.6), X(dx + 1.1), Y(2.7)))+        out.append("<polygon points='%.1f,%.1f %.1f,%.1f %.1f,%.1f' fill='#dfe4ec'/>"+                   % (X(dx + 1.10), Y(2.70), X(dx + 1.02), Y(2.24), X(dx + 0.64), Y(2.34)))+    for p, x0, x1 in ((PINS[0], -3.81, -1.27), (PINS[1], 3.81, 1.27)):+        out.append("<g class='pingroup' data-num='%s' data-name='%s'>"+                   "<line class='pinvis' x1='%.1f' y1='%.1f' x2='%.1f' y2='%.1f' stroke='#dfe4ec' stroke-width='4'/>"+                   "<line x1='%.1f' y1='%.1f' x2='%.1f' y2='%.1f' stroke='transparent' stroke-width='26' pointer-events='stroke'/></g>"+                   % (p["number"], p["name"], X(x0), Y(0), X(x1), Y(0),+                      X(x0), Y(0), X(x1), Y(0)))+        out.append("<text x='%.1f' y='%.1f' font-size='60' fill='#9fb0c8' text-anchor='middle' dominant-baseline='central'>%s</text>"+                   % (X((x0 + x1) / 2), Y(0.45), p["number"]))+        out.append("<text x='%.1f' y='%.1f' font-size='60' fill='#e6e9ef' text-anchor='middle' dominant-baseline='central'>%s</text>"+                   % (X((x0 + x1) / 2), Y(-0.55), p["name"]))+    return ("<svg xmlns='http://www.w3.org/2000/svg' viewBox='-560 -340 1120 640'"+            " style='width:100%%;height:100%%'>%s</svg>" % "".join(out))+++def footprint_svg():+    out = []+    def line(cls, colour, x1, y1, x2, y2, wid):+        out.append("<line class='%s' x1='%.3f' y1='%.3f' x2='%.3f' y2='%.3f'"+                   " stroke='%s' stroke-width='%.3f'/>"+                   % (cls, x1, y1, x2, y2, colour, wid))+    hx, hy = BODY_W / 2, BODY_D / 2+    for a, b, c, d in ((-SILK_X, -SILK_Y, SILK_X, -SILK_Y),+                       (SILK_X, -SILK_Y, SILK_X, SILK_Y),+                       (SILK_X, SILK_Y, -SILK_X, SILK_Y),+                       (-SILK_X, SILK_Y, -SILK_X, -SILK_Y)):+        line("ly-silk", "#d8b4fe", a, b, c, d, 0.12)+    for a, b, c, d in ((-CRTY_X, -CRTY_Y, CRTY_X, -CRTY_Y),+                       (CRTY_X, -CRTY_Y, CRTY_X, CRTY_Y),+                       (CRTY_X, CRTY_Y, -CRTY_X, CRTY_Y),+                       (-CRTY_X, CRTY_Y, -CRTY_X, -CRTY_Y)):+        line("ly-courtyard", "#4ade80", a, b, c, d, 0.05)+    for a, b, c, d in ((-hx, -hy + CHAMFER, -hx + CHAMFER, -hy),+                       (-hx + CHAMFER, -hy, hx, -hy),+                       (hx, -hy, hx, hy), (hx, hy, -hx, hy),+                       (-hx, hy, -hx, -hy + CHAMFER)):+        line("ly-fab", "#f472b6", a, b, c, d, 0.10)+    out.append("<circle class='ly-fab' cx='0' cy='%.3f' r='%.3f' fill='none'"+               " stroke='#f472b6' stroke-width='0.10'/>" % (-hy, LENS_DIA / 2))+    for a, b, c, d in ((0, -hy - 0.35, 0, -hy - 1.15),+                       (0, -hy - 1.15, -0.30, -hy - 0.85),+                       (0, -hy - 1.15, 0.30, -hy - 0.85)):+        line("ly-fab", "#f472b6", a, b, c, d, 0.10)+    for p in PINS:+        out.append("<rect class='ly-copper pad' data-num='%s' data-name='%s'"+                   " x='%.3f' y='%.3f' width='%.3f' height='%.3f' rx='%.3f'"+                   " fill='#f59e0b' stroke='#00000033' stroke-width='0.03' opacity='0.94'/>"+                   % (p["number"], p["name"], p["pad_x"] - PAD_W / 2, -PAD_H / 2,+                      PAD_W, PAD_H, PAD_W * 0.25))+        out.append("<text class='padnum' x='%.3f' y='0' font-size='0.540' fill='#1c1206'"+                   " text-anchor='middle' dominant-baseline='central' pointer-events='none'"+                   " font-family='ui-monospace,monospace' font-weight='700'>%s</text>"+                   % (p["pad_x"], p["number"]))+    out.append("<polygon class='ly-silk adom-pin1' points='%.3f,%.3f %.3f,%.3f %.3f,%.3f'"+               " fill='#d8b4fe' stroke='none'><title>pin 1 (cathode)</title></polygon>"+               % (-PAD_X, -SILK_Y - 0.10, -PAD_X - 0.24, -SILK_Y - 0.53,+                  -PAD_X + 0.24, -SILK_Y - 0.53))+    pad = 0.45+    vw, vh = 2 * CRTY_X + 2 * pad, 2 * CRTY_Y + 2 * pad + 0.6+    return ("<svg xmlns='http://www.w3.org/2000/svg' viewBox='%.3f %.3f %.3f %.3f'"+            " style='width:100%%;height:100%%'>%s</svg>"+            % (-CRTY_X - pad, -CRTY_Y - pad - 0.6, vw, vh, "".join(out)))+++# --------------------------------------------------------------------- JSON+def pads_json():+    return json.dumps({+        "schema": "adom.pads/1",+        "slug": SLUG,+        "footprint": FP,+        "generated_from": {+            "footprint": "datasheet:[email protected]",+            "pins": "page.json:component.pins",+            "overrides": None,+        },+        "pin1": {"pad": "1", "at": [-PAD_X, 0.0], "corner": "left",+                 "source": "datasheet-cathode-marking",+                 "derived_from": "[email protected]"},+        "pad_count": 2,+        "symbol_pins": [p["number"] for p in PINS],+        "paste_apertures": 0,+        "issues": [],+        "pads": [{+            "pad": p["number"], "kind": "smd", "shape": "roundrect",+            "at": [p["pad_x"], 0.0], "rot": 0.0, "size": [PAD_W, PAD_H],+            "layers": ["F.Cu", "F.Mask", "F.Paste"], "thermal": False,+            "pin": p["number"], "name": p["name"], "type": p["type"],+            "description": p["description"], "is_pin1": p["number"] == "1",+            "source": {+                "geometry": "LW A6SG binning FK0PN0 v1.1, p.15 Recommended Solder Pad",+                "meaning": "page.json:component.pins",+                "correspondence": "kicad: symbol pin number == pad name"},+        } for p in PINS],+    }, indent=1) + "\n"+++FILES = ["page.json", "README.md",+         "%s.kicad_sym" % SLUG, "%s.kicad_mod" % SLUG,+         "%s.glb" % SLUG, "%s.step" % SLUG, "%s.lbr" % SLUG,+         "docs/hero.png", "docs/%s-datasheet.pdf" % SLUG,+         "viewers/symbol.html", "viewers/footprint.html", "pads.json",+         "skills/sideled-lw-a6sg/SKILL.md"]+++def page_json(sha):+    return json.dumps({+        "schema_version": 1,+        "type": "component",+        "kind": "component",+        "slug": SLUG,+        "title": MPN,+        "brief": "ams OSRAM Standard LEDs - SMD, White SIDELED®, side-emitting. "+                 "Datasheet-verified pinout, KiCad symbol/footprint/3D.",+        "version": "0.1.2",+        "license": "CC-BY-SA-4.0",+        "tags": ["led", "sideled", "side-emitting", "kicad", "automotive", SLUG],+        "component": {+            "mpn": MPN,+            "manufacturer": MFR,+            "package": PKG,+            "pin_count": 2,+            "category": "LED",+            "subcategory": "Standard LEDs - SMD, White SIDELED®",+            "datasheet": "docs/%s-datasheet.pdf" % SLUG,+            "parts": {+                "symbol": "%s.kicad_sym" % SLUG,+                "footprint": "%s.kicad_mod" % SLUG,+                "model_3d": "%s.glb" % SLUG,+                "model_3d_step": "%s.step" % SLUG,+                "lbr": "%s.lbr" % SLUG,+            },+            "distributor_links": {+                "mouser": "https://www.mouser.com/en/ProductDetail/ams-OSRAM/LW-A6SG-V2BA-FK0PN0?qs=vLWxofP3U2xiZuGh4CU2Qw%3D%3D",+                "digikey": "https://www.digikey.com/en/products/detail/ams-osram-ag/LW-A6SG-V2BA-FK0PN0-0-20-R33/13624925",+            },+            "pins": [{"number": p["number"], "name": p["name"],+                      "type": p["type"], "description": p["description"]}+                     for p in PINS],+            "provenance": {+                "extractor": "adom component pipeline (Claude)",+                "datasheet": {+                    "url": "docs/%s-datasheet.pdf" % SLUG,+                    "title": "LW A6SG binning FK0PN0",+                    "version": "1.1",+                    "dated": "2020-06-01",+                    "pages": 25,+                    "sha256": sha[:16],+                    "source_tier": "manufacturer",+                    "retrieved": "2026-08-27",+                },+                "cad": "Generated from the manufacturer datasheet "+                       "(land pattern p.15, package outline p.14)",+                "pins": {"total": 2, "described": 2, "cites_pages": True,+                         "refuses_to_fabricate": True},+            },+            "viewers": {"symbol": "viewers/symbol.html",+                        "footprint": "viewers/footprint.html"},+            "pin1": {"pad": "1", "at": [-PAD_X, 0.0], "corner": "left",+                     "source": "datasheet-cathode-marking",+                     "derived_from": "[email protected]",+                     "mark": "drawn"},+        },+        "metadata": {"specs": dict(SPECS)},+        "sections": ["hero", "viewers", "readme", "breadcrumbs", "info"],+        "discovery_triggers": TRIGGERS,+        "discovery_pitch": PITCH,+        "author": {"name": "adonis", "email": "[email protected]"},+        "dependencies": {},+        "hero": {"type": "image", "path": "docs/hero.png"},+        "files": FILES,+    }, indent=2) + "\n"+++TRIGGERS = [SLUG, "lw a6sg", "a6sg", "sideled", "osram sideled",+            "white sideled", "side emitting led", "side firing led",+            "q65113a1198", "light pipe led"]+PITCH = ("LW A6SG-V2BA-FK0PN0 (ams OSRAM) white SIDELED - side-emitting LED "+         "with datasheet-verified pinout, land pattern, KiCad symbol/footprint "+         "and 3D model.")+++def package_json():+    return json.dumps({+        "slug": SLUG,+        "version": "0.1.2",+        "type": "component",+        "description": "ams OSRAM Standard LEDs - SMD, White SIDELED®, "+                       "side-emitting, 900-2240 mcd at IF = 20 mA. "+                       "Datasheet-verified pinout, KiCad symbol/footprint/3D.",+        "tags": ["led", "sideled", "side-emitting", "kicad", "automotive", SLUG],+        "kind": "component",+        "discovery_triggers": TRIGGERS,+        "discovery_pitch": PITCH,+        "dependencies": {},+        "hero": {"type": "image", "path": "docs/hero.png"},+        # The hero lives in the page repo after the first publish materialised+        # it; keeping it out of the tarball stops every installer re-downloading+        # 1 MB of page art on each update.+        "files": [f for f in FILES if f != "docs/hero.png"],+    }, indent=2) + "\n"+++def readme(sha):+    pin_rows = "\n".join(+        "| %s | %s | %s | %s |" % (p["number"], p["name"], p["type"],+                                   p["description"]) for p in PINS)+    spec_rows = "\n".join("| %s | %s |" % kv for kv in SPECS)+    el_rows = "\n".join("| %s | %s | %s | %s | %s | %s | %s |" % r+                        for r in ELECTRICAL)+    return """# %(mpn)s++**%(mfr)s** · Standard LEDs - SMD, White SIDELED® · %(pkg)s++A white, side-emitting SIDELED® from %(mfr)s: light leaves through the domed+lens on one *vertical* face, parallel to the board, rather than upward. ThinGaN+chip, diffused silicone resin, 120° Lambertian, binned to Cx = Cy = 0.33 and+900–2240 mcd at 20 mA. Ordering code Q65113A1198.++Pinout, land pattern and body dimensions are read off the manufacturer+datasheet; the symbol, footprint and 3D model are generated from those numbers.++## Pinout++| Pin | Name | Type | Function |+|---|---|---|---|+%(pins)s++Pin 1 is the cathode. The package carries a cathode marking on that side+(datasheet p.14); the footprint repeats it as a filled silkscreen triangle and+the 3D model as a chamfered body corner.++## Key specifications++| Parameter | Value |+|---|---|+%(specs)s++## Electrical and optical++| Parameter | Symbol | Min | Typ | Max | Unit | Conditions |+|---|---|---|---|---|---|---|+%(elec)s++Brightness bins for this ordering code: V2 (900–1120 mcd), AA (1120–1400),+AB (1400–1800), BA (1800–2240), all at IF = 20 mA (p.5). Chromaticity bin+group FK0…PN0 (p.6–8). MSL 2 per JEDEC J-STD-020E, reflow peak 245 °C+(p.16). ESD 2 kV HBM Class 2. Corrosion robustness class 1B. Approximate+weight 67.0 mg.++## Land pattern++From the datasheet's *Recommended Solder Pad* drawing (p.15):++| | Value |+|---|---|+| Pads | 2 × SMD roundrect, 1.20 × 3.00 mm |+| Outer span | 3.70 mm (pads centred at x = ±1.25 mm) |+| Copper area | > 16 mm² per pad recommended for heat dissipation |+| Courtyard | 4.50 × 4.13 mm |++Lead spacing on the part is 2.54 mm (p.14) while the recommended land is drawn+on a 2.50 mm pitch — the 0.04 mm difference sits well inside the 1.2 mm pad for+a 1.0 mm lead, and the footprint follows the manufacturer's land drawing.++The datasheet asks for a nitrogen reflow atmosphere for best joint quality, and+the package is **not** suitable for ultrasonic cleaning (p.15).++## CAD++- **Symbol** `%(sym)s` — standard 2-pin LED, pin 1 K / pin 2 A.+- **Footprint** `%(fp)s` — emission direction is **−Y**, marked with an arrow+  and the Ø2.4 mm lens window on F.Fab.+- **3D** `%(slug)s.step` (+ GLB) — 4.00 × 3.63 × 4.77 mm envelope including the+  0.80 mm lead standoff, origin on the land pattern, part sitting on Z = 0.+- **EAGLE** `%(slug)s.lbr` — same package, symbol and device set.++## Provenance++Everything here is traceable to the manufacturer datasheet rather than a+distributor listing:++- **Datasheet:** [%(mfr)s, *LW A6SG binning FK0PN0*, v1.1, 2020-06-01](docs/%(slug)s-datasheet.pdf)+  (25 pp, sha256 `%(sha)s`, retrieved 2026-08-27)+- **Pins and electricals:** read from pp. 2–6 and 14–16; every row above cites+  its page.+- **Land pattern:** p.15 *Recommended Solder Pad*, used as drawn.+- **Body outline:** p.14 *Dimensional Drawing* — 4 ±0.2 × 3.63 ±0.2 mm plan,+  3.97 +0.23/−0.17 mm tall, 2.85 mm body depth, 0.80 mm standoff, Ø2.4 mm lens,+  2.54 mm lead spacing, 1 ±0.1 mm lead width.++Two things the datasheet does not dimension, chosen and recorded here rather+than invented silently:++1. **Body-to-land offset.** The solder-pad drawing gives no body-relative+   origin, so the 3.63 mm body envelope is centred on the land pattern.+2. **Lead-frame thickness (0.20 mm) and foot length**, and the size of the+   cathode chamfer (0.50 mm), are representative — they affect the 3D model's+   appearance only, never the land pattern or the pinout.++## Sourcing++| Distributor | Part number | Note |+|---|---|---|+| Mouser | 720-LWA6SGA1366 | %(mpn)s |+| Mouser | 720-LWA6SGA1198 | LW A6SG-V2BA-FK0PN0-0-20-R33 (taped) |+| DigiKey | 475-LWA6SG-V2BA-FK0PN0-0-20-R33TR-ND | taped variant, in stock |++The bare `%(mpn)s` ordering code is marked **obsolete / not recommended for new+designs**; the `-0-20-R33` tape-and-reel variant is the one still stocked. Check+availability before committing a design.++[Datasheet PDF](docs/%(slug)s-datasheet.pdf)+""" % dict(mpn=MPN, mfr=MFR, pkg=PKG, pins=pin_rows, specs=spec_rows,+           elec=el_rows, sym=SYM, fp=FP, slug=SLUG, sha=sha[:16])+++if __name__ == "__main__":+    import hashlib+    sha = hashlib.sha256(+        open(os.path.join(HERE, "docs", "%s-datasheet.pdf" % SLUG), "rb").read()+    ).hexdigest()+    print("datasheet sha256 %s" % sha)+    w("%s.kicad_sym" % SLUG, kicad_sym())+    w("%s.kicad_mod" % SLUG, kicad_mod())+    w("%s.lbr" % SLUG, lbr())+    w("pads.json", pads_json())+    w("page.json", page_json(sha))+    w("package.json", package_json())+    w("README.md", readme(sha))+    w("viewers/symbol.html",+      viewer("symbol", "<span class=h>Schematic symbol</span><span class=sp></span>",+             symbol_svg(), "%s/%s symbol" % (OWNER.upper(), SYM)))+    w("viewers/footprint.html",+      viewer("footprint",+             '<span class=h>PCB footprint</span><span class=sp></span>'+             '<button class="lyrtog on" data-lyr="copper"><span class="d" style="background:#e35d5d"></span>Copper</button>'+             '<button class="lyrtog on" data-lyr="silk"><span class="d" style="background:#e6edf3"></span>Silkscreen</button>'+             '<button class="lyrtog" data-lyr="fab"><span class="d" style="background:#8b949e"></span>Fab</button>'+             '<button class="lyrtog" data-lyr="courtyard"><span class="d" style="background:#bc8cff"></span>Courtyard</button>',+             footprint_svg(), "%s/%s footprint" % (OWNER.upper(), SYM)))
tools/gen_step.pyadded+342
@@ -0,0 +1,342 @@+#!/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))
tools/hero.htmladded+50
@@ -0,0 +1,50 @@+<!doctype html><html><head><meta charset=utf-8>+<link rel="preconnect" href="https://fonts.googleapis.com">+<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>+<link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;700;800&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">+<style>+*{margin:0;padding:0;box-sizing:border-box}+html,body{width:4000px;height:2500px;overflow:hidden}+body{+  background:#0c1016;+  font-family:'Plus Jakarta Sans','Liberation Sans',system-ui,sans-serif;+  -webkit-font-smoothing:antialiased;+}+/* teal glow, right of centre — matches the sibling component heroes */+.orb{position:absolute;right:-620px;top:-520px;width:3100px;height:3100px;border-radius:50%;+  background:radial-gradient(circle at 50% 50%,#16252b 0%,#111d23 38%,#0d151b 62%,#0c1016 78%);}+.bar{position:absolute;left:0;top:0;width:4000px;height:24px;+  background:linear-gradient(90deg,#05e1df 0%,#34bcfc 52%,#8273f7 100%);}+.wrap{position:absolute;left:260px;top:0;width:3480px;height:2500px}+.eyebrow{position:absolute;top:296px;font-size:72px;font-weight:700;letter-spacing:.30em;+  color:#00e6dc;text-transform:uppercase;line-height:1}+#title{position:absolute;top:540px;font-weight:800;color:#eef3f8;line-height:1.0;+  letter-spacing:-.022em;white-space:nowrap}+#sub{position:absolute;font-size:118px;font-weight:500;color:#c4cdd8;line-height:1.14;+  letter-spacing:-.012em;max-width:3400px}+#sub2{position:absolute;font-size:80px;font-weight:400;color:#8b949e;line-height:1.2;+  letter-spacing:-.004em}+.footer{position:absolute;top:2286px;font-family:'JetBrains Mono','Liberation Mono',ui-monospace,monospace;+  font-size:58px;font-weight:400;color:#4d5866;letter-spacing:.06em}+</style></head><body>+<div class=orb></div><div class=bar></div>+<div class=wrap>+  <div class=eyebrow>Component</div>+  <div id=title>LW A6SG-V2BA-FK0PN0</div>+  <div id=sub>Standard LEDs - SMD, White SIDELED&reg;</div>+  <div id=sub2>ams OSRAM &middot; SIDELED&reg; (side-emitting, 2-lead)</div>+  <div class=footer>Manufacturer datasheet &middot; Datasheet-derived CAD &middot; Adom Wiki</div>+</div>+<script>+// Fit the MPN to the 3480px column, then stack the two subtitle lines under it+// at the same rhythm the sibling component heroes use.+var t=document.getElementById('title'), s=document.getElementById('sub'), s2=document.getElementById('sub2');+var size=390;+t.style.fontSize=size+'px';+while(t.getBoundingClientRect().width>3400 && size>120){ size-=4; t.style.fontSize=size+'px'; }+var tb=t.getBoundingClientRect();+s.style.top  = Math.round(tb.bottom+150)+'px';+var sb=s.getBoundingClientRect();+s2.style.top = Math.round(sb.bottom+68)+'px';+document.title='ready:'+size;+</script></body></html>
tools/render_hero.mjsadded+15
@@ -0,0 +1,15 @@+import puppeteer from '/home/adom/project/adom_modules/adom/hero-studio/node_modules/puppeteer-core/lib/esm/puppeteer/puppeteer-core.js';+const CHROME='/home/adom/.cache/puppeteer/chrome/linux-152.0.7977.54/chrome-linux64/chrome';+const [,,src,out]=process.argv;+const b=await puppeteer.launch({executablePath:CHROME,headless:true,+  args:['--no-sandbox','--disable-dev-shm-usage','--font-render-hinting=none','--force-color-profile=srgb']});+const p=await b.newPage();+await p.setViewport({width:4000,height:2500,deviceScaleFactor:1});+await p.goto('file://'+src,{waitUntil:'networkidle0'});+try{ await p.evaluate(()=>document.fonts.ready); }catch(e){}+await new Promise(r=>setTimeout(r,600));+console.log('fit:',await p.title());+console.log('fonts:',await p.evaluate(()=>[...document.fonts].filter(f=>f.status==='loaded').map(f=>f.family+' '+f.weight).join(', ')||'none'));+await p.screenshot({path:out,type:'png'});+await b.close();+console.log('OK:',out);
tools/shot.mjsadded+13
@@ -0,0 +1,13 @@+import puppeteer from '/home/adom/project/adom_modules/adom/hero-studio/node_modules/puppeteer-core/lib/esm/puppeteer/puppeteer-core.js';+const b=await puppeteer.launch({executablePath:'/home/adom/.cache/puppeteer/chrome/linux-152.0.7977.54/chrome-linux64/chrome',headless:true,args:['--no-sandbox','--disable-dev-shm-usage']});+for(const [src,out] of [[process.argv[2],process.argv[3]],[process.argv[4],process.argv[5]]]){+  const p=await b.newPage(); const errs=[];+  p.on('pageerror',e=>errs.push(String(e)));+  await p.setViewport({width:760,height:560});+  await p.goto('file://'+src,{waitUntil:'networkidle0'});+  await new Promise(r=>setTimeout(r,400));+  await p.screenshot({path:out});+  console.log(out, errs.length?('ERRORS: '+errs.join('; ')):'clean');+  await p.close();+}+await b.close();
tools/viewer.tpl.htmladded+65
@@ -0,0 +1,65 @@+<!doctype html><html lang=en><head><meta charset=utf-8>+<meta name=viewport content="width=device-width,initial-scale=1"><title>@@TITLE@@</title>+<style>+:root{--bg:#0d1117;--panel:#161b22;--edge:#30363d;--ink:#e6edf3;--mut:#8b949e;--acc:#00b8b0;--accb:#00e6dc;--hi:#6ea8fe}+*{box-sizing:border-box;margin:0}html,body{height:100%}+body{background:var(--bg);color:var(--ink);font-family:'Satoshi',system-ui,sans-serif;display:flex;flex-direction:column}+.bar{display:flex;flex-wrap:wrap;align-items:center;gap:8px;padding:8px 11px;border-bottom:1px solid var(--edge);flex:none}+.bar .h{font-size:11px;text-transform:uppercase;letter-spacing:.06em;color:var(--mut);font-weight:700}+.bar .sp{flex:1}+.lyrtog{display:inline-flex;align-items:center;gap:6px;font-size:11px;color:var(--mut);background:#1a2130;border:1px solid var(--edge);border-radius:999px;padding:3px 9px;cursor:pointer;user-select:none}+.lyrtog.on{color:var(--ink);border-color:#485162}.lyrtog:not(.on){opacity:.5}+.lyrtog .d{width:8px;height:8px;border-radius:50%;background:#485162}+.stage{flex:1;min-height:0;position:relative;overflow:hidden;background:var(--bg);cursor:grab;touch-action:none}+.stage.drag{cursor:grabbing}+.stage svg{position:absolute;inset:0;width:100%;height:100%}+.pingroup,.pad{cursor:pointer}+.pingroup.hi .pinvis{stroke:var(--hi)!important;stroke-width:7}+rect.pad.hi{stroke:var(--hi);stroke-width:.15;paint-order:stroke}+.lyr-off{display:none}+#tip{position:fixed;z-index:99;pointer-events:none;display:none;max-width:250px;background:#0d1117;border:1px solid var(--acc);border-radius:8px;padding:7px 10px;box-shadow:0 8px 26px #000a}+#tip .tn{font:700 12px ui-monospace,monospace;color:var(--accb)}+#tip .tt{font-size:10px;text-transform:uppercase;letter-spacing:.04em;color:var(--mut);margin-top:1px}+#tip .td{font-size:11px;color:var(--ink);margin-top:5px;line-height:1.4}+</style></head><body>+<div class=bar>@@BAR@@</div>+<div class=stage id=stage>@@SVG@@</div>+<div id=tip></div>+<script>+var KIND="@@KIND@@", MPN="@@MPN@@", PINS=@@PINS@@;+var byNum={}; PINS.forEach(function(p){byNum[String(p.number)]=p;});+var stage=document.getElementById('stage'), svg=stage.querySelector('svg'), tip=document.getElementById('tip');+// ---- pan/zoom ----+var vb=(svg.getAttribute('viewBox')||'0 0 100 100').split(/\s+/).map(Number), base=vb.slice();+function setVB(){svg.setAttribute('viewBox',vb.join(' '));}+stage.addEventListener('wheel',function(e){e.preventDefault();var r=stage.getBoundingClientRect();var mx=(e.clientX-r.left)/r.width,my=(e.clientY-r.top)/r.height;var f=e.deltaY<0?0.88:1.14;var nw=vb[2]*f,nh=vb[3]*f;vb[0]+=(vb[2]-nw)*mx;vb[1]+=(vb[3]-nh)*my;vb[2]=nw;vb[3]=nh;setVB();},{passive:false});+var pan=null;+stage.addEventListener('pointerdown',function(e){pan={x:e.clientX,y:e.clientY};stage.classList.add('drag');stage.setPointerCapture(e.pointerId);});+stage.addEventListener('pointermove',function(e){if(!pan)return;var r=stage.getBoundingClientRect();vb[0]-=(e.clientX-pan.x)/r.width*vb[2];vb[1]-=(e.clientY-pan.y)/r.height*vb[3];pan={x:e.clientX,y:e.clientY};setVB();});+function endPan(){pan=null;stage.classList.remove('drag');}+stage.addEventListener('pointerup',endPan);stage.addEventListener('pointerleave',endPan);+stage.addEventListener('dblclick',function(){vb=base.slice();setVB();});+// ---- cross-highlight bus ----+function els(n){return [].slice.call(document.querySelectorAll('[data-num="'+n+'"]'));}+function clearHi(){[].slice.call(document.querySelectorAll('.hi')).forEach(function(e){e.classList.remove('hi');});}+function hi(n){els(n).forEach(function(e){e.classList.add('hi');});}+function post(n){try{parent.postMessage({type:'lbr-hover',pin:n===null?null:String(n),mpn:MPN},'*');}catch(_){}}+function showTip(n,x,y){var p=byNum[String(n)];if(!p)return;tip.innerHTML='<span class=tn></span><div class=tt></div><div class=td></div>';tip.querySelector('.tn').textContent=n+'  '+(p.name||'');tip.querySelector('.tt').textContent=p.type||'';var d=p.desc||'';if(d)tip.querySelector('.td').textContent=d;else tip.querySelector('.td').style.display='none';tip.style.display='block';tip.style.left=(x+14)+'px';tip.style.top=(y+12)+'px';}+[].slice.call(document.querySelectorAll('.pingroup,.pad')).forEach(function(g){+  var n=g.getAttribute('data-num');+  g.addEventListener('mouseenter',function(e){clearHi();hi(n);post(n);showTip(n,e.clientX,e.clientY);});+  g.addEventListener('mousemove',function(e){tip.style.left=(e.clientX+14)+'px';tip.style.top=(e.clientY+12)+'px';});+  g.addEventListener('mouseleave',function(){clearHi();post(null);tip.style.display='none';});+});+window.addEventListener('message',function(e){var d=e.data||{};if(d.type!=='lbr-hover')return;clearHi();if(d.pin!=null)hi(d.pin);});+// ---- footprint layer toggles ----+[].slice.call(document.querySelectorAll('.lyrtog')).forEach(function(b){+  function apply(on){[].slice.call(document.querySelectorAll('.ly-'+b.getAttribute('data-lyr'))).forEach(function(el){el.classList.toggle('lyr-off',!on);});}+  apply(b.classList.contains('on'));+  b.addEventListener('click',function(){apply(b.classList.toggle('on'));});+});+// ---- auto-grow the host readme iframe ----+function ph(){var h=Math.max(360,Math.min(16000,document.body.scrollHeight||420));parent.postMessage({type:'readme-height',height:h},'*');}+window.addEventListener('load',ph);setTimeout(ph,300);+</script>+</body></html>