mirror of
https://github.com/storytold/deckcraft.git
synced 2026-10-24 02:27:08 -06:00
Merge pull request #1134 from pick2510/fix/3d-in-scaled-groups
This commit is contained in:
@@ -141,6 +141,7 @@ app's JSON control channel (`deckcraft --control PORT`).
|
||||
| 2026-10-23 | A shape with no fill and no line (a text box) no longer draws its shape-level glow as a halo round its box; the text's own glow is all PowerPoint shows |
|
||||
| 2026-10-23 | Normal view: a slide zoomed past the window gets a horizontal and a vertical scroll bar on the axes that overflow; drag the thumb or click the track to reach every edge (#1157) |
|
||||
| 2026-10-22 | Shape Fill and Shape Outline format the members of a selected group, including nested groups, in one undo step. Fill transparency retains each member's effective colour (#1079) |
|
||||
| 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-18 | Slide thumbnails and the Slide Sorter render only the slides in view: on a deck of more than 600 slides they used to evict their own thumbnails, so the pane flashed white and never finished (#969) |
|
||||
| 2026-10-18 | Resizing with the mouse snaps the edge being dragged to the slide, guides and other shapes: a fixed edge that already lined up used to take the snap and leave the dragged edge free, and a fixed edge near a line shifted the dragged one (#961) |
|
||||
@@ -266,6 +267,7 @@ app's JSON control channel (`deckcraft --control PORT`).
|
||||
| 2026-10-22 | trivial | Progress log: table text takes the master's other-text style |
|
||||
| 2026-10-16 | trivial | Progress log: responsive mouse-wheel navigation on the fitted canvas |
|
||||
| 2026-10-21 | trivial | Progress log: theme `spDef` anchor no longer applied to a file's own shapes |
|
||||
| 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 |
|
||||
|
||||
@@ -843,6 +843,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.
|
||||
@@ -1049,6 +1058,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)
|
||||
@@ -1062,7 +1082,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 * 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).
|
||||
if let Some(raw) = effects.as_ref().and_then(|e| e.raw3d.as_deref())
|
||||
@@ -1073,7 +1094,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 * pt, col, &light::Lighting::from_raw(raw));
|
||||
}
|
||||
// Fill.
|
||||
match &s.kind {
|
||||
@@ -1168,6 +1189,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 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,
|
||||
|
||||
@@ -2281,6 +2281,81 @@ 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 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#"<a:scene3d><a:camera prst="isometricOffAxis1Right"/><a:lightRig rig="threePt" dir="t"/></a:scene3d><a:sp3d extrusionH="254000" contourW="38100"><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 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.
|
||||
|
||||
Reference in New Issue
Block a user