← Commit history

GLB perfected: 300um solder balls on annular ring (was oversized), baked silkscreen text geometry (verified readable top-down), animated contact shadows (shadows now move with the insertion), verified holes=drill/pins=body press-fit

John Lauer ·64c87396bc ·3mo ago ·parent bff3a37
6 files changed +68−19
AdomMachinePins-4pin-animated.glb
⋯ 1 unchanged line ⋯
AdomMachinePins-complete.zip
⋯ 1 unchanged line ⋯
screenshots/board-3d-render.png
⋯ 1 unchanged line ⋯
screenshots/board-3d-top-silk.pngadded
⋯ 1 unchanged line ⋯
scripts/add_anim_board.py+21−12
@@ -1,35 +1,44 @@ #!/usr/bin/env python3-"""Add a gentle looping insertion animation to the 4 PIN_* nodes of the board GLB."""+"""Add a gentle looping insertion animation to the board GLB:+- PIN_* nodes translate up by LIFT mm then back (pins press into their own holes)+- SHADOW_* nodes scale up as the pin lifts (the contact shadow spreads/softens), so the+  shadow VISIBLY animates in the native viewer regardless of any ground-plane behavior.""" 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)+LIFT = float(sys.argv[3])/1000.0   # mm -> 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])+pin_nodes    = [i for i,n in enumerate(g.nodes) if n.name and n.name.startswith("PIN_")]+shadow_nodes = [i for i,n in enumerate(g.nodes) if n.name and n.name.startswith("SHADOW_")]+print("pins:", [g.nodes[i].name for i in pin_nodes], "shadows:", [g.nodes[i].name for i in shadow_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)+scale = np.array([[1,1,1],[1,1,1],[1.7,1.7,1],[1.7,1.7,1],[1,1,1]], dtype=np.float32)  # shadow spread  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+def add_acc(view, count, typ, mn=None, mx=None):+    g.accessors.append(Accessor(bufferView=view, componentType=pygltflib.FLOAT, count=count, type=typ, min=mn, max=mx))+    return len(g.accessors)-1++t_idx  = add_acc(add_view(times.tobytes()), len(times), "SCALAR", [float(times.min())],[float(times.max())])+tr_idx = add_acc(add_view(trans.tobytes()), len(trans), "VEC3")+sc_idx = add_acc(add_view(scale.tobytes()), len(scale), "VEC3")  samplers=[]; channels=[] for pn in pin_nodes:-    samplers.append(AnimationSampler(input=in_idx, output=out_idx, interpolation="LINEAR"))+    samplers.append(AnimationSampler(input=t_idx, output=tr_idx, interpolation="LINEAR"))     channels.append(AnimationChannel(sampler=len(samplers)-1, target=AnimationChannelTarget(node=pn, path="translation")))+for sn in shadow_nodes:+    samplers.append(AnimationSampler(input=t_idx, output=sc_idx, interpolation="LINEAR"))+    channels.append(AnimationChannel(sampler=len(samplers)-1, target=AnimationChannelTarget(node=sn, path="scale"))) 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)+print(f"animation: {len(pin_nodes)} pins lift {LIFT*1000}mm + {len(shadow_nodes)} shadows spread -> {OUT}")
scripts/build_board_glb.py+47−7
@@ -1,9 +1,23 @@ #!/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).+baked silkscreen text geometry, animated contact shadows, and a SUBTLE insertion animation. Fully renderable/verifiable offline (uncompressed).""" import DracoPy, pygltflib, numpy as np, trimesh, sys+from matplotlib.textpath import TextPath+from matplotlib.font_manager import FontProperties+from shapely.geometry import Polygon, MultiPolygon+from shapely.ops import unary_union++def text_mesh(s, height_mm, thick_mm=0.12):+    """Extruded silk text, normal +Z, reads correctly viewed from +Z (the wiki view). Centered on origin."""+    tp=TextPath((0,0), s, size=height_mm, prop=FontProperties(family="DejaVu Sans", weight="bold"))+    polys=[Polygon(pp).buffer(0) for pp in tp.to_polygons(closed_only=True) if len(pp)>=3]+    geom=unary_union(polys)+    parts=list(geom.geoms) if isinstance(geom,MultiPolygon) else [geom]+    m=trimesh.util.concatenate([trimesh.creation.extrude_polygon(p,height=thick_mm) for p in parts if p.area>1e-6])+    m.apply_translation([-(m.bounds[0][0]+m.bounds[1][0])/2, -(m.bounds[0][1]+m.bounds[1][1])/2, 0])+    return m  SRC = "/tmp/AdomBoardFusion.glb"          # raw real Fusion board (Draco) OUT = sys.argv[1] if len(sys.argv)>1 else "/tmp/AdomBoard-lead2.glb"@@ -89,16 +103,42 @@ for k in range(4):     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)+# 300um Essemtec jet solder-paste balls, ON the annular ring (between drill edge and pad edge),+# at ~0.45mm pitch (paste.rs standard). Real spec size = 0.30mm dia; do NOT oversize.+BALL_D=0.00030; BALL_R=BALL_D/2.0; PITCH=0.00045+# per-pin footprint geometry (meters): medium drill1.1/pad1.6 ; large drill3.45/pad5.2+FP={0:(0.00055,0.00080), 1:(0.00055,0.00080), 2:(0.001725,0.00260), 3:(0.001725,0.00260)} for k,(c,barrel_r) in pin_info.items():-    ring_r=barrel_r*1.18-    n = 16 if barrel_r>0.0015 else 9+    drill_r,pad_r=FP[k]+    ring_r=(drill_r+pad_r)/2.0                 # centerline of the annular ring+    n=max(6, int(round(2*np.pi*ring_r/PITCH)))     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)+        # seat the ball so it rests ON the pad surface (center one radius above board top)+        s.apply_translation([c[0]+ring_r*np.cos(th), c[1]+ring_r*np.sin(th), board_top+BALL_R])+        add_mesh(s.vertices,s.faces,[0.62,0.63,0.66,1.0],f"ball_{k}_{j}",metal=True)+    print(f"  balls PIN_{k}: {n} x {BALL_D*1000:.2f}mm on ring r={ring_r*1000:.2f}mm")++# --- silkscreen: real white text geometry on the board top (matches the Fusion 2D silk) ---+LABELS={0:("MP-MED-STD","D1.1 R0.25"), 1:("MP-MED-SHORT","D1.1 R0.25"),+        2:("MP-LRG-STD","D3.45 R0.875"), 3:("MP-LRG-SHORT","D3.45 R0.875")}+SILK_H=0.0014   # 1.4mm text+for k,(c,barrel_r) in pin_info.items():+    name,spec_s=LABELS[k]+    y0 = c[1]-barrel_r-0.0028                       # first line just below the pad+    for li,txt in enumerate((name,spec_s)):+        tm_txt=text_mesh(txt, SILK_H*1000)          # returns mm-scaled, centered+        tm_txt.apply_scale(0.001)                   # mm -> meters+        tm_txt.apply_translation([c[0], y0-li*0.0019, board_top+0.00005])+        add_mesh(tm_txt.vertices,tm_txt.faces,[0.97,0.97,0.97,1.0],f"silk_{k}_{li}")+    print(f"  silk PIN_{k}: '{name}' / '{spec_s}'")++# --- animated contact shadow: a flat dark disc under each pin on the board top ---+for k,(c,barrel_r) in pin_info.items():+    disc=trimesh.creation.cylinder(radius=barrel_r*1.05, height=0.00002, sections=48)+    disc.apply_translation([c[0], c[1], board_top+0.00003])+    add_mesh(disc.vertices,disc.faces,[0.02,0.02,0.02,0.42],f"SHADOW_{k}")  scene.export(OUT) print("exported",OUT)