Files
vectorcraft/crates/render/src/tests_changed.rs
T
Jürg Lehni 60f4c4f145 Render: moved type with mitred strokes redraws where its spikes reach
Follows #3461: character strokes' miter spikes reach past half the weight, which the redrawn area missed.
2026-10-22 06:52:06 +07:00

238 lines
10 KiB
Rust

//! Drawing again only what an edit changed gives the pixels of drawing it all again.
use super::*;
use vectorcraft_color::{Color, Paint};
use vectorcraft_doc::Appearance;
use vectorcraft_geom::{Point, shapes};
const W: u32 = 160;
const H: u32 = 120;
/// A view that doesn't line the art up with the pixels, so the edges are anti-aliased.
fn view() -> Affine {
Affine::translate((3.3, 2.7)) * Affine::scale(1.37)
}
fn rect(d: &mut Document, r: Rect, fill: Color) -> Node {
let id = d.alloc_id();
Node::path(id, shapes::rectangle(r), Appearance::basic(Paint::solid(fill), Paint::solid(Color::BLACK), 2.0))
}
fn shadow() -> vectorcraft_doc::Effect {
vectorcraft_doc::Effect {
id: "stylize.dropShadow".into(),
params: serde_json::json!({"x": 6.0, "y": 6.0, "blur": 4.0, "opacity": 75.0}),
visible: true,
}
}
/// A layer with a rectangle, a group at half opacity holding two, a clip group, and one with a
/// drop shadow; the ids of the loose rectangle, a member of each group, and the shadowed one.
fn document() -> (Document, [NodeId; 5]) {
let mut d = Document::new(110.0, 80.0);
let l = d.layers[0].id;
let loose = rect(&mut d, Rect::new(5.0, 5.0, 25.0, 20.0), Color::rgb(1.0, 0.0, 0.0));
let loose = d.insert(Some(l), 0, loose).unwrap();
let (a, b) = (
rect(&mut d, Rect::new(30.0, 5.0, 50.0, 25.0), Color::rgb(0.0, 0.6, 0.0)),
rect(&mut d, Rect::new(40.0, 15.0, 60.0, 35.0), Color::rgb(0.0, 0.0, 1.0)),
);
let mut group = Node::new(d.alloc_id(), NodeKind::Group { children: vec![Arc::new(a), Arc::new(b)], clip: false });
group.opacity = 0.5;
let member = group.children().unwrap()[1].id;
d.insert(Some(l), 1, group).unwrap();
let (mask, art) =
(rect(&mut d, Rect::new(5.0, 40.0, 30.0, 60.0), Color::WHITE), rect(&mut d, Rect::new(0.0, 45.0, 40.0, 55.0), Color::rgb(0.8, 0.4, 0.0)));
let clipped = art.id;
let clip = Node::new(d.alloc_id(), NodeKind::Group { children: vec![Arc::new(mask), Arc::new(art)], clip: true });
d.insert(Some(l), 2, clip).unwrap();
let mut shadowed = rect(&mut d, Rect::new(60.0, 45.0, 80.0, 60.0), Color::rgb(0.9, 0.9, 0.2));
shadowed.appearance.effects.push(shadow());
let shadowed = d.insert(Some(l), 3, shadowed).unwrap();
let other = rect(&mut d, Rect::new(85.0, 10.0, 105.0, 30.0), Color::rgb(0.5, 0.0, 0.5));
let other = d.insert(Some(l), 4, other).unwrap();
(d, [loose, member, clipped, shadowed, other])
}
/// Draws `new` again only where it changed from `old`, over `old` drawn whole, and checks that
/// this is `new` drawn whole; returns what changed.
fn redraw(old: &Document, new: &Document) -> Changed {
let mut r = Renderer::new();
let opts = RenderOptions::default();
let before = r.render(old, W, H, view(), &opts);
let after = r.render(new, W, H, view(), &opts);
let change = changed(old, new);
let mut patched = Rendered { width: W, height: H, pixels: before.pixels.clone() };
match change {
Changed::Everything => patched.pixels = after.pixels.clone(),
Changed::Nothing => {}
Changed::Area(area) => {
if let Some([x0, y0, x1, y1]) = view_pixels(area, view(), W, H) {
let part = r.render(new, x1 - x0, y1 - y0, Affine::translate((-f64::from(x0), -f64::from(y0))) * view(), &opts);
for y in 0..y1 - y0 {
let (from, to) = ((y * (x1 - x0) * 4) as usize, (((y0 + y) * W + x0) * 4) as usize);
let n = ((x1 - x0) * 4) as usize;
patched.pixels[to..to + n].copy_from_slice(&part.pixels[from..from + n]);
}
}
}
}
assert!(patched.pixels == after.pixels, "{change:?}: drawn again only there, it differs from drawing it all");
change
}
fn area(c: Changed) -> Rect {
match c {
Changed::Area(a) => a,
other => panic!("an area: {other:?}"),
}
}
#[test]
fn moving_an_object_changes_where_it_was_and_is() {
let (old, [loose, ..]) = document();
let mut new = old.clone();
new.node_mut(loose).unwrap().transform(Affine::translate((7.0, 3.0)), false);
let a = area(redraw(&old, &new));
assert!(a.contains(Point::new(6.0, 6.0)) && a.contains(Point::new(31.0, 22.0)), "before and after: {a:?}");
assert!(a.x1 < 35.0, "not the objects beside it: {a:?}");
}
#[test]
fn recolouring_a_member_of_a_translucent_group_changes_only_the_member() {
let (old, [_, member, ..]) = document();
let mut new = old.clone();
new.node_mut(member).unwrap().appearance.fill_mut().unwrap().paint = Paint::solid(Color::rgb(1.0, 0.5, 0.0));
let a = area(redraw(&old, &new));
assert!(a.x0 > 35.0, "not the group's other member: {a:?}");
}
#[test]
fn moving_clipped_art_changes_its_whole_clip_group() {
let (old, [_, _, clipped, ..]) = document();
let mut new = old.clone();
new.node_mut(clipped).unwrap().transform(Affine::translate((0.0, 4.0)), false);
redraw(&old, &new);
}
#[test]
fn moving_a_shadowed_object_changes_where_its_shadow_falls() {
let (old, [.., shadowed, _]) = document();
let mut new = old.clone();
new.node_mut(shadowed).unwrap().transform(Affine::translate((-5.0, 5.0)), false);
let a = area(redraw(&old, &new));
assert!(a.x1 > 86.0 && a.y1 > 70.0, "the shadow, before and after: {a:?}");
}
#[test]
fn adding_removing_and_restacking_change_those_objects() {
let (old, [loose, _, _, _, other]) = document();
let mut added = old.clone();
let n = rect(&mut added, Rect::new(70.0, 5.0, 90.0, 15.0), Color::BLACK);
let l = added.layers[0].id;
added.insert(Some(l), 5, n).unwrap();
redraw(&old, &added);
let mut removed = old.clone();
removed.remove(loose).unwrap();
redraw(&old, &removed);
let mut restacked = old.clone();
let n = restacked.remove(other).unwrap();
restacked.insert(Some(l), 0, (*n).clone()).unwrap();
redraw(&old, &restacked);
}
/// A blur too wide for a raster of all of it is cropped to the frame. A part of the frame drawn
/// again, during a drag too, draws it from that raster: the same pixels, and no preview at the
/// drag's lower resolution.
#[test]
fn a_part_of_a_frame_draws_a_cropped_blur_from_the_frames_raster() {
let (mut d, _) = document();
let mut back = rect(&mut d, Rect::new(0.0, 0.0, 110.0, 80.0), Color::rgb(0.1, 0.3, 0.9));
back.appearance.effects.push(vectorcraft_doc::Effect { id: "blur.gaussian".into(), params: serde_json::json!({"radius": 500.0}), visible: true });
let l = d.layers[0].id;
d.insert(Some(l), 0, back).unwrap();
let view = Affine::scale(2.0) * Affine::translate((-20.0, -15.0));
let mut r = Renderer::new();
let whole = r.render(&d, W, H, view, &RenderOptions::default());
let [x0, y0, x1, y1] = [40, 30, 100, 80];
let dragging = RenderOptions { interacting: true, dragging: true, ..Default::default() };
let part = r.render(&d, x1 - x0, y1 - y0, Affine::translate((-f64::from(x0), -f64::from(y0))) * view, &dragging);
assert_eq!(r.stats.previewed, 0, "drawn from the frame's raster");
for y in 0..y1 - y0 {
for x in 0..x1 - x0 {
assert_eq!(part.pixel(x, y), whole.pixel(x0 + x, y0 + y), "at {x}, {y}");
}
}
}
/// Type on a path whose path a script reshapes: its letters stand above the path and run past
/// where it ends, and they change where they were and where they are, outside the path's box.
#[test]
fn reshaping_the_path_of_type_on_a_path_changes_where_its_letters_were_and_are() {
let mut old = Document::new(110.0, 80.0);
let l = old.layers[0].id;
let on = |y: f64| {
let mut t = vectorcraft_doc::text::TextObject::point(
Point::ZERO,
"Text along a path",
vectorcraft_doc::text::CharStyle { size: 14.0, ..Default::default() },
);
let line = kurbo::Line::new((10.0, 60.0), (90.0, y)).to_path(0.1);
t.kind = vectorcraft_doc::TextKind::OnPath { path: vectorcraft_geom::PathData::from_bezpath(&line), start: 0.0, end: None };
t.xf = Affine::IDENTITY;
// The engine lays out type that changed ([`vectorcraft_doc::TextObject::cached_bounds`]).
t.cached_bounds = Some(vectorcraft_text::layout(vectorcraft_text::FontDb::global(), &t).bounds);
t
};
let text = old.insert(Some(l), 0, Node::new(NodeId(0), NodeKind::Text(Box::new(on(55.0))))).unwrap();
let mut new = old.clone();
if let Some(NodeKind::Text(t)) = new.node_mut(text).map(|n| &mut n.kind) {
**t = on(25.0);
}
redraw(&old, &new);
}
/// Stroked type with sharp corners: the miter spikes of its characters' strokes reach past half
/// the weight, and change where they were and where they are.
#[test]
fn moving_type_with_mitred_strokes_changes_where_its_spikes_reach() {
let mut old = Document::new(110.0, 80.0);
let l = old.layers[0].id;
let style = vectorcraft_doc::text::CharStyle {
size: 30.0,
stroke: Paint::solid(Color::BLACK),
stroke_width: 10.0,
stroke_join: vectorcraft_doc::LineJoin::Miter,
stroke_miter_limit: 10.0,
..Default::default()
};
let mut t = vectorcraft_doc::text::TextObject::point(Point::new(10.0, 50.0), "VAWV", style);
t.cached_bounds = Some(vectorcraft_text::layout(vectorcraft_text::FontDb::global(), &t).bounds);
let text = old.insert(Some(l), 0, Node::new(NodeId(0), NodeKind::Text(Box::new(t)))).unwrap();
let mut new = old.clone();
new.node_mut(text).unwrap().transform(Affine::translate((9.0, 7.0)), false);
redraw(&old, &new);
}
#[test]
fn an_unchanged_document_changes_nothing_and_a_new_symbol_everything() {
let (old, _) = document();
assert_eq!(redraw(&old, &old.clone()), Changed::Nothing);
let mut new = old.clone();
let art = Arc::new(rect(&mut new, Rect::new(0.0, 0.0, 5.0, 5.0), Color::BLACK));
new.symbols.push(vectorcraft_doc::Symbol { name: "Dot".into(), art });
assert_eq!(redraw(&old, &new), Changed::Everything);
}
/// While a pattern is edited its art also draws in the copies round it: an edit of it changes
/// everything.
#[test]
fn an_edit_while_a_pattern_is_edited_changes_everything() {
let (mut old, [loose, ..]) = document();
let l = old.layers[0].id;
old.pattern_edit = Some(vectorcraft_doc::PatternEdit { pattern: "Tile".into(), layer: l, original: None });
let mut new = old.clone();
new.node_mut(loose).unwrap().transform(Affine::translate((7.0, 3.0)), false);
assert_eq!(changed(&old, &new), Changed::Everything);
}