← Commit history

Hero: datasheet note under the chip (off-copper terminal strips, 0.40 mm pad gap, explanation readable from below)

John Lauer ·149270d4e6 ·16d ago ·parent de0116a
2 files changed +168
provenance/tools/hero_post_callout.pyadded+168
@@ -0,0 +1,168 @@+#!/usr/bin/env python3+"""WS2812B-2020 hero: a 'Datasheet note' layer that explains the one thing that looks wrong from underneath.++Worldsemi's terminals are 0.78 mm long on a 1.00 mm pitch, but its PCB Solder Pad drawing puts 0.7 x 0.7 mm pads+0.40 mm apart (1.10 mm pitch), so the inner 0.09 mm of every bottom terminal sits off the copper. This adds, under the+chip and readable from below:+  - four small amber dashed boxes (lightly filled) on those off-copper terminal strips,+  - a text panel on the underside with leader lines to the four boxes,+  - hover tooltips on all of it (extras.tooltip),+all in one declared layer (extras.adomLayer key 'datasheet_note'), so a reader can switch it off.+usage: ws_callout.py HERO.glb   (in place; refuses to run twice)"""+import io, json, math, struct, sys+sys.path.insert(0, "/home/adom/project/basic-parts-overhaul")+from hero_copper import read_glb, write_glb+from PIL import Image, ImageDraw, ImageFont++glb = sys.argv[1]+js, bin_ = read_glb(glb)+NODE = "Datasheet note - why part of each terminal has no copper"+if any(n.get("name") == NODE for n in js["nodes"]): sys.exit(json.dumps(dict(glb=glb, skip="note already present")))++MM = 0.001+Z = -0.006 * MM                        # just under the chip's seat (z = 0), above the copper fill (-0.018 mm)+AMBER = [1.0, 0.55, 0.05]+TIP = ("No copper under this strip, by design. Worldsemi's PCB Solder Pad drawing (WS2812B-2020 datasheet p.2) "+       "puts 0.7 x 0.7 mm pads 0.40 mm apart, while the terminals are 0.78 mm long on a 1.00 mm pitch, so the "+       "inner 0.09 mm of each terminal sits off the pad. The 0.40 mm gap keeps neighbouring pins from bridging.")+++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+++def mesh(name, tris, mat, uv=None, tooltip=None):+    """tris: list of (x, y) mm triangles corners in the board frame (y up), all at Z."""+    pts = [p for t in tris for p in t]+    pos = b"".join(struct.pack("<3f", x * MM, y * MM, Z) for x, y in pts)+    nrm = b"".join(struct.pack("<3f", 0, 0, -1) for _ in pts)+    acc = js.setdefault("accessors", [])+    xs = [x * MM for x, y in pts]; ys = [y * MM for x, y in pts]+    acc.append(dict(bufferView=buf(pos, 34962), componentType=5126, count=len(pts), type="VEC3", min=[min(xs), min(ys), Z], max=[max(xs), max(ys), Z]))+    acc.append(dict(bufferView=buf(nrm, 34962), componentType=5126, count=len(pts), type="VEC3"))+    attrs = dict(POSITION=len(acc) - 2, NORMAL=len(acc) - 1)+    if uv:+        acc.append(dict(bufferView=buf(b"".join(struct.pack("<2f", u, v) for u, v in uv), 34962), componentType=5126, count=len(uv), type="VEC2"))+        attrs["TEXCOORD_0"] = len(acc) - 1+    js["meshes"].append(dict(name=name, primitives=[dict(attributes=attrs, material=mat)]))+    nd = dict(name=name, mesh=len(js["meshes"]) - 1)+    if tooltip: nd["extras"] = dict(tooltip=tooltip)+    js["nodes"].append(nd); return len(js["nodes"]) - 1+++def rect(x0, y0, x1, y1):+    return [((x0, y0), (x1, y0), (x1, y1)), ((x0, y0), (x1, y1), (x0, y1))]+++def seg(a, b, w):+    """a thin quad (two triangles) from a to b, w mm wide."""+    dx, dy = b[0] - a[0], b[1] - a[1]; L = math.hypot(dx, dy) or 1; nx, ny = -dy / L * w / 2, dx / L * w / 2+    p = [(a[0] + nx, a[1] + ny), (b[0] + nx, b[1] + ny), (b[0] - nx, b[1] - ny), (a[0] - nx, a[1] - ny)]+    return [(p[0], p[1], p[2]), (p[0], p[2], p[3])]+++def dashed_rect(x0, y0, x1, y1, w=0.012, dash=0.04, gap=0.025):+    tris = []; corners = [(x0, y0), (x1, y0), (x1, y1), (x0, y1), (x0, y0)]+    for a, b in zip(corners, corners[1:]):+        L = math.hypot(b[0] - a[0], b[1] - a[1]); t = 0.0+        while t < L:+            t1 = min(t + dash, L); f0, f1 = t / L, t1 / L+            tris += seg((a[0] + (b[0] - a[0]) * f0, a[1] + (b[1] - a[1]) * f0), (a[0] + (b[0] - a[0]) * f1, a[1] + (b[1] - a[1]) * f1), w)+            t += dash + gap+    return tris+++ex = js.setdefault("extensionsUsed", [])+if "KHR_materials_unlit" not in ex: ex.append("KHR_materials_unlit")+mats = js.setdefault("materials", [])+def mat(name, rgba, tex=None):+    m = dict(name=name, alphaMode="BLEND", doubleSided=True, extensions={"KHR_materials_unlit": {}},+             pbrMetallicRoughness=dict(baseColorFactor=rgba, metallicFactor=0, roughnessFactor=1))+    if tex is not None: m["pbrMetallicRoughness"]["baseColorTexture"] = dict(index=tex)+    mats.append(m); return len(mats) - 1+m_line = mat("Datasheet note - amber dashes", AMBER + [0.95])+m_fill = mat("Datasheet note - amber fill", AMBER + [0.30])++# the four off-copper strips: bottom terminal x 0.565..1.10, inner end y 0.11..0.20 from the centre (pad starts at 0.20)+kids = []+centres = []+for sx in (-1, 1):+    for sy in (-1, 1):+        xa, xb = sorted((sx * 0.565, sx * 1.10)); ya, yb = sorted((sy * 0.11, sy * 0.20))+        kids.append(mesh("note_strip_fill_%+d%+d" % (sx, sy), rect(xa, ya, xb, yb), m_fill, tooltip=TIP))+        kids.append(mesh("note_strip_dash_%+d%+d" % (sx, sy), dashed_rect(xa - 0.008, ya - 0.008, xb + 0.008, yb + 0.008), m_line, tooltip=TIP))+        centres.append((sx * 0.83, sy * 0.155))++# text panel on the underside, between the pad columns (x -0.53..0.53), drawn to read correctly from BELOW+PW, PH = 1.0, 0.62                      # mm+img = Image.new("RGBA", (2000, 1240), (18, 22, 28, 215)); dr = ImageDraw.Draw(img)+fb = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf", 92)+fr = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf", 74)+dr.rectangle([6, 6, 1993, 1233], outline=(255, 150, 20, 255), width=10)+lines = [("No copper under the amber strips", fb, (255, 170, 40)),+         ("Worldsemi's PCB Solder Pad drawing puts", fr, (235, 238, 242)),+         ("0.7 x 0.7 mm pads 0.40 mm apart. The", fr, (235, 238, 242)),+         ("terminals are 0.78 mm long on a 1.00 mm", fr, (235, 238, 242)),+         ("pitch, so the inner 0.09 mm of each one", fr, (235, 238, 242)),+         ("sits off the pad. The 0.40 mm gap keeps", fr, (235, 238, 242)),+         ("neighbouring pins from bridging.", fr, (235, 238, 242)),+         ("WS2812B-2020 datasheet p.2", fr, (150, 200, 210))]+y = 60+for t, f, c in lines:+    dr.text((70, y), t, font=f, fill=c); y += (120 if f is fb else 140) if t == lines[0][0] else 132+png = io.BytesIO(); img.save(png, "PNG")+js.setdefault("images", []).append(dict(bufferView=buf(png.getvalue()), mimeType="image/png", name="datasheet-note"))+js.setdefault("samplers", []).append(dict(magFilter=9729, minFilter=9987, wrapS=33071, wrapT=33071))+js.setdefault("textures", []).append(dict(source=len(js["images"]) - 1, sampler=len(js["samplers"]) - 1))+m_text = mat("Datasheet note - text", [1, 1, 1, 1], tex=len(js["textures"]) - 1)+x0, x1, y0, y1 = -PW / 2, PW / 2, -PH / 2, PH / 2+tri = [((x0, y0), (x1, y0), (x1, y1)), ((x0, y0), (x1, y1), (x0, y1))]+# seen from below, +X appears on the LEFT: the image's left edge (u = 0) goes at +X so the text reads correctly+uvx = lambda x: (x1 - x) / PW; uvy = lambda yv: (y1 - yv) / PH+uv = [(uvx(x), uvy(yv)) for t in tri for x, yv in t]+kids.append(mesh("note_text_panel", tri, m_text, uv=uv, tooltip=TIP))+# leader lines from the panel's corners to the strips+for (cx, cy) in centres:+    px, py = (math.copysign(PW / 2, cx), math.copysign(PH / 2 * 0.6, cy))+    kids.append(mesh("note_leader_%+.0f%+.0f" % (cx, cy), seg((px, py), (cx - math.copysign(0.27, cx), cy), 0.008), m_line))++# 0.40 mm dimension between the two pads of each column: ticks on the pad edges, a shaft with arrowheads, a label+def label_tex(text, w_px=900, h_px=260):+    im = Image.new("RGBA", (w_px, h_px), (18, 22, 28, 200)); d = ImageDraw.Draw(im)+    f = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf", 150)+    tw = d.textlength(text, font=f); d.text(((w_px - tw) / 2, 40), text, font=f, fill=(255, 170, 40))+    b = io.BytesIO(); im.save(b, "PNG")+    js["images"].append(dict(bufferView=buf(b.getvalue()), mimeType="image/png", name="dim-" + text))+    js["textures"].append(dict(source=len(js["images"]) - 1, sampler=len(js["samplers"]) - 1))+    return mat("Datasheet note - dimension label", [1, 1, 1, 1], tex=len(js["textures"]) - 1)+m_dim = label_tex("0.40 mm")+GAP = 0.20                                   # pad inner edges at y = +/-0.20 (pads 0.7 tall on +/-0.55)+for sx in (-1, 1):+    xa = sx * 0.70                           # arrow inside the pad column, clear of the castellation+    tris = []+    for sy in (-1, 1):                       # extension ticks along each pad edge+        tris += seg((sx * 0.60, sy * GAP), (sx * 1.00, sy * GAP), 0.008)+    tris += seg((xa, -GAP + 0.045), (xa, GAP - 0.045), 0.010)        # shaft+    for sy in (-1, 1):                       # arrowheads touching the pad edges+        tip = (xa, sy * GAP); base = sy * (GAP - 0.05)+        tris.append(((xa - 0.025, base), (xa + 0.025, base), tip))+    kids.append(mesh("note_dim_040_%+d" % sx, tris, m_line, tooltip="0.40 mm between the pads (Worldsemi PCB Solder Pad drawing)"))+    # label beside the arrow, horizontal, readable from below+    lx0, lx1 = sorted((sx * 0.74, sx * 1.18)); ly0, ly1 = -0.065, 0.065+    tri2 = [((lx0, ly0), (lx1, ly0), (lx1, ly1)), ((lx0, ly0), (lx1, ly1), (lx0, ly1))]+    uv2 = [((lx1 - x) / (lx1 - lx0), (ly1 - yv) / (ly1 - ly0)) for t in tri2 for x, yv in t]+    kids.append(mesh("note_dim_label_%+d" % sx, tri2, m_dim, uv=uv2, tooltip="0.40 mm between the pads (Worldsemi PCB Solder Pad drawing)"))++js["nodes"].append(dict(name=NODE, children=kids,+                        extras=dict(adomLayer=dict(key="datasheet_note", label="Datasheet note", group="Footprint"),+                                    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", {})["datasheet_note"] = dict(+    what="underside callout: off-copper terminal strips + explanation, readable from below", source="WS2812B-2020 datasheet p.1-2")+write_glb(glb, js, bin_)+print(json.dumps(dict(glb=glb, nodes=len(kids), text_panel_mm=[PW, PH])))
ws2812b-2020-hero.glb
⋯ 1 unchanged line ⋯