oxihuman_morph/
session.rs1use anyhow::{Context, Result};
12use serde::{Deserialize, Serialize};
13use std::collections::HashMap;
14use std::path::{Path, PathBuf};
15
16use crate::params::ParamState;
17
18#[derive(Debug, Clone, Serialize, Deserialize)]
20pub struct MorphSession {
21 pub version: String,
23 pub params: SessionParams,
25 pub targets_dir: Option<PathBuf>,
27 pub loaded_target_names: Vec<String>,
29 pub label: Option<String>,
31}
32
33#[derive(Debug, Clone, Serialize, Deserialize)]
35pub struct SessionParams {
36 pub height: f32,
37 pub weight: f32,
38 pub muscle: f32,
39 pub age: f32,
40 #[serde(default)]
41 pub extra: HashMap<String, f32>,
42}
43
44impl From<&ParamState> for SessionParams {
45 fn from(p: &ParamState) -> Self {
46 SessionParams {
47 height: p.height,
48 weight: p.weight,
49 muscle: p.muscle,
50 age: p.age,
51 extra: p.extra.clone(),
52 }
53 }
54}
55
56impl From<SessionParams> for ParamState {
57 fn from(s: SessionParams) -> Self {
58 ParamState {
59 height: s.height,
60 weight: s.weight,
61 muscle: s.muscle,
62 age: s.age,
63 extra: s.extra,
64 }
65 }
66}
67
68impl MorphSession {
69 pub fn new(params: &ParamState) -> Self {
71 MorphSession {
72 version: "0.1.0".to_string(),
73 params: SessionParams::from(params),
74 targets_dir: None,
75 loaded_target_names: Vec::new(),
76 label: None,
77 }
78 }
79
80 pub fn with_label(mut self, label: &str) -> Self {
82 self.label = Some(label.to_string());
83 self
84 }
85
86 pub fn with_targets_dir(mut self, dir: impl Into<PathBuf>) -> Self {
88 self.targets_dir = Some(dir.into());
89 self
90 }
91
92 pub fn add_target_name(&mut self, name: &str) {
94 if !self.loaded_target_names.contains(&name.to_string()) {
95 self.loaded_target_names.push(name.to_string());
96 }
97 }
98
99 pub fn to_param_state(&self) -> ParamState {
101 ParamState {
102 height: self.params.height,
103 weight: self.params.weight,
104 muscle: self.params.muscle,
105 age: self.params.age,
106 extra: self.params.extra.clone(),
107 }
108 }
109
110 pub fn to_json(&self) -> Result<String> {
112 Ok(serde_json::to_string_pretty(self)?)
113 }
114
115 pub fn from_json(s: &str) -> Result<Self> {
117 Ok(serde_json::from_str(s)?)
118 }
119
120 pub fn save(&self, path: &Path) -> Result<()> {
122 let json = self.to_json()?;
123 std::fs::write(path, json)
124 .with_context(|| format!("saving session to {}", path.display()))?;
125 Ok(())
126 }
127
128 pub fn load(path: &Path) -> Result<Self> {
130 let json = std::fs::read_to_string(path)
131 .with_context(|| format!("reading session from {}", path.display()))?;
132 Self::from_json(&json)
133 }
134}
135
136#[cfg(test)]
137mod tests {
138 use super::*;
139 use crate::params::ParamState;
140
141 fn sample_session() -> MorphSession {
142 let mut session = MorphSession::new(&ParamState::new(0.7, 0.3, 0.8, 0.2));
143 session.add_target_name("height");
144 session.add_target_name("muscle");
145 session.loaded_target_names.push("weight".to_string());
146 session
147 }
148
149 #[test]
150 fn session_round_trip_json() {
151 let session = sample_session();
152 let json = session.to_json().expect("should succeed");
153 let restored = MorphSession::from_json(&json).expect("should succeed");
154 assert!((restored.params.height - 0.7).abs() < 1e-5);
155 assert!((restored.params.muscle - 0.8).abs() < 1e-5);
156 assert_eq!(restored.loaded_target_names.len(), 3);
157 }
158
159 #[test]
160 fn session_to_param_state() {
161 let session = sample_session();
162 let p = session.to_param_state();
163 assert!((p.height - 0.7).abs() < 1e-5);
164 assert!((p.age - 0.2).abs() < 1e-5);
165 }
166
167 #[test]
168 fn session_save_load_file() {
169 let session = sample_session()
170 .with_label("test session")
171 .with_targets_dir("/tmp/targets");
172 let path = std::path::PathBuf::from("/tmp/test_oxihuman_session.json");
173 session.save(&path).expect("should succeed");
174 let loaded = MorphSession::load(&path).expect("should succeed");
175 assert_eq!(loaded.label, Some("test session".to_string()));
176 assert!((loaded.params.weight - 0.3).abs() < 1e-5);
177 std::fs::remove_file(&path).ok();
178 }
179
180 #[test]
181 fn add_target_name_deduplicates() {
182 let mut session = MorphSession::new(&ParamState::default());
183 session.add_target_name("height");
184 session.add_target_name("height"); assert_eq!(session.loaded_target_names.len(), 1);
186 }
187
188 #[test]
189 fn session_with_extra_params() {
190 let mut p = ParamState::default();
191 p.extra.insert("expression".to_string(), 0.4);
192 let session = MorphSession::new(&p);
193 let json = session.to_json().expect("should succeed");
194 let restored = MorphSession::from_json(&json).expect("should succeed");
195 assert_eq!(restored.params.extra.get("expression").copied(), Some(0.4));
196 }
197
198 #[test]
199 fn from_param_state_conversion() {
200 let p = ParamState::new(0.1, 0.2, 0.3, 0.4);
201 let sp = SessionParams::from(&p);
202 let p2: ParamState = sp.into();
203 assert!((p2.height - 0.1).abs() < 1e-5);
204 assert!((p2.age - 0.4).abs() < 1e-5);
205 }
206}