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
@@ -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);@@ -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();