mirror of
https://github.com/storytold/deckcraft.git
synced 2026-10-24 10:37:03 -06:00
Anim: group animations by sequence in one pass (#410)
Timeline::with found trigger sequences with Vec::contains and then rescanned every animation once per sequence, and checked MAX_EFFECTS only after a match, so a slide with many distinct triggers cost quadratic work before the effect cap could stop it. Animations are now grouped once (an index by sequence, first-appearance order kept, main first), the build stops at MAX_EFFECTS before the next sequence, and paragraph groups are deduplicated with a set instead of Vec::contains. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -789,3 +789,38 @@ fn show_follows_links() {
|
||||
assert_eq!(s.follow(&p, &Action::LastViewed, None), LinkJump::None);
|
||||
assert_eq!(s.slide, 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn trigger_sequences_keep_first_appearance_order_when_interleaved() {
|
||||
let mut anims = vec![];
|
||||
for (i, tr) in [7u32, 5, 7, 0, 9, 5].iter().enumerate() {
|
||||
let mut a = anim(1 + (i as u32 % 4), AnimClass::Entrance, "fade", AnimStart::OnClick);
|
||||
a.trigger = (*tr != 0).then_some(ShapeId(*tr));
|
||||
anims.push(a);
|
||||
}
|
||||
let tl = Timeline::new(&slide_with(anims), W, H);
|
||||
assert_eq!(tl.trigger_shapes(), vec![ShapeId(7), ShapeId(5), ShapeId(9)]);
|
||||
assert_eq!(tl.trigger_steps(ShapeId(7)), 2);
|
||||
assert_eq!(tl.trigger_steps(ShapeId(5)), 2);
|
||||
assert_eq!(tl.trigger_steps(ShapeId(9)), 1);
|
||||
assert_eq!(tl.steps(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn many_distinct_triggers_past_the_effect_cap_build_in_order() {
|
||||
// One sequence per animation: grouping used to rescan every animation per trigger
|
||||
// (~1.2e10 steps here), now it is one pass, and nothing past the cap is looked at.
|
||||
let n = 110_000u32;
|
||||
let anims: Vec<Animation> = (0..n)
|
||||
.map(|i| {
|
||||
let mut a = anim(1, AnimClass::Entrance, "appear", AnimStart::OnClick);
|
||||
a.trigger = Some(ShapeId(1000 + i));
|
||||
a
|
||||
})
|
||||
.collect();
|
||||
let tl = Timeline::new(&slide_with(anims), W, H);
|
||||
assert_eq!(tl.effects().len(), 100_000);
|
||||
let shapes = tl.trigger_shapes();
|
||||
assert_eq!(shapes.len(), 100_000);
|
||||
assert!(shapes.iter().enumerate().all(|(i, s)| *s == ShapeId(1000 + i as u32)));
|
||||
}
|
||||
|
||||
+23
-15
@@ -1,5 +1,7 @@
|
||||
//! A slide's animation sequence: click steps, effect start times and per-shape state over time.
|
||||
|
||||
use std::collections::{HashMap, HashSet};
|
||||
|
||||
use deckcraft_model::anim::TextBuild;
|
||||
use deckcraft_model::style::Fill;
|
||||
use deckcraft_model::{AnimClass, AnimStart, Animation, ColorScheme, Rgba, SchemeSlot, ShapeId, Slide, Xfrm};
|
||||
@@ -151,25 +153,31 @@ impl Timeline {
|
||||
let accent = scheme.map(|s| s.get(SchemeSlot::Accent1)).unwrap_or(Rgba::rgb(0x15, 0x60, 0x82));
|
||||
|
||||
let mut tl = Timeline::default();
|
||||
// Sequences in order of first appearance: main first.
|
||||
let mut seqs: Vec<Sequence> = vec![Sequence::Main];
|
||||
for a in &slide.animations {
|
||||
if let Some(tr) = a.trigger {
|
||||
let s = Sequence::Trigger(tr);
|
||||
if !seqs.contains(&s) {
|
||||
seqs.push(s);
|
||||
}
|
||||
// Group the animations by sequence in one pass, sequences in order of first appearance
|
||||
// (main first), so many distinct triggers cost linear work, not a scan per trigger.
|
||||
let mut seqs: Vec<(Sequence, Vec<usize>)> = vec![(Sequence::Main, Vec::new())];
|
||||
let mut seq_index: HashMap<Sequence, usize> = HashMap::from([(Sequence::Main, 0)]);
|
||||
for (ai, a) in slide.animations.iter().enumerate() {
|
||||
let s = a.trigger.map(Sequence::Trigger).unwrap_or(Sequence::Main);
|
||||
let i = *seq_index.entry(s).or_insert_with(|| {
|
||||
seqs.push((s, Vec::new()));
|
||||
seqs.len() - 1
|
||||
});
|
||||
if let Some((_, members)) = seqs.get_mut(i) {
|
||||
members.push(ai);
|
||||
}
|
||||
}
|
||||
for seq in seqs {
|
||||
// Paragraph groups already recorded (the list itself keeps their order).
|
||||
let mut grouped: HashSet<(ShapeId, usize, usize)> = HashSet::new();
|
||||
for (seq, members) in seqs {
|
||||
if tl.effects.len() >= MAX_EFFECTS {
|
||||
break;
|
||||
}
|
||||
let mut steps: Vec<StepInfo> = Vec::new();
|
||||
let mut group_begin = 0.0f64;
|
||||
let mut max_end = 0.0f64;
|
||||
for (ai, a) in slide.animations.iter().enumerate() {
|
||||
let aseq = a.trigger.map(Sequence::Trigger).unwrap_or(Sequence::Main);
|
||||
if aseq != seq {
|
||||
continue;
|
||||
}
|
||||
for ai in members {
|
||||
let Some(a) = slide.animations.get(ai) else { continue };
|
||||
if tl.effects.len() >= MAX_EFFECTS {
|
||||
break;
|
||||
}
|
||||
@@ -188,7 +196,7 @@ impl Timeline {
|
||||
owner = Some(i);
|
||||
v.push(Some(i));
|
||||
} else if let Some(o) = owner
|
||||
&& !tl.groups.contains(&(a.shape, i, o))
|
||||
&& grouped.insert((a.shape, i, o))
|
||||
{
|
||||
tl.groups.push((a.shape, i, o));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user