mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-26 02:37:01 -06:00
Merge pull request #2080 from fabian-co/feat/recolor-library-swatches
Recolor Artwork: library colours apply as their swatches; import a library
This commit is contained in:
@@ -282,6 +282,23 @@ impl RowMap {
|
||||
self.key
|
||||
}
|
||||
|
||||
/// The tint (0..1) of the new colour current colour `c` takes, when it is a tint of the new
|
||||
/// colour: what a colour linked to a global or spot swatch keeps as its tint. `None` when the
|
||||
/// method makes it something else (a shade, or another hue).
|
||||
pub fn tint(&self, c: Color) -> Option<f32> {
|
||||
match self.method {
|
||||
Method::Exact => Some(1.0),
|
||||
Method::PreserveTints if is_tint(c, self.key) => Some(tint_of(c, self.key)),
|
||||
Method::PreserveTints => Some(1.0),
|
||||
Method::ScaleTints => Some(tint_of(c, self.key)),
|
||||
Method::TintsShades => {
|
||||
let d = c.to_lab().l - self.mean_l;
|
||||
(d >= 0.0).then(|| 1.0 - d / (100.0 - self.mean_l).max(1e-3))
|
||||
}
|
||||
Method::HueShift => (c == self.key).then_some(1.0),
|
||||
}
|
||||
}
|
||||
|
||||
/// The new colour of current colour `c`, in `c`'s model.
|
||||
pub fn map(&self, c: Color) -> Color {
|
||||
let to = keep_model(c, self.to);
|
||||
|
||||
@@ -204,6 +204,12 @@ impl<'a> Recolor<'a> {
|
||||
Self { links: Some(links), ..Self::new(Scope { images: false, ..scope }, &same) }
|
||||
}
|
||||
|
||||
/// A pass that maps solid colours and gradient stops with their swatch link and tint
|
||||
/// (`links`), and meshes, images and patterns through `f`.
|
||||
pub(crate) fn with_links(scope: Scope, f: &'a dyn Fn(Color) -> Color, links: &'a LinkMap<'a>) -> Self {
|
||||
Self { links: Some(links), ..Self::new(scope, f) }
|
||||
}
|
||||
|
||||
/// Recolour the objects `ids` and everything inside them (each once), add the new images and
|
||||
/// pattern swatches, and return the number of paints, meshes and images changed.
|
||||
pub(crate) fn run(mut self, d: &mut Document, ids: &[NodeId]) -> usize {
|
||||
|
||||
@@ -7,7 +7,7 @@ use std::collections::HashMap;
|
||||
|
||||
use serde_json::{Value, json};
|
||||
use vectorcraft_color::recolor::{ColorKey, Method, Preserve, Rng, RowMap, cluster};
|
||||
use vectorcraft_color::{Color, Paint};
|
||||
use vectorcraft_color::{Color, Paint, Swatch};
|
||||
use vectorcraft_doc::swatches::node_colors;
|
||||
use vectorcraft_doc::{AppearanceItem, Document, NodeId, NodeKind};
|
||||
|
||||
@@ -30,7 +30,7 @@ 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 id or name; or \"document\" (the document's swatches): new colours snap to its nearest colour, group?: colour group to rewrite, groupColors?: [colour] (what `group` is rewritten with; 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}",
|
||||
"{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 id or name; or \"document\" (the document's swatches): new colours snap to its nearest colour, group?: colour group to rewrite, groupColors?: [colour] (what `group` is rewritten with; 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, except a library swatch's with limitTo: art takes the swatch itself, linked to a global or spot colour at its tints, and the swatch joins the document's swatches) and the group, as one undo step → {changed, swatches: [names added to the document's swatches]}",
|
||||
has_doc,
|
||||
apply
|
||||
),
|
||||
@@ -252,21 +252,34 @@ fn parse_rows(p: &Value, cmd: &str) -> Result<Vec<Row>> {
|
||||
}
|
||||
}
|
||||
|
||||
/// A parsed map ready to recolour: the row of each current colour.
|
||||
/// The document swatch a row's new colour is: its name, whether it is global (art links to it,
|
||||
/// with tints) and the colour art linked to it shows (a process colour's own colour).
|
||||
#[derive(Clone)]
|
||||
struct Link {
|
||||
name: String,
|
||||
global: bool,
|
||||
color: Color,
|
||||
}
|
||||
|
||||
/// A parsed map ready to recolour: the row of each current colour, and the swatch of each row's
|
||||
/// new colour.
|
||||
struct ColorMap {
|
||||
rows: Vec<RowMap>,
|
||||
links: Vec<Option<Link>>,
|
||||
keys: HashMap<ColorKey, usize>,
|
||||
hexes: HashMap<String, usize>,
|
||||
}
|
||||
|
||||
impl ColorMap {
|
||||
fn new(rows: &[Row], method: Method) -> Self {
|
||||
let mut out = Self { rows: vec![], keys: HashMap::new(), hexes: HashMap::new() };
|
||||
for r in rows.iter().filter(|r| !r.from.is_empty()) {
|
||||
/// `links`: the swatch of each of `rows`' new colours (as many as `rows`, or none).
|
||||
fn new(rows: &[Row], method: Method, links: &[Option<Link>]) -> Self {
|
||||
let mut out = Self { rows: vec![], links: vec![], keys: HashMap::new(), hexes: HashMap::new() };
|
||||
for (n, r) in rows.iter().enumerate().filter(|(_, r)| !r.from.is_empty()) {
|
||||
let Some(to) = r.new_color() else { continue };
|
||||
let i = out.rows.len();
|
||||
let colors: Vec<Color> = r.from.iter().map(Match::color).collect();
|
||||
out.rows.push(RowMap::new(method, &colors, to));
|
||||
out.links.push(links.get(n).cloned().flatten());
|
||||
for m in &r.from {
|
||||
match m {
|
||||
Match::Key(k) => out.keys.entry(*k).or_insert(i),
|
||||
@@ -277,9 +290,39 @@ impl ColorMap {
|
||||
out
|
||||
}
|
||||
|
||||
/// The row of current colour `c`.
|
||||
fn row_of(&self, c: Color) -> Option<usize> {
|
||||
self.keys.get(&ColorKey::of(&c)).or_else(|| if self.hexes.is_empty() { None } else { self.hexes.get(&c.to_hex()) }).copied()
|
||||
}
|
||||
|
||||
fn map(&self, c: Color) -> Color {
|
||||
let row = self.keys.get(&ColorKey::of(&c)).or_else(|| if self.hexes.is_empty() { None } else { self.hexes.get(&c.to_hex()) });
|
||||
row.map_or(c, |i| self.rows[*i].map(c))
|
||||
self.row_of(c).and_then(|i| self.rows.get(i)).map_or(c, |r| r.map(c))
|
||||
}
|
||||
|
||||
/// Recolour a colour that can link to a swatch: a row whose new colour is a global or spot
|
||||
/// swatch links its colours to it (at their tints; shades and other hues stay unlinked), a
|
||||
/// process swatch gives its colour in its own model; other rows map the colour, which loses its
|
||||
/// link when it changes. True when anything changed.
|
||||
fn map_link(&self, c: &mut Color, link: &mut Option<String>, tint: &mut f32) -> bool {
|
||||
let Some(i) = self.row_of(*c) else { return false };
|
||||
let (Some(row), Some(l)) = (self.rows.get(i), self.links.get(i)) else { return false };
|
||||
let new = match (l, row.tint(*c)) {
|
||||
(Some(l), Some(t)) if l.global => {
|
||||
let t = t.clamp(0.0, 1.0);
|
||||
let shown = l.color.tinted(t);
|
||||
let changed = link.as_deref() != Some(l.name.as_str()) || *tint != t || *c != shown;
|
||||
(*c, *link, *tint) = (shown, Some(l.name.clone()), t);
|
||||
return changed;
|
||||
}
|
||||
(Some(l), Some(t)) => l.color.tinted(t),
|
||||
_ => row.map(*c),
|
||||
};
|
||||
if new == *c {
|
||||
return false;
|
||||
}
|
||||
*c = new;
|
||||
*link = None;
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -293,13 +336,19 @@ fn apply(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
r.to = r.to.map(|c| pl.nearest(c));
|
||||
}
|
||||
}
|
||||
// With Limit to Library, a row's new colour is a swatch of the library: the art takes that
|
||||
// swatch (a spot colour stays a spot colour), which joins the document's swatches.
|
||||
let lib_swatches = match str_param(p, "limitTo").filter(|k| !k.is_empty()) {
|
||||
Some(k) => super::swatchlib::library_swatches(s, k).unwrap_or_default(),
|
||||
None => vec![],
|
||||
};
|
||||
let row_swatches: Vec<Option<Swatch>> =
|
||||
rows.iter().map(|r| r.new_color().and_then(|c| lib_swatches.iter().find(|w| w.paint.color() == Some(c)).cloned())).collect();
|
||||
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 label = if ids.is_empty() { "Edit Color Group" } else { "Recolor Artwork" };
|
||||
let new: Vec<Color> = match p.get("groupColors").and_then(Value::as_array) {
|
||||
@@ -312,15 +361,38 @@ fn apply(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
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 (changed, edit, added) = s.edit(label, |d, _| {
|
||||
// The rows' library swatches join the document (one it has with that name and colour is
|
||||
// used as it is).
|
||||
let mut added = vec![];
|
||||
let links: Vec<Option<Link>> = row_swatches
|
||||
.iter()
|
||||
.map(|w| {
|
||||
let w = w.as_ref()?;
|
||||
let name = match d.swatch(&w.name) {
|
||||
Some(x) if x.paint == w.paint => w.name.clone(),
|
||||
_ => {
|
||||
let name = d.free_swatch_name(&w.name);
|
||||
d.swatches.push(Swatch { name: name.clone(), ..w.clone() });
|
||||
added.push(name.clone());
|
||||
name
|
||||
}
|
||||
};
|
||||
let color = d.global_color(&name).filter(|_| w.global).or(w.paint.color())?;
|
||||
Some(Link { name, global: w.global, color })
|
||||
})
|
||||
.collect();
|
||||
let map = ColorMap::new(&rows, method, &links);
|
||||
let f = |c: Color| map.map(c);
|
||||
let lf = |c: &mut Color, link: &mut Option<String>, tint: &mut f32| map.map_link(c, link, tint);
|
||||
let changed = Recolor::with_links(scope, &f, &lf).run(d, &ids);
|
||||
let edit = group.map(|g| super::swatch::rewrite_group(d, g, &new, rename, C)).transpose()?;
|
||||
Ok((changed, edit))
|
||||
Ok((changed, edit, added))
|
||||
})?;
|
||||
if let Some(e) = edit {
|
||||
e.defaults(s);
|
||||
}
|
||||
Ok(json!({ "changed": changed }))
|
||||
Ok(json!({ "changed": changed, "swatches": added }))
|
||||
}
|
||||
|
||||
fn randomize(_: &mut Session, p: &Value) -> Result<Value> {
|
||||
|
||||
@@ -393,6 +393,17 @@ pub fn library_colors(s: &Session, key: &str) -> Option<Vec<Color>> {
|
||||
Some(library(s, key)?.1.iter().filter_map(colors).collect())
|
||||
}
|
||||
|
||||
/// The colour swatches of library `key` (an id or name) or, for [`DOCUMENT_SWATCHES`], of the
|
||||
/// active document (tint swatches left out: they stand for their base). `None` when there is no
|
||||
/// such library (or document).
|
||||
pub fn library_swatches(s: &Session, key: &str) -> Option<Vec<Swatch>> {
|
||||
let own = |sw: &&Swatch| matches!(sw.paint, Paint::Solid { swatch: None, .. });
|
||||
if key == DOCUMENT_SWATCHES {
|
||||
return Some(s.active()?.doc.swatches_iter().filter(own).cloned().collect());
|
||||
}
|
||||
Some(library(s, key)?.1.iter().filter(own).cloned().collect())
|
||||
}
|
||||
|
||||
/// The `limitTo` parameter's palette ([`limit_palette`]); `None` without one (or for "").
|
||||
pub(crate) fn limit_param(s: &Session, p: &Value, cmd: &str) -> Result<Option<Palette>> {
|
||||
let Some(key) = str_param(p, "limitTo").filter(|k| !k.is_empty()) else { return Ok(None) };
|
||||
|
||||
@@ -316,3 +316,54 @@ fn cmyk_object_reads_one_scale_for_every_component() {
|
||||
assert!(close(k(json!({"c": 0.1, "m": 0.5, "y": 0.9, "k": 0.2})), [0.1, 0.5, 0.9, 0.2]));
|
||||
assert!(close(k(json!({"c": 0, "m": 1, "y": 1, "k": 0})), [0.0, 1.0, 1.0, 0.0]));
|
||||
}
|
||||
|
||||
/// Recolouring to a library's spot colour (Limit to Library) applies the spot colour itself, linked
|
||||
/// to it (tints keep their tint), and adds it to the document's swatches; a process swatch of the
|
||||
/// library gives its colour in its own model.
|
||||
#[test]
|
||||
fn recolor_to_a_library_spot_colour_links_it_and_adds_the_swatch() {
|
||||
let mut s = session();
|
||||
// A library with a Lab spot colour and a CMYK process colour (made here, then removed).
|
||||
run(&mut s, "swatch.new", json!({"name": "Ink 101", "color": {"l": 50.0, "a": 70.0, "b": 40.0}, "spot": true}));
|
||||
run(&mut s, "swatch.new", json!({"name": "Leaf", "color": {"c": 0.6, "m": 0.0, "y": 1.0, "k": 0.1}}));
|
||||
let data = run(&mut s, "swatch.library.save", json!({"names": ["Ink 101", "Leaf"], "name": "Inks"}))["data"].as_str().unwrap().to_string();
|
||||
let lib = run(&mut s, "swatch.library.load", json!({"data": data, "name": "inks.vcswatches"}))["library"].as_str().unwrap().to_string();
|
||||
run(&mut s, "swatch.delete", json!({"names": ["Ink 101", "Leaf"]}));
|
||||
let red = rect(&mut s, json!("#e02010"));
|
||||
let pink = rect(&mut s, json!("#f09088"));
|
||||
let green = rect(&mut s, json!("#20c040"));
|
||||
select_all(&mut s);
|
||||
let map = json!([
|
||||
{"from": ["rgb 224 32 16", "rgb 240 144 136"], "to": "#e02010"},
|
||||
{"from": ["rgb 32 192 64"], "to": "#20c040"},
|
||||
]);
|
||||
let r = run(&mut s, "recolor.apply", json!({"map": map, "method": "scaleTints", "limitTo": lib}));
|
||||
assert_eq!(r["swatches"], json!(["Ink 101", "Leaf"]), "{r}");
|
||||
let d = &s.doc().unwrap().doc;
|
||||
let ink = d.swatch("Ink 101").unwrap();
|
||||
assert!(ink.spot && ink.global, "the spot colour joined the document's swatches");
|
||||
let paint = |id: NodeId| d.node(id).unwrap().appearance.fill_paint();
|
||||
match paint(red) {
|
||||
vectorcraft_color::Paint::Solid { color, swatch, tint } => {
|
||||
assert_eq!(swatch.as_deref(), Some("Ink 101"), "linked to the spot colour");
|
||||
assert_eq!(tint, 1.0);
|
||||
assert_eq!(color, Color::lab(50.0, 70.0, 40.0), "in the spot colour's own model");
|
||||
}
|
||||
p => panic!("{p:?}"),
|
||||
}
|
||||
match paint(pink) {
|
||||
vectorcraft_color::Paint::Solid { swatch, tint, .. } => {
|
||||
assert_eq!(swatch.as_deref(), Some("Ink 101"));
|
||||
assert!(tint > 0.1 && tint < 0.9, "a tint of the spot colour: {tint}");
|
||||
}
|
||||
p => panic!("{p:?}"),
|
||||
}
|
||||
assert_eq!(paint(green), vectorcraft_color::Paint::solid(Color::cmyk(0.6, 0.0, 1.0, 0.1)), "a process swatch's colour, in CMYK");
|
||||
// Again: the document's swatch is used, not added twice.
|
||||
run(&mut s, "edit.undo", json!({}));
|
||||
select_all(&mut s);
|
||||
run(&mut s, "swatch.library.add", json!({"library": lib, "names": ["Ink 101"]}));
|
||||
select_all(&mut s);
|
||||
let r = run(&mut s, "recolor.apply", json!({"map": map, "method": "scaleTints", "limitTo": lib}));
|
||||
assert_eq!(r["swatches"], json!(["Leaf"]));
|
||||
}
|
||||
|
||||
@@ -4,7 +4,9 @@
|
||||
//! Assign tab: Colors (Auto, or a count: similar colours merge into rows), Preserve White, Black and
|
||||
//! Grays, the rows (click to select, Shift or Cmd/Ctrl-click to add; a row's arrow excludes or
|
||||
//! includes it; the hex field edits its new colour), Merge and Separate, Randomize order or
|
||||
//! saturation and brightness, Reset, the Method and Limit to Library. Edit tab: a hue and
|
||||
//! saturation and brightness, Reset, the Method and Limit to Library (its folder button imports a
|
||||
//! library file and limits to it; a library's colours apply as its swatches: a spot colour stays a
|
||||
//! spot colour, and joins the document's swatches). 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. Opened on a colour group (Edit or Apply Color Group), OK also rewrites
|
||||
@@ -443,9 +445,24 @@ fn assign_tab(app: &mut VectorcraftApp, ui: &mut Ui, d: &mut Dialog, rows: &mut
|
||||
let cur = d.str("limitTo");
|
||||
let at = keys.iter().position(|k| *k == cur).unwrap_or(0);
|
||||
let current = shown.get(at).copied().unwrap_or_default();
|
||||
if let Some(k) = widgets::dropdown_names(ui, "recolor-library", current, &shown, 180.0).and_then(|i| keys.get(i)) {
|
||||
d.fields.insert("limitTo".into(), json!(k));
|
||||
}
|
||||
ui.horizontal(|ui| {
|
||||
if let Some(k) = widgets::dropdown_names(ui, "recolor-library", current, &shown, 180.0).and_then(|i| keys.get(i)) {
|
||||
d.fields.insert("limitTo".into(), json!(k));
|
||||
}
|
||||
// Import a library (.ase, .acb, .gpl, .vcswatches or a document's swatches) and limit to it.
|
||||
if app.services.pick_open.is_some() && widgets::icon_button(ui, "folder-open", tl!("Other Library…"), false, 24.0).clicked() {
|
||||
let r = crate::picks::in_dialog(app, d, |app, d| {
|
||||
let pick = crate::FilePick { filters: vectorcraft_engine::cmd::fileio::open_filters().collect(), ..Default::default() };
|
||||
let Some(path) = crate::picks::open(app, &pick) else { return Ok(()) };
|
||||
let r = app.run("swatch.library.load", json!({ "path": path }))?;
|
||||
d.fields.insert("limitTo".into(), r["library"].clone());
|
||||
Ok(())
|
||||
});
|
||||
if let Err(e) = r {
|
||||
app.status(e);
|
||||
}
|
||||
}
|
||||
});
|
||||
ui.end_row();
|
||||
});
|
||||
changed
|
||||
|
||||
Reference in New Issue
Block a user