mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-27 03:06:57 -06:00
M3.59: tints of global and spot colours, tint swatches and Global colour balance
A colour linked to a global or spot swatch keeps a tint (Paint::Solid.tint, 1 by default and
left out of files at 100 %): paint.setFill/setStroke {swatch, tint} apply the swatch at that
percentage, swatch edits recolour every linked colour at its own tint, and Color::tinted
resolves it (CMYK and Gray inks scale, RGB mixes toward white). swatch.new saves a tint as a
tint swatch ("Ink 40%") linked to its base. Spot tints separate as that percentage of their
plate (Separations Preview, PDF Separation value) instead of an ink-ratio guess. Adjust Color
Balance's Global mode (tint) shifts the tints of linked colours. The Color panel shows Tint
mode (the swatch chip, its name and a T slider) for linked colours; the Swatches panel
highlights the tint swatch in use. Gradient stops get the same link and tint fields (the
swatch walks cover them), so GradientStop is no longer Copy.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
e94748dc17
commit
b2ececc7b4
@@ -2,7 +2,7 @@
|
||||
//! three Blend commands. They recolour the selected objects and everything inside them through the
|
||||
//! shared colour visitor [`Recolor`]: fills and strokes (solid colours, gradient stops and text
|
||||
//! runs), gradient meshes, embedded images and pattern fills. Colours linked to a global swatch are
|
||||
//! unlinked when they change.
|
||||
//! unlinked when they change; Adjust Color Balance's Global mode shifts their tints instead.
|
||||
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::sync::Arc;
|
||||
@@ -75,7 +75,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
"Adjust Color Balance…",
|
||||
["Edit", "Edit Colors"],
|
||||
None,
|
||||
"{mode?: \"rgb\"|\"cmyk\"|\"gray\"|\"global\" (default: from the channels given), r?, g?, b? | c?, m?, y?, k? | gray?: -100..100 (% added per channel, default 0), convert?: false (true: the results stay in the adjusted model; false: each colour keeps its own), fill?, stroke?, includeImages?, includePatterns?, ids?} reaching what edit.colors.invert does → {changed}. Global mode (tints of global and spot colours) isn't available yet",
|
||||
"{mode?: \"rgb\"|\"cmyk\"|\"gray\"|\"global\" (default: from the channels given), r?, g?, b? | c?, m?, y?, k? | gray? | tint?: -100..100 (% added per channel, default 0; `tint` is global mode: it shifts the tints of colours and gradient stops linked to global and spot swatches, which stay linked, and leaves other colours and images alone), convert?: false (true: the results stay in the adjusted model; false: each colour keeps its own), fill?, stroke?, includeImages?, includePatterns?, ids?} reaching what edit.colors.invert does → {changed}",
|
||||
has_selection,
|
||||
adjust_balance
|
||||
),
|
||||
@@ -131,34 +131,37 @@ pub(crate) fn to_gray(c: Color) -> Color {
|
||||
c.in_model(Model::Gray)
|
||||
}
|
||||
|
||||
/// Apply `f` to a paint; returns whether anything changed.
|
||||
pub(crate) fn map_paint(p: &mut Paint, f: &dyn Fn(Color) -> Color) -> bool {
|
||||
match p {
|
||||
Paint::Solid { color, swatch } => {
|
||||
let n = f(*color);
|
||||
if n != *color {
|
||||
*color = n;
|
||||
*swatch = None;
|
||||
return true;
|
||||
}
|
||||
false
|
||||
}
|
||||
Paint::Gradient(g) => {
|
||||
let mut ch = false;
|
||||
for st in &mut g.gradient.stops {
|
||||
let n = f(st.color);
|
||||
if n != st.color {
|
||||
st.color = n;
|
||||
ch = true;
|
||||
}
|
||||
}
|
||||
if ch {
|
||||
g.swatch = None;
|
||||
}
|
||||
ch
|
||||
}
|
||||
_ => false,
|
||||
/// What a recolouring pass applies to a colour that can link to a swatch (a solid colour or a
|
||||
/// gradient stop): `(colour, link, tint)`, true when it changed them.
|
||||
pub(crate) type LinkMap<'a> = dyn Fn(&mut Color, &mut Option<String>, &mut f32) -> bool + 'a;
|
||||
|
||||
/// Apply `f` to the solid colour or gradient stops of a paint ([`Paint::map_links`]); a gradient
|
||||
/// that changes is no longer its gradient swatch's. Returns whether anything changed.
|
||||
fn map_links(p: &mut Paint, f: &LinkMap) -> bool {
|
||||
let changed = p.map_links(&mut |c, l, t| f(c, l, t));
|
||||
if changed && let Paint::Gradient(g) = p {
|
||||
g.swatch = None;
|
||||
}
|
||||
changed
|
||||
}
|
||||
|
||||
/// Apply `f` to a paint's colours; a colour that changes loses its swatch link. Returns whether
|
||||
/// anything changed.
|
||||
pub(crate) fn map_paint(p: &mut Paint, f: &dyn Fn(Color) -> Color) -> bool {
|
||||
map_links(p, &|c, link, _| {
|
||||
let n = f(*c);
|
||||
if n == *c {
|
||||
return false;
|
||||
}
|
||||
*c = n;
|
||||
*link = None;
|
||||
true
|
||||
})
|
||||
}
|
||||
|
||||
/// The identity colour map (a pass that only maps linked colours).
|
||||
fn same(c: Color) -> Color {
|
||||
c
|
||||
}
|
||||
|
||||
/// What a recolouring pass covers: fills (with the points of gradient meshes), strokes, the pixels
|
||||
@@ -191,6 +194,8 @@ impl Scope {
|
||||
/// change are kept as copies.
|
||||
pub(crate) struct Recolor<'a> {
|
||||
f: &'a dyn Fn(Color) -> Color,
|
||||
/// Maps solid colours and gradient stops with their link and tint in place of `f`.
|
||||
links: Option<&'a LinkMap<'a>>,
|
||||
scope: Scope,
|
||||
/// Image key → the key of its recoloured copy (`None`: unchanged).
|
||||
images: HashMap<String, Option<String>>,
|
||||
@@ -203,7 +208,13 @@ pub(crate) struct Recolor<'a> {
|
||||
|
||||
impl<'a> Recolor<'a> {
|
||||
pub(crate) fn new(scope: Scope, f: &'a dyn Fn(Color) -> Color) -> Self {
|
||||
Self { f, scope, images: HashMap::new(), patterns: HashMap::new(), new_images: vec![], new_patterns: vec![], changed: 0 }
|
||||
Self { f, links: None, scope, images: HashMap::new(), patterns: HashMap::new(), new_images: vec![], new_patterns: vec![], changed: 0 }
|
||||
}
|
||||
|
||||
/// A pass that maps only solid colours and gradient stops, with their swatch link and tint
|
||||
/// (`links`); meshes and images keep their colours.
|
||||
pub(crate) fn links(scope: Scope, links: &'a LinkMap<'a>) -> Self {
|
||||
Self { links: Some(links), ..Self::new(Scope { images: false, ..scope }, &same) }
|
||||
}
|
||||
|
||||
/// Recolour the objects `ids` and everything inside them (each once), add the new images and
|
||||
@@ -294,7 +305,10 @@ impl<'a> Recolor<'a> {
|
||||
fn paint(&mut self, d: &Document, p: &mut Paint) {
|
||||
let changed = match p {
|
||||
Paint::Pattern { pattern, .. } if self.scope.patterns => self.pattern(d, pattern).map(|new| *pattern = new).is_some(),
|
||||
_ => map_paint(p, self.f),
|
||||
_ => match self.links {
|
||||
Some(f) => map_links(p, f),
|
||||
None => map_paint(p, self.f),
|
||||
},
|
||||
};
|
||||
self.changed += changed as usize;
|
||||
}
|
||||
@@ -348,12 +362,16 @@ impl<'a> Recolor<'a> {
|
||||
}
|
||||
|
||||
pub(crate) fn recolor(s: &mut Session, p: &Value, label: &str, f: &dyn Fn(Color) -> Color) -> Result<Value> {
|
||||
run_pass(s, p, label, Recolor::new(Scope::of(p), f))
|
||||
}
|
||||
|
||||
/// Run `pass` on the `ids` param's objects (default: the selection) as one undo step.
|
||||
fn run_pass(s: &mut Session, p: &Value, label: &str, pass: Recolor) -> Result<Value> {
|
||||
let ids = match ids_param(p, "ids") {
|
||||
Some(v) => v,
|
||||
None => selected_roots(s)?,
|
||||
};
|
||||
let scope = Scope::of(p);
|
||||
let n = s.edit(label, |d, _| Ok(Recolor::new(scope, f).run(d, &ids)))?;
|
||||
let n = s.edit(label, |d, _| Ok(pass.run(d, &ids)))?;
|
||||
Ok(json!({ "changed": n }))
|
||||
}
|
||||
|
||||
@@ -376,7 +394,8 @@ fn adjust_balance(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
None if given(&["c", "m", "y", "k"]) => "cmyk".into(),
|
||||
None if given(&["gray"]) => "gray".into(),
|
||||
None if given(&["r", "g", "b"]) => "rgb".into(),
|
||||
None => return Err(bad(C, "give a mode or r/g/b, c/m/y/k or gray adjustments")),
|
||||
None if given(&["tint"]) => "global".into(),
|
||||
None => return Err(bad(C, "give a mode or r/g/b, c/m/y/k, gray or tint adjustments")),
|
||||
};
|
||||
let g = |k: &str| (f64_or(p, k, 0.0).clamp(-100.0, 100.0) / 100.0) as f32;
|
||||
let cl = |v: f32| v.clamp(0.0, 1.0);
|
||||
@@ -403,7 +422,20 @@ fn adjust_balance(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
})
|
||||
}
|
||||
"global" => {
|
||||
return Err(bad(C, "mode `global` shifts the tints of global and spot colours, which aren't supported yet: use rgb, cmyk or gray"));
|
||||
// Each linked colour moves to the shifted tint of its global swatch's current colour.
|
||||
let d = &s.doc()?.doc;
|
||||
let bases: HashMap<String, Color> = d.swatches_iter().filter_map(|w| Some((w.name.clone(), d.global_color(&w.name)?))).collect();
|
||||
let dt = g("tint");
|
||||
let shift = move |c: &mut Color, link: &mut Option<String>, tint: &mut f32| {
|
||||
let Some(base) = link.as_ref().and_then(|l| bases.get(l)) else { return false };
|
||||
let t = cl(*tint + dt);
|
||||
if t == *tint {
|
||||
return false;
|
||||
}
|
||||
(*tint, *c) = (t, base.tinted(t));
|
||||
true
|
||||
};
|
||||
return run_pass(s, p, "Adjust Colors", Recolor::links(Scope::of(p), &shift));
|
||||
}
|
||||
other => return Err(bad(C, format!("unknown mode `{other}` (rgb|cmyk|gray|global)"))),
|
||||
};
|
||||
|
||||
@@ -268,6 +268,15 @@ fn convert_mode(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
// Tints are their converted swatch's colour at their tint (exact ink percentages).
|
||||
let bases: Vec<(String, Color)> = d.swatches_iter().filter_map(|w| Some((w.name.clone(), d.global_color(&w.name)?))).collect();
|
||||
d.map_solid_paints(&mut |c, link, tint| match bases.iter().find(|(n, _)| Some(n) == link.as_ref()) {
|
||||
Some((_, base)) if *tint < 1.0 => {
|
||||
*c = base.tinted(*tint);
|
||||
true
|
||||
}
|
||||
_ => false,
|
||||
});
|
||||
Ok(())
|
||||
})?;
|
||||
Ok(json!({ "changed": changed }))
|
||||
|
||||
@@ -218,7 +218,7 @@ fn flare(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
seed ^= seed << 17;
|
||||
(seed >> 11) as f64 / (1u64 << 53) as f64
|
||||
};
|
||||
let stop = |offset: f32, opacity: f32| GradientStop { offset, color: vectorcraft_color::Color::WHITE, opacity, midpoint: 0.5 };
|
||||
let stop = |offset: f32, opacity: f32| GradientStop { opacity, ..GradientStop::new(offset, vectorcraft_color::Color::WHITE) };
|
||||
let radial = |centre: Point, radius: f64, stops: Vec<GradientStop>| {
|
||||
let mut g = GradientPaint::new(Gradient { kind: GradientKind::Radial, stops });
|
||||
g.geom = Some(GradientGeom { start: centre, end: centre + vectorcraft_geom::Vec2::new(radius, 0.0), aspect: 1.0 });
|
||||
|
||||
@@ -124,6 +124,8 @@ pub(crate) fn parse_stops(v: &Value) -> Parsed<Vec<GradientStop>> {
|
||||
color,
|
||||
opacity: opacity.clamp(0.0, 1.0) as f32,
|
||||
midpoint: num("midpoint")?.unwrap_or(0.5).clamp(0.13, 0.87) as f32,
|
||||
swatch: None,
|
||||
tint: 1.0,
|
||||
})
|
||||
})
|
||||
.collect::<Parsed<Vec<_>>>()?;
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
use serde_json::{Value, json};
|
||||
use vectorcraft_color::{BlendMode, Color, Paint};
|
||||
use vectorcraft_doc::{Appearance, CharStyle, Node, NodeId, NodeKind};
|
||||
use vectorcraft_doc::{Appearance, CharStyle, Document, Node, NodeId, NodeKind};
|
||||
|
||||
use super::appearance::{ItemTarget, appearance_targets, edit_items, item_target};
|
||||
use super::gradient::{item_paint_bounds, place_paint, place_run_paint, run_paint_mut, run_stroke_weight, unplaced};
|
||||
@@ -16,7 +16,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
"Fill",
|
||||
[],
|
||||
None,
|
||||
"{color?: \"#rrggbb\"|[r,g,b]|{c,m,y,k}|{gray}, none?: true, swatch?: name (a gradient swatch fits each object, keeping its aspect), gradient?: {kind?: linear|radial|freeform, stops?: [{offset 0..1, color, opacity? 0..1 (or 0..100), midpoint? 0.13..0.87}] (at least 2; default white→black), angle?: deg, start?: [x,y], end?: [x,y] (the vector in document coordinates, both or neither; type objects keep it in text space), aspect?: % (radial; without start/end the gradient is placed on each object's bounds), swatch?: linked gradient swatch name}, item?: appearance item index|null (omitted: the Appearance panel's active item if it is a fill, else the top fill), ids?, focus?: true (false keeps the active proxy), keepModel?: false (in a CMYK document, RGB colours and gradient stops are stored as CMYK unless true; Gray stays Gray)} sets the selection's fill and the default (new art fits a gradient to itself)",
|
||||
"{color?: \"#rrggbb\"|[r,g,b]|{c,m,y,k}|{gray}, none?: true, swatch?: name (a global or spot colour stays linked, so swatch edits recolour it; a tint swatch links to its base at its tint; a gradient swatch fits each object, keeping its aspect), tint?: 0..100 (% of a global or spot `swatch`; default 100, or a tint swatch's own), gradient?: {kind?: linear|radial|freeform, stops?: [{offset 0..1, color, opacity? 0..1 (or 0..100), midpoint? 0.13..0.87}] (at least 2; default white→black), angle?: deg, start?: [x,y], end?: [x,y] (the vector in document coordinates, both or neither; type objects keep it in text space), aspect?: % (radial; without start/end the gradient is placed on each object's bounds), swatch?: linked gradient swatch name}, item?: appearance item index|null (omitted: the Appearance panel's active item if it is a fill, else the top fill), ids?, focus?: true (false keeps the active proxy), keepModel?: false (in a CMYK document, RGB colours and gradient stops are stored as CMYK unless true; Gray stays Gray)} sets the selection's fill and the default (new art fits a gradient to itself)",
|
||||
has_doc,
|
||||
|s, p| set_paint(s, p, true)
|
||||
),
|
||||
@@ -304,19 +304,18 @@ pub(crate) fn paint_from(s: &Session, p: &Value) -> Result<Option<Paint>> {
|
||||
return Ok(Some(Paint::None));
|
||||
}
|
||||
if let Some(name) = str_param(p, "swatch") {
|
||||
let st = s.doc()?;
|
||||
let d = &s.doc()?.doc;
|
||||
// A pattern definition works as its swatch even without a swatch entry.
|
||||
if st.doc.swatch(name).is_none() && st.doc.pattern(name).is_some() {
|
||||
if d.swatch(name).is_none() && d.pattern(name).is_some() {
|
||||
return Ok(Some(vectorcraft_doc::pattern::pattern_paint(name)));
|
||||
}
|
||||
let sw = st.doc.swatch(name).ok_or_else(|| EngineError::Other(format!("no swatch `{name}`")))?;
|
||||
let mut paint = sw.paint.clone();
|
||||
if sw.global
|
||||
&& let Paint::Solid { swatch, .. } = &mut paint
|
||||
{
|
||||
*swatch = Some(name.to_string());
|
||||
}
|
||||
return Ok(Some(paint));
|
||||
let sw = d.swatch(name).ok_or_else(|| EngineError::Other(format!("no swatch `{name}`")))?;
|
||||
let tint = tint_param(p, "paint")?;
|
||||
return match &sw.paint {
|
||||
Paint::Solid { .. } => swatch_solid(d, name, tint).map(Some).map_err(|e| bad("paint", e)),
|
||||
_ if tint.is_some() => Err(bad("paint", "`tint` applies to global and spot colours")),
|
||||
other => Ok(Some(other.clone())),
|
||||
};
|
||||
}
|
||||
if let Some(g) = p.get("gradient") {
|
||||
let mut gp = super::gradient::parse_gradient(g).map_err(|e| bad("paint", e))?;
|
||||
@@ -330,8 +329,35 @@ pub(crate) fn paint_from(s: &Session, p: &Value) -> Result<Option<Paint>> {
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
/// The `tint` param (a percentage) as 0..1.
|
||||
pub(crate) fn tint_param(p: &Value, cmd: &str) -> Result<Option<f32>> {
|
||||
p.get("tint")
|
||||
.filter(|v| !v.is_null())
|
||||
.map(|v| v.as_f64().map(|t| (t / 100.0).clamp(0.0, 1.0) as f32).ok_or_else(|| bad(cmd, "`tint` must be a number (%)")))
|
||||
.transpose()
|
||||
}
|
||||
|
||||
/// The solid paint colour swatch `name` applies at `tint` (0..1): a global or spot colour links to
|
||||
/// itself (default 100%), a tint swatch to its base (default: its own tint), a process colour is
|
||||
/// just its colour (it has no tints).
|
||||
pub(crate) fn swatch_solid(d: &Document, name: &str, tint: Option<f32>) -> std::result::Result<Paint, String> {
|
||||
let sw = d.swatch(name).ok_or_else(|| format!("no swatch `{name}`"))?;
|
||||
if sw.paint.color().is_none() {
|
||||
return Err(format!("`{name}` isn't a colour swatch"));
|
||||
}
|
||||
match (d.swatch_link(name), tint) {
|
||||
// A tint swatch whose base is gone keeps its colour.
|
||||
(Some((base, own)), _) => Ok(d.tint_paint(&base, tint.unwrap_or(own)).unwrap_or_else(|| sw.paint.clone())),
|
||||
(None, None) => Ok(sw.paint.clone()),
|
||||
(None, Some(_)) => Err(format!("`{name}` is a process colour: tints apply to global and spot colours")),
|
||||
}
|
||||
}
|
||||
|
||||
fn set_paint(s: &mut Session, p: &Value, fill: bool) -> Result<Value> {
|
||||
let cmd = if fill { "paint.setFill" } else { "paint.setStroke" };
|
||||
if p.get("tint").is_some() && p.get("swatch").is_none() {
|
||||
return Err(bad(cmd, "`tint` goes with a global or spot `swatch`"));
|
||||
}
|
||||
let paint = paint_from(s, p)?.ok_or_else(|| bad(cmd, "give color, none, swatch or gradient"))?;
|
||||
apply_paint(s, p, paint, fill)
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
//! The Swatches panel: new, delete and duplicate swatches, colour groups and sorting.
|
||||
|
||||
use serde_json::{Value, json};
|
||||
use vectorcraft_color::{Color, Paint, Swatch, SwatchGroup};
|
||||
use vectorcraft_color::{Color, Paint, Swatch, SwatchGroup, tint_percent};
|
||||
use vectorcraft_doc::pattern::pattern_paint;
|
||||
use vectorcraft_doc::swatches::{color_name, node_colors};
|
||||
use vectorcraft_doc::swatches::{LinkFn, color_name, node_colors};
|
||||
use vectorcraft_doc::{Document, NodeId};
|
||||
|
||||
use super::paint::paint_from;
|
||||
@@ -17,7 +17,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
"New Swatch",
|
||||
["Window", "Swatches"],
|
||||
None,
|
||||
"{name?, color? | colors?: [colour] (one swatch each, in one undo step) | swatch? | gradient? | pattern?: name (default: the current fill), mode?: \"gray\"|\"rgb\"|\"hsb\"|\"cmyk\"|\"web\" (convert the colour), global?, spot? (a spot colour, always global), group?: colour group name (solid colours only; default: ungrouped)} 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, names: [every new swatch]}",
|
||||
"{name?, color? | colors?: [colour] (one swatch each, in one undo step) | swatch? | gradient? | pattern?: name (default: the current fill), tint?: 0..100 (% of the global or spot `swatch`, or of the one the current fill links to), mode?: \"gray\"|\"rgb\"|\"hsb\"|\"cmyk\"|\"web\" (convert the colour), global?, spot? (a spot colour, always global), group?: colour group name (solid colours only; default: ungrouped)} save a colour, gradient or pattern as a swatch. A tint of a global or spot colour (below 100%, without mode, global or spot) becomes a tint swatch, \"Name 40%\", linked to its base: it follows edits to the base and applies as that tint of it. 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, names: [every new swatch]}",
|
||||
has_doc,
|
||||
swatch_new
|
||||
),
|
||||
@@ -46,7 +46,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
"Swatch Options",
|
||||
["Window", "Swatches"],
|
||||
None,
|
||||
"{name, newName?, color?, mode?: \"gray\"|\"rgb\"|\"hsb\"|\"cmyk\"|\"web\" (convert the colour), global?, spot? (spot colours are always global), paint?: paint.setFill params ({color}, {gradient}, {swatch} or {pattern}) replacing the swatch's colour, gradient or pattern with one of the same kind} edit a swatch in any colour group, as one undo step. Fills, strokes and text linked to a global swatch take its new colour and name; turning Global off unlinks them (they keep their colour). Colour, mode and spot apply to solid colours only → {name, relinked: paints changed}",
|
||||
"{name, newName?, color?, mode?: \"gray\"|\"rgb\"|\"hsb\"|\"cmyk\"|\"web\" (convert the colour), global?, spot? (spot colours are always global), paint?: paint.setFill params ({color}, {gradient}, {swatch} or {pattern}) replacing the swatch's colour, gradient or pattern with one of the same kind} edit a swatch in any colour group, as one undo step. Fills, strokes, text, gradient stops and tint swatches linked to a global swatch take its new colour (at their own tint) and name; turning Global off unlinks them (they keep their colour). Colour, mode and spot apply to solid colours only; a tint swatch only takes a new name (edit its base) → {name, relinked: paints changed}",
|
||||
has_doc,
|
||||
swatch_edit
|
||||
),
|
||||
@@ -55,7 +55,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
"Swatches",
|
||||
[],
|
||||
None,
|
||||
"{group?: name (only that colour group's swatches)} → {swatches: [{name, kind: \"none\"|\"color\"|\"gradient\"|\"pattern\", group, global, spot, color?, hex?, gradient?, pattern?}], groups: [{name, swatches: [names]}]}",
|
||||
"{group?: name (only that colour group's swatches)} → {swatches: [{name, kind: \"none\"|\"color\"|\"gradient\"|\"pattern\", group, global, spot, color?, hex?, tintOf?: base swatch of a tint swatch, tint?: its %, gradient?, pattern?}], groups: [{name, swatches: [names]}]}",
|
||||
has_doc,
|
||||
swatch_list
|
||||
),
|
||||
@@ -129,7 +129,17 @@ fn swatch_new(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
}
|
||||
(None, Some(name)) if s.doc()?.doc.pattern(name).is_some() => vec![pattern_paint(name)],
|
||||
(None, Some(name)) => return Err(bad(C, format!("no pattern `{name}`"))),
|
||||
(None, None) => vec![paint_from(s, p)?.unwrap_or_else(|| s.paint.fill.clone())],
|
||||
(None, None) => {
|
||||
let paint = paint_from(s, p)?.unwrap_or_else(|| s.paint.fill.clone());
|
||||
// `tint` alone: that tint of the global colour the current fill links to.
|
||||
match (super::paint::tint_param(p, C)?, &paint) {
|
||||
(Some(t), Paint::Solid { swatch: Some(l), .. }) if p.get("swatch").is_none() => {
|
||||
vec![s.doc()?.doc.tint_paint(l, t).ok_or_else(|| bad(C, format!("`{l}` isn't a global or spot colour")))?]
|
||||
}
|
||||
(Some(_), _) if p.get("swatch").is_none() => return Err(bad(C, "`tint` needs a global or spot colour")),
|
||||
_ => vec![paint],
|
||||
}
|
||||
}
|
||||
};
|
||||
let spot = bool_or(p, "spot", false);
|
||||
let global = spot || bool_or(p, "global", false);
|
||||
@@ -138,6 +148,8 @@ fn swatch_new(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let paints = paints
|
||||
.into_iter()
|
||||
.map(|paint| match paint {
|
||||
// A tint stays linked to its base: a tint swatch.
|
||||
Paint::Solid { swatch: Some(_), tint, .. } if tint < 1.0 && mode.is_none() && !global => Ok(paint),
|
||||
// The swatch holds the colour itself, not a link to the swatch it may have come from.
|
||||
Paint::Solid { color, .. } => Ok(Paint::solid(match mode {
|
||||
Some(m) => convert_to_mode(color, m, C)?,
|
||||
@@ -157,7 +169,9 @@ fn swatch_new(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
Some(n) => d.free_swatch_name(n),
|
||||
None => d.new_swatch_name(&paint),
|
||||
};
|
||||
let swatch = Swatch { name: name.clone(), paint, global, spot };
|
||||
// A tint swatch's kind is its base's.
|
||||
let tint = matches!(paint, Paint::Solid { swatch: Some(_), .. });
|
||||
let swatch = Swatch { name: name.clone(), paint, global: global && !tint, spot: spot && !tint };
|
||||
match group {
|
||||
Some(g) => {
|
||||
d.swatch_groups.iter_mut().find(|x| x.name == g).ok_or_else(|| bad(C, format!("no colour group `{g}`")))?.swatches.push(swatch)
|
||||
@@ -204,7 +218,8 @@ fn swatch_delete(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
if !bool_or(p, "unlink", true) {
|
||||
global.clear();
|
||||
}
|
||||
let mut unlink = |_: &mut Color, link: &mut Option<String>| link.as_ref().is_some_and(|l| global.contains(l)) && link.take().is_some();
|
||||
let mut unlink =
|
||||
|_: &mut Color, link: &mut Option<String>, _: &mut f32| link.as_ref().is_some_and(|l| global.contains(l)) && link.take().is_some();
|
||||
let unlinked = s.edit("Delete Swatch", |d, _| {
|
||||
d.swatch_groups.retain(|g| !names.contains(&g.name));
|
||||
for n in &names {
|
||||
@@ -244,7 +259,7 @@ fn link_colors(d: &mut Document, ids: &[NodeId], links: &[(Color, String)]) -> u
|
||||
return 0;
|
||||
}
|
||||
let live: Vec<String> = d.swatches_iter().map(|w| w.name.clone()).collect();
|
||||
d.map_solid_paints_in(ids, &mut |c, link| {
|
||||
d.map_solid_paints_in(ids, &mut |c, link, _| {
|
||||
if link.as_ref().is_some_and(|l| live.contains(l)) {
|
||||
return false;
|
||||
}
|
||||
@@ -416,15 +431,15 @@ struct Relink {
|
||||
from: String,
|
||||
/// The swatch's name after the edit.
|
||||
to: String,
|
||||
/// The new colour of a global swatch (linked paints take it).
|
||||
/// The new colour of a global swatch (linked paints take it, at their tint).
|
||||
color: Option<Color>,
|
||||
/// false: drop the links (the swatch stopped being global); paints keep their colour.
|
||||
keep: bool,
|
||||
}
|
||||
|
||||
impl Relink {
|
||||
/// Apply to one paint; true when it changed.
|
||||
fn apply(&self, c: &mut Color, link: &mut Option<String>) -> bool {
|
||||
/// Apply to one linked colour; true when it changed.
|
||||
fn apply(&self, c: &mut Color, link: &mut Option<String>, tint: &mut f32) -> bool {
|
||||
if link.as_deref() != Some(self.from.as_str()) {
|
||||
return false;
|
||||
}
|
||||
@@ -436,28 +451,27 @@ impl Relink {
|
||||
if renamed {
|
||||
*link = Some(self.to.clone());
|
||||
}
|
||||
let recolored = self.color.is_some_and(|n| n != *c);
|
||||
if let Some(n) = self.color {
|
||||
let new = self.color.map(|n| n.tinted(*tint));
|
||||
let recolored = new.is_some_and(|n| n != *c);
|
||||
if let Some(n) = new {
|
||||
*c = n;
|
||||
}
|
||||
renamed || recolored
|
||||
}
|
||||
|
||||
fn defaults(&self, s: &mut Session) {
|
||||
map_default_paints(s, &mut |c, l| self.apply(c, l));
|
||||
map_default_paints(s, &mut |c, l, t| self.apply(c, l, t));
|
||||
}
|
||||
}
|
||||
|
||||
/// Apply a swatch-link rewrite to the default fill and stroke for new art, except during a live
|
||||
/// preview (Cancel rolls the document back, not the session).
|
||||
pub(super) fn map_default_paints(s: &mut Session, f: &mut dyn FnMut(&mut Color, &mut Option<String>) -> bool) {
|
||||
pub(super) fn map_default_paints(s: &mut Session, f: &mut LinkFn) {
|
||||
if s.in_interaction() {
|
||||
return;
|
||||
}
|
||||
for p in [&mut s.paint.fill, &mut s.paint.stroke] {
|
||||
if let Paint::Solid { color, swatch } = p {
|
||||
f(color, swatch);
|
||||
}
|
||||
p.map_links(f);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -474,6 +488,11 @@ fn swatch_edit(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
if solid.is_none() && (given("color") || given("mode") || bool_or(p, "spot", false)) {
|
||||
return Err(bad(C, "colour, mode and spot apply to solid-colour swatches only"));
|
||||
}
|
||||
if let Some((base, _)) = sw.tint_of()
|
||||
&& ["color", "mode", "paint", "global", "spot"].iter().any(|k| given(k))
|
||||
{
|
||||
return Err(bad(C, format!("`{name}` is a tint of `{base}`: edit `{base}` (a tint swatch only takes a new name)")));
|
||||
}
|
||||
// A replacement paint of the swatch's kind (a dropped colour or edited gradient).
|
||||
let paint = match p.get("paint").filter(|v| !v.is_null()) {
|
||||
Some(v) => match (paint_from(s, v)?, &sw.paint) {
|
||||
@@ -510,7 +529,7 @@ fn swatch_edit(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(d.map_solid_paints(&mut |c, l| relink.apply(c, l)))
|
||||
Ok(d.map_solid_paints(&mut |c, l, t| relink.apply(c, l, t)))
|
||||
})?;
|
||||
relink.defaults(s);
|
||||
Ok(json!({"name": to, "relinked": relinked}))
|
||||
@@ -520,10 +539,14 @@ pub(super) fn swatch_json(sw: &Swatch, group: Option<&str>) -> Value {
|
||||
let mut v = json!({"name": sw.name, "group": group, "global": sw.global, "spot": sw.spot});
|
||||
match &sw.paint {
|
||||
Paint::None => v["kind"] = json!("none"),
|
||||
Paint::Solid { color, .. } => {
|
||||
Paint::Solid { color, swatch, tint } => {
|
||||
v["kind"] = json!("color");
|
||||
v["color"] = json!(color);
|
||||
v["hex"] = json!(color.to_hex());
|
||||
if let Some(base) = swatch {
|
||||
v["tintOf"] = json!(base);
|
||||
v["tint"] = json!(tint_percent(*tint));
|
||||
}
|
||||
}
|
||||
Paint::Gradient(g) => {
|
||||
v["kind"] = json!("gradient");
|
||||
@@ -592,20 +615,28 @@ fn swatch_add_used(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
|
||||
fn swatch_unused(s: &mut Session, _: &Value) -> Result<Value> {
|
||||
let d = &s.doc()?.doc;
|
||||
let (mut links, mut colors, mut gradients, mut patterns) = (vec![], vec![], vec![], vec![]);
|
||||
// Links with their tints, so a tint swatch is used where its tint of its base is.
|
||||
let (mut links, mut colors, mut gradients, mut patterns, mut gradient_links) = (vec![], vec![], vec![], vec![], vec![]);
|
||||
let mut color = |c: &Color, link: Option<&String>, tint: f32| match link {
|
||||
Some(l) if d.swatch(l).is_some() => links.push((l.clone(), tint)),
|
||||
_ => colors.push(*c),
|
||||
};
|
||||
d.visit_paints(&mut |p| match p {
|
||||
Paint::Solid { swatch: Some(l), .. } if d.swatch(l).is_some() => links.push(l.clone()),
|
||||
Paint::Solid { color, .. } => colors.push(*color),
|
||||
Paint::Solid { color: c, swatch, tint } => color(c, swatch.as_ref(), *tint),
|
||||
Paint::Gradient(g) => {
|
||||
links.extend(g.swatch.clone());
|
||||
colors.extend(g.gradient.stops.iter().map(|s| s.color));
|
||||
g.gradient.stops.iter().for_each(|s| color(&s.color, s.swatch.as_ref(), s.tint));
|
||||
gradient_links.extend(g.swatch.clone());
|
||||
gradients.push(g.gradient.clone());
|
||||
}
|
||||
Paint::Pattern { pattern, .. } => patterns.push(pattern.clone()),
|
||||
Paint::None => {}
|
||||
});
|
||||
let used = |w: &Swatch| {
|
||||
links.contains(&w.name)
|
||||
gradient_links.contains(&w.name)
|
||||
|| links.iter().any(|(l, t)| match w.tint_of() {
|
||||
Some((base, tint)) => l == base && *t == tint,
|
||||
None => *l == w.name,
|
||||
})
|
||||
|| match &w.paint {
|
||||
Paint::None => true,
|
||||
Paint::Solid { color, .. } => colors.contains(color),
|
||||
@@ -632,11 +663,12 @@ fn swatch_merge(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
}
|
||||
let kept = d.swatch(keep).ok_or_else(|| bad(C, format!("no swatch `{keep}`")))?;
|
||||
let (color, link) = (kept.paint.color().unwrap_or_default(), kept.global.then(|| keep.clone()));
|
||||
let mut relink = |c: &mut Color, l: &mut Option<String>| {
|
||||
// Tints keep their tint (of the kept colour).
|
||||
let mut relink = |c: &mut Color, l: &mut Option<String>, t: &mut f32| {
|
||||
if !l.as_ref().is_some_and(|l| merged.contains(l)) {
|
||||
return false;
|
||||
}
|
||||
*c = color;
|
||||
*c = color.tinted(*t);
|
||||
*l = link.clone();
|
||||
true
|
||||
};
|
||||
|
||||
@@ -397,7 +397,7 @@ fn reset_defaults(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
(d.swatches, d.swatch_groups) = (defaults, groups);
|
||||
added.extend(d.swatches_iter().filter(|w| !before.contains(&w.name)).map(|w| w.name.clone()));
|
||||
live.extend(d.swatches_iter().filter(|w| w.global).map(|w| w.name.clone()));
|
||||
d.map_solid_paints(&mut |_, l| unlink_dead(&live, l));
|
||||
d.map_solid_paints(&mut |_, l, _| unlink_dead(&live, l));
|
||||
return Ok(added);
|
||||
}
|
||||
for (i, w) in defaults.into_iter().enumerate() {
|
||||
@@ -427,7 +427,7 @@ fn reset_defaults(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
Ok(added)
|
||||
})?;
|
||||
if replace {
|
||||
map_default_paints(s, &mut |_, l| unlink_dead(&live, l));
|
||||
map_default_paints(s, &mut |_, l, _| unlink_dead(&live, l));
|
||||
}
|
||||
Ok(json!({ "added": added }))
|
||||
}
|
||||
|
||||
@@ -355,7 +355,7 @@ fn sample_color(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
};
|
||||
let mut stops = g.gradient.stops;
|
||||
let n = stops.len();
|
||||
stops.get_mut(i).ok_or_else(|| bad(C, format!("no stop {i} (the gradient has {n})")))?.color = color;
|
||||
stops.get_mut(i).ok_or_else(|| bad(C, format!("no stop {i} (the gradient has {n})")))?.set_color(color, None);
|
||||
("paint.editGradient", json!({ "stops": vectorcraft_tools::params::stops_json(&stops), "stroke": stroke }))
|
||||
}
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user