From 061e2c8ad5a10bfbffb32a96a2ca5d8cfabbf581 Mon Sep 17 00:00:00 2001 From: Brandon Thomas Date: Tue, 13 Oct 2026 05:14:32 -0400 Subject: [PATCH] Render: shapes filled with their group's fill (a:grpFill) show it A shape inside a group whose fill is grpFill drew nothing (the fill was read but the renderer skipped it). PowerPoint fills it with the group's fill laid out over the whole group: a gradient or pattern runs on across the children rather than restarting in each. Groups without a fill of their own pass their parent's on. Wild synthetic set: group fills slide 27.33 -> 7.88. Co-Authored-By: Claude Opus 5.5 (1M context) --- crates/render/src/lib.rs | 21 ++++++++++++++++++++- crates/render/src/tests_wild.rs | 26 ++++++++++++++++++++++++++ 2 files changed, 46 insertions(+), 1 deletion(-) diff --git a/crates/render/src/lib.rs b/crates/render/src/lib.rs index ae2598e..3f2dd43 100644 --- a/crates/render/src/lib.rs +++ b/crates/render/src/lib.rs @@ -244,11 +244,14 @@ pub struct Renderer { /// The transform a top-level shape is drawn under (slide to pixels); a group's children draw /// under it combined with the group's child-coordinate mapping, which must not scale lines. view: Affine, + /// The fill of the group being drawn, with the transform and box it is laid out in: children + /// filled with the group's fill (`a:grpFill`) show it, as one fill across the whole group. + group_fill: Option<(Fill, Affine, Rect)>, } impl Default for Renderer { fn default() -> Self { - Renderer { resources: Resources::new(), paths: Vec::new(), bg: None, drawn: None, view: Affine::IDENTITY } + Renderer { resources: Resources::new(), paths: Vec::new(), bg: None, drawn: None, view: Affine::IDENTITY, group_fill: None } } } @@ -779,9 +782,16 @@ impl Renderer { ShapeKind::Group { children, child } => { let ch = Rect::new(child.x, child.y, child.x + child.w, child.y + child.h); let gm = parent * deckcraft_geom::group_child_affine(&x, ch); + // A group's own fill (a group filled with its parent's passes that on). + let own = match &s.fill { + Some(Fill::Group) | None => self.group_fill.clone(), + Some(fl) => Some((fl.clone(), m, Rect::new(0.0, 0.0, x.w, x.h))), + }; + let outer = std::mem::replace(&mut self.group_fill, own); for c in children { self.shape(ctx, f, rctx, c, gm, depth + 1); } + self.group_fill = outer; } ShapeKind::Table(t) => { // Rows grow with their text: what the cells cover, borders included. @@ -925,6 +935,15 @@ impl Renderer { let path = (view.inverse() * m) * sp.path.clone(); paint::fill_path(ctx, rctx, bg, *ph, &path, slide, *view, sp.fill, fade(i).fill, f.pres); } + // `grpFill`: the group's fill, laid out over the whole group. + (Fill::Group, _) => { + if let Some((gf, gm, gbox)) = &self.group_fill + && gm.determinant().abs() > 1e-12 + { + let path = (gm.inverse() * m) * sp.path.clone(); + paint::fill_path(ctx, rctx, gf, None, &path, *gbox, *gm, sp.fill, fade(i).fill, f.pres); + } + } _ => paint::fill_path(ctx, rctx, fl, fill_ph, &sp.path, Rect::new(0.0, 0.0, w, h), m, sp.fill, fade(i).fill, f.pres), } ctx.set_fill_rule(peniko::Fill::NonZero); diff --git a/crates/render/src/tests_wild.rs b/crates/render/src/tests_wild.rs index a58bf3a..a43a571 100644 --- a/crates/render/src/tests_wild.rs +++ b/crates/render/src/tests_wild.rs @@ -287,3 +287,29 @@ fn a_pictures_border_lies_outside_the_picture() { let img = render_slide(&picture_deck(Some(line)), 0, &RenderOpts { scale: 1.0, ..Default::default() }); assert!(close(img.pixel(120, 96), RED, 30) && close(img.pixel(140, 96), WHITE, 10), "{:?}", img.pixel(140, 96)); } + +#[test] +fn children_filled_with_the_group_fill_show_one_fill_across_the_group() { + use deckcraft_model::style::{Gradient, GradientShape, GradientStop}; + let stops = vec![GradientStop { pos: 0.0, color: ColorRef::rgb(RED) }, GradientStop { pos: 1.0, color: ColorRef::rgb(Rgba::rgb(0, 0, 255)) }]; + let grad = Fill::Gradient(Gradient { stops, shape: GradientShape::Linear { angle: 0.0, scaled: false }, rotate_with_shape: true }); + let child = |id: u32, x: f64| Shape { + id: ShapeId(id), + xfrm: Some(Xfrm::new(x, 0.0, 90.0, 100.0)), + fill: Some(Fill::Group), + line: no_line(), + ..Default::default() + }; + let g = Shape { + id: ShapeId(16), + xfrm: Some(Xfrm::new(100.0, 100.0, 200.0, 100.0)), + fill: Some(grad), + kind: ShapeKind::Group { children: vec![child(17, 0.0), child(18, 110.0)], child: Xfrm::new(0.0, 0.0, 200.0, 100.0) }, + ..Default::default() + }; + let img = render(vec![g]); + let (l, r) = (img.pixel(105, 150), img.pixel(295, 150)); + assert!(l[0] > 200 && l[2] < 60, "left end of the group's gradient {l:?}"); + assert!(r[2] > 200 && r[0] < 60, "right end {r:?}"); + assert!(close(img.pixel(200, 150), WHITE, 5), "the gap between the children stays empty"); +}