skill
Publish a Hardware Component to the Adom Wiki
Public Made by Adomby adom
The one-stop-shop for publishing a hardware component (or molecule/board) to the Adom Wiki. The whole lifecycle: page anatomy, the clone→edit→preview→push loop and its hard rules, making a great hero image (Hero Component Studio — 16:10, overlay-safe, datasheet-backed), the animated 3D composite, the interactive 3D viewer, ct thumbnails, the datasheet-accuracy audit, and every wiki push/pull gotcha we've hit. Now includes the worked Path B (board) pipeline: STEP-to-GLB, refdes tagging, layer de-
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249
# Worked example — LED animated composite builder (IN-S42ATR).
# ADAPT the absolute paths at the top for your part. Requires the cadquery venv (see SKILL §7).
# Does: emboss (auto-fit taper) + densified-NN 3-material colour + board/pads/solder/silk
# + insert animation + red lens + LED_lit glow + KHR_lights_punctual light + node Z-scale height fix.
"""
Build IN-S42ATR.insertion.glb — the animated LED-on-footprint composite:
fr4_board / pad_top / silk (cathode mark) / solder_top / IN-S42ATR (LED) + LED_lit glow
+ smooth `insert` animation on the LED node.
LED mesh = embossed (IN-S42ATR + RED on the sides) + 3 original materials via densified
NN + a red lens (top) + a toggleable emissive LED_lit child (on/off via Layers menu).
All meters, Z-up (matches the cap composite the wiki viewer already renders).
"""
import struct, json
import numpy as np
import cadquery as cq
import trimesh
import pygltflib
STEP_IN = "/tmp/claude-1001/-home-adom-project/7572280e-1088-44ed-9829-35fccaaabecb/scratchpad/wiki-fresh-in-s42atr/IN-S42ATR.step"
ORIG_GLB = "/tmp/claude-1001/-home-adom-project/7572280e-1088-44ed-9829-35fccaaabecb/scratchpad/wiki-fresh-in-s42atr/IN-S42ATR.glb"
OUT = "/tmp/claude-1001/-home-adom-project/7572280e-1088-44ed-9829-35fccaaabecb/scratchpad/wiki-edit-in-s42atr/IN-S42ATR.glb"
INSERTION = "/tmp/claude-1001/-home-adom-project/7572280e-1088-44ed-9829-35fccaaabecb/scratchpad/wiki-edit-in-s42atr/IN-S42ATR.insertion.glb"
DEPTH=0.05; FONT,FP="DejaVu Sans","/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf"; TOL,ANG=0.015,0.35
ZS=0.90 # datasheet height 0.45mm / model 0.50mm
LIGHT_INT=0.00004; LIGHT_RNG=0.004 # red point-light intensity (candela) + range (m) — tuned for mm scale (brighter)
# ===== 1. Emboss + tessellate =====
body=cq.importers.importStep(STEP_IN)
# GENERAL taper check: measure the body's X-width vs height straight from the
# component's own mesh vertices (the color-preserving GLB = the tessellated STEP),
# so this works for ANY tapered / domed / stepped component (nothing hardcoded).
def _load_positions_mm(glb_path):
import struct as _st
fp=open(glb_path,'rb'); fp.read(12); L=_st.unpack('<I',fp.read(4))[0]; fp.read(4); jj=_st.loads=json.loads(fp.read(L))
b2=_st.unpack('<I',fp.read(4))[0]; fp.read(4); bb=fp.read(b2)
pts=[]
for mm in jj['meshes']:
for pr in mm['primitives']:
a=jj['accessors'][pr['attributes']['POSITION']]; bvv=jj['bufferViews'][a['bufferView']]
off=bvv.get('byteOffset',0)+a.get('byteOffset',0); n=a['count']*3
vals=_st.unpack(f'<{n}f',bb[off:off+n*4]); pts.append(np.array(vals).reshape(a['count'],3))
return np.vstack(pts)*1000.0 # m → mm
_BP=_load_positions_mm(ORIG_GLB)
# Build a width-vs-height profile from the mesh's distinct Z "rings", then interpolate
# (mesh vertices only exist at geometry rings, so a thin slice between them is empty).
_zr=np.unique(np.round(_BP[:,2],3))
_levels=[]
for _z in _zr:
_sl=_BP[np.abs(_BP[:,2]-_z)<0.006]
if len(_sl): _levels.append((float(_z), float(_sl[:,0].max()-_sl[:,0].min())))
_levels.sort(); _ZL=np.array([l[0] for l in _levels]); _WL=np.array([l[1] for l in _levels])
print(f"taper profile: {len(_BP)} verts, width-by-height rings={list(zip(np.round(_ZL,2),np.round(_WL,2)))}")
def face_w(z):
return float(np.interp(z,_ZL,_WL))
# Auto-fit: largest font so the WHOLE string fits the usable face width AND the band
# height (with margin). Prevents the part number / value from being clipped.
def fit_font(txt,max_w,max_h,mf=0.90):
bb=cq.Workplane("XY").text(txt,1.0,0.01,font=FONT,fontPath=FP,halign="center",valign="center",combine=False).val().BoundingBox()
return min(max_w/bb.xlen, max_h/bb.ylen)*mf
MARGIN=0.05
Zc_pn,H_pn = 0.28,0.12
uw_pn=face_w(Zc_pn+H_pn/2)-2*MARGIN; f_pn=fit_font("IN-S42ATR",uw_pn,H_pn)
Zc_red,H_red = 0.29,0.20
uw_red=face_w(Zc_red+H_red/2)-2*MARGIN; f_red=fit_font("RED",uw_red,H_red)
print(f"fit: IN-S42ATR font={f_pn:.3f} (usable_w={uw_pn:.3f}) RED font={f_red:.3f} (usable_w={uw_red:.3f})")
pn=cq.Workplane(cq.Plane(origin=(0,0.25,Zc_pn),xDir=(-1,0,0),normal=(0,1,0))).text("IN-S42ATR",f_pn,-DEPTH,font=FONT,fontPath=FP,halign="center",valign="center",combine=False)
red=cq.Workplane(cq.Plane(origin=(0,-0.25,Zc_red),xDir=(1,0,0),normal=(0,-1,0))).text("RED",f_red,-DEPTH,font=FONT,fontPath=FP,halign="center",valign="center",combine=False)
# Hard check: the placed text must fit inside the usable width and stay off the gold
# terminations (|x| < 0.5 minus margin). Fail loudly if not.
for nm,t,uw in (("IN-S42ATR",pn,uw_pn),("RED",red,uw_red)):
bb=t.val().BoundingBox()
assert bb.xlen<=uw+1e-6, f"{nm} width {bb.xlen:.3f} > usable {uw:.3f}"
assert max(abs(bb.xmin),abs(bb.xmax))<0.48, f"{nm} reaches x={max(abs(bb.xmin),abs(bb.xmax)):.3f} into termination zone"
print(f" check {nm}: width={bb.xlen:.3f}<= {uw:.3f}, x_extent=±{max(abs(bb.xmin),abs(bb.xmax)):.3f} OK")
embossed=body.cut(pn).cut(red)
vmm,tris=embossed.val().tessellate(TOL,ANG)
P=np.array([[v.x,v.y,v.z] for v in vmm])*0.001; tris=np.array(tris,dtype=np.int64)
tp=P[tris]; fn=np.cross(tp[:,1]-tp[:,0],tp[:,2]-tp[:,0]); Ln=np.linalg.norm(fn,axis=1,keepdims=True); Ln[Ln==0]=1; fn/=Ln
Vled=tp.reshape(-1,3).astype(np.float32); Nled=np.repeat(fn,3,axis=0).astype(np.float32)
EC=tp.mean(axis=1).astype(np.float32)
# ===== 2. Densified NN colour transfer =====
f=open(ORIG_GLB,'rb'); f.read(12); ln=struct.unpack('<I',f.read(4))[0]; f.read(4); j=json.loads(f.read(ln))
bl=struct.unpack('<I',f.read(4))[0]; f.read(4); bd=f.read(bl)
CTM={5123:'H',5125:'I',5126:'f'}; SZ={5123:2,5125:4,5126:4}; NCc={'SCALAR':1,'VEC3':3}
def acc(i):
a=j['accessors'][i]; bv=j['bufferViews'][a['bufferView']]; off=bv.get('byteOffset',0)+a.get('byteOffset',0); n=a['count']*NCc[a['type']]
v=struct.unpack(f'<{n}'+CTM[a['componentType']],bd[off:off+n*SZ[a['componentType']]]); return np.array(v).reshape(a['count'],NCc[a['type']])
bary=np.array([(i/6,k/6,1-i/6-k/6) for i in range(7) for k in range(7-i)])
RP=[]; RM=[]
for m in j['meshes']:
for pr in m['primitives']:
mt=pr.get('material'); pos=acc(pr['attributes']['POSITION']); idx=acc(pr['indices']).reshape(-1,3)
for tr in idx: RP.append(bary@pos[tr]); RM.append(np.full(len(bary),mt))
REF=np.vstack(RP).astype(np.float32); RMAT=np.concatenate(RM)
mat_of=np.empty(len(EC),dtype=np.int64)
for s in range(0,len(EC),2000):
ec=EC[s:s+2000]; mat_of[s:s+2000]=RMAT[(((ec[:,None,:]-REF[None,:,:])**2).sum(-1)).argmin(1)]
# top lens = pale tris that face up near the top
topmask=(mat_of==0)&(fn[:,2]>0.8)&(EC[:,2]>0.00042)
cat=np.where(topmask,4,mat_of) # 4 = lens_red
ids=np.arange(len(tris)*3).reshape(-1,3)
groups={k:ids[cat==k].reshape(-1).astype(np.uint32) for k in (0,1,2,4)}
print("LED tri split:",{k:len(v)//3 for k,v in groups.items()})
# glow geometry = the top-lens tris, nudged up 4µm
glow_tris=tris[topmask]
gtp=P[glow_tris]+np.array([0,0,0.000004])
Vglow=gtp.reshape(-1,3).astype(np.float32)
gfn=np.cross(gtp[:,1]-gtp[:,0],gtp[:,2]-gtp[:,0]); gL=np.linalg.norm(gfn,axis=1,keepdims=True); gL[gL==0]=1; gfn/=gL
Nglow=np.repeat(gfn,3,axis=0).astype(np.float32)
print(f"glow tris: {len(glow_tris)}")
# ===== 3. Footprint layer meshes (trimesh) =====
def box(sx,sy,sz,cx=0,cy=0,cz=0,color=(200,200,200,255)):
b=trimesh.creation.box(extents=[sx,sy,sz]).apply_translation([cx,cy,cz])
b.visual=trimesh.visual.ColorVisuals(b,face_colors=color); return b
BL,BW,BT=0.0032,0.0024,0.0016
board=box(BL,BW,BT,cz=-BT/2,color=(10,60,30,255))
padL=box(0.0004,0.0006,0.000035,cx=-0.0005,cz=0.0000175); padR=box(0.0004,0.0006,0.000035,cx=0.0005,cz=0.0000175)
pads=trimesh.util.concatenate([padL,padR]); pads.visual=trimesh.visual.ColorVisuals(pads,face_colors=(210,160,60,255))
# cathode silk bar on -Y edge, -X side (from footprint fp_line -0.7,-0.5 .. -0.2,-0.5)
silk=box(0.0005,0.00012,0.00003,cx=-0.00045,cy=-0.0005,cz=0.000045); silk.visual=trimesh.visual.ColorVisuals(silk,face_colors=(220,220,220,255))
def dot(x):
d=trimesh.creation.icosphere(subdivisions=2,radius=0.00013); d.apply_scale([1,1,0.6]); d.apply_translation([x,0,0.000035]); return d
solder=trimesh.util.concatenate([dot(-0.0005),dot(0.0005)]); solder.visual=trimesh.visual.ColorVisuals(solder,face_colors=(190,195,205,255))
# ===== 4. Assemble GLB with pygltflib =====
g=pygltflib.GLTF2(); g.asset=pygltflib.Asset(version="2.0"); blob=bytearray()
def av(d,t):
global blob
while len(blob)%4: blob+=b"\x00"
o=len(blob); blob+=d; g.bufferViews.append(pygltflib.BufferView(buffer=0,byteOffset=o,byteLength=len(d),target=t)); return len(g.bufferViews)-1
def vec3acc(A):
bv=av(A.astype(np.float32).tobytes(),34962); i=len(g.accessors)
g.accessors.append(pygltflib.Accessor(bufferView=bv,componentType=5126,count=len(A),type="VEC3",min=A.min(0).tolist(),max=A.max(0).tolist())); return i
def idxacc(A):
A=A.astype(np.uint32); bv=av(A.tobytes(),34963); i=len(g.accessors)
g.accessors.append(pygltflib.Accessor(bufferView=bv,componentType=5125,count=len(A),type="SCALAR",min=[int(A.min())],max=[int(A.max())])); return i
def mat(name,color,met=0.0,rough=0.6,emis=None,alpha=None,emis_str=None):
pbr=pygltflib.PbrMetallicRoughness(baseColorFactor=color,metallicFactor=met,roughnessFactor=rough)
m=pygltflib.Material(name=name,pbrMetallicRoughness=pbr)
if emis: m.emissiveFactor=emis
if emis_str is not None:
m.extensions={"KHR_materials_emissive_strength":{"emissiveStrength":emis_str}}
if "KHR_materials_emissive_strength" not in (g.extensionsUsed or []):
g.extensionsUsed=(g.extensionsUsed or [])+["KHR_materials_emissive_strength"]
if alpha is not None: m.alphaMode="BLEND"
g.materials.append(m); return len(g.materials)-1
def tm_prim(tm, material):
v=np.array(tm.vertices,dtype=np.float32); faces=np.array(tm.faces).reshape(-1)
# flat normals
fnv=tm.face_normals; nv=np.repeat(fnv,3,axis=0).astype(np.float32)
vexp=v[np.array(tm.faces).reshape(-1)].astype(np.float32)
pa=vec3acc(vexp); na=vec3acc(nv); ia=idxacc(np.arange(len(vexp)))
return pygltflib.Primitive(attributes=pygltflib.Attributes(POSITION=pa,NORMAL=na),indices=ia,material=material,mode=4)
# materials
m_board=mat("board",[0.04,0.24,0.12,1],0,0.75); m_pad=mat("pad",[0.82,0.63,0.24,1],0.6,0.4)
m_silk=mat("silk",[0.87,0.87,0.87,1],0,0.8); m_sold=mat("solder",[0.75,0.76,0.80,1],0.85,0.35)
m_pale=mat("mat_0",[0.80,0.79,0.66,1],0.25,0.55); m_gold=mat("mat_1",[0.71,0.50,0.21,1],0.7,0.35)
m_green=mat("mat_2",[0.09,0.42,0.17,1],0,0.6)
m_lens=mat("lens_red",[0.40,0.02,0.02,1],0,0.75,emis=[0.40,0.01,0.01],emis_str=1.4) # brighter deep-red lens
m_glow=mat("led_glow",[0.85,0.02,0.02,1],0,0.70,emis=[1.0,0.04,0.04],alpha=0.9,emis_str=1.6) # brighter deep-red glow
def add_node(name,prims,children=None,scale=None,trans=None):
g.meshes.append(pygltflib.Mesh(name=name,primitives=prims)); mi=len(g.meshes)-1
nd=pygltflib.Node(name=name,mesh=mi)
if children is not None: nd.children=children
if scale: nd.scale=scale
if trans: nd.translation=trans
g.nodes.append(nd); return len(g.nodes)-1
# layer nodes
n_board=add_node("fr4_board",[tm_prim(board,m_board)])
n_pad=add_node("pad_top",[tm_prim(pads,m_pad)])
n_silk=add_node("silk",[tm_prim(silk,m_silk)])
n_sold=add_node("solder_top",[tm_prim(solder,m_sold)])
# LED mesh prims (share LED pos/normal accessors)
pa=vec3acc(Vled); na=vec3acc(Nled)
led_prims=[]
for k,mi in ((0,m_pale),(1,m_gold),(2,m_green),(4,m_lens)):
if len(groups[k])==0: continue
led_prims.append(pygltflib.Primitive(attributes=pygltflib.Attributes(POSITION=pa,NORMAL=na),indices=idxacc(groups[k]),material=mi,mode=4))
# glow mesh
gpa=vec3acc(Vglow); gna=vec3acc(Nglow)
glow_prim=pygltflib.Primitive(attributes=pygltflib.Attributes(POSITION=gpa,NORMAL=gna),indices=idxacc(np.arange(len(Vglow))),material=m_glow,mode=4)
# LED_lit is TOP-LEVEL (so the wiki Layers menu lists it) and gets its own animation
# channel so it still rides the LED during the insert animation.
n_glow=add_node("LED_lit",[glow_prim],trans=[0.0,0.0,0.0],scale=[1.0,1.0,ZS])
n_led=add_node("IN-S42ATR",led_prims,trans=[0.0,0.0,0.0],scale=[1.0,1.0,ZS])
# REAL red light: embed a KHR_lights_punctual point light at the emitter so the lit LED
# actually casts red onto the board/pads (Babylon renders it), not just a flat rectangle.
LIGHT_INTENSITY=LIGHT_INT # candela — tuned for the mm-scale scene
LIGHT_RANGE=LIGHT_RNG # metres
g.extensionsUsed=(g.extensionsUsed or [])+["KHR_lights_punctual"]
g.extensions={"KHR_lights_punctual":{"lights":[{
"type":"point","name":"led_emitter","color":[1.0,0.03,0.015],
"intensity":LIGHT_INTENSITY,"range":LIGHT_RANGE}]}}
n_light=len(g.nodes)
g.nodes.append(pygltflib.Node(name="LED_light",translation=[0.0,0.0,0.00060],
extensions={"KHR_lights_punctual":{"light":0}}))
g.nodes[n_led].children=[n_light] # rides the LED via parent transform
g.scenes=[pygltflib.Scene(nodes=[n_board,n_pad,n_silk,n_sold,n_led,n_glow])]; g.scene=0
# ===== 5. insert animation on the LED node =====
# STANDARD for basic-parts animated composites: lift = 3 x the component's own height
# (measured from its geometry, so it scales to any part).
COMP_H=(_BP[:,2].max()-_BP[:,2].min())*0.001 # component height, m
LIFT=3.0*COMP_H*ZS
print(f"lift = 3 x component height ({COMP_H*1000:.3f}mm) = {LIFT*1000:.3f}mm")
times=[0.0,1.2,3.0,4.2,6.0]
outs=[(0,0,0),(0,0,0),(0,0,LIFT),(0,0,LIFT),(0,0,0)]
tb=struct.pack(f'<{len(times)}f',*times); ob=struct.pack(f'<{len(outs)*3}f',*[c for o in outs for c in o])
while len(blob)%4: blob+=b"\x00"
tin=len(blob); blob+=tb
while len(blob)%4: blob+=b"\x00"
oin=len(blob); blob+=ob
tbv=len(g.bufferViews); g.bufferViews.append(pygltflib.BufferView(buffer=0,byteOffset=tin,byteLength=len(tb)))
obv=len(g.bufferViews); g.bufferViews.append(pygltflib.BufferView(buffer=0,byteOffset=oin,byteLength=len(ob)))
tacc=len(g.accessors); g.accessors.append(pygltflib.Accessor(bufferView=tbv,componentType=5126,count=len(times),type="SCALAR",min=[times[0]],max=[times[-1]]))
oacc=len(g.accessors); g.accessors.append(pygltflib.Accessor(bufferView=obv,componentType=5126,count=len(outs),type="VEC3",min=[min(o[i] for o in outs) for i in range(3)],max=[max(o[i] for o in outs) for i in range(3)]))
g.animations=[pygltflib.Animation(name="insert",
samplers=[pygltflib.AnimationSampler(input=tacc,output=oacc,interpolation="LINEAR")],
channels=[
pygltflib.AnimationChannel(sampler=0,target=pygltflib.AnimationChannelTarget(node=n_led,path="translation")),
pygltflib.AnimationChannel(sampler=0,target=pygltflib.AnimationChannelTarget(node=n_glow,path="translation")),
])]
g.buffers=[pygltflib.Buffer(byteLength=len(blob))]; g.set_binary_blob(bytes(blob))
import os; os.makedirs(os.path.dirname(OUT),exist_ok=True)
g.save(OUT)
import shutil; shutil.copy(OUT,INSERTION)
print(f"OK: wrote {OUT} ({len(blob):,} B) + insertion.glb")
print("nodes:",[n.name for n in g.nodes]); print("materials:",[m.name for m in g.materials])