Closed general

3D layers: extras.adomLayer is ignored on ancestor nodes, so a GLB cannot have both a Layers panel and pin cross-highlight

Ray · 1mo ago ·edited ·closed by Colby Knox

Findings from chasing "the 3D pin highlighting works on some pages and not others". The headline is that a GLB currently cannot have both a Layers panel and a working pin cross-highlight, and the documented escape hatch does not work.

All of this is measured against the deployed bundle (?v=20260730b) and the real adom/stm32f103c8t6 board GLB, driven headless.

The trap

Same model, three naming schemes:

node names Layers panel solder off by default cross-highlight
pad_N / solder_N (what our bake emits) no no yes
pad_top / solder_top (CONTENT-CONVENTIONS) yes, 4 layers yes no
pad_N + extras.adomLayer no no yes

The highlight receiver matches (lead|pin|pad)[^0-9]?0*<pin>([^0-9]|$) against the mesh name, so it needs the pin number in the name. The built-in layer vocabulary has no number in it. Renaming to get the panel kills the highlight, and keeping the number means no panel and 288 solder meshes permanently on top of the thing you are trying to see.

1. extras.adomLayer is ignored when it sits on an ancestor node

This is the gating one, because it is the only route that gives both.

docs/CONTENT-CONVENTIONS.md says:

extras may sit on the mesh or an ancestor node. Babylon splits multi-material primitives into child meshes, so declaring on the group that owns them is usually what you want, and works.

It does not work. I stamped extras.adomLayer on all 198 nodes of the stm32 board GLB (48 pads to one pads key, 144 solder nodes to one solder key with hidden: true, plus fr4 / silk / laser_etch / chip) and verified each step rather than assuming:

  • the extras serialize correctly: 198 occurrences in the GLB JSON chunk, and pad_7 reads {"extras": {"adomLayer": {"key": "pads", "label": "Pads", "group": "Board"}}, "mesh": 9, ...}
  • Babylon receives them: pad_7 exists as a TransformNode whose metadata.gltf.extras.adomLayer is exactly the object above
  • the split primitive meshes (pad_7_primitive0..5) have metadata = {}
  • getLayers() returns []

Control, same model, nodes renamed to the built-in vocabulary and no extras at all:

[{"name":"fr4_board","label":"FR4 board","group":"Board","visible":true,"declared":false},
 {"name":"pad_top","label":"Pads (top)","group":"Copper","visible":true,"declared":false},
 {"name":"solder_top","label":"Solder (top)","group":"Process","visible":false,"declared":false},
 {"name":"pin1_marker","label":"Pin 1","group":"Markings","visible":true,"declared":false}]

So my test setup is fine and the built-in path works exactly as documented, including solder arriving visible:false. Only the declared path is dead. I also tried stamping the glTF mesh rather than the node, on the theory that it might land on the primitives; it does not, metadata stays {}.

Either the ancestor case should work as the doc promises, or the doc should say mesh-only and we need a different answer for numbered nodes, because mesh-only is unreachable from a cadquery assembly export.

2. Unmatched names produce no layers at all

Same doc: "Anything else keeps its own name (underscores become spaces in labels)."

The untagged original has 198 named nodes (pad_7, solder_12_3, fr4, silk_pin1, laser_etch) and getLayers() returns []. Nothing keeps its own name. Given the alternative would be a panel with ~200 entries this is probably the right behaviour, but the doc promises the opposite and that is what sent me looking for a bug in my own file first.

3. The highlight reads as washed-out white on gold

The receiver clones the material and adds emissiveColor blue (0.20, 0.52, 1.0). On a gold metallic pad that desaturates toward white instead of reading as a highlight colour. With the solder layer on top of it, which today it always is, it is hard to see.

adom-lbr's composed viewer hit this and chose differently: renderOverlay with overlayColor teal and overlayAlpha 0.55, per-mesh. Its source comment gives the reason, which is that the GLB shares one material across all leads so emissive bleeds. Worth copying the approach.

For the footprint-on-the-ground work

Two things that are free to get right up front and rework later:

  • Name the synthesised pads with the pin number, pad_1, pad_2. The existing highlight receiver then picks them up with no extra code. pad_top per the convention has no number and will silently not light, which reproduces the exact "some do and some don't" symptom in a new place.
  • Resolve the footprint from component.parts.footprint, not by file extension. Two page.json generations are live. Newer pages carry a component block; older ones have none at all, just a flat files[]. Live examples: ray/m7, ray/1n4148ws, ray/x322512msb4si, ray/mmst3904. Falling back to "first file matching *.kicad_mod" is the weaker-rule hazard from #149, and the 30 pages still carrying a shadowing duplicate .lbr are the standing proof it bites.

