From d1d133bf095509f44dfd740cb8937a0300b00b53 Mon Sep 17 00:00:00 2001 From: bflatastic Date: Sun, 4 Oct 2026 17:45:49 +0700 Subject: [PATCH] M3.13: swatch.newGroup falls back to "Color Group" for a blank name, never gives a swatch its own group's name, and takes mesh point colours from the artwork Co-Authored-By: Claude Opus 5.5 --- crates/doc/src/swatches.rs | 5 ++++- crates/engine/src/cmd/swatch.rs | 6 +++--- crates/engine/src/tests_swatches.rs | 24 ++++++++++++++++++++++++ 3 files changed, 31 insertions(+), 4 deletions(-) diff --git a/crates/doc/src/swatches.rs b/crates/doc/src/swatches.rs index bba962c0..6ff1c173 100644 --- a/crates/doc/src/swatches.rs +++ b/crates/doc/src/swatches.rs @@ -119,7 +119,7 @@ pub fn color_name(c: Color) -> String { } /// Every colour in the subtree of `n` with the swatch it links to: solid fills, strokes and text -/// runs (with their link) and gradient stops (unlinked), in paint order. +/// runs (with their link), then gradient stops and mesh points (unlinked), in paint order. pub fn node_colors(n: &Node, f: &mut dyn FnMut(&Color, Option<&str>)) { n.walk(&mut |m| { for p in node_paints(m) { @@ -129,6 +129,9 @@ pub fn node_colors(n: &Node, f: &mut dyn FnMut(&Color, Option<&str>)) { _ => {} } } + if let NodeKind::Mesh(mesh) = &m.kind { + mesh.points.iter().for_each(|p| f(&p.color, None)); + } }); } diff --git a/crates/engine/src/cmd/swatch.rs b/crates/engine/src/cmd/swatch.rs index e020a0d7..0aedd729 100644 --- a/crates/engine/src/cmd/swatch.rs +++ b/crates/engine/src/cmd/swatch.rs @@ -35,7 +35,7 @@ pub fn specs() -> Vec { "New Color Group", ["Window", "Swatches"], None, - "{name?, swatches?: [names] (solid colours move into the group; gradients, patterns and None stay), colors?: [colour] (added as new swatches), fromArtwork?: false (add the unique colours of the selected art: fills, strokes, text and gradient stops; a colour linked to a global swatch moves that swatch into the group), toGlobal?: true (with fromArtwork: the new swatches are global and the selected art's matching unlinked colours link to them), includeTints?: false (with fromArtwork: tints of global swatches also get swatches of their own)} → {name, swatches: [names in the group], linked: paints linked}", + "{name?, swatches?: [names] (solid colours move into the group; gradients, patterns and None stay), colors?: [colour] (added as new swatches), fromArtwork?: false (add the unique colours of the selected art: fills, strokes, text, gradient stops and mesh points; a colour linked to a global swatch moves that swatch into the group), toGlobal?: true (with fromArtwork: the new swatches are global and the selected art's matching unlinked colours link to them), includeTints?: false (with fromArtwork: tints of global swatches also get swatches of their own)} → {name, swatches: [names in the group], linked: paints linked}", has_doc, swatch_new_group ), @@ -168,7 +168,7 @@ fn swatch_new_group(s: &mut Session, p: &Value) -> Result { let from_art = bool_or(p, "fromArtwork", false); let to_global = from_art && bool_or(p, "toGlobal", true); let tints = bool_or(p, "includeTints", false); - let requested = str_param(p, "name").map(str::to_string); + let requested = name_param(p, "name"); let (name, members, linked) = s.edit("New Color Group", |d, sel| { // New swatches: (colour, global). let mut new: Vec<(Color, bool)> = colors.iter().map(|c| (*c, false)).collect(); @@ -193,7 +193,7 @@ fn swatch_new_group(s: &mut Session, p: &Value) -> Result { // The global swatches made from the art's colours, which its unlinked paints link to. let mut links: Vec<(Color, String)> = vec![]; for (c, global) in new { - let nm = unique_name(&color_name(c), |n| d.swatch_name_taken(n) || group.swatches.iter().any(|sw| sw.name == n)); + let nm = unique_name(&color_name(c), |n| n == name || d.swatch_name_taken(n) || group.swatches.iter().any(|sw| sw.name == n)); if global { links.push((c, nm.clone())); } diff --git a/crates/engine/src/tests_swatches.rs b/crates/engine/src/tests_swatches.rs index eeaca99a..2062cfae 100644 --- a/crates/engine/src/tests_swatches.rs +++ b/crates/engine/src/tests_swatches.rs @@ -324,3 +324,27 @@ fn tints_of_global_swatches_get_swatches_only_when_asked() { assert_eq!(with["swatches"], json!(["Brand", "R=255 G=0 B=0", "R=0 G=255 B=0", "R=149 G=183 B=216"])); assert_eq!(doc(&s).node(b).unwrap().appearance.stroke_paint(), Paint::Solid { color: tint, swatch: Some("Brand".into()) }, "tints stay linked"); } + +#[test] +fn group_names_are_never_blank_and_never_shared_with_their_swatches() { + let mut s = session(); + art(&mut s); + // A blank name falls back to the default; a swatch never takes its own group's name. + let r = run(&mut s, "swatch.newGroup", json!({"name": " ", "colors": ["#0000ff"]})); + assert_eq!(r["name"], "Color Group"); + let r = run(&mut s, "swatch.newGroup", json!({"name": "R=255 G=0 B=0", "fromArtwork": true})); + assert_eq!(r["name"], "R=255 G=0 B=0"); + assert_eq!(r["swatches"], json!(["Brand", "R=255 G=0 B=0 2", "R=0 G=255 B=0"])); +} + +#[test] +fn a_group_from_a_mesh_holds_its_point_colours() { + let mut s = session(); + let a = rect(&mut s); + run(&mut s, "paint.setFill", json!({"ids": [a.0], "color": "#ff0000"})); + run(&mut s, "select.set", json!({"ids": [a.0]})); + run(&mut s, "object.mesh.create", json!({"rows": 2, "cols": 2, "appearance": "center", "highlight": 100})); + let r = run(&mut s, "swatch.newGroup", json!({"fromArtwork": true})); + let names = r["swatches"].as_array().unwrap(); + assert!(names.contains(&json!("R=255 G=0 B=0")) && names.contains(&json!("R=255 G=255 B=255")), "{r}"); +}