---
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/adom-kicad-library@new-v1.1`**
(`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_urn`s 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_urn`s 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.newDesignFromLocal` → **no-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).
