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.
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+.jsonsidecar, 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_circlesilk, and names the pin from the pad number. ⚠️ An EMPTY pin name (<pin name="">+<connect pin="">) makes Fusion reject the.lbrwith "has errors and cannot be opened", verifyadom-lbr lintis clean. - Merge the four into one library: one
<layers>skeleton + concatenated packages/symbols/devicesets; give each symbol's single pin a UNIQUE name (elseduplicate-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:
- 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 - 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.newDesignFromLocal→ no-dialog placed-board import.package3d_urnon.brdelements → real 3D bodies on the board (fusion_new_electronics_from_eagledocstring documents it).fusion_build_library_3druns detached + pollable (survives AD's 60s relay cap), but prefer per-pinfusion_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-artrestored 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_3dreliability 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.
- ONE library, not a library per pin. Adom users want a single
.lbrwith all four pins in one list. Never ship four single-pin libraries; merge them. - 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
>NAMEon 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. - 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 manifold3dfor the trimesh backend). Do not draw green board across where the hole should be. - 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. - 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.
- 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 backgrounddesktop_screenshot_window(PrintWindow, no foreground). Do NOT usedesktop_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.) - No em-dashes anywhere on the wiki page. They read as AI-slop; use commas, colons, or hyphens.
- Only ship silkscreen board shots. A 3D board with no silkscreen is not worth showing.
- 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.
- 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:
- Decode the real Fusion board. Export the Fusion 3D PCB to STEP,
step2glbit -> a Draco GLB.trimeshCANNOT read Draco (returns zero-area meshes). UseDracoPy.decodeper primitive + compose node transforms yourself. Pins are the nodes namedMpinContact*/=>[0:1:1:*]; cluster them by xy into 4, merge each into ONE node (PIN_0..PIN_3) so it animates as a unit. - 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 (needsmanifold3d). Green,alphaMode=BLENDalpha ~0.46, single-sided (doubleSided=False) so the transparent slab does not show its own back-face triangulation. - 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. - 300um HEMISPHERE solder paste.
icospherethenslice_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). - 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. - Callout arrows: apex ON the target. trimesh
conehas its apex at +Z*height; after orienting the axis to the arrow direction, translate the BASE totip - height*dirso the POINT lands on the target (ball / hole), not 1mm past it. - Callout wording. Show
hole Ø<drill> pin Ø<body>andpin <Δ>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). - 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, nonocastnaming needed. - 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. - 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 theadd_anim.pybytearray pattern (do NOT hand-splice bufferView offsets). - 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=Ncache-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).
---
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_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).