From 98b6d04413e1c556b376cb2ffd6331ae0ae2b5fd Mon Sep 17 00:00:00 2001 From: NetterSilver <133535268+Silvertree2010@users.noreply.github.com> Date: Tue, 20 Oct 2026 22:50:30 +0200 Subject: [PATCH] Keep rounded corners round in non-uniformly scaled groups Fixes #1083 Co-Authored-By: Claude Haiku 5.5 Co-Authored-By: Claude Opus 5.5 --- crates/render/src/lib.rs | 23 ++++++++++++++++++++++- crates/render/src/tests.rs | 31 +++++++++++++++++++++++++++++++ 2 files changed, 53 insertions(+), 1 deletion(-) diff --git a/crates/render/src/lib.rs b/crates/render/src/lib.rs index cf124a9f..ecdc42e7 100644 --- a/crates/render/src/lib.rs +++ b/crates/render/src/lib.rs @@ -851,6 +851,27 @@ impl Renderer { (tw, th, m * Affine::scale_non_uniform(1.0 / sx, 1.0 / sy)) } + /// A shape's geometry in its own `w` × `h` space, built at its size on the slide: inside a + /// group whose child coordinates scale the shape unevenly, the preset's rounded corners and + /// other size-derived features stay as they would be on a plain shape of that size (#1083). + fn scaled_geometry(&self, s: &Shape, m: Affine, depth: usize, w: f64, h: f64) -> Geometry { + let (tw, th, _) = self.text_space(m, depth, w, h); + if (tw, th) == (w, h) { + return shape_geometry(s, w, h); + } + let mut geo = shape_geometry(s, tw, th); + let k = |a: f64, b: f64| if b.abs() > 1e-12 { a / b } else { 1.0 }; + let back = Affine::scale_non_uniform(k(w, tw), k(h, th)); + for p in &mut geo.paths { + p.path.apply_affine(back); + } + geo.text_rect = back.transform_rect_bbox(geo.text_rect); + for p in &mut geo.sites { + *p = back * *p; + } + geo + } + fn shape_at(&mut self, ctx: &mut RenderContext, f: &Frame, rctx: &Ctx, s: &Shape, parent: Affine, depth: usize) { if depth > 32 || (s.hidden && !f.opts.show_hidden) || f.opts.skip.contains(&s.id) { return; @@ -1007,7 +1028,7 @@ impl Renderer { ) { let (geo, fades) = match self.paths.iter().find(|p| p.id == s.id) { Some(pm) => self.morphs.geometry(pm, s, w, h), - None => (shape_geometry(s, w, h), Vec::new()), + None => (self.scaled_geometry(s, m, depth, w, h), Vec::new()), }; let fade = |i: usize| fades.get(i).copied().unwrap_or(deckcraft_geom::morph::Fade { fill: 1.0, stroke: 1.0 }); let (fill, fill_ph) = resolve::fill(rctx, s); diff --git a/crates/render/src/tests.rs b/crates/render/src/tests.rs index 4ee1c889..406cf0de 100644 --- a/crates/render/src/tests.rs +++ b/crates/render/src/tests.rs @@ -1971,6 +1971,37 @@ fn text_in_a_scaled_group_keeps_its_point_size() { } } +#[test] +fn rounded_corners_in_a_stretched_group_stay_round() { + // #1083: a group that stretches its child space 4x across makes a rounded rectangle wider on + // the slide, but its corners stay circular, like a plain rounded rectangle of that size. + let round = |id: u32, w: f64, h: f64| Shape { + id: ShapeId(id), + xfrm: Some(Xfrm::new(0.0, 0.0, w, h)), + geom: Geom::Preset { name: "roundRect".into(), adj: vec![16667.0] }, + fill: Some(Fill::solid(ColorRef::rgb(deckcraft_color::Rgba::BLACK))), + line: Some(deckcraft_model::style::Line { fill: Some(Fill::None), ..Default::default() }), + ..Default::default() + }; + let render = |sh: Shape| { + let mut p = Presentation::default(); + std::sync::Arc::make_mut(&mut p.slides[0]).shapes = vec![sh]; + render_slide(&p, 0, &RenderOpts { scale: 1.0, ..Default::default() }) + }; + let mut plain = round(700, 400.0, 100.0); + plain.xfrm = Some(Xfrm::new(100.0, 100.0, 400.0, 100.0)); + let plain = render(plain); + let grouped = render(Shape { + id: ShapeId(701), + xfrm: Some(Xfrm::new(100.0, 100.0, 400.0, 100.0)), + kind: ShapeKind::Group { children: vec![round(702, 100.0, 100.0)], child: Xfrm::new(0.0, 0.0, 100.0, 100.0) }, + ..Default::default() + }); + for (x, y) in [(120, 102), (102, 120), (480, 102), (498, 180), (120, 198)] { + assert_eq!(plain.pixel(x, y), grouped.pixel(x, y), "pixel ({x},{y}) of a roundRect in a stretched group"); + } +} + #[test] fn a_fill_overlay_blends_over_the_fill() { // `a:fillOverlay blend="mult"`: a 50% grey multiplied over a light fill darkens it; "screen"