# Content conventions for viewer GLBs

What a GLB must look like to get the viewer's built-in behaviors (tooltips,
the coming layers toolbar) and to avoid the known build-flag landmines. Aimed
at content pipelines (machine-pin bakers, kicad-to-glb, board exporters).
Everything here was learned the hard way on adom/bq25792-charger and the
machine-pin pages (adom/wiki #35, #47, #48).

## Layer names

Name meshes (or the transform nodes grouping them) with these exact names.
The viewer maps them to human labels today (hover tooltips) and the layers
toolbar will group toggles by them.

| Name | Label shown | Notes |
|------|-------------|-------|
| `fr4_board` | FR4 board | the board substrate |
| `pad_top` / `pad_bottom` | Pads (top/bottom) | copper pads |
| `solder_top` / `solder_bottom` | Solder (top/bottom) | joints/balls; default OFF in the layers toolbar |
| `paste_top` / `paste_bottom` | Paste (top/bottom) | paste dots |
| `silk` or `silk_top` / `silk_bottom` | Silkscreen | |
| `barrel` | Plated barrel | plated through-hole barrels |
| `pin1_marker` | Pin 1 | pin-1 designator geometry |

Anything else keeps its own name (underscores become spaces in labels).

## Hover tooltips (adom/wiki #35)

- Put a `tooltip` string in the glTF `extras` of the node or mesh:
  `"extras": { "tooltip": "Solder ball\nDia 0.30 mm (300 um)" }`.
  Babylon lands it at `mesh.metadata.gltf.extras.tooltip`. `\n` makes a
  multi-line label. This always wins over the name.
- No extras: the mesh name is the fallback, run through the layer-label table
  above. Auto-generated junk names (`mesh_*`, `node_*`, `*_primitive3`,
  `__root__`, ...) show nothing, so unnamed models stay label-free.
- The viewer checks the picked mesh, then up to 5 ancestors, so extras on a
  group node cover all its children (Babylon splits multi-material primitives
  into child meshes).

## GLB build flags that work

- `--instance false`. `gltf-transform optimize` defaults to
  EXT_mesh_gpu_instancing, which mis-renders here as one giant mis-scaled
  instance. The viewer detects it and warns in the console, but the fix is
  rebuilding without it.
- Draco (`KHR_draco_mesh_compression`) works on the wiki's `/viewer/3d`
  (self-hosted decoder + `wasm-unsafe-eval`), but NOT inside a readme
  sandbox (no wasm, no workers). Content meant to load in readmes should
  ship uncompressed or quantized.
- `KHR_mesh_quantization` is decoder-free and loads everywhere. Framing and
  bounds verified correct on Babylon 9.5 (gltf-transform v3 output). If you
  see a mis-framed quantized model, file it with the GLB; one historical
  report (radius ~2700) could not be reproduced.
- `--flatten false --join true` preserves the node structure tooltips and
  layers rely on while still reducing draw calls.
- Animations must target TRS on nodes (not skins) for the exploded-stackup
  style effects; the viewer auto-unfreezes the shadow map when a GLB has
  animation groups, so animated content gets moving shadows for free.

## Shadows

- The bundle uses PCF (QUALITY_HIGH). PCSS (`useContactHardeningShadow`)
  combined with `enableSoftTransparentShadow` silently renders no shadows at
  all (pixel-diff proven: PCSS 0.0% change vs PCF 26.7%); the bundle detects
  the combination every frame and falls back to PCF with a console warning.
- Shadows only darken direct light. If you re-light a scene, keep the spot
  dominant (env as fill, ~0.35 intensity) or shadows wash out on PBR surfaces.

## Verifying lighting/shadow changes (John's protocol)

Freeze the scene, verify twice, hide the ground, then pixel-diff two
captures. Never pixel-diff an animating scene; motion reads as change.

## Embedding quick reference

```js
const viewer = Adom3DViewerBabylon9.init(container, { zUp: true, showViewCube: true, showGround: true });
await viewer.loadModel(url);   // frames, registers shadow casters
```

The wiki's `/viewer/3d/<type>/<slug>` route accepts `?model=<repo-path>`,
`?ground=0`, `?axes=1`, `?animate=0`, `?bottomlight=1`, and
`?camera=alpha,beta[,radiusScale]` so a page can embed variants and start
states without page scripting.

Sandboxed readme embeds need no storage shim as of 20260717a; the bundle
guards its own storage access. The full sandbox contract lives in git-wiki's
`docs/readme-sandbox.md`.
