1use elara_core::StateTime;
7
8use crate::{FaceState, PoseState, SceneState, VisualState, VisualStateId};
9
10#[derive(Debug, Clone)]
12pub struct Keyframe {
13 pub id: VisualStateId,
15
16 pub timestamp: StateTime,
18
19 pub state: VisualState,
21
22 pub interval_ms: u32,
24
25 pub sequence: u64,
27}
28
29impl Keyframe {
30 pub fn new(state: VisualState, interval_ms: u32) -> Self {
32 Self {
33 id: state.id,
34 timestamp: state.timestamp,
35 sequence: state.sequence,
36 state,
37 interval_ms,
38 }
39 }
40
41 pub fn estimated_size(&self) -> usize {
43 let mut size = 32; if self.state.face.is_some() {
46 size += 128; }
48 if self.state.pose.is_some() {
49 size += 256; }
51 if self.state.scene.is_some() {
52 size += 64; }
54
55 size
56 }
57}
58
59#[derive(Debug, Clone, Copy, PartialEq, Eq)]
61pub enum DeltaType {
62 None,
64 Face,
66 Pose,
68 Scene,
70 Multiple,
72}
73
74#[derive(Debug, Clone)]
76pub struct Delta {
77 pub id: VisualStateId,
79
80 pub keyframe_ref: VisualStateId,
82
83 pub prev_delta_ref: Option<VisualStateId>,
85
86 pub timestamp: StateTime,
88
89 pub delta_type: DeltaType,
91
92 pub face_delta: Option<FaceDelta>,
94
95 pub pose_delta: Option<PoseDelta>,
97
98 pub scene_delta: Option<SceneDelta>,
100
101 pub sequence: u64,
103}
104
105impl Delta {
106 pub fn from_states(
108 prev: &VisualState,
109 curr: &VisualState,
110 keyframe_ref: VisualStateId,
111 ) -> Self {
112 let face_delta = match (&prev.face, &curr.face) {
113 (Some(prev_face), Some(curr_face)) => FaceDelta::compute(prev_face, curr_face),
114 (None, Some(curr_face)) => Some(FaceDelta::full(curr_face.clone())),
115 (Some(_), None) => Some(FaceDelta::removed()),
116 (None, None) => None,
117 };
118
119 let pose_delta = match (&prev.pose, &curr.pose) {
120 (Some(prev_pose), Some(curr_pose)) => PoseDelta::compute(prev_pose, curr_pose),
121 (None, Some(curr_pose)) => Some(PoseDelta::full(curr_pose.clone())),
122 (Some(_), None) => Some(PoseDelta::removed()),
123 (None, None) => None,
124 };
125
126 let scene_delta = match (&prev.scene, &curr.scene) {
127 (Some(prev_scene), Some(curr_scene)) => SceneDelta::compute(prev_scene, curr_scene),
128 (None, Some(curr_scene)) => Some(SceneDelta::full(curr_scene.clone())),
129 (Some(_), None) => Some(SceneDelta::removed()),
130 (None, None) => None,
131 };
132
133 let delta_type = match (
134 face_delta.is_some(),
135 pose_delta.is_some(),
136 scene_delta.is_some(),
137 ) {
138 (false, false, false) => DeltaType::None,
139 (true, false, false) => DeltaType::Face,
140 (false, true, false) => DeltaType::Pose,
141 (false, false, true) => DeltaType::Scene,
142 _ => DeltaType::Multiple,
143 };
144
145 Self {
146 id: curr.id,
147 keyframe_ref,
148 prev_delta_ref: Some(prev.id),
149 timestamp: curr.timestamp,
150 delta_type,
151 face_delta,
152 pose_delta,
153 scene_delta,
154 sequence: curr.sequence,
155 }
156 }
157
158 pub fn is_empty(&self) -> bool {
160 self.delta_type == DeltaType::None
161 }
162
163 pub fn estimated_size(&self) -> usize {
165 let mut size = 24; if let Some(ref fd) = self.face_delta {
168 size += fd.estimated_size();
169 }
170 if let Some(ref pd) = self.pose_delta {
171 size += pd.estimated_size();
172 }
173 if let Some(ref sd) = self.scene_delta {
174 size += sd.estimated_size();
175 }
176
177 size
178 }
179
180 pub fn apply(&self, base: &VisualState) -> VisualState {
182 let mut result = base.clone();
183 result.id = self.id;
184 result.timestamp = self.timestamp;
185 result.sequence = self.sequence;
186 result.is_keyframe = false;
187 result.keyframe_ref = Some(self.keyframe_ref);
188
189 if let Some(ref fd) = self.face_delta {
190 result.face = fd.apply(base.face.as_ref());
191 }
192 if let Some(ref pd) = self.pose_delta {
193 result.pose = pd.apply(base.pose.as_ref());
194 }
195 if let Some(ref sd) = self.scene_delta {
196 result.scene = sd.apply(base.scene.as_ref());
197 }
198
199 result
200 }
201}
202
203#[derive(Debug, Clone)]
205pub enum FaceDelta {
206 Removed,
208 Full(FaceState),
210 Partial {
212 head_rotation: Option<(f32, f32, f32)>,
214 emotion_change: Option<(String, f32)>, mouth_openness: Option<f32>,
218 speaking: Option<bool>,
220 gaze_yaw: Option<f32>,
222 gaze_pitch: Option<f32>,
223 },
224}
225
226impl FaceDelta {
227 pub fn compute(prev: &FaceState, curr: &FaceState) -> Option<FaceDelta> {
229 let head_changed = (prev.head_rotation.0 - curr.head_rotation.0).abs() > 0.05
231 || (prev.head_rotation.1 - curr.head_rotation.1).abs() > 0.05
232 || (prev.head_rotation.2 - curr.head_rotation.2).abs() > 0.05;
233
234 let mouth_changed = (prev.mouth.openness - curr.mouth.openness).abs() > 0.1;
235 let speaking_changed = prev.speaking != curr.speaking;
236 let gaze_changed = (prev.gaze.yaw - curr.gaze.yaw).abs() > 0.1
237 || (prev.gaze.pitch - curr.gaze.pitch).abs() > 0.1;
238
239 let prev_dom = prev.emotion.dominant();
241 let curr_dom = curr.emotion.dominant();
242 let emotion_changed = prev_dom.0 != curr_dom.0 || (prev_dom.1 - curr_dom.1).abs() > 0.2;
243
244 if !head_changed && !mouth_changed && !speaking_changed && !gaze_changed && !emotion_changed
245 {
246 return None;
247 }
248
249 Some(FaceDelta::Partial {
250 head_rotation: if head_changed {
251 Some(curr.head_rotation)
252 } else {
253 None
254 },
255 emotion_change: if emotion_changed {
256 Some((curr_dom.0.to_string(), curr_dom.1))
257 } else {
258 None
259 },
260 mouth_openness: if mouth_changed {
261 Some(curr.mouth.openness)
262 } else {
263 None
264 },
265 speaking: if speaking_changed {
266 Some(curr.speaking)
267 } else {
268 None
269 },
270 gaze_yaw: if gaze_changed {
271 Some(curr.gaze.yaw)
272 } else {
273 None
274 },
275 gaze_pitch: if gaze_changed {
276 Some(curr.gaze.pitch)
277 } else {
278 None
279 },
280 })
281 }
282
283 pub fn full(face: FaceState) -> FaceDelta {
285 FaceDelta::Full(face)
286 }
287
288 pub fn removed() -> FaceDelta {
290 FaceDelta::Removed
291 }
292
293 pub fn estimated_size(&self) -> usize {
295 match self {
296 FaceDelta::Removed => 1,
297 FaceDelta::Full(_) => 128,
298 FaceDelta::Partial { .. } => 32,
299 }
300 }
301
302 pub fn apply(&self, base: Option<&FaceState>) -> Option<FaceState> {
304 match self {
305 FaceDelta::Removed => None,
306 FaceDelta::Full(face) => Some(face.clone()),
307 FaceDelta::Partial {
308 head_rotation,
309 emotion_change,
310 mouth_openness,
311 speaking,
312 gaze_yaw,
313 gaze_pitch,
314 } => {
315 let mut face = base?.clone();
316
317 if let Some(rot) = head_rotation {
318 face.head_rotation = *rot;
319 }
320 if let Some((_, val)) = emotion_change {
321 face.emotion.joy = *val;
323 }
324 if let Some(openness) = mouth_openness {
325 face.mouth.openness = *openness;
326 }
327 if let Some(spk) = speaking {
328 face.speaking = *spk;
329 }
330 if let Some(yaw) = gaze_yaw {
331 face.gaze.yaw = *yaw;
332 }
333 if let Some(pitch) = gaze_pitch {
334 face.gaze.pitch = *pitch;
335 }
336
337 Some(face)
338 }
339 }
340 }
341}
342
343#[derive(Debug, Clone)]
345pub enum PoseDelta {
346 Removed,
348 Full(PoseState),
350 Partial {
352 changed_joints: Vec<(usize, crate::JointState)>,
354 gesture: Option<crate::Gesture>,
356 activity: Option<crate::ActivityState>,
358 },
359}
360
361impl PoseDelta {
362 pub fn compute(prev: &PoseState, curr: &PoseState) -> Option<PoseDelta> {
364 let mut changed_joints = Vec::new();
365
366 for (i, (prev_joint, curr_joint)) in prev.joints.iter().zip(curr.joints.iter()).enumerate()
368 {
369 let pos_changed = prev_joint.position.distance(&curr_joint.position) > 0.01;
370 if pos_changed {
371 changed_joints.push((i, *curr_joint));
372 }
373 }
374
375 let gesture_changed = prev.gesture != curr.gesture;
376 let activity_changed = prev.activity != curr.activity;
377
378 if changed_joints.is_empty() && !gesture_changed && !activity_changed {
379 return None;
380 }
381
382 Some(PoseDelta::Partial {
383 changed_joints,
384 gesture: if gesture_changed {
385 Some(curr.gesture)
386 } else {
387 None
388 },
389 activity: if activity_changed {
390 Some(curr.activity)
391 } else {
392 None
393 },
394 })
395 }
396
397 pub fn full(pose: PoseState) -> PoseDelta {
398 PoseDelta::Full(pose)
399 }
400
401 pub fn removed() -> PoseDelta {
402 PoseDelta::Removed
403 }
404
405 pub fn estimated_size(&self) -> usize {
406 match self {
407 PoseDelta::Removed => 1,
408 PoseDelta::Full(_) => 256,
409 PoseDelta::Partial { changed_joints, .. } => 8 + changed_joints.len() * 16,
410 }
411 }
412
413 pub fn apply(&self, base: Option<&PoseState>) -> Option<PoseState> {
414 match self {
415 PoseDelta::Removed => None,
416 PoseDelta::Full(pose) => Some(pose.clone()),
417 PoseDelta::Partial {
418 changed_joints,
419 gesture,
420 activity,
421 } => {
422 let mut pose = base?.clone();
423
424 for (idx, joint_state) in changed_joints {
425 if *idx < pose.joints.len() {
426 pose.joints[*idx] = *joint_state;
427 }
428 }
429
430 if let Some(g) = gesture {
431 pose.gesture = *g;
432 }
433 if let Some(a) = activity {
434 pose.activity = *a;
435 }
436
437 Some(pose)
438 }
439 }
440 }
441}
442
443#[derive(Debug, Clone)]
445pub enum SceneDelta {
446 Removed,
448 Full(SceneState),
450 Partial {
452 background_color: Option<crate::Color>,
454 lighting: Option<crate::LightingCondition>,
456 detail_level: Option<f32>,
458 },
459}
460
461impl SceneDelta {
462 pub fn compute(prev: &SceneState, curr: &SceneState) -> Option<SceneDelta> {
463 let color_changed = (prev.background_color.r - curr.background_color.r).abs() > 0.1
464 || (prev.background_color.g - curr.background_color.g).abs() > 0.1
465 || (prev.background_color.b - curr.background_color.b).abs() > 0.1;
466
467 let lighting_changed = prev.lighting != curr.lighting;
468 let detail_changed = (prev.detail_level - curr.detail_level).abs() > 0.1;
469
470 if !color_changed && !lighting_changed && !detail_changed {
471 return None;
472 }
473
474 Some(SceneDelta::Partial {
475 background_color: if color_changed {
476 Some(curr.background_color)
477 } else {
478 None
479 },
480 lighting: if lighting_changed {
481 Some(curr.lighting)
482 } else {
483 None
484 },
485 detail_level: if detail_changed {
486 Some(curr.detail_level)
487 } else {
488 None
489 },
490 })
491 }
492
493 pub fn full(scene: SceneState) -> SceneDelta {
494 SceneDelta::Full(scene)
495 }
496
497 pub fn removed() -> SceneDelta {
498 SceneDelta::Removed
499 }
500
501 pub fn estimated_size(&self) -> usize {
502 match self {
503 SceneDelta::Removed => 1,
504 SceneDelta::Full(_) => 64,
505 SceneDelta::Partial { .. } => 16,
506 }
507 }
508
509 pub fn apply(&self, base: Option<&SceneState>) -> Option<SceneState> {
510 match self {
511 SceneDelta::Removed => None,
512 SceneDelta::Full(scene) => Some(scene.clone()),
513 SceneDelta::Partial {
514 background_color,
515 lighting,
516 detail_level,
517 } => {
518 let mut scene = base?.clone();
519
520 if let Some(color) = background_color {
521 scene.background_color = *color;
522 }
523 if let Some(light) = lighting {
524 scene.lighting = *light;
525 }
526 if let Some(detail) = detail_level {
527 scene.detail_level = *detail;
528 }
529
530 Some(scene)
531 }
532 }
533 }
534}
535
536#[cfg(test)]
537mod tests {
538 use super::*;
539 use elara_core::NodeId;
540
541 #[test]
542 fn test_keyframe_creation() {
543 let node = NodeId::new(1);
544 let time = StateTime::from_millis(0);
545 let state = VisualState::keyframe(node, time, 1);
546 let keyframe = Keyframe::new(state, 1000);
547
548 assert_eq!(keyframe.interval_ms, 1000);
549 assert!(keyframe.estimated_size() > 0);
550 }
551
552 #[test]
553 fn test_delta_computation() {
554 let node = NodeId::new(1);
555 let time1 = StateTime::from_millis(0);
556 let time2 = StateTime::from_millis(100);
557
558 let state1 = VisualState::keyframe(node, time1, 1);
559 let state2 = VisualState::keyframe(node, time2, 2);
560
561 let delta = Delta::from_states(&state1, &state2, state1.id);
562
563 assert!(delta.is_empty());
565 }
566}