mirror of
https://github.com/storytold/designcraft.git
synced 2026-11-14 14:22:39 +00:00
Merge pull request #2654 from dotCooCoo/fix/effect-damage-bounds
Canvas: moving an object with a drop shadow repaints only its area
This commit is contained in:
5 files changed
+325
-16
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();
|
||||
|
||||
+141
-8
@@ -11,6 +11,8 @@ use std::sync::Arc;
|
||||
use designcraft_doc::{Document, Item, ItemId, SpreadRef, Story, StoryId};
|
||||
use designcraft_geom::Rect;
|
||||
|
||||
use crate::fx::max_scale;
|
||||
|
||||
/// Changed regions in spread coordinates, or `None` when the whole view must be repainted.
|
||||
pub fn damage(old: &Document, new: &Document) -> Option<Vec<(SpreadRef, Rect)>> {
|
||||
damage_with(old, new, None)
|
||||
@@ -158,36 +160,47 @@ fn shows_other_text(d: &Document, s: &Story) -> bool {
|
||||
fn item_area(it: &Item) -> Option<Rect> {
|
||||
let mut ok = true;
|
||||
it.walk(&mut |i| {
|
||||
// Effects spill outside; text wrap reflows other frames.
|
||||
if i.effects.any() || i.has_wrap() {
|
||||
// Text wrap reflows other frames. A nested item's effects are drawn through its parents'
|
||||
// transforms, which can scale how far they reach.
|
||||
if i.has_wrap() || !std::ptr::eq(i, it) && i.effects.any() {
|
||||
ok = false;
|
||||
}
|
||||
});
|
||||
if !ok {
|
||||
return None;
|
||||
}
|
||||
let pad = it.stroke.extent().max(0.0) + 2.0 + if it.is_text_frame() { 24.0 } else { 0.0 };
|
||||
Some(painted_bounds(it).inflate(pad, pad))
|
||||
// The stroke and the effects' spread are drawn through the item's transform. The item's own
|
||||
// drop shadow and outer glow reach `fx::outset_scaled` past what it paints; its inner effects
|
||||
// and feathers stay inside the shape.
|
||||
let scale = max_scale(it.xf)?;
|
||||
let stroke = it.stroke.extent().max(0.0) * scale;
|
||||
let pad = stroke + 2.0 + if it.is_text_frame() { 24.0 } else { 0.0 } + crate::fx::outset_scaled(it, scale);
|
||||
if !pad.is_finite() {
|
||||
return None;
|
||||
}
|
||||
Some(painted_bounds(it)?.inflate(pad, pad))
|
||||
}
|
||||
|
||||
/// An item's bounds, grown to its text: text is composed in the box around the frame's path in
|
||||
/// the frame's own coordinates, which a sheared then rotated frame turns past its path's bounds.
|
||||
fn painted_bounds(it: &Item) -> Rect {
|
||||
/// Its children's bounds include their strokes. None when a child's transform is not finite.
|
||||
fn painted_bounds(it: &Item) -> Option<Rect> {
|
||||
let mut b = it.bounds();
|
||||
if it.is_text_frame() {
|
||||
b = b.union(it.xf.transform_rect_bbox(it.inner_bounds()));
|
||||
}
|
||||
for c in it.children() {
|
||||
b = b.union(it.xf.transform_rect_bbox(painted_bounds(c)));
|
||||
let stroke = c.stroke.extent().max(0.0) * max_scale(c.xf)?;
|
||||
b = b.union(it.xf.transform_rect_bbox(painted_bounds(c)?.inflate(stroke, stroke)));
|
||||
}
|
||||
b
|
||||
Some(b)
|
||||
}
|
||||
|
||||
/// A text frame's area: its spread and bounds with room for glyph overhang.
|
||||
fn frame_area(d: &Document, f: ItemId) -> Option<(SpreadRef, Rect)> {
|
||||
let loc = d.find(f)?;
|
||||
let it = d.item_at(&loc)?;
|
||||
let b = d.parent_xf(&loc).transform_rect_bbox(painted_bounds(it));
|
||||
let b = d.parent_xf(&loc).transform_rect_bbox(painted_bounds(it)?);
|
||||
// Overhang of italics, swashes and big initials, and anchored objects pinned to lines.
|
||||
Some((loc.spread, b.inflate(24.0 + 0.1 * b.width().max(b.height()), 24.0)))
|
||||
}
|
||||
@@ -384,4 +397,124 @@ mod tests {
|
||||
let spreads: Vec<SpreadRef> = dmg.iter().map(|(r, _)| *r).collect();
|
||||
assert!(spreads.contains(&SpreadRef::Doc(1)), "{dmg:?}");
|
||||
}
|
||||
|
||||
/// Moving an object with a drop shadow or an outer glow damages where its effect was and is,
|
||||
/// and every pixel the move changes lies in that damage: the canvas repaints those regions,
|
||||
/// not the whole view.
|
||||
#[test]
|
||||
fn moving_an_object_with_a_shadow_or_glow_damages_its_effect_area() {
|
||||
for glow in [false, true] {
|
||||
let mut d = Document::new(&NewDocument { pages: 1, ..Default::default() });
|
||||
let lid = d.default_layer();
|
||||
let id = ItemId(d.alloc());
|
||||
let mut it =
|
||||
Item::new(id, lid, designcraft_doc::Shape::Rectangle, designcraft_geom::shapes::rectangle(Rect::new(100.0, 100.0, 250.0, 200.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, 30.0, 20.0);
|
||||
} else {
|
||||
let s = &mut it.effects.drop_shadow;
|
||||
(s.on, s.color, s.opacity, s.size, s.distance, s.spread) = (true, designcraft_color::swatch::BLACK.into(), 1.0, 20.0, 25.0, 30.0);
|
||||
}
|
||||
d.insert_item(SpreadRef::Doc(0), it, None).unwrap();
|
||||
let old = d.clone();
|
||||
let it = d.item_mut(id).unwrap();
|
||||
it.path.transform(designcraft_geom::Affine::translate((60.0, 40.0)));
|
||||
let changed = changed_pixels_in_damage(&old, &d);
|
||||
assert!(changed.iter().any(|p| !Rect::new(98.0, 98.0, 312.0, 242.0).contains(*p)), "glow {glow}: the effect paints outside the object");
|
||||
}
|
||||
// A hardened glow on a scaled object: its growth is stroked through the transform, so it
|
||||
// reaches three times as far on the spread as the glow's size says.
|
||||
let mut d = Document::new(&NewDocument { pages: 1, ..Default::default() });
|
||||
let lid = d.default_layer();
|
||||
let id = ItemId(d.alloc());
|
||||
let mut it = Item::new(id, lid, designcraft_doc::Shape::Rectangle, designcraft_geom::shapes::rectangle(Rect::new(50.0, 50.0, 90.0, 90.0)));
|
||||
it.xf = designcraft_geom::Affine::scale(3.0);
|
||||
it.fill = designcraft_doc::Fill::swatch(designcraft_color::swatch::BLACK);
|
||||
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);
|
||||
d.insert_item(SpreadRef::Doc(0), it, None).unwrap();
|
||||
let old = d.clone();
|
||||
let it = d.item_mut(id).unwrap();
|
||||
it.xf = designcraft_geom::Affine::translate((60.0, 40.0)) * it.xf;
|
||||
// The square covers 150..270 before the move; its glow reaches 60 pt past that.
|
||||
let changed = changed_pixels_in_damage(&old, &d);
|
||||
assert!(changed.iter().any(|p| p.x < 110.0), "the hard glow grows with the object's scale");
|
||||
// An effect on an object inside a group is drawn through the group's transform: unbounded.
|
||||
let mut d = Document::new(&NewDocument { pages: 1, ..Default::default() });
|
||||
let lid = d.default_layer();
|
||||
let mut child =
|
||||
Item::new(ItemId(d.alloc()), lid, designcraft_doc::Shape::Rectangle, designcraft_geom::shapes::rectangle(Rect::new(0.0, 0.0, 9.0, 9.0)));
|
||||
child.effects.drop_shadow.on = true;
|
||||
let gid = ItemId(d.alloc());
|
||||
let mut group = Item::new(gid, lid, designcraft_doc::Shape::Group, Default::default());
|
||||
group.content = designcraft_doc::Content::Group { items: vec![Arc::new(child)] };
|
||||
d.insert_item(SpreadRef::Doc(0), group, None).unwrap();
|
||||
let old = d.clone();
|
||||
d.item_mut(gid).unwrap().xf = designcraft_geom::Affine::scale(4.0);
|
||||
assert!(damage(&old, &d).is_none());
|
||||
}
|
||||
|
||||
/// A stroke is drawn through its item's transform: a frame scaled up paints its stroke that
|
||||
/// much wider, and moving it damages all of it.
|
||||
#[test]
|
||||
fn moving_a_scaled_frame_damages_its_whole_stroke() {
|
||||
let mut d = Document::new(&NewDocument { pages: 1, ..Default::default() });
|
||||
let lid = d.default_layer();
|
||||
let id = ItemId(d.alloc());
|
||||
let mut it = Item::new(id, lid, designcraft_doc::Shape::Rectangle, designcraft_geom::shapes::rectangle(Rect::new(50.0, 50.0, 125.0, 100.0)));
|
||||
it.xf = designcraft_geom::Affine::scale(2.0);
|
||||
it.stroke.swatch = designcraft_color::swatch::BLACK.into();
|
||||
it.stroke.weight = 20.0;
|
||||
it.stroke.align = designcraft_doc::StrokeAlign::Outside;
|
||||
it.effects.outer_glow.on = true;
|
||||
it.effects.outer_glow.size = 0.0;
|
||||
d.insert_item(SpreadRef::Doc(0), it, None).unwrap();
|
||||
let old = d.clone();
|
||||
let it = d.item_mut(id).unwrap();
|
||||
it.xf = designcraft_geom::Affine::translate((60.0, 40.0)) * it.xf;
|
||||
// The stroke reaches 40 pt past the frame's 100..250 × 100..200 outline on the spread.
|
||||
let changed = changed_pixels_in_damage(&old, &d);
|
||||
assert!(changed.iter().any(|p| p.x < 70.0), "the stroke paints its scaled width");
|
||||
// The same frame inside a group with a gradient feather: the child's stroke counts too.
|
||||
let mut d = Document::new(&NewDocument { pages: 1, ..Default::default() });
|
||||
let mut child = old.spreads[0].items[0].as_ref().clone();
|
||||
child.effects = Default::default();
|
||||
child.id = ItemId(d.alloc());
|
||||
let gid = ItemId(d.alloc());
|
||||
let outline = designcraft_geom::shapes::rectangle(Rect::new(100.0, 100.0, 250.0, 200.0));
|
||||
let mut group = Item::new(gid, d.default_layer(), designcraft_doc::Shape::Group, outline);
|
||||
group.content = designcraft_doc::Content::Group { items: vec![Arc::new(child)] };
|
||||
let gf = &mut group.effects.gradient_feather;
|
||||
(gf.on, gf.start, gf.end) = (true, 1.0, 0.5);
|
||||
d.insert_item(SpreadRef::Doc(0), group, None).unwrap();
|
||||
let old = d.clone();
|
||||
d.item_mut(gid).unwrap().xf = designcraft_geom::Affine::translate((60.0, 40.0));
|
||||
let changed = changed_pixels_in_damage(&old, &d);
|
||||
assert!(changed.iter().any(|p| p.x < 70.0), "the child's stroke paints its scaled width");
|
||||
}
|
||||
|
||||
/// The pixels a page render changes from `old` to `new`, each checked to lie in the damage.
|
||||
fn changed_pixels_in_damage(old: &Document, new: &Document) -> Vec<designcraft_geom::Point> {
|
||||
let dmg = damage(old, new).expect("bounded damage");
|
||||
let cache = designcraft_compose::Cache::new();
|
||||
let mut r = crate::Renderer::new();
|
||||
r.threads = 0;
|
||||
let opts = crate::RenderOptions::default();
|
||||
let before = r.render_page(old, &cache, 0, 1.0, false, &opts).unwrap();
|
||||
let after = r.render_page(new, &cache, 0, 1.0, false, &opts).unwrap();
|
||||
let mut changed = Vec::new();
|
||||
for y in 0..before.height {
|
||||
for x in 0..before.width {
|
||||
if before.pixel(x, y) == after.pixel(x, y) {
|
||||
continue;
|
||||
}
|
||||
let p = designcraft_geom::Point::new(f64::from(x) + 0.5, f64::from(y) + 0.5);
|
||||
assert!(dmg.iter().any(|(_, r)| r.inflate(1.0, 1.0).contains(p)), "changed pixel {p:?} outside {dmg:?}");
|
||||
changed.push(p);
|
||||
}
|
||||
}
|
||||
changed
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
+108
-4
@@ -40,17 +40,37 @@ pub(crate) fn offset(angle: f64, distance: f64) -> Vec2 {
|
||||
|
||||
/// How far (points) the effects of `it` reach outside its geometry.
|
||||
pub(crate) fn outset(it: &Item) -> f64 {
|
||||
outset_scaled(it, 1.0)
|
||||
}
|
||||
|
||||
/// [`outset`] for an item whose transform stretches lengths by up to `scale`: the blur and the
|
||||
/// shadow's distance are spread lengths, but the spread's growth is stroked through the item's
|
||||
/// transform and grows with it.
|
||||
pub(crate) fn outset_scaled(it: &Item, scale: f64) -> f64 {
|
||||
let e = &it.effects;
|
||||
let extra = |size: f64, pct: f64| split(size, pct).1 * (scale - 1.0).max(0.0);
|
||||
let mut o: f64 = 0.0;
|
||||
if e.drop_shadow.on {
|
||||
o = o.max(bound(e.drop_shadow.distance).abs() + bound(e.drop_shadow.size).abs() * 1.6 + 1.0);
|
||||
let s = &e.drop_shadow;
|
||||
o = o.max(bound(s.distance).abs() + bound(s.size).abs() * 1.6 + 1.0 + extra(s.size, s.spread));
|
||||
}
|
||||
if e.outer_glow.on {
|
||||
o = o.max(bound(e.outer_glow.size).abs() * 1.6 + 1.0);
|
||||
let g = &e.outer_glow;
|
||||
o = o.max(bound(g.size).abs() * 1.6 + 1.0 + extra(g.size, g.spread));
|
||||
}
|
||||
o
|
||||
}
|
||||
|
||||
/// The largest factor `m` stretches a length by (its linear part's largest singular value), or
|
||||
/// None when that is not finite.
|
||||
pub(crate) fn max_scale(m: Affine) -> Option<f64> {
|
||||
let [a, b, c, d, _, _] = m.as_coeffs();
|
||||
let s = a * a + b * b + c * c + d * d;
|
||||
let det = a * d - b * c;
|
||||
let v = ((s + (s * s - 4.0 * det * det).max(0.0).sqrt()) / 2.0).sqrt();
|
||||
v.is_finite().then_some(v)
|
||||
}
|
||||
|
||||
/// Whether `it` has a basic feather: a width on a closed shape.
|
||||
fn basic_feather(it: &Item) -> bool {
|
||||
it.effects.feather > 0.0 && it.path.is_closed()
|
||||
@@ -120,7 +140,9 @@ impl Renderer {
|
||||
pub(crate) fn draw_item_fx(&mut self, ctx: &mut RenderContext, f: &Frame, it: &Item, bp: &BezPath, xf: Affine, page: ShownOn) {
|
||||
let doc = f.doc;
|
||||
let e = &it.effects;
|
||||
let sw = if it.stroke.is_none() { 0.0 } else { it.stroke.extent() };
|
||||
// The stroke and the effects' growth are drawn through `xf`, which stretches them.
|
||||
let scale = max_scale(xf).unwrap_or(1.0);
|
||||
let sw = if it.stroke.is_none() { 0.0 } else { it.stroke.extent() * scale };
|
||||
// Spread-space reach of what the object paints.
|
||||
let reach = xf.transform_rect_bbox(bp.bounding_box()).inflate(sw + 1.0, sw + 1.0);
|
||||
// The silhouette is the shape when the object paints its area (fill / image); text frames
|
||||
@@ -150,7 +172,7 @@ impl Renderer {
|
||||
let filter = shadow(sigma, paint);
|
||||
// The offset moves the geometry, not the filter: vello drops layer content that lies
|
||||
// entirely outside the viewport before filtering.
|
||||
let r = (reach + off).inflate(grow, grow);
|
||||
let r = (reach + off).inflate(grow * scale, grow * scale);
|
||||
// The effect's blend sees what's behind the object: a layer around the filtered one.
|
||||
let mix = crate::blend_mode(mode).mix;
|
||||
if mix != Mix::Normal {
|
||||
@@ -1516,4 +1538,86 @@ mod tests {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A hardened outer glow is stroked through the item's transform, so a scaled item's glow
|
||||
/// grows with it. The multithreaded renderer draws the glow offscreen, cropped to its reach,
|
||||
/// and that reach scales too.
|
||||
#[test]
|
||||
fn a_scaled_items_hard_glow_is_not_cropped() {
|
||||
use designcraft_doc::build::NewDocument;
|
||||
use designcraft_doc::{Document, SpreadRef};
|
||||
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);
|
||||
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);
|
||||
// max_scale's own cases: a plain scale, a rotation and a shear.
|
||||
assert_eq!(max_scale(it.xf), Some(3.0));
|
||||
assert!((max_scale(Affine::rotate(0.7)).unwrap() - 1.0).abs() < 1e-12);
|
||||
assert!((max_scale(Affine::skew(1.0, 0.0)).unwrap() - (1.0 + 5f64.sqrt()) / 2.0).abs() < 1e-12);
|
||||
assert_eq!(max_scale(Affine::scale(f64::INFINITY)), None);
|
||||
// The 20 pt growth is 60 pt on the spread: the glow covers 90..330.
|
||||
assert_eq!(outset_scaled(&it, 3.0), outset(&it) + 40.0);
|
||||
d.insert_item(SpreadRef::Doc(0), it, None).unwrap();
|
||||
let cache = crate::Cache::new();
|
||||
for threads in [0, 2] {
|
||||
let mut r = Renderer::new();
|
||||
r.threads = threads;
|
||||
let page = r.render_page(&d, &cache, 0, 1.0, false, &crate::RenderOptions::default()).unwrap();
|
||||
let p = page.pixel(100, 210);
|
||||
let lum = u32::from(p[0]) + u32::from(p[1]) + u32::from(p[2]);
|
||||
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}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2420,6 +2420,34 @@ fn edits_patch_the_canvas_only_at_full_resolution() {
|
||||
}
|
||||
}
|
||||
|
||||
/// Moving a frame with a drop shadow repaints the regions the frame and its shadow cover, as
|
||||
/// moving one without a shadow does, instead of rendering the whole canvas again on every step.
|
||||
#[test]
|
||||
fn moving_a_frame_with_a_drop_shadow_patches_the_canvas() {
|
||||
let mut app = DesignApp::new(designcraft_engine::Session::new(), crate::Services::default());
|
||||
let ctx = egui::Context::default();
|
||||
let frame = |app: &mut DesignApp| {
|
||||
let raw = egui::RawInput { screen_rect: Some(Rect::from_min_size(Pos2::ZERO, vec2(1200.0, 900.0))), ..Default::default() };
|
||||
let mut out = ctx.run_ui(raw, |ui| {
|
||||
app.logic(ui.ctx());
|
||||
app.ui(ui);
|
||||
});
|
||||
out.textures_delta.clear();
|
||||
};
|
||||
app.run("file.new", json!({})).unwrap();
|
||||
app.run("frame.create", json!({"rect": [72, 72, 200, 180], "content": "unassigned"})).unwrap();
|
||||
app.run("object.fill", json!({"swatch": "[Black]"})).unwrap();
|
||||
app.run("object.dropShadow", json!({"on": true})).unwrap();
|
||||
for _ in 0..3 {
|
||||
frame(&mut app);
|
||||
}
|
||||
for step in 1..=3 {
|
||||
app.run("transform.move", json!({"dx": 4, "dy": 2})).unwrap();
|
||||
frame(&mut app);
|
||||
assert_eq!(app.canvas.patches, step, "drag step {step} repaints only what changed");
|
||||
}
|
||||
}
|
||||
|
||||
/// The Type tool's caret at the end of a new text frame holding `text`, the app's first frames
|
||||
/// drawn; returns the story id.
|
||||
fn typing_app(app: &mut DesignApp, ctx: &egui::Context, text: &str) -> Value {
|
||||
|
||||
Reference in new issue
Block a user