@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...
@leumas/body-viewer
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.
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: nullmeans 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
vieworfocusIdsdirectly 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
vertexColorson an instanced material.setColorAtalready 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
boundingSphereafter 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
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.
What it exports
72 exports: 22 components, 4 hooks, 46 helpers. Import from @leumas/body-viewer.
BodyViewerBundleLoaderCATEGORY_COLORSCOLOR_MODESCOLOR_MODESDEFAULT_HIDDEN_SLUGSDEFAULT_HIDDEN_SLUGSDETAIL_LEVELSDETAIL_LEVELSEMPTY_SETInstancedBodyLayerPanelLayerPanelMATERIAL_IDSMOUNT_CHUNKMOUNT_FIRST_CHUNKREALISTIC_COLORSREVEAL_MODESREVEAL_MODESSYSTEM_COLORSVIEWSVIEWSuseViewerSelectoruseViewerSelectoruseViewerStateuseViewerStateapplyCommandapplyCommandapplyCommandsapplyCommandsbundleGeometrybundleGeometrybundleReportbundleReportbundleRevisionbundleRevisionbundlesInbundlesIncolorForcommandsForStepcommandsForStepconfidenceFadecreateViewerStorecreateViewerStoredisposeBundlesdisposeBundlesdisposeShapeCacheexplodedPositionfailBundlefailBundlegeometriesFromgeometriesFromhasBundleMeshhasBundleMeshinstancesForisVisibleisVisiblematerialFormaterialPropsmountOrderopacityForopacityForplanBatchesquantizeRadiusregisterBundleregisterBundleresolveInstanceshapeExtentshapeGeometryshapeKeysimplifysystemColor