mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-13 10:45:45 +00:00
Merge pull request #6961 from dotCooCoo/fix/gradient-repeat-runs
Gradients: a repeated gradient with many stops keeps every run
This commit is contained in:
@@ -47,7 +47,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
"Expand…",
|
||||
["Object"],
|
||||
None,
|
||||
"{object?: true (symbol instances without effects (their fills' and strokes' included) and not in perspective → their art, as Break Link to Symbol places it (the instances that art holds stay instances), type → outlines, live shapes → paths, effects baked, blends → groups of their steps, repeats → groups of their instances, Intertwine groups → groups of their objects and of the parts drawn over the others (clip groups), envelopes → their distorted content, placed documents → editable copies of their art, as placing the file without link gives, its symbols, patterns, swatches and images joining the document), fill?: true (gradient fills → `gradient` art, pattern fills → their tiles in a clip group shaped like the object), stroke?: true (strokes → filled outlines, the stroked paths that baking effects leaves included), gradient?: \"objects\" (default: `steps` solid strips, concentric ellipses for radial gradients, wedges around the center for conic ones, nested squares for diamond ones) | \"mesh\" (a gradient mesh); both in a clip group shaped like the object (freeform gradients always become a mesh shaped like it), steps?: 1..1000 (255; a gradient that repeats or reflects gets the same whole number of them in each run, at least one, so a run starts on an object's edge)} one undo step; a dimension whose art an option changes becomes a plain group of its art, except one selected with art it measures, directly or in groups without fills, strokes or effects of their own: its lines and label stay as they are, and if the new art no longer fits its link it stays where it was, no longer linked → {ids}",
|
||||
"{object?: true (symbol instances without effects (their fills' and strokes' included) and not in perspective → their art, as Break Link to Symbol places it (the instances that art holds stay instances), type → outlines, live shapes → paths, effects baked, blends → groups of their steps, repeats → groups of their instances, Intertwine groups → groups of their objects and of the parts drawn over the others (clip groups), envelopes → their distorted content, placed documents → editable copies of their art, as placing the file without link gives, its symbols, patterns, swatches and images joining the document), fill?: true (gradient fills → `gradient` art, pattern fills → their tiles in a clip group shaped like the object), stroke?: true (strokes → filled outlines, the stroked paths that baking effects leaves included), gradient?: \"objects\" (default: `steps` solid strips, concentric ellipses for radial gradients, wedges around the center for conic ones, nested squares for diamond ones) | \"mesh\" (a gradient mesh); both in a clip group shaped like the object (freeform gradients always become a mesh shaped like it), steps?: 1..1000 (255; a gradient that repeats or reflects gets the same whole number of them in each run, at least one, so a run starts on an object's edge; one that needs more than 256 runs across the box stays a gradient)} one undo step; a dimension whose art an option changes becomes a plain group of its art, except one selected with art it measures, directly or in groups without fills, strokes or effects of their own: its lines and label stay as they are, and if the new art no longer fits its link it stays where it was, no longer linked → {ids}",
|
||||
has_selection,
|
||||
expand
|
||||
),
|
||||
@@ -194,10 +194,12 @@ impl Expandable {
|
||||
NodeKind::Text(t) => t.runs.iter().any(|r| r.style.has_stroke()),
|
||||
_ => shape && !c.appearance.stroke_paint().is_none(),
|
||||
};
|
||||
e.gradient |= match &c.kind {
|
||||
NodeKind::Text(t) => t.runs.iter().any(|r| matches!(r.style.fill, Paint::Gradient(_))),
|
||||
_ => shape && c.appearance.items.iter().any(|i| gradient_fill(i).is_some()),
|
||||
};
|
||||
// Once one is found, the next path is not measured.
|
||||
e.gradient = e.gradient
|
||||
|| match &c.kind {
|
||||
NodeKind::Text(t) => t.runs.iter().any(|r| matches!(r.style.fill, Paint::Gradient(_))),
|
||||
_ => shape && gradient_expands(d, c),
|
||||
};
|
||||
e.fill |= e.gradient;
|
||||
e.fill |= match &c.kind {
|
||||
NodeKind::Text(t) => t.runs.iter().any(|r| matches!(&r.style.fill, Paint::Pattern { pattern, .. } if d.pattern(pattern).is_some())),
|
||||
@@ -222,6 +224,43 @@ fn gradient_fill(i: &AppearanceItem) -> Option<(&FillLayer, &GradientPaint)> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether path `l` has a gradient fill that Fill expands ([`expand_fills`]): one that repeats or
|
||||
/// reflects stays a gradient when its box needs more runs than are written ([`padded_on`]). For a
|
||||
/// path that live-effect plug-ins paint, whether a piece of the art they paint has one
|
||||
/// ([`expand_pieces`]).
|
||||
fn gradient_expands(d: &Document, l: &Node) -> bool {
|
||||
if !l.appearance.items.iter().any(|i| gradient_fill(i).is_some()) {
|
||||
return false;
|
||||
}
|
||||
if !vectorcraft_render::effects::has_painted_effects(l) {
|
||||
return piece_gradient_expands(d, l);
|
||||
}
|
||||
let Some(art) = vectorcraft_render::effects::reshape(Some(d), l, None) else { return false };
|
||||
let mut any = false;
|
||||
art.walk(&mut |c| {
|
||||
any = any
|
||||
|| (matches!(c.kind, NodeKind::Path { .. } | NodeKind::Compound { .. })
|
||||
&& !c.is_ruler_guide()
|
||||
&& c.appearance.items.iter().any(|i| gradient_fill(i).is_some())
|
||||
&& piece_gradient_expands(d, c));
|
||||
});
|
||||
any
|
||||
}
|
||||
|
||||
/// [`gradient_expands`] for a path that no live-effect plug-in paints ([`expand_piece_fills`]).
|
||||
fn piece_gradient_expands(d: &Document, l: &Node) -> bool {
|
||||
if !l.appearance.items.iter().filter_map(gradient_fill).any(|(_, g)| g.gradient.is_spread()) {
|
||||
return l.appearance.items.iter().any(|i| gradient_fill(i).is_some());
|
||||
}
|
||||
let reshaped = reshaped(d, l);
|
||||
let l = reshaped.as_ref().unwrap_or(l);
|
||||
let Some((path, _)) = node_path(l) else { return false };
|
||||
let Some(bounds) = path.bounds() else { return false };
|
||||
l.appearance.items.iter().filter(|i| gradient_fill(i).is_some()).any(|item| {
|
||||
fill_shape(d, l, item, &path, bounds).is_some_and(|(_, _, b, moved)| gradient_fill(&moved).is_some_and(|(_, g)| padded_on(g, b).is_some()))
|
||||
})
|
||||
}
|
||||
|
||||
/// The definition and placement of a visible pattern fill whose pattern `d` defines.
|
||||
fn pattern_fill<'a, 'd>(d: &'d Document, i: &'a AppearanceItem) -> Option<(&'a FillLayer, &'d PatternDef, Affine)> {
|
||||
match i {
|
||||
@@ -476,8 +515,9 @@ fn gradient_art(d: &mut Document, l: &Node, path: &PathData, bounds: Rect, f: &F
|
||||
let m = GradientMesh::for_path_with(path, FREEFORM_MESH, FREEFORM_MESH, &*gradient_sampler(g, bounds, d.color_mode.model()))?;
|
||||
vec![Node::new(d.alloc_id(), NodeKind::Mesh(m))]
|
||||
} else {
|
||||
// A gradient that repeats or reflects expands as its stops repeated over the box.
|
||||
let (g, runs) = padded_on(g, bounds);
|
||||
// A gradient that repeats or reflects expands as its stops repeated over the box (and
|
||||
// stays a gradient when the box needs more runs than are written).
|
||||
let (g, runs) = padded_on(g, bounds)?;
|
||||
let g = &*g;
|
||||
let frame = Frame::new(g, bounds);
|
||||
match to {
|
||||
@@ -1244,26 +1284,32 @@ pub(crate) fn gradient_sampler(g: &GradientPaint, bounds: Rect, doc_model: Model
|
||||
Box::new(move |p| g.gradient.sample_keep_model(spread(geom.param_at(g.gradient.kind, p)) as f32))
|
||||
}
|
||||
|
||||
/// For stops repeated by [`padded_on`]: where along the vector (0 to 1) their first run starts and
|
||||
/// how many runs lie evenly from there to 1.
|
||||
type Runs = Option<(f64, usize)>;
|
||||
|
||||
/// `g` placed on box `bounds` as a gradient that pads past its ends: a linear, radial or diamond
|
||||
/// gradient that repeats or reflects with its stops repeated as many times as the box needs
|
||||
/// ([`vectorcraft_color::Gradient::padded_over`]), a conic one with its stops repeated around the
|
||||
/// turn ([`vectorcraft_color::Gradient::unrolled`]); others as they are. With it, for the repeated
|
||||
/// stops, where along the vector (0 to 1) their first run starts and how many runs lie evenly from
|
||||
/// there to 1.
|
||||
fn padded_on(g: &GradientPaint, bounds: Rect) -> (std::borrow::Cow<'_, GradientPaint>, Option<(f64, usize)>) {
|
||||
/// there to 1. `None` when the box needs more runs than are written
|
||||
/// ([`vectorcraft_color::Padded::capped`]): the gradient stays.
|
||||
fn padded_on(g: &GradientPaint, bounds: Rect) -> Option<(std::borrow::Cow<'_, GradientPaint>, Runs)> {
|
||||
use std::borrow::Cow;
|
||||
if !g.gradient.is_spread() {
|
||||
return (Cow::Borrowed(g), None);
|
||||
return Some((Cow::Borrowed(g), None));
|
||||
}
|
||||
let geom = g.resolve(bounds);
|
||||
match g.gradient.padded_over(&geom, bounds) {
|
||||
Some(p) if p.capped => None,
|
||||
Some(p) => {
|
||||
let runs = Some((p.first, p.runs));
|
||||
(Cow::Owned(GradientPaint { gradient: p.gradient, geom: Some(p.geom), angle: p.geom.angle_deg(), ..g.clone() }), runs)
|
||||
Some((Cow::Owned(GradientPaint { gradient: p.gradient, geom: Some(p.geom), angle: p.geom.angle_deg(), ..g.clone() }), runs))
|
||||
}
|
||||
None => {
|
||||
let runs = Some((0.0, g.gradient.repeat_count() as usize));
|
||||
(Cow::Owned(GradientPaint { gradient: g.gradient.unrolled().into_owned(), geom: Some(geom), ..g.clone() }), runs)
|
||||
Some((Cow::Owned(GradientPaint { gradient: g.gradient.unrolled().into_owned(), geom: Some(geom), ..g.clone() }), runs))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user