diff --git a/crates/render/src/lib.rs b/crates/render/src/lib.rs index f88d492a..7c78c411 100644 --- a/crates/render/src/lib.rs +++ b/crates/render/src/lib.rs @@ -1056,6 +1056,17 @@ impl Renderer { ctx.push_layer(None, None, None, None, None); let _ = r; } + // Shape-space units per point for the 3-D sizes, measured without the camera's turn (which + // `shape_at` puts in `m`): its foreshortening is not a child-coordinate scale. + let pt = { + let turn = effects + .as_ref() + .and_then(|e| e.raw3d.as_deref()) + .and_then(camera::from_raw) + .and_then(|c| camera::affine(&c, w, h)) + .filter(|t| t.determinant().abs() > 1e-9); + self.per_point(turn.map_or(m, |t| m * t.inverse()), depth) + }; // A 3-D extrusion under a turned camera: the sides, swept behind the face. if let Some(raw) = effects.as_ref().and_then(|e| e.raw3d.as_deref()) && let Some(ex) = extrude::from_raw(raw) @@ -1069,7 +1080,7 @@ impl Renderer { }; let pre = m * turn.inverse(); let lit = light::Lighting::from_raw(raw); - let d = ex.depth * self.per_point(m, depth); + let d = ex.depth * pt; extrude::draw(ctx, &outline, w, h, &cam, d, pre, turn, extrude::side_color(rctx, &ex, face), &lit); } // A 3-D contour: a band around the outline, behind the face (and in front of any sides). @@ -1081,7 +1092,7 @@ impl Renderer { _ => None, }; let col = c.color.as_ref().map(|k| rctx.color(k, face)).or(face).unwrap_or(Rgba::rgb(0xff, 0xff, 0xff)); - extrude::draw_contour(ctx, &outline, m, c.width * self.per_point(m, depth), col, &light::Lighting::from_raw(raw)); + extrude::draw_contour(ctx, &outline, m, c.width * pt, col, &light::Lighting::from_raw(raw)); } // Fill. match &s.kind { @@ -1176,8 +1187,7 @@ impl Renderer { }; // A 3-D bevel: the face and a band inside the outline lit as surfaces. if let Some(bv) = effects.as_ref().and_then(|e| e.raw3d.as_deref()).and_then(bevel::from_raw) { - let k = self.per_point(m, depth); - let bv = bevel::Bevel { w: bv.w * k, h: bv.h * k, ..bv }; + let bv = bevel::Bevel { w: bv.w * pt, h: bv.h * pt, ..bv }; let solid = match &fill { Some(Fill::Solid { color: c }) => Some(rctx.color(c, fill_ph)), _ => None, diff --git a/crates/render/src/tests.rs b/crates/render/src/tests.rs index d7122f05..348b89c3 100644 --- a/crates/render/src/tests.rs +++ b/crates/render/src/tests.rs @@ -2310,6 +2310,36 @@ fn group_child_scale_does_not_scale_3d_depth_contours_or_bevels() { assert!(d * 20 < drawn, "{d} differing values against {drawn} inked pixels"); } +#[test] +fn a_turned_camera_in_a_one_to_one_group_draws_its_3d_sizes_as_outside_one() { + // The camera's turn is part of the shape's transform; its foreshortening is not a group scale, + // so a 1:1 group must not change extrusion depths, contours or bevels under a turned camera. + let mut deck = extruded_deck(); + let bevelled = Shape { + id: ShapeId(705), + xfrm: Some(Xfrm::new(40.0, 340.0, 170.0, 130.0)), + geom: Geom::preset("rect"), + style: Some(ShapeStyle::accent(deckcraft_color::SchemeSlot::Accent1)), + effects: Some(deckcraft_model::Effects { + raw3d: Some(r#""#.into()), + ..Default::default() + }), + ..Default::default() + }; + std::sync::Arc::make_mut(&mut deck.slides[0]).shapes.push(bevelled); + let opts = RenderOpts { scale: 1.0, ..Default::default() }; + let alone = render_slide(&deck, 0, &opts); + let group = Shape { + id: ShapeId(10), + xfrm: Some(Xfrm::new(0.0, 0.0, 960.0, 540.0)), + kind: ShapeKind::Group { children: deck.slides[0].shapes.clone(), child: Xfrm::new(0.0, 0.0, 960.0, 540.0) }, + ..Default::default() + }; + let grouped = render_slide(&deck_with(vec![group]), 0, &opts); + let d = differing(&alone, &grouped); + assert!(d < 200, "{d} values differ between the shapes alone and in a 1:1 group"); +} + #[test] fn table_rows_below_the_slide_are_not_laid_out_and_the_slide_looks_the_same() { // A fuzzed deck's 200 x 200 table of long text took 70 s to draw, most of it off the slide.