by adom
0402 Thick Film Resistor · 4.7 kΩ ±1% · 1/16 W
master
John Lauer
WS2812B standard: metal terminals, datasheet PDF, distributors, verification log, 3D models record, solder balls, outli…
0262513
17d ago
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105
#!/usr/bin/env python3
"""Hero GLB: a hidden 'Solder balls' layer showing where the 300 um jet-paste balls land on the footprint's pads.
Follows the 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 drawn as a hemisphere with its flat face on the pad, dull leaded
solder grey (63Sn/37Pb), the layer hidden by default. Ball positions use the same rule as adom-footprint's paste-dot
overlay (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.
The count per pad follows from the pad size (2 x 2 on a 0.7 mm pad, 1 on a 0.5 mm pad), identical to `adom-footprint embed`.
usage: hero_post_solderballs.py HERO.glb [FOOTPRINT.kicad_mod] (in place; refuses to run twice)
The footprint defaults to the single .kicad_mod beside the GLB."""
import glob, json, math, os, re, struct, sys
D, PITCH, MARGIN = 0.30, 0.25, 0.06 # mm
MM = 0.001
Z0 = -0.016 # flat face just above the copper fill (-0.018 mm), under the chip seat
SEG, RINGS = 16, 6
NODE = "Solder balls - 300 um jet paste"
TIP = "Jet-paste solder ball, 0.30 mm across, 0.25 mm pitch (Adom 300 um solder-ball layer standard).\n63Sn/37Pb leaded paste; %s per pad on this footprint, %d in all."
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(); out = []
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):
out.append(dict(num=m.group(1), x=float(m.group(2)), y=-float(m.group(3)), w=float(m.group(4)), h=float(m.group(5)))) # KiCad y down -> y up
return out
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)]
glb = sys.argv[1]
mod = sys.argv[2] if len(sys.argv) > 2 else (glob.glob(os.path.join(os.path.dirname(os.path.abspath(glb)), "*.kicad_mod")) or [None])[0]
if not mod: sys.exit("no .kicad_mod beside the GLB; pass one")
js, bin_ = read_glb(glb)
if any(n.get("name") == NODE for n in js["nodes"]): sys.exit(json.dumps(dict(glb=glb, skip="solder balls already present")))
balls = [(x, y, p["num"]) for p in pads(mod) for y in grid(p["y"], p["h"]) for x in grid(p["x"], p["w"])]
_pp = pads(mod); _per = [len(grid(p["x"], p["w"])) * len(grid(p["y"], p["h"])) for p in _pp]
_per_txt = ("%d x %d" % (len(grid(_pp[0]["x"], _pp[0]["w"])), len(grid(_pp[0]["y"], _pp[0]["h"])))) if len(set(_per)) == 1 and _per[0] > 1 else ("1" if set(_per) == {1} else "varies")
TIP = TIP % (_per_txt, len(balls))
def buf(data, target=None):
global bin_
bin_ += b"\0" * (-len(bin_) % 4)
bv = dict(buffer=0, byteOffset=len(bin_), byteLength=len(data)); bv.update({"target": target} if target else {})
js.setdefault("bufferViews", []).append(bv); bin_ += data
return len(js["bufferViews"]) - 1
# one hemisphere mesh at the origin (mm), dome up (+Z), flat disc down; every ball is a node translated onto its pad
r = D / 2; P, N, I = [], [], []
for k in range(RINGS + 1): # dome: rings from the equator (k=0) to the pole
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((r * nx, r * ny, r * nz)); N.append((nx, ny, nz))
for k in range(RINGS):
for s in range(SEG):
a = k * (SEG + 1) + s; b = a + SEG + 1
I += [a, a + 1, b, a + 1, b + 1, b]
c0 = len(P); P.append((0, 0, 0)); N.append((0, 0, -1))
for s in range(SEG + 1): # flat face on the pad
ph = 2 * math.pi * s / SEG; P.append((r * math.cos(ph), r * math.sin(ph), 0)); N.append((0, 0, -1))
for s in range(SEG):
I += [c0, c0 + 2 + s, c0 + 1 + s]
pos = b"".join(struct.pack("<3f", x * MM, y * MM, z * MM) for x, y, z in P)
nrm = b"".join(struct.pack("<3f", *n) for n in N)
idx = b"".join(struct.pack("<H", i) for i in I)
acc = js.setdefault("accessors", [])
acc.append(dict(bufferView=buf(pos, 34962), componentType=5126, count=len(P), type="VEC3",
min=[-r * MM, -r * MM, 0], max=[r * MM, r * MM, r * MM]))
acc.append(dict(bufferView=buf(nrm, 34962), componentType=5126, count=len(N), type="VEC3"))
acc.append(dict(bufferView=buf(idx, 34963), componentType=5123, count=len(I), type="SCALAR"))
js.setdefault("materials", []).append(dict(name="Solder ball - 63Sn/37Pb, dull", doubleSided=True,
pbrMetallicRoughness=dict(baseColorFactor=[0.60, 0.61, 0.63, 1.0], metallicFactor=0.55, roughnessFactor=0.6)))
js.setdefault("meshes", []).append(dict(name="solder_ball_300um", primitives=[dict(
attributes=dict(POSITION=len(acc) - 3, NORMAL=len(acc) - 2), indices=len(acc) - 1, material=len(js["materials"]) - 1)]))
mesh_i = len(js["meshes"]) - 1
kids = []
for x, y, num in balls:
js["nodes"].append(dict(name="solder_ball_pad%s_%+.3f_%+.3f" % (num, x, y), mesh=mesh_i, translation=[x * MM, y * MM, Z0 * MM],
extras=dict(tooltip="Pad %s solder ball\n%s" % (num, TIP))))
kids.append(len(js["nodes"]) - 1)
js["nodes"].append(dict(name=NODE, children=kids, extras=dict(
adomLayer=dict(key="solder_balls", label="Solder balls (300 um jet paste)", group="Footprint", hidden=True), tooltip=TIP)))
js["scenes"][js.get("scene", 0)]["nodes"].append(len(js["nodes"]) - 1)
js["buffers"][0]["byteLength"] = len(bin_) + (-len(bin_) % 4)
js["asset"].setdefault("extras", {}).setdefault("adomHero", {}).setdefault("layers", {})["solder_balls"] = dict(
what="300 um jet-paste solder balls on the footprint pads, hidden by default", source="john/adom-solder-ball-layer-standard; " + os.path.basename(mod))
write_glb(glb, js, bin_)
print(json.dumps(dict(glb=glb, balls=len(balls), per_pad=len(balls) // max(1, len(pads(mod))), footprint=os.path.basename(mod))))