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>
This commit is contained in:
bflatastic 2026-10-05 01:47:54 +07:00
parent 0fe6db00e8
commit 0b5cff1743
3 changed files with 39 additions and 3 deletions

View File

@ -374,6 +374,11 @@ impl Comp {
essential: None,
}
}
/// Whether the composition has a frame at its time `t`: nested, it shows nothing before it
/// starts or after it ends (a precomp layer trimmed or remapped past it).
pub fn covers(&self, t: Tick) -> bool {
t >= Tick::ZERO && t < self.duration
}
/// A new, empty composition with this one's settings (frame rate, pixel aspect, background,
/// motion blur, frame blending and the 3D renderer), as Pre-compose makes.
pub fn nested_like(&self, width: u32, height: u32, duration: Tick) -> Comp {

View File

@ -558,7 +558,11 @@ impl<'a> Renderer<'a> {
/// 3D layers use this comp's camera and lights.
pub fn collapse_into(&self, ctx: &EvalCtx<'a>, layer: &Layer, item: ItemId, opacity: f32) -> Option<(Renderer<'a>, EvalCtx<'a>)> {
let nc = self.project.comp(item)?;
let nctx = EvalCtx { comp_id: item, comp: nc, time: ctx.nested_time(layer), ..*ctx };
let t = ctx.nested_time(layer);
if !nc.covers(t) {
return None;
}
let nctx = EvalCtx { comp_id: item, comp: nc, time: t, ..*ctx };
let (l2c, _) = ctx.layer_to_comp(layer);
let outer = Some(self.outer.map_or(l2c, |o| o * l2c));
let world = ctx.world_matrix(layer);
@ -925,10 +929,15 @@ impl<'a> Renderer<'a> {
return None;
}
// A composition proxy (a rendered still or movie) stands in for the comp.
// Nothing before the nested comp starts or after it ends.
let lt = ctx.nested_time(layer);
if !self.project.comp(*item).is_some_and(|nc| nc.covers(lt)) {
return None;
}
if let Some(pf) = self.proxy_for(*item)
&& let Some(nc) = self.project.comp(*item)
{
let img = self.footage.frame(*item, pf, ctx.nested_time(layer))?;
let img = self.footage.frame(*item, pf, lt)?;
return Some(self.footage_buf(&img, pf, nc.width, nc.height));
}
// Essential Properties overrides render the nested comp with this instance's
@ -945,7 +954,6 @@ impl<'a> Renderer<'a> {
Some(p) => Renderer { project: p, ..base },
None => base,
};
let lt = ctx.nested_time(layer);
// Frame blending between the nested comp's frames (time-stretched/remapped).
let mode = frame_blend_mode(ctx, layer);
if mode != FrameBlend::Off

View File

@ -65,3 +65,26 @@ fn a_comp_preserving_its_resolution_renders_full_size_and_lands_in_place() {
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");
}
}