mirror of
https://github.com/storytold/deckcraft.git
synced 2026-11-03 00:41:28 -06:00
fix(render): size a group's own tile and a picture shape's own tile in points too
Review of #1693 found two more places that drew a tiled picture fill in the shape's own (child-space) units inside a scaled group. A group nested in a scaled group stored its own tiled fill unconverted, so the members that take it (grpFill) got oversized tiles. The owning group now converts the fill with per_point_xy when it stores it, so inheriting members, directly or through a chain of grpFill groups, get the converted tile. A picture shape (p:pic) whose blip fill is a tile passed its own fill straight to paint::picture. It now goes through paint::tiled_picture_per_point, which tiled_per_point also uses. Tests: a group's tile passed to its members and a picture shape's own tile, each in outer groups scaled 1, 0.25, 1600 and 2x0.5; both fail without the change. Docs: the Picture and texture fill checklist row is re-assessed (remains P) with its status line and revision history, and the ROADMAP progress log row mentions both cases. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5.5
parent
62ada3ab9a
commit
9e59b5f223
+1
-1
@@ -148,7 +148,7 @@ app's JSON control channel (`deckcraft --control PORT`).
|
||||
|
||||
| Date | What landed |
|
||||
|---|---|
|
||||
| 2026-11-02 | Tiled picture fills in groups: a tile (`a:blipFill` with `a:tile`) keeps its size, offset and anchor in slide points whatever the group's child-coordinate scale, uniform or stretched unevenly, nested or flipped, and tiles with `rotWithShape="0"` keep their size in points at any zoom, as PowerPoint draws them (measured on its own PNG export of a synthetic probe deck: every grouped case within ~3/255 of the ungrouped control's own difference from PowerPoint, which is ~13/255 from resampling). Before, a group whose child space was ~1800 times smaller than its box showed one flat texel of the texture (a Venn diagram with paper-textured circles and a hatched intersection) |
|
||||
| 2026-11-02 | Tiled picture fills in groups: a tile (`a:blipFill` with `a:tile`) keeps its size, offset and anchor in slide points whatever the group's child-coordinate scale, uniform or stretched unevenly, nested or flipped (also a group's own tile fill passed to its members, and a picture shape's own tile), and tiles with `rotWithShape="0"` keep their size in points at any zoom, as PowerPoint draws them (measured on its own PNG export of a synthetic probe deck: every grouped case within ~3/255 of the ungrouped control's own difference from PowerPoint, which is ~13/255 from resampling). Before, a group whose child space was ~1800 times smaller than its box showed one flat texel of the texture (a Venn diagram with paper-textured circles and a hatched intersection) |
|
||||
| 2026-10-31 | No Fill shape interiors allow selection and editing of underlying objects, even when a quick style remains; hit-testing uses the renderer’s fill resolution. Outlines, rotation, text targets and actual fills keep their hit behaviour (#1621) |
|
||||
| 2026-10-30 | Print-preview tests use the URL emitted by egui and its exact file path; previews are explicitly removed before the macOS process exits |
|
||||
| 2026-10-30 | `slide.actions` lists shape click and hover actions and text-run hyperlinks (including table cells and grouped shapes) for audit; listing never runs an action. The UI pane is a follow-up |
|
||||
|
||||
@@ -274,13 +274,21 @@ pub fn picture(ctx: &mut RenderContext, pres: &Presentation, pf: &PictureFill, b
|
||||
/// returned as it is.
|
||||
pub fn tiled_per_point(fill: &Fill, per_point: (f64, f64)) -> std::borrow::Cow<'_, Fill> {
|
||||
let Fill::Picture(pf) = fill else { return std::borrow::Cow::Borrowed(fill) };
|
||||
let PictureMode::Tile { tx, ty, sx, sy, flip, align } = &pf.mode else { return std::borrow::Cow::Borrowed(fill) };
|
||||
match tiled_picture_per_point(pf, per_point) {
|
||||
std::borrow::Cow::Borrowed(_) => std::borrow::Cow::Borrowed(fill),
|
||||
std::borrow::Cow::Owned(p) => std::borrow::Cow::Owned(Fill::Picture(p)),
|
||||
}
|
||||
}
|
||||
|
||||
/// [`tiled_per_point`] for a picture fill itself (a picture shape's own fill is not a [`Fill`]).
|
||||
pub fn tiled_picture_per_point(pf: &PictureFill, per_point: (f64, f64)) -> std::borrow::Cow<'_, PictureFill> {
|
||||
let PictureMode::Tile { tx, ty, sx, sy, flip, align } = &pf.mode else { return std::borrow::Cow::Borrowed(pf) };
|
||||
let (kx, ky) = per_point;
|
||||
if !(kx.is_finite() && ky.is_finite() && kx > 1e-12 && ky > 1e-12) || ((kx - 1.0).abs() < 1e-9 && (ky - 1.0).abs() < 1e-9) {
|
||||
return std::borrow::Cow::Borrowed(fill);
|
||||
return std::borrow::Cow::Borrowed(pf);
|
||||
}
|
||||
let mode = PictureMode::Tile { tx: tx * kx, ty: ty * ky, sx: sx * kx, sy: sy * ky, flip: flip.clone(), align: align.clone() };
|
||||
std::borrow::Cow::Owned(Fill::Picture(PictureFill { mode, ..pf.clone() }))
|
||||
std::borrow::Cow::Owned(PictureFill { mode, ..pf.clone() })
|
||||
}
|
||||
|
||||
/// Where the first tile of a tiled picture fill goes in `bounds`: its corner, edge middle or
|
||||
|
||||
@@ -436,7 +436,8 @@ impl Renderer {
|
||||
// A group's own fill (a group filled with its parent's passes that on).
|
||||
let own = match &s.fill {
|
||||
Some(Fill::Group) | None => self.group_fill.clone(),
|
||||
Some(fl) => Some((fl.clone(), m, Rect::new(0.0, 0.0, x.w, x.h))),
|
||||
// A tile is given in points; this group's own space may be scaled by its parents.
|
||||
Some(fl) => Some((paint::tiled_per_point(fl, self.per_point_xy(m, depth)).into_owned(), m, Rect::new(0.0, 0.0, x.w, x.h))),
|
||||
};
|
||||
let outer = std::mem::replace(&mut self.group_fill, own);
|
||||
// A group's outer shadow is cast by everything in it, as PowerPoint draws it: the
|
||||
@@ -763,7 +764,8 @@ impl Renderer {
|
||||
let bounds = Rect::new(0.0, 0.0, w, h);
|
||||
ctx.push_clip_layer(outline);
|
||||
let t = f.opts.gif_times.iter().find(|g| g.0 == s.id).map(|g| g.1);
|
||||
paint::picture(ctx, f.pres, &paint::resolved(rctx, pf, None), bounds, m, t);
|
||||
let pf = paint::tiled_picture_per_point(pf, pt_xy);
|
||||
paint::picture(ctx, f.pres, &paint::resolved(rctx, &pf, None), bounds, m, t);
|
||||
ctx.pop_layer();
|
||||
}
|
||||
// An object DeckCraft doesn't model (an embedded OLE object, an unknown graphic): its
|
||||
|
||||
@@ -506,6 +506,68 @@ fn a_tile_keeps_its_size_in_a_group_with_scaled_child_coordinates() {
|
||||
}
|
||||
}
|
||||
|
||||
/// A slide with `shape` (built for a box of `w` x `h` child units) in a group at (100, 100) that is
|
||||
/// 240 x 60 pt on the slide and scaled `kx` x `ky` against its child space, drawn at 1 px per point.
|
||||
fn in_a_scaled_group(kx: f64, ky: f64, p: Presentation, shape: impl FnOnce(f64, f64) -> Shape) -> Image {
|
||||
let (w, h) = (240.0 / kx, 60.0 / ky);
|
||||
let group = Shape {
|
||||
id: ShapeId(21),
|
||||
xfrm: Some(Xfrm::new(100.0, 100.0, 240.0, 60.0)),
|
||||
kind: ShapeKind::Group { children: vec![shape(w, h)], child: Xfrm::new(0.0, 0.0, w, h) },
|
||||
..Default::default()
|
||||
};
|
||||
let mut p = p;
|
||||
std::sync::Arc::make_mut(&mut p.slides[0]).shapes = vec![group];
|
||||
render_slide(&p, 0, &RenderOpts { scale: 1.0, ..Default::default() })
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_tile_a_group_passes_to_its_members_keeps_its_size_inside_a_scaled_group() {
|
||||
use deckcraft_model::style::{PictureFill, PictureMode};
|
||||
// The group that owns the tiled fill sits inside another group whose child space is scaled: the
|
||||
// members that take it (`grpFill`) get tiles of 60 x 30 pt, as with no outer group.
|
||||
let blue = Rgba::rgb(0, 0, 255);
|
||||
for (kx, ky) in [(1.0, 1.0), (0.25, 0.25), (1600.0, 1600.0), (2.0, 0.5)] {
|
||||
let mut p = Presentation::default();
|
||||
let media = p.add_media("t.png", "image/png", half_red_half_blue()).expect("media");
|
||||
let mode = PictureMode::Tile { tx: 0.0, ty: 0.0, sx: 10.0, sy: 10.0, flip: "none".into(), align: "tl".into() };
|
||||
let img = in_a_scaled_group(kx, ky, p, |w, h| {
|
||||
let member =
|
||||
Shape { id: ShapeId(23), xfrm: Some(Xfrm::new(0.0, 0.0, w, h)), fill: Some(Fill::Group), line: no_line(), ..Default::default() };
|
||||
Shape {
|
||||
id: ShapeId(22),
|
||||
xfrm: Some(Xfrm::new(0.0, 0.0, w, h)),
|
||||
fill: Some(Fill::Picture(PictureFill { media, mode, ..Default::default() })),
|
||||
kind: ShapeKind::Group { children: vec![member], child: Xfrm::new(0.0, 0.0, w, h) },
|
||||
..Default::default()
|
||||
}
|
||||
});
|
||||
let (a, b, c) = (img.pixel(115, 115), img.pixel(145, 115), img.pixel(175, 115));
|
||||
assert!(close(a, RED, 30) && close(b, blue, 30) && close(c, RED, 30), "outer group {kx} x {ky}: {a:?} {b:?} {c:?}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_picture_shapes_own_tile_keeps_its_size_inside_a_scaled_group() {
|
||||
use deckcraft_model::style::{PictureFill, PictureMode};
|
||||
// A picture (`p:pic`) whose blip fill is a tile rather than a stretch.
|
||||
let blue = Rgba::rgb(0, 0, 255);
|
||||
for (kx, ky) in [(1.0, 1.0), (0.25, 0.25), (1600.0, 1600.0), (2.0, 0.5)] {
|
||||
let mut p = Presentation::default();
|
||||
let media = p.add_media("t.png", "image/png", half_red_half_blue()).expect("media");
|
||||
let mode = PictureMode::Tile { tx: 0.0, ty: 0.0, sx: 10.0, sy: 10.0, flip: "none".into(), align: "tl".into() };
|
||||
let img = in_a_scaled_group(kx, ky, p, |w, h| Shape {
|
||||
id: ShapeId(22),
|
||||
xfrm: Some(Xfrm::new(0.0, 0.0, w, h)),
|
||||
kind: ShapeKind::Picture { fill: PictureFill { media, mode, ..Default::default() } },
|
||||
line: no_line(),
|
||||
..Default::default()
|
||||
});
|
||||
let (a, b, c) = (img.pixel(115, 115), img.pixel(145, 115), img.pixel(175, 115));
|
||||
assert!(close(a, RED, 30) && close(b, blue, 30) && close(c, RED, 30), "group {kx} x {ky}: {a:?} {b:?} {c:?}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_upright_tile_in_a_turned_shape_keeps_its_size_in_points_at_any_zoom() {
|
||||
use deckcraft_model::style::{PictureFill, PictureMode};
|
||||
|
||||
@@ -2,7 +2,9 @@
|
||||
|
||||
> **Generated:** 2026-10-14 (bullets/numbering remain D after empty-line marker review; summary by `cargo xtask parity`; rows re-scored by the 2026-10-14 community PR wave, duplicate Thesaurus, AutoCorrect, Artistic effects and Corrections rows folded; Preview in editor re-scored 2026-10-13; earlier rows re-scored 2026-10-10 against PowerPoint for Mac 16.113.4; video/animated GIF row re-scored 2026-10-11, #171) · **Target:** Microsoft PowerPoint for Mac 16.113 (Microsoft 365)
|
||||
|
||||
> **Last updated:** 2026-11-01 · Locked-shape and no-outline regressions added for #1621; fill row re-assessed: remains D; No Fill hit-testing now follows resolved appearance. Scores unchanged.
|
||||
> **Last updated:** 2026-11-02 · Tiled picture fills keep their size in scaled groups and upright tiles (#1693); Picture and texture fill row re-assessed: remains P, since only how tiles are sized changed. Scores unchanged.
|
||||
>
|
||||
> **Previously (2026-11-01):** Locked-shape and no-outline regressions added for #1621; fill row re-assessed: remains D; No Fill hit-testing now follows resolved appearance. Scores unchanged.
|
||||
|
||||
The summary between `<!-- SUMMARY -->
|
||||
**Weighted breadth parity: 84%** over 191 features.
|
||||
@@ -240,4 +242,5 @@ Open P0 items: Contextual tabs (Shape Format, Picture Format, Table Design/Layou
|
||||
|
||||
| Date | Change | Summary |
|
||||
|---|---|---|
|
||||
| 2026-11-02 | trivial | Tiled picture fills keep their size in scaled groups and upright tiles (#1693); Picture and texture fill row remains P and summary scores are unchanged |
|
||||
| 2026-10-31 | trivial | No Fill interior hit-testing follows resolved fill (#1621); fill row remains D and summary scores are unchanged |
|
||||
|
||||
Reference in New Issue
Block a user