main
adonis fix polarity: cathode is the +X pad; adopt the official ams OSRAM geometry STEP for the 3D model and body outline b657465 1mo ago
#!/usr/bin/env python3
"""
Place the official ams OSRAM geometry STEP into KiCad 3D-model coordinates.

The vendor file LW_A6SG_20091210_geometry.STEP uses its own frame:

    X_o   lead axis        leads centred at +/-1.27 (2.54 spacing), body +/-2.0
    Y_o   board normal     foot underside at -2.575, package top at +1.450
    Z_o   emission axis    lens plane (the r=1.2 circle) at 0, body back at -3.765

KiCad wants z up with the board at z = 0 and the origin on the land pattern. The
lead's flat contact runs Z_o = -3.475 .. -0.975, so its centre is Z_o = -2.225:

    x = -X_o
    y =  Z_o + 2.225      emission -> +y  (KiCad mirrors y into footprint space,
    z =  Y_o + 2.575      so +y here lands on the footprint's -Y, screen up)

det = +1, a proper rotation: no mirroring, every vendor surface normal stays valid.

Rather than rewrite 459 points and 426 directions -- which would also rewrite the
shape representation's own root placement and so apply the transform twice -- the
whole transform is written into that root placement, #1887. Its location, axis
and ref_direction are each referenced exactly once, so every other byte of the
vendor B-rep is passed through untouched.

**Polarity.** The vendor model carries the cathode marking on the -X_o side: a
cluster of top-face features around X_o = -1.25 with no mirror partner at +1.25.
That matches the datasheet front view, which labels the left-hand lead 'Cathode'.
Under this rotation -X_o becomes +x, so on the footprint -- a view looking DOWN
at the board -- the cathode is on the RIGHT when the lens points up the page.
Reading the datasheet's front view as though it were the top view is what flips
this: a bird's-eye view reverses the lead axis.
"""
import os
import re

DY, DZ = 2.225, 2.575
PLACEMENT = 1887               # the shape representation's own datum axis
NUM = r'(-?[\d.]+(?:[Ee][-+]?\d+)?)'
TRIP = r'\(\s*%s,\s*%s,\s*%s\s*\)' % (NUM, NUM, NUM)


def place(src, dst):
    txt = open(src).read()

    def point(m):
        x, y, z = (float(v) for v in m.group(2, 3, 4))
        return "%s( %.9f, %.9f, %.9f )" % (m.group(1), -x, z + DY, y + DZ)

    def direction(m):
        x, y, z = (float(v) for v in m.group(2, 3, 4))
        return "%s( %.9f, %.9f, %.9f )" % (m.group(1), -x, z, y)

    # Only 3-tuples are model-space geometry; 2-tuples are pcurve parameter
    # space and must pass through untouched.
    txt, npt = re.subn(r'(CARTESIAN_POINT \([^()]*)' + TRIP, point, txt)
    txt, ndir = re.subn(r'(DIRECTION \([^()]*)' + TRIP, direction, txt)

    # The pass above also rotated the representation's own datum axis, which
    # some readers apply as a transform on top of the geometry and some ignore.
    # Pin it to identity so every reader agrees on where the part is. Its three
    # entities are referenced only here, so nothing else moves.
    m = re.search(r'#%d\s*=\s*AXIS2_PLACEMENT_3D\s*\([^;]*?\)\s*;' % PLACEMENT, txt)
    if not m:
        raise SystemExit("root placement #%d not found" % PLACEMENT)
    # [0] is the entity's own id, so the three operands start at [1].
    loc, axis, ref = (int(x) for x in re.findall(r'#(\d+)', m.group(0))[1:4])
    for num, kind, vals in ((loc, 'CARTESIAN_POINT', (0.0, 0.0, 0.0)),
                            (axis, 'DIRECTION', (0.0, 0.0, 1.0)),
                            (ref, 'DIRECTION', (1.0, 0.0, 0.0))):
        uses = len(re.findall(r'#%d\b' % num, txt)) - 1
        if uses != 1:
            raise SystemExit("#%d is shared (%d uses); resetting it would move "
                             "geometry as well" % (num, uses))
        txt, hit = re.subn(r'#%d\s*=\s*%s\s*\([^;]*?\)\s*;' % (num, kind),
                           "#%d = %s ( 'NONE',  ( %.9f, %.9f, %.9f ) ) ;"
                           % (num, kind, *vals), txt, count=1)
        if hit != 1:
            raise SystemExit("#%d is not a %s; the placement operands were "
                             "misread" % (num, kind))

    txt = colourise(txt)
    txt = txt.replace("FILE_NAME ('LW_A6SG_20091210_geometry.STEP'",
                      "FILE_NAME ('lw-a6sg-v2ba-fk0pn0.step'")
    open(dst, 'w').write(txt)
    return npt, ndir


