mirror of
https://github.com/storytold/designcraft.git
synced 2026-11-14 14:22:39 +00:00
Render: cull a scaled object by its scaled stroke and glow
Culling grew an object's box by its unscaled stroke and effect outset, so a view or repaint patch that showed only the outer part of a scaled object's stroke or hardened glow skipped the object. Culling now scales both by the object's largest scale factor, as drawing does. The CMYK ink-pass area grows each object, and each nested child, by its stroke and effect outset under its full transform.
This commit is contained in:
1 parent
8121bca1fe
commit
6d8f26a6be
3 files changed
+92
-4
No files matched your search
@@ -371,10 +371,18 @@ impl Renderer {
|
||||
}
|
||||
}
|
||||
// What an object can paint on the spread: its bounds with its (and its children's)
|
||||
// strokes and effects.
|
||||
// strokes and effects, which are drawn through each one's transform and grow with it.
|
||||
fn outset(it: &Item, outer: Affine, o: &mut f64) {
|
||||
let xf = outer * it.xf;
|
||||
let scale = crate::fx::max_scale(xf).unwrap_or(1.0);
|
||||
*o = o.max(crate::fx::outset_scaled(it, scale) + it.stroke.extent() * scale);
|
||||
for c in it.children() {
|
||||
outset(c, xf, o);
|
||||
}
|
||||
}
|
||||
let reach = |it: &Item| {
|
||||
let mut o: f64 = 0.0;
|
||||
it.walk(&mut |c| o = o.max(crate::fx::outset(c) + c.stroke.extent()));
|
||||
outset(it, Affine::IDENTITY, &mut o);
|
||||
it.bounds().inflate(o + 2.0, o + 2.0)
|
||||
};
|
||||
let items: Vec<(Rect, bool)> = sp.items.iter().filter(|it| visible(it)).map(|it| (reach(it), transparent(it))).collect();
|
||||
@@ -601,6 +609,39 @@ mod tests {
|
||||
assert_eq!(draw(&doc, &mut renderer).pixels, rgb.pixels);
|
||||
}
|
||||
|
||||
/// A hardened outer glow is stroked through the object's transform, and through its group's,
|
||||
/// so the ink-pass area grows by the scaled growth: the square at 30..60 glows out to -30.
|
||||
#[test]
|
||||
fn cmyk_area_holds_a_scaled_objects_glow() {
|
||||
for nested in [false, true] {
|
||||
let mut doc = document();
|
||||
let mut item = Item::new(
|
||||
ItemId(doc.alloc()),
|
||||
doc.default_layer(),
|
||||
Shape::Rectangle,
|
||||
designcraft_geom::shapes::rectangle(Rect::new(10.0, 10.0, 20.0, 20.0)),
|
||||
);
|
||||
item.fill = Fill::swatch("C=100 M=0 Y=0 K=0");
|
||||
let g = &mut item.effects.outer_glow;
|
||||
(g.on, g.color, g.opacity, g.size, g.spread) = (true, designcraft_color::swatch::BLACK.into(), 1.0, 20.0, 100.0);
|
||||
if nested {
|
||||
let mut group = Item::new(ItemId(doc.alloc()), doc.default_layer(), Shape::Group, Default::default());
|
||||
group.xf = Affine::scale(3.0);
|
||||
group.content = Content::Group { items: vec![Arc::new(item)] };
|
||||
doc.insert_item(SpreadRef::Doc(0), group, None).unwrap();
|
||||
} else {
|
||||
item.xf = Affine::scale(3.0);
|
||||
doc.insert_item(SpreadRef::Doc(0), item, None).unwrap();
|
||||
}
|
||||
let cache = Cache::new();
|
||||
let opts = RenderOptions::default();
|
||||
let f =
|
||||
Frame { doc: &doc, cache: &cache, view: Affine::IDENTITY, visible: Rect::new(-1e4, -1e4, 1e4, 1e4), px: 1.0, opts: &opts, mt: false };
|
||||
let area = Renderer::new().transparent_area(&f, SpreadRef::Doc(0)).unwrap();
|
||||
assert!(area.x0 <= -30.0, "nested {nested}: the area {area:?} holds the glow out to -30");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cmyk_pass_count_is_constant_and_unchanged_views_are_cached() {
|
||||
let mut doc = document();
|
||||
|
||||
@@ -179,7 +179,10 @@ impl Renderer {
|
||||
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);
|
||||
// The stroke and the effects' growth are drawn through `xf`, which stretches them.
|
||||
let scale = fx::max_scale(xf).unwrap_or(1.0);
|
||||
let stroke = it.stroke.extent() * scale;
|
||||
let vb = xf.transform_rect_bbox(it.inner_bounds()).inflate(stroke + 2.0, stroke + 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() {
|
||||
@@ -193,7 +196,7 @@ impl Renderer {
|
||||
_ => 0.0,
|
||||
};
|
||||
let effects = it.effects.any() && f.opts.quality != DisplayQuality::Fast;
|
||||
let fx_outset = if effects { fx::outset(it) } else { 0.0 };
|
||||
let fx_outset = if effects { fx::outset_scaled(it, scale) } else { 0.0 };
|
||||
if !rect_overlaps(vb.inflate(margin + fx_outset, margin + fx_outset), f.visible) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1576,4 +1576,48 @@ mod tests {
|
||||
assert!(lum < 200, "{threads} threads: the glow 50 pt left of the scaled square is drawn, luminance {lum}");
|
||||
}
|
||||
}
|
||||
|
||||
/// Culling measures a scaled item's stroke and hardened glow as they are drawn, through its
|
||||
/// transform, so a view (or a repaint patch) that shows only that part still draws it.
|
||||
#[test]
|
||||
fn a_scaled_items_hard_glow_and_stroke_are_not_culled() {
|
||||
use designcraft_doc::build::NewDocument;
|
||||
use designcraft_doc::{Document, SpreadRef};
|
||||
let square = |glow: bool| {
|
||||
let mut d = Document::new(&NewDocument::default());
|
||||
let lid = d.default_layer();
|
||||
let mut it = Item::new(
|
||||
designcraft_doc::ItemId(d.alloc()),
|
||||
lid,
|
||||
designcraft_doc::Shape::Rectangle,
|
||||
designcraft_geom::shapes::rectangle(Rect::new(50.0, 50.0, 90.0, 90.0)),
|
||||
);
|
||||
it.xf = Affine::scale(3.0);
|
||||
it.fill = designcraft_doc::Fill::swatch(designcraft_color::swatch::BLACK);
|
||||
if glow {
|
||||
let g = &mut it.effects.outer_glow;
|
||||
(g.on, g.color, g.opacity, g.size, g.spread) = (true, designcraft_color::swatch::BLACK.into(), 1.0, 20.0, 100.0);
|
||||
} else {
|
||||
(it.stroke.swatch, it.stroke.weight) = (designcraft_color::swatch::BLACK.into(), 20.0);
|
||||
}
|
||||
d.insert_item(SpreadRef::Doc(0), it, None).unwrap();
|
||||
d
|
||||
};
|
||||
let cache = crate::Cache::new();
|
||||
let opts = crate::RenderOptions { background: Some([255, 255, 255, 255]), ..Default::default() };
|
||||
// The square covers 150..270 on the spread. Its glow grows 60 pt (to 90), past the
|
||||
// unscaled 33 pt reach; its 20 pt stroke is 60 pt wide (to 120), past the unscaled 22 pt.
|
||||
for (glow, x) in [(true, 95.0), (false, 123.0)] {
|
||||
let d = square(glow);
|
||||
for threads in [0, 2] {
|
||||
let mut r = Renderer::new();
|
||||
r.threads = threads;
|
||||
let place = [crate::Placed { spread: SpreadRef::Doc(0), xf: Affine::IDENTITY }];
|
||||
let image = r.render(&d, &cache, &place, (4, 20), Affine::translate((-x, -200.0)), &opts);
|
||||
let p = image.pixel(1, 10);
|
||||
let lum = u32::from(p[0]) + u32::from(p[1]) + u32::from(p[2]);
|
||||
assert!(lum < 200, "glow {glow}, {threads} threads: the view at x {x} shows the scaled square's edge, luminance {lum}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user