diff --git a/crates/engine/src/cmd/graph/series.rs b/crates/engine/src/cmd/graph/series.rs index 1c0a6b1828..3936900ad9 100644 --- a/crates/engine/src/cmd/graph/series.rs +++ b/crates/engine/src/cmd/graph/series.rs @@ -282,6 +282,41 @@ fn kept_on(slot: &vectorcraft_doc::SeriesPaint, fill: bool, on: Option) -> 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) -> 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 { diff --git a/crates/engine/src/cmd/graph/tests.rs b/crates/engine/src/cmd/graph/tests.rs index 36f68b9b33..f3ec2dbeaa 100644 --- a/crates/engine/src/cmd/graph/tests.rs +++ b/crates/engine/src/cmd/graph/tests.rs @@ -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