BODY_COLOUR = (0.930, 0.930, 0.915)     # p.1: "white SMT package"


def colourise(txt):
    """Attach one surface style to the solid.

    The vendor file carries no presentation data, so a converted GLB comes out
    untextured default grey. The part is a single MANIFOLD_SOLID_BREP, so one
    colour is all the model can carry; white matches the datasheet's "white SMT
    package". Purely additive -- no existing entity is touched.
    """
    brep = int(re.search(r'#(\d+)\s*=\s*MANIFOLD_SOLID_BREP', txt).group(1))
    ctx = int(re.findall(r'#(\d+)',
              re.search(r'ADVANCED_BREP_SHAPE_REPRESENTATION\s*\([^;]*?\)\s*;',
                        txt).group(0))[-1])
    n = max(int(x) for x in re.findall(r'#(\d+)\s*=', txt))
    ids = {k: n + i for i, k in enumerate(
        ('rgb', 'fac', 'fas', 'ssfa', 'sss', 'ssu', 'psa', 'si', 'mdgpr'), 1)}
    block = "\n".join([
        "#%d = COLOUR_RGB ( 'body', %.6f, %.6f, %.6f ) ;" % (ids['rgb'], *BODY_COLOUR),
        "#%d = FILL_AREA_STYLE_COLOUR ( '', #%d ) ;" % (ids['fac'], ids['rgb']),
        "#%d = FILL_AREA_STYLE ( '', ( #%d ) ) ;" % (ids['fas'], ids['fac']),
        "#%d = SURFACE_STYLE_FILL_AREA ( #%d ) ;" % (ids['ssfa'], ids['fas']),
        "#%d = SURFACE_SIDE_STYLE ( '', ( #%d ) ) ;" % (ids['sss'], ids['ssfa']),
        "#%d = SURFACE_STYLE_USAGE ( .BOTH., #%d ) ;" % (ids['ssu'], ids['sss']),
        "#%d = PRESENTATION_STYLE_ASSIGNMENT ( ( #%d ) ) ;" % (ids['psa'], ids['ssu']),
        "#%d = STYLED_ITEM ( 'colour', ( #%d ), #%d ) ;" % (ids['si'], ids['psa'], brep),
        "#%d = MECHANICAL_DESIGN_GEOMETRIC_PRESENTATION_REPRESENTATION"
        " ( '', ( #%d ), #%d ) ;" % (ids['mdgpr'], ids['si'], ctx),
        "",
    ])
    return txt.replace("ENDSEC;\nEND-ISO-10303-21;", block + "ENDSEC;\nEND-ISO-10303-21;")


if __name__ == '__main__':
    here = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
    npt, ndir = place(os.path.join(here, 'LW_A6SG_20091210_geometry.STEP'),
                      os.path.join(here, 'lw-a6sg-v2ba-fk0pn0.step'))
    print("OK: %d points, %d directions rotated; root placement reset to identity"
          % (npt, ndir))