Graphs: a series mark's gradient written unchanged leaves the kept gradient

A command that writes a series mark's paint compared the mark's gradient
with the series' kept gradient placed on the mark's box exactly. After
the mark was moved, the two differed by rounding, so the graph stored a
slightly different gradient and recorded an undo step. The vector,
focal point and freeform points now match within 1e-6 pt.
This commit is contained in:
dotCooCoo
2026-11-13 15:35:01 -08:00
parent 419698529c
commit 06031b7c9d
2 changed files with 49 additions and 1 deletions
+36 -1
View File
@@ -282,6 +282,41 @@ fn kept_on(slot: &vectorcraft_doc::SeriesPaint, fill: bool, on: Option<Rect>) ->
Some(Paint::Gradient(g))
}
/// Whether `a` and `b` are the same paint, a gradient's vector, focal point and freeform points each within 1e-6 pt (the
/// rounding a move or a placement on another box leaves).
fn same_paint_placed(a: &Paint, b: &Paint) -> bool {
let (Paint::Gradient(a), Paint::Gradient(b)) = (a, b) else { return a == b };
let near = |p: vectorcraft_geom::Point, q: vectorcraft_geom::Point| (p - q).hypot() <= 1e-6;
let mut a = (**a).clone();
match (&mut a.geom, &b.geom) {
(Some(ag), Some(bg)) => {
let focal = match (ag.focal, bg.focal) {
(Some(p), Some(q)) => near(p, q),
(p, q) => p.is_none() && q.is_none(),
};
if !(near(ag.start, bg.start) && near(ag.end, bg.end) && focal) {
return false;
}
(ag.start, ag.end, ag.focal) = (bg.start, bg.end, bg.focal);
}
(None, None) => {}
_ => return false,
}
match (&mut a.freeform, &b.freeform) {
(Some(af), Some(bf)) => {
if af.points.len() != bf.points.len() || !af.points.iter().zip(&bf.points).all(|(p, q)| near(p.at, q.at)) {
return false;
}
for (p, q) in af.points.iter_mut().zip(&bf.points) {
p.at = q.at;
}
}
(None, None) => {}
_ => return false,
}
a == **b
}
/// 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.
@@ -361,7 +396,7 @@ fn store_series_paints(doc: &mut Document, captures: Vec<Capture>) -> bool {
}
let Some(slot) = spec.series_paints.get_mut(index) else { continue };
// A mark that shows the kept gradient as it was drawn leaves the kept gradient as it is.
let drawn = kept_on(slot, fill, from).is_some_and(|kept| kept == paint);
let drawn = kept_on(slot, fill, from).is_some_and(|kept| same_paint_placed(&kept, &paint));
let (paint, unit) = unit_box_paint(paint, from);
if fill {
if !drawn {
+13
View File
@@ -2702,6 +2702,19 @@ fn rewriting_a_series_marks_drawn_gradient_stroke_records_no_undo_step() {
assert!(s.doc().unwrap().doc == was, "writing mark {:?} changed the document", mark.id);
assert_eq!(s.doc().unwrap().history.undo.len(), undo);
}
// A mark moved with its gradient shows the kept gradient on its moved box, up to rounding.
let kept = spec(&s, id).series_paints[0].clone();
for (i, mark) in series_parts(&s, id, 0).into_iter().filter(|m| matches!(m.appearance.stroke_paint(), Paint::Gradient(_))).enumerate() {
s.execute("select.set", &json!({"ids": [mark.id.0]})).unwrap();
s.execute("object.move", &json!({"dx": 13.37 + i as f64 * 0.731, "dy": -7.19})).unwrap();
let moved = s.doc().unwrap().doc.node(mark.id).unwrap().geometric_bounds().unwrap();
assert!((moved.x0 - mark.geometric_bounds().unwrap().x0 - 13.37 - i as f64 * 0.731).abs() < 1e-9, "mark {:?} did not move", mark.id);
let undo = s.doc().unwrap().history.undo.len();
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 moved 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