Gradients: a gradient spanning the selection on a lower stroke fits the box that stroke paints

With span: "selection", paint.setStroke placed the gradient on the union of
each target's topmost stroke box, also when `item` named a lower stroke,
including an aligned row of a graph design's art. With a 40 pt lower stroke
under a 1 pt topmost one, the gradient reached 0.5 pt past the objects
instead of 20 pt. Each target's box is now that of the row it edits.
This commit is contained in:
dotCooCoo
2026-11-15 08:34:35 -08:00
parent bd67a010f6
commit 133e54b970
2 changed files with 38 additions and 9 deletions
+16 -9
View File
@@ -555,9 +555,15 @@ pub(crate) fn apply_paint(s: &mut Session, p: &Value, paint: Paint, fill: bool)
let ids = super::graph::paint_targets(&s.doc()?.doc, &selected);
let of = super::appearance::reference_of(s, p, item, cmd)?;
let paint = if span {
// With a design's art, the gradient spans only the objects with a row equal to the art's, which take it.
let spanned: Vec<NodeId> = ids.iter().zip(item.per_target_of(s, of, &ids)).filter_map(|(id, it)| it.map(|_| *id)).collect();
placed_across(&s.doc()?.doc, &spanned, paint, p, fill)
// With a design's art, the gradient spans only the objects with a row equal to the art's, which take it. Each
// object's box is that of the row it edits.
let d = &s.doc()?.doc;
let mut spanned = Vec::with_capacity(ids.len());
for (id, it) in ids.iter().zip(item.per_target_of(s, of, &ids)) {
let (Some(it), Some(n)) = (it, d.node(*id)) else { continue };
spanned.push((*id, it.resolve(&n.appearance, fill, cmd)?));
}
placed_across(d, &spanned, paint, p, fill)
} else {
paint
};
@@ -730,14 +736,15 @@ fn spans_targets(p: &Value, paint: &Paint) -> std::result::Result<bool, String>
Ok(true)
}
/// `paint` placed once on the union of the bounds of `ids` (each one's fill box, or the box its
/// stroke paints), at the gradient's angle and aspect: every target gets the same vector in
/// document coordinates, so the ramp runs on across them (#2127).
fn placed_across(d: &Document, ids: &[NodeId], paint: Paint, p: &Value, fill: bool) -> Paint {
/// `paint` placed once on the union of the bounds of `ids` (each one's fill box, or the box the
/// stroke it edits paints: the row given with it, `None` for the topmost), at the gradient's angle
/// and aspect: every target gets the same vector in document coordinates, so the ramp runs on
/// across them (#2127).
fn placed_across(d: &Document, ids: &[(NodeId, Option<usize>)], paint: Paint, p: &Value, fill: bool) -> Paint {
let union = ids
.iter()
.filter_map(|id| d.node(*id))
.filter_map(|n| item_paint_bounds(n, None, fill).or_else(|| n.geometric_bounds()))
.filter_map(|(id, index)| Some((d.node(*id)?, *index)))
.filter_map(|(n, index)| item_paint_bounds(n, index, fill).or_else(|| n.geometric_bounds()))
.filter(|b| [b.x0, b.y0, b.x1, b.y1].iter().all(|v| v.is_finite()))
.reduce(|a, b| a.union(b));
let (Some(union), Paint::Gradient(g)) = (union, &paint) else { return paint };
+22
View File
@@ -119,6 +119,28 @@ fn strokes_span_the_targets_too() {
assert!(ga.start.x < 0.0 && ga.end.x > 400.0, "the stroke's box, half its weight out: {ga:?}");
}
#[test]
fn a_lower_stroke_row_spans_the_box_that_row_paints() {
let mut s = session();
let a = rect(&mut s, 0.0, 50.0, 100.0, 100.0);
let b = rect(&mut s, 300.0, 50.0, 100.0, 100.0);
// [Fill, Stroke 40 pt, Stroke 1 pt] on both.
s.execute("appearance.addStroke", &json!({"ids": [a.0, b.0]})).unwrap();
s.execute("stroke.set", &json!({"ids": [a.0, b.0], "item": 1, "weight": 40})).unwrap();
for aligned in [false, true] {
s.execute("paint.setStroke", &json!({"ids": [a.0, b.0], "item": 1, "aligned": aligned, "gradient": black_to_white(), "span": "selection"}))
.unwrap();
for id in [a, b] {
let Paint::Gradient(g) = &s.doc().unwrap().doc.node(id).unwrap().appearance.stroke_at(Some(1)).unwrap().paint else {
panic!("expected a gradient on stroke row 1")
};
// The 40 pt row reaches 20 pt past the union 0..400, not the 1 pt topmost stroke's 0.5 pt.
let geom = g.geom.expect("placed in document coordinates");
assert!(near(geom.start, Point::new(-20.0, 100.0)) && near(geom.end, Point::new(420.0, 100.0)), "aligned {aligned}, {id:?}: {geom:?}");
}
}
}
#[test]
fn the_new_art_default_stays_unplaced() {
let mut s = session();