Canvas redraw: the renderer's measure tests check what their names say

Review follow-ups:
- The tests that a group with effects of its own is measured once a pass
  plant its kept bounds after the first artboard and read them back over
  the next two, so a measure made again fails. Its solid is built by the
  free measure, which the renderer's three_d_built doesn't count.
- The budget test checks that every object, past the budget too, is
  measured as the free measure does and draws as a new renderer draws it.
- A 3D effect that builds nothing draws the object exactly as without it,
  with a Drop Shadow or Gaussian Blur too, which reach past the path. The
  doc comment said those differ past the path; they don't.
- The brush widened in view first checks that the wider brush draws
  otherwise.
- Recoloring a traced shape changes no more than the shape, its stroke
  and a pixel's anti-aliasing.
This commit is contained in:
Drew Dibble
2026-11-09 13:13:17 -07:00
parent 049f8c3963
commit af754bcb23
2 changed files with 40 additions and 18 deletions
+37 -17
View File
@@ -771,6 +771,14 @@ fn measured_art_is_held_within_its_budget() {
r.bounds_of(&symbols, n);
}
assert!(r.measured_bytes <= one * 3 && r.measured.len() <= 3, "{} bytes in {} entries", r.measured_bytes, r.measured.len());
// Every object is measured right, those past the budget too, and draws as a new renderer
// draws it, its art made again.
for n in d.layers[0].children().unwrap() {
assert_eq!(r.bounds_of(&symbols, n), super::cull_bounds(&symbols, n), "{:?}", n.id);
}
let (view, opts) = (Affine::translate((-500.0, 0.0)), RenderOptions::default());
let drawn = r.render(&d, 800, 100, view, &opts).pixels;
assert!(drawn == Renderer::new().render(&d, 800, 100, view, &opts).pixels);
}
/// An edit to 3D objects the frame doesn't draw (hidden, in a hidden group, drawn hidden) builds
@@ -920,11 +928,18 @@ fn a_group_holding_3d_art_is_measured_once_a_pass() {
d.insert(Some(l), 0, g).unwrap();
let g = d.layers[0].children().unwrap()[0].clone();
let mut r = Renderer::new();
for i in 0..3 {
let first = Rect::new(0.0, 0.0, 100.0, 100.0);
assert_eq!(r.art_bounds_on(&d, first), crate::encode::art_bounds_on(&d, first));
let key = Arc::as_ptr(&g) as usize;
assert!(r.geom.contains_key(&key), "kept for the next artboard");
// The group's bounds, planted over the next artboards: they read the kept bounds, measuring
// nothing again.
let planted = Rect::new(100.0, 0.0, 300.0, 100.0);
r.geom.get_mut(&key).unwrap().bounds = Some(planted);
for i in 1..3 {
let rect = Rect::new(f64::from(i) * 100.0, 0.0, f64::from(i) * 100.0 + 100.0, 100.0);
assert_eq!(r.art_bounds_on(&d, rect), crate::encode::art_bounds_on(&d, rect));
assert_eq!(r.art_bounds_on(&d, rect), Some(planted), "artboard {i}");
}
assert!(r.geom.contains_key(&(Arc::as_ptr(&g) as usize)), "kept for the next artboard");
}
/// A pass that only measures (Fit to Artwork Bounds, an export) holds no 3D art past its budget:
@@ -1070,6 +1085,8 @@ fn a_rasterized_3d_objects_brush_widened_is_drawn() {
#[test]
fn a_rasterized_objects_brush_widened_in_view_is_drawn_again() {
let (drawn, fresh) = drawn_after_a_brush_widens(40.0, 12.0);
let (_, before) = drawn_after_a_brush_widens(40.0, 3.0);
assert!(fresh != before, "the wider brush draws otherwise");
assert!(drawn == fresh, "as a new renderer draws it");
}
@@ -1096,11 +1113,18 @@ fn a_group_with_a_hidden_member_is_measured_once_a_pass() {
d.insert(Some(l), 0, g).unwrap();
let g = d.layers[0].children().unwrap()[0].clone();
let mut r = Renderer::new();
for i in 0..3 {
let first = Rect::new(0.0, 0.0, 100.0, 100.0);
assert_eq!(r.art_bounds_on(&d, first), crate::encode::art_bounds_on(&d, first));
let key = Arc::as_ptr(&g) as usize;
assert!(r.pass_bounds.contains_key(&key), "kept for the next artboard");
// The group's bounds, planted over the next artboards: they read the kept bounds, measuring
// nothing again.
let planted = Rect::new(100.0, 0.0, 300.0, 100.0);
r.pass_bounds.get_mut(&key).unwrap().1 = Some(planted);
for i in 1..3 {
let rect = Rect::new(f64::from(i) * 100.0, 0.0, f64::from(i) * 100.0 + 100.0, 100.0);
assert_eq!(r.art_bounds_on(&d, rect), crate::encode::art_bounds_on(&d, rect));
assert_eq!(r.art_bounds_on(&d, rect), Some(planted), "artboard {i}");
}
assert!(r.pass_bounds.contains_key(&(Arc::as_ptr(&g) as usize)), "kept for the next artboard");
}
/// Art made again for an object, after what it read from outside it changed, takes the place of
@@ -1724,11 +1748,9 @@ fn an_image_inner_glow_follows_its_visible_pixels() {
/// A 3D effect on a path's fill or stroke that builds nothing (Revolve after Roughen) draws the
/// item as without it: the art made for the object is drawn without it, not as an object with
/// effects again, and that art again, without end. Without effects of its own the object draws
/// exactly as without the 3D effect. With a raster effect (Drop Shadow, Gaussian Blur) it draws from
/// that art, where without the 3D effect it draws as a path, whose raster effects are clipped to
/// the path's own geometry: there the item's Roughen spikes are cut off, so the two differ past
/// the path, and match inside it.
/// effects again, and that art again, without end. The object draws exactly as without the 3D
/// effect, with a raster effect of its own (Drop Shadow, Gaussian Blur) too, which reaches past
/// the path.
#[test]
fn a_3d_effect_that_builds_nothing_draws_the_item_as_without_it() {
let rough =
@@ -1756,12 +1778,10 @@ fn a_3d_effect_that_builds_nothing_draws_the_item_as_without_it() {
Renderer::new().render(&d, 100, 100, Affine::IDENTITY, &RenderOptions::default()).pixels
};
let (with, without) = (draw(true), draw(false));
if object.is_empty() {
assert!(with == without, "on the fill {on_fill}: as without it");
} else {
// Inside the path, well within its Roughen.
let at = |p: &[u8], x: usize, y: usize| p[(y * 100 + x) * 4..][..4].to_vec();
assert_eq!(at(&with, 45, 45), at(&without, 45, 45), "{object:?}");
assert!(with == without, "{object:?} on the fill {on_fill}: as without it");
if !object.is_empty() {
let past = (0..100usize).flat_map(|y| (0..100usize).map(move |x| (x, y))).filter(|(x, y)| *x > 62 || *y > 62);
assert!(past.into_iter().any(|(x, y)| with[(y * 100 + x) * 4 + 3] > 0), "{object:?}: drawn past the path");
}
}
}
+3 -1
View File
@@ -621,7 +621,9 @@ fn recolouring_a_traced_shape_changes_only_the_shape() {
let mut new = old.clone();
new.node_mut(shape).unwrap().appearance.fill_mut().unwrap().paint = Paint::solid(Color::rgb(0.0, 0.0, 1.0));
let a = area(redraw(&old, &new));
assert!(a.x0 > 72.0, "not the source or the other shape: {a:?}");
// The shape, its 2 pt stroke and a pixel's anti-aliasing, at most.
let shape = Rect::new(80.0, 10.0, 90.0, 20.0).inflate(3.0, 3.0);
assert!(a.union(shape) == shape, "only the shape: {a:?}");
}
/// Templates and guides that the view hides draw nothing: an edit to them changes nothing.