mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-08 06:01:12 +00:00
.ai import: a clipping path keeps its stroke
A clipping path painted with a stroke (`W S`, `W B`) lost the stroke when a .ai file opened from its editing data. The clipping path now keeps it, and it paints over the clipped art as a clipping path's stroke does in VectorCraft. A gradient-filled clipping path stroked with `1 BB` or `2 BB` put the stroke on the painted copy of its fill, under the clipped art; it now goes on the clipping path too. Several clipping paths in one group make one compound path, which keeps their stroke, opacity and blend mode when they all have the same ones.
This commit is contained in:
@@ -185,7 +185,8 @@ impl<'a> Reader<'a> {
|
||||
..FillLayer::new(fill_paint.clone().unwrap_or(gs.fill.clone()))
|
||||
}));
|
||||
}
|
||||
if stroke && !clip {
|
||||
// A clipping path keeps its stroke, which paints over the art it clips.
|
||||
if stroke {
|
||||
let mut st = StrokeLayer::new(stroke_paint.unwrap_or(gs.stroke.clone()), gs.width);
|
||||
st.cap = gs.cap;
|
||||
st.join = gs.join;
|
||||
@@ -297,11 +298,20 @@ impl<'a> Reader<'a> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Stroke the path just painted (a gradient-filled one, `1 BB` or `2 BB`) with the stroke's state.
|
||||
/// Stroke the path just painted (a gradient-filled one, `1 BB` or `2 BB`) with the stroke's state. A clipping path
|
||||
/// takes the stroke itself, not the painted copy of its fill.
|
||||
pub(super) fn stroke_last(&mut self) {
|
||||
let gs = self.gs.clone();
|
||||
let Some(n) = self.last_object() else { return };
|
||||
if !matches!(n.kind, NodeKind::Path { clipping: false, .. }) || n.appearance.items.iter().any(|i| matches!(i, AppearanceItem::Stroke(_))) {
|
||||
if !self.after_object {
|
||||
return;
|
||||
}
|
||||
let Some(kids) = self.frames.last_mut().map(|f| &mut f.children) else { return };
|
||||
let Some(mut at) = kids.len().checked_sub(1) else { return };
|
||||
if kids.get(at).is_some_and(|n| self.clip_fills.contains(&n.id)) && at > 0 {
|
||||
at -= 1;
|
||||
}
|
||||
let Some(n) = kids.get_mut(at) else { return };
|
||||
if !matches!(n.kind, NodeKind::Path { .. }) || n.appearance.items.iter().any(|i| matches!(i, AppearanceItem::Stroke(_))) {
|
||||
return;
|
||||
}
|
||||
let mut st = StrokeLayer::new(gs.stroke.clone(), gs.width);
|
||||
|
||||
@@ -195,8 +195,19 @@ impl<'a> Reader<'a> {
|
||||
1 => clip.pop().map(|(_, n)| n),
|
||||
_ => {
|
||||
let rule = clip.first().map_or(FillRule::NonZero, |(_, n)| path_rule(n));
|
||||
// The compound path keeps the clipping paths' stroke, opacity and blend mode when they all have the same
|
||||
// ones: it has one appearance for all its paths.
|
||||
let look = |n: &Node| (n.appearance.clone(), n.opacity, n.blend);
|
||||
let shared =
|
||||
clip.split_first().and_then(|((_, first), others)| others.iter().all(|(_, n)| look(n) == look(first)).then(|| look(first)));
|
||||
let children = clip.into_iter().map(|(_, n)| Arc::new(n)).collect();
|
||||
Some(Node::new(self.doc.alloc_id(), NodeKind::Compound { children, rule }))
|
||||
let mut n = Node::new(self.doc.alloc_id(), NodeKind::Compound { children, rule });
|
||||
if let Some((appearance, opacity, blend)) = shared {
|
||||
n.appearance = appearance;
|
||||
n.opacity = opacity;
|
||||
n.blend = blend;
|
||||
}
|
||||
Some(n)
|
||||
}
|
||||
};
|
||||
let clips = clip_node.is_some();
|
||||
|
||||
@@ -360,6 +360,53 @@ fn compound_paths_and_clipping() {
|
||||
assert!(*clip && matches!(children[0].kind, NodeKind::Path { clipping: true, .. }));
|
||||
}
|
||||
|
||||
/// A stroked clipping path (`W S`, `W B`, or a gradient fill with `1 BB`) keeps its stroke on the clipping path, and a
|
||||
/// fill goes on the painted copy under the clipped art.
|
||||
#[test]
|
||||
fn a_clipping_paths_stroke_stays_on_the_clipping_path() {
|
||||
let blue = Some(Color::rgb(0.0, 0.0, 1.0));
|
||||
let red = Some(Color::rgb(1.0, 0.0, 0.0));
|
||||
// (clipping, fill colour, a gradient fill, stroke colour and width) of each of the clipping group's children.
|
||||
let children = |doc: &Document| {
|
||||
let NodeKind::Group { children, clip: true } = &art(doc)[0].kind else { panic!("{:?}", art(doc)[0].kind) };
|
||||
children
|
||||
.iter()
|
||||
.map(|c| {
|
||||
let clipping = matches!(c.kind, NodeKind::Path { clipping: true, .. });
|
||||
(clipping, fill(c).color(), matches!(fill(c), Paint::Gradient(_)), c.appearance.stroke().map(|s| (s.paint.color(), s.width)))
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
};
|
||||
let under = rect_path(0.0, 0.0, 30.0, 30.0);
|
||||
let clip = rect_path(5.0, 5.0, 25.0, 25.0);
|
||||
let stroked = format!("0 Ae\nq\n{RED}{under}f\n0 0 1 0 0 0 1 XA\n3 w\n{clip}W\nS\nQ\n");
|
||||
assert_eq!(children(&read_ok(&layer(&stroked)).doc), [(true, None, false, Some((blue, 3.0))), (false, red, false, None)]);
|
||||
let both = format!("0 Ae\nq\n{RED}{under}f\n0 1 0 0 0 1 0 Xa\n0 0 1 0 0 0 1 XA\n3 w\n{clip}W\nB\nQ\n");
|
||||
assert_eq!(
|
||||
children(&read_ok(&layer(&both)).doc),
|
||||
[(true, None, false, Some((blue, 3.0))), (false, Some(Color::rgb(0.0, 1.0, 0.0)), false, None), (false, red, false, None)]
|
||||
);
|
||||
let defs = "%AI5_BeginGradient: (G)\n(G) 0 2 Bd\n[\n0 0 0 0 1 0 0 2 0 6 %_BS\n%_0 0 0 0 1 0 0 2 0 6 0 0 Bs\n\
|
||||
%_0 0 0 0 0 0 1 2 1 6 100 50 Bs\nBD\n%AI5_EndGradient\n";
|
||||
let gradient = format!("0 Ae\nq\n{RED}{under}f\n0 0 1 0 0 0 1 XA\n3 w\n{clip}W\nBb\n0 0 0 0 Bh\n1 (G) 5 5 0 20 1 0 0 1 0 0 1 Bg\nf\n1 BB\nQ\n");
|
||||
assert_eq!(
|
||||
children(&read_ok(&format!("{defs}{}", layer(&gradient))).doc),
|
||||
[(true, None, false, Some((blue, 3.0))), (false, None, true, None), (false, red, false, None)]
|
||||
);
|
||||
// Several clipping paths clip as one compound path, which keeps their stroke when they all have the same one.
|
||||
let inner = rect_path(10.0, 10.0, 20.0, 20.0);
|
||||
let compound_stroke = |first: &str, second: &str| {
|
||||
let body = format!("0 Ae\nq\n{RED}{under}f\n0 0 1 0 0 0 1 XA\n3 w\n{first}{clip}W\nS\n{second}{inner}W\nS\nQ\n");
|
||||
let doc = read_ok(&layer(&body)).doc;
|
||||
let NodeKind::Group { children, clip: true } = &art(&doc)[0].kind else { panic!("{:?}", art(&doc)[0].kind) };
|
||||
assert!(matches!(children[0].kind, NodeKind::Compound { .. }), "{:?}", children[0].kind);
|
||||
(children[0].appearance.stroke().map(|s| (s.paint.color(), s.width)), children[0].opacity, children[0].blend)
|
||||
};
|
||||
assert_eq!(compound_stroke("", ""), (Some((blue, 3.0)), 1.0, BlendMode::Normal));
|
||||
assert_eq!(compound_stroke("9 0.5 0 0 0 Xy\n", ""), (Some((blue, 3.0)), 0.5, BlendMode::Lighten));
|
||||
assert_eq!(compound_stroke("", "5 w\n"), (None, 1.0, BlendMode::Normal), "strokes that differ");
|
||||
}
|
||||
|
||||
/// A clipping group's clipping path can sit alone in a group of its own, after the art it clips (`q … u <path> W n U
|
||||
/// … Q`): it still clips the clipping group.
|
||||
#[test]
|
||||
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user