main
John Lauer README: component table per the wiki format, open-source section, no duplicate hero; generators at gen/ 39a59ef 7d ago
#!/usr/bin/env python3
"""Write an AP214 STEP file made of coloured axis-aligned boxes (plain Python, no CAD kernel).

Used for the AI-generated simplified OCuLink receptacle model (Amphenol G14A42111C12HR): dimensions from Amphenol
drawing G14A421X1BXXXHR rev D. Model frame = KiCad footprint frame turned into KiCad's 3D frame:
x right, y = -(pcb y), z up from the board top."""
import datetime


class Step:
    def __init__(self, name):
        self.name = name
        self.lines = []
        self.n = 0
        self.solids = []      # (brep_id, rgb)

    def add(self, s):
        self.n += 1
        self.lines.append(f"#{self.n}={s};")
        return self.n

    def pt(self, x, y, z):
        return self.add(f"CARTESIAN_POINT('',({x:.4f},{y:.4f},{z:.4f}))")

    def dirn(self, x, y, z):
        return self.add(f"DIRECTION('',({x:.1f},{y:.1f},{z:.1f}))")

    def box(self, x0, y0, z0, x1, y1, z1, rgb):
        P = {}
        for i, x in enumerate((x0, x1)):
            for j, y in enumerate((y0, y1)):
                for k, z in enumerate((z0, z1)):
                    P[(i, j, k)] = self.add(f"VERTEX_POINT('',#{self.pt(x, y, z)})")
        coords = {(i, j, k): ((x0, x1)[i], (y0, y1)[j], (z0, z1)[k]) for i in (0, 1) for j in (0, 1) for k in (0, 1)}
        edges = {}

        def edge(a, b):
            key = tuple(sorted((a, b)))
            if key in edges:
                return edges[key], key[0] == a
            pa, pb = coords[key[0]], coords[key[1]]
            d = tuple(pb[t] - pa[t] for t in range(3))
            L = max(abs(c) for c in d)
            dv = self.dirn(*(c / L for c in d))
            vec = self.add(f"VECTOR('',#{dv},{L:.4f})")
            ln = self.add(f"LINE('',#{self.pt(*pa)},#{vec})")
            e = self.add(f"EDGE_CURVE('',#{P[key[0]]},#{P[key[1]]},#{ln},.T.)")
            edges[key] = e
            return e, key[0] == a

        faces = []
        # each face: 4 corners in counter-clockwise order seen from outside, outward normal
        F = [
            ([(0, 0, 0), (0, 1, 0), (1, 1, 0), (1, 0, 0)], (0, 0, -1), (x0, y0, z0)),
            ([(0, 0, 1), (1, 0, 1), (1, 1, 1), (0, 1, 1)], (0, 0, 1), (x0, y0, z1)),
            ([(0, 0, 0), (1, 0, 0), (1, 0, 1), (0, 0, 1)], (0, -1, 0), (x0, y0, z0)),
            ([(0, 1, 0), (0, 1, 1), (1, 1, 1), (1, 1, 0)], (0, 1, 0), (x0, y1, z0)),
            ([(0, 0, 0), (0, 0, 1), (0, 1, 1), (0, 1, 0)], (-1, 0, 0), (x0, y0, z0)),
            ([(1, 0, 0), (1, 1, 0), (1, 1, 1), (1, 0, 1)], (1, 0, 0), (x1, y0, z0)),
        ]
        for corners, nrm, org in F:
            oes = []
            for a, b in zip(corners, corners[1:] + corners[:1]):
                e, same = edge(a, b)
                oes.append(self.add(f"ORIENTED_EDGE('',*,*,#{e},{'.T.' if same else '.F.'})"))
            loop = self.add(f"EDGE_LOOP('',({','.join('#%d' % o for o in oes)}))")
            bound = self.add(f"FACE_OUTER_BOUND('',#{loop},.T.)")
            ref = (1, 0, 0) if abs(nrm[0]) < 0.5 else (0, 1, 0)
            ax = self.add(f"AXIS2_PLACEMENT_3D('',#{self.pt(*org)},#{self.dirn(*nrm)},#{self.dirn(*ref)})")
            pl = self.add(f"PLANE('',#{ax})")
            faces.append(self.add(f"ADVANCED_FACE('',(#{bound}),#{pl},.T.)"))
        shell = self.add(f"CLOSED_SHELL('',({','.join('#%d' % f for f in faces)}))")
        brep = self.add(f"MANIFOLD_SOLID_BREP('{self.name}_{len(self.solids)}',#{shell})")
        self.solids.append((brep, rgb))
        return brep

    def write(self, path, description):
        head = self.lines
        self.lines = []
        n0 = self.n
        ctx_ids = []
        # units and context
        lu = self.add("(LENGTH_UNIT()NAMED_UNIT(*)SI_UNIT(.MILLI.,.METRE.))")
        au = self.add("(NAMED_UNIT(*)PLANE_ANGLE_UNIT()SI_UNIT($,.RADIAN.))")
        su = self.add("(NAMED_UNIT(*)SI_UNIT($,.STERADIAN.)SOLID_ANGLE_UNIT())")
        unc = self.add(f"UNCERTAINTY_MEASURE_WITH_UNIT(LENGTH_MEASURE(1.E-07),#{lu},'distance_accuracy_value','confusion accuracy')")
        ctx = self.add(f"(GEOMETRIC_REPRESENTATION_CONTEXT(3)GLOBAL_UNCERTAINTY_ASSIGNED_CONTEXT((#{unc}))GLOBAL_UNIT_ASSIGNED_CONTEXT((#{lu},#{au},#{su}))REPRESENTATION_CONTEXT('Context #1','3D Context with UNIT and UNCERTAINTY'))")
        org = self.add(f"AXIS2_PLACEMENT_3D('',#{self.pt(0, 0, 0)},#{self.dirn(0, 0, 1)},#{self.dirn(1, 0, 0)})")
        appctx = self.add("APPLICATION_CONTEXT('core data for automotive mechanical design processes')")
        self.add(f"APPLICATION_PROTOCOL_DEFINITION('international standard','automotive_design',2000,#{appctx})")
        pctx = self.add(f"PRODUCT_CONTEXT('',#{appctx},'mechanical')")
        prod = self.add(f"PRODUCT('{self.name}','{self.name}','',(#{pctx}))")
        pdfw = self.add(f"PRODUCT_DEFINITION_FORMATION('','',#{prod})")
        pdctx = self.add(f"PRODUCT_DEFINITION_CONTEXT('part definition',#{appctx},'design')")
        pd = self.add(f"PRODUCT_DEFINITION('design','',#{pdfw},#{pdctx})")
        pds = self.add(f"PRODUCT_DEFINITION_SHAPE('','',#{pd})")
        brep_list = ",".join("#%d" % b for b, _ in self.solids)
        rep = self.add(f"ADVANCED_BREP_SHAPE_REPRESENTATION('{self.name}',(#{org},{brep_list}),#{ctx})")
        self.add(f"SHAPE_DEFINITION_REPRESENTATION(#{pds},#{rep})")
        # colours
        styled = []
        for b, (r, g, bl) in self.solids:
            col = self.add(f"COLOUR_RGB('',{r:.3f},{g:.3f},{bl:.3f})")
            fas = self.add(f"FILL_AREA_STYLE_COLOUR('',#{col})")
            fa = self.add(f"FILL_AREA_STYLE('',(#{fas}))")
            ssfa = self.add(f"SURFACE_STYLE_FILL_AREA(#{fa})")
            sss = self.add(f"SURFACE_SIDE_STYLE('',(#{ssfa}))")
            ssu = self.add(f"SURFACE_STYLE_USAGE(.BOTH.,#{sss})")
            psa = self.add(f"PRESENTATION_STYLE_ASSIGNMENT((#{ssu}))")
            styled.append(self.add(f"STYLED_ITEM('color',(#{psa}),#{b})"))
        self.add(f"MECHANICAL_DESIGN_GEOMETRIC_PRESENTATION_REPRESENTATION('',({','.join('#%d' % s for s in styled)}),#{ctx})")
        body = head + self.lines
        now = datetime.datetime(2026, 10, 12, 22, 0, 0).strftime("%Y-%m-%dT%H:%M:%S")
        with open(path, "w") as fh:
            fh.write("ISO-10303-21;\nHEADER;\n")
            fh.write(f"FILE_DESCRIPTION(('{description}'),'2;1');\n")
            fh.write(f"FILE_NAME('{self.name}.step','{now}',('Adom AI (Claude), generated'),('Adom'),'stepbox.py','stepbox.py','');\n")
            fh.write("FILE_SCHEMA(('AUTOMOTIVE_DESIGN { 1 0 10303 214 1 1 1 1 }'));\nENDSEC;\nDATA;\n")
            fh.write("\n".join(body))
            fh.write("\nENDSEC;\nEND-ISO-10303-21;\n")


