diff --git a/crates/engine/src/cmd/graph/series.rs b/crates/engine/src/cmd/graph/series.rs index 3bea7a84f4..aeaceb9e91 100644 --- a/crates/engine/src/cmd/graph/series.rs +++ b/crates/engine/src/cmd/graph/series.rs @@ -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) -> Option { 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) -> Option { 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) -> (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) diff --git a/crates/engine/src/cmd/graph/tests.rs b/crates/engine/src/cmd/graph/tests.rs index b4c1c8fe13..7df6967538 100644 --- a/crates/engine/src/cmd/graph/tests.rs +++ b/crates/engine/src/cmd/graph/tests.rs @@ -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]