1use std::collections::HashMap;
26use std::path::Path;
27
28use anyhow::{bail, Context, Result};
29use serde_json::Value;
30
31use oxihuman_core::animation_curve::{AnimCurve, InterpMode, Keyframe};
32use oxihuman_morph::params::ParamState;
33
34#[derive(Debug, Clone)]
37pub enum AnimSource {
38 Snapshots(Vec<ParamState>),
40 Curves(HashMap<String, AnimCurve>),
43}
44
45impl AnimSource {
46 pub fn fixed_frame_count(&self) -> Option<usize> {
48 match self {
49 AnimSource::Snapshots(snaps) => Some(snaps.len()),
50 AnimSource::Curves(_) => None,
51 }
52 }
53
54 pub fn suggested_frame_count(&self, fps: f32) -> usize {
57 match self {
58 AnimSource::Snapshots(snaps) => snaps.len().max(1),
59 AnimSource::Curves(curves) => {
60 let max_duration = curves
61 .values()
62 .map(|c| c.duration())
63 .fold(0.0_f32, f32::max);
64 let safe_fps = if fps > 0.0 { fps } else { 1.0 };
65 ((max_duration * safe_fps).round() as usize + 1).max(1)
66 }
67 }
68 }
69
70 pub fn params_at_frame(&self, i: usize, fps: f32, start_time: f32) -> ParamState {
77 match self {
78 AnimSource::Snapshots(snaps) => {
79 let idx = i.min(snaps.len().saturating_sub(1));
80 snaps.get(idx).cloned().unwrap_or_else(ParamState::default)
81 }
82 AnimSource::Curves(curves) => {
83 let safe_fps = if fps > 0.0 { fps } else { 1.0 };
84 let t = start_time + (i as f32) / safe_fps;
85 let mut params = ParamState::default();
86 for (name, curve) in curves {
87 apply_named_value(&mut params, name, curve.evaluate(t));
88 }
89 params
90 }
91 }
92 }
93}
94
95pub fn load_anim_source(path: &Path) -> Result<AnimSource> {
97 let src = std::fs::read_to_string(path)
98 .with_context(|| format!("reading anim JSON: {}", path.display()))?;
99 let value: Value = serde_json::from_str(&src)
100 .with_context(|| format!("parsing anim JSON: {}", path.display()))?;
101 parse_anim_value(&value)
102}
103
104fn apply_named_value(params: &mut ParamState, name: &str, value: f32) {
107 match name {
108 "height" => params.height = value,
109 "weight" => params.weight = value,
110 "muscle" => params.muscle = value,
111 "age" => params.age = value,
112 other => {
113 params.extra.insert(other.to_string(), value);
114 }
115 }
116}
117
118fn looks_like_keyframe(v: &Value) -> bool {
121 matches!(v, Value::Object(map) if map.contains_key("time") && map.contains_key("params"))
122}
123
124fn snapshot_from_map(map: &serde_json::Map<String, Value>) -> Result<ParamState> {
128 let mut params = ParamState::default();
129 for (key, value) in map {
130 let f = value
131 .as_f64()
132 .with_context(|| format!("param '{}' must be a number", key))? as f32;
133 apply_named_value(&mut params, key, f);
134 }
135 Ok(params)
136}
137
138fn parse_anim_value(value: &Value) -> Result<AnimSource> {
139 match value {
140 Value::Array(entries) => {
141 if entries.is_empty() {
142 bail!("anim JSON array must not be empty");
143 }
144 if entries.iter().all(looks_like_keyframe) {
145 Ok(AnimSource::Curves(build_curves(
146 entries,
147 InterpMode::Linear,
148 )?))
149 } else {
150 let snapshots = entries
151 .iter()
152 .map(|entry| {
153 entry
154 .as_object()
155 .context("each anim JSON array entry must be an object")
156 .and_then(snapshot_from_map)
157 })
158 .collect::<Result<Vec<_>>>()?;
159 Ok(AnimSource::Snapshots(snapshots))
160 }
161 }
162 Value::Object(map) => {
163 let keyframes = map
164 .get("keyframes")
165 .and_then(Value::as_array)
166 .context("anim JSON object must contain a \"keyframes\" array")?;
167 if keyframes.is_empty() {
168 bail!("\"keyframes\" array must not be empty");
169 }
170 let interp = match map.get("interp").and_then(Value::as_str) {
171 None | Some("linear") => InterpMode::Linear,
172 Some("step") => InterpMode::Step,
173 Some("cubic") => InterpMode::Cubic,
174 Some(other) => bail!("unknown interp mode '{}': use linear|step|cubic", other),
175 };
176 Ok(AnimSource::Curves(build_curves(keyframes, interp)?))
177 }
178 _ => bail!("anim JSON must be an array or an object with a \"keyframes\" array"),
179 }
180}
181
182fn build_curves(entries: &[Value], interp: InterpMode) -> Result<HashMap<String, AnimCurve>> {
185 let mut curves: HashMap<String, AnimCurve> = HashMap::new();
186 for entry in entries {
187 let obj = entry
188 .as_object()
189 .context("each keyframe must be a JSON object")?;
190 let time = obj
191 .get("time")
192 .and_then(Value::as_f64)
193 .context("keyframe missing numeric \"time\"")? as f32;
194 let params = obj
195 .get("params")
196 .and_then(Value::as_object)
197 .context("keyframe missing \"params\" object")?;
198 let tan_in = obj.get("tan_in").and_then(Value::as_object);
199 let tan_out = obj.get("tan_out").and_then(Value::as_object);
200
201 for (name, raw_value) in params {
202 let value = raw_value
203 .as_f64()
204 .with_context(|| format!("keyframe param '{}' must be a number", name))?
205 as f32;
206 let t_in = tan_in
207 .and_then(|m| m.get(name))
208 .and_then(Value::as_f64)
209 .unwrap_or(0.0) as f32;
210 let t_out = tan_out
211 .and_then(|m| m.get(name))
212 .and_then(Value::as_f64)
213 .unwrap_or(0.0) as f32;
214 curves
215 .entry(name.clone())
216 .or_insert_with(|| AnimCurve::new(interp))
217 .insert(Keyframe::with_tangents(time, value, t_in, t_out));
218 }
219 }
220 Ok(curves)
221}
222
223#[cfg(test)]
224mod tests {
225 use super::*;
226
227 #[test]
229 fn dense_snapshots_parse() {
230 let v: Value = serde_json::from_str(
231 r#"[{"height":0.1,"weight":0.5,"muscle":0.5,"age":0.5},
232 {"height":0.9,"weight":0.5,"muscle":0.5,"age":0.5}]"#,
233 )
234 .expect("valid json");
235 let src = parse_anim_value(&v).expect("should parse");
236 assert_eq!(src.fixed_frame_count(), Some(2));
237 let p0 = src.params_at_frame(0, 24.0, 0.0);
238 let p1 = src.params_at_frame(1, 24.0, 0.0);
239 assert!((p0.height - 0.1).abs() < 1e-6);
240 assert!((p1.height - 0.9).abs() < 1e-6);
241 }
242
243 #[test]
245 fn dense_snapshots_partial_fields_default_and_extra() {
246 let v: Value =
247 serde_json::from_str(r#"[{"height":0.2,"custom":0.75}]"#).expect("valid json");
248 let src = parse_anim_value(&v).expect("should parse");
249 let p = src.params_at_frame(0, 24.0, 0.0);
250 assert!((p.height - 0.2).abs() < 1e-6);
251 assert!((p.weight - ParamState::default().weight).abs() < 1e-6);
252 assert_eq!(p.extra.get("custom").copied(), Some(0.75));
253 }
254
255 #[test]
257 fn dense_snapshots_index_clamps() {
258 let v: Value = serde_json::from_str(r#"[{"height":0.3}]"#).expect("valid json");
259 let src = parse_anim_value(&v).expect("should parse");
260 let p = src.params_at_frame(50, 24.0, 0.0);
261 assert!((p.height - 0.3).abs() < 1e-6);
262 }
263
264 #[test]
266 fn keyframe_object_form_samples_linearly() {
267 let v: Value = serde_json::from_str(
268 r#"{"interp":"linear","keyframes":[
269 {"time":0.0,"params":{"height":0.0}},
270 {"time":1.0,"params":{"height":1.0}}]}"#,
271 )
272 .expect("valid json");
273 let src = parse_anim_value(&v).expect("should parse");
274 assert_eq!(src.fixed_frame_count(), None);
275 let p_mid = src.params_at_frame(12, 24.0, 0.0); assert!((p_mid.height - 0.5).abs() < 1e-3);
277 }
278
279 #[test]
281 fn keyframe_step_holds_value() {
282 let v: Value = serde_json::from_str(
283 r#"{"interp":"step","keyframes":[
284 {"time":0.0,"params":{"weight":0.2}},
285 {"time":2.0,"params":{"weight":0.8}}]}"#,
286 )
287 .expect("valid json");
288 let src = parse_anim_value(&v).expect("should parse");
289 let p = src.params_at_frame(24, 24.0, 0.0); assert!((p.weight - 0.2).abs() < 1e-6);
291 }
292
293 #[test]
295 fn bare_keyframe_array_accepted() {
296 let v: Value = serde_json::from_str(
297 r#"[{"time":0.0,"params":{"age":0.1}},{"time":2.0,"params":{"age":0.9}}]"#,
298 )
299 .expect("valid json");
300 let src = parse_anim_value(&v).expect("should parse");
301 assert_eq!(src.fixed_frame_count(), None);
302 }
303
304 #[test]
306 fn suggested_frame_count_matches_duration() {
307 let v: Value = serde_json::from_str(
308 r#"{"keyframes":[{"time":0.0,"params":{"height":0.0}},
309 {"time":2.0,"params":{"height":1.0}}]}"#,
310 )
311 .expect("valid json");
312 let src = parse_anim_value(&v).expect("should parse");
313 assert_eq!(src.suggested_frame_count(10.0), 21);
315 }
316
317 #[test]
319 fn empty_array_rejected() {
320 let v: Value = serde_json::from_str("[]").expect("valid json");
321 assert!(parse_anim_value(&v).is_err());
322 }
323
324 #[test]
326 fn object_without_keyframes_rejected() {
327 let v: Value = serde_json::from_str(r#"{"foo":1}"#).expect("valid json");
328 assert!(parse_anim_value(&v).is_err());
329 }
330
331 #[test]
333 fn unknown_interp_rejected() {
334 let v: Value = serde_json::from_str(
335 r#"{"interp":"bogus","keyframes":[{"time":0.0,"params":{"height":0.0}}]}"#,
336 )
337 .expect("valid json");
338 assert!(parse_anim_value(&v).is_err());
339 }
340
341 #[test]
343 fn non_numeric_snapshot_value_rejected() {
344 let v: Value = serde_json::from_str(r#"[{"height":"tall"}]"#).expect("valid json");
345 assert!(parse_anim_value(&v).is_err());
346 }
347
348 #[test]
350 fn top_level_scalar_rejected() {
351 let v: Value = serde_json::from_str("42").expect("valid json");
352 assert!(parse_anim_value(&v).is_err());
353 }
354
355 #[test]
357 fn cubic_tangents_endpoints() {
358 let v: Value = serde_json::from_str(
359 r#"{"interp":"cubic","keyframes":[
360 {"time":0.0,"params":{"muscle":0.0},"tan_out":{"muscle":0.0}},
361 {"time":1.0,"params":{"muscle":1.0},"tan_in":{"muscle":0.0}}]}"#,
362 )
363 .expect("valid json");
364 let src = parse_anim_value(&v).expect("should parse");
365 let p0 = src.params_at_frame(0, 1.0, 0.0);
366 let p1 = src.params_at_frame(1, 1.0, 0.0);
367 assert!((p0.muscle - 0.0).abs() < 1e-3);
368 assert!((p1.muscle - 1.0).abs() < 1e-3);
369 }
370}