qualia_core_db/render/anatomy_pack.rs
1//! Shared metadata schema for a `.hmc` **anatomy asset pack** — the per-organ
2//! `meta` carried in each bundle entry.
3//!
4//! A packed anatomy body is a `.hmc` bundle (see [`crate::bundle`]) whose
5//! entries are the per-organ sealed `.10d` meshes. Each entry's opaque `meta`
6//! holds one CBOR-encoded [`AnatomyOrganMeta`]: which body **system** the organ
7//! belongs to, an **approximate** anatomical position for assembling the whole
8//! body, and a **neutral default colour** (the pack ships no personal data — a
9//! person's real burden colouring, from `AnatomyViewReport::paint_organs`,
10//! overrides this at runtime when their records are loaded).
11//!
12//! This lives in `qualia-core-db` so the producer (`qualia-client-core`, which
13//! discovers/compiles organs) and the consumer (the browser `QualiaPortal`
14//! renderer + the native read-through loader) share **one** typed schema.
15
16use serde::{Deserialize, Serialize};
17
18/// Per-organ render metadata carried in a `.hmc` anatomy-pack entry's `meta`.
19#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
20pub struct AnatomyOrganMeta {
21 /// The organ's **primary** body-system id (its default colour/placement),
22 /// e.g. `"digestive"`. An organ is a building block shared across systems;
23 /// see [`AnatomyOrganMeta::systems`] for the full set.
24 pub system: String,
25 /// Human-readable name of this part (e.g. `"diaphragm"`, `"liver"`), for a selectable parts list.
26 /// Empty/absent in older packs → the consumer falls back to the entry key (`#[serde(default)]`).
27 #[serde(default)]
28 pub label: String,
29 /// **All** body systems the organ participates in, primary first (the pancreas
30 /// is `["digestive", "endocrine", "exocrine"]`). Lets the renderer colour by the
31 /// primary system *or* blend across memberships, and lets a person's condition on
32 /// any member system light the organ. Empty/absent in older packs → treat as
33 /// `[system]` (back-compatible; `#[serde(default)]`).
34 #[serde(default)]
35 pub systems: Vec<String>,
36 /// Approximate anatomical position offset in normalised body space
37 /// `[x, y, z]` (0..1; x=right, y=up, z=front). Approximate placement — a
38 /// future pass can substitute real CCF transforms.
39 pub position: [f32; 3],
40 /// Neutral default linear RGBA for the organ (overridden by the person's
41 /// σ-derived burden colour at runtime when their data is present).
42 pub rgba: [f32; 4],
43}
44
45impl AnatomyOrganMeta {
46 /// Encode to CBOR for storage in a bundle entry's `meta`.
47 pub fn to_cbor(&self) -> Vec<u8> {
48 let mut out = Vec::new();
49 // Infallible for this small POD struct into a Vec writer.
50 ciborium::into_writer(self, &mut out).expect("cbor encode AnatomyOrganMeta");
51 out
52 }
53
54 /// Decode from a bundle entry's `meta` bytes.
55 pub fn from_cbor(bytes: &[u8]) -> Option<Self> {
56 ciborium::from_reader(bytes).ok()
57 }
58}
59
60#[cfg(test)]
61mod tests {
62 use super::*;
63
64 #[test]
65 fn cbor_round_trips() {
66 let m = AnatomyOrganMeta {
67 system: "digestive".to_string(),
68 label: "pancreas".to_string(),
69 systems: vec![
70 "digestive".to_string(),
71 "endocrine".to_string(),
72 "exocrine".to_string(),
73 ],
74 position: [0.5, 0.6, 0.55],
75 rgba: [0.8, 0.3, 0.3, 1.0],
76 };
77 let bytes = m.to_cbor();
78 let back = AnatomyOrganMeta::from_cbor(&bytes).unwrap();
79 assert_eq!(back, m);
80 assert_eq!(back.systems.len(), 3, "all memberships round-trip");
81 assert!(AnatomyOrganMeta::from_cbor(b"not cbor").is_none());
82 }
83
84 /// A pack written before `systems` existed must still decode (the field defaults to empty), so an
85 /// older `.hmc` on disk keeps working. The consumer treats an empty `systems` as `[system]`.
86 #[test]
87 fn old_meta_without_systems_field_still_decodes() {
88 #[derive(serde::Serialize)]
89 struct OldMeta {
90 system: String,
91 position: [f32; 3],
92 rgba: [f32; 4],
93 }
94 let mut bytes = Vec::new();
95 ciborium::into_writer(
96 &OldMeta {
97 system: "respiratory".to_string(),
98 position: [0.4, 0.6, 0.5],
99 rgba: [0.5, 0.7, 0.9, 1.0],
100 },
101 &mut bytes,
102 )
103 .unwrap();
104 let m = AnatomyOrganMeta::from_cbor(&bytes).expect("old meta decodes");
105 assert_eq!(m.system, "respiratory");
106 assert!(m.systems.is_empty(), "absent field defaults to empty");
107 }
108}