1use std::collections::{HashMap, HashSet};
10
11#[derive(Debug, Clone)]
17pub struct DirtyTracker {
18 dirty: HashSet<String>,
19}
20
21impl Default for DirtyTracker {
22 fn default() -> Self {
23 Self::new()
24 }
25}
26
27impl DirtyTracker {
28 pub fn new() -> Self {
30 Self {
31 dirty: HashSet::new(),
32 }
33 }
34
35 pub fn mark_dirty(&mut self, name: &str) {
37 self.dirty.insert(name.to_string());
38 }
39
40 pub fn mark_all_dirty(&mut self, all_names: &[&str]) {
43 for name in all_names {
44 self.dirty.insert((*name).to_string());
45 }
46 }
47
48 pub fn clear(&mut self) {
50 self.dirty.clear();
51 }
52
53 pub fn is_dirty(&self, name: &str) -> bool {
55 self.dirty.contains(name)
56 }
57
58 pub fn dirty_count(&self) -> usize {
60 self.dirty.len()
61 }
62
63 pub fn dirty_targets(&self) -> Vec<&str> {
65 self.dirty.iter().map(|s| s.as_str()).collect()
66 }
67
68 pub fn is_clean(&self) -> bool {
70 self.dirty.is_empty()
71 }
72}
73
74#[derive(Debug, Clone)]
82struct TargetContribution {
83 buf: Vec<f32>,
85}
86
87#[derive(Debug, Clone)]
91pub struct IncrementalMorphCache {
92 contributions: HashMap<String, TargetContribution>,
93 vertex_count: usize,
94}
95
96impl IncrementalMorphCache {
97 pub fn new(vertex_count: usize) -> Self {
99 Self {
100 contributions: HashMap::new(),
101 vertex_count,
102 }
103 }
104
105 pub fn vertex_count(&self) -> usize {
107 self.vertex_count
108 }
109
110 pub fn target_count(&self) -> usize {
112 self.contributions.len()
113 }
114
115 pub fn update_target(
121 &mut self,
122 name: &str,
123 deltas: &[(u32, f32, f32, f32)],
124 weight: f32,
125 vertex_count: usize,
126 ) {
127 let len = vertex_count * 3;
128 let entry = self
129 .contributions
130 .entry(name.to_string())
131 .or_insert_with(|| TargetContribution {
132 buf: vec![0.0; len],
133 });
134
135 if entry.buf.len() != len {
137 entry.buf.resize(len, 0.0);
138 }
139
140 for v in entry.buf.iter_mut() {
142 *v = 0.0;
143 }
144
145 for &(vid, dx, dy, dz) in deltas {
147 let idx = vid as usize * 3;
148 if idx + 2 < len {
149 entry.buf[idx] += weight * dx;
150 entry.buf[idx + 1] += weight * dy;
151 entry.buf[idx + 2] += weight * dz;
152 }
153 }
154 }
155
156 pub fn remove_target(&mut self, name: &str) {
158 self.contributions.remove(name);
159 }
160
161 pub fn rebuild_mesh(&self, base_positions: &[f32]) -> Vec<f32> {
167 let len = base_positions.len();
168 let mut out = base_positions.to_vec();
169 for contrib in self.contributions.values() {
170 let n = contrib.buf.len().min(len);
171 for (out_val, &src_val) in out[..n].iter_mut().zip(contrib.buf[..n].iter()) {
172 *out_val += src_val;
173 }
174 }
175 out
176 }
177
178 pub fn rebuild_incremental(
191 &self,
192 current: &mut [f32],
193 dirty: &DirtyTracker,
194 old_contributions: &HashMap<String, Vec<f32>>,
195 ) {
196 let len = current.len();
197
198 for dirty_name in dirty.dirty_targets() {
199 if let Some(old_buf) = old_contributions.get(dirty_name) {
201 let n = old_buf.len().min(len);
202 for i in 0..n {
203 current[i] -= old_buf[i];
204 }
205 }
206
207 if let Some(new_contrib) = self.contributions.get(dirty_name) {
209 let n = new_contrib.buf.len().min(len);
210 for (cur_val, &src_val) in current[..n].iter_mut().zip(new_contrib.buf[..n].iter())
211 {
212 *cur_val += src_val;
213 }
214 }
215 }
216 }
217
218 pub fn snapshot_contribution(&self, name: &str) -> Option<Vec<f32>> {
221 self.contributions.get(name).map(|c| c.buf.clone())
222 }
223
224 pub fn snapshot_all(&self) -> HashMap<String, Vec<f32>> {
226 self.contributions
227 .iter()
228 .map(|(k, v)| (k.clone(), v.buf.clone()))
229 .collect()
230 }
231
232 pub fn clear(&mut self) {
234 self.contributions.clear();
235 }
236}
237
238#[cfg(test)]
243mod tests {
244 use super::*;
245
246 #[test]
249 fn tracker_starts_clean() {
250 let tracker = DirtyTracker::new();
251 assert!(tracker.is_clean());
252 assert_eq!(tracker.dirty_count(), 0);
253 assert!(!tracker.is_dirty("foo"));
254 }
255
256 #[test]
257 fn mark_and_query() {
258 let mut tracker = DirtyTracker::new();
259 tracker.mark_dirty("height");
260 tracker.mark_dirty("weight");
261
262 assert!(tracker.is_dirty("height"));
263 assert!(tracker.is_dirty("weight"));
264 assert!(!tracker.is_dirty("age"));
265 assert_eq!(tracker.dirty_count(), 2);
266 }
267
268 #[test]
269 fn mark_all_dirty() {
270 let mut tracker = DirtyTracker::new();
271 let names = vec!["a", "b", "c"];
272 tracker.mark_all_dirty(&names);
273 assert_eq!(tracker.dirty_count(), 3);
274 for name in &names {
275 assert!(tracker.is_dirty(name));
276 }
277 }
278
279 #[test]
280 fn clear_resets() {
281 let mut tracker = DirtyTracker::new();
282 tracker.mark_dirty("x");
283 tracker.clear();
284 assert!(tracker.is_clean());
285 assert_eq!(tracker.dirty_count(), 0);
286 }
287
288 #[test]
289 fn dirty_targets_returns_all_marked() {
290 let mut tracker = DirtyTracker::new();
291 tracker.mark_dirty("alpha");
292 tracker.mark_dirty("beta");
293 let mut targets = tracker.dirty_targets();
294 targets.sort();
295 assert_eq!(targets, vec!["alpha", "beta"]);
296 }
297
298 #[test]
299 fn duplicate_mark_is_idempotent() {
300 let mut tracker = DirtyTracker::new();
301 tracker.mark_dirty("x");
302 tracker.mark_dirty("x");
303 assert_eq!(tracker.dirty_count(), 1);
304 }
305
306 fn base_3v() -> Vec<f32> {
309 vec![0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0, 0.0]
311 }
312
313 #[test]
314 fn empty_cache_rebuild_equals_base() {
315 let cache = IncrementalMorphCache::new(3);
316 let base = base_3v();
317 let result = cache.rebuild_mesh(&base);
318 assert_eq!(result, base);
319 }
320
321 #[test]
322 fn update_target_and_rebuild() {
323 let mut cache = IncrementalMorphCache::new(3);
324 let deltas = vec![(1u32, 0.5f32, 0.0f32, 0.0f32)];
326 cache.update_target("height", &deltas, 1.0, 3);
327
328 let base = base_3v();
329 let result = cache.rebuild_mesh(&base);
330
331 assert!((result[3] - 1.5).abs() < 1e-6);
333 assert!((result[0] - 0.0).abs() < 1e-6);
335 assert!((result[6] - 0.0).abs() < 1e-6);
336 }
337
338 #[test]
339 fn update_target_with_weight() {
340 let mut cache = IncrementalMorphCache::new(3);
341 let deltas = vec![(0u32, 2.0f32, 0.0f32, 0.0f32)];
342 cache.update_target("stretch", &deltas, 0.5, 3);
343
344 let base = base_3v();
345 let result = cache.rebuild_mesh(&base);
346
347 assert!((result[0] - 1.0).abs() < 1e-6);
349 }
350
351 #[test]
352 fn remove_target_excludes_contribution() {
353 let mut cache = IncrementalMorphCache::new(3);
354 let deltas = vec![(0u32, 10.0f32, 0.0f32, 0.0f32)];
355 cache.update_target("big", &deltas, 1.0, 3);
356 cache.remove_target("big");
357
358 let base = base_3v();
359 let result = cache.rebuild_mesh(&base);
360 assert!((result[0] - 0.0).abs() < 1e-6);
361 }
362
363 #[test]
364 fn multiple_targets_sum() {
365 let mut cache = IncrementalMorphCache::new(3);
366 cache.update_target("a", &[(0u32, 1.0f32, 0.0f32, 0.0f32)], 1.0, 3);
367 cache.update_target("b", &[(0u32, 0.0f32, 2.0f32, 0.0f32)], 1.0, 3);
368
369 let base = base_3v();
370 let result = cache.rebuild_mesh(&base);
371 assert!((result[0] - 1.0).abs() < 1e-6);
373 assert!((result[1] - 2.0).abs() < 1e-6);
374 assert!((result[2] - 0.0).abs() < 1e-6);
375 }
376
377 #[test]
378 fn incremental_rebuild_matches_full() {
379 let base = base_3v();
380 let mut cache = IncrementalMorphCache::new(3);
381
382 let deltas = vec![(1u32, 2.0f32, 0.0f32, 0.0f32)];
384 cache.update_target("h", &deltas, 0.5, 3);
385 let old_snap = cache.snapshot_all();
386 let mut current = cache.rebuild_mesh(&base);
387
388 cache.update_target("h", &deltas, 0.8, 3);
390 let mut dirty = DirtyTracker::new();
391 dirty.mark_dirty("h");
392
393 cache.rebuild_incremental(&mut current, &dirty, &old_snap);
394
395 let full = cache.rebuild_mesh(&base);
397
398 for (i, (a, b)) in current.iter().zip(full.iter()).enumerate() {
399 assert!(
400 (a - b).abs() < 1e-5,
401 "mismatch at index {}: incremental={}, full={}",
402 i,
403 a,
404 b
405 );
406 }
407 }
408
409 #[test]
410 fn incremental_new_target_matches_full() {
411 let base = base_3v();
412 let mut cache = IncrementalMorphCache::new(3);
413
414 cache.update_target("a", &[(0u32, 1.0f32, 0.0f32, 0.0f32)], 1.0, 3);
416 let old_snap = cache.snapshot_all();
417 let mut current = cache.rebuild_mesh(&base);
418
419 cache.update_target("b", &[(2u32, 0.0f32, 0.0f32, 3.0f32)], 1.0, 3);
421 let mut dirty = DirtyTracker::new();
422 dirty.mark_dirty("b");
423
424 cache.rebuild_incremental(&mut current, &dirty, &old_snap);
425
426 let full = cache.rebuild_mesh(&base);
427 for (i, (a, b)) in current.iter().zip(full.iter()).enumerate() {
428 assert!(
429 (a - b).abs() < 1e-5,
430 "mismatch at index {}: incremental={}, full={}",
431 i,
432 a,
433 b
434 );
435 }
436 }
437
438 #[test]
439 fn incremental_remove_target_matches_full() {
440 let base = base_3v();
441 let mut cache = IncrementalMorphCache::new(3);
442
443 cache.update_target("a", &[(0u32, 5.0f32, 0.0f32, 0.0f32)], 1.0, 3);
444 cache.update_target("b", &[(1u32, 0.0f32, 3.0f32, 0.0f32)], 1.0, 3);
445 let old_snap = cache.snapshot_all();
446 let mut current = cache.rebuild_mesh(&base);
447
448 cache.remove_target("a");
450 let mut dirty = DirtyTracker::new();
451 dirty.mark_dirty("a");
452
453 cache.rebuild_incremental(&mut current, &dirty, &old_snap);
454
455 let full = cache.rebuild_mesh(&base);
456 for (i, (a, b)) in current.iter().zip(full.iter()).enumerate() {
457 assert!(
458 (a - b).abs() < 1e-5,
459 "mismatch at index {}: incremental={}, full={}",
460 i,
461 a,
462 b
463 );
464 }
465 }
466
467 #[test]
468 fn incremental_no_dirty_is_noop() {
469 let base = base_3v();
470 let mut cache = IncrementalMorphCache::new(3);
471 cache.update_target("x", &[(0u32, 1.0f32, 2.0f32, 3.0f32)], 1.0, 3);
472 let old_snap = cache.snapshot_all();
473 let mut current = cache.rebuild_mesh(&base);
474 let before = current.clone();
475
476 let dirty = DirtyTracker::new(); cache.rebuild_incremental(&mut current, &dirty, &old_snap);
478
479 assert_eq!(current, before);
480 }
481
482 #[test]
483 fn snapshot_contribution_round_trip() {
484 let mut cache = IncrementalMorphCache::new(3);
485 let deltas = vec![(0u32, 1.0f32, 2.0f32, 3.0f32)];
486 cache.update_target("t", &deltas, 0.5, 3);
487
488 let snap = cache.snapshot_contribution("t");
489 assert!(snap.is_some());
490 let snap = snap.expect("snapshot should exist");
491 assert!((snap[0] - 0.5).abs() < 1e-6);
493 assert!((snap[1] - 1.0).abs() < 1e-6);
494 assert!((snap[2] - 1.5).abs() < 1e-6);
495 }
496
497 #[test]
498 fn out_of_bounds_vertex_id_is_ignored() {
499 let mut cache = IncrementalMorphCache::new(2);
500 let deltas = vec![(10u32, 1.0f32, 1.0f32, 1.0f32)];
502 cache.update_target("oob", &deltas, 1.0, 2);
503
504 let base = vec![0.0f32; 6];
505 let result = cache.rebuild_mesh(&base);
506 assert_eq!(result, base);
508 }
509
510 #[test]
511 fn clear_empties_cache() {
512 let mut cache = IncrementalMorphCache::new(3);
513 cache.update_target("a", &[(0u32, 1.0, 0.0, 0.0)], 1.0, 3);
514 assert_eq!(cache.target_count(), 1);
515 cache.clear();
516 assert_eq!(cache.target_count(), 0);
517 }
518
519 #[test]
522 fn multi_frame_incremental_consistency() {
523 let base = base_3v();
524 let deltas_h = vec![
525 (0u32, 1.0f32, 0.0f32, 0.0f32),
526 (1u32, 0.0f32, 0.5f32, 0.0f32),
527 ];
528 let deltas_w = vec![
529 (1u32, 0.0f32, 0.0f32, 1.0f32),
530 (2u32, 0.3f32, 0.0f32, 0.0f32),
531 ];
532
533 let weight_sequences: &[(f32, f32)] = &[
534 (0.0, 0.0),
535 (0.5, 0.2),
536 (0.8, 0.6),
537 (1.0, 1.0),
538 (0.3, 0.9),
539 (0.0, 0.0),
540 ];
541
542 let mut cache = IncrementalMorphCache::new(3);
543 let mut current = base.clone();
544 let mut old_snap: HashMap<String, Vec<f32>> = HashMap::new();
545
546 for &(wh, ww) in weight_sequences {
547 cache.update_target("h", &deltas_h, wh, 3);
549 cache.update_target("w", &deltas_w, ww, 3);
550
551 let mut dirty = DirtyTracker::new();
552 dirty.mark_dirty("h");
553 dirty.mark_dirty("w");
554
555 cache.rebuild_incremental(&mut current, &dirty, &old_snap);
556
557 let full = cache.rebuild_mesh(&base);
558
559 for (i, (a, b)) in current.iter().zip(full.iter()).enumerate() {
560 assert!(
561 (a - b).abs() < 1e-4,
562 "frame wh={}, ww={}: mismatch at {}: inc={}, full={}",
563 wh,
564 ww,
565 i,
566 a,
567 b
568 );
569 }
570
571 old_snap = cache.snapshot_all();
572 }
573 }
574}