fix(compose): key parent-page compositions by absolute page index

Parent-page text now uses the absolute page index as its cache key, so repeated page numbers and renumbering do not reuse another page's composition.

Validation: targeted canvas and PDF regressions, plus all six cargo xtask ci gates on main after #514 (1,179 native tests passed, 3 ignored; 15 crates passed WASM compilation checks).

Task: #523
This commit is contained in:
seanth
2026-10-11 21:27:52 -06:00
committed by GitHub
parent 80c2904681
commit a21e8db14f
5 changed files with 111 additions and 46 deletions
+4 -4
View File
@@ -66,7 +66,7 @@ fn shadow(sigma: f64, color: peniko::Color) -> Filter {
impl Renderer {
/// Draw an item with soft effects: below-effects, the (feathered) body, inner shadow.
pub(crate) fn draw_item_fx(&mut self, ctx: &mut RenderContext, f: &Frame, it: &Item, bp: &BezPath, xf: Affine, page_name: Option<&str>) {
pub(crate) fn draw_item_fx(&mut self, ctx: &mut RenderContext, f: &Frame, it: &Item, bp: &BezPath, xf: Affine, page: Option<usize>) {
let doc = f.doc;
let e = &it.effects;
let sw = if it.stroke.is_none() { 0.0 } else { it.stroke.extent() };
@@ -95,7 +95,7 @@ impl Renderer {
let moved = Frame { view: fr.view * Affine::translate(off), ..*fr };
c.set_transform(moved.view);
c.push_layer(None, None, None, None, Some(filter));
me.draw_body(c, &moved, it, bp, xf, page_name);
me.draw_body(c, &moved, it, bp, xf, page);
if grow > 0.0 && solid {
c.set_transform(moved.view * xf);
c.set_paint(peniko::Color::BLACK);
@@ -119,7 +119,7 @@ impl Renderer {
self.with_filters(ctx, f, reach, w / 4.0, |me, c, fr| {
c.set_transform(fr.view * xf);
c.push_clip_layer(bp);
me.draw_body(c, fr, it, bp, xf, page_name);
me.draw_body(c, fr, it, bp, xf, page);
// Keep the body where the blurred inset silhouette is.
c.set_transform(Affine::IDENTITY);
c.push_layer(None, Some(BlendMode::new(Mix::Normal, Compose::DestIn)), None, None, None);
@@ -139,7 +139,7 @@ impl Renderer {
c.pop_layer();
});
} else {
self.draw_body(ctx, f, it, bp, xf, page_name);
self.draw_body(ctx, f, it, bp, xf, page);
}
// Directional feather: one fading ramp per side, multiplied in.
let df = &e.directional_feather;
+13 -14
View File
@@ -216,7 +216,7 @@ pub struct Renderer {
resources: Resources,
glyphs: text::GlyphCache,
/// Composed stories looked up during the current render.
stories: HashMap<(StoryId, Option<String>), Arc<ComposedStory>>,
stories: HashMap<(StoryId, Option<usize>), Arc<ComposedStory>>,
pub threads: u16,
pub stats: FrameStats,
}
@@ -408,7 +408,6 @@ impl Renderer {
let Some((ppi, ppage)) = doc.parent_page_for(first + pi) else { continue };
let parent = &doc.parents[ppi];
let dx = page.x - parent.pages[ppage].x;
let page_name = doc.page_name(first + pi);
let pr = parent.pages[ppage].bounds();
for it in &parent.items {
if it.layer != layer.id || page.overridden.contains(&it.id) {
@@ -419,7 +418,7 @@ impl Renderer {
continue;
}
let _ = pr;
self.draw_item(ctx, f, it, Affine::translate((dx, 0.0)), Some(&page_name));
self.draw_item(ctx, f, it, Affine::translate((dx, 0.0)), Some(first + pi));
}
}
}
@@ -431,7 +430,7 @@ impl Renderer {
}
}
fn draw_item(&mut self, ctx: &mut RenderContext, f: &Frame, it: &Item, parent: Affine, page_name: Option<&str>) {
fn draw_item(&mut self, ctx: &mut RenderContext, f: &Frame, it: &Item, parent: Affine, page: Option<usize>) {
if it.hidden || f.opts.hidden.contains(&it.id) || f.opts.printing_only && it.nonprinting {
return;
}
@@ -458,7 +457,7 @@ impl Renderer {
ctx.set_transform(f.view * xf);
ctx.push_clip_layer(&bp);
for c in it.children() {
self.draw_item(ctx, f, c, xf, page_name);
self.draw_item(ctx, f, c, xf, page);
}
ctx.pop_layer();
if !it.stroke.is_none() {
@@ -489,11 +488,11 @@ impl Renderer {
ctx.fill_path(&c.path.to_bezpath());
ctx.pop_layer();
}
self.draw_item(ctx, f, c, xf, page_name);
self.draw_item(ctx, f, c, xf, page);
}
} else {
for c in it.shown_children() {
self.draw_item(ctx, f, c, xf, page_name);
self.draw_item(ctx, f, c, xf, page);
}
}
if layered {
@@ -520,9 +519,9 @@ impl Renderer {
ctx.push_layer(None, Some(blend_mode(it.blend)), Some(it.opacity), None, None);
}
if effects {
self.draw_item_fx(ctx, f, it, &bp, xf, page_name);
self.draw_item_fx(ctx, f, it, &bp, xf, page);
} else {
self.draw_body(ctx, f, it, &bp, xf, page_name);
self.draw_body(ctx, f, it, &bp, xf, page);
}
if layered {
ctx.pop_layer();
@@ -531,18 +530,18 @@ impl Renderer {
/// The composed story for a frame (memoised for the current render: the cache lookup
/// validates the whole thread each time).
fn story(&mut self, f: &Frame, sid: StoryId, page_name: Option<&str>) -> Arc<ComposedStory> {
let key = (sid, page_name.map(str::to_string));
fn story(&mut self, f: &Frame, sid: StoryId, page: Option<usize>) -> Arc<ComposedStory> {
let key = (sid, page);
if let Some(cs) = self.stories.get(&key) {
return cs.clone();
}
let cs = f.cache.get(f.doc, sid, page_name);
let cs = f.cache.get(f.doc, sid, page);
self.stories.insert(key, cs.clone());
cs
}
/// Fill, content and stroke of a leaf item (no opacity, blend or effects).
pub(crate) fn draw_body(&mut self, ctx: &mut RenderContext, f: &Frame, it: &Item, bp: &BezPath, xf: Affine, page_name: Option<&str>) {
pub(crate) fn draw_body(&mut self, ctx: &mut RenderContext, f: &Frame, it: &Item, bp: &BezPath, xf: Affine, page: Option<usize>) {
let doc = f.doc;
// Fill.
if !it.fill.is_none() {
@@ -575,7 +574,7 @@ impl Renderer {
ctx.pop_layer();
}
Content::Text(tf) => {
let cs = self.story(f, tf.story, page_name);
let cs = self.story(f, tf.story, page);
if let Some(ft) = cs.frame(it.id) {
match &tf.options.path {
Some(pt) => self.draw_path_text(ctx, f, &cs, ft, xf, bp, pt),