b3d-library

Asset library component. Loads a GLB file via LoadAssetContainer and holds it as a reusable parts catalog — nothing is added to the scene until you call instantiate(name).

Libraries register with the parent B3d by type, so consumers (like a tile map) can discover them via owner.getLibrary('tiles') without holding direct references.

Demo

import { b3d, b3dLibrary, b3dLight, b3dSkybox, b3dGround, placeOnSurface, label3d, list3d, button3d } from 'tosijs-3d'
import { elements } from 'tosijs'
const { div, p } = elements

const lib = b3dLibrary({ url: '/test-3.glb', type: 'scene' })

function isInsertable(node) {
  return node.isMesh || node.children.some(c => c.isMesh)
}

// Flatten the GLB hierarchy into one scrollable pick list: every insertable node
// (a mesh, or a group that contains meshes), children indented under their parent.
// Instantiating a group clones it whole; instantiating a leaf clones just that.
function flattenInsertable(nodes, depth = 0, out = []) {
  for (const node of nodes) {
    if (isInsertable(node)) {
      out.push({ label: '- '.repeat(depth) + node.name, name: node.name })
    }
    if (node.children.length) flattenInsertable(node.children, depth + 1, out)
  }
  return out
}

const scene = b3d(
  {
    // Dual-presence picker: the mesh list lives in the ⚙ panel, so you can spawn
    // parts from inside VR too. The hook re-reads the hierarchy each time the panel
    // is (re)built; refreshScenePanel() below updates an already-open panel once
    // the GLB finishes loading.
    scenePanel: () => {
      const items = flattenInsertable(lib.getHierarchy())
      return [
        label3d({ text: items.length ? 'Spawn a mesh' : 'Loading…' }),
        list3d({
          items,
          onSelect: (it) => {
            const placed = lib.instantiate(it.name)
            // Animated models come alive on spawn: loop their first group
            // (the scout opens its cockpit — see "Animations travel with the
            // instance" below).
            placed?.metadata?.animationGroups?.[0]?.start(true)
            // Rest the spawn on the ground rather than at the origin (where it may
            // float or clip depending on the GLB).
            if (placed) placeOnSurface(placed)
          },
        }),
        button3d({ label: 'Clear all', onClick: () => lib.clearInstances() }),
      ]
    },
    // No custom camera — b3d's default orbit camera already has sensible limits
    // (≥5° above the horizon, bounded zoom) so you can't tilt under the ground or
    // zoom through the parts.
  },
  b3dLight({ y: 1, intensity: 0.7 }),
  b3dSkybox({ timeOfDay: 12 }),
  b3dGround({ width: 20, height: 20 }),
  lib,
)

// Refresh an already-open panel once the model has loaded (opening it after the
// load already picks up the list via the rebuild-on-open above).
lib.ready.then(() => scene.refreshScenePanel())

preview.append(
  scene,
  div(
    { class: 'debug-panel' },
    p('Open the ⚙ to spawn library meshes — works in VR too.'),
  ),
)
tosi-b3d { width: 100%; height: 100%; }
.debug-panel {
  position: absolute;
  top: 10px;
  right: 10px;
  display: flex;
  flex-direction: column;
  gap: 8px;
  padding: 8px 16px;
  background: rgba(0, 0, 0, 0.6);
  color: white;
  border-radius: 6px;
  font-size: 14px;
  z-index: 10;
}
.debug-panel select, .debug-panel button {
  color: white;
  background: #444;
  border: 1px solid #888;
  border-radius: 4px;
  padding: 4px 8px;
  font-size: 13px;
}

Attributes

Attribute Default Description
url '' GLB/glTF file URL
type '' Library type for scene registry lookup

API

Authoring conventions (Blender)

Animations travel with the instance

If the source model carries AnimationGroups (the scout's Cockpit Open and gear-retract animations in test-3.glb), instantiate clones them retargeted onto the instancenode.clone() alone would leave them on the container, animating the original nobody can see. They land on instance.metadata.animationGroups, named <group>::<instance> so multiple instances animate independently:

const scout = lib.instantiate('scout', { canonical: true })
const cockpit = scout.metadata.animationGroups
  .find((g) => g.name.startsWith('Cockpit Open'))
cockpit.start(false) // one shot; .start(true) loops

Only groups whose targets all live inside the model's subtree travel — the scene's ambient animations stay behind. Pass animations: false to skip; clearInstances() disposes the clones with their instance.