mirror of
https://github.com/storytold/designcraft.git
synced 2026-11-14 14:22:39 +00:00
* An item a parent overrode and deleted is gone from the pages that use it (#1469) In InDesign, removing an inherited item from a parent page means overriding it there and deleting the override: the parent's page keeps the item in its override list with no copy of its own. Only the page being drawn was checked, so the pages that use that parent still drew the base's item -- a signature block from parent B over the notice text of every page using parent E. The override lists of the whole parent chain are checked, not just the page's own. An override always names an item of a parent further back, so one set covers the chain. Rebased onto the post-split main: the parent-item loop moved from render's lib.rs to draw.rs and the tests to tests.rs. One thing this does not touch: blendspace.rs checks `page.overridden` the same way in two places, for deciding which spreads hold transparency. A deleted override there would count an absent item as present, which can only cost an unnecessary flatten rather than draw the wrong thing, so it is left for a maintainer to judge rather than widened into this fix. * Parent items: one set of overridden items for drawing, PDF and PPTX export, hit-testing, snapping and frame edges * Override All: an item a parent removed is not brought back as a copy
300 lines
13 KiB
Rust
300 lines
13 KiB
Rust
use super::color::{HIDDEN_LAYERS, blend_mode, color_of, overprints, rect_overlaps, screen_color, set_fill_paint, spread_blends};
|
|
use super::types::{DisplayQuality, Frame};
|
|
use super::{
|
|
Affine, Arc, BezPath, BlendMode, Compose, ComposedStory, Content, DcBlend, Item, Mix, RenderContext, Renderer, Shape, ShownOn, SpreadRef,
|
|
StoryId, Vec2, blendspace, corners, fx, grid, kurbo, peniko,
|
|
};
|
|
|
|
/// An item's fill under its content; an overprinting fill multiplies into what is beneath.
|
|
fn draw_body_fill(ctx: &mut RenderContext, f: &Frame, it: &Item, bp: &BezPath, xf: Affine) {
|
|
if it.fill.is_none() {
|
|
return;
|
|
}
|
|
let doc = f.doc;
|
|
ctx.set_transform(f.view * xf);
|
|
let op = overprints(f, &it.fill.swatch, it.fill.tint, it.fill.overprint);
|
|
if op {
|
|
ctx.push_layer(None, Some(BlendMode::new(Mix::Multiply, Compose::SrcOver)), None, None, None);
|
|
}
|
|
if set_fill_paint(ctx, doc, &it.fill, bp.bounding_box()) {
|
|
ctx.fill_path(bp);
|
|
}
|
|
ctx.reset_paint_transform();
|
|
if op {
|
|
ctx.pop_layer();
|
|
}
|
|
}
|
|
|
|
impl Renderer {
|
|
pub(crate) fn draw_spread(&mut self, ctx: &mut RenderContext, f: &Frame, sr: SpreadRef) {
|
|
let doc = f.doc;
|
|
let Some(sp) = doc.spread(sr) else { return };
|
|
// Paper.
|
|
if f.opts.paper {
|
|
let paper = doc.resolve_color(designcraft_color::swatch::PAPER, 1.0).unwrap_or(designcraft_color::Color::WHITE);
|
|
if f.opts.page_shadow {
|
|
ctx.set_transform(f.view);
|
|
ctx.set_paint(peniko::Color::from_rgba8(0, 0, 0, 90));
|
|
for p in &sp.pages {
|
|
let s = 3.0 * f.px;
|
|
ctx.fill_rect(&(p.bounds() + Vec2::new(s, s)));
|
|
}
|
|
}
|
|
ctx.set_transform(f.view);
|
|
ctx.set_paint(color_of(&paper, 1.0));
|
|
// One rect for equal-height spreads avoids an anti-aliasing seam at the spine.
|
|
if sp.pages.windows(2).all(|w| w[0].height == w[1].height) {
|
|
ctx.fill_rect(&sp.bounds());
|
|
} else {
|
|
for p in &sp.pages {
|
|
ctx.fill_rect(&p.bounds());
|
|
}
|
|
}
|
|
}
|
|
let grid_area = f.opts.document_grid.iter().find(|(r, _)| *r == sr).map(|(_, a)| *a);
|
|
let grid = &doc.settings.grid;
|
|
if let Some(area) = grid_area.filter(|_| grid.in_back) {
|
|
grid::draw(ctx, f, sp, grid, area);
|
|
}
|
|
if blendspace::uses_cmyk(doc, sr, f.opts) {
|
|
self.draw_cmyk_spread(ctx, f, sr);
|
|
if let Some(area) = grid_area.filter(|_| !grid.in_back) {
|
|
grid::draw(ctx, f, sp, grid, area);
|
|
}
|
|
return;
|
|
}
|
|
// Blend modes blend with the objects below, not with the paper (as in InDesign and in PDF,
|
|
// where the page's group starts transparent): a Color Dodge or Screen object over blank paper
|
|
// shows as it is. So the items are drawn in their own group, laid over the paper once.
|
|
let isolate = f.opts.paper && spread_blends(doc, sp);
|
|
if isolate {
|
|
ctx.push_layer(None, None, None, None, None);
|
|
}
|
|
// Layers back to front; within each layer parent items first.
|
|
for layer in doc.layers.iter().rev() {
|
|
if !layer.visible || f.opts.printing_only && !layer.printable {
|
|
continue;
|
|
}
|
|
// Document pages show their parent and its bases; a parent spread shows its bases.
|
|
let first = match sr {
|
|
SpreadRef::Doc(si) => Some(doc.first_page_of_spread(si)),
|
|
SpreadRef::Parent(_) => None,
|
|
};
|
|
// Base items compose for where they show: the document page, or this parent spread.
|
|
let shown_on = |pi: usize| match (sr, first) {
|
|
(SpreadRef::Parent(p), _) => ShownOn::Parent(p),
|
|
(_, Some(a)) => ShownOn::Page(a + pi),
|
|
(_, None) => ShownOn::Own,
|
|
};
|
|
for (pi, page) in sp.pages.iter().enumerate() {
|
|
if !page.show_parent_items {
|
|
continue;
|
|
}
|
|
let chain = doc.shown_parent_pages(sr, pi);
|
|
// This page's overrides and those of every parent page behind it (#1469).
|
|
let overridden = doc.overridden_parent_items(sr, pi);
|
|
for (ppi, ppage) in chain {
|
|
let Some(parent) = doc.parents.get(ppi) else { continue };
|
|
let Some(ppg) = parent.pages.get(ppage) else { continue };
|
|
let dx = page.x - ppg.x;
|
|
for it in &parent.items {
|
|
if it.layer != layer.id || overridden.contains(&it.id) {
|
|
continue;
|
|
}
|
|
// Parent items belong to the parent page they sit on.
|
|
if parent.page_at_x(it.bounds().center().x) != Some(ppage) && parent.pages.len() > 1 {
|
|
continue;
|
|
}
|
|
self.draw_item(ctx, f, it, Affine::translate((dx, 0.0)), shown_on(pi));
|
|
}
|
|
}
|
|
}
|
|
for it in &sp.items {
|
|
if it.layer == layer.id {
|
|
self.draw_item(ctx, f, it, Affine::IDENTITY, ShownOn::Own);
|
|
}
|
|
}
|
|
}
|
|
if isolate {
|
|
ctx.pop_layer();
|
|
}
|
|
if let Some(area) = grid_area.filter(|_| !grid.in_back) {
|
|
grid::draw(ctx, f, sp, grid, area);
|
|
}
|
|
}
|
|
|
|
pub(crate) fn draw_item(&mut self, ctx: &mut RenderContext, f: &Frame, it: &Item, parent: Affine, page: ShownOn) {
|
|
if self.drawing_items.len() >= 64 || self.drawing_items.contains(&it.id) {
|
|
return;
|
|
}
|
|
self.drawing_items.push(it.id);
|
|
self.draw_item_inner(ctx, f, it, parent, page);
|
|
self.drawing_items.pop();
|
|
}
|
|
|
|
/// A group: children carry their own transforms relative to the group.
|
|
fn draw_group(&mut self, ctx: &mut RenderContext, f: &Frame, it: &Item, xf: Affine, page: ShownOn) {
|
|
// Isolate Blending / Knockout: the group is its own layer (blend modes stop at it).
|
|
let layered = it.opacity < 0.999 || it.blend != DcBlend::Normal || it.isolate || it.knockout;
|
|
if layered {
|
|
ctx.set_transform(Affine::IDENTITY);
|
|
ctx.push_layer(None, Some(blend_mode(it.blend)), Some(it.opacity), None, None);
|
|
}
|
|
// Gradient feather: the group's objects drawn together, then faded as one.
|
|
let feather = it.effects.gradient_feather.on && f.opts.quality != DisplayQuality::Fast;
|
|
if feather {
|
|
ctx.set_transform(Affine::IDENTITY);
|
|
ctx.push_layer(None, None, None, None, None);
|
|
}
|
|
if it.knockout {
|
|
// Knockout: each object first clears its area of those behind it, so it composites
|
|
// with the group's backdrop rather than with its siblings.
|
|
for c in it.shown_children() {
|
|
if !c.hidden {
|
|
ctx.set_transform(Affine::IDENTITY);
|
|
ctx.push_layer(None, Some(BlendMode::new(Mix::Normal, Compose::DestOut)), None, None, None);
|
|
ctx.set_transform(f.view * xf * c.xf);
|
|
ctx.set_paint(peniko::Color::BLACK);
|
|
ctx.fill_path(&c.path.to_bezpath());
|
|
ctx.pop_layer();
|
|
}
|
|
self.draw_item(ctx, f, c, xf, page);
|
|
}
|
|
} else {
|
|
for c in it.shown_children() {
|
|
self.draw_item(ctx, f, c, xf, page);
|
|
}
|
|
}
|
|
if feather {
|
|
fx::gradient_feather_mask(ctx, f, it, xf);
|
|
ctx.pop_layer();
|
|
}
|
|
if layered {
|
|
ctx.pop_layer();
|
|
}
|
|
}
|
|
|
|
fn draw_item_inner(&mut self, ctx: &mut RenderContext, f: &Frame, it: &Item, parent: Affine, page: ShownOn) {
|
|
if it.hidden || f.opts.hidden.contains(&it.id) || f.opts.printing_only && it.nonprinting {
|
|
return;
|
|
}
|
|
let xf = parent * it.xf;
|
|
let vb = xf.transform_rect_bbox(it.inner_bounds()).inflate(it.stroke.extent() + 2.0, it.stroke.extent() + 2.0);
|
|
// A frame with items pasted into it draws like any other object (draw_body: fill, the items
|
|
// clipped to the frame, stroke), so its effects apply to all of it.
|
|
if !it.children().is_empty() && !it.has_nested_items() {
|
|
self.draw_group(ctx, f, it, xf, page);
|
|
return;
|
|
}
|
|
// Composed text stays inside its frame apart from glyph overhang (italics, swashes, big
|
|
// initials), so text frames get a margin rather than being exempt from culling.
|
|
let margin = match it.content {
|
|
Content::Text(_) => 36.0 + 0.25 * vb.width().max(vb.height()),
|
|
_ => 0.0,
|
|
};
|
|
let effects = it.effects.any() && f.opts.quality != DisplayQuality::Fast;
|
|
let fx_outset = if effects { fx::outset(it) } else { 0.0 };
|
|
if !rect_overlaps(vb.inflate(margin + fx_outset, margin + fx_outset), f.visible) {
|
|
return;
|
|
}
|
|
self.stats.items += 1;
|
|
let bp = if it.corners.is_none() { it.path.to_bezpath() } else { corners::apply(&it.path, &it.corners) };
|
|
let layered = it.opacity < 0.999 || it.blend != DcBlend::Normal;
|
|
if layered {
|
|
ctx.set_transform(Affine::IDENTITY);
|
|
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);
|
|
} else {
|
|
self.draw_body(ctx, f, it, &bp, xf, page);
|
|
}
|
|
if layered {
|
|
ctx.pop_layer();
|
|
}
|
|
}
|
|
|
|
/// 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: ShownOn) -> Arc<ComposedStory> {
|
|
let key = (sid, page);
|
|
if let Some(cs) = self.stories.get(&key) {
|
|
return cs.clone();
|
|
}
|
|
let cs = f.cache.get_on(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: ShownOn) {
|
|
draw_body_fill(ctx, f, it, bp, xf);
|
|
// Content.
|
|
match &it.content {
|
|
Content::Graphic(g) => {
|
|
let g = &if it.media.is_some() { it.drawn_graphic(&f.doc.assets).unwrap_or_else(|| g.clone()) } else { g.clone() };
|
|
ctx.set_transform(f.view * xf);
|
|
ctx.push_clip_layer(bp);
|
|
let hidden = it.has_pdf_layer_override().then(|| it.pdf_hidden_layers.clone());
|
|
let previous = HIDDEN_LAYERS.with(|h| h.replace(hidden));
|
|
self.draw_graphic(ctx, f, g, xf);
|
|
HIDDEN_LAYERS.with(|h| h.replace(previous));
|
|
ctx.pop_layer();
|
|
}
|
|
// Items pasted into the frame, clipped by it.
|
|
Content::Group { items } if it.has_nested_items() => {
|
|
ctx.set_transform(f.view * xf);
|
|
ctx.push_clip_layer(bp);
|
|
for c in items {
|
|
self.draw_item(ctx, f, c, xf, page);
|
|
}
|
|
ctx.pop_layer();
|
|
}
|
|
Content::Text(tf) => {
|
|
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),
|
|
None => self.draw_text(ctx, f, &cs, ft, xf * f.doc.text_local(it)),
|
|
}
|
|
}
|
|
}
|
|
_ => {}
|
|
}
|
|
// Form fields (screen only; PDF viewers draw their own): a tint, a check mark.
|
|
if let Some(ff) = &it.form_field
|
|
&& !f.opts.printing_only
|
|
{
|
|
let r = it.inner_bounds();
|
|
ctx.set_transform(f.view * xf);
|
|
ctx.set_paint(screen_color([80, 140, 255], 40));
|
|
ctx.fill_rect(&r);
|
|
ctx.set_paint(screen_color([60, 110, 220], 200));
|
|
ctx.set_stroke(kurbo::Stroke::new(0.75));
|
|
ctx.stroke_path(&r.to_path(0.1));
|
|
if ff.kind == designcraft_doc::FieldKind::CheckBox && !ff.value.is_empty() && ff.value != "Off" {
|
|
let (w, h) = (r.width(), r.height());
|
|
let mut c = BezPath::new();
|
|
c.move_to((r.x0 + w * 0.22, r.y0 + h * 0.48));
|
|
c.line_to((r.x0 + w * 0.42, r.y0 + h * 0.72));
|
|
c.line_to((r.x0 + w * 0.78, r.y0 + h * 0.24));
|
|
ctx.set_paint(screen_color([0, 0, 0], 255));
|
|
ctx.set_stroke(kurbo::Stroke::new((w.min(h) * 0.12).max(0.8)).with_caps(kurbo::Cap::Round).with_join(kurbo::Join::Round));
|
|
ctx.stroke_path(&c);
|
|
}
|
|
}
|
|
// Stroke.
|
|
if !it.stroke.is_none() {
|
|
let op = overprints(f, &it.stroke.swatch, it.stroke.tint, it.stroke.overprint);
|
|
if op {
|
|
ctx.set_transform(f.view * xf);
|
|
ctx.push_layer(None, Some(BlendMode::new(Mix::Multiply, Compose::SrcOver)), None, None, None);
|
|
}
|
|
self.draw_stroke(ctx, f, it, bp, xf);
|
|
if op {
|
|
ctx.pop_layer();
|
|
}
|
|
}
|
|
}
|
|
}
|