M3.79: Edit or Apply Color Group opens Recolor Artwork on a colour group
Double-clicking a colour group's folder in the Swatches panel, or the options
button (Edit or Apply Color Group) while a group is selected, opens Recolor
Artwork on the group: without art the rows are the group's colours and OK
rewrites the group; with art selected the art is reduced to the group's size,
takes its colours, and art and group change in one undo step. New
swatch.editGroup {group, colors, rename?} rewrites a group in order (art linked
to its global swatches follows); recolor.apply takes group, groupColors and
rename.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
fca068a0a7
commit
fcc0f4ca0f
|
|
@ -30,8 +30,8 @@ pub fn specs() -> Vec<CommandSpec> {
|
|||
"Recolor Artwork",
|
||||
["Edit", "Edit Colors"],
|
||||
None,
|
||||
"{map: rows [{from: [colour keys (recolor.colors) or \"#rrggbb\" (every colour shown as that hex)], to: colour (hex, key, {c,m,y,k} or {l,a,b}) | null, exclude?: false (the row's colours are kept)}] | {\"key or #rrggbb\": colour, …} (one row per colour), method?: \"exact\" (default)|\"preserveTints\"|\"scaleTints\"|\"tintsShades\"|\"hueShift\" (how a row's colours take its new colour, relative to the row's darkest, average or most saturated colour), limitTo?: swatch library (new colours snap to its nearest colour), includeImages?: true, includePatterns?: true} recolour the selection (gradients, text, meshes, image pixels and pattern tiles included; each new colour keeps the model of the one it replaces) as one undo step → {changed}",
|
||||
has_selection,
|
||||
"{map: rows [{from: [colour keys (recolor.colors) or \"#rrggbb\" (every colour shown as that hex)], to: colour (hex, key, {c,m,y,k} or {l,a,b}) | null, exclude?: false (the row's colours are kept)}] | {\"key or #rrggbb\": colour, …} (one row per colour), method?: \"exact\" (default)|\"preserveTints\"|\"scaleTints\"|\"tintsShades\"|\"hueShift\" (how a row's colours take its new colour, relative to the row's darkest, average or most saturated colour), limitTo?: swatch library (new colours snap to its nearest colour), group?: colour group rewritten with groupColors?: [colour] (default: the rows' new colours, in order, excluded rows left out), rename?: the group's new name, includeImages?: true, includePatterns?: true} recolour the selection (gradients, text, meshes, image pixels and pattern tiles included; each new colour keeps the model of the one it replaces) and the group, as one undo step → {changed}",
|
||||
has_doc,
|
||||
apply
|
||||
),
|
||||
cmd!(
|
||||
|
|
@ -305,10 +305,32 @@ fn apply(s: &mut Session, p: &Value) -> Result<Value> {
|
|||
}
|
||||
}
|
||||
let ids = s.doc()?.selection.objects.clone();
|
||||
let group = str_param(p, "group");
|
||||
if ids.is_empty() && group.is_none() {
|
||||
return Err(bad(C, "select artwork to recolour, or give a colour `group`"));
|
||||
}
|
||||
let map = ColorMap::new(&rows, method);
|
||||
let f = |c: Color| map.map(c);
|
||||
let scope = Scope { fill: true, stroke: true, ..Scope::of(p) };
|
||||
let changed = s.edit("Recolor Artwork", |d, _| Ok(Recolor::new(scope, &f).run(d, &ids)))?;
|
||||
let label = if ids.is_empty() { "Edit Color Group" } else { "Recolor Artwork" };
|
||||
let new: Vec<Color> = match p.get("groupColors").and_then(Value::as_array) {
|
||||
Some(cs) => cs
|
||||
.iter()
|
||||
.map(|v| {
|
||||
color_value(v).map(|c| palette.as_ref().map_or(c, |pl| pl.nearest(c))).ok_or_else(|| bad(C, format!("bad colour {v} in groupColors")))
|
||||
})
|
||||
.collect::<Result<_>>()?,
|
||||
None => rows.iter().filter_map(Row::new_color).collect(),
|
||||
};
|
||||
let rename = str_param(p, "rename");
|
||||
let (changed, edit) = s.edit(label, |d, _| {
|
||||
let changed = Recolor::new(scope, &f).run(d, &ids);
|
||||
let edit = group.map(|g| super::swatch::rewrite_group(d, g, &new, rename, C)).transpose()?;
|
||||
Ok((changed, edit))
|
||||
})?;
|
||||
if let Some(e) = edit {
|
||||
e.defaults(s);
|
||||
}
|
||||
Ok(json!({ "changed": changed }))
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -122,6 +122,15 @@ pub fn specs() -> Vec<CommandSpec> {
|
|||
has_doc,
|
||||
swatch_spot_options
|
||||
),
|
||||
cmd!(
|
||||
"swatch.editGroup",
|
||||
"Edit Color Group",
|
||||
["Window", "Swatches"],
|
||||
None,
|
||||
"{group: colour group, colors: [colour] (hex, key, {c,m,y,k} or {l,a,b}; the group's swatches take them in order, extra colours become new swatches, swatches past the end are removed; built-in swatches can't change), rename?: new group name} rewrite a colour group as one undo step; art linked to its global swatches takes their new colours (and keeps its colour, unlinked, when its swatch goes) → {name, swatches: [names], relinked}",
|
||||
has_doc,
|
||||
swatch_edit_group
|
||||
),
|
||||
]
|
||||
}
|
||||
|
||||
|
|
@ -770,3 +779,78 @@ fn follow_bases(bases: &[(String, Color)]) -> impl FnMut(&mut Color, &mut Option
|
|||
std::mem::replace(c, new) != new
|
||||
}
|
||||
}
|
||||
|
||||
/// A colour group rewrite: its name afterwards and the swatch links it changed.
|
||||
pub(super) struct GroupEdit {
|
||||
name: String,
|
||||
swatches: Vec<String>,
|
||||
relinks: Vec<Relink>,
|
||||
relinked: usize,
|
||||
}
|
||||
|
||||
impl GroupEdit {
|
||||
/// Apply the link changes to the default fill and stroke for new art.
|
||||
pub(super) fn defaults(&self, s: &mut Session) {
|
||||
self.relinks.iter().for_each(|r| r.defaults(s));
|
||||
}
|
||||
}
|
||||
|
||||
/// Rewrite colour group `group` with `colors`: its swatches take them in order (keeping their
|
||||
/// names, kinds and global or spot flags), extra colours are added as new swatches and swatches
|
||||
/// past the end are removed. Paints linked to its global swatches follow (a removed swatch's are
|
||||
/// unlinked); `rename` renames the group. A built-in swatch (None, Registration) it would change or
|
||||
/// remove is refused.
|
||||
pub(super) fn rewrite_group(d: &mut Document, group: &str, colors: &[Color], rename: Option<&str>, cmd: &str) -> Result<GroupEdit> {
|
||||
let gi = d.swatch_groups.iter().position(|g| g.name == group).ok_or_else(|| bad(cmd, format!("no colour group `{group}`")))?;
|
||||
let name = match rename.map(str::trim).filter(|n| !n.is_empty() && *n != group) {
|
||||
Some(n) => d.free_swatch_name(n),
|
||||
None => group.to_string(),
|
||||
};
|
||||
let old = &d.swatch_groups[gi].swatches;
|
||||
if let Some(w) = old.iter().enumerate().find(|(i, w)| w.is_reserved() && colors.get(*i).is_none_or(|c| w.paint.color() != Some(*c))).map(|x| x.1)
|
||||
{
|
||||
return Err(bad(cmd, format!("`{}` can't be edited or removed", w.name)));
|
||||
}
|
||||
let mut relinks = vec![];
|
||||
let mut swatches = std::mem::take(&mut d.swatch_groups[gi].swatches);
|
||||
for gone in swatches.split_off(colors.len().min(swatches.len())) {
|
||||
if gone.global {
|
||||
relinks.push(Relink { from: gone.name.clone(), to: gone.name, color: None, keep: false });
|
||||
}
|
||||
}
|
||||
for (w, c) in swatches.iter_mut().zip(colors) {
|
||||
if w.paint.color() != Some(*c) {
|
||||
// A tint swatch becomes a colour of its own.
|
||||
w.paint = Paint::solid(*c);
|
||||
if w.global {
|
||||
// Art linked to a Lab spot shows it as the Spot Colors options say.
|
||||
relinks.push(Relink { from: w.name.clone(), to: w.name.clone(), color: Some(d.linked_color(*c, w.spot)), keep: true });
|
||||
}
|
||||
}
|
||||
}
|
||||
for c in colors.iter().skip(swatches.len()) {
|
||||
let nm = unique_name(&color_name(*c), |n| n == name || d.swatch_name_taken(n) || swatches.iter().any(|w| w.name == n));
|
||||
swatches.push(Swatch { name: nm, paint: Paint::solid(*c), global: false, spot: false });
|
||||
}
|
||||
let g = &mut d.swatch_groups[gi];
|
||||
(g.name, g.swatches) = (name.clone(), swatches);
|
||||
let names = g.swatches.iter().map(|w| w.name.clone()).collect();
|
||||
let relinked = if relinks.is_empty() { 0 } else { d.map_solid_paints(&mut |c, l, t| relinks.iter().any(|r| r.apply(c, l, t))) };
|
||||
Ok(GroupEdit { name, swatches: names, relinks, relinked })
|
||||
}
|
||||
|
||||
fn swatch_edit_group(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
const C: &str = "swatch.editGroup";
|
||||
let group = str_param(p, "group").ok_or_else(|| bad(C, "missing `group`"))?;
|
||||
let colors: Vec<Color> = p
|
||||
.get("colors")
|
||||
.and_then(Value::as_array)
|
||||
.ok_or_else(|| bad(C, "missing `colors`"))?
|
||||
.iter()
|
||||
.map(|v| color_value(v).ok_or_else(|| bad(C, format!("bad colour {v}"))))
|
||||
.collect::<Result<_>>()?;
|
||||
let rename = str_param(p, "rename");
|
||||
let e = s.edit("Edit Color Group", |d, _| rewrite_group(d, group, &colors, rename, C))?;
|
||||
e.defaults(s);
|
||||
Ok(json!({"name": e.name, "swatches": e.swatches, "relinked": e.relinked}))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
//! Recolor Artwork (M3.78): colour reduction, preserve rules, recolour methods, Limit to Library,
|
||||
//! randomize.
|
||||
//! randomize; Edit Color Group (M3.79).
|
||||
|
||||
use serde_json::{Value, json};
|
||||
use vectorcraft_color::Color;
|
||||
|
|
@ -200,6 +200,8 @@ fn methods_and_bad_params() {
|
|||
assert!(s.execute("recolor.apply", &json!({"map": {"#ff0000": "#00ff00"}, "method": "nope"})).is_err());
|
||||
assert!(s.execute("recolor.apply", &json!({"map": {"#ff0000": "nonsense"}})).is_err());
|
||||
assert!(s.execute("recolor.apply", &json!({})).is_err());
|
||||
run(&mut s, "select.none", json!({}));
|
||||
assert!(s.execute("recolor.apply", &json!({"map": {}})).is_err(), "needs art or a group");
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
@ -222,3 +224,82 @@ fn randomize_shuffles_new_colours_and_skips_excluded_rows() {
|
|||
let hue = |v: &Value| color_value(v).unwrap().to_hsb()[0];
|
||||
assert!((hue(&sb["map"][0]["to"]) - 0.0).abs() < 1.0 && (hue(&sb["map"][1]["to"]) - 120.0).abs() < 1.0, "hue kept");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn edit_group_rewrites_in_order_as_one_undo_step() {
|
||||
let mut s = session();
|
||||
let g = run(&mut s, "swatch.newGroup", json!({"name": "Theme", "colors": ["#ff0000", "#00ff00", "#0000ff"]}));
|
||||
let names = keys(&g["swatches"]);
|
||||
let group = |s: &Session| {
|
||||
let d = &s.doc().unwrap().doc;
|
||||
let g = d.swatch_groups.iter().find(|g| g.name == "Theme" || g.name == "Brand Theme").unwrap();
|
||||
(g.name.clone(), g.swatches.iter().map(|w| (w.name.clone(), w.paint.color().unwrap().to_hex())).collect::<Vec<_>>())
|
||||
};
|
||||
let before = group(&s);
|
||||
let r = run(
|
||||
&mut s,
|
||||
"swatch.editGroup",
|
||||
json!({"group": "Theme", "colors": ["#111111", "cmyk 0 0 0 50", "#333333", "#444444"], "rename": "Brand Theme"}),
|
||||
);
|
||||
assert_eq!(r["name"], "Brand Theme");
|
||||
let (name, sw) = group(&s);
|
||||
assert_eq!(name, "Brand Theme");
|
||||
assert_eq!(sw.iter().map(|w| w.1.as_str()).collect::<Vec<_>>()[..1], ["#111111"]);
|
||||
assert_eq!(sw.len(), 4, "an extra colour becomes a new swatch");
|
||||
assert_eq!(sw[..3].iter().map(|w| w.0.clone()).collect::<Vec<_>>(), names, "the swatches keep their names, in order");
|
||||
assert!(matches!(s.doc().unwrap().doc.swatch(&names[1]).unwrap().paint.color(), Some(Color::Cmyk { .. })));
|
||||
run(&mut s, "edit.undo", json!({}));
|
||||
assert_eq!(group(&s), before, "one undo step");
|
||||
// Fewer colours drop the swatches past the end.
|
||||
run(&mut s, "swatch.editGroup", json!({"group": "Theme", "colors": ["#ababab"]}));
|
||||
assert_eq!(group(&s).1.len(), 1);
|
||||
assert!(s.execute("swatch.editGroup", &json!({"group": "Nope", "colors": []})).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn edit_group_relinks_art_and_apply_rewrites_the_group_with_the_art() {
|
||||
let mut s = session();
|
||||
run(&mut s, "swatch.newGroup", json!({"name": "Theme", "colors": ["#ff0000"]}));
|
||||
let name = s.doc().unwrap().doc.swatch_groups.iter().find(|g| g.name == "Theme").unwrap().swatches[0].name.clone();
|
||||
run(&mut s, "swatch.edit", json!({"name": name, "global": true}));
|
||||
let linked = rect(&mut s, json!("#000000"));
|
||||
run(&mut s, "paint.setFill", json!({"ids": [linked.0], "swatch": name}));
|
||||
let r = run(&mut s, "swatch.editGroup", json!({"group": "Theme", "colors": ["#00ff00"]}));
|
||||
assert_eq!(r["relinked"], 1);
|
||||
assert_eq!(fill(&s, linked).to_hex(), "#00ff00", "linked art follows its swatch");
|
||||
// Recolor with a group: the art and the group change in one undo step.
|
||||
let plain = rect(&mut s, json!("#0000ff"));
|
||||
run(&mut s, "select.set", json!({"ids": [plain.0]}));
|
||||
let undo = s.doc().unwrap().history.undo.len();
|
||||
run(&mut s, "recolor.apply", json!({"map": [{"from": ["#0000ff"], "to": "#ffff00"}], "group": "Theme"}));
|
||||
assert_eq!(s.doc().unwrap().history.undo.len(), undo + 1);
|
||||
assert_eq!(fill(&s, plain).to_hex(), "#ffff00");
|
||||
assert_eq!(s.doc().unwrap().doc.swatch(&name).unwrap().paint.color().unwrap().to_hex(), "#ffff00");
|
||||
// Without a selection, only the group.
|
||||
run(&mut s, "select.none", json!({}));
|
||||
run(&mut s, "recolor.apply", json!({"map": [{"from": ["#ffff00"], "to": "#ff00ff"}], "group": "Theme"}));
|
||||
assert_eq!(s.doc().unwrap().doc.swatch(&name).unwrap().paint.color().unwrap().to_hex(), "#ff00ff");
|
||||
assert_eq!(s.doc().unwrap().history.undo.last().unwrap().label, "Edit Color Group");
|
||||
// Explicit group colours.
|
||||
run(&mut s, "recolor.apply", json!({"map": [], "group": "Theme", "groupColors": ["#010203", "cmyk 0 0 0 20"]}));
|
||||
let g = s.doc().unwrap().doc.swatch_groups.iter().find(|g| g.name == "Theme").unwrap().clone();
|
||||
assert_eq!(g.swatches.iter().map(|w| w.paint.color().unwrap()).collect::<Vec<_>>(), [Color::rgb8(1, 2, 3), Color::cmyk(0.0, 0.0, 0.0, 0.2)]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn edit_group_refuses_to_change_built_in_swatches() {
|
||||
let mut s = session();
|
||||
run(&mut s, "swatch.newGroup", json!({"name": "Marks", "colors": ["#ff0000"]}));
|
||||
// A group holding the built-in Registration swatch (as an older file might).
|
||||
s.edit("test", |d, _| {
|
||||
d.swatch_groups.iter_mut().find(|g| g.name == "Marks").unwrap().swatches.insert(0, vectorcraft_color::swatch::registration().clone());
|
||||
Ok(())
|
||||
})
|
||||
.unwrap();
|
||||
let reg = vectorcraft_color::swatch::registration().paint.color().unwrap();
|
||||
assert!(s.execute("swatch.editGroup", &json!({"group": "Marks", "colors": ["#00ff00", "#0000ff"]})).is_err(), "changed");
|
||||
assert!(s.execute("swatch.editGroup", &json!({"group": "Marks", "colors": []})).is_err(), "removed");
|
||||
let key = vectorcraft_color::recolor::ColorKey::of(®).to_string();
|
||||
let ok = run(&mut s, "swatch.editGroup", json!({"group": "Marks", "colors": [key, "#0000ff"]}));
|
||||
assert_eq!(ok["swatches"][0], vectorcraft_color::swatch::REGISTRATION, "kept as it is");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,12 +7,13 @@
|
|||
//! saturation and brightness, Reset, the Method and Limit to Library. Edit tab: a hue and
|
||||
//! saturation wheel with a marker per new colour (drag one; with linked handles they all turn and
|
||||
//! scale together), a brightness slider and the harmony rules. The preset menu sets up a 1, 2 or 3
|
||||
//! colour job or a library.
|
||||
//! colour job or a library. Opened on a colour group (Edit or Apply Color Group), OK also rewrites
|
||||
//! the group with the new colours (renamed by `groupName`); without art only the group changes.
|
||||
//!
|
||||
//! Fields (`ui.dialog.set`): `rows` ([{from: [keys], to: key, exclude?}]), `colors` (null: Auto, or a
|
||||
//! count), `method`, `preserveWhite`, `preserveBlack`, `preserveGrays`, `limitTo` (library id, "" for
|
||||
//! none), `tab` (`assign` or `edit`), `rule` (harmony id), `linked`, `preview`. Changing `colors`,
|
||||
//! a preserve flag or `limitTo` reduces the rows again.
|
||||
//! none), `group`, `groupName`, `tab` (`assign` or `edit`), `rule` (harmony id), `linked`,
|
||||
//! `preview`. Changing `colors`, a preserve flag or `limitTo` reduces the rows again.
|
||||
|
||||
use egui::{Pos2, Rect, Sense, Ui, pos2, vec2};
|
||||
use serde_json::{Value, json};
|
||||
|
|
@ -44,17 +45,28 @@ const TABLE_H: f32 = 220.0;
|
|||
const TAB_H: f32 = 400.0;
|
||||
const PRESETS: [&str; 5] = ["Custom", "1 Color Job", "2 Color Job", "3 Color Job", "Color Library"];
|
||||
|
||||
/// Open Recolor Artwork (`ui.recolorDialog {colors?, library?}`) on the selected art. `colors` is
|
||||
/// a count (an n-colour job) or the new colours to assign; `library` limits to a library ("" picks
|
||||
/// Open Recolor Artwork (`ui.recolorDialog {colors?, library?, group?}`) on the selected art and,
|
||||
/// with `group`, on that colour group: art is reduced to as many rows as the group has colours,
|
||||
/// which become their new colours; without art the rows are the group's colours. `colors` is a
|
||||
/// count (an n-colour job) or the new colours to assign; `library` limits to a library ("" picks
|
||||
/// the first).
|
||||
pub fn open(app: &mut VectorcraftApp, p: &Value) -> Result<Value, String> {
|
||||
let st = app.session.active().ok_or("no document open")?;
|
||||
let art = !st.selection.is_empty();
|
||||
if !art {
|
||||
return Err("select artwork to recolor".into());
|
||||
let group = p.get("group").and_then(Value::as_str);
|
||||
let group_colors: Option<Vec<Color>> = match group {
|
||||
Some(g) => {
|
||||
let g = st.doc.swatch_groups.iter().find(|x| x.name == g).ok_or_else(|| format!("no colour group `{g}`"))?;
|
||||
Some(g.swatches.iter().filter_map(|w| w.paint.color()).collect())
|
||||
}
|
||||
None => None,
|
||||
};
|
||||
if !art && group.is_none() {
|
||||
return Err("select artwork (or a colour group) to recolor".into());
|
||||
}
|
||||
let assign: Vec<Value> = match p.get("colors") {
|
||||
Some(Value::Array(cs)) => cs.iter().filter_map(color_value).map(|c| key(&c)).collect(),
|
||||
let assign: Vec<Value> = match (&group_colors, p.get("colors")) {
|
||||
(Some(cs), _) => cs.iter().map(key).collect(),
|
||||
(None, Some(Value::Array(cs))) => cs.iter().filter_map(color_value).map(|c| key(&c)).collect(),
|
||||
_ => vec![],
|
||||
};
|
||||
let count = match p.get("colors").and_then(Value::as_u64) {
|
||||
|
|
@ -67,10 +79,12 @@ pub fn open(app: &mut VectorcraftApp, p: &Value) -> Result<Value, String> {
|
|||
Some(l) => swatchlib::library(&app.session, l).map(|(info, _)| info.id).ok_or_else(|| format!("no swatch library `{l}`"))?,
|
||||
None => String::new(),
|
||||
};
|
||||
// Without art the rows are the group's colours, as they are.
|
||||
let rows: Vec<Value> = if art { vec![] } else { assign.iter().map(|k| json!({"from": [k], "to": k})).collect() };
|
||||
let fields = json!({
|
||||
"rows": [], "colors": count, "method": Method::ScaleTints.id(),
|
||||
"rows": rows, "colors": count, "method": Method::ScaleTints.id(),
|
||||
"preserveWhite": true, "preserveBlack": true, "preserveGrays": false, "limitTo": limit,
|
||||
"tab": "assign", "rule": Harmony::Complementary.id(), "linked": true,
|
||||
"group": group, "groupName": group, "tab": "assign", "rule": Harmony::Complementary.id(), "linked": true,
|
||||
"preview": true, "__art": art, "__assign": assign,
|
||||
});
|
||||
app.ui.dialog = Some(Dialog::new(KIND, fields));
|
||||
|
|
@ -100,7 +114,7 @@ fn reduce_params(d: &Dialog) -> Value {
|
|||
}
|
||||
|
||||
/// Reduce the art's colours into rows again when the count, preserve flags or library changed
|
||||
/// (assigning the opener's colours as the new colours).
|
||||
/// (assigning a group's colours or the opener's colours as the new colours).
|
||||
fn sync(app: &mut VectorcraftApp, d: &mut Dialog) {
|
||||
const DONE: &str = "__reduced";
|
||||
let sig = reduce_params(d);
|
||||
|
|
@ -156,9 +170,23 @@ fn selection(d: &Dialog) -> Vec<usize> {
|
|||
d.fields.get("__sel").and_then(Value::as_array).map(|a| a.iter().filter_map(Value::as_u64).map(|i| i as usize).collect()).unwrap_or_default()
|
||||
}
|
||||
|
||||
/// `recolor.apply` parameters: the rows, method and library.
|
||||
/// `recolor.apply` parameters: the rows, method, library and the group with its new name and
|
||||
/// colours: the rows' new colours, then (with art) the group's colours no row took.
|
||||
fn apply_params(d: &Dialog) -> Value {
|
||||
json!({"map": rows(d), "method": d.str("method"), "limitTo": d.str("limitTo")})
|
||||
let rs = rows(d);
|
||||
let group = d.fields.get("group").and_then(Value::as_str).map(|g| {
|
||||
let rest =
|
||||
d.fields.get("__assign").and_then(Value::as_array).filter(|_| d.bool("__art")).map_or(&[][..], |a| a.get(rs.len()..).unwrap_or_default());
|
||||
let colors: Vec<&Value> = rs.iter().map(|r| &r["to"]).chain(rest).collect();
|
||||
let name = d.str("groupName");
|
||||
let rename = (!name.trim().is_empty() && name != g).then_some(name);
|
||||
json!({"group": g, "groupColors": colors, "rename": rename})
|
||||
});
|
||||
let mut p = json!({"map": rs, "method": d.str("method"), "limitTo": d.str("limitTo")});
|
||||
if let (Some(Value::Object(g)), Some(p)) = (group, p.as_object_mut()) {
|
||||
p.extend(g.into_iter().filter(|(_, v)| !v.is_null()));
|
||||
}
|
||||
p
|
||||
}
|
||||
|
||||
fn body(app: &mut VectorcraftApp, ui: &mut Ui, d: &mut Dialog) -> bool {
|
||||
|
|
@ -188,6 +216,13 @@ fn body(app: &mut VectorcraftApp, ui: &mut Ui, d: &mut Dialog) -> bool {
|
|||
snap(app, d, &mut rs);
|
||||
d.fields.insert("rows".into(), Value::Array(rs));
|
||||
}
|
||||
if d.fields.get("group").is_some_and(Value::is_string) {
|
||||
ui.add_space(6.0);
|
||||
ui.horizontal(|ui| {
|
||||
ui.label(egui::RichText::new("Color Group:").color(t.text_dim));
|
||||
form::text(ui, d, "groupName", 200.0);
|
||||
});
|
||||
}
|
||||
sync(app, d);
|
||||
if d.bool("__art") {
|
||||
form::preview(app, ui, d, "Recolor Artwork", CMD, apply_params(d));
|
||||
|
|
@ -195,7 +230,7 @@ fn body(app: &mut VectorcraftApp, ui: &mut Ui, d: &mut Dialog) -> bool {
|
|||
false
|
||||
}
|
||||
|
||||
/// OK: keep the previewed recolour as one undo step.
|
||||
/// OK: keep the previewed recolour as one undo step (or apply it, e.g. to a colour group only).
|
||||
fn confirm(app: &mut VectorcraftApp, d: &Dialog) -> Result<Value, String> {
|
||||
let mut d = d.clone();
|
||||
sync(app, &mut d);
|
||||
|
|
@ -237,24 +272,27 @@ fn assign_tab(app: &mut VectorcraftApp, ui: &mut Ui, d: &mut Dialog, rows: &mut
|
|||
let t = Tokens::get(ui.ctx());
|
||||
let mut changed = false;
|
||||
let total: usize = rows.iter().map(|r| r["from"].as_array().map_or(0, Vec::len)).sum();
|
||||
ui.horizontal(|ui| {
|
||||
ui.label(egui::RichText::new("Colors:").color(t.text_dim));
|
||||
let labels: Vec<String> = std::iter::once("Auto".to_string()).chain((1..=total).map(|n| n.to_string())).collect();
|
||||
let refs: Vec<&str> = labels.iter().map(String::as_str).collect();
|
||||
let cur = d.fields.get("colors").and_then(Value::as_u64).map_or("Auto".to_string(), |n| n.to_string());
|
||||
if let Some(i) = widgets::dropdown(ui, "recolor-count", &cur, &refs, 70.0) {
|
||||
d.fields.insert("colors".into(), if i == 0 { Value::Null } else { json!(i) });
|
||||
}
|
||||
ui.add_space(18.0);
|
||||
ui.label(egui::RichText::new("Preserve:").color(t.text_dim));
|
||||
for (k, label) in [("preserveWhite", "White"), ("preserveBlack", "Black"), ("preserveGrays", "Grays")] {
|
||||
let on = d.bool(k);
|
||||
if widgets::check(ui, label, on, d.bool("__art")) {
|
||||
d.fields.insert(k.into(), json!(!on));
|
||||
// Reduction and the preserve rules apply to art (a colour group alone has a row per colour).
|
||||
if d.bool("__art") {
|
||||
ui.horizontal(|ui| {
|
||||
ui.label(egui::RichText::new("Colors:").color(t.text_dim));
|
||||
let labels: Vec<String> = std::iter::once("Auto".to_string()).chain((1..=total).map(|n| n.to_string())).collect();
|
||||
let refs: Vec<&str> = labels.iter().map(String::as_str).collect();
|
||||
let cur = d.fields.get("colors").and_then(Value::as_u64).map_or("Auto".to_string(), |n| n.to_string());
|
||||
if let Some(i) = widgets::dropdown(ui, "recolor-count", &cur, &refs, 70.0) {
|
||||
d.fields.insert("colors".into(), if i == 0 { Value::Null } else { json!(i) });
|
||||
}
|
||||
}
|
||||
});
|
||||
ui.add_space(6.0);
|
||||
ui.add_space(18.0);
|
||||
ui.label(egui::RichText::new("Preserve:").color(t.text_dim));
|
||||
for (k, label) in [("preserveWhite", "White"), ("preserveBlack", "Black"), ("preserveGrays", "Grays")] {
|
||||
let on = d.bool(k);
|
||||
if widgets::check(ui, label, on, true) {
|
||||
d.fields.insert(k.into(), json!(!on));
|
||||
}
|
||||
}
|
||||
});
|
||||
ui.add_space(6.0);
|
||||
}
|
||||
ui.horizontal(|ui| {
|
||||
ui.add_space(6.0);
|
||||
ui.add_sized([CURRENT_W, 16.0], egui::Label::new(egui::RichText::new(format!("Current Colors ({total})")).color(t.text_dim)));
|
||||
|
|
|
|||
|
|
@ -162,7 +162,7 @@ pub const UI_COMMANDS: &[(&str, &str, &str, &str)] = &[
|
|||
"ui.recolorDialog",
|
||||
"Recolor Artwork…",
|
||||
"",
|
||||
"{colors?: n (an n-colour job: n rows, Scale Tints) | [colour] (new colours to assign, in order), library?: id or name (Limit to Library; \"\" the first library)} open Recolor Artwork (dialog `recolor`; engine: recolor.reduce / recolor.apply)",
|
||||
"{colors?: n (an n-colour job: n rows, Scale Tints) | [colour] (new colours to assign, in order), library?: id or name (Limit to Library; \"\" the first library), group?: colour group (Edit or Apply Color Group: OK rewrites the group with the new colours and recolours the selected art, if any)} open Recolor Artwork (dialog `recolor`; engine: recolor.reduce / recolor.apply)",
|
||||
),
|
||||
("effect.applyLast", "Apply Last Effect", "Cmd+Shift+E", "{}"),
|
||||
("file.export.pdf", "Save as PDF…", "", "{path?, artboard? | artboards? | range?: \"1-3, 5\"} (document.export options)"),
|
||||
|
|
|
|||
|
|
@ -396,8 +396,9 @@ fn drag_of(e: &Entry, names: Vec<String>) -> PanelDrag {
|
|||
#[derive(Default)]
|
||||
struct TileEvents {
|
||||
clicked: Option<(Entry, egui::Modifiers)>,
|
||||
/// Double-click opens the swatch's editor (Swatch Options, Gradient panel or pattern editing).
|
||||
edit: Option<String>,
|
||||
/// Double-click opens the swatch's editor (Swatch Options, Gradient panel or pattern editing),
|
||||
/// or Edit or Apply Color Group on a colour group.
|
||||
edit: Option<Entry>,
|
||||
drop: Option<Drop>,
|
||||
/// A drag is held over a tile or row.
|
||||
over: bool,
|
||||
|
|
@ -408,8 +409,8 @@ struct TileEvents {
|
|||
/// starts a [`PanelDrag`] carrying the rows it moves; see [`tile_drop`] for drags held over it.
|
||||
fn tile_input(ui: &Ui, resp: Response, e: &Entry, items: &[Entry], sel: &[String], list: bool, ev: &mut TileEvents) {
|
||||
let name = e.name();
|
||||
if matches!(e, Entry::Swatch { .. }) && resp.double_clicked() {
|
||||
ev.edit = Some(name.to_string());
|
||||
if matches!(e, Entry::Swatch { .. } | Entry::Folder(_)) && resp.double_clicked() {
|
||||
ev.edit = Some(e.clone());
|
||||
}
|
||||
if resp.drag_started() {
|
||||
let with_sel = sel.iter().any(|s| s == name);
|
||||
|
|
@ -685,8 +686,12 @@ fn body(app: &mut VectorcraftApp, ui: &mut Ui, salt: &'static str) {
|
|||
let zone = ui.interact(out.inner_rect, ui.id().with("swatch-drop"), Sense::hover());
|
||||
zone_input(ui, &zone, &mut ev);
|
||||
});
|
||||
if let Some(name) = ev.edit {
|
||||
app.run("ui.swatchOptions", json!({"name": name})).ok();
|
||||
match ev.edit {
|
||||
Some(Entry::Folder(g)) => edit_group(app, &g),
|
||||
Some(e) => {
|
||||
app.run("ui.swatchOptions", json!({"name": e.name()})).ok();
|
||||
}
|
||||
None => {}
|
||||
}
|
||||
if let Some(drop) = ev.drop {
|
||||
apply_drop(app, drop);
|
||||
|
|
@ -815,6 +820,11 @@ fn selected_group(app: &VectorcraftApp, sel: &[String]) -> Option<String> {
|
|||
sel.iter().find(|n| d.swatch_groups.iter().any(|g| g.name == **n)).cloned()
|
||||
}
|
||||
|
||||
/// Edit or Apply Color Group: Recolor Artwork on colour group `group` (and the selected art).
|
||||
fn edit_group(app: &mut VectorcraftApp, group: &str) {
|
||||
app.run("ui.recolorDialog", json!({ "group": group })).ok();
|
||||
}
|
||||
|
||||
/// Delete `names` (swatches and colour groups) after asking, or at once with `now` (Alt-click).
|
||||
fn delete(app: &mut VectorcraftApp, names: Vec<String>, now: bool) {
|
||||
let params = json!({"names": names});
|
||||
|
|
@ -846,8 +856,13 @@ fn bottom(app: &mut VectorcraftApp, ui: &mut Ui, sel: &[String]) {
|
|||
}
|
||||
}
|
||||
});
|
||||
// With a colour group selected the options button edits or applies the group.
|
||||
let opts = editable(app, sel);
|
||||
if widgets::icon_button_enabled(ui, "dc-options", "Swatch Options", false, opts.is_some(), 24.0).clicked()
|
||||
if let Some(g) = selected_group(app, sel).filter(|_| opts.is_none()) {
|
||||
if widgets::icon_button(ui, "palette", "Edit or Apply Color Group", false, 24.0).clicked() {
|
||||
edit_group(app, &g);
|
||||
}
|
||||
} else if widgets::icon_button_enabled(ui, "dc-options", "Swatch Options", false, opts.is_some(), 24.0).clicked()
|
||||
&& let Some(n) = opts
|
||||
{
|
||||
app.run("ui.swatchOptions", json!({"name": n})).ok();
|
||||
|
|
@ -917,6 +932,11 @@ pub fn menu(app: &mut VectorcraftApp, ui: &mut Ui) {
|
|||
delete(app, del, false);
|
||||
}
|
||||
let group = selected_group(app, &selected);
|
||||
if menu_item(ui, "Edit or Apply Color Group…", group.is_some(), false)
|
||||
&& let Some(g) = &group
|
||||
{
|
||||
edit_group(app, g);
|
||||
}
|
||||
if menu_item(ui, "Ungroup Color Group", group.is_some(), false)
|
||||
&& let Some(g) = group
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,10 +1,12 @@
|
|||
//! Recolor Artwork (M3.78), drawn headlessly.
|
||||
//! Recolor Artwork (M3.78) and Edit or Apply Color Group (M3.79), drawn headlessly.
|
||||
|
||||
use egui::{Event, Pos2, Rect, vec2};
|
||||
use egui::{Event, Modifiers, PointerButton, Pos2, Rect, vec2};
|
||||
use serde_json::{Value, json};
|
||||
use vectorcraft_color::Color;
|
||||
use vectorcraft_color::recolor::ColorKey;
|
||||
use vectorcraft_engine::Session;
|
||||
|
||||
use crate::{VectorcraftApp, dialogs};
|
||||
use crate::{VectorcraftApp, dialogs, panels};
|
||||
|
||||
fn app() -> VectorcraftApp {
|
||||
let mut app = VectorcraftApp::new(Session::new(), Default::default());
|
||||
|
|
@ -30,6 +32,11 @@ fn dialog_frame(app: &mut VectorcraftApp, ctx: &egui::Context) {
|
|||
frame(app, ctx, vec![], time, |app, ui| dialogs::show(app, ui.ctx()));
|
||||
}
|
||||
|
||||
fn press(at: Pos2, clicks: usize) -> Vec<Event> {
|
||||
let button = |pressed| Event::PointerButton { pos: at, button: PointerButton::Primary, pressed, modifiers: Modifiers::NONE };
|
||||
std::iter::once(Event::PointerMoved(at)).chain((0..clicks).flat_map(|_| [button(true), button(false)])).collect()
|
||||
}
|
||||
|
||||
/// Rectangles filled with `colors`, all selected.
|
||||
fn art(app: &mut VectorcraftApp, colors: &[&str]) {
|
||||
for (i, c) in colors.iter().enumerate() {
|
||||
|
|
@ -44,6 +51,12 @@ fn colors(app: &mut VectorcraftApp) -> usize {
|
|||
app.session.execute("recolor.colors", &json!({})).unwrap()["colors"].as_array().unwrap().len()
|
||||
}
|
||||
|
||||
fn group_keys(app: &VectorcraftApp, name: &str) -> Vec<Value> {
|
||||
let d = &app.session.doc().unwrap().doc;
|
||||
let g = d.swatch_groups.iter().find(|g| g.name == name).unwrap();
|
||||
g.swatches.iter().filter_map(|w| w.paint.color()).map(|c| json!(ColorKey::of(&c).to_string())).collect()
|
||||
}
|
||||
|
||||
fn field(app: &VectorcraftApp, k: &str) -> Value {
|
||||
app.ui.dialog.as_ref().and_then(|d| d.fields.get(k).cloned()).unwrap_or(Value::Null)
|
||||
}
|
||||
|
|
@ -109,3 +122,83 @@ fn presets_open_colour_jobs_and_libraries_and_both_tabs_draw() {
|
|||
assert_eq!(tos, [json!("rgb 18 52 86"), json!("rgb 171 205 239")]);
|
||||
dialogs::cancel(&mut app);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn double_clicking_a_colour_group_opens_it_and_ok_rewrites_it() {
|
||||
let mut app = app();
|
||||
let ctx = context();
|
||||
let show: fn(&mut VectorcraftApp, &mut egui::Ui) = panels::swatches::show;
|
||||
frame(&mut app, &ctx, vec![], 0.0, show);
|
||||
let folder = ctx.read_response(egui::Id::new(("swatch-tile", "Brights"))).expect("the Brights folder").rect.center();
|
||||
frame(&mut app, &ctx, press(folder, 2), 1.0, show);
|
||||
let d = app.ui.dialog.as_ref().expect("a dialog opened");
|
||||
assert_eq!((d.kind.as_str(), d.str("group")), (dialogs::recolor::KIND, "Brights".to_string()));
|
||||
let keys = group_keys(&app, "Brights");
|
||||
let from: Vec<Value> = field(&app, "rows").as_array().unwrap().iter().map(|r| r["from"][0].clone()).collect();
|
||||
assert_eq!(from, keys, "a row per group colour, in order");
|
||||
dialog_frame(&mut app, &ctx);
|
||||
// A new first colour and a new name; nothing is selected, so only the group changes.
|
||||
let mut rows = field(&app, "rows");
|
||||
rows[0]["to"] = json!("cmyk 100 0 0 0");
|
||||
set(&mut app, "rows", rows);
|
||||
set(&mut app, "groupName", json!("Brights 2026"));
|
||||
let undo = app.session.doc().unwrap().history.undo.len();
|
||||
dialogs::confirm(&mut app).unwrap();
|
||||
let mut want = keys.clone();
|
||||
want[0] = json!("cmyk 100 0 0 0");
|
||||
assert_eq!(group_keys(&app, "Brights 2026"), want);
|
||||
assert_eq!(app.session.doc().unwrap().history.undo.len(), undo + 1, "one undo step");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_options_button_edits_a_selected_group_and_applies_it_to_the_art() {
|
||||
let mut app = app();
|
||||
let ctx = context();
|
||||
art(&mut app, &["#ff0000", "#00ff00", "#0000ff", "#ffff00", "#ff00ff"]);
|
||||
let show: fn(&mut VectorcraftApp, &mut egui::Ui) = panels::swatches::show;
|
||||
frame(&mut app, &ctx, vec![], 0.0, show);
|
||||
let folder = ctx.read_response(egui::Id::new(("swatch-tile", "Brights"))).unwrap().rect.center();
|
||||
frame(&mut app, &ctx, press(folder, 1), 1.0, show);
|
||||
frame(&mut app, &ctx, vec![], 2.0, show);
|
||||
// The bottom bar's third button: Swatch Options, or Edit or Apply Color Group for a group.
|
||||
let mut bar: Vec<Rect> = ctx.viewport(|vp| {
|
||||
vp.prev_pass.widgets.layers().flat_map(|(_, w)| w.iter()).filter(|w| w.rect.size() == vec2(24.0, 24.0)).map(|w| w.rect).collect()
|
||||
});
|
||||
let bottom = bar.iter().map(|r| r.top()).fold(f32::MIN, f32::max);
|
||||
bar.retain(|r| r.top() == bottom);
|
||||
bar.sort_by(|a, b| a.left().total_cmp(&b.left()));
|
||||
frame(&mut app, &ctx, press(bar[2].center(), 1), 3.0, show);
|
||||
assert_eq!(field(&app, "group"), "Brights");
|
||||
dialog_frame(&mut app, &ctx);
|
||||
let group = group_keys(&app, "Brights");
|
||||
let tos: Vec<Value> = field(&app, "rows").as_array().unwrap().iter().map(|r| r["to"].clone()).collect();
|
||||
assert_eq!(tos, group, "the art's colours in as many rows as the group has, taking its colours");
|
||||
dialogs::confirm(&mut app).unwrap();
|
||||
let used: Vec<String> = app.session.execute("recolor.colors", &json!({})).unwrap()["colors"]
|
||||
.as_array()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.map(|c| c["key"].as_str().unwrap().to_string())
|
||||
.collect();
|
||||
assert!(used.iter().all(|k| group.contains(&json!(k))), "the art uses the group's colours: {used:?}");
|
||||
assert!(matches!(ColorKey::parse(&used[0]).unwrap().color(), Color::Rgb { .. }));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_group_keeps_the_colours_no_art_row_took() {
|
||||
let mut app = app();
|
||||
let ctx = context();
|
||||
art(&mut app, &["#ff0000", "#0000ff"]);
|
||||
let before = group_keys(&app, "Brights");
|
||||
app.run("ui.recolorDialog", json!({"group": "Brights"})).unwrap();
|
||||
dialog_frame(&mut app, &ctx);
|
||||
assert_eq!(field(&app, "rows").as_array().unwrap().len(), 2, "two art colours");
|
||||
let mut rows = field(&app, "rows");
|
||||
rows[1]["to"] = json!("rgb 1 2 3");
|
||||
set(&mut app, "rows", rows);
|
||||
dialogs::confirm(&mut app).unwrap();
|
||||
let after = group_keys(&app, "Brights");
|
||||
assert_eq!(after.len(), before.len());
|
||||
assert_eq!((&after[0], &after[1]), (&before[0], &json!("rgb 1 2 3")));
|
||||
assert_eq!(after[2..], before[2..]);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -82,12 +82,15 @@ canvas while open; `ui.dialog.confirm` keeps the result as one undo step (`edit.
|
|||
`edit.colors.saturate`) and `ui.dialog.cancel` rolls it back. Global mode answers with an error until tints of global
|
||||
and spot colours exist; the dialog then stays open.
|
||||
|
||||
Recolor Artwork: `ui.recolorDialog {colors?, library?}` opens the `recolor` dialog on the selected art (`colors`: a
|
||||
count for an n-colour job, or colours to assign as the new colours; `library`: Limit to Library, `""` for the first).
|
||||
Fields: `rows` (`[{from: [keys], to: key, exclude?}]` as `recolor.reduce` returns them), `colors` (null for Auto),
|
||||
`method`, `preserveWhite`, `preserveBlack`, `preserveGrays`, `limitTo`, `tab` (`assign` or `edit`), `rule` (harmony
|
||||
id), `linked`, `preview`. Setting `colors`, a preserve flag or `limitTo` reduces the rows again (also on
|
||||
`ui.dialog.confirm`). The dialog previews `recolor.apply` on the canvas; confirm keeps it as one undo step.
|
||||
Recolor Artwork: `ui.recolorDialog {colors?, library?, group?}` opens the `recolor` dialog on the selected art
|
||||
(`colors`: a count for an n-colour job, or colours to assign as the new colours; `library`: Limit to Library, `""` for
|
||||
the first; `group`: a colour group, Edit or Apply Color Group). Fields: `rows` (`[{from: [keys], to: key, exclude?}]`
|
||||
as `recolor.reduce` returns them), `colors` (null for Auto), `method`, `preserveWhite`, `preserveBlack`,
|
||||
`preserveGrays`, `limitTo`, `groupName`, `tab` (`assign` or `edit`), `rule` (harmony id), `linked`, `preview`. Setting
|
||||
`colors`, a preserve flag or `limitTo` reduces the rows again (also on `ui.dialog.confirm`). The dialog previews
|
||||
`recolor.apply` on the canvas; confirm keeps it as one undo step (with a group, the group is rewritten in the same
|
||||
step; with no art selected only the group changes). In the Swatches panel, double-clicking a colour group's folder,
|
||||
or the options button while a group is selected, opens it.
|
||||
|
||||
Library panel: `engine.execute {command: "window.swatchLibrary", params: {library}}` opens the read-only library
|
||||
panel on a swatch library (`ui.inspect` shows it as `library_panel: {kind, id}`; `library: null` closes it).
|
||||
|
|
|
|||
11
docs/mcp.md
11
docs/mcp.md
|
|
@ -319,20 +319,23 @@ patterns out. The Blend commands also grade gradient meshes, which keep their sh
|
|||
|
||||
Recolor Artwork keys colours by identity, not hex: `recolor.colors` lists each colour's `key` (`"rgb 255 0 0"`,
|
||||
`"cmyk 0 100 100 0"`, `"gray 40"`, `"lab 55 60 -40"`; RGB in 0–255 levels, CMYK and Gray in percent), so CMYK,
|
||||
Lab and RGB colours that look alike stay apart, and every colour parameter also accepts a key. `recolor.reduce {colors?, method?,
|
||||
preserve?, limitTo?}` groups the selection's colours into rows of similar colours (k-means in Lab, weighted by use;
|
||||
Lab and RGB colours that look alike stay apart, and every colour parameter also accepts a key.
|
||||
`recolor.reduce {colors?, method?, preserve?, limitTo?}` groups the selection's colours into rows of similar colours (k-means in Lab, weighted by use;
|
||||
tints of a global swatch share its row; White and Black are preserved by default) and returns a map of rows
|
||||
`[{from: [keys], to: key}]` to edit and pass to `recolor.apply {map, method?, limitTo?}`.
|
||||
`[{from: [keys], to: key}]` to edit and pass to `recolor.apply {map, method?, limitTo?, group?, groupColors?, rename?}`.
|
||||
`method` says how a row's colours take its new colour: `exact`, `preserveTints` (tints of the row's darkest colour
|
||||
stay tints), `scaleTints` (every colour becomes a tint as light, relative to the darkest), `tintsShades` (lighter and
|
||||
darker than the row's average become tints and shades) or `hueShift` (the most saturated colour takes the new colour,
|
||||
the others turn by the same hue). `limitTo` snaps new colours to a swatch library's nearest colour;
|
||||
`recolor.randomize {map, order?, saturationBrightness?, seed?}` shuffles or varies new colours; a row with
|
||||
`exclude: true` keeps its colours.
|
||||
`exclude: true` keeps its colours. `swatch.editGroup {group, colors, rename?}` rewrites a colour group in one undo
|
||||
step (art linked to its global swatches follows), and `recolor.apply` with `group` recolours the art and rewrites the
|
||||
group together.
|
||||
|
||||
```json
|
||||
{"name":"run_command","arguments":{"command":"recolor.reduce","params":{"colors":2,"preserve":{"grays":true}}}}
|
||||
{"name":"run_command","arguments":{"command":"recolor.apply","params":{"map":[{"from":["cmyk 0 100 100 0","cmyk 0 40 40 0"],"to":"cmyk 100 50 0 0"}],"method":"scaleTints"}}}
|
||||
{"name":"run_command","arguments":{"command":"swatch.editGroup","params":{"group":"Brights","colors":["#ff0000","cmyk 0 0 100 0"]}}}
|
||||
```
|
||||
|
||||
## Group, layer and type appearance
|
||||
|
|
|
|||
Loading…
Reference in New Issue