# @leumas/body-3d

Procedurally composed 3D bodies: the entity-kit silhouette vocabulary, blueprint part lists, and the resolver that lets a game draw either without owning a single GLB.


Procedurally composed 3D bodies and props. `three` and data — no React, no DOM, no router.

```js
import { bodyParts, partsToObject3D, blueprintParts } from '@leumas/body-3d';
import { createProceduralResolver } from '@leumas/body-3d/resolver';

const { root, limbs } = partsToObject3D({ body: 'bot', color: '#7fd1c0' });
scene.add(root);            // limbs: [{role: 'leg-l', hinge}, …] — what an animator drives
```

## Why it exists

The silhouette vocabulary lived in `@leumas/features/nav-views/world`, which is a React package:
importing it drags React, the router and a stylesheet. `@leumas/game-3d` needs the same bodies and
has exactly one runtime dependency. So the geometry moved down to a package both can import;
`features/nav-views/world/bodies3d.js` is now a one-line re-export shim, and every existing call site
kept working.

## What it holds

| Export | What it is |
|---|---|
| `bodyParts(id)` · `BODY3D_IDS` | the entity-kit silhouette vocabulary, as shared cached geometry |
| `bodyMaterials(color, emissive)` | the four material roles — `body`, `light`, `dark`, `glow` |
| `partsToObject3D({body \| parts, color, scale})` | parts list → `Object3D`, **with limb hinges** |
| `blueprintParts(blueprint, seed)` | a forge catalogue record → a stand-in prop of the right size |
| `createProceduralResolver({manifest, lookup})` | the `resolveUrl` / `loadModel` pair for the asset registry |

## The two things to know

**Geometry is shared and you must not dispose it.** Every camera in a room points at one tripod. A
teardown that walks the tree calling `geometry.dispose()` blanks every other instance of that
species, with no error, and the next mount renders nothing. Assembled meshes are tagged
`userData.sharedGeometry` so a teardown can tell what it owns; `disposeBodyCache()` and
`disposeBlueprintCache()` are the only things allowed to free them, and both are process-wide.

**The resolver is a fallback, and the order matters.** A source listed in the manifest resolves to
its real URL and the registry's own GLTF loader fetches it, DRACO and KTX2 included. Only a source
with no file becomes `proc:<source>` and is composed here. That is what makes a generated-asset pass
an upgrade — same source strings, same call, better mesh when a file lands — rather than a migration
out of something that replaced it.

## Limbs

A part may name a role (`leg-l`, `leg-r`, `arm-l`, `arm-r`, `head`) and a `pivot`, the height its
joint sits at. `partsToObject3D` hangs those parts off a hinge group at the pivot and offsets the
mesh back to where it rests, so rotating the hinge swings the limb about the joint. Rotating the mesh
instead swings it about its own centre, which reads as a limb being stirred rather than a body
walking. Several parts may share a role and travel as one limb.

Bodies with no roles return `limbs: []`, and an animation driver over them is a no-op rather than an
error — most silhouettes (a crate, a rack, a camera) have no joints.

`@leumas/game-3d/anim` drives these hinges from replicated velocity and grounded state.


---
Source: shared/packages/body-3d/README.md
Canonical: https://docs.leumas.tech/p/packages/body-3d
