mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-15 21:57:04 +00:00
Effects: a painted clipping path's stroke keeps its Transform stroke scaling
A fill or stroke of a painted clipping path whose Transform effect scales strokes or moves patterns is now drawn through its art, as an export bakes such a path, so a 2 pt stroke scaled to 200 % paints at 4 pt. The HPGL writer closes the stroke pieces of a clipping path whose fill paints, as it closes the outline of any filled path.
This commit is contained in:
@@ -596,6 +596,9 @@ struct Walk<'a> {
|
||||
clips: Vec<Region>,
|
||||
/// The points those masks hold ([`MAX_MASK_POINTS`]).
|
||||
mask_points: usize,
|
||||
/// Set while the stroke pieces of a clipping path whose fill paints are drawn: they close, as
|
||||
/// its fill closes its outline.
|
||||
closing: bool,
|
||||
runs: Vec<Run>,
|
||||
points: usize,
|
||||
clip_tests: u64,
|
||||
@@ -624,6 +627,7 @@ impl<'a> Walk<'a> {
|
||||
tolerance,
|
||||
clips: vec![],
|
||||
mask_points: 0,
|
||||
closing: false,
|
||||
runs: vec![],
|
||||
points: 0,
|
||||
clip_tests: 0,
|
||||
@@ -855,9 +859,15 @@ impl<'a> Walk<'a> {
|
||||
}
|
||||
// Drawn whole where its stroke paints: a fill closes its outline, and type's characters
|
||||
// without a stroke are drawn too. Pieces of a clipping path whose fill and stroke effects
|
||||
// were applied are drawn in its stroke pieces alone.
|
||||
// were applied are drawn in its stroke pieces alone, closed where its fill paints.
|
||||
if let Some(stroke) = &paint.stroke {
|
||||
self.node(if first.extras().clip_pieces { stroke } else { first }, false);
|
||||
if first.extras().clip_pieces {
|
||||
let closing = std::mem::replace(&mut self.closing, paint.fill.as_ref().is_some_and(fills_in_pieces));
|
||||
self.node(stroke, false);
|
||||
self.closing = closing;
|
||||
} else {
|
||||
self.node(first, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -877,7 +887,8 @@ impl<'a> Walk<'a> {
|
||||
|
||||
/// A path drawn in its color: the topmost stroke's that paints, else the topmost fill's that
|
||||
/// paints (visible, with a paint and some opacity). A filled path's every subpath is closed,
|
||||
/// as its fill closes it, also when its stroke gives the color.
|
||||
/// as its fill closes it, also when its stroke gives the color, and so is each stroke piece
|
||||
/// of a clipping path whose fill paints ([`Walk::closing`]).
|
||||
fn shape(&mut self, n: &Node, bp: &BezPath) {
|
||||
if bp.elements().is_empty() {
|
||||
return;
|
||||
@@ -899,7 +910,7 @@ impl<'a> Walk<'a> {
|
||||
(None, None) => return,
|
||||
};
|
||||
let rgb = self.flat(paint);
|
||||
self.draw(rgb, bp, fill.is_some());
|
||||
self.draw(rgb, bp, fill.is_some() || self.closing);
|
||||
}
|
||||
|
||||
/// `bp` drawn in `rgb`: flattened (with `close` every subpath closed), then cut and added.
|
||||
@@ -1155,6 +1166,12 @@ fn strokes_in_pieces(n: &Node) -> bool {
|
||||
n.children().is_some_and(|ch| ch.iter().filter(|c| c.visible && c.opacity > 0.0).any(|c| top_stroke(c).is_some() || strokes_in_pieces(c)))
|
||||
}
|
||||
|
||||
/// Whether a member of `n`, at any depth of its groups, has a fill that paints ([`top_fill`]),
|
||||
/// with it and the groups around it shown and not fully transparent.
|
||||
fn fills_in_pieces(n: &Node) -> bool {
|
||||
n.children().is_some_and(|ch| ch.iter().filter(|c| c.visible && c.opacity > 0.0).any(|c| top_fill(c).is_some() || fills_in_pieces(c)))
|
||||
}
|
||||
|
||||
/// The paint of the topmost fill of `n` that paints: visible, with a paint and some opacity.
|
||||
fn top_fill(n: &Node) -> Option<&Paint> {
|
||||
n.appearance.items.iter().rev().find_map(|i| match i {
|
||||
|
||||
@@ -793,6 +793,40 @@ fn a_clipping_path_is_drawn_where_it_paints() {
|
||||
// In a group at 0% opacity they are not drawn, and the clipping path is drawn in its fill.
|
||||
let faded = grouped(0.0);
|
||||
assert_eq!(plot(&faded, &opts(&faded)).pens, [[0, 0, 255], [0, 0, 0]]);
|
||||
// An open clipping path with a fill is drawn closed also where its stroke's effect moved it.
|
||||
let open_moved = |fill_opacity: f32| {
|
||||
doc_with(|d| {
|
||||
let mut open = BezPath::new();
|
||||
open.move_to((20.0, 20.0));
|
||||
open.line_to((40.0, 20.0));
|
||||
open.line_to((40.0, 40.0));
|
||||
let a = Appearance::basic(Paint::solid(Color::rgb8(0, 0, 255)), Paint::solid(Color::rgb8(255, 0, 0)), 1.0);
|
||||
let mut clip = Node::path(d.alloc_id(), PathData::from_bezpath(&open), a);
|
||||
for item in &mut clip.appearance.items {
|
||||
match item {
|
||||
AppearanceItem::Fill(f) => f.opacity = fill_opacity,
|
||||
AppearanceItem::Stroke(s) => {
|
||||
let params = serde_json::json!({"moveH": 5});
|
||||
s.effects.push(vectorcraft_doc::Effect { id: "distort.transform".into(), params, visible: true });
|
||||
}
|
||||
}
|
||||
}
|
||||
let black = Appearance::basic(Paint::None, Paint::solid(Color::BLACK), 1.0);
|
||||
let clipped = Node::path(d.alloc_id(), line((0.0, 30.0), (200.0, 30.0)), black);
|
||||
let mut group = Node::group(d.alloc_id(), vec![Arc::new(clip), Arc::new(clipped)]);
|
||||
if let NodeKind::Group { clip, .. } = &mut group.kind {
|
||||
*clip = true;
|
||||
}
|
||||
vec![group]
|
||||
})
|
||||
};
|
||||
let filled = open_moved(1.0);
|
||||
let t = text(&plot(&filled, &opts(&filled)));
|
||||
assert!(t.ends_with("SP2;PU353,1129;PD635,1129,635,847,353,1129;PU;SP0;"), "{t}");
|
||||
// A fill at 0% opacity does not close it.
|
||||
let unfilled = open_moved(0.0);
|
||||
let t = text(&plot(&unfilled, &opts(&unfilled)));
|
||||
assert!(t.ends_with("SP2;PU353,1129;PD635,1129,635,847;PU;SP0;"), "{t}");
|
||||
}
|
||||
|
||||
/// A symbol `art` and an instance of it at its place.
|
||||
|
||||
@@ -91,6 +91,17 @@ fn clip_effects_applied(n: &Node, items: bool, take: fn() -> bool) -> Option<Nod
|
||||
.iter()
|
||||
.zip(&p.appearance.items)
|
||||
.map(|(item, carried)| {
|
||||
// A fill or stroke whose Transform effect scales strokes or moves patterns is drawn
|
||||
// through its art, as an export bakes such a path ([`crate::reshape`]).
|
||||
let mut whole = Node::path(n.id, g.clone(), Appearance { items: vec![carried.clone()], ..Default::default() });
|
||||
if let NodeKind::Path { rule: r, .. } = &mut whole.kind {
|
||||
*r = rule;
|
||||
}
|
||||
if crate::has_transform_art(&whole)
|
||||
&& let Some(art) = crate::reshape(None, &whole, None)
|
||||
{
|
||||
return std::sync::Arc::new(art);
|
||||
}
|
||||
let (ig, mut it) = crate::bake::apply_item(item, carried, &g, &ctx);
|
||||
crate::bake::clear_item_effects(&mut it);
|
||||
let mut piece = Node::path(n.id, ig, Appearance { items: vec![it], ..Default::default() });
|
||||
|
||||
@@ -459,6 +459,40 @@ fn a_painted_clipping_paths_fill_and_stroke_effects_apply_before_the_effects_aro
|
||||
assert_eq!(stroke.children().map(|ch| ch.len()), Some(1), "only the stroke's group: {stroke:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_painted_clipping_paths_stroke_keeps_the_width_its_transform_scales_it_to() {
|
||||
// A compound clipping path with a 2 pt stroke whose Transform scales strokes to 200 % without
|
||||
// transforming the object: the export paints the stroke at 4 pt.
|
||||
let square = Node::path(NodeId(4), shapes::rectangle(Rect::new(50.0, 50.0, 150.0, 150.0)), Appearance::default());
|
||||
let mut clip = Node::new(NodeId(3), NodeKind::Compound { children: vec![Arc::new(square)], rule: vectorcraft_geom::FillRule::NonZero });
|
||||
clip.appearance = Appearance::basic(Paint::solid(Color::BLACK), Paint::solid(Color::rgb(0.0, 0.0, 1.0)), 2.0).into();
|
||||
let scale = json!({"scaleH": 200, "scaleV": 200, "objects": false, "strokes": true});
|
||||
for item in &mut clip.appearance.items {
|
||||
if let vectorcraft_doc::AppearanceItem::Stroke(s) = item {
|
||||
s.effects.push(fx("distort.transform", scale.clone()));
|
||||
}
|
||||
}
|
||||
fn widths(n: &Node, out: &mut Vec<f64>) {
|
||||
for item in &n.appearance.items {
|
||||
if let vectorcraft_doc::AppearanceItem::Stroke(s) = item {
|
||||
out.push(s.width);
|
||||
}
|
||||
}
|
||||
n.children().into_iter().flatten().for_each(|c| widths(c, out));
|
||||
}
|
||||
let mut d = Document::new(400.0, 400.0);
|
||||
let layer = d.layers[0].id;
|
||||
d.insert(Some(layer), 0, clip_group(2, clip)).unwrap();
|
||||
let baked = baked_clip(&d);
|
||||
let stroke = baked.clip_paint().stroke.unwrap();
|
||||
let mut found = vec![];
|
||||
widths(&stroke, &mut found);
|
||||
assert!(!found.is_empty() && found.iter().all(|w| (w - 4.0).abs() < 1e-6), "{found:?} {stroke:?}");
|
||||
// The stroke is still drawn where the path lies, as the Transform does not move the object.
|
||||
let bounds = stroke.geometric_bounds().unwrap();
|
||||
assert!((bounds.x0 - 50.0).abs() < 0.5 && (bounds.width() - 100.0).abs() < 0.5, "{bounds:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_baked_clipping_path_gives_its_pieces_ids_of_their_own() {
|
||||
// An unpainted clipping path whose fill and stroke have Transform effects, in a clip group
|
||||
|
||||
Reference in New Issue
Block a user