# @leumas/body-viewer

The generic 3D body viewer — a React/r3f surface that renders ANY species' model manifest, with the layer system, the selection model, isolate/ghost/clip, camera presets and the closed visual-command...


The generic 3D body surface. It takes a loaded body and a model manifest and draws whatever they
describe — grep it for `human`, `heart`, `skeletal` or `bone` and you find none of them.

```jsx
const Viewer = lazy(() => import('@leumas/body-viewer'));   // three + fiber + drei: ALWAYS lazy

const store = createViewerStore({ species: 'human', variant: 'adult-female' });
<BodyViewer body={body} manifest={body.manifests[body.variant.modelManifest]} store={store} />
<LayerPanel body={body} store={store} />
```

## The two rules

**1. A `THREE.Mesh` is never application truth.** Selection, visibility, opacity and isolation are
sets of canonical structure ids. The scene is a function of them. That is what makes the tree, the
graph, the 3D and an agent's command interchangeable — all four read and write the same ids — and it
is why a variant switch cannot leave a highlight pointing at geometry that is no longer the structure
it names.

**2. The command list is closed.** `applyCommand` is a trust boundary: the command is generated by a
model, relayed through a tool result, and executed in a browser. Anything outside `VISUAL_ACTIONS` is
refused **by name**, no command in this package reads or writes anything but view state, and an id
that does not exist in the body is **reported** rather than silently dropped.

## Import it lazily

`three`, `@react-three/fiber` and `@react-three/drei` are peers, and both Leumas products' eager JS is
budgeted at ~300 KB. `./store` and `./commands` are three-free subpaths, so a panel that only reads
the selection — or an agent bridge that only applies a command — never touches WebGL.

## What is in it

| | |
|---|---|
| `store.js` | the canonical viewer state, as an external store (`useSyncExternalStore`) |
| `commands.js` | the closed visual-action executor + lesson-step → command mapping |
| `BodyViewer.jsx` | the r3f scene, selection, hover, clipping, the camera-nonce driver |
| `LayerPanel.jsx` | layers, colour mode, peel, ghost, cross-section, camera presets |
| `palette.js` | six colour schemes, keyed by slug so an unknown species is still legible |
| `geometry.js` | placements → drawable instances, the primitive vocabulary, the shared geometry cache, explode offsets |
| `batches.js` | **pure** — grouping into InstancedMeshes, slot assignment, the confidence fade. No `three`, no React |
| `InstancedBody.jsx` | the InstancedMeshes, the per-frame pick loop, the colour/alpha/matrix writes |

## It draws the WHOLE body, in about sixty draw calls

One React component and one `THREE.Mesh` per structure is fine for the ~100 a body plan places by
hand and ruinous for the ~6,500 a species slice contains. Every distinct shape is now two
InstancedMeshes — opaque and transparent — and an instance moves between them with a zero-scale
matrix rather than a scene-graph rebuild.

The slot-to-structure mapping lives in `batches.js`, away from the renderer, because getting it wrong
by one renders a complete, plausible body at sixty frames a second in which clicking the liver selects
the spleen. `test/instancing.test.js` shuffles the input and asserts every slot still resolves.

**Only an `authored` placement gets a rich primitive.** A derived one gets a generic proxy sized from
its category, because we know roughly where it is and nothing at all about its shape — and a
convincingly-shaped organ nobody has evidence for is the exact failure the schematic approach exists
to refuse. It is also the performance answer: the cache holds ~100 rich buffers plus a couple of dozen
proxies rather than 6,500 distinct meshes.

## Three traps it already handles

- **`visibleSystems: null` means ALL, not none.** The first "hide one layer" click resolves it against
  the full system list; treating null as empty turns one click into a blank viewer.
- **The camera moves on a NONCE, not on a value.** Reacting to `view` or `focusIds` directly means
  every unrelated re-render re-applies the last camera intent, and the user's orbit snaps back to the
  front whenever anything else updates.
- **Shared geometry must not be disposed by a component teardown.** Same rule as `@leumas/body-3d`:
  `disposeShapeCache()` is process-wide and is the only sanctioned way to free it.

Plus three more that arrived with instancing, all guarded by `check:body`:

- **Never `vertexColors` on an instanced material.** `setColorAt` already defines
  `USE_INSTANCING_COLOR`; adding `USE_COLOR` makes the shader multiply by a per-vertex attribute the
  geometry lacks and every instance renders black — with a correct palette and a correct buffer.
- **Null `boundingSphere` after every matrix write.** three computes an InstancedMesh's sphere lazily
  on the first raycast and never again, so a stale one culls the whole mesh and picking silently stops
  working for everything.
- **Inject per-instance alpha AFTER `#include <clipping_planes_fragment>`.** The cross-section discards
  there; patching earlier multiplies alpha into pixels about to be thrown away.

## Prove it

```sh
pnpm --filter @leumas/body-viewer test   # 13 tests, no browser, no WebGL
```

The store and the executor are plain JavaScript, and every viewer failure a user actually notices
lives in one of them.


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