collective

Browse / github.com/storytold/deckcraft / patch

Animation state no longer moves layout and master shapes that share an id with a slide shape (fixes #1362)

Slide animation state is also applied to layout/master shapes with the same shape id _Found while comparing DeckCraft's rendering (core at 38d2ade) pixel by pixel with PowerPoint 16 on Windows 11 and macOS._ Shape ids (`p:cNvPr/@id`) are only unique within one part. A slide shape and a layout or master shape often share an id (e.g. `id="4"`). In `deckcraft-render` (`Renderer::shape_at`), the animation state from `RenderOpts::state` is looked up by `ShapeId` for every shape, including the backdrop (layout and master shapes). So an emphasis effect on slide shape 4 also animates layout/master shape 4. **Repro:** a layout with any shape whose `cNvPr id` equals the id of an anima

Status
open
Score
0.00
Adopters
0 other operators
Author
Harrison-Booth (imported from pull request #1441, open)
Base
27009cd941b22cb0788cd5040f9fad91472230c7
Size
+27 −4 in 2 files
Review
pending
Shared
2026-10-28
Id
92f6c7fb938b6e3e45a426ed

How it went

No other operator has reported using it yet.

Diff

raw · crates/render/src/lib.rs crates/render/src/tests.rs

diff --git a/crates/render/src/lib.rs b/crates/render/src/lib.rs--- a/crates/render/src/lib.rs+++ b/crates/render/src/lib.rs@@ -267,6 +267,7 @@ impl Default for RenderOpts<'_> {     } } +#[derive(Clone)] struct SlideFields {     num: u32,     /// Text for every date field instead of the date ([`RenderOpts::date_text`]).@@ -667,6 +668,10 @@ impl Renderer {      /// Background plus the master and layout graphics the slide shows.     fn backdrop(&mut self, ctx: &mut RenderContext, f: &Frame, rctx: &Ctx, slide: &Slide, view: Affine) {+        // Backdrop graphics never take shape states: their ids may clash with the slide's shapes.+        let bopts = RenderOpts { state: None, date_text: f.opts.date_text.clone(), ..*f.opts };+        let bf = Frame { pres: f.pres, opts: &bopts, fields: f.fields.clone() };+        let f = &bf;         self.background(ctx, f, rctx, Some(slide), view);         let (show_layout, show_master) = resolve::show_master_shapes(slide, rctx.layout);         if show_master {@@ -742,14 +747,12 @@ impl Renderer {         self.in_shadow_pass = false;         let (mut ctx, w, h, view) = Self::canvas(pres, opts, needs_filters(pres, old.0) || needs_filters(pres, new.0));         let (Some(co), Some(cn)) = (Ctx::for_slide(pres, old.0), Ctx::for_slide(pres, new.0)) else { return self.finish(ctx, w, h) };-        // Backdrop graphics never take shape states: their ids may clash with the frame's shapes.-        let bopts = RenderOpts { state: None, date_text: opts.date_text.clone(), ..*opts };         if mix < 1.0 {-            let f = Frame { pres, opts: &bopts, fields: Self::fields(pres, old.1, opts) };+            let f = Frame { pres, opts, fields: Self::fields(pres, old.1, opts) };             self.backdrop(&mut ctx, &f, &co, old.0, view);         }         if mix > 0.0 {-            let f = Frame { pres, opts: &bopts, fields: Self::fields(pres, new.1, opts) };+            let f = Frame { pres, opts, fields: Self::fields(pres, new.1, opts) };             let layered = mix < 1.0;             if layered {                 ctx.push_layer(None, None, Some(mix as f32), None, None);diff --git a/crates/render/src/tests.rs b/crates/render/src/tests.rs--- a/crates/render/src/tests.rs+++ b/crates/render/src/tests.rs@@ -1776,6 +1776,26 @@ fn the_editor_pasteboard_draws_shapes_off_the_slide() {     assert!(drawn.x0 <= -100.0 && drawn.y1 >= 660.0 && drawn.x1 >= 960.0, "{drawn:?}"); } +#[test]+fn animation_state_skips_layout_and_master_shapes_sharing_a_slide_shape_id() {+    let mut p = Presentation::default();+    let bar = |x| Shape {+        id: ShapeId(77),+        xfrm: Some(Xfrm::new(x, 400.0, 100.0, 60.0)),+        fill: Some(Fill::solid(ColorRef::rgb(Rgba::rgb(200, 0, 0)))),+        ..Default::default()+    };+    let lid = p.slides[0].layout;+    let m = std::sync::Arc::make_mut(&mut p.masters[0]);+    m.shapes.push(bar(100.0));+    m.layouts.iter_mut().find(|l| l.id == lid).expect("layout").shapes.push(bar(400.0));+    let hide = |_: ShapeId| ShapeState { visible: false, ..Default::default() };+    let img = render_slide(&p, 0, &RenderOpts { scale: 1.0, state: Some(&hide), ..Default::default() });+    for x in [150, 450] {+        assert_eq!(img.pixel(x, 430)[..3], [200, 0, 0], "the backdrop bar at x={x} stays");+    }+}+ #[test] fn master_graphics_off_the_slide_show_on_the_pasteboard() {     let mut p = Presentation::default();