mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-01 13:47:21 -06:00
Merge pull request #4828 from dotCooCoo/fix/brush-warp-mask
Brushes: opacity masks on art brush pieces bend with the art
This commit is contained in:
@@ -41,7 +41,8 @@ const MASK_STEPS: usize = 32;
|
||||
/// space, so the art keeps its curves (#3926). Where that cannot be done in no more anchors than
|
||||
/// chords would take, the path is flattened with `flat_tol` (art space) and subdivided so no chord
|
||||
/// is longer than `max_du` along x, as it always was. Stroke widths scale by `width_scale`.
|
||||
/// Text, images and symbol instances can't bend and are dropped.
|
||||
/// Text, images and symbol instances can't bend and are dropped. The art of a linked opacity mask
|
||||
/// bends with the object it masks.
|
||||
pub(crate) fn warp_node(n: &Node, f: &dyn Fn(Point) -> Point, flat_tol: f64, max_du: f64, doc_tol: f64, width_scale: f64) -> Option<Node> {
|
||||
let mut out = n.clone();
|
||||
out.appearance.scale_strokes(width_scale);
|
||||
@@ -63,9 +64,21 @@ pub(crate) fn warp_node(n: &Node, f: &dyn Fn(Point) -> Point, flat_tol: f64, max
|
||||
}
|
||||
_ => return None,
|
||||
}
|
||||
map_mask(&mut out, |art| warp_node(art, f, flat_tol, max_du, doc_tol, width_scale));
|
||||
Some(out)
|
||||
}
|
||||
|
||||
/// Replace the art of `n`'s linked opacity mask, if it has one, with what `map` makes of it. Where
|
||||
/// `map` draws nothing, the mask's art becomes an empty group.
|
||||
fn map_mask(n: &mut Node, map: impl FnOnce(&Node) -> Option<Node>) {
|
||||
if let Some(m) = n.extras_if_any().and_then(|e| e.mask.as_mut())
|
||||
&& m.linked
|
||||
{
|
||||
let id = m.art.id;
|
||||
m.art = Arc::new(map(&m.art).unwrap_or_else(|| Node::group(id, vec![])));
|
||||
}
|
||||
}
|
||||
|
||||
/// Each subpath of `p` through `f`: fitted cubics when they take no more anchors than the chords
|
||||
/// would, else the chords. Points `f` cannot place (not finite) are left out of the chords.
|
||||
fn warp_path(p: &PathData, f: &dyn Fn(Point) -> Point, flat_tol: f64, max_du: f64, fit_tol: f64) -> PathData {
|
||||
@@ -338,7 +351,8 @@ fn affine_fit(f: &dyn Fn(Point) -> Point, b: Rect, tol: f64) -> Option<Affine> {
|
||||
/// Everything else matches [`warp_node`] deliberately, so curves are the only difference between
|
||||
/// the two routes: the same node kinds are kept and the rest dropped, `live` is cleared on every
|
||||
/// path, an empty path or a group left with no children is dropped, and strokes are scaled by
|
||||
/// `width_scale` at every level rather than by the transform's own factor.
|
||||
/// `width_scale` at every level rather than by the transform's own factor, the art of linked
|
||||
/// opacity masks included.
|
||||
fn affine_node(n: &Node, a: Affine, width_scale: f64) -> Option<Node> {
|
||||
let mut out = prepared(n, width_scale)?;
|
||||
out.transform(a, false);
|
||||
@@ -365,6 +379,7 @@ fn prepared(n: &Node, width_scale: f64) -> Option<Node> {
|
||||
}
|
||||
_ => return None,
|
||||
}
|
||||
map_mask(&mut out, |art| prepared(art, width_scale));
|
||||
Some(out)
|
||||
}
|
||||
|
||||
@@ -416,6 +431,7 @@ fn widen_strokes(n: &Node, factor: &dyn Fn(&Node) -> f64) -> Node {
|
||||
if let NodeKind::Group { children, .. } | NodeKind::Layer { children, .. } | NodeKind::Compound { children, .. } = &mut out.kind {
|
||||
*children = children.iter().map(|c| Arc::new(widen_strokes(c, factor))).collect();
|
||||
}
|
||||
map_mask(&mut out, |art| Some(widen_strokes(art, factor)));
|
||||
out
|
||||
}
|
||||
|
||||
@@ -590,6 +606,9 @@ fn anchors_of(n: &Node) -> usize {
|
||||
if let Some(c) = n.children() {
|
||||
stack.extend(c.iter().map(Arc::as_ref));
|
||||
}
|
||||
if let Some(m) = &n.extras().mask {
|
||||
stack.push(&m.art);
|
||||
}
|
||||
}
|
||||
total
|
||||
}
|
||||
@@ -1133,4 +1152,52 @@ mod tests {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A linked opacity mask on brush art bends and widens with the art, on a bent run and on a
|
||||
/// straight one, so a mask drawn over the art still covers it. An unlinked mask stays put.
|
||||
#[test]
|
||||
fn a_linked_opacity_mask_bends_with_the_art() {
|
||||
let b = Rect::new(0.0, -20.0, 40.0, 20.0);
|
||||
let stroked = |id: u64| {
|
||||
Node::path(
|
||||
NodeId(id),
|
||||
vectorcraft_geom::shapes::ellipse(b),
|
||||
Appearance::basic(
|
||||
vectorcraft_color::Paint::solid(vectorcraft_color::Color::BLACK),
|
||||
vectorcraft_color::Paint::solid(vectorcraft_color::Color::BLACK),
|
||||
2.0,
|
||||
),
|
||||
)
|
||||
};
|
||||
let masked = |linked: bool| {
|
||||
let mut n = stroked(0);
|
||||
let mut mask = vectorcraft_doc::OpacityMask::new(stroked(1), true);
|
||||
mask.linked = linked;
|
||||
n.extras_mut().mask = Some(Box::new(mask));
|
||||
n
|
||||
};
|
||||
let round = vectorcraft_geom::shapes::ellipse(Rect::new(0.0, 0.0, 400.0, 400.0)).to_bezpath();
|
||||
let bent_track = tracks(&round, 0.05).remove(0);
|
||||
let mut line = BezPath::new();
|
||||
line.move_to((0.0, 0.0));
|
||||
line.line_to((320.0, 0.0));
|
||||
let straight_track = tracks(&line, 0.01).remove(0);
|
||||
let along = AlongMap::linear(b.width(), 100.0, 140.0);
|
||||
for (name, t) in [("bent", &bent_track), ("straight", &straight_track)] {
|
||||
let out = bend(t, &masked(true), b, along, 2.0).expect("the art is drawn");
|
||||
let mask = out.extras().mask.as_ref().expect("the mask is kept");
|
||||
assert_eq!(mask.art.kind, out.kind, "{name} run: the mask's art bends with the art it was drawn over");
|
||||
assert_eq!(mask.art.appearance, out.appearance, "{name} run: the mask's strokes widen with the art's");
|
||||
|
||||
let out = bend(t, &masked(false), b, along, 2.0).expect("the art is drawn");
|
||||
let mask = out.extras().mask.as_ref().expect("the mask is kept");
|
||||
assert_eq!(*mask.art, stroked(1), "{name} run: an unlinked mask stays where it was");
|
||||
|
||||
let out = bend_widened(t, &masked(true), b, along, 2.0, &|_| 0.5).expect("the art is drawn");
|
||||
let mask = out.extras().mask.as_ref().expect("the mask is kept");
|
||||
assert_eq!(mask.art.kind, out.kind, "{name} run, widened: the mask's art bends with the art");
|
||||
assert_eq!(mask.art.appearance, out.appearance, "{name} run, widened: the mask's strokes narrow with the art's");
|
||||
}
|
||||
assert_eq!(anchors_of(&masked(true)), 8, "the anchor budget counts the mask's art");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user