master
John Lauer WS2812B standard: metal terminals, datasheet PDF, distributors, verification log, 3D models record, solder balls, outli… 0262513 17d ago
#!/usr/bin/env python3
"""Replace the solder wedges in a component scene GLB (<MPN>.glb showcase, <MPN>.insertion.glb) with the
300 um jet-paste solder balls, the same ones the hero's 'Solder balls' layer shows.

Adom footprint standard (john/adom-solder-ball-layer-standard): balls 0.30 mm across at 0.25 mm pitch (adjacent balls
overlap slightly, by design), each a hemisphere with its flat face on the pad, dull leaded solder grey (63Sn/37Pb).
Positions follow adom-footprint's paste-dot rule (1.0.26): per SMD pad a grid at 0.25 mm pitch centred on the pad,
kept 0.06 mm inside the pad edge (2 x 2 on a 0.7 mm pad, 1 on a 0.5 mm pad). The scene keeps its node 'solder_top' (same name,
same place in the tree), only its geometry and material change. Scene frame: metres, Z up, the balls sit on the pad
copper's top face, read from the scene's pad_top mesh; KiCad Y-down flipped to Y up.
usage: scene_solderballs.py SCENE.glb FOOTPRINT.kicad_mod   (in place; idempotent)"""
import json, math, re, struct, sys

D, PITCH, MARGIN, CU = 0.30, 0.25, 0.06, 0.04
MM = 0.001
SEG, RINGS = 16, 6


def read_glb(p):
    b = open(p, "rb").read(); n = struct.unpack("<I", b[12:16])[0]; js = json.loads(b[20:20 + n]); o = 20 + n
    return js, bytearray(b[o + 8:o + 8 + struct.unpack("<I", b[o:o + 4])[0]] if o < len(b) else b"")


def write_glb(p, js, bin_):
    j = json.dumps(js, separators=(",", ":")).encode(); j += b" " * (-len(j) % 4); bin_ += b"\0" * (-len(bin_) % 4)
    open(p, "wb").write(struct.pack("<III", 0x46546C67, 2, 12 + 8 + len(j) + 8 + len(bin_)) + struct.pack("<II", len(j), 0x4E4F534A) + j
                       + struct.pack("<II", len(bin_), 0x004E4942) + bytes(bin_))


def pads(mod):
    t = open(mod).read()
    return [(float(m.group(1)), -float(m.group(2)), float(m.group(3)), float(m.group(4))) for m in re.finditer(
        r'\(pad\s+"?[^"\s]*"?\s+smd\s+\w+\s+\(at\s+([-\d.]+)\s+([-\d.]+)(?:\s+[-\d.]+)?\)\s+\(size\s+([-\d.]+)\s+([-\d.]+)\)', t)]


def grid(c, span):
    n = max(1, int(math.floor((span - 2 * MARGIN - D) / PITCH + 1e-9)) + 1)
    return [c + (i - (n - 1) / 2) * PITCH for i in range(n)]


def balls(mod):
    return [(x, y) for px, py, w, h in pads(mod) for y in grid(py, h) for x in grid(px, w)]


def hemispheres(centres, z0):
    r = D / 2; P, N, I = [], [], []
    for cx, cy in centres:
        b0 = len(P)
        for k in range(RINGS + 1):
            th = (math.pi / 2) * k / RINGS
            for s in range(SEG + 1):
                ph = 2 * math.pi * s / SEG; nx, ny, nz = math.cos(th) * math.cos(ph), math.cos(th) * math.sin(ph), math.sin(th)
                P.append((cx + r * nx, cy + r * ny, z0 + r * nz)); N.append((nx, ny, nz))
        for k in range(RINGS):
            for s in range(SEG):
                a = b0 + k * (SEG + 1) + s; b = a + SEG + 1
                I += [a, a + 1, b, a + 1, b + 1, b]
        c0 = len(P); P.append((cx, cy, z0)); N.append((0, 0, -1))
        for s in range(SEG + 1):
            ph = 2 * math.pi * s / SEG; P.append((cx + r * math.cos(ph), cy + r * math.sin(ph), z0)); N.append((0, 0, -1))
        for s in range(SEG):
            I += [c0, c0 + 2 + s, c0 + 1 + s]
    return P, N, I


if __name__ == "__main__":
    glb, mod = sys.argv[1:3]
    js, bin_ = read_glb(glb)
    names = [n.get("name") for n in js["nodes"]]
    node = js["nodes"][names.index("solder_top")]
    pa = js["accessors"][js["meshes"][js["nodes"][names.index("pad_top")]["mesh"]]["primitives"][0]["attributes"]["POSITION"]]
    cu = pa["max"][2] / MM if "max" in pa else CU                       # pad copper top in this scene (mm)
    C = balls(mod); P, N, I = hemispheres(C, cu)

    def buf(data, target):
        global bin_
        bin_ += b"\0" * (-len(bin_) % 4)
        js["bufferViews"].append(dict(buffer=0, byteOffset=len(bin_), byteLength=len(data), target=target)); bin_ += data
        return len(js["bufferViews"]) - 1
    acc = js["accessors"]
    xs = [p[0] * MM for p in P]; ys = [p[1] * MM for p in P]; zs = [p[2] * MM for p in P]
    acc.append(dict(bufferView=buf(b"".join(struct.pack("<3f", x * MM, y * MM, z * MM) for x, y, z in P), 34962), componentType=5126,
                    count=len(P), type="VEC3", min=[min(xs), min(ys), min(zs)], max=[max(xs), max(ys), max(zs)]))
    acc.append(dict(bufferView=buf(b"".join(struct.pack("<3f", *n) for n in N), 34962), componentType=5126, count=len(N), type="VEC3"))
    acc.append(dict(bufferView=buf(b"".join(struct.pack("<I", i) for i in I), 34963), componentType=5125, count=len(I), type="SCALAR"))
    prim = js["meshes"][node["mesh"]]["primitives"][0]
    prim.update(attributes=dict(POSITION=len(acc) - 3, NORMAL=len(acc) - 2), indices=len(acc) - 1, mode=4)
    js["materials"][prim["material"]].update(name="solder_63Sn37Pb_jet_paste", doubleSided=True,
                                             pbrMetallicRoughness=dict(baseColorFactor=[0.60, 0.61, 0.63, 1.0], metallicFactor=0.55, roughnessFactor=0.6))
    node["extras"] = dict(node.get("extras") or {}, tooltip="Jet-paste solder ball, 0.30 mm across, 0.25 mm pitch\n(Adom 300 um solder-ball standard), %d on this footprint" % len(C))
    js["buffers"][0]["byteLength"] = len(bin_) + (-len(bin_) % 4)
    write_glb(glb, js, bin_)
    print(json.dumps(dict(glb=glb, balls=len(C), per_pad=len(C) // max(1, len(pads(mod))))))