M3.10: swatch names follow the colour model (C=10 M=20 Y=30 K=0, R=.. G=.. B=.., Gray K=..) and are unique document-wide; New Swatch saves the active gradient or pattern, Ctrl/Cmd-click makes a spot colour; colour groups take solid colours only

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
bflatastic
2026-10-04 12:20:52 +07:00
co-authored by Claude Opus 5.5
parent 00a0874d4d
commit 2f21fc3a66
4 changed files with 119 additions and 33 deletions
+59 -23
View File
@@ -3,6 +3,7 @@
use serde_json::{Value, json};
use vectorcraft_color::{Color, Paint, Swatch, SwatchGroup};
use vectorcraft_doc::Document;
use vectorcraft_doc::pattern::pattern_paint;
use super::paint::paint_from;
use super::*;
@@ -10,14 +11,22 @@ use crate::EngineError;
pub fn specs() -> Vec<CommandSpec> {
vec![
cmd!("swatch.new", "New Swatch", ["Window", "Swatches"], None, "{name?, color?, global?} (default: current fill)", has_doc, swatch_new),
cmd!(
"swatch.new",
"New Swatch",
["Window", "Swatches"],
None,
"{name?, color? | swatch? | gradient? | pattern?: name (default: the current fill), global?, spot? (a spot colour, always global)} save a colour, gradient or pattern as a swatch. Names are unique (\"Sky 2\"); a colour's default name is its values (\"C=10 M=20 Y=30 K=0\", \"R=255 G=128 B=0\", \"Gray K=40\") → {name}",
has_doc,
swatch_new
),
cmd!("swatch.delete", "Delete Swatch", ["Window", "Swatches"], None, "{name}", has_doc, swatch_delete),
cmd!(
"swatch.newGroup",
"New Color Group",
["Window", "Swatches"],
None,
"{name?, swatches?: [names] (moved into the group), colors?: [colour] (added as new swatches)}",
"{name?, swatches?: [names] (solid colours move into the group; gradients, patterns and None stay), colors?: [colour] (added as new swatches)} → {name}",
has_doc,
swatch_new_group
),
@@ -27,21 +36,29 @@ pub fn specs() -> Vec<CommandSpec> {
}
fn swatch_new(s: &mut Session, p: &Value) -> Result<Value> {
let paint = match paint_from(s, p)? {
Some(x) => x,
None => s.paint.fill.clone(),
const C: &str = "swatch.new";
let paint = match str_param(p, "pattern") {
Some(name) if s.doc()?.doc.pattern(name).is_some() => pattern_paint(name),
Some(name) => return Err(bad(C, format!("no pattern `{name}`"))),
None => paint_from(s, p)?.unwrap_or_else(|| s.paint.fill.clone()),
};
let name = match str_param(p, "name") {
Some(n) => n.to_string(),
None => match &paint {
Paint::Solid { color, .. } => rgb_name(*color),
_ => format!("Swatch {}", s.doc()?.doc.swatches.len() + 1),
},
let spot = bool_or(p, "spot", false);
let global = spot || bool_or(p, "global", false);
let paint = match paint {
// The swatch holds the colour itself, not a link to the swatch it may have come from.
Paint::Solid { color, .. } => Paint::solid(color),
Paint::None => return Err(bad(C, "a swatch needs a colour, gradient or pattern")),
_ if spot => return Err(bad(C, "only solid colours can be spot colours")),
other => other,
};
let global = bool_or(p, "global", false);
s.edit("New Swatch", |d, _| {
d.swatches.push(Swatch { name: name.clone(), paint, global, spot: false });
Ok(())
let requested = str_param(p, "name").map(str::to_string);
let name = s.edit("New Swatch", |d, _| {
let name = match requested {
Some(n) => free_name(d, &n),
None => default_name(d, &paint),
};
d.swatches.push(Swatch { name: name.clone(), paint, global, spot });
Ok(name)
})?;
Ok(json!({ "name": name }))
}
@@ -58,15 +75,30 @@ fn swatch_delete(s: &mut Session, p: &Value) -> Result<Value> {
ok()
}
/// The default name of a new solid swatch: its 8-bit RGB values.
fn rgb_name(c: Color) -> String {
let [r, g, b] = c.to_rgb();
format!("R={} G={} B={}", (r * 255.0).round(), (g * 255.0).round(), (b * 255.0).round())
/// The default name of a new solid swatch: its values in its own colour model.
fn color_name(c: Color) -> String {
let pct = |v: f32| (v * 100.0).round();
let byte = |v: f32| (v * 255.0).round();
match c {
Color::Cmyk { c, m, y, k } => format!("C={} M={} Y={} K={}", pct(c), pct(m), pct(y), pct(k)),
Color::Rgb { r, g, b } => format!("R={} G={} B={}", byte(r), byte(g), byte(b)),
Color::Gray { k } => format!("Gray K={}", pct(k)),
}
}
/// A free default name for a new swatch of `paint`: its colour values, or "New Gradient Swatch 1"…
fn default_name(d: &Document, paint: &Paint) -> String {
let base = match paint {
Paint::Solid { color, .. } => return free_name(d, &color_name(*color)),
Paint::Gradient(_) => "New Gradient Swatch",
_ => "New Pattern Swatch",
};
(1..).map(|i| format!("{base} {i}")).find(|n| !d.swatch_name_taken(n)).unwrap_or_else(|| base.to_string())
}
/// A swatch or colour-group name not used yet in `d`.
fn free_name(d: &Document, base: &str) -> String {
unique_name(base, |n| d.swatch(n).is_some() || d.swatch_groups.iter().any(|g| g.name == n))
unique_name(base, |n| d.swatch_name_taken(n))
}
fn swatch_new_group(s: &mut Session, p: &Value) -> Result<Value> {
@@ -77,13 +109,17 @@ fn swatch_new_group(s: &mut Session, p: &Value) -> Result<Value> {
let name = s.edit("New Color Group", |d, _| {
let name = free_name(d, requested.as_deref().unwrap_or("Color Group"));
let mut group = SwatchGroup { name: name.clone(), swatches: vec![] };
// Colour groups hold solid colours only.
for n in &names {
if let Some(pos) = d.swatches.iter().position(|sw| &sw.name == n) {
group.swatches.push(d.swatches.remove(pos));
if d.swatch(n).is_some_and(|sw| matches!(sw.paint, Paint::Solid { .. }))
&& let Some(sw) = d.remove_swatch(n)
{
group.swatches.push(sw);
}
}
for c in &colors {
group.swatches.push(Swatch { name: rgb_name(*c), paint: Paint::solid(*c), global: false, spot: false });
let nm = unique_name(&color_name(*c), |n| d.swatch_name_taken(n) || group.swatches.iter().any(|sw| sw.name == n));
group.swatches.push(Swatch { name: nm, paint: Paint::solid(*c), global: false, spot: false });
}
d.swatch_groups.push(group);
Ok(name)