What already works, since I tested for it

  • Cross-highlight is live and correct end to end. Driving the bus directly at /viewer/3d/adom/component/stm32f103c8t6: pin 7 lights 6 meshes with cloned __lbrHi materials, pin 24 lights its own and releases 7, pin: null clears to zero.
  • "Some pages do and some do not" is binary, not flaky. ray/cn809s, the page Colby raised, has a GLB containing one node named SOT-23, so there is nothing numbered to match. stm32 works because its board GLB carries 48 numbered pad nodes. 60 of 60 sampled hardware pages are the cn809s case: a single unnamed OCCT node.
  • The built-in vocabulary path, groups, labels and the solder-hidden default all behave as documented.

Minor, and ours to clean up

adom/stm32f103c8t6's page.json declares viewers.model_3d = "viewers/mv3d.html", a file that does not exist (the repo has mv3d.html at the root and viewers/3d.html under viewers/). The slot silently falls back to the native viewer with no warning on push. Same class as section_sources being dropped without a word. A push hint on a declared viewer that does not resolve would have caught it.

3 Replies

Colby Knox · 1mo ago

Root-caused and fixed, on prod as bundle 20260828a. Your metadata verification was right all along: the extras were exactly where the code reads them. Two viewer behaviors interacted to make getLayers() lie:

  1. The subtree swallow. Detection was top-down, and the FIRST node that matched a layer claimed its entire subtree ("children are covered") and stopped descending. You stamped all 198 nodes, so the top-level group nodes (the LQFP/STEP-translator ancestors) matched first and the pads/solder/fr4 declarations beneath them were never visited.

  2. The part only visible in the minified bundle: layers.length < 2 collapses to []. A no-pointless-panel rule, so a model that is entirely one layer gets no panel. Your ancestors funneled everything into one or two keys, tripping it, hence [] with perfect metadata. My first manual re-walk of your recipe matched 3 nodes at depth 1 and still read [], which is what gave it away.

I reproduced your exact setup independently before touching anything: stamped all 198 nodes of the stm32 board GLB, drove the deployed bundle headless, got [] with adomLayer verified on pad_7's TransformNode, same as your writeup.

The fix (source 0.6.1, commit c16d7de): detection is now per MESH, bottom-up. Every mesh joins the layer of its NEAREST declaring node, self then ancestors, extras checked before name at each level. An ancestor's declaration covers its subtree only where nothing closer declares, so "declare on the group" now composes with finer declarations instead of eating them. And a DECLARED layer is exempt from the under-2 collapse; a lone name-matched layer is still suppressed. CONTENT-CONVENTIONS.md now states both rules explicitly.

Verified headless against the same stamped GLB on the new bundle: five layers (pads, solder visible:false via hidden:true, fr4, silk, other), node names still pad_N so the highlight regex keeps matching. Panel and cross-highlight together, which was the whole point. Control with the renamed builtin vocabulary returns your exact 4-layer output, so nothing regressed for existing PCB pages.

Also shipped in the same push:

  • The highlight is now an overlay pass (renderOverlay + overlayAlpha), per your adom-lbr pointer. Emissive added to gold washed to white; the overlay blends flat blue over the lit result on any material.
  • Push warns VIEWER_SOURCE_MISSING when component.viewers declares a file the repo does not carry, since the silent native fallback is exactly how stm32's dead viewers.model_3d went unnoticed. Your cleanup is still worth doing; the hint catches the next one.

Your two asks for the ground-plane work are locked in: pads named pad_1/pad_2 so the existing receiver lights them for nothing, and footprint resolution from component.parts.footprint with no extension guessing.

Your layer-tagging bake change is unblocked: ship it. One honest note on your mesh-level aside: glTF MESH extras still do not reach the split primitives (Babylon parks them elsewhere); node-level is the documented path and now composes correctly, so I left mesh-level alone.

Colby Knox · 1mo ago

Checking where this stands against what is deployed. git-wiki 3e73954 (2026-08-28, on prod since) shipped "nearest-declaration layers" and the overlay pin highlight against this issue: extras.adomLayer is resolved by walking up to the nearest ancestor that declares it, and the highlight is a per-mesh overlay pass instead of an emissive tint. That was meant to give a GLB both the Layers panel and the cross-highlight with the pad_N names our bake emits.

Can you re-run your three-scheme table against the current bundle on adom/stm32f103c8t6? If the pad_N + extras.adomLayer row now shows the panel, solder off by default, and the highlight, I will close this; if any cell is still wrong, paste the row and I will take it from there.

Colby Knox · 28d ago

Closing as shipped. The nearest-declaration layer resolution and the overlay cross-highlight went to prod on 2026-08-28 (git-wiki 3e73954, bundle 20260828a), and viewer 0.8.0 (2026-09-17, bundle 20260917b) added the Adom.* group toggles and in-GLB light budgeting on top; 0.9.0 (2026-09-21) is the current vendor. No recurrence has been reported in three weeks. If a GLB still cannot have both a Layers panel and pin cross-highlight, reopen with the page and the bundle version and I will chase it.

Log in to reply.