mirror of
https://github.com/storytold/effectcraft.git
synced 2026-10-31 21:48:20 -06:00
Project: comp_contains walks precomps with a visited set, not recursion (#1237)
Project::comp_contains recursed through precomp layers with no visited
set or depth limit. A damaged project with a precomp cycle that doesn't
pass the queried comp (A and B nesting each other, C unrelated) recursed
until the stack ran out, which aborts the process ("stack overflow,
aborting"): it can't be caught as a command error, so unsaved work is
lost. layer.addItem checks it before adding a precomp, and the renderer
calls it for every nested comp and its audio. A long chain used the
stack the same way, and comps shared by many paths were walked once per
path (exponential for nested diamonds).
It is now an explicit stack with a visited set: same answers (a comp
contains itself; a target reached through a cycle is found), each comp
visited once, no recursion. The renderer already skips a precomp that
contains the comp being rendered, so a cyclic project now renders
instead of crashing; nothing is repaired on load.
Tests: project comp_contains_ends_on_cycles_long_chains_and_shared_
precomps (cycle with an unrelated target, self-nesting, a target through
a cycle, 40 levels of diamonds, a 20 000-comp chain); engine
a_project_with_a_precomp_cycle_opens_adds_and_renders_without_running_
out_of_stack (file.open, layer.addItem both ways, render). On main the
engine test aborts with a stack overflow.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
549e9d8251
commit
b26a340a7e
@@ -102,3 +102,38 @@ fn commands_cap_sizes_at_30000_px() {
|
||||
let f = s.project.items.values().find_map(|i| if let ItemKind::Footage(f) = &i.kind { Some(f.clone()) } else { None }).unwrap();
|
||||
assert_eq!((f.width, f.height, f.frame_rate), (limits::MAX_SIZE, 8, FrameRate::FPS_29_97));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_project_with_a_precomp_cycle_opens_adds_and_renders_without_running_out_of_stack() {
|
||||
// Comps A and B nest each other (a damaged file); C is unrelated. Checking whether A
|
||||
// contains C walked the A ⇄ B cycle until the stack ran out (#1237).
|
||||
let mut s = Session::default();
|
||||
let mut ids = vec![];
|
||||
for name in ["A", "B", "C"] {
|
||||
s.execute("comp.new", json!({"name": name, "width": 32, "height": 32, "frameRate": 30, "duration": 1})).unwrap();
|
||||
ids.push(s.active_comp_id().unwrap());
|
||||
}
|
||||
let (a, b, c) = (ids[0], ids[1], ids[2]);
|
||||
{
|
||||
let proj = std::sync::Arc::make_mut(&mut s.project);
|
||||
for (outer, inner) in [(a, b), (b, a)] {
|
||||
let comp = proj.comp(outer).unwrap().clone();
|
||||
let l = effectcraft_project::build::layer(proj, &comp, "pre", effectcraft_project::LayerSource::Comp { item: inner }, (32, 32), None);
|
||||
let ItemKind::Comp(cc) = &mut proj.item_mut(outer).unwrap().kind else { panic!("comp") };
|
||||
std::sync::Arc::make_mut(cc).layers.push(l);
|
||||
}
|
||||
}
|
||||
let path = tmp("cycle.ecproj");
|
||||
std::fs::write(&path, s.project.to_file_json().unwrap()).unwrap();
|
||||
let mut t = Session::default();
|
||||
t.execute("file.open", json!({"path": path})).unwrap();
|
||||
t.open_comp(c);
|
||||
// Adding A into C asks whether A contains C: it doesn't, so the layer goes in.
|
||||
let r = t.execute("layer.addItem", json!({"comp": c.0, "item": a.0})).unwrap();
|
||||
assert!(r["layer"].as_u64().is_some(), "{r}");
|
||||
// Adding C into A would nest C in itself through A: refused, as before.
|
||||
assert!(t.execute("layer.addItem", json!({"comp": a.0, "item": c.0})).is_err());
|
||||
// C renders: inside A the renderer skips B's copy of A instead of looping.
|
||||
let img = t.render(c, Tick::ZERO, RenderOpts { scale: 1.0, ..Default::default() });
|
||||
assert_eq!((img.width, img.height), (32, 32));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user