From 0b5cff1743f9696957f7ab14ad2d912654a005e2 Mon Sep 17 00:00:00 2001 From: bflatastic Date: Mon, 5 Oct 2026 01:47:54 +0700 Subject: [PATCH] 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 --- crates/project/src/lib.rs | 5 +++++ crates/render/src/lib.rs | 14 +++++++++++--- crates/render/src/tests_nested.rs | 23 +++++++++++++++++++++++ 3 files changed, 39 insertions(+), 3 deletions(-) diff --git a/crates/project/src/lib.rs b/crates/project/src/lib.rs index 0e7f8c27..2771c65e 100644 --- a/crates/project/src/lib.rs +++ b/crates/project/src/lib.rs @@ -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 { diff --git a/crates/render/src/lib.rs b/crates/render/src/lib.rs index 777fae25..540ce9e9 100644 --- a/crates/render/src/lib.rs +++ b/crates/render/src/lib.rs @@ -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 diff --git a/crates/render/src/tests_nested.rs b/crates/render/src/tests_nested.rs index ea0e03e5..6b153d32 100644 --- a/crates/render/src/tests_nested.rs +++ b/crates/render/src/tests_nested.rs @@ -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"); + } +}