1use webizen_render::scene_contract::{
18 EpistemicState, RenderScene, SceneCamera, SceneNode, ScenePoint,
19};
20
21use super::anatomy_view::{AnatomyViewReport, SystemPercept};
22
23fn system_position(system_id: &str) -> ScenePoint {
27 let p = match system_id {
28 "nervous" => (0.50, 0.12, 0.50),
30 "sensory" => (0.50, 0.10, 0.55),
31 "endocrine" => (0.50, 0.20, 0.50),
32 "respiratory" => (0.50, 0.28, 0.50),
34 "circulatory" => (0.50, 0.30, 0.45),
35 "immune" => (0.45, 0.32, 0.50),
36 "digestive" => (0.50, 0.45, 0.50),
38 "urinary" => (0.50, 0.50, 0.55),
39 "reticuloendothelial" => (0.55, 0.42, 0.50),
40 "hematopoietic" => (0.50, 0.50, 0.40),
41 "reproductive" => (0.50, 0.58, 0.55),
43 "musculoskeletal" => (0.50, 0.50, 0.50),
45 "integumentary" => (0.50, 0.50, 0.60),
46 "thermoregulatory" => (0.50, 0.50, 0.62),
47 "ens" => (0.50, 0.45, 0.52), "glymphatic" => (0.50, 0.12, 0.52), "ecs" => (0.50, 0.50, 0.50), _ => (0.50, 0.50, 0.50),
52 };
53 ScenePoint {
54 x: p.0,
55 y: p.1,
56 z: p.2,
57 }
58}
59
60fn is_overlay(system_id: &str) -> bool {
62 matches!(system_id, "ens" | "glymphatic" | "ecs")
63}
64
65fn base_radius(system_id: &str) -> f64 {
68 if is_overlay(system_id) {
69 28.0
70 } else {
71 10.0
72 }
73}
74
75fn rgba_to_css(rgba: [f32; 4]) -> String {
77 let r = (rgba[0] * 255.0).round().clamp(0.0, 255.0) as u8;
78 let g = (rgba[1] * 255.0).round().clamp(0.0, 255.0) as u8;
79 let b = (rgba[2] * 255.0).round().clamp(0.0, 255.0) as u8;
80 format!("#{r:02x}{g:02x}{b:02x}")
81}
82
83pub fn body_scene(report: &AnatomyViewReport, azimuth_deg: f64, elevation_deg: f64) -> RenderScene {
87 body_scene_with_fit(
88 report,
89 azimuth_deg,
90 elevation_deg,
91 &wellfare_core::anatomy::BodyFit::identity(),
92 )
93}
94
95pub fn body_scene_with_fit(
97 report: &AnatomyViewReport,
98 azimuth_deg: f64,
99 elevation_deg: f64,
100 fit: &wellfare_core::anatomy::BodyFit,
101) -> RenderScene {
102 let percepts = report.system_percepts();
103 let mut scene = RenderScene {
104 background: "#0a0f14".to_string(),
105 camera: orbit_camera(azimuth_deg, elevation_deg),
106 epistemic_filter: EpistemicState::Collapsed,
107 ..Default::default()
108 };
109
110 let all_systems = [
113 "nervous",
114 "sensory",
115 "endocrine",
116 "respiratory",
117 "circulatory",
118 "immune",
119 "digestive",
120 "urinary",
121 "reticuloendothelial",
122 "hematopoietic",
123 "reproductive",
124 "musculoskeletal",
125 "integumentary",
126 "thermoregulatory",
127 "ens",
128 "glymphatic",
129 "ecs",
130 ];
131
132 for &sys in all_systems.iter() {
133 let percept = percepts
134 .iter()
135 .find(|p| p.system_id == sys)
136 .cloned()
137 .unwrap_or_else(|| SystemPercept {
138 system_id: sys.to_string(),
139 level: wellfare_core::anatomy::WellbeingLevel::Settled,
140 sigma: wellfare_core::anatomy::burden_to_sigma(0),
141 rgba: [0.29, 0.62, 0.36, 1.0], frequency_hz: 0.0,
143 });
144 let pos = fit_silhouette_point(system_position(sys), fit);
145 let overlay = is_overlay(sys);
146 let burden_scale = match percept.level {
148 wellfare_core::anatomy::WellbeingLevel::UnderStrain => 2.2,
149 wellfare_core::anatomy::WellbeingLevel::WorthWatching => 1.5,
150 wellfare_core::anatomy::WellbeingLevel::Settled => 1.0,
151 };
152 let radius = base_radius(sys) * burden_scale;
153 let alpha = if overlay { 0.35 } else { 0.92 };
154 scene.add_node(SceneNode {
155 id: sys.to_string(),
156 position: pos,
157 color: rgba_to_css(percept.rgba),
158 radius,
159 alpha,
160 is_inferencing: percept.level != wellfare_core::anatomy::WellbeingLevel::Settled,
161 pulse_rate: if percept.level == wellfare_core::anatomy::WellbeingLevel::UnderStrain {
162 1.2
163 } else {
164 0.0
165 },
166 tensor: Default::default(),
167 epistemic_state: EpistemicState::Collapsed,
168 version: 0.0,
169 entity_id: 0,
170 affordance_bits: 0,
171 });
172 }
173
174 scene
175}
176
177fn fit_silhouette_point(
178 p: ScenePoint,
179 fit: &wellfare_core::anatomy::BodyFit,
180) -> ScenePoint {
181 let y_up = 1.0 - p.y as f32;
183 let y_seg = if y_up < fit.pelvis_y_norm {
184 fit.leg_scale_y
185 } else {
186 fit.torso_scale_y
187 };
188 let y_from_feet = (1.0 - p.y) * (y_seg as f64) * (fit.stature_scale as f64);
189 let cx = 0.50;
190 ScenePoint {
191 x: cx + (p.x - cx) * (fit.arm_span_scale_x as f64) * (fit.shoulder_scale_x as f64),
192 y: (1.0 - y_from_feet).clamp(0.02, 0.98),
193 z: p.z,
194 }
195}
196
197fn orbit_camera(azimuth_deg: f64, elevation_deg: f64) -> SceneCamera {
200 let az = azimuth_deg.to_radians();
201 let el = elevation_deg.clamp(-89.0, 89.0).to_radians();
202 let radius = 1.8;
203 let x = 0.5 + radius * el.cos() * az.sin();
204 let y = 0.5 + radius * el.sin();
205 let z = radius * el.cos() * az.cos();
206 SceneCamera {
207 position: [x, y, z],
208 target: [0.5, 0.5, 0.0],
209 fov: 50.0,
210 }
211}
212
213#[cfg(test)]
214mod tests {
215 use super::*;
216 use wellfare_core::anatomy::{PhysiologicalState, RecordRef};
217
218 #[test]
219 fn body_scene_has_a_node_per_system_and_orbits() {
220 let refs = vec![RecordRef::new("r:htn", "condition", "Hypertension")];
221 let report = super::super::anatomy_view::build_report(
222 refs,
223 wellfare_core::anatomy::Lens::Person,
224 2,
225 PhysiologicalState::Baseline,
226 );
227 let scene = body_scene(&report, 0.0, 0.0);
228 assert_eq!(scene.nodes.len(), 17, "every body system is rendered");
230 let circ = scene.nodes.iter().find(|n| n.id == "circulatory").unwrap();
232 assert!(
233 circ.radius > 10.0,
234 "burdened circulatory is enlarged: {}",
235 circ.radius
236 );
237 assert!(circ.is_inferencing, "burdened system is inferencing");
238 assert!(circ.pulse_rate > 0.0, "under-strain pulses");
239 let resp = scene.nodes.iter().find(|n| n.id == "respiratory").unwrap();
241 assert!(!resp.is_inferencing, "settled system is not inferencing");
242 assert_eq!(resp.pulse_rate, 0.0, "settled does not pulse");
243 let ens = scene.nodes.iter().find(|n| n.id == "ens").unwrap();
245 assert!(ens.alpha < 0.5, "overlay is translucent: {}", ens.alpha);
246 }
247
248 #[test]
249 fn orbit_camera_rotates_with_azimuth() {
250 let front = orbit_camera(0.0, 0.0);
251 let side = orbit_camera(90.0, 0.0);
252 assert!(
254 front.position[2] > side.position[2],
255 "front view has more z"
256 );
257 assert!(
258 (side.position[0] - 0.5).abs() > 0.01,
259 "side view shifts x off-centre"
260 );
261 let up = orbit_camera(0.0, 45.0);
263 assert!(
264 up.position[1] > front.position[1],
265 "elevation raises the camera"
266 );
267 }
268
269 #[test]
270 fn rgba_to_css_round_trips_primary_channels() {
271 assert_eq!(rgba_to_css([1.0, 0.0, 0.0, 1.0]), "#ff0000");
272 assert_eq!(rgba_to_css([0.0, 1.0, 0.0, 1.0]), "#00ff00");
273 assert_eq!(rgba_to_css([0.0, 0.0, 1.0, 1.0]), "#0000ff");
274 assert_eq!(rgba_to_css([2.0, -1.0, 0.5, 1.0]), "#ff0080");
276 }
277}