mirror of
https://github.com/storytold/deckcraft.git
synced 2026-10-24 10:37:03 -06:00
Render: 3-D sizes in a scaled group stay in points
A group's child coordinates scale its shapes' boxes, but extrusion depths, contour widths and bevel sizes are points, as PowerPoint draws them. The renderer applied them in the child space, so the group's scale multiplied them: a 1 pt contour on a small extruded cube in a legacy group (1360 child units across a 61 pt group) was drawn hundreds of points wide, a band across the whole slide, and the cube's extrusion ran off towards the vanishing point. per_point() gives the shape-space units per point (the inverse of the child-coordinate scale that line_space already separates for line widths); the extrusion depth, contour width and bevel width and height are converted with it before drawing. Outside a group it is 1. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
5a76518ddd
commit
48dbdffe07
@@ -131,6 +131,7 @@ app's JSON control channel (`deckcraft --control PORT`).
|
||||
|
||||
| Date | What landed |
|
||||
|---|---|
|
||||
| 2026-10-21 | 3-D contours, extrusion depths and bevels keep their size in points inside a group whose child coordinates scale its shapes: a 1 pt contour on a small extruded cube in a legacy group drew hundreds of points wide, a band across the slide |
|
||||
| 2026-10-18 | Native chart sidecar bytes move from JSON arrays into referenced ZIP entries, with shared media byte/item limits and bounded reads; older inline sidecars still load. Synthetic format regressions cover large workbooks and malformed members |
|
||||
| 2026-10-17 | Opening a file caps absurd effect sizes (glow, soft edges, shadows, reflections, on shapes, text and table text) at what Format Shape allows: a glow of 10^18 pt in a .pptx made rendering ask for 34 GB and abort |
|
||||
| 2026-10-17 | Opening a .deckcraft file whose shapes repeat an id gives the repeats fresh ids (groups, layouts and masters too), so the deck validates and edits by id find one shape |
|
||||
@@ -245,6 +246,7 @@ app's JSON control channel (`deckcraft --control PORT`).
|
||||
| 2026-10-15 | trivial | Progress log: PPTX trigger click-step round trip. Feature scores and milestone names unchanged |
|
||||
| 2026-10-16 | trivial | Progress log: PNG export with a transparent background (#784) |
|
||||
| 2026-10-16 | trivial | Progress log: a `.ppsx` from the system starts the slide show (#862) |
|
||||
| 2026-10-21 | trivial | Progress log: 3-D sizes in scaled groups stay in points |
|
||||
| 2026-10-14 | trivial | remote MCP checks reply IDs, envelopes and bounded newline framing (#718) |
|
||||
| 2026-10-14 | minor | Gray list markers on empty paragraphs while editing; caret keeps the list indent before typing |
|
||||
| 2026-10-14 | minor | Community PR wave folded: headline breadth 84% over 191 features (P0 92%, P1 82%, P2 55%) and the feature-area breadths from `cargo xtask parity`; one row per area; Upcoming item 2 merged; per-PR count notes dropped from the progress log. Ready stays ~55%, stage alpha |
|
||||
|
||||
@@ -833,6 +833,15 @@ impl Renderer {
|
||||
if depth > 0 && view.determinant().abs() > 1e-12 && view.is_finite() { (view, view.inverse() * m) } else { (m, Affine::IDENTITY) }
|
||||
}
|
||||
|
||||
/// Shape-space units per point: a group's child coordinates can scale a shape's box (a legacy
|
||||
/// 1360-unit child space makes one unit hundreds of EMUs), but 3-D sizes kept in points
|
||||
/// (contour width, extrusion depth, bevel) stay points, as PowerPoint draws them. 1 outside a
|
||||
/// group.
|
||||
fn per_point(&self, m: Affine, depth: usize) -> f64 {
|
||||
let k = scale_of(self.line_space(m, depth).1);
|
||||
if k.is_finite() && k > 1e-12 { 1.0 / k } else { 1.0 }
|
||||
}
|
||||
|
||||
/// The size and transform a shape's text is set in: inside a group whose child coordinates
|
||||
/// scale the shape, the shape's size in slide points and `m` with that scale taken out, so
|
||||
/// insets and font sizes stay in points; otherwise `w`, `h` and `m` themselves.
|
||||
@@ -1060,7 +1069,8 @@ impl Renderer {
|
||||
};
|
||||
let pre = m * turn.inverse();
|
||||
let lit = light::Lighting::from_raw(raw);
|
||||
extrude::draw(ctx, &outline, w, h, &cam, ex.depth, pre, turn, extrude::side_color(rctx, &ex, face), &lit);
|
||||
let d = ex.depth * self.per_point(m, depth);
|
||||
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).
|
||||
if let Some(raw) = effects.as_ref().and_then(|e| e.raw3d.as_deref())
|
||||
@@ -1071,7 +1081,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, col, &light::Lighting::from_raw(raw));
|
||||
extrude::draw_contour(ctx, &outline, m, c.width * self.per_point(m, depth), col, &light::Lighting::from_raw(raw));
|
||||
}
|
||||
// Fill.
|
||||
match &s.kind {
|
||||
@@ -1166,6 +1176,8 @@ 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 solid = match &fill {
|
||||
Some(Fill::Solid { color: c }) => Some(rctx.color(c, fill_ph)),
|
||||
_ => None,
|
||||
|
||||
@@ -2265,6 +2265,51 @@ fn extrusions_show_their_sides_under_a_turned_camera() {
|
||||
assert_eq!(ink(&flat, 0, 960), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn group_child_scale_does_not_scale_3d_depth_contours_or_bevels() {
|
||||
// A legacy group child space (here 100 points per unit) scales the shapes' boxes, but
|
||||
// extrusion depths, contour widths and bevels are points: a 1 pt contour on a small extruded
|
||||
// cube in such a group was drawn hundreds of points wide, a band across the whole slide.
|
||||
let mut deck = extruded_deck();
|
||||
let bevelled = Shape {
|
||||
id: ShapeId(704),
|
||||
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#"<a:scene3d><a:camera prst="orthographicFront"/><a:lightRig rig="threePt" dir="t"/></a:scene3d><a:sp3d><a:bevelT w="254000" h="254000"/></a:sp3d>"#.into()),
|
||||
..Default::default()
|
||||
}),
|
||||
..Default::default()
|
||||
};
|
||||
std::sync::Arc::make_mut(&mut deck.slides[0]).shapes.push(bevelled);
|
||||
let shapes = deck.slides[0].shapes.clone();
|
||||
let grouped = |k: f64| {
|
||||
let children = shapes
|
||||
.iter()
|
||||
.map(|s| {
|
||||
let x = s.xfrm.unwrap_or_default();
|
||||
Shape { xfrm: Some(Xfrm::new(x.x / k, x.y / k, x.w / k, x.h / k)), ..s.clone() }
|
||||
})
|
||||
.collect();
|
||||
let group = Shape {
|
||||
id: ShapeId(10),
|
||||
xfrm: Some(Xfrm::new(0.0, 0.0, 960.0, 540.0)),
|
||||
kind: ShapeKind::Group { children, child: Xfrm::new(0.0, 0.0, 960.0 / k, 540.0 / k) },
|
||||
..Default::default()
|
||||
};
|
||||
render_slide(&deck_with(vec![group]), 0, &RenderOpts { scale: 1.0, ..Default::default() })
|
||||
};
|
||||
let reference = grouped(1.0);
|
||||
let ink = |img: &Image| img.pixels.as_chunks::<4>().0.iter().filter(|p| **p != [255, 255, 255, 255]).count();
|
||||
let drawn = ink(&reference);
|
||||
assert!(drawn > 20_000, "the reference draws the shapes: {drawn}");
|
||||
let scaled = grouped(100.0);
|
||||
let (n, d) = (ink(&scaled), differing(&reference, &scaled));
|
||||
assert!(n.abs_diff(drawn) * 20 < drawn, "{n} inked pixels against {drawn}");
|
||||
assert!(d * 20 < drawn, "{d} differing values against {drawn} inked pixels");
|
||||
}
|
||||
|
||||
#[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.
|
||||
|
||||
Reference in New Issue
Block a user