def oculink_model(path):
    """Simplified G14A42111C12HR: dimensions from Amphenol drawing G14A421X1BXXXHR rev D."""
    s = Step("Amphenol_G14A42111C12HR")
    BLACK, NICKEL, GOLD = (0.08, 0.08, 0.09), (0.72, 0.72, 0.70), (0.86, 0.69, 0.25)
    W2 = 13.15 / 2
    # KiCad 3D frame: y3d = -y_pcb. Body from pcb y -1.98 (mating face) to 7.02 -> y3d +1.98 .. -7.02; height 3.90
    s.box(-W2 + 0.25, -7.02, 0.0, W2 - 0.25, 1.70, 3.65, BLACK)            # insulator housing
    s.box(-W2, -6.90, 3.65, W2, 1.98, 3.90, NICKEL)                         # shell top
    s.box(-W2, -6.90, 0.25, -W2 + 0.25, 1.98, 3.65, NICKEL)                 # shell sides
    s.box(W2 - 0.25, -6.90, 0.25, W2, 1.98, 3.65, NICKEL)
    s.box(-W2, 1.73, 0.25, W2, 1.98, 0.75, NICKEL)                          # shell front lip (bottom)
    # shell legs (option C, 2.90 long) at x +/-6.58, pcb y 7.05 and 2.26: 0.5 x 1.1 blades through the board
    for sx in (-6.58, 6.58):
        for py in (7.05, 2.26):
            s.box(sx - 0.25, -py - 0.55, -2.90 + 0.25, sx + 0.25, -py + 0.55, 0.25, NICKEL)
    # SMT contact tails: B row at pcb y 7.37, A row at 5.18 (0.2 x 0.7 x 0.15), pitch 0.5
    for n in range(21):
        xb = 5.25 - 0.5 * n
        xa = 4.75 - 0.5 * n
        s.box(xb - 0.1, -7.37 - 0.35, 0.0, xb + 0.1, -7.37 + 0.35, 0.15, GOLD)
        s.box(xa - 0.1, -5.18 - 0.35, 0.0, xa + 0.1, -5.18 + 0.35, 0.15, GOLD)
    # plastic pegs dia 1.00 (as 0.8 squares) at pcb (+5.41, 4.46) and (-5.94, 4.46)
    for (px, py) in ((5.41, 4.46), (-5.94, 4.46)):
        s.box(px - 0.4, -py - 0.4, -1.2, px + 0.4, -py + 0.4, 0.0, BLACK)
    s.write(path, "AI-generated simplified model of Amphenol G14A42111C12HR OCuLink 4X R/A receptacle; dimensions from Amphenol drawing G14A421X1BXXXHR rev D; not a manufacturer model")
    return path


if __name__ == "__main__":
    import os, sys
    out = os.path.join(os.path.dirname(__file__), "..", "kicad", "3d", "Amphenol_G14A42111C12HR.step")
    os.makedirs(os.path.dirname(out), exist_ok=True)
    oculink_model(out)
    print("wrote", out, os.path.getsize(out), "bytes")