by adom
2020 Addressable RGB LED · 800 Kbps single-line cascade · 16 mA per channel (V1.0 sheet), 12 mA from V1.2
master
John Lauer
Scenes: the 300 um jet-paste solder balls replace the solder wedges; models.json without the deleted v1/v2 models
7723e88
16d ago
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091
#!/usr/bin/env python3
"""Replace the solder wedges in a WS2812B-2020 scene GLB (WS2812B-2020.glb, WS2812B-2020.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, so 2 x 2 per 0.7 mm pad here. 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, pad copper top at
z = 0.04 mm, 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")]
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), 2 x 2 per pad")
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))))))