fix(track): keep Roto Brush spans around their base frame within a frame limit; index strokes by frame

This commit is contained in:
Dylan M. Taylor
2026-11-10 21:57:54 -05:00
parent c30fb9aeed
commit 4d8846429f
2 changed files with 39 additions and 2 deletions
+21 -2
View File
@@ -116,12 +116,25 @@ fn fnv(h: &mut u64, bytes: &[u8]) {
}
}
/// Frames a segmentation span may reach on each side of its base frame (72 minutes at 60 fps):
/// every frame of the span gets a content key, and the propagation jobs visit each one.
pub const MAX_SPAN_FRAMES: i64 = 1 << 18;
impl RotoData {
/// Read saved data. The span is kept around the base frame and within [`MAX_SPAN_FRAMES`]
/// of it on each side, whatever the file says.
pub fn from_json(s: &str) -> RotoData {
if s.trim().is_empty() {
return RotoData::default();
}
serde_json::from_str(s).unwrap_or_default()
let mut d: RotoData = serde_json::from_str(s).unwrap_or_default();
d.bound_span();
d
}
fn bound_span(&mut self) {
let Some(base) = self.base else { return };
let [a, b] = self.span;
self.span = [a.min(base).max(base.saturating_sub(MAX_SPAN_FRAMES)), b.max(base).min(base.saturating_add(MAX_SPAN_FRAMES))];
}
pub fn to_json(&self) -> String {
serde_json::to_string(self).unwrap_or_default()
@@ -169,6 +182,7 @@ impl RotoData {
}
None => {}
}
self.bound_span();
self.strokes.push(s);
}
/// Largest Refine Edge stroke radius (layer pixels).
@@ -181,10 +195,15 @@ impl RotoData {
pub fn chain_keys(&self, seed: u64) -> BTreeMap<i64, u64> {
let mut out = BTreeMap::new();
let Some(base) = self.base else { return out };
// Strokes by frame, once: not a pass over every stroke for each frame of the span.
let mut by_frame: BTreeMap<i64, Vec<&Stroke>> = BTreeMap::new();
for s in &self.strokes {
by_frame.entry(s.frame).or_default().push(s);
}
let key = |prev: u64, f: i64| {
let mut h = prev ^ 0x9e37_79b9_7f4a_7c15;
fnv(&mut h, &f.to_le_bytes());
for s in self.strokes.iter().filter(|s| s.frame == f) {
for s in by_frame.get(&f).into_iter().flatten() {
fnv(&mut h, serde_json::to_string(s).unwrap_or_default().as_bytes());
}
h
+18
View File
@@ -829,4 +829,22 @@ fn propagate_survives_empty_frames() {
assert_eq!((next.w, next.h), (w as usize, h as usize));
assert!(next.matte.is_empty());
}
#[test]
fn saved_spans_stay_around_the_base_frame_within_the_limit() {
let mut d = RotoData::default();
for s in base_strokes(5) {
d.add(s, 20, [0, 30]);
}
d.span = [i64::MIN, i64::MAX];
let r = RotoData::from_json(&d.to_json());
assert_eq!(r.span, [5 - MAX_SPAN_FRAMES, 5 + MAX_SPAN_FRAMES]);
assert_eq!(r.chain_keys(1).len() as i64, 2 * MAX_SPAN_FRAMES + 1);
// A span that leaves out its base frame is widened to hold it.
d.span = [10, 20];
assert_eq!(RotoData::from_json(&d.to_json()).span, [5, 20]);
// A stroke far away extends the span only up to the limit.
let mut e = RotoData::from_json(&RotoData { span: [0, 25], ..d.clone() }.to_json());
e.add(line(StrokeKind::Fg, i64::MAX, [0.0, 0.0], [1.0, 1.0], 3.0), 20, [0, 30]);
assert_eq!(e.span, [0, 5 + MAX_SPAN_FRAMES]);
}