SNOMED Body-Map Kit
Reusable, pluggable, real-time anatomical annotation surface keyed to SNOMED body sites.
Status (2026-05-30): core shipped + sandbox/Node-verified; Supabase realtime adapter + the
2D/dental mappers + full host extraction are staged (designed here). Lives in
web/packages/medical-kit/src/body-model-3d/ (@medical-kit/body-model-3d).
The 3D SNOMED body annotator is not one screen — it’s a kit: a shared marker store, a marker-source registry, a pluggable body-part-mapper contract, a Canvas viewer, a still-image poster facade, and a pluggable realtime transport. Any app surface (ClinicWorkspace, Chief Complaints + AI, Screening modal, OR Record + packing/surgical, …) ties into the same markers and drops/reads pins without prop-drilling.
1. What the kit exports
| Export | Purpose |
|---|---|
BodyModelViewer |
R3F Canvas: GLB body, drei `` markers (free placement via position/onAnnotatePoint), occlusion, focusPosition camera fly, onPosterCapture, demand frameloop. |
BodyModelPoster + MarkerOverlay |
Freeze-to-still facade: still image + JS-projected interactive DOM markers; boots WebGL only on drag. Fail-safe, flag-gated. |
useBodyAnnotation (Zustand) |
The shared marker store — markers, selectedId, add/update/remove/select/setAll/clear. The tie-in backbone. |
registerMarkerSource / getMarkerSource |
Source registry — a form declares itself so its markers get a label/colour and re-open its values on select. |
connectRealtime / RealtimeAdapter |
Pluggable transport — local marker changes publish to peers, remote snapshots flow into the store (echo-guarded). Supabase slots in here. |
registerBodyPartMapper / BodyPartMapper |
The mapper contract — 3D SNOMED today; 2D body diagram / dental odontogram / eye-map are future mappers satisfying the same contract. |
COORDINATE_SETS, resolveAnatomyCoding, getRegionCodings |
SNOMED/FMA/PCS region coding (deployment-aware). |
2. The pluggable mapper (BodyPartMapper)
A mapper is any anatomy surface. The host renders the active mapper; the store + sources are mapper-agnostic, so a marker dropped by a form pops up on whichever mapper is mounted.
interface BodyPartMapper {
id: string; // 'snomed-3d' | 'body-2d' | 'dental' | …
label: string;
codeSystem: string; // 'http://snomed.info/sct'
domain: string[]; // ['body'] | ['teeth'] | ['eye'] — routes a marker to the right mapper
render: (p: BodyMapperRenderProps) => ReactNode; // 3D canvas / 2D svg / chart
search?: (q: string) => { regionId; label; code? }[];
views?: string[]; // 'orbit' | 'front' | 'back' | 'left' | 'right' | 'quad'
}
The 3D SNOMED mapper wraps BodyModelViewer. A future dental mapper renders an odontogram from
the same store; mapperForDomain('teeth') routes a tooth marker to it. No 2D mapper pulls in
R3F — each provides its own renderer.
3. Integration contract — how the named surfaces tie in
Every surface uses the same two calls. No props, no provider.
// once, at module load:
registerMarkerSource({ id: 'or-record', label: 'OR record', color: '#e74c3c', glyph: '🧽' });
// Chief Complaints + AI — AI extracts complaint → drops a pin:
useBodyAnnotation.getState().add({
regionId: 'chest', nodeName: 'Chest', kind: 'complaint', type: 'warning', sev: 'moderate',
source: 'complaint', payload: { complaint: 'Sharp chest pain', onset: '2h' },
});
// OR Record modal (packing / surgical counts) — count incorrect → red pin pops up:
useBodyAnnotation.getState().add({
regionId: 'abdomen', kind: 'surgical', type: 'critical', sev: 'critical',
source: 'or-record', payload: { item: 'Raytec', in: 5, out: 4, status: 'INCORRECT' },
});
// Screening modal — a positive finding pin; ClinicWorkspace mounts the viewer + reads selection.
const selected = useBodyAnnotation((s) => s.markers.find((m) => m.id === s.selectedId));
| Surface | What it pushes | What it reads |
|---|---|---|
| ClinicWorkspace | hosts the mapper; arms tools | selectedId → opens the right modal for the pin’s source |
| Chief Complaints + AI | AI-extracted complaint pins (source: 'complaint') |
existing complaint pins to refine |
| Screening modal | positive findings as pins | findings by region |
| OR Record (packing/surgical) | packing + count pins (source: 'or-record') |
retained-item / incorrect-count pins |
Selecting a sourced marker re-opens that form’s values in the detail panel — verified live
(a Chief-Complaint drop surfaced LINKED FORM · Chief complaint · "Sharp chest pain…").
4. Realtime via Supabase (pop-up across clients)
connectRealtime(adapter) makes the store transport-agnostic. A Supabase adapter:
function supabaseBodyAnnotations(supabase, encounterId): RealtimeAdapter {
const ch = supabase.channel(`body:${encounterId}`);
return {
publish: (markers) => ch.send({ type: 'broadcast', event: 'markers', payload: markers }),
subscribe: (apply) => {
ch.on('broadcast', { event: 'markers' }, ({ payload }) => apply(payload)).subscribe();
// hydrate from the table on connect, then live:
supabase.from('body_annotations').select('*').eq('encounter_id', encounterId)
.then(({ data }) => data && apply(data.map(rowToMarker)));
return () => supabase.removeChannel(ch);
},
};
}
Read-model rule (web/CLAUDE.md): the frontend never writes a read-model table directly.
Two compliant paths:
- Collaborative layer (like the dynamic-sheet presence/edit-lock pattern): a dedicated
body_annotationstable with RLS scoping rows to the encounter + author, frontend writes allowed (it’s collaboration data, not the clinical write-truth), realtime broadcast for live pop-up. Promote a pin to a clinical record (Condition/Observation/Procedure) through the backend write path when signed. - Mirror-only: markers authored via backend APIs → projected to
body_annotations→ realtime → store (publishomitted; read-only mirror).
body_annotations sketch: id, encounter_id, region_id, kind, type, severity, note, position jsonb, source, payload jsonb, history jsonb, author_id, created_at. Echo-guarded in the store so
an applied remote snapshot never re-publishes.
5. Miniapp / embedding
The kit is import-only — a miniapp mounts `` (or the viewer) + the panels it
needs, calls connectRealtime(supabaseBodyAnnotations(...)) once, and registerMarkerSource for
its forms. The sandbox target BodyModelAnnotationStudio is the reference host (tool dock, dark
marker list, deep-values detail, pain scale/tracker, MSK + region-aware special tests, OR counts,
device LDA forms, search→zoom, single/split/quad views, linked-forms, timeline scrubber).
6. Verified vs staged
- Shipped + verified: store CRUD + source registry (Node); realtime publish/apply + echo guard (Node); mapper registry + domain routing (Node); free placement, camera-focus, poster facade (type-clean); the full studio incl. linked-form pop-up (live: 4→5 + payload), single/split/quad (4 canvases), all panels (DOM, no error boundary).
- Staged: the Supabase adapter +
body_annotationsmigration + RLS; the 2D/dental mappers; extracting the studio chrome into a reusable ``; wiring the four real app surfaces; pin↔card dashed leader line. Note: WebGL render + screenshots couldn’t be verified in the sandbox preview (1px pane / flaky GL) — verify the live 3D in a real browser.