From 76bb829e3af2f356d049c7ca36ce14ffdcfa2289 Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Sun, 8 Nov 2026 06:49:57 -0800 Subject: [PATCH] Graphs: a style that removes a mark's painted row resets the stored opacity Applying or redefining a graphic style on a series mark whose fill or stroke had paint, when the style leaves that row without paint, now stores the row's opacity and blend mode on the graph. Before, the opacity and blend mode another mark of the series left stayed, so a Filled Lines band's outline kept a stroke opacity after a style without a stroke went on a stroked marker. A row with no paint before and after the edit still keeps what the graph stores. --- crates/engine/src/cmd/appearance.rs | 8 ++++++-- crates/engine/src/cmd/graph/tests.rs | 26 ++++++++++++++++++++++++++ 2 files changed, 32 insertions(+), 2 deletions(-) diff --git a/crates/engine/src/cmd/appearance.rs b/crates/engine/src/cmd/appearance.rs index e24b55105..4f9152bcf 100644 --- a/crates/engine/src/cmd/appearance.rs +++ b/crates/engine/src/cmd/appearance.rs @@ -687,11 +687,15 @@ pub(super) fn capture_series_writes( let items = d.node(id).map_or(&[][..], |n| &n.appearance.items[..]); items.iter().rev().find(|it| it.is_fill() == kind).is_some_and(AppearanceItem::visible) }; - let written = |d: &vectorcraft_doc::Document, (fill, stroke): (bool, bool)| -> Vec<(NodeId, bool)> { + let kinds = |(fill, stroke): (bool, bool)| -> Vec<(NodeId, bool)> { let rows = ids.iter().flat_map(|id| [fill.then_some((*id, true)), stroke.then_some((*id, false))]).flatten(); - painted_rows(d, rows.filter(|(id, kind)| !top_only || top_shows(d, *id, *kind))) + rows.filter(|(id, kind)| !top_only || top_shows(d, *id, *kind)).collect() }; + let written = |d: &vectorcraft_doc::Document, rows: (bool, bool)| painted_rows(d, kinds(rows)); let mut opacity_rows = top_opacity_changed(before, d, ids); + // A written row that had paint before the edit counts too when the edit left it without paint: only a row without + // paint both before and after keeps what the graph stores. + opacity_rows.extend(painted_rows(before, kinds(opacities))); opacity_rows.extend(written(d, opacities)); let mut rows = top_paint_changed(before, d, ids); rows.extend(written(d, paints)); diff --git a/crates/engine/src/cmd/graph/tests.rs b/crates/engine/src/cmd/graph/tests.rs index b90d456bb..2c93799ba 100644 --- a/crates/engine/src/cmd/graph/tests.rs +++ b/crates/engine/src/cmd/graph/tests.rs @@ -1218,6 +1218,32 @@ fn a_filled_line_bands_outline_keeps_a_stroke_opacity_and_blend_mode() { assert_eq!((st.opacity, st.blend), (0.5, BlendMode::Multiply)); } +/// A graphic style without a stroke applied to a marker whose stroke had paint replaces the stroke opacity and blend mode +/// the series' Filled Lines band left: the band's outline is drawn opaque and Normal again. A marker that had no stroke +/// before the style leaves them as they are. +#[test] +fn a_style_that_removes_a_marks_stroke_replaces_the_kept_stroke_opacity() { + for stroked in [true, false] { + let (mut s, id, band) = band_graph(); + let marker = series_parts(&s, id, 0)[1].id; + if stroked { + let row = top_row(&s, marker, false); + s.execute("appearance.setItem", &json!({"ids": [marker.0], "index": row, "color": "#ff0000"})).unwrap(); + } + let row = top_row(&s, band, false); + s.execute("appearance.setItem", &json!({"ids": [band.0], "index": row, "opacity": 40, "blend": "multiply"})).unwrap(); + assert_eq!(spec(&s, id).series_paints[0].stroke_opacity, Some(0.4)); + blue_style(&mut s, "Blue", false); + s.execute("graphicStyle.apply", &json!({"name": "Blue", "ids": [marker.0]})).unwrap(); + let kept = spec(&s, id).series_paints[0].clone(); + let want = if stroked { (None, None) } else { (Some(0.4), Some(BlendMode::Multiply)) }; + assert_eq!((kept.stroke_opacity, kept.stroke_blend), want, "stroked {stroked}"); + redrawn_band(&mut s, id); + let st = series_parts(&s, id, 0)[0].appearance.stroke().unwrap().clone(); + assert_eq!((Some(st.opacity).filter(|o| *o < 1.0), Some(st.blend).filter(|b| *b != BlendMode::Normal)), want, "stroked {stroked}"); + } +} + /// A Filled Lines band's stroke row removed with the Appearance panel or set to None with `paint.setStroke` leaves the /// band its color: an empty stored stroke is not a color for the band. #[test]