mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-13 01:14:29 -06:00
When only anchors of a path or compound path move, the area an edit changed is now the box of the segments that start or end at a moved anchor (and the line that closes an open subpath's fill at a moved end), before and after, grown by the farthest the strokes reach. The canvas then draws that part of the frame again instead of the whole object. Objects with a non-solid paint, dashes, a width profile, arrowheads, a brush, effects or an opacity mask still count whole. At 1600 x 1200, single-threaded, the frame after one anchor of a filled closed path moves takes 16.6 million CPU cycles instead of 56.4 million with 50,000 anchors, and 65.6 million instead of 180.0 million with 200,000 anchors.
424 lines
19 KiB
Rust
424 lines
19 KiB
Rust
//! Drawing again only what an edit changed gives the pixels of drawing it all again.
|
|
|
|
use crate::*;
|
|
use vectorcraft_color::{Color, Paint};
|
|
use vectorcraft_doc::Appearance;
|
|
use vectorcraft_geom::{Anchor, PathData, Point, SubPath, Vec2, 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 { dynamic: false, 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);
|
|
}
|
|
|
|
/// Type stretched more on one axis than its area grows: its characters' strokes stretch with it.
|
|
#[test]
|
|
fn moving_stretched_type_changes_where_its_stretched_strokes_reach() {
|
|
let mut old = Document::new(110.0, 80.0);
|
|
let l = old.layers[0].id;
|
|
let style = vectorcraft_doc::text::CharStyle {
|
|
size: 4.0,
|
|
stroke: Paint::solid(Color::BLACK),
|
|
stroke_width: 1.0,
|
|
stroke_join: vectorcraft_doc::LineJoin::Miter,
|
|
stroke_miter_limit: 10.0,
|
|
..Default::default()
|
|
};
|
|
let mut t = vectorcraft_doc::text::TextObject::point(Point::ZERO, "VAWV", style);
|
|
t.cached_bounds = Some(vectorcraft_text::layout(vectorcraft_text::FontDb::global(), &t).bounds);
|
|
// Its area stays, so its scale factor is 1, while it grows ten times as wide.
|
|
t.xf = Affine::translate((10.0, 40.0)) * Affine::scale_non_uniform(10.0, 0.1);
|
|
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);
|
|
area(redraw(&old, &new));
|
|
}
|
|
|
|
/// `n` anchors round an ellipse at (55, 40) with radii `rx` and `ry` and a ripple, every third a
|
|
/// corner and the others smooth with handles along the ellipse.
|
|
fn wavy(n: usize, rx: f64, ry: f64) -> SubPath {
|
|
let anchors = (0..n)
|
|
.map(|i| {
|
|
let t = i as f64 / n as f64 * std::f64::consts::TAU;
|
|
let k = 1.0 + 0.06 * (t * 12.0).sin();
|
|
let p = Point::new(55.0 + rx * k * t.cos(), 40.0 + ry * k * t.sin());
|
|
if i % 3 == 0 {
|
|
return Anchor::corner(p);
|
|
}
|
|
let along = Vec2::new(-rx * t.sin(), ry * t.cos()) * (0.4 / n as f64);
|
|
Anchor { h_in: p - along, h_out: p + along, ..Anchor::corner(p) }
|
|
})
|
|
.collect();
|
|
SubPath::new(anchors, true)
|
|
}
|
|
|
|
/// The seven-pointed star {7/3} round (55, 40), each edge split by 39 anchors on it: its edges
|
|
/// cross, so parts of it are covered more than once.
|
|
fn star() -> SubPath {
|
|
let tip = |i: usize| {
|
|
let t = (i * 3 % 7) as f64 / 7.0 * std::f64::consts::TAU;
|
|
Point::new(55.0 + 45.0 * t.sin(), 40.0 - 35.0 * t.cos())
|
|
};
|
|
let anchors = (0..7).flat_map(|i| (0..40).map(move |j| Anchor::corner(tip(i).lerp(tip(i + 1), j as f64 / 40.0)))).collect();
|
|
SubPath::new(anchors, true)
|
|
}
|
|
|
|
/// A document with path `sub` filled with `rule`, stroked `stroke` points wide when that is more
|
|
/// than 0, and its id.
|
|
fn long_path(sub: SubPath, rule: FillRule, stroke: f64) -> (Document, NodeId) {
|
|
let mut d = Document::new(110.0, 80.0);
|
|
let l = d.layers[0].id;
|
|
let id = d.alloc_id();
|
|
let line = if stroke > 0.0 { Paint::solid(Color::BLACK) } else { Paint::None };
|
|
let mut n = Node::path(id, PathData::single(sub), Appearance::basic(Paint::solid(Color::rgb(0.1, 0.5, 0.8)), line, stroke));
|
|
if let NodeKind::Path { rule: r, .. } = &mut n.kind {
|
|
*r = rule;
|
|
}
|
|
let id = d.insert(Some(l), 0, n).unwrap();
|
|
(d, id)
|
|
}
|
|
|
|
/// `d` with anchor `i` of the first subpath of path `id` and its handles moved by `by`.
|
|
fn move_anchor(d: &Document, id: NodeId, i: usize, by: Vec2) -> Document {
|
|
let mut d = d.clone();
|
|
if let Some(NodeKind::Path { path, .. }) = d.node_mut(id).map(|n| &mut n.kind) {
|
|
let a = &mut path.subpaths[0].anchors[i];
|
|
(a.p, a.h_in, a.h_out) = (a.p + by, a.h_in + by, a.h_out + by);
|
|
}
|
|
d
|
|
}
|
|
|
|
/// Moving one anchor of a long filled path changes the fill only round the segments at that
|
|
/// anchor, with either fill rule, a smooth or a corner anchor, and a closing segment.
|
|
#[test]
|
|
fn moving_an_anchor_of_a_long_path_changes_only_round_its_segments() {
|
|
for rule in [FillRule::NonZero, FillRule::EvenOdd] {
|
|
let (old, id) = long_path(wavy(2000, 45.0, 32.0), rule, 0.0);
|
|
for i in [0, 700, 1001] {
|
|
let a = area(redraw(&old, &move_anchor(&old, id, i, Vec2::new(4.0, -3.0))));
|
|
assert!(a.width() < 8.0 && a.height() < 8.0, "{rule:?}, anchor {i}: {a:?}");
|
|
}
|
|
}
|
|
}
|
|
|
|
/// The fill of an open path closes it with a line from its last anchor to its first: moving an
|
|
/// end changes where that line was and is too.
|
|
#[test]
|
|
fn moving_an_end_of_an_open_filled_path_changes_the_line_that_closes_its_fill() {
|
|
let mut half = wavy(3000, 45.0, 32.0);
|
|
half.anchors.truncate(1500);
|
|
half.closed = false;
|
|
let (old, id) = long_path(half, FillRule::NonZero, 0.0);
|
|
for i in [0, 1499] {
|
|
redraw(&old, &move_anchor(&old, id, i, Vec2::new(0.0, 12.0)));
|
|
}
|
|
}
|
|
|
|
/// Where a path covers its middle twice, the two fill rules draw it differently: an anchor moved
|
|
/// across that part changes only round its segments.
|
|
#[test]
|
|
fn moving_an_anchor_where_a_path_crosses_itself_changes_only_round_its_segments() {
|
|
for rule in [FillRule::NonZero, FillRule::EvenOdd] {
|
|
let (old, id) = long_path(star(), rule, 0.0);
|
|
let a = area(redraw(&old, &move_anchor(&old, id, 20, Vec2::new(-6.0, 5.0))));
|
|
assert!(a.width() < 12.0 && a.height() < 12.0, "{rule:?}: {a:?}");
|
|
}
|
|
}
|
|
|
|
/// A stroke with miter joints changes round the moved anchor's segments and as far as its spikes
|
|
/// reach from them, and the open end of a stroke with square caps as far as its cap's corners.
|
|
#[test]
|
|
fn moving_an_anchor_of_a_stroked_path_changes_as_far_as_its_joints_reach() {
|
|
let (old, id) = long_path(star(), FillRule::NonZero, 1.5);
|
|
let a = area(redraw(&old, &move_anchor(&old, id, 60, Vec2::new(3.0, 4.0))));
|
|
assert!(a.width() < 30.0, "{a:?}");
|
|
let mut open = star();
|
|
open.closed = false;
|
|
let (old, id) = long_path(open, FillRule::NonZero, 3.0);
|
|
let mut sq = old.clone();
|
|
if let Some(AppearanceItem::Stroke(s)) = sq.node_mut(id).unwrap().appearance.items.get_mut(1) {
|
|
s.cap = vectorcraft_doc::LineCap::Square;
|
|
}
|
|
let a = area(redraw(&sq, &move_anchor(&sq, id, 279, Vec2::new(5.0, 2.0))));
|
|
assert!(a.width() < 50.0, "{a:?}");
|
|
}
|
|
|
|
/// A compound path with a hole changes only round the moved anchor of the member it moved in.
|
|
#[test]
|
|
fn moving_an_anchor_of_a_compound_path_changes_only_round_its_segments() {
|
|
for rule in [FillRule::NonZero, FillRule::EvenOdd] {
|
|
let mut old = Document::new(110.0, 80.0);
|
|
let l = old.layers[0].id;
|
|
let (outer, inner) = (old.alloc_id(), old.alloc_id());
|
|
let mut hole = wavy(600, 25.0, 15.0);
|
|
if rule == FillRule::NonZero {
|
|
// Wound the other way, so it is a hole by either rule.
|
|
hole.anchors.reverse();
|
|
for a in &mut hole.anchors {
|
|
std::mem::swap(&mut a.h_in, &mut a.h_out);
|
|
}
|
|
}
|
|
let members = vec![
|
|
Arc::new(Node::path(outer, PathData::single(wavy(1500, 45.0, 32.0)), Appearance::default())),
|
|
Arc::new(Node::path(inner, PathData::single(hole), Appearance::default())),
|
|
];
|
|
let mut compound = Node::new(old.alloc_id(), NodeKind::Compound { children: members, rule });
|
|
compound.appearance = Appearance::basic(Paint::solid(Color::rgb(0.8, 0.3, 0.1)), Paint::None, 0.0).into();
|
|
old.insert(Some(l), 0, compound).unwrap();
|
|
let new = move_anchor(&old, inner, 150, Vec2::new(3.0, 3.0));
|
|
let a = area(redraw(&old, &new));
|
|
assert!(a.width() < 8.0 && a.height() < 8.0, "{rule:?}: {a:?}");
|
|
}
|
|
}
|
|
|
|
/// A gradient fill and a dashed stroke are laid out by the whole path: moving one anchor changes
|
|
/// all of it, as does adding an anchor.
|
|
#[test]
|
|
fn a_path_painted_by_its_whole_shape_changes_whole_when_an_anchor_moves() {
|
|
let (plain, id) = long_path(wavy(400, 45.0, 32.0), FillRule::NonZero, 1.0);
|
|
let mut graded = plain.clone();
|
|
let stops = vec![vectorcraft_color::GradientStop::new(0.0, Color::BLACK), vectorcraft_color::GradientStop::new(1.0, Color::WHITE)];
|
|
graded.node_mut(id).unwrap().appearance.fill_mut().unwrap().paint = Paint::Gradient(Box::new(vectorcraft_color::GradientPaint::new(
|
|
vectorcraft_color::Gradient::new(vectorcraft_color::GradientKind::Linear, stops),
|
|
)));
|
|
let mut dashed = plain.clone();
|
|
if let Some(AppearanceItem::Stroke(s)) = dashed.node_mut(id).unwrap().appearance.items.get_mut(1) {
|
|
s.dash = Some(vectorcraft_doc::Dash { pattern: vec![3.0, 2.0], ..Default::default() });
|
|
}
|
|
for old in [graded, dashed] {
|
|
let a = area(redraw(&old, &move_anchor(&old, id, 100, Vec2::new(2.0, 2.0))));
|
|
assert!(a.width() > 80.0 && a.height() > 60.0, "{a:?}");
|
|
}
|
|
let mut added = plain.clone();
|
|
if let Some(NodeKind::Path { path, .. }) = added.node_mut(id).map(|n| &mut n.kind) {
|
|
path.subpaths[0].anchors.insert(10, Anchor::corner(Point::new(50.0, 40.0)));
|
|
}
|
|
let a = area(redraw(&plain, &added));
|
|
assert!(a.width() > 80.0 && a.height() > 60.0, "{a:?}");
|
|
}
|