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.