Files
effectcraft/crates/render/src/tests_nested.rs
T
bflatasticandClaude Opus 5.5 0b5cff1743 M4.10: a nested comp shows nothing before it starts or after it ends
A precomp layer trimmed out (or time-remapped) past its nested comp's end kept drawing the
nested layers whose bars ran on. Like After Effects, the nested comp now shows nothing outside
its own span (`Comp::covers`), flattened, through a proxy or with Collapse Transformations, on
the CPU and the GPU (both go through `Renderer::source` / `collapse_into`).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 01:47:54 +07:00

91 lines
4.7 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! Nested compositions: Preserve frame rate when nested or in render queue, Preserve resolution
//! when nested.
use effectcraft_color::Label;
use effectcraft_keyframe::{Keyframe, Value};
use effectcraft_project::build;
use effectcraft_project::render_queue::RenderSettings;
use effectcraft_project::{Comp, ItemId, ItemKind, LayerSource, Project, Solid};
use effectcraft_time::{FrameRate, Tick};
use crate::render_frame;
fn s(x: f64) -> Tick {
Tick::from_seconds_f64(x)
}
/// A 30 fps outer comp nesting a 10 fps 100×100 comp whose 10×10 solid moves from x=10 (0 s)
/// to x=90 (2 s). Returns (project, outer, inner).
fn nested(preserve_rate: bool, preserve_res: bool) -> (Project, ItemId, ItemId) {
let mut p = Project::default();
let outer = Comp::new(100, 100, FrameRate::FPS_30, s(4.0));
let mut inner = Comp::new(100, 100, FrameRate::from_f64(10.0), s(4.0));
inner.preserve_frame_rate = preserve_rate;
inner.preserve_resolution = preserve_res;
let sid = p.add_item("S", Label::Red, None, ItemKind::Solid(Solid { color: [1.0, 1.0, 1.0], width: 10, height: 10, pixel_aspect: 1.0 }));
let mut l = build::layer(&mut p, &inner, "S", LayerSource::Solid { item: sid }, (10, 10), None);
l.props.prop_mut("transform/position").unwrap().keys =
vec![Keyframe::new(Tick::ZERO, Value::Vec3([10.0, 50.0, 0.0])), Keyframe::new(s(2.0), Value::Vec3([90.0, 50.0, 0.0]))];
inner.layers.push(l);
let iid = p.add_item("Inner", Label::Sandstone, None, ItemKind::Comp(inner.into()));
let cid = p.add_item("Outer", Label::Sandstone, None, ItemKind::Comp(outer.clone().into()));
let pre = build::layer(&mut p, &outer, "Inner", LayerSource::Comp { item: iid }, (100, 100), None);
p.comp_mut(cid).unwrap().layers.push(pre);
(p, cid, iid)
}
#[test]
fn a_comp_preserving_its_frame_rate_shows_only_its_own_frames_when_nested() {
// 2/30 s falls between the nested comp's frames 0 (0 s) and 1 (0.1 s).
let between = s(2.0 / 30.0);
let (p, cid, _) = nested(false, false);
assert_ne!(render_frame(&p, cid, between, 1.0).data, render_frame(&p, cid, Tick::ZERO, 1.0).data, "follows the outer comp's frames");
let (p, cid, _) = nested(true, false);
assert_eq!(render_frame(&p, cid, between, 1.0).data, render_frame(&p, cid, Tick::ZERO, 1.0).data, "holds its own frame 0");
assert_ne!(render_frame(&p, cid, s(0.1), 1.0).data, render_frame(&p, cid, Tick::ZERO, 1.0).data, "then its frame 1");
}
#[test]
fn the_render_queue_keeps_a_preserved_frame_rate() {
let (p, _, iid) = nested(true, false);
let settings = RenderSettings { frame_rate: Some(FrameRate::FPS_30), ..Default::default() };
assert_eq!(settings.rate(p.comp(iid).unwrap()), FrameRate::from_f64(10.0));
let (p, _, iid) = nested(false, false);
assert_eq!(settings.rate(p.comp(iid).unwrap()), FrameRate::FPS_30);
}
#[test]
fn a_comp_preserving_its_resolution_renders_full_size_and_lands_in_place() {
for preserve in [false, true] {
let (p, cid, _) = nested(false, preserve);
let img = render_frame(&p, cid, Tick::ZERO, 0.5);
assert_eq!((img.width, img.height), (50, 50));
// The solid covers x 5..15, y 45..55 in the 100×100 comp: 2.5..7.5, 22.5..27.5 at half size.
assert!(img.get(5, 25)[3] > 0.99, "preserve {preserve}: inside");
assert!(img.get(20, 25)[3] < 0.01 && img.get(5, 40)[3] < 0.01, "preserve {preserve}: outside");
}
}
#[test]
fn a_nested_comp_shows_nothing_past_its_end() {
for collapse in [false, true] {
let mut p = Project::default();
let outer = Comp::new(100, 100, FrameRate::FPS_30, s(4.0));
// A 1 s comp with a solid covering it for 4 s (longer than the comp itself).
let mut inner = Comp::new(100, 100, FrameRate::FPS_30, s(1.0));
let sid = p.add_item("S", Label::Red, None, ItemKind::Solid(Solid { color: [1.0, 1.0, 1.0], width: 100, height: 100, pixel_aspect: 1.0 }));
let mut l = build::layer(&mut p, &inner, "S", LayerSource::Solid { item: sid }, (100, 100), None);
l.out_point = s(4.0);
inner.layers.push(l);
let iid = p.add_item("Inner", Label::Sandstone, None, ItemKind::Comp(inner.into()));
let cid = p.add_item("Outer", Label::Sandstone, None, ItemKind::Comp(outer.clone().into()));
// The precomp layer is trimmed out to 3 s, past the nested comp's end.
let mut pre = build::layer(&mut p, &outer, "Inner", LayerSource::Comp { item: iid }, (100, 100), None);
pre.out_point = s(3.0);
pre.switches.collapse = collapse;
p.comp_mut(cid).unwrap().layers.push(pre);
assert!(render_frame(&p, cid, s(0.5), 1.0).get(50, 50)[3] > 0.99, "collapse {collapse}: inside its span");
assert!(render_frame(&p, cid, s(2.0), 1.0).get(50, 50)[3] < 0.01, "collapse {collapse}: past its end");
}
}