Graphs: an unchanged paint write keeps a series' kept gradient

A paint write that leaves a series mark's gradient as the graph drew it
now keeps the series' stored gradient and its unit-box flag in two more
cases:

- a mark with no width or no height, such as a column for a value of 0,
  where the stored gradient was replaced by the mark's gradient in
  document coordinates and no mark took it on the next redraw;
- a gradient kept in document coordinates (a file saved before the unit
  box), which a stroke weight written as it was moved to the unit box,
  so the next redraw placed it on each mark's own box.
This commit is contained in:
dotCooCoo
2026-11-13 20:09:25 -08:00
parent 78e7227358
commit 877eb84f6d
2 changed files with 64 additions and 9 deletions
+13 -9
View File
@@ -285,11 +285,16 @@ fn settle_series_paints(doc: &mut Document, before: SeriesPaintsBefore) -> bool
/// The box a series keeps a gradient relative to: (0, 0)–(1, 1), as graphic styles keep theirs.
const UNIT_BOX: Rect = vectorcraft_doc::GraphicStyle::UNIT_BOX;
/// The gradient `slot` keeps for its fill (`fill`) or stroke relative to the unit box, placed on box `on` as the graph
/// draws it there. None when the slot keeps no such gradient or there is no box.
/// The gradient `slot` keeps for its fill (`fill`) or stroke as the graph draws it on box `on`: one kept relative to the
/// unit box placed on `on`, also when `on` has no width or no height, and one kept in document coordinates as it is kept.
/// None when the slot keeps no gradient, or keeps one relative to the unit box and there is no box.
fn kept_on(slot: &vectorcraft_doc::SeriesPaint, fill: bool, on: Option<Rect>) -> Option<Paint> {
let (kept, unit) = if fill { (&slot.fill, slot.fill_unit_box) } else { (&slot.stroke, slot.stroke_unit_box) };
let (Some(Paint::Gradient(g)), true, Some(on)) = (kept, unit, on) else { return None };
let Some(Paint::Gradient(g)) = kept else { return None };
if !unit {
return Some(Paint::Gradient(g.clone()));
}
let on = on?;
let mut g = g.clone();
g.rebase(UNIT_BOX, on);
Some(Paint::Gradient(g))
@@ -331,18 +336,17 @@ fn same_paint_placed(a: &Paint, b: &Paint) -> bool {
}
/// The box a gradient on `node`'s fill (`width` None) or on its stroke of `width` is placed on: the geometric bounds, grown
/// by half the weight for a stroke. None for a box with no width or no height, which a gradient's place cannot be kept
/// relative to.
/// by half the weight for a stroke. The box can have no width or no height.
fn paint_box(node: &Node, width: Option<f64>) -> Option<Rect> {
let b = node.geometric_bounds()?;
let b = width.map_or(b, |w| vectorcraft_doc::appearance::stroke_paint_bounds(b, w));
(b.width() > 1e-9 && b.height() > 1e-9).then_some(b)
Some(width.map_or(b, |w| vectorcraft_doc::appearance::stroke_paint_bounds(b, w)))
}
/// `paint` as a series keeps it: a gradient placed on `from` (the edited mark's [`paint_box`]) is moved to the same place
/// relative to [`UNIT_BOX`], so each mark takes it on its own box. Returns the paint and whether it was moved.
/// relative to [`UNIT_BOX`], so each mark takes it on its own box. A box with no width or no height, which a gradient's
/// place cannot be kept relative to, leaves the paint in document coordinates. Returns the paint and whether it was moved.
fn unit_box_paint(paint: Paint, from: Option<Rect>) -> (Paint, bool) {
match (paint, from) {
match (paint, from.filter(|b| b.width() > 1e-9 && b.height() > 1e-9)) {
(Paint::Gradient(mut g), Some(from)) => {
g.rebase(from, UNIT_BOX);
(Paint::Gradient(g), true)
+51
View File
@@ -2967,6 +2967,57 @@ fn rewriting_a_series_marks_drawn_gradient_stroke_records_no_undo_step() {
}
}
/// Writing the fill of a series mark with no height (a column for a value of 0) as it is, after the graph drew the mark's
/// fill from a gradient the series keeps relative to the unit box, leaves the kept gradient and its unit-box flag as they
/// are.
#[test]
fn rewriting_a_drawn_gradient_fill_on_a_series_mark_with_no_height_keeps_the_kept_gradient() {
let (mut s, id, _, marks) = column_marks();
s.execute("paint.setFill", &json!({"ids": [marks[0].0], "gradient": {"kind": "linear"}})).unwrap();
s.execute("paint.setGradientGeom", &json!({"ids": [marks[0].0], "start": [0, 0], "end": [10, 40]})).unwrap();
let kept = spec(&s, id).series_paints[0].clone();
assert!(kept.fill_unit_box);
s.execute("graph.setData", &json!({"id": id.0, "rows": [[0, 2], [3, 4], [6, 1]]})).unwrap();
let flat = series_parts(&s, id, 0)
.into_iter()
.find(|m| matches!(m.appearance.fill_paint(), Paint::Gradient(_)) && m.geometric_bounds().is_some_and(|b| b.height() < 1e-9))
.unwrap()
.id;
let changed = s.edit("Write the fill as it is", |d, _| Ok(super::capture_series_top_paints(d, &[(flat, true)]))).unwrap();
assert!(!changed);
assert_eq!(spec(&s, id).series_paints[0], kept);
}
/// Writing a series mark's stroke weight as it is, after the graph drew the mark's stroke from a gradient the series keeps
/// in document coordinates (a file saved before the unit box), leaves the kept gradient and its unit-box flag as they are
/// and records no undo step.
#[test]
fn rewriting_a_series_marks_stroke_drawn_from_a_gradient_kept_in_document_coordinates_keeps_it() {
let (mut s, id, _, marks) = column_marks();
s.execute("paint.setStroke", &json!({"ids": [marks[1].0], "gradient": {"kind": "radial"}})).unwrap();
s.execute("stroke.set", &json!({"ids": [marks[1].0], "weight": 2})).unwrap();
s.execute("paint.setGradientGeom", &json!({"ids": [marks[1].0], "stroke": true, "start": [150, 200], "end": [170, 260]})).unwrap();
let placed = s.doc().unwrap().doc.node(marks[1]).unwrap().appearance.stroke_paint();
s.edit("Kept in document coordinates", |d, _| {
let mut extras = d.node_mut(id).unwrap().extras_mut();
let slot = &mut extras.graph.as_mut().unwrap().series_paints[0];
(slot.stroke, slot.stroke_unit_box, slot.stroke_width) = (Some(placed.clone()), false, Some(2.0));
Ok(())
})
.unwrap();
s.execute("graph.setData", &json!({"id": id.0, "rows": [[1, 2], [3, 4], [6, 1]]})).unwrap();
let kept = spec(&s, id).series_paints[0].clone();
let undo = s.doc().unwrap().history.undo.len();
let drawn: Vec<_> = series_parts(&s, id, 0).into_iter().filter(|m| m.appearance.stroke_paint() == placed).collect();
assert!(drawn.len() >= 3, "{}", drawn.len());
for mark in drawn {
let row = top_row(&s, mark.id, false);
s.execute("appearance.setItem", &json!({"ids": [mark.id.0], "index": row, "weight": mark.appearance.stroke_width()})).unwrap();
assert_eq!(spec(&s, id).series_paints[0], kept, "writing mark {:?} changed the kept stroke", mark.id);
assert_eq!(s.doc().unwrap().history.undo.len(), undo);
}
}
/// A gradient placed on a line series' stroke is kept relative to the line's stroke box, and with Draw Filled Lines the
/// band, which takes the kept stroke as its fill, has it at the same place on its own box.
#[test]