Closed general

Let a GLB declare its own layers via extras.adomLayer

Colby Knox · 11d ago ·closed by Colby Knox

Split out of adom/wiki#60 (@aravk item 4). Filed here rather than on adom/wiki because it is viewer engine work, not wiki work.

Problem

The 3D viewer's Layers panel is driven by a hardcoded key list (ThreeDViewer.svelte, LAYER_ORDER plus LAYER_LABELS): fr4_board, pad_top, solder_top, silk, barrel, pin1_marker and friends. A GLB whose meshes follow that naming convention gets a layers panel; anything else gets nothing.

That works for the PCB pipeline, which is what it was built for. It does not work for any other kind of model. AravK's downstream Static / Pick-and-Place viewer had to layer its own vocabulary on top as host-page code, because there is no way for a GLB to declare what its own layers are.

Ask

A generic mechanism, so any GLB can carry its own layer vocabulary:

extras.adomLayer = { key, label, group }

read from glTF extras during traversal, the same way extras.tooltip already is.

Why it is not a small change

  • Traversal: extras may sit on the mesh or an ancestor node, since Babylon splits multi-material primitives into child meshes. The tooltip code already walks up to five ancestors for this reason, and layer detection needs the same treatment.
  • Grouping and ordering: the current panel gets its order from the hardcoded array. Declared layers have no inherent order, so group needs to define sectioning and something has to define ordering within a group. Declaration order is the obvious default but needs deciding.
  • Back-compat: the 12 convention keys must keep working unchanged, including the default-off behaviour for solder and paste layers. Simplest shape is that the convention becomes a built-in vocabulary expressed in the same mechanism, rather than a parallel code path.
  • Panel UI: today it is a flat list with a hide-under-two-layers rule. Groups mean sections, and an arbitrary vocabulary means labels that were never length-checked.
  • Defaults: the convention hardcodes which layers start hidden. A declared layer needs a way to say that, so extras.adomLayer probably wants an optional hidden or defaultVisible.

Prior art in the codebase

docs/CONTENT-CONVENTIONS.md documents the naming convention. extras.tooltip is the working example of reading glTF extras through the ancestor chain, and is the model to copy.

2 Replies

Colby Knox · 11d ago

Reframing this after a direction call from @kcknox: the viewer should stay a blank canvas, with wiki-specific behaviour built on top through its API rather than baked into the engine.

That does not make this issue less worth doing. It makes it the single clearest example of the problem.

The engine currently contains PCB domain knowledge. ThreeDViewer.svelte hardcodes a twelve-key vocabulary: fr4_board, pad_top, pad_bottom, solder_top, solder_bottom, paste_top, paste_bottom, silk, silk_top, silk_bottom, barrel, pin1_marker. It also hardcodes their display labels, their ordering, and the policy that solder and paste layers start hidden. None of that is generic 3D viewing. It is a printed circuit board vocabulary living inside an engine that is supposed to know nothing about printed circuit boards.

So this issue is not "add a feature for a downstream consumer". It is remove the domain knowledge the engine should not have had, which happens to also give @aravk what he asked for. The generic mechanism and the extraction are the same change:

  • The engine gains a content-declared layer mechanism (extras.adomLayer, read through the ancestor chain the way extras.tooltip already is) plus getLayers() / setLayerVisible(), which already exist.
  • The PCB vocabulary stops being engine code. It becomes either content-declared, in the GLBs the board pipeline emits, or consumer-supplied by the wiki page that embeds the viewer.
  • AravK's Static / Pick-and-Place vocabulary then needs no host-page workaround, because it is expressed the same way the PCB one is.

One question the blank-canvas direction raises that the original issue did not. The layers panel is UI chrome with a policy in it (hide under two layers, solder off by default). Under a strict reading, the engine exposes the layer API and the embedder renders its own panel. That is the cleaner architecture and a larger change, since today's panel is the only layers UI anyone has.

My inclination is to do this in two steps rather than one: make the vocabulary content-declared first, keeping the existing panel as the default renderer so nothing regresses, and treat "move the panel out of the engine" as a separate decision once there is a second consumer to prove the shape. Extracting the vocabulary is the part that pays off immediately and is what unblocks downstream consumers; extracting the panel is a refactor with no user-visible win until someone actually wants a different panel.

Back-compat is non-negotiable either way: the twelve keys keep working, including the default-hidden behaviour, or every component page on the wiki regresses.

Colby Knox · 11d ago

Shipped in 0.6.0, live on prod (bundle 20260730b).

The engine no longer contains a PCB vocabulary as logic. Those twelve names are now a table of data, in exactly the shape a GLB can declare for itself:

"extras": { "adomLayer": { "key": "reels", "label": "Reels", "group": "Mechanism" } }

key is the stable identifier setLayerVisible() takes, label is what the panel shows, group sections the panel, and hidden: true starts a layer switched off the way solder and paste do.

Deliberately not a second code path. The board names are simply the default vocabulary, which is what keeps every existing component page working, and content declarations win over them so a node that happens to be called silk can still say it is something else.

Details that were easy to get wrong:

  • extras is read through the ancestor chain, up to six levels, the way extras.tooltip already is. Babylon splits multi-material primitives into child meshes, so the declaration almost always sits on the parent group rather than the mesh that gets picked.
  • Ordering: built-in keys keep the table's order, so PCB pages are byte-for-byte unchanged. Declared layers follow in the order they appear in the file, which is the only ordering the content actually expresses.
  • Declared strings are sanitized and length-capped before they reach the DOM. Content is untrusted.
  • getLayers() now returns group and declared alongside name/label/visible, so a consumer building its own panel does not have to re-derive either.

Verified in a browser against two fixtures, not just unit-tested: a GLB using only the board names (all four layers found, correct labels and groups, solder_top still defaults hidden, declared: false) and one whose node names are deliberately meaningless (Mesh_001...) and whose layers come entirely from extras (custom labels, grouping, and hidden all honored, declared: true). Then confirmed a real production board still renders correctly on prod.

Left for later, on purpose. The panel itself still lives in the engine. Under a strict reading of the blank-canvas rule the embedder should render its own, and getLayers()/setLayerVisible() now expose everything needed to do that. I did not move it because there is no second embedder yet to prove the shape, and extracting UI with one consumer tends to bake that consumer's assumptions into the API. Worth revisiting when someone actually wants a different panel.

Docs: docs/CONTENT-CONVENTIONS.md has the declaration format and the gotchas; the README API list is updated.

Log in to reply.