main
noah Hero: heatsink vias as one switchable group (1/5) b5e4ff4 7d ago
"""Add the footprint's heatsink vias to a component hero GLB as the Adom.HeatsinkVias layer (wiki-component standard):
copper barrels, 65 % transparent, through a pretend 1.6 mm board below the seating plane, declared as the
switchable "Heatsink Vias" group (the viewer makes every Adom.* node one; a declared adomLayer as well would list it twice).

    python3 hero_post_vias.py <slug>-hero.glb <footprint.kicad_mod>

Reads the plated thru_hole pads straight off the footprint. Frame: the GLB is Z-up in metres (step2glb), KiCad is
Y-down in mm, so a pad at (x, y) becomes (x, -y) * 1e-3. Idempotent: an existing Adom.HeatsinkVias node is replaced.
"""
import math, re, struct, sys
import numpy as np
import pygltflib
from pygltflib import Accessor, BufferView, Mesh, Primitive, Attributes, Material, PbrMetallicRoughness, Node

BOARD = 1.6e-3
SEG = 28


def vias(mod):
    t = open(mod).read()
    out = []
    for num, x, y, size, drill in re.findall(r'\(pad "([^"]+)" thru_hole circle \(at ([-\d.]+) ([-\d.]+)\) \(size ([\d.]+) [\d.]+\) \(drill ([\d.]+)\)', t):
        out.append((num, float(x), float(y), float(size), float(drill)))
    return out


def tube(cx, cy, ro, ri, z0, z1):
    """Triangles of a hollow barrel: outer wall, inner wall, top and bottom annuli. Returns (positions, normals, indices)."""
    P, N, I = [], [], []
    def add(p, n):
        P.append(p); N.append(n); return len(P) - 1
    for k in range(SEG):
        a0, a1 = 2 * math.pi * k / SEG, 2 * math.pi * (k + 1) / SEG
        c0, s0, c1, s1 = math.cos(a0), math.sin(a0), math.cos(a1), math.sin(a1)
        for r, sgn in ((ro, 1), (ri, -1)):
            q = [add((cx + r * c, cy + r * s, z), (sgn * c, sgn * s, 0)) for c, s, z in ((c0, s0, z0), (c1, s1, z0), (c1, s1, z1), (c0, s0, z1))]
            I += [q[0], q[1], q[2], q[0], q[2], q[3]] if sgn > 0 else [q[0], q[2], q[1], q[0], q[3], q[2]]
        for z, nz in ((z1, 1), (z0, -1)):
            q = [add((cx + r * c, cy + r * s, z), (0, 0, nz)) for r, c, s in ((ri, c0, s0), (ro, c0, s0), (ro, c1, s1), (ri, c1, s1))]
            I += [q[0], q[1], q[2], q[0], q[2], q[3]] if nz > 0 else [q[0], q[2], q[1], q[0], q[3], q[2]]
    return P, N, I


def main(glb, mod):
    g = pygltflib.GLTF2().load(glb)
    vs = vias(mod)
    if not vs:
        print('OK %s: no heatsink vias in the footprint' % glb); return
    # drop an earlier run
    old = [i for i, n in enumerate(g.nodes) if n.name == 'Adom.HeatsinkVias']
    for sc in g.scenes:
        sc.nodes = [n for n in sc.nodes if n not in old]
    P, N, I = [], [], []
    for num, x, y, size, drill in vs:
        p, n, i = tube(x * 1e-3, -y * 1e-3, size / 2 * 1e-3, drill / 2 * 1e-3, -BOARD, 0.0)
        base = len(P); P += p; N += n; I += [base + k for k in i]
    pos = np.array(P, dtype=np.float32); nor = np.array(N, dtype=np.float32); idx = np.array(I, dtype=np.uint32)
    blob = bytearray(g.binary_blob() or b'')
    def push(arr, target):
        while len(blob) % 4: blob.append(0)
        off = len(blob); data = arr.tobytes(); blob.extend(data)
        g.bufferViews.append(BufferView(buffer=0, byteOffset=off, byteLength=len(data), target=target))
        return len(g.bufferViews) - 1
    bp = push(pos, pygltflib.ARRAY_BUFFER); bn = push(nor, pygltflib.ARRAY_BUFFER); bi = push(idx, pygltflib.ELEMENT_ARRAY_BUFFER)
    g.accessors.append(Accessor(bufferView=bp, componentType=pygltflib.FLOAT, count=len(pos), type='VEC3', min=pos.min(0).tolist(), max=pos.max(0).tolist()))
    ap = len(g.accessors) - 1
    g.accessors.append(Accessor(bufferView=bn, componentType=pygltflib.FLOAT, count=len(nor), type='VEC3')); an = len(g.accessors) - 1
    g.accessors.append(Accessor(bufferView=bi, componentType=pygltflib.UNSIGNED_INT, count=len(idx), type='SCALAR')); ai = len(g.accessors) - 1
    g.materials.append(Material(name='Adom.HeatsinkVias.copper', alphaMode='BLEND', doubleSided=True,
                                pbrMetallicRoughness=PbrMetallicRoughness(baseColorFactor=[0.85, 0.52, 0.25, 0.35], metallicFactor=0.6, roughnessFactor=0.35)))
    mat = len(g.materials) - 1
    g.meshes.append(Mesh(name='Adom.HeatsinkVias', primitives=[Primitive(attributes=Attributes(POSITION=ap, NORMAL=an), indices=ai, material=mat)]))
    node = Node(name='Adom.HeatsinkVias', mesh=len(g.meshes) - 1,
                extras={'tooltip': 'Heatsink vias from the footprint: %d plated holes, %.2f mm drill, through a 1.6 mm board (reference artwork, not part of the chip)' % (len(vs), vs[0][4])})
    g.nodes.append(node)
    g.scenes[g.scene or 0].nodes.append(len(g.nodes) - 1)
    g.buffers[0].byteLength = len(blob)
    g.set_binary_blob(bytes(blob))
    g.save(glb)
    print('OK %s: %d heatsink vias added as Adom.HeatsinkVias' % (glb, len(vs)))


if __name__ == '__main__':
    main(sys.argv[1], sys.argv[2])