From 47fbe1c14a588c4174b5c8b6463cafbbf9620026 Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Thu, 12 Nov 2026 20:43:38 -0800 Subject: [PATCH] Patterns: Delete, Cancel and rename reach a graph's stored paints The series fill and stroke and the label fill and stroke a graph keeps for its next redraw are now in the paint traversal, so Delete Pattern and canceling a new pattern make them no paint and a Pattern Options rename renames them. Before, a later graph.setData drew the paint of a missing pattern again. Global swatch edits reach the same paints. The progress-doc line now says that only canceling a new pattern clears its paints; canceling the edit of an existing pattern puts the definition, and after a rename the old name, back. --- crates/doc/src/swatches.rs | 68 ++++++++++++++++++++++++++-- crates/engine/src/cmd/patterncmds.rs | 4 +- crates/engine/src/tests/pattern.rs | 55 ++++++++++++++++++++++ docs/mcp.md | 4 +- docs/target-app-parity.md | 2 +- 5 files changed, 125 insertions(+), 8 deletions(-) diff --git a/crates/doc/src/swatches.rs b/crates/doc/src/swatches.rs index e245e66156..ba79f7c280 100644 --- a/crates/doc/src/swatches.rs +++ b/crates/doc/src/swatches.rs @@ -143,8 +143,8 @@ impl Document { } /// Visit every paint in the places [`Document::map_solid_paints`] visits (fills, strokes, text - /// runs, image colorize paints and dynamic symbol instances' own paints in the art, opacity masks - /// included, symbol definitions, graph designs, pattern tiles and the tile that pattern + /// runs, image colorize paints, dynamic symbol instances' own paints and graphs' stored series and + /// label paints in the art, opacity masks included, symbol definitions, graph designs, pattern tiles and the tile that pattern /// editing's Cancel puts back; graphic styles; the swatches; character styles' fill and /// stroke), whatever its kind. `f(paint)` gives the paint to put in its place, or `None` to keep /// it. Only nodes holding a changed paint (and the paths to them) are copied. Returns the number @@ -210,7 +210,7 @@ impl Document { pub fn visit_paints(&self, f: &mut dyn FnMut(&Paint)) { let mut tree = |n: &Node| { walk_with_masks(n, &mut |m| { - node_paints(m).chain(override_paints(m)).for_each(&mut *f); + node_paints(m).chain(override_paints(m)).chain(graph_paints(m)).for_each(&mut *f); if let NodeKind::Mesh(mesh) = &m.kind { mesh.points.iter().for_each(|p| f(&Paint::solid(p.color))); } @@ -382,6 +382,23 @@ pub fn override_paints(n: &Node) -> impl Iterator { n.extras().symbol_overrides.iter().flat_map(|o| [o.fill.as_ref(), o.stroke.as_ref()]).flatten() } +/// The paints a graph keeps for its series and its labels ([`crate::graph::GraphSpec`]), which its +/// marks and labels take each time it is drawn again and which [`node_paints`] leaves out. +pub fn graph_paints(n: &Node) -> impl Iterator { + let g = n.extras().graph.as_deref(); + let series = g.into_iter().flat_map(|g| &g.series_paints).flat_map(|s| [s.fill.as_ref(), s.stroke.as_ref()]).flatten(); + let labels = g.into_iter().flat_map(|g| [&g.label_styles.values, &g.label_styles.categories, &g.label_styles.legend]).flatten(); + series.chain(labels.flat_map(|st| [&st.fill, &st.stroke])) +} + +/// Mutable [`graph_paints`] of graph `g`, in the same order. +fn graph_paints_mut(g: &mut crate::graph::GraphSpec) -> impl Iterator { + let series = g.series_paints.iter_mut().flat_map(|s| [s.fill.as_mut(), s.stroke.as_mut()]).flatten(); + let l = &mut g.label_styles; + let labels = [&mut l.values, &mut l.categories, &mut l.legend].into_iter().flatten(); + series.chain(labels.flat_map(|st| [&mut st.fill, &mut st.stroke])) +} + /// Mutable [`node_paints`], in the same order. pub fn node_paints_mut(n: &mut Node) -> impl Iterator { let runs = match &mut n.kind { @@ -423,6 +440,22 @@ fn map_node(n: &Node, f: &mut PaintFn) -> Option<(Node, usize)> { count += k; } } + // A graph's series and label paints, which its marks and labels take when it is drawn again. + let graph: Vec<(usize, Paint)> = graph_paints(n).enumerate().filter_map(|(i, p)| f(p).map(|q| (i, q))).collect(); + if !graph.is_empty() { + count += graph.len(); + let m = out.get_or_insert_with(|| n.clone()); + if let Some(spec) = m.extras_if_any().and_then(|e| e.graph.as_deref_mut()) { + let mut graph = graph.into_iter().peekable(); + for (i, p) in graph_paints_mut(spec).enumerate() { + if graph.peek().is_some_and(|c| c.0 == i) + && let Some((_, q)) = graph.next() + { + *p = q; + } + } + } + } if let Some(ch) = n.children() { for (i, c) in ch.iter().enumerate() { if let Some((new, k)) = map_node(c, f) { @@ -648,6 +681,35 @@ mod tests { assert_eq!(map_node_solid_paints(&mut loose, &mut to_blue), 0, "nothing left to change"); } + #[test] + fn mapping_reaches_the_series_and_label_paints_a_graph_keeps() { + let (mut d, a, _) = doc(); + let red = linked(Color::rgb(1.0, 0.0, 0.0), "Brand"); + let spec = crate::graph::GraphSpec { + series_paints: vec![crate::SeriesPaint { fill: Some(red.clone()), stroke: Some(red.clone()), ..Default::default() }], + label_styles: crate::graph::LabelStyles { + legend: Some(crate::CharStyle { fill: red.clone(), stroke: Paint::None, ..Default::default() }), + ..Default::default() + }, + ..Default::default() + }; + d.node_mut(a).unwrap().extras_mut().graph = Some(Box::new(spec)); + let mut seen = 0; + d.visit_paints(&mut |p| seen += usize::from(*p == red)); + assert_eq!(seen, 4, "the fill and the graph's series fill, series stroke and legend fill"); + let blue = Color::rgb(0.0, 0.0, 1.0); + let mut to_blue = |c: &mut Color, s: &mut Option, _: &mut f32| s.as_deref() == Some("Brand") && std::mem::replace(c, blue) != blue; + assert_eq!(d.map_solid_paints(&mut to_blue), 4); + let g = d.node(a).unwrap().extras().graph.clone().unwrap(); + assert_eq!(g.series_paints[0].fill, Some(linked(blue, "Brand"))); + assert_eq!(g.series_paints[0].stroke, Some(linked(blue, "Brand"))); + let legend = g.label_styles.legend.unwrap(); + assert_eq!((legend.fill, legend.stroke), (linked(blue, "Brand"), Paint::None)); + let mut cleared = |p: &Paint| (*p == linked(blue, "Brand")).then_some(Paint::None); + assert_eq!(d.map_paints(&mut cleared), 4, "the fill and the graph's three"); + assert!(graph_paints(d.node(a).unwrap()).all(Paint::is_none)); + } + #[test] fn mapping_within_ids_visits_each_paint_once() { let (mut d, a, b) = doc(); diff --git a/crates/engine/src/cmd/patterncmds.rs b/crates/engine/src/cmd/patterncmds.rs index dbf726f528..2800e8e674 100644 --- a/crates/engine/src/cmd/patterncmds.rs +++ b/crates/engine/src/cmd/patterncmds.rs @@ -461,8 +461,8 @@ fn pattern_paints<'a>(name: &'a str, to: Option<&'a str>) -> impl FnMut(&Paint) /// Remove pattern `name` from `d`: its definition and every swatch, grouped or not, whose paint is /// the pattern. Every paint of the pattern left anywhere in the document (the art, opacity masks, -/// type, symbol definitions, graph designs, other patterns' tiles, graphic and character styles and -/// the art of brushes) becomes no paint. Returns the number of such paints. +/// type, symbol definitions, graph designs, graphs' stored series and label paints, other patterns' +/// tiles, graphic and character styles and the art of brushes) becomes no paint. Returns the number of such paints. fn forget_pattern(d: &mut Document, name: &str) -> usize { d.patterns.retain(|p| p.name != name); let is_it = |sw: &vectorcraft_color::Swatch| matches!(&sw.paint, Paint::Pattern { pattern, .. } if pattern == name); diff --git a/crates/engine/src/tests/pattern.rs b/crates/engine/src/tests/pattern.rs index c03284f60f..0f29befd80 100644 --- a/crates/engine/src/tests/pattern.rs +++ b/crates/engine/src/tests/pattern.rs @@ -623,6 +623,61 @@ fn rename_pattern_reaches_paints_outside_the_layers() { assert!(is_pattern(&s.paint.fill, "Dots"), "Undo gives the fill its old name back"); } +/// A new graph whose stored series fill and stroke and value label fill and stroke are pattern +/// `name`, as a later redraw would give them to its marks and labels. +fn graph_painted_with(s: &mut Session, name: &str) -> NodeId { + let v = s.execute("graph.create", &json!({"type": "column", "x": 200, "y": 200, "width": 60, "height": 40, "csv": ",2024\nQ1,3\nQ2,5"})); + let id = id_of(&v.unwrap()); + let p = pattern_paint(name); + let mut extras = doc_mut(s).node_mut(id).unwrap().extras_mut(); + let spec = extras.graph.as_mut().unwrap(); + spec.series_paints = vec![vectorcraft_doc::SeriesPaint { fill: Some(p.clone()), stroke: Some(p.clone()), ..Default::default() }]; + spec.label_styles.values = Some(vectorcraft_doc::CharStyle { fill: p.clone(), stroke: p, ..Default::default() }); + id +} + +/// The series and label paints a graph stores, in order: series fill and stroke, then the value +/// labels' fill and stroke. +fn stored_graph_paints(s: &Session, id: NodeId) -> Vec { + let n = node(s, id); + let g = n.extras().graph.as_deref().unwrap(); + let series = g.series_paints.iter().flat_map(|p| [p.fill.clone(), p.stroke.clone()]).flatten(); + series.chain(g.label_styles.values.iter().flat_map(|st| [st.fill.clone(), st.stroke.clone()])).collect() +} + +/// Delete Pattern clears the series and label paints a graph stores, and a rename renames them, so +/// a later `graph.setData` draws no paint of a missing pattern. +#[test] +fn delete_and_rename_reach_a_graphs_stored_paints() { + let mut s = session(); + make_dots(&mut s); + let g = graph_painted_with(&mut s, "Dots"); + s.execute("pattern.options", &json!({"name": "Dots", "newName": "Spots"})).unwrap(); + assert!(stored_graph_paints(&s, g).iter().all(|p| is_pattern(p, "Spots")), "the rename reaches the graph"); + s.execute("edit.undo", &json!({})).unwrap(); + assert!(stored_graph_paints(&s, g).iter().all(|p| is_pattern(p, "Dots"))); + let v = s.execute("pattern.delete", &json!({"name": "Dots"})).unwrap(); + assert_eq!(v["unpainted"], 4, "the series fill and stroke and the value labels' fill and stroke"); + assert!(stored_graph_paints(&s, g).iter().all(Paint::is_none)); + s.execute("graph.setData", &json!({"id": g.0, "rows": [[2], [4]]})).unwrap(); + assert_eq!(pattern_uses(doc(&s), "Dots"), 0, "the redrawn graph holds no paint of the deleted pattern"); +} + +/// Canceling a new pattern clears the series and label paints a graph stores. +#[test] +fn cancel_new_pattern_reaches_a_graphs_stored_paints() { + let mut s = session(); + // The graph is made outside the editing layer, and its paints take the new pattern's name. + let g = graph_painted_with(&mut s, "Tiles"); + let r = rect(&mut s, 0.0, 0.0, 10.0, 10.0); + sel(&mut s, &[r]); + s.execute("object.pattern.make", &json!({"name": "Tiles"})).unwrap(); + s.execute("object.pattern.cancel", &json!({})).unwrap(); + assert_eq!(stored_graph_paints(&s, g).len(), 4); + assert!(stored_graph_paints(&s, g).iter().all(Paint::is_none)); + assert_eq!(pattern_uses(doc(&s), "Tiles"), 0); +} + /// With New Art Has Basic Appearance off, Redo of Delete Pattern gives new art the paints the /// command left, the selected object's appearance taken after the pattern's fill was cleared. #[test] diff --git a/docs/mcp.md b/docs/mcp.md index 0280f5449b..5190089ecc 100644 --- a/docs/mcp.md +++ b/docs/mcp.md @@ -4095,8 +4095,8 @@ old name keeps it. A blank name gives "New Pattern N" in `object.pattern.make` a `object.pattern.cancel {}` leaves pattern editing mode and discards the changes. For a pattern that `object.pattern.make` made for the edit, it removes the pattern and every swatch of it, a color group's included. Every other paint of the pattern becomes no paint: on art outside the editing layer, in opacity mask art, type runs, symbol definitions, graph -designs, other patterns' tiles, graphic and character styles and brush art, and in the fill, stroke and appearance new -art takes. A pattern that `object.pattern.saveCopy` saved during the edit stays, with those paints cleared in its tile. +designs, the series and label paints a graph keeps for its next redraw, other patterns' tiles, graphic and character +styles and brush art, and in the fill, stroke and appearance new art takes. A pattern that `object.pattern.saveCopy` saved during the edit stays, with those paints cleared in its tile. For an existing pattern, Cancel puts the definition back as it was when editing started; when `pattern.options` renamed it during the edit, the old name comes back with its swatches and paints, unless another pattern, swatch or color group has taken that name. `pattern.delete {name}` clears the paints of the pattern in the same places and returns diff --git a/docs/target-app-parity.md b/docs/target-app-parity.md index 06ff166035..8b8215bcfe 100644 --- a/docs/target-app-parity.md +++ b/docs/target-app-parity.md @@ -704,7 +704,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur - **Control bar with the Artboard tool:** while the Artboard tool is in use, the Control bar shows the active artboard's controls in place of the object controls: Preset, Portrait and Landscape, New Artboard, Delete Artboard, the Name field, Artboard Options, Rearrange All Artboards, the Transform link, Move and Scale Artwork with Artboard and, while the bar has room, the reference point with X and Y, and W and H with the Constrain Width and Height Proportions link. Each runs the command its Properties or Transform panel counterpart runs (`artboard.setProps`, `artboard.new`, `artboard.delete`, `tool.options {tool: "artboard"}`), and the reference point is the one the Transform panel uses. In an 800-point window the controls up to the Transform link fit, Move and Scale Artwork with Artboard show as their boxes alone with their names in tooltips, and the link opens the Transform panel with the artboard's position and size. Pointer and key tests in `panels/artboards.rs` cover the controls, W about the reference point, the orientation and the name. Public reference: [Using multiple artboards](https://web.archive.org/web/20210925094818/https://helpx.adobe.com/illustrator/using/using-multiple-artboards.html) (archived copy). - **Names of new and renamed artboards, brushes and patterns:** `artboard.new` and `artboard.setProps` trim the white space around the name they are given, as the Artboards panel's rename does, and a blank name gives the artboard its default name, Artboard N for the Nth artboard. `brush.new`, `brush.duplicate`, `brush.options`, `object.pattern.make`, `object.pattern.saveCopy` and `pattern.options` trim the name as the Brush Options dialog and the Pattern Options panel do, and compare the trimmed name with the names there are; a blank name gives the name each command gives when there is none, and Brush Options and Pattern Options keep the name. Tests in `tests/panelcmds.rs`, `tests/brushsym.rs` and `tests/pattern.rs` cover each command. Public references: [Using multiple artboards](https://web.archive.org/web/20210925094818/https://helpx.adobe.com/illustrator/using/using-multiple-artboards.html), [Brushes](https://web.archive.org/web/20251018101754/https://helpx.adobe.com/illustrator/using/brushes.html) and [Create and edit patterns](https://web.archive.org/web/20211031180358/https://helpx.adobe.com/illustrator/using/create-edit-patterns.html) (archived copies). - **Pattern names and swatch names:** `pattern.options` refuses a new name that another pattern, a swatch or a color group has. It takes a name that only the pattern's own swatches have. The reference page does not say what happens when the new name is taken; the refusal follows VectorCraft's rule that swatches and color groups share one namespace (`Document::swatch_name_taken`), which `swatch.new` and `swatch.edit` also follow. A rename gives the new name only to the pattern's swatches that had the old one; any other swatch with the old name keeps it, and art linked to that swatch stays linked. `object.pattern.make` and `object.pattern.saveCopy` number a color group's name as they number a swatch's. Tests in `tests/pattern.rs` cover each command. Public reference: [Create and edit patterns](https://web.archive.org/web/20211031180358/https://helpx.adobe.com/illustrator/using/create-edit-patterns.html) (archived copy). -- **Cancel, Delete and rename leave no paint of a missing pattern:** the help page lists Save a Copy, Done and Cancel for leaving pattern editing (https://helpx.adobe.com/illustrator/using/create-edit-patterns.html). Canceling a pattern made for the edit removes it with every swatch of it, a color group's included. Canceling, `pattern.delete` and a `pattern.options` rename reach every paint of the pattern: art outside the editing layer, opacity mask art, type runs, symbol definitions, graph designs, other patterns' tiles (a copy Save a Copy made included), graphic and character styles, brush art, and the fill, stroke and appearance new art takes. Cancel and Delete make those paints no paint, a VectorCraft rule; a rename renames them. Cancel after a rename in Pattern Options gives the pattern its old name back unless another pattern or swatch has taken it. Undo and Redo restore new art's paints with the document. Tests in `tests/pattern.rs` cover each command. +- **Cancel, Delete and rename leave no paint of a missing pattern:** the help page lists Save a Copy, Done and Cancel for leaving pattern editing (https://helpx.adobe.com/illustrator/using/create-edit-patterns.html). Canceling a pattern made for the edit removes it with every swatch of it, a color group's included. Canceling a new pattern, `pattern.delete` and a `pattern.options` rename reach every paint of the pattern: art outside the editing layer, opacity mask art, type runs, symbol definitions, graph designs, the series and label paints a graph keeps for its next redraw, other patterns' tiles (a copy Save a Copy made included), graphic and character styles, brush art, and the fill, stroke and appearance new art takes. Canceling a new pattern and Delete make those paints no paint, a VectorCraft rule; a rename renames them. Canceling the edit of an existing pattern puts its definition back as it was when editing started; after a rename in Pattern Options, it also gives the pattern its old name back with its swatches and paints, unless another pattern, swatch or color group has taken that name. Undo and Redo restore new art's paints with the document. Tests in `tests/pattern.rs` cover each command. - **A clip group in Properties:** with one clip group selected, the panel has Edit Clipping Path and Edit Contents at its top, under the Clip Group heading, which select the clipping path or the clipped art for the selection tools to transform alone, as Object › Clipping Mask does (: with a clip group selected, the tutorial uses Edit Clipping Path at the top of the Properties panel to select the mask for editing, and uses Edit Contents to select the masked art). Edit Contents shows only when the group clips something. Not checked against the reference app: whether Properties keeps the buttons while the clipping path or the contents is selected; the Control bar has no clip-group buttons here. Evidence: `panels::properties::tests::one_clip_group_has_edit_clipping_path_and_edit_contents_at_the_top`. - **Contextual controls in Properties:** the [Properties panel overview](https://helpx.adobe.com/illustrator/desktop/get-started/learn-the-basics/properties-panel-overview.html) lists controls that appear for particular selections and tools; Properties has them, each running the existing command. Quick Actions has Blend Options for one selected blend, with the Blend tool or any other, as the [blending page](https://helpx.adobe.com/illustrator/using/blending-objects.html) describes (#4817) (the Blend Options dialog, OK one undo step) and Merge Live Paint when a Live Paint group is selected with more paths and nothing Live Paint refuses (`livePaint.merge`). Appearance has the stroke's Variable Width Profile list, the Control bar's, with the Width tool or when the stroke has a variable width (`stroke.set {profile}`), and beside fx the Delete icon: with one effect on the one selected object (its own or a fill's or stroke's) its name and an icon that removes it (`effect.remove`), with several an icon that opens the Appearance panel, as the overview describes. With Direct Selection and corners of one path selected, Corner and Corner Radius set the kind and radius of those corners (`object.setLiveShape`); a live rectangle's and polygon's own rows already do. Each edit is one undo step. The overview's Variable Font icon was already in Character. Not checked against the reference app: whether Merge needs two Live Paint groups (the overview speaks of several selected groups; `livePaint.merge` takes a group and paths), and where exactly each control sits in its section. Evidence: `panels::properties::tests::{blend_options_is_a_quick_action_for_one_blend_with_the_blend_tool, merge_live_paint_is_a_quick_action_for_a_live_paint_group_and_more, the_width_profile_shows_for_a_variable_width_or_the_width_tool, direct_selected_corners_show_their_kind_and_radius, one_effect_has_a_delete_icon_in_appearance}`. - **Edit Artboards in Properties:** a Transform section comes first (#5219): the reference point the Transform panel and Control bar share, the active artboard's X and Y (of that point) and W and H with the Constrain Width and Height Proportions link, its preset and orientation, and Move and Scale Artwork with Artboard. The Artboards section follows, with the name field, Artboard Options and Fill. The active artboard's name is a field. Typing a new name and pressing Enter or clicking elsewhere renames that artboard in one undo step, as the Artboards panel's rename does (`artboard.setProps {index, name}`); pressing Escape or leaving the field empty keeps the old name. A click on the Artboard Options icon beside the field opens Artboard Options for the active artboard, as double-clicking the Artboard tool's button does (`tool.options {tool: "artboard"}`). Pointer and key tests in `panels/properties.rs` cover both. Public references: [Resize artboards](https://helpx.adobe.com/illustrator/desktop/create-manage-artboards/add-edit-artboards/resize-artboards.html) and [Create and add new artboards](https://helpx.adobe.com/illustrator/desktop/create-manage-artboards/add-edit-artboards/add-new-artboards.html).