component
Adom Machine Pins, Fusion Library + Sample Board
Public Made by Adomby adom
One Fusion 360 .lbr with all four Adom press-fit machine pins (working 3D), plus a sample board (2D/3D/silkscreen) ganging all four together. Fully documented and rebuildable.
← Commit history
Fix pins-bigger-than-holes: real Fusion board GLB, transparent FR4, gentle 2.6mm insertion float (was 17mm), solder-paste balls per pad; verified via Draco decode + headless render; skill records the fix
6 files changed
+186
AdomMachinePins-4pin-animated.glb⋯ 1 unchanged line ⋯
AdomMachinePins-complete.zip⋯ 1 unchanged line ⋯
SKILL.md+47@@ -142,6 +142,53 @@ These are non-negotiable lessons from building this exact library. Every one was and mis-hit the tiny rows. (Arrow keys sent to the canvas also zoom it out.) 7. **No em-dashes anywhere on the wiki page.** They read as AI-slop; use commas, colons, or hyphens. 8. **Only ship silkscreen board shots.** A 3D board with no silkscreen is not worth showing.+9. **Keep the insertion float TINY (2 to 3mm), never a launch.** A big lift (the first build used 17mm)+ floats the pins high above their holes, so the wide contact barrel reads as "the pin is way bigger+ than the hole" even though the geometry is a correct press-fit. Lift the pin just enough to clearly+ pop out of and reseat in its OWN hole. The barrel is meant to be wider than the drill (it seats on+ the pad); only the thin tail passes through, so the pin must stay visually attached to its hole.+10. **VERIFY THE GLB BY RENDERING IT before you push, every time.** Do not ship a GLB you have not+ looked at. See the verification recipe below.++## Verify the GLB offline (the step that was skipped, causing repeated rejects)++The real Fusion board GLB (via `step2glb`) is **Draco-compressed** (`KHR_draco_mesh_compression`).+`trimesh` cannot decode Draco, so it reads ALL vertices as coincident (area 0, bounds 0) and any+render is black. That is why offline checks silently "passed" while the board looked wrong to the user.+To actually SEE it:++```+pip install --break-system-packages DracoPy pyrender+# render headless with EGL:+import os; os.environ["PYOPENGL_PLATFORM"]="egl"+import DracoPy, pygltflib, numpy as np, pyrender, trimesh+g=pygltflib.GLTF2().load(glb); blob=g.binary_blob()+for mesh in g.meshes: # decode each primitive+ for p in mesh.primitives:+ ext=p.extensions["KHR_draco_mesh_compression"]; bv=g.bufferViews[ext["bufferView"]]+ dm=DracoPy.decode(blob[(bv.byteOffset or 0):(bv.byteOffset or 0)+bv.byteLength])+ v=np.array(dm.points).reshape(-1,3); f=np.array(dm.faces).reshape(-1,3) # apply node world transform+# pyrender: PerspectiveCamera(znear=size*0.02) (default 0.05m near-plane clips a 0.05m board -> black),+# render with RenderFlags.SKIP_CULL_FACES (CAD normals are often inverted).+```++Render at rest AND at the animation peak (lift the PIN_* nodes) and LOOK at both. Only push once the+pins visibly seat in their own holes and the solder balls ring each pad.++## The reliable lead-GLB recipe that shipped (use the real Fusion board, do not hand-build the slab)++The user explicitly wanted the REAL Fusion board, not a hand-modeled slab. So: export the Fusion 3D+PCB to STEP, `step2glb` it (real holes, pads, pins, colors), then Draco-DECODE it and rebuild an+uncompressed GLB with `trimesh` so you can add what STEP drops and can render-verify it:+ - board/soldermask meshes -> `alphaMode=BLEND` alpha ~0.45 (transparent FR4, see the tails through it);+ - merge each pin's many sub-meshes (nodes named `MpinContact*` / `=>[0:1:1:*]`) into ONE node per pin+ (`PIN_0..PIN_3`) so it animates as a unit;+ - 300um solder-paste balls: a ring of small metallic spheres just outside each barrel base, on the+ board top surface (auto-size the ring from each pin's detected barrel radius: ~9 balls medium, ~16 large);+ - add a gentle 2 to 3mm insertion animation on the PIN_* nodes (see requirement 9);+ - keep balls STATIC (paste stays on the pad; only the pin moves).+Scripts that shipped this: `/tmp/build_board_glb.py` (decode + rebuild + balls) and+`/tmp/add_anim_board.py` (4-pin animation). ## Recreate the lead GLB animation (all 4 pins, real holes, silk, solder balls)
screenshots/board-3d-render.png⋯ 1 unchanged line ⋯
scripts/add_anim_board.pyadded+35@@ -0,0 +1,35 @@+#!/usr/bin/env python3+"""Add a gentle looping insertion animation to the 4 PIN_* nodes of the board GLB."""+import sys, numpy as np, pygltflib+from pygltflib import GLTF2, Animation, AnimationSampler, AnimationChannel, AnimationChannelTarget, Accessor, BufferView+IN, OUT = sys.argv[1], sys.argv[2]+LIFT = float(sys.argv[3])/1000.0 # mm -> meters (scene is meters)++g = GLTF2().load(IN)+pin_nodes = [i for i,n in enumerate(g.nodes) if n.name and n.name.startswith("PIN_")]+print("animating nodes:", [g.nodes[i].name for i in pin_nodes])++times = np.array([0.0,1.4,3.0,4.4,6.0], dtype=np.float32)+trans = np.array([[0,0,0],[0,0,0],[0,0,LIFT],[0,0,LIFT],[0,0,0]], dtype=np.float32)++blob = bytearray(g.binary_blob())+while len(blob)%4: blob.append(0)+def add_view(b):+ while len(blob)%4: blob.append(0)+ off=len(blob); blob.extend(b)+ g.bufferViews.append(BufferView(buffer=0, byteOffset=off, byteLength=len(b)))+ return len(g.bufferViews)-1+tv=add_view(times.tobytes())+g.accessors.append(Accessor(bufferView=tv,componentType=pygltflib.FLOAT,count=len(times),type="SCALAR",+ min=[float(times.min())],max=[float(times.max())])); in_idx=len(g.accessors)-1+ov=add_view(trans.tobytes())+g.accessors.append(Accessor(bufferView=ov,componentType=pygltflib.FLOAT,count=len(trans),type="VEC3")); out_idx=len(g.accessors)-1++samplers=[]; channels=[]+for pn in pin_nodes:+ samplers.append(AnimationSampler(input=in_idx, output=out_idx, interpolation="LINEAR"))+ channels.append(AnimationChannel(sampler=len(samplers)-1, target=AnimationChannelTarget(node=pn, path="translation")))+g.animations=[Animation(name="insert", samplers=samplers, channels=channels)]+g.buffers[0].byteLength=len(blob); g.set_binary_blob(bytes(blob))+g.save(OUT)+print("added insertion animation, lift",LIFT*1000,"mm ->",OUT)
scripts/build_board_glb.pyadded+104@@ -0,0 +1,104 @@+#!/usr/bin/env python3+"""Rebuild the Adom pins board lead GLB from the REAL Fusion board (Draco-decoded),+with: transparent FR4, real pins grouped per-node, 300um solder-paste balls on each pad,+and a SUBTLE insertion animation (small lift so pins clearly seat in their own holes).+Fully renderable/verifiable offline (uncompressed)."""+import DracoPy, pygltflib, numpy as np, trimesh, sys++SRC = "/tmp/AdomBoardFusion.glb" # raw real Fusion board (Draco)+OUT = sys.argv[1] if len(sys.argv)>1 else "/tmp/AdomBoard-lead2.glb"+LIFT_MM = float(sys.argv[2]) if len(sys.argv)>2 else 2.6 # gentle press-in, not a launch++g = pygltflib.GLTF2().load(SRC); blob = g.binary_blob()+mats = g.materials or []+def matcol(idx):+ if idx is None or idx>=len(mats): return [0.7,0.7,0.7,1.0]+ pbr = mats[idx].pbrMetallicRoughness+ return list(pbr.baseColorFactor) if pbr and pbr.baseColorFactor else [0.7,0.7,0.7,1.0]++# world transforms per mesh index (+ node name)+def world_mats(g):+ W={}+ def rec(ni,M):+ n=g.nodes[ni]; L=np.eye(4)+ if n.matrix: L=np.array(n.matrix,float).reshape(4,4).T+ elif n.translation: L[:3,3]=n.translation+ Mn=M@L+ if n.mesh is not None: W.setdefault(n.mesh,[]).append((Mn,n.name or ""))+ for c in (n.children or []): rec(c,Mn)+ for r in g.scenes[g.scene or 0].nodes: rec(r,np.eye(4))+ return W+W = world_mats(g)++# decode + split into board verts and per-pin clusters (meters)+pin_pts=[]; board_meshes=[]+PIN_CENTERS=np.array([[0.010,0.010],[0.040,0.010],[0.010,0.040],[0.040,0.040]])+pin_clusters={i:[] for i in range(4)}+for mi,mesh in enumerate(g.meshes):+ for p in mesh.primitives:+ ext=p.extensions["KHR_draco_mesh_compression"]; bv=g.bufferViews[ext["bufferView"]]+ off=bv.byteOffset or 0; dm=DracoPy.decode(blob[off:off+bv.byteLength])+ v=np.array(dm.points).reshape(-1,3); f=np.array(dm.faces).reshape(-1,3)+ col=matcol(p.material)+ for (M,nm) in W.get(mi,[(np.eye(4),"")]):+ vv=(M[:3,:3]@v.T).T+M[:3,3]+ is_pin = ("MpinContact" in nm) or nm.startswith("=>")+ if is_pin:+ c=vv[:,:2].mean(0); k=int(np.argmin(((PIN_CENTERS-c)**2).sum(1)))+ pin_clusters[k].append((vv,f,col))+ else:+ board_meshes.append((vv,f,col))++# board top surface z (max z of board/pad geometry)+board_top = max(vv[:,2].max() for vv,_,_ in board_meshes)+print("board top z (mm):", round(board_top*1000,3))++scene = trimesh.Scene()++def add_mesh(vv,f,rgba,name,metal=False):+ m=trimesh.Trimesh(vertices=vv,faces=f,process=False)+ r,gr,b,a=rgba+ m.visual=trimesh.visual.TextureVisuals(material=trimesh.visual.material.PBRMaterial(+ baseColorFactor=[int(r*255),int(gr*255),int(b*255),int(a*255)],+ metallicFactor=0.9 if metal else 0.2, roughnessFactor=0.35 if metal else 0.55,+ alphaMode="BLEND" if a<0.99 else "OPAQUE", doubleSided=True))+ scene.add_geometry(m,node_name=name,geom_name=name)++# board: FR4 body + soldermask transparent (~0.5), pads/copper opaque gold+for i,(vv,f,col) in enumerate(board_meshes):+ is_green = col[1]>=col[0] and col[1]>0.12+ is_gold = col[0]>0.6 and col[2]<0.5+ if is_gold:+ add_mesh(vv,f,[0.85,0.62,0.20,1.0],f"pad_{i}",metal=True)+ else:+ base=[0.18,0.16,0.06] if not is_green else [0.05,0.42,0.12]+ add_mesh(vv,f,base+[0.45],f"board_{i}")++# pins: merge each cluster into ONE node so it animates as a unit; detect barrel radius+pin_info={}+for k in range(4):+ parts=pin_clusters[k]+ if not parts: continue+ V=[]; F=[]; off=0+ for vv,f,col in parts:+ V.append(vv); F.append(f+off); off+=len(vv)+ V=np.vstack(V); F=np.vstack(F)+ c=V[:,:2].mean(0)+ barrel_r=float(np.hypot(V[:,0]-c[0], V[:,1]-c[1]).max()) # widest = socket barrel+ pin_info[k]=(c, barrel_r)+ add_mesh(V,F,[0.90,0.66,0.30,1.0],f"PIN_{k}",metal=True)+ print(f"PIN_{k} center=({c[0]*1000:.1f},{c[1]*1000:.1f})mm barrel OD={barrel_r*2000:.2f}mm")++# 300um solder-paste jet balls: ring just outside each barrel base, on the board top surface+BALL_R=0.00035 # 0.35mm radius (~0.7mm blob; jet paste reads better a touch above 300um)+for k,(c,barrel_r) in pin_info.items():+ ring_r=barrel_r*1.18+ n = 16 if barrel_r>0.0015 else 9+ for j in range(n):+ th=2*np.pi*j/n+ s=trimesh.creation.icosphere(subdivisions=2,radius=BALL_R)+ s.apply_translation([c[0]+ring_r*np.cos(th), c[1]+ring_r*np.sin(th), board_top])+ add_mesh(s.vertices,s.faces,[0.80,0.80,0.84,1.0],f"ball_{k}_{j}",metal=True)++scene.export(OUT)+print("exported",OUT)