name: adom-machine-pin-fusion-library description: > Build the complete Adom press-fit machine-pin Fusion 360 library (all 4 pins: Medium/Large × Standard/Short) WITH working 3D, and a sample board that gangs them together (2D + 3D + silkscreen), entirely in the background. Covers: generating the .lbr from KiCad (round pad, fp_circle silk, per-pin --symbol-art), the CRITICAL 3D-binding process (per-pin make_3d_package + wip_urn + package3d/package3dinstances), authoring + opening the board with NO modal dialogs (fusion_open_board / newDesignFromLocal), and the bridge/APS features that make it all background. Use when creating/correcting the Adom machine-pin Fusion library, its board, or when a pin's 3D shows as a flat pad / Placeholder. Triggers: machine pin, machine contact, MachinePin, press-fit pin, adom modular pins, machine pin lbr, fusion machine pin library, package3d, wip_urn, 3d flat pad, placeholder 3d, scaffold pin, 32mm grid pin, sample board machine pins.

Adom machine-pin Fusion library + board, the full recipe

Reproducible, background, end-to-end. Source of truth: adom-inc/[email protected] (footprints/*.kicad_mod, symbols/*.kicad_sym, 3dmodels/*.step). Live result + downloads + screenshots: wiki.adom.inc/adom/adom-machine-pins-fusion-lbr.

0. The pins (all PRESS-FIT, this is the key fact)

Pin Body OD Drill Pad Ring Interference
Medium-Standard / -Short Ø1.20 Ø1.10 Ø1.60 0.25 0.10 mm
Large-Standard / -Short Ø3.60 Ø3.45 Ø5.20 0.875 0.15 mm

The body OD is larger than the plated drill on purpose (Mill-Max press-fit, solder-assisted with 300 µm jet paste). The undersized hole is CORRECT, never "fix" it to a clearance hole.

1. Generate the four .lbr (adom-lbr 2.4.0+)

adom-lbr generate --sym <MPN>.kicad_sym --fp <MPN>.kicad_mod \
  --symbol-art <MPN>-symbol-outline.mm.svg -o <MPN>.lbr
  • --symbol-art (outline-contract .mm.svg + .json sidecar, placed 1:1) OVERRIDES the shared generic kicad_sym drawing so each size's symbol is distinct. adom-symbol makes the outline.
  • 2.4.0 natively emits a round thru-hole pad, the real fp_circle silk, and names the pin from the pad number. ⚠️ An EMPTY pin name (<pin name=""> + <connect pin="">) makes Fusion reject the .lbr with "has errors and cannot be opened", verify adom-lbr lint is clean.
  • Merge the four into one library: one <layers> skeleton + concatenated packages/symbols/devicesets; give each symbol's single pin a UNIQUE name (else duplicate-pin-name).

2. Bind the real 3D, ONE PIN AT A TIME (do NOT trust the batch)

The batch fusion_build_library_3d reliably TIMED OUT / stalled (cluttered Fusion state + the 60s relay cap); a demo board came back with 3/4 pins as flat pads. What WORKS: a single fusion_make_3d_package per pin completes < 60s in a clean Fusion and returns a fresh wip_urn.

Prereqs: a clean-ish Fusion (restart if many tabs are open) + fusion_aps_signin (warm SSO, NEVER the password) so the f3d uploads to the Hub.

for pin in the four:
  fusion_make_3d_package {lbrPath:<pin>.lbr, modelPath:<pin>.step, orient:false}
    -> record wip_urn   (orient:false, the STEP is already Z-up; tall pins must not be flattened)

3. Write the 3D into the library, BOTH places (the #1 gotcha)

For each part the .lbr needs:

  1. a library-level <package3d name="PKG" urn="" wip_urn="urn:..." locally_modified="yes" type="model"><description>PKG</description><packageinstances><packageinstance name="PKG"/> </packageinstances></package3d> (in a <packages3d> block after </packages>), AND
  2. inside every <device package="PKG">, after </connects>: <package3dinstances><package3dinstance package3d_urn="urn:..."/></package3dinstances>.

Both are required. With only (1), the Fusion library editor shows a "Placeholder" 3D, not the pin. The bridge's _inject_package3d_bindings does exactly this (merge-aware/idempotent), reuse it.

4. The sample board, background, NO modal dialogs

Hand-author an EAGLE .brd (well-formed XML): <settings>/<grid>, the full <layers>, a board outline on layer 20, an embedded <library> with <packages> + <packages3d> (fresh urns), and <elements> placing the four pins, each element with package3d_urn="<that pin's wip_urn>" (no urn ⇒ flat pad). Add silkscreen with <text x y size layer="21">name / drill / ring</text> in <plain>.

Open it in the BACKGROUND:

fusion_open_board  {filePath:<.brd>}   # newDesignFromLocal -> instantiates placed parts, ZERO dialogs
fusion_show_3d_board {}                # renders the 3D PCB with the real pin bodies

⛔ Do NOT use fusion_new_electronics_from_eagle / ImportSCHAndBRDCmd for a placed board, its two native Open dialogs are MODAL and STEAL the user's focus. There is no background way to drive them; fusion_open_board is the background route. (A DRC-clean KiCad .kicad_pcb equivalent can be rendered headlessly via service-kicad pcb export svg.)

5. If you IMPORT this library and the 3D is flat / Placeholder

The wip_urns are bound to the account that built it and may not resolve on yours. Re-bind on your own account (§2), then rewrite the .lbr's <package3d>/<package3dinstances> and the board's element package3d_urns with your fresh urns, and re-open. Same procedure as the original build.

Bridge features this relies on (all keep Fusion in the background)

  • fusion_open_board = Document.newDesignFromLocalno-dialog placed-board import.
  • package3d_urn on .brd elements → real 3D bodies on the board (fusion_new_electronics_from_eagle docstring documents it).
  • fusion_build_library_3d runs detached + pollable (survives AD's 60s relay cap), but prefer per-pin fusion_make_3d_package.
  • Universal error-dialog guard (flips success:false on a Fusion "Error" popup); notify-before- foreground; APS warm-SSO sign-in + browser-profile cache + never-charge cap; HWND/parent-child screenshot fixes.

Known follow-ups

  • adom-lbr: the empty-pin-name and round-pad fixes landed in 2.4.0; --symbol-art restored there too.
  • 300 µm solder-ball layer standard: a hidden User.* layer in every footprint (see the solder-ball-layer standard page); paste.rs computes it on the fly for SMD only today.
  • Batch build_library_3d reliability on slow/cluttered sessions, per-pin binding is the workaround.

Hard requirements from review (do all of these, they were each called out)

These are non-negotiable lessons from building this exact library. Every one was a correction.

  1. ONE library, not a library per pin. Adom users want a single .lbr with all four pins in one list. Never ship four single-pin libraries; merge them.
  2. Short, clean, non-embarrassing names, everywhere. Rename the deviceset, symbol, package, AND footprint to short forms (MP-MED-STD / MP-MED-SHORT / MP-LRG-STD / MP-LRG-SHORT), not MachinePinMediumStandard. The deviceset name renders as the >NAME on the symbol, so a long name looks terrible; the long name also fills the Footprint/Package columns. Do NOT abbreviate Short to "SHT" (it reads as a swear word); spell it SHORT. When you rename the package, keep the <package3d wip_urn> so the 3D stays bound (the binding is URN-based, not name-based); update <package name>, <device package=>, <package3d name>, and <packageinstance name> together.
  3. Real holes, not solder-mask over a hole. The FR4 board must have a REAL through-hole cut at each pad (boolean-subtract a drill-diameter cylinder from the board mesh; pip install manifold3d for the trimesh backend). Do not draw green board across where the hole should be.
  4. Silkscreen on the board. Put the pin name + drill + annular ring as silk text next to each pad, in both the 2D board (<text ... layer="21">) and the 3D (matching the Fusion board). Geometry-only exports (STEP/OBJ) DROP silk because it is a surface graphic, so you cannot just export the Fusion board for a silk-carrying GLB; match it in the GLB instead.
  5. Match the Fusion 3D board. The lead 3D on the page must look like the real Fusion 3D board: real holes, silk, real pins, transparent FR4, the annular ring, plus the insertion animation and the 300um solder-paste jet balls. The lead 3D shows ALL FOUR pins, never a single pin.
  6. Never move the user's mouse / foreground Fusion to navigate. To switch the selected component in the library editor, use the EAGLE command in the BACKGROUND: fusion_electron_run {command:"EDIT <deviceset>.dev"}, then a background desktop_screenshot_window (PrintWindow, no foreground). Do NOT use desktop_click/fusion_click_fusion/arrow-keys on the list; they steal focus, move the mouse, 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 lead-GLB recipe that shipped (authoritative, every step below was learned the hard way)

The lead 3D is ONE GLB the native Babylon viewer auto-plays. Base it on the REAL Fusion board (the user insisted, no hand-modeled slab for the pins). Full builder: [scripts/build_board_glb.py]; animation: [scripts/add_anim_board.py]. The exact ordered recipe:

  1. Decode the real Fusion board. Export the Fusion 3D PCB to STEP, step2glb it -> a Draco GLB. trimesh CANNOT read Draco (returns zero-area meshes). Use DracoPy.decode per primitive + compose node transforms yourself. Pins are the nodes named MpinContact* / =>[0:1:1:*]; cluster them by xy into 4, merge each into ONE node (PIN_0..PIN_3) so it animates as a unit.
  2. 1.6mm FR4 board with REAL holes. The Fusion 3D export is only ~0.3mm thick, REBUILD it: a trimesh.creation.box([50,50,1.6]), .difference() a drill-diameter cylinder per pad (needs manifold3d). Green, alphaMode=BLEND alpha ~0.46, single-sided (doubleSided=False) so the transparent slab does not show its own back-face triangulation.
  3. 1oz copper (~35um), NOT buried in the FR4. Per pad: a trimesh.creation.annulus(drill/2, pad/2) ring sitting PROUD on the top surface, another PROUD on the bottom, and a thin barrel (annulus(drill/2-CU, drill/2)) lining the hole, sticking out a hair top+bottom. Copper is real 1oz = a super-thin layer, do NOT make it thick.
  4. 300um HEMISPHERE solder paste. icosphere then slice_plane([0,0,0],[0,0,1],cap=True) = a dome with a flat bottom on the pad. Real 0.30mm dia (do NOT oversize), on the annular ring at ~0.45mm pitch. Keep balls STATIC (paste stays on the pad; only the pin moves).
  5. Silkscreen + callouts = 3D EXTRUDED TEXT, never a texture. A flat texture on the board fights you forever: its UV + the GLB export's per-face mirror + any rotation keep DE-SYNCING the label positions from the pins (this caused a med<->large label swap that shipped). 3D text meshes (text_mesh() -> extrude_polygon) live in the same coordinate space as the pins, so labels stay glued to their pins. Cut-out letter counters via even-odd = full-polygon containment (o.contains(other)); a centroid point falls in the counter of O/D/e and misclassifies.
  6. Callout arrows: apex ON the target. trimesh cone has its apex at +Z*height; after orienting the axis to the arrow direction, translate the BASE to tip - height*dir so the POINT lands on the target (ball / hole), not 1mm past it.
  7. Callout wording. Show hole Ø<drill> pin Ø<body> and pin <Δ>mm wider = interference fit. Do NOT say "gas-tight" (press-fit marketing jargon, overstated, not everyday EE language). Snap the SOLDER-PASTE arrow tip to the ACTUAL nearest ball centre (balls are discrete 300um dots).
  8. Text must cast NO shadow, and this is GLB-only. The viewer's ShadowGenerator has transparencyShadow=false, so it SKIPS alpha-blended meshes when building the shadow map. Give the silk/callout materials alpha ~0.98 (alphaMode=BLEND, still visually solid) => they render but cast no shadow. No viewer change, no nocast naming needed.
  9. Orientation: do NOT rotate the assembly. 3D text in its natural orientation already reads left-to-right at the viewer's DEFAULT camera. Rotating the assembly 180 (a tempting "face the camera" fix) flips the text upside-down. Leave ROT_CENTER=None.
  10. Animation: one glTF translation animation, a sampler+channel per PIN_* node, gentle 2-3mm lift (a big lift makes the pins look like giant floating pins over tiny holes). Append accessors to the binary blob with the add_anim.py bytearray pattern (do NOT hand-splice bufferView offsets).
  11. VERIFY BEFORE PUSH, every time. Headless-render (DracoPy + pyrender EGL, see the section above) AND load in the real Babylon viewer via pup. Confirm by MEASUREMENT that each label's nearest pin is the right SIZE (Ø1.6 medium vs Ø4.8 large) — the label swap was invisible in code.

Native Fusion Electronics files to ship

Provide fusion/AdomPinsBoard.fsch (schematic) + .fbrd (board) + .f3d (3D design) + .step. Fusion Electronics IS EAGLE 9.x, so .fsch/.fbrd are just the EAGLE XML (<eagle version="9.6.2">) with Fusion's extensions — rename the EAGLE .sch/.brd, they open directly in Fusion (File > Open). The .f3d is the 3D BREP design (NOT the ECAD design); it does not contain the schematic/board.

The ONE thing that still needs a viewer PR (adom-3d-viewer-babylon9, Colby)

Real cast shadows do NOT animate: the viewer freezes the shadow map (AdomStatics.refreshShadows() sets refreshRate = REFRESHRATE_RENDER_ONCE). Fix = keep REFRESHRATE_RENDER_ONEVERYFRAME when the scene has animationGroups. That is a viewer-repo change (staged), send it as a PR to Colby; it cannot be fixed from the GLB.

pup / show-the-user workflow (do not make them hunt for the window)

  • The pup window does NOT auto-update on a push. After each push, re-navigate it with a ?v=N cache-bust (the viewer caches the GLB) so it shows the new model.
  • Do NOT location.reload(true) — it wedges the CDP session and you have to reopen the window.
  • REUSE one maximized window; show it on the machine the user is physically at (ask which if a VNC is in play — a window on the VNC'd host is on the wrong screen).