main
Caleb Publish 0.1.1 029796b 1d ago
#!/usr/bin/env python3
"""Build the XINGLIGHT XL-1606 side-view LED (XL-1606SURC/UBC/UGC/UOC/UYC) 3D model from the datasheet's
Outline Dimension page (p.7, identical in all five PDFs, PDF dated 2025-11-04), hand-built in CadQuery (Adom, tier 3).

Part frame (the datasheet's front view): X = length 1.7, Y = width 0.6, Z = height 1.1, lens up, pin 1 (cathode) at +X.
Printed: L 1.7, W 0.6, H 1.1, base (terminal) height 0.48, lens top width 1.2, bottom terminals 0.4 each with a 0.9 gap,
top-view terminal strips (1.7 - 1.4) / 2 = 0.15, general tolerance +/-0.25.
Measured off the drawing (521 px/mm at 600 dpi, calibrated on the printed 1.7 and checked on 1.2, 0.48 and 1.1):
lens flank width 1.27 at the base top, top corner radius ~0.08; green cathode mark on the bottom face 0.10-0.30 mm from
the centre toward pin 1, full width. Undimensioned: terminal plating thickness (0.03 mm used, rendering only).

Mounted pose. The LED lies on its side with the lens pointing along the board. Which face lies down (and so on which side
of the lens the cathode is) comes from the tape drawing (p.8), a true top view: in the pocket the lens points in the feed
direction and the cathode is toward the sprocket holes (reel note: the sprocket-hole side is the negative pole; the diode
symbol and the A-A section's green mark agree). Turned so the lens points up the page, the cathode is on the LEFT. The
Recommended Soldering Pattern (p.7) gives the body's position on the pads (lens past the pads' top edge by 0.60, base
lens-side face 0.02 inside that edge, back face 0.03 past the pad centre line; 510.5 px/mm at 600 dpi, calibrated on the
printed 0.77 and 0.95); its outline is the front view pasted in (the green mark it shows sits on a face that is vertical
when mounted), so it cannot say which face is down. The page footprint has pad 1 (cathode) at -X, so the lens points to
footprint -y (KiCad, up) = STEP +Y. Transform part -> STEP: rotate 180 deg about (0, 1, 1)/sqrt(2)
(X_p -> -X, Y_p -> +Z, Z_p -> +Y), then translate (0, -0.03, +0.30).
usage: build_xl1606.py out.step"""
import sys, math
import cadquery as cq

L, W, H, HB = 1.70, 0.60, 1.10, 0.48          # length, width, height, base (terminal) height
TB, TT, T = 0.40, 0.15, 0.03                  # terminal length on bottom/front/back, on top, plating thickness
LENS_BASE, LENS_TOP, R_TOP = 1.27, 1.20, 0.08
MARK = (0.10, 0.30, 0.01)                     # cathode mark x-range (toward pin 1) and depth

def terminal(sign):
    x0, x1 = L / 2 - TB, L / 2
    outer = cq.Workplane('XY').box(TB, W, HB, centered=False).translate((x0, -W / 2, 0))
    inner = cq.Workplane('XY').box(TB - T + 0.01, W - 2 * T, HB - 2 * T, centered=False).translate((x0 - 0.01, -W / 2 + T, T))
    top_open = cq.Workplane('XY').box(TB - TT + 0.01, W + 0.02, T + 0.01, centered=False).translate((x0 - 0.01, -W / 2 - 0.01, HB - T))
    t = outer.cut(inner).cut(top_open)
    return t.mirror('YZ') if sign < 0 else t

def build():
    term_p, term_n = terminal(+1), terminal(-1)            # +X = pin 1 (cathode), -X = pin 2 (anode)
    base = cq.Workplane('XY').box(L, W, HB, centered=(True, True, False)).cut(term_p).cut(term_n)
    mark = cq.Workplane('XY').box(MARK[1] - MARK[0], W, MARK[2], centered=False).translate((MARK[0], -W / 2, 0))
    base = base.cut(mark)
    hb, ht = LENS_BASE / 2, LENS_TOP / 2
    lens = (cq.Workplane('XZ').polyline([(-hb, HB), (hb, HB), (ht, H), (-ht, H)]).close().extrude(W / 2, both=True))
    lens = lens.edges('|Y and >Z').fillet(R_TOP)
    parts = {'body': (base, (0.95, 0.95, 0.93)), 'terminal_K_pin1': (term_p, (0.86, 0.86, 0.84)),
             'terminal_A_pin2': (term_n, (0.86, 0.86, 0.84)), 'lens': (lens, (0.86, 0.93, 0.96)),
             'cathode_mark': (mark, (0.05, 0.62, 0.20))}
    return parts

def mounted(wp):
    s = wp.val().rotate(cq.Vector(0, 0, 0), cq.Vector(0, 1, 1), 180).translate(cq.Vector(0, -0.03, W / 2))
    return s

if __name__ == '__main__':
    out = sys.argv[1]
    frame = sys.argv[2] if len(sys.argv) > 2 else 'mounted'
    a = cq.Assembly(name='XL-1606')
    for name, (wp, col) in build().items():
        shape = mounted(wp) if frame == 'mounted' else wp.val()
        a.add(shape, name=name, color=cq.Color(*col))
    a.save(out, 'STEP')
    print(out)