component
JST SM04B-GHS-TB(LF)(SN) GH 4-circuit side entry SMT header
Public Made by Adomby adom
JST GH 1.25 mm pitch, 4 circuits, side entry (right angle) SMT shrouded header with two solder tabs; mates with GHR-04V-S and SSHL-002T-P0.2.
main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108
#!/usr/bin/env python3
"""GLB stats + preview render (numpy/PIL, flat shading), reading quantized accessors correctly.
usage: glbtool.py stats a.glb [...] -> JSON per file
glbtool.py render a.glb out.png [view=iso|top] [size=400]"""
import sys, json, hashlib, math, struct
import numpy as np
from pygltflib import GLTF2
from PIL import Image, ImageDraw
CT = {5120: np.int8, 5121: np.uint8, 5122: np.int16, 5123: np.uint16, 5125: np.uint32, 5126: np.float32}
NC = {"SCALAR": 1, "VEC2": 2, "VEC3": 3, "VEC4": 4, "MAT4": 16}
def acc(g, blob, i):
a = g.accessors[i]; bv = g.bufferViews[a.bufferView]; dt = np.dtype(CT[a.componentType]); n = NC[a.type]
off = (bv.byteOffset or 0) + (a.byteOffset or 0); stride = bv.byteStride or dt.itemsize * n
raw = np.frombuffer(blob, dtype=np.uint8, count=stride * (a.count - 1) + dt.itemsize * n, offset=off)
arr = np.lib.stride_tricks.as_strided(raw, shape=(a.count, n * dt.itemsize), strides=(stride, 1)).copy().view(dt).reshape(a.count, n).astype(np.float64)
if a.normalized:
arr = {np.int8: lambda x: np.maximum(x / 127, -1), np.uint8: lambda x: x / 255, np.int16: lambda x: np.maximum(x / 32767, -1), np.uint16: lambda x: x / 65535}[dt.type](arr)
return arr
def node_mat(n):
if n.matrix: return np.array(n.matrix).reshape(4, 4).T
M = np.eye(4)
if n.scale: M = np.diag(list(n.scale) + [1]) @ M
if n.rotation:
x, y, z, w = n.rotation
R = np.array([[1-2*(y*y+z*z), 2*(x*y-z*w), 2*(x*z+y*w)], [2*(x*y+z*w), 1-2*(x*x+z*z), 2*(y*z-x*w)], [2*(x*z-y*w), 2*(y*z+x*w), 1-2*(x*x+y*y)]])
R4 = np.eye(4); R4[:3, :3] = R; M = R4 @ M
if n.translation:
T = np.eye(4); T[:3, 3] = n.translation; M = T @ M
return M
def walk(g):
out = []
def rec(i, P, path):
n = g.nodes[i]; M = P @ node_mat(n); p = path + [n.name or str(i)]
if n.mesh is not None: out.append((n, M, p))
for c in n.children or []: rec(c, M, p)
for r in g.scenes[g.scene or 0].nodes: rec(r, np.eye(4), [])
return out
def load(f):
g = GLTF2().load(f); return g, g.binary_blob()
def tris(f):
g, blob = load(f); res = []
for n, M, path in walk(g):
for pr in g.meshes[n.mesh].primitives:
P = acc(g, blob, pr.attributes.POSITION); P = (M @ np.c_[P, np.ones(len(P))].T).T[:, :3]
I = acc(g, blob, pr.indices).astype(int).reshape(-1, 3) if pr.indices is not None else np.arange(len(P)).reshape(-1, 3)
mat = g.materials[pr.material] if pr.material is not None else None
col = mat.pbrMetallicRoughness.baseColorFactor if mat and mat.pbrMetallicRoughness else [0.6, 0.6, 0.6, 1]
res.append(dict(path="/".join(path), P=P, I=I, col=col))
return g, res
def stats(f):
g, res = tris(f); data = open(f, "rb").read()
allP = np.vstack([r["P"] for r in res])
dec = 0
for a in g.accessors: dec += a.count * NC[a.type] * np.dtype(CT[a.componentType]).itemsize
names = [n.name or "" for n in g.nodes]
helpers = [n for n in names if any(k in n.lower() for k in ("pad outline", "silkscreen", "signal name", "reference artwork", "footprint pads", "courtyard"))]
mark = [r for r in res if "LaserEtch" in r["path"]]
chip = [r for r in res if "LaserEtch" not in r["path"]]
mz = None
if mark:
mp = np.vstack([r["P"] for r in mark]); top = max(r["P"][:, 2].max() for r in chip)
mz = dict(mark_z_min_mm=round(mp[:, 2].min() * 1000, 4), mark_z_max_mm=round(mp[:, 2].max() * 1000, 4), chip_top_mm=round(top * 1000, 4),
lift_um=round((mp[:, 2].min() - top) * 1e6, 2))
return dict(file=f.split("/")[-1], sha256=hashlib.sha256(data).hexdigest(), bytes=len(data), triangles=int(sum(len(r["I"]) for r in res)),
primitives=len(res), decodedBytes=int(dec), boundsMm=[round(v * 1000, 3) for v in (*allP.min(0), *allP.max(0))],
helperNodes=helpers, mark=mz, extensions=g.extensionsUsed or [])
def render(f, out, view="iso", size=400):
g, res = tris(f)
allP = np.vstack([r["P"] for r in res]); c = (allP.min(0) + allP.max(0)) / 2
if view == "top":
R = np.eye(3)
elif view.startswith("hero"):
# pose: reading direction (-Y in the board frame) turned to +X, then a 3/4 view from the front, above
P90 = np.array([[0, -1, 0], [1, 0, 0], [0, 0, 1]])
a = math.radians(28); b = math.radians(-48)
Rz = np.array([[math.cos(a), -math.sin(a), 0], [math.sin(a), math.cos(a), 0], [0, 0, 1]])
Rx = np.array([[1, 0, 0], [0, math.cos(b), -math.sin(b)], [0, math.sin(b), math.cos(b)]])
R = Rx @ Rz @ P90
else: # iso: rotate -45 about z then tilt 55 about x (camera from front-left-up, pin-1 side)
a = math.radians(-35); b = math.radians(-55)
Rz = np.array([[math.cos(a), -math.sin(a), 0], [math.sin(a), math.cos(a), 0], [0, 0, 1]])
Rx = np.array([[1, 0, 0], [0, math.cos(b), -math.sin(b)], [0, math.sin(b), math.cos(b)]])
R = Rx @ Rz
light = np.array([0.3, -0.5, 0.8]); light /= np.linalg.norm(light)
polys = []
for r in res:
Q = (r["P"] - c) @ R.T
for t in r["I"]:
v = Q[t]; nrm = np.cross(v[1] - v[0], v[2] - v[0]); ln = np.linalg.norm(nrm)
if ln == 0: continue
nrm /= ln
if nrm[2] < 0: nrm = -nrm
sh = 0.35 + 0.65 * max(0, float(nrm @ light))
col = tuple(int(255 * min(1, (x ** (1 / 2.2)) * sh)) for x in r["col"][:3])
polys.append(((1 if "LaserEtch" in r["path"] else 0), v[:, 2].mean(), v[:, :2], col))
polys.sort(key=lambda p: (p[0], p[1]))
polys = [(p[1], p[2], p[3]) for p in polys]
Q = np.vstack([p[1] for p in polys]); ext = (Q.max(0) - Q.min(0)).max()
s = size * 0.86 / ext; S = 3
im = Image.new("RGBA", (size * S, size * S), (0, 0, 0, 0) if view.startswith("hero") else (36, 40, 46, 255)); d = ImageDraw.Draw(im)
for _, v, col in polys:
d.polygon([(size * S / 2 + x * s * S, size * S / 2 - y * s * S) for x, y in v], fill=col)
im.resize((size, size), Image.LANCZOS).save(out)
if __name__ == "__main__":
if sys.argv[1] == "stats":
print(json.dumps([stats(f) for f in sys.argv[2:]], indent=1))
else:
render(sys.argv[2], sys.argv[3], *(sys.argv[4:5] or ["iso"]), *(map(int, sys.argv[5:6])))