Recolor: the Global Adjust sliders change every color at once

The Edit tab's color mode menu offers Global Adjust, with Saturation,
Brightness, Temperature and Luminosity sliders from -100% to 100% that
open at 0%, which changes nothing. One slider changes every recolored
color, each one separately, in its own model; the rows keep their new
colors and the art previews the change. The Edit tab also shows the
color sliders of the selected new color in the other modes.

recolor.apply takes globalAdjust {saturation, brightness, temperature,
luminosity}. Excluded rows and colors no row holds stay as they are, a
rewritten color group takes the same change, and with Limit to Library
each row's new color takes it before it snaps to the library.

Docs: docs/mcp.md, docs/control-protocol.md, docs/target-app-parity.md.
This commit is contained in:
dotCooCoo
2026-11-01 17:20:56 -08:00
parent 639c9d6ce9
commit 0567eaffbf
34 changed files with 590 additions and 41 deletions
+142
View File
@@ -255,6 +255,77 @@ pub fn shaded(c: Color, f: f32) -> Color {
}
}
/// Recolor Artwork's Global Adjust: the same change to every color, by four amounts from -1 to 1
/// (-100% to +100%), where 0 leaves a color as it is. The steps run in this order:
///
/// 1. Saturation multiplies HSB saturation by 1 + `saturation` (-1 gives a gray, 1 doubles it).
/// 2. Brightness moves HSB brightness that share of the way to its top (above 0) or to black
/// (below 0).
/// 3. Temperature adds 0.2 × `temperature` to display red and takes as much from blue (warmer
/// above 0, cooler below).
/// 4. Luminosity moves Lab lightness that share of the way to 100 (above 0) or to 0 (below 0),
/// with Lab a and b, and so the Lab hue, kept.
///
/// The result is in the model of the color it changes, with its components clamped to their
/// ranges. An amount that is not finite counts as 0.
#[derive(Clone, Copy, Debug, Default, PartialEq)]
pub struct GlobalAdjust {
pub saturation: f32,
pub brightness: f32,
pub temperature: f32,
pub luminosity: f32,
}
impl GlobalAdjust {
/// The four amounts, in order, each finite and within -1..=1.
fn amounts(&self) -> [f32; 4] {
[self.saturation, self.brightness, self.temperature, self.luminosity].map(|v| if v.is_finite() { v.clamp(-1.0, 1.0) } else { 0.0 })
}
/// Whether every color stays as it is (all four amounts 0).
pub fn is_identity(&self) -> bool {
self.amounts().iter().all(|v| *v == 0.0)
}
/// `c` with the adjustment, in `c`'s model; `c` itself when [`GlobalAdjust::is_identity`].
pub fn apply(&self, c: Color) -> Color {
let [sat, bri, temp, lum] = self.amounts();
if [sat, bri, temp, lum].iter().all(|v| *v == 0.0) {
return c;
}
// `v` moved the share `a` of the way to `top` (a > 0) or to 0 (a < 0).
let toward = |v: f32, a: f32, top: f32| if a >= 0.0 { v + (top - v) * a } else { v * (1.0 + a) };
let mut out = c;
if sat != 0.0 || bri != 0.0 {
let [h, s, v] = out.to_hsb();
out = Color::from_hsb(h, (s * (1.0 + sat)).clamp(0.0, 1.0), toward(v.clamp(0.0, 1.0), bri, 1.0));
}
if temp != 0.0 {
let [r, g, b] = out.to_rgb();
out = Color::rgb((r + 0.2 * temp).clamp(0.0, 1.0), g.clamp(0.0, 1.0), (b - 0.2 * temp).clamp(0.0, 1.0));
}
if lum != 0.0 {
let Lab { l, a, b } = match out {
Color::Lab { l, a, b } => Lab::new(l, a, b),
_ => out.to_lab(),
};
out = Color::lab(toward(l.clamp(0.0, 100.0), lum, 100.0), a, b);
}
clamped(keep_model(c, out))
}
}
/// `c` with its components in their ranges: RGB, CMYK and Gray 0..1, Lab lightness 0..100.
fn clamped(c: Color) -> Color {
let u = |v: f32| v.clamp(0.0, 1.0);
match c {
Color::Rgb { r, g, b } => Color::rgb(u(r), u(g), u(b)),
Color::Cmyk { c, m, y, k } => Color::cmyk(u(c), u(m), u(y), u(k)),
Color::Gray { k } => Color::gray(u(k)),
Color::Lab { l, a, b } => Color::lab(l.clamp(0.0, 100.0), a, b),
}
}
/// One row of Recolor Artwork prepared for mapping: its current colours take `to` by `method`.
#[derive(Clone, Copy, Debug)]
pub struct RowMap {
@@ -615,6 +686,77 @@ mod tests {
assert!(row.iter().all(|c| kept.map(*c).model() == c.model()));
}
fn adjust(saturation: f32, brightness: f32, temperature: f32, luminosity: f32) -> GlobalAdjust {
GlobalAdjust { saturation, brightness, temperature, luminosity }
}
#[test]
fn global_adjust_at_zero_leaves_every_color_as_it_is() {
let zero = GlobalAdjust::default();
assert!(zero.is_identity());
assert!(adjust(f32::NAN, 0.0, f32::INFINITY, 0.0).is_identity(), "amounts that aren't finite count as 0");
for c in [Color::rgb(0.3, 0.6, 0.9), Color::cmyk(0.1234, 0.5, 0.0, 0.07), Color::gray(0.4), Color::lab(55.0, 60.5, -40.25)] {
assert_eq!(zero.apply(c), c);
}
assert!(!adjust(0.0, 0.0, 0.0, 0.01).is_identity());
}
#[test]
fn global_adjust_saturation_and_brightness_reach_their_ends() {
let c = Color::rgb(0.8, 0.4, 0.2);
let [h, s, v] = c.to_hsb();
let gray = adjust(-1.0, 0.0, 0.0, 0.0).apply(c).to_hsb();
assert!(gray[1].abs() < 1e-4 && (gray[2] - v).abs() < 1e-4, "-100% saturation gives a gray as bright: {gray:?}");
let more = adjust(0.5, 0.0, 0.0, 0.0).apply(c).to_hsb();
assert!((more[0] - h).abs() < 0.5 && more[1] > s && (more[1] - (s * 1.5).min(1.0)).abs() < 1e-3, "{more:?}");
assert!(adjust(-1.0, 0.0, 0.0, 0.0).apply(Color::rgb(0.5, 0.5, 0.5)) == Color::rgb(0.5, 0.5, 0.5), "a gray stays gray");
assert!(adjust(0.0, -1.0, 0.0, 0.0).apply(c).to_hsb()[2] < 1e-4, "-100% brightness is black");
assert!((adjust(0.0, 1.0, 0.0, 0.0).apply(c).to_hsb()[2] - 1.0).abs() < 1e-4, "+100% brightness is full brightness");
let half = adjust(0.0, 0.5, 0.0, 0.0).apply(c).to_hsb();
assert!((half[2] - (v + (1.0 - v) * 0.5)).abs() < 1e-3 && (half[1] - s).abs() < 1e-3, "{half:?}");
}
#[test]
fn global_adjust_temperature_warms_and_cools() {
let c = Color::rgb(0.5, 0.5, 0.5);
let warm = adjust(0.0, 0.0, 1.0, 0.0).apply(c).to_rgb();
let cool = adjust(0.0, 0.0, -1.0, 0.0).apply(c).to_rgb();
assert!(warm[0] > warm[1] && warm[1] > warm[2], "warmer: more red, less blue {warm:?}");
assert!(cool[2] > cool[1] && cool[1] > cool[0], "cooler: more blue, less red {cool:?}");
let edge = adjust(0.0, 0.0, 1.0, 0.0).apply(Color::rgb(1.0, 0.0, 0.0));
assert_eq!(edge, Color::rgb(1.0, 0.0, 0.0), "clamped at the ends of the channels");
}
#[test]
fn global_adjust_luminosity_keeps_the_hue() {
let lab = Color::lab(50.0, 30.0, -20.0);
assert_eq!(adjust(0.0, 0.0, 0.0, 0.5).apply(lab), Color::lab(75.0, 30.0, -20.0), "a Lab color keeps a and b exactly");
assert_eq!(adjust(0.0, 0.0, 0.0, -0.5).apply(lab), Color::lab(25.0, 30.0, -20.0));
let c = Color::rgb(0.6, 0.35, 0.3);
let (before, after) = (c.to_lab(), adjust(0.0, 0.0, 0.0, 0.2).apply(c).to_lab());
assert!(after.l > before.l, "lighter: {after:?}");
assert!((after.hue() - before.hue()).abs() < 2.0, "hue kept: {} vs {}", after.hue(), before.hue());
let darkest = adjust(0.0, 0.0, 0.0, -1.0).apply(c).to_lab();
assert!(darkest.l < before.l * 0.5, "-100% is far darker: {darkest:?}");
assert_eq!(adjust(0.0, 0.0, 0.0, -1.0).apply(Color::gray(0.4)), Color::gray(1.0), "-100% makes a gray black");
}
#[test]
fn global_adjust_keeps_the_model_and_the_ranges() {
let all = adjust(1.0, 1.0, 1.0, 1.0);
for c in [Color::rgb(0.3, 0.6, 0.9), Color::cmyk(0.2, 0.9, 0.1, 0.3), Color::gray(0.4), Color::lab(55.0, 60.0, -40.0)] {
let out = all.apply(c);
assert_eq!(out.model(), c.model(), "{c:?} → {out:?}");
let comps: Vec<f32> = match out {
Color::Rgb { r, g, b } => vec![r, g, b],
Color::Cmyk { c, m, y, k } => vec![c, m, y, k],
Color::Gray { k } => vec![k],
Color::Lab { l, .. } => vec![l / 100.0],
};
assert!(comps.iter().all(|v| v.is_finite() && (0.0..=1.0).contains(v)), "{out:?}");
}
}
#[test]
fn palette_snaps_to_the_nearest_colour() {
let p = Palette::new([Color::rgb(1.0, 0.0, 0.0), Color::cmyk(1.0, 0.0, 0.0, 0.0), Color::gray(0.5)]);
+52 -12
View File
@@ -7,7 +7,7 @@
use std::collections::{BTreeSet, HashMap};
use serde_json::{Value, json};
use vectorcraft_color::recolor::{ColorKey, Method, Preserve, Rng, RowMap, cluster};
use vectorcraft_color::recolor::{ColorKey, GlobalAdjust, Method, Preserve, Rng, RowMap, cluster};
use vectorcraft_color::{Color, Paint, Swatch};
use vectorcraft_doc::swatches::{node_colors, node_paints};
use vectorcraft_doc::{Document, Node, NodeId, NodeKind};
@@ -33,7 +33,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), tint?: 100 (percent; with limitTo, art linked to the global or spot swatch the new color is takes this share of its tint)}] | {\"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 (model?: \"keep\" default | \"new\" (the new colour's own model, so a CMYK colour replacing RGB art gives CMYK art) | \"document\" (the document's colour mode)), 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; art linked to a global swatch of `group` that is rewritten with its row's new color stays linked to it, at its tint, unless the row converts the color into the art's model; other recolored art, and art in a row linked to a global swatch of `group` that is rewritten with another color, keeps its row's new color and loses its swatch link) and the group, as one undo step → {changed, swatches: [names added to the document's swatches], relinked?: with group, the other paints linked to its global swatches that took their new colors, note?: set when a colour kept a model other than its new colour's (see model)}",
"{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), tint?: 100 (percent; with limitTo, art linked to the global or spot swatch the new color is takes this share of its tint)}] | {\"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, globalAdjust?: {saturation?: 0, brightness?: 0, temperature?: 0, luminosity?: 0} (Global Adjust: percent from -100 to 100, 0 changes nothing; every colour a row that isn't excluded recolours, and the group's new colours, take the same change after the row's new colour: saturation scales HSB saturation by 1 + saturation/100, brightness moves HSB brightness that share of the way to full or to black, temperature adds 0.2 × temperature/100 to red and takes it from blue, luminosity moves Lab lightness that share of the way to 100 or 0 keeping a and b; with limitTo each row's new colour takes the change before it snaps to the library)} recolour the selection (gradients, text, meshes, image pixels and pattern tiles included; each new colour keeps the model of the one it replaces (model?: \"keep\" default | \"new\" (the new colour's own model, so a CMYK colour replacing RGB art gives CMYK art) | \"document\" (the document's colour mode)), 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; art linked to a global swatch of `group` that is rewritten with its row's new color stays linked to it, at its tint, unless the row converts the color into the art's model; other recolored art, and art in a row linked to a global swatch of `group` that is rewritten with another color, keeps its row's new color and loses its swatch link) and the group, as one undo step → {changed, swatches: [names added to the document's swatches], relinked?: with group, the other paints linked to its global swatches that took their new colors, note?: set when a colour kept a model other than its new colour's (see model)}",
has_doc,
apply
),
@@ -224,6 +224,30 @@ fn reduce(s: &mut Session, p: &Value) -> Result<Value> {
Ok(json!({"map": map, "preserved": kept.iter().map(|u| key_str(&u.color)).collect::<Vec<_>>(), "method": method.id()}))
}
/// The `globalAdjust` parameter: `{saturation?, brightness?, temperature?, luminosity?}`, each a
/// percentage from -100 to 100 (0 when left out or null), as amounts from -1 to 1.
fn adjust_param(p: &Value, cmd: &str) -> Result<GlobalAdjust> {
let given = match p.get("globalAdjust") {
None | Some(Value::Null) => return Ok(GlobalAdjust::default()),
Some(v @ Value::Object(_)) => v,
Some(_) => return Err(bad(cmd, "`globalAdjust` must be an object")),
};
let amount = |key: &str| -> Result<f32> {
let x = super::number_or(given, key, 0.0, cmd)?;
let (min, max) = (-100, 100);
if !(f64::from(min)..=f64::from(max)).contains(&x) {
return Err(bad(cmd, format!("`{key}` must be between {min} and {max} (got {x})")));
}
Ok((x / 100.0) as f32)
};
Ok(GlobalAdjust {
saturation: amount("saturation")?,
brightness: amount("brightness")?,
temperature: amount("temperature")?,
luminosity: amount("luminosity")?,
})
}
/// A current colour of a row: an exact colour, or every colour shown as a hex.
enum Match {
Key(ColorKey),
@@ -370,15 +394,25 @@ struct ColorMap {
hexes: HashMap<String, usize>,
/// Set when a colour took a new colour of another model in its own model.
converted: std::cell::Cell<bool>,
/// The Global Adjust every mapped colour takes after its row's new colour.
adjust: GlobalAdjust,
}
impl ColorMap {
/// `links`: the swatch of each of `rows`' new colours (as many as `rows`, or none); `group`: the
/// global swatches of the color group being rewritten; `model`: the model rule of a row without
/// its own.
fn new(rows: &[Row], method: Method, model: ModelRule, links: &[Option<Link>], group: Vec<GroupSwatch>) -> Self {
let mut out =
Self { rows: vec![], links: vec![], tos: vec![], group, keys: HashMap::new(), hexes: HashMap::new(), converted: Default::default() };
/// its own; `adjust`: the Global Adjust of each mapped colour.
fn new(rows: &[Row], method: Method, model: ModelRule, links: &[Option<Link>], group: Vec<GroupSwatch>, adjust: GlobalAdjust) -> Self {
let mut out = Self {
rows: vec![],
links: vec![],
tos: vec![],
group,
keys: HashMap::new(),
hexes: HashMap::new(),
converted: Default::default(),
adjust,
};
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();
@@ -387,7 +421,8 @@ impl ColorMap {
// Colors that can't link to the swatch take the row's tint of it too.
let mapped = link.as_ref().filter(|l| l.global && l.tint < 1.0).map_or(to, |l| to.tinted(l.tint));
out.rows.push(RowMap::new(method, &colors, mapped).with_own_model(r.model.unwrap_or(model) != ModelRule::Keep));
out.tos.push(to);
// Compared with the group's new colours, which take the adjustment too.
out.tos.push(adjust.apply(to));
out.links.push(link);
for m in &r.from {
match m {
@@ -409,7 +444,7 @@ impl ColorMap {
if r.converts(c) {
self.converted.set(true);
}
r.map(c)
self.adjust.apply(r.map(c))
}
/// Recolour a colour that can link to a swatch: a row whose new colour is a global or spot
@@ -448,7 +483,7 @@ impl ColorMap {
if row.converts(*c) {
self.converted.set(true);
}
row.map(*c)
self.adjust.apply(row.map(*c))
}
};
// A color its row leaves as it is still loses its link to a swatch of the rewritten group
@@ -468,9 +503,13 @@ fn apply(s: &mut Session, p: &Value) -> Result<Value> {
let method = method_param(p, C, Method::Exact)?;
let palette = super::swatchlib::limit_param(s, p, C)?;
let mut rows = parse_rows(p, C)?;
let adjust = adjust_param(p, C)?;
// With Limit to Library a row's new colour takes the Global Adjust before it snaps, so the art
// still takes the library's colours; otherwise every colour the rows map takes it.
let mapped_adjust = if palette.is_some() { GlobalAdjust::default() } else { adjust };
if let Some(pl) = &palette {
for r in &mut rows {
r.to = r.to.map(|c| pl.nearest(c));
r.to = r.to.map(|c| pl.nearest(adjust.apply(c)));
}
}
// With Limit to Library, a row's new colour is a swatch of the library: the art takes that
@@ -510,11 +549,12 @@ fn apply(s: &mut Session, p: &Value) -> Result<Value> {
.iter()
.map(|v| {
color_value(v)
.map(|c| adjust.apply(c))
.map(|c| in_doc_model(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(),
None => rows.iter().filter_map(Row::new_color).map(|c| mapped_adjust.apply(c)).collect(),
};
let rename = text_param(p, "rename", C)?;
let colorized_ok = super::paint::check_colorized(&s.doc()?.doc).is_ok();
@@ -553,7 +593,7 @@ fn apply(s: &mut Session, p: &Value) -> Result<Value> {
.collect()
})
.unwrap_or_default();
let map = ColorMap::new(&rows, method, model, &links, kept);
let map = ColorMap::new(&rows, method, model, &links, kept, mapped_adjust);
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);
+115
View File
@@ -721,3 +721,118 @@ fn document_model_converts_the_colours_of_a_group_too() {
run(&mut s, "recolor.apply", json!({"map": [], "group": "Theme", "groupColors": ["#00ff00"], "model": "document"}));
assert!(group(&s).iter().all(|c| c.model() == vectorcraft_color::cms::Model::Cmyk), "{:?}", group(&s));
}
fn stroke(s: &Session, id: NodeId) -> Color {
s.doc().unwrap().doc.node(id).unwrap().appearance.stroke_paint().color().unwrap()
}
/// Global Adjust changes every colour the rows recolour, each one separately (the colours of a
/// row of several, fills and strokes alike), in its own model, as one undo step; an excluded
/// row's colours and preserved colours stay.
#[test]
fn global_adjust_changes_every_recolored_color() {
let mut s = session();
let red = rect(&mut s, json!("#ff0000"));
let pink = rect(&mut s, json!("#ff8080"));
let cmyk = rect(&mut s, json!({"c": 100, "m": 0, "y": 0, "k": 0}));
let kept = rect(&mut s, json!("#00ff00"));
run(&mut s, "paint.setStroke", json!({"ids": [red.0], "color": "#0000ff", "width": 2}));
select_all(&mut s);
let map = json!([
{"from": ["rgb 255 0 0", "rgb 255 128 128"], "to": "rgb 255 0 0"},
{"from": ["cmyk 100 0 0 0"], "to": "cmyk 100 0 0 0"},
{"from": ["rgb 0 0 255"], "to": "rgb 0 0 255"},
{"from": ["rgb 0 255 0"], "to": "rgb 0 255 0", "exclude": true},
]);
let undo = s.doc().unwrap().history.undo.len();
let sat = |c: Color| c.to_hsb()[1];
let saturations = [fill(&s, red), fill(&s, pink), fill(&s, cmyk), stroke(&s, red)].map(sat);
let r = run(&mut s, "recolor.apply", json!({"map": map, "method": "scaleTints", "globalAdjust": {"saturation": -50}}));
assert_eq!(r["changed"], 4, "three fills and a stroke: {r}");
assert_eq!(s.doc().unwrap().history.undo.len(), undo + 1, "one undo step");
let after = [fill(&s, red), fill(&s, pink), fill(&s, cmyk), stroke(&s, red)].map(sat);
for (n, what) in ["red", "pink", "cyan", "the stroke"].iter().enumerate() {
// CMYK reads back through the profile-free conversion, so its saturation is near half, not exact.
assert!((after[n] - saturations[n] * 0.5).abs() < 0.05, "{what} has about half its saturation: {} → {}", saturations[n], after[n]);
}
assert!(close(fill(&s, red), Color::rgb(1.0, 0.5, 0.5)), "{:?}", fill(&s, red));
assert!(close(fill(&s, pink), Color::rgb(1.0, 0.75, 0.75)), "each colour of a row changes on its own: {:?}", fill(&s, pink));
assert!(matches!(fill(&s, cmyk), Color::Cmyk { .. }), "a CMYK colour stays CMYK: {:?}", fill(&s, cmyk));
assert_eq!(fill(&s, kept).to_hex(), "#00ff00", "an excluded row keeps its colours");
run(&mut s, "edit.undo", json!({}));
assert_eq!(fill(&s, red).to_hex(), "#ff0000");
assert_eq!(fill(&s, pink).to_hex(), "#ff8080");
assert_eq!(stroke(&s, red).to_hex(), "#0000ff");
// Brightness -100 makes every recolored color black; the rows' new colors are taken first.
run(&mut s, "recolor.apply", json!({"map": [{"from": ["#ff0000"], "to": "#00ff00"}], "globalAdjust": {"brightness": -100}}));
assert_eq!(fill(&s, red).to_hex(), "#000000");
assert_eq!(fill(&s, pink).to_hex(), "#ff8080", "a colour no row holds stays");
}
/// Global Adjust at 0 changes nothing, a global swatch's link included, and a non-zero adjustment
/// gives the art the adjusted colour without the link.
#[test]
fn global_adjust_at_zero_keeps_colors_and_links() {
let mut s = session();
run(&mut s, "swatch.new", json!({"name": "Brand", "color": "#2878f0", "global": true}));
let art = rect(&mut s, json!("#000000"));
run(&mut s, "paint.setFill", json!({"ids": [art.0], "swatch": "Brand"}));
select_all(&mut s);
let link = |s: &Session| match s.doc().unwrap().doc.node(art).unwrap().appearance.fill_paint() {
vectorcraft_color::Paint::Solid { swatch, .. } => swatch,
p => panic!("a solid fill: {p:?}"),
};
let before = fill(&s, art);
let zero = json!({"saturation": 0, "brightness": 0, "temperature": 0, "luminosity": 0});
run(&mut s, "recolor.apply", json!({"map": {"rgb 40 120 240": "rgb 40 120 240"}, "globalAdjust": zero}));
assert_eq!(fill(&s, art), before);
assert_eq!(link(&s).as_deref(), Some("Brand"), "the link stays");
run(&mut s, "recolor.apply", json!({"map": {"rgb 40 120 240": "rgb 40 120 240"}, "globalAdjust": {"temperature": 50}}));
let [r, _, b] = fill(&s, art).to_rgb();
let [r0, _, b0] = before.to_rgb();
assert!(r > r0 && b < b0, "warmer: {:?}", fill(&s, art));
assert_eq!(link(&s), None, "the adjusted colour is no longer the swatch's");
}
/// With Limit to Library each row's new colour takes the Global Adjust and then snaps to the
/// library; a colour group rewritten with the rows' new colours takes the adjustment too.
#[test]
fn global_adjust_with_a_library_and_a_group() {
let mut s = session();
let art = rect(&mut s, json!("#f01020"));
select_all(&mut s);
run(&mut s, "recolor.apply", json!({"map": {"#f01020": "#f01020"}, "limitTo": "Web Safe 216", "globalAdjust": {"brightness": -100}}));
assert_eq!(fill(&s, art).to_hex(), "#000000", "black, a colour of the library");
run(&mut s, "edit.undo", json!({}));
run(&mut s, "swatch.newGroup", json!({"name": "Theme", "colors": ["#f01020"]}));
run(&mut s, "recolor.apply", json!({"map": [{"from": ["#f01020"], "to": "#f01020"}], "group": "Theme", "globalAdjust": {"saturation": -100}}));
let g = s.doc().unwrap().doc.swatch_groups.iter().find(|g| g.name == "Theme").unwrap().clone();
let c = g.swatches[0].paint.color().unwrap();
assert!(c.to_hsb()[1] < 1e-3, "the group's colour is adjusted: {c:?}");
assert_eq!(c, fill(&s, art), "as the art is");
}
/// `globalAdjust` must be an object of numbers from -100 to 100; a refusal changes nothing and
/// records no undo step. A null counts as left out.
#[test]
fn global_adjust_refuses_params_of_the_wrong_form() {
let mut s = session();
let art = rect(&mut s, json!("#ff0000"));
select_all(&mut s);
let undo = s.doc().unwrap().history.undo.len();
let map = json!({"#ff0000": "#ff0000"});
for (adjust, msg) in [
(json!(5), "`globalAdjust` must be an object"),
(json!({"saturation": "50"}), "`saturation` must be a number"),
(json!({"brightness": 101}), "`brightness` must be between -100 and 100 (got 101)"),
(json!({"luminosity": -100.5}), "`luminosity` must be between -100 and 100 (got -100.5)"),
] {
let e = s.execute("recolor.apply", &json!({"map": map, "globalAdjust": adjust})).unwrap_err().to_string();
assert!(e.contains(msg), "{adjust}: {e}");
}
assert_eq!(fill(&s, art).to_hex(), "#ff0000", "nothing changed");
assert_eq!(s.doc().unwrap().history.undo.len(), undo, "no undo step");
run(&mut s, "recolor.apply", json!({"map": map, "globalAdjust": null}));
run(&mut s, "recolor.apply", json!({"map": map, "globalAdjust": {"saturation": null, "temperature": -100}}));
assert_ne!(fill(&s, art).to_hex(), "#ff0000", "an amount at the end of the range applies");
}
+143 -24
View File
@@ -13,7 +13,9 @@
//! 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
//! scale together), a brightness slider, the harmony rules, and the color sliders with their mode
//! menu, whose Global Adjust shows Saturation, Brightness, Temperature and Luminosity (-100% to
//! 100%, 0% changes nothing), which change every new color at once. 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
//! the group with the new colours (renamed by `groupName`); without art only the group changes.
//! Opened on colours alone (the Color Guide's Edit or Apply Colors without art), OK saves the new
@@ -24,13 +26,15 @@
//! `preserveGrays`, `limitTo` (library id, "document" for the document's swatches, "" for none),
//! `group`, `groupName`, `tab` (`assign` or `edit`), `rule` (harmony id), `linked`, `preview`,
//! `sliderMode` (`hsb`, `rgb`, `cmyk`, `lab` or `tint`; unset: the row color's own model, HSB for
//! a gray and Tint for a spot color). Changing `colors`, a preserve flag or `limitTo` reduces the
//! rows again.
//! a gray and Tint for a spot color; `global`: Global Adjust, on the Edit tab), `globalAdjust`
//! (`{saturation, brightness, temperature, luminosity}` in percent, sent as `recolor.apply`'s
//! `globalAdjust` in every slider mode). Changing `colors`, a preserve flag or `limitTo` reduces
//! the rows again.
use egui::{Rect, Sense, Ui, pos2, vec2};
use serde_json::{Value, json};
use vectorcraft_color::harmony::{Harmony, move_on_wheel};
use vectorcraft_color::recolor::{ColorKey, Method};
use vectorcraft_color::recolor::{ColorKey, GlobalAdjust, Method};
use vectorcraft_color::{Color, keep_model};
use vectorcraft_engine::cmd::color_value;
use vectorcraft_engine::cmd::swatchlib;
@@ -70,9 +74,18 @@ const NEW_CHIP: &str = "recolor-new";
/// The color modes of the sliders under the rows; a row whose new color is a spot color of the
/// library also has Tint.
const SLIDER_MODES: [Mode; 4] = [Mode::Hsb, Mode::Rgb, Mode::Cmyk, Mode::Lab];
/// Widths of a color slider and of its number field.
/// Widths of a color slider's label, of the slider and of its number field.
const SLIDER_LABEL_W: f32 = 12.0;
const SLIDER_W: f32 = 170.0;
const SLIDER_FIELD_W: f32 = 48.0;
/// The `sliderMode` of the Edit tab's Global Adjust sliders, and the menu's name for it.
const GLOBAL: &str = "global";
const GLOBAL_ADJUST: &str = "Global Adjust";
/// The `globalAdjust` field's keys (and `recolor.apply`'s), in the order of the sliders.
const ADJUSTS: [&str; 4] = ["saturation", "brightness", "temperature", "luminosity"];
/// Widths of a Global Adjust slider's label and of the slider.
const GLOBAL_LABEL_W: f32 = 76.0;
const GLOBAL_SLIDER_W: f32 = 110.0;
/// The slider values last shown: `{row, key, mode, v}` (HSB keeps its hue at no saturation).
const COMPS: &str = "__comps";
@@ -260,6 +273,11 @@ fn apply_params(d: &Dialog) -> Value {
});
// A row edited with the sliders carries `model: "new"`: its art takes the mode they showed.
let mut p = json!({"map": rs, "method": d.str("method"), "limitTo": d.str("limitTo")});
// The Global Adjust amounts, kept while the sliders show another mode; at 0% they are left out.
let values = adjust_values(d);
if values.iter().any(|v| *v != 0.0) {
p["globalAdjust"] = Value::Object(ADJUSTS.iter().zip(values).map(|(k, v)| (k.to_string(), json!(v))).collect());
}
if let (Some(Value::Object(g)), Some(p)) = (group, p.as_object_mut()) {
p.extend(g.into_iter().filter(|(_, v)| !v.is_null()));
}
@@ -288,7 +306,7 @@ fn body(app: &mut VectorcraftApp, ui: &mut Ui, d: &mut Dialog) -> bool {
ui.add_space(4.0);
let mut rs = rows(d);
let top = ui.cursor().top();
let (changed, pick) = if d.str("tab") == "edit" { (edit_tab(ui, d, &mut rs), None) } else { assign_tab(app, ui, d, &mut rs) };
let (changed, pick) = if d.str("tab") == "edit" { (edit_tab(app, ui, d, &mut rs), None) } else { assign_tab(app, ui, d, &mut rs) };
if d.str("tab") != "edit" {
ui.add_space((TAB_H - (ui.cursor().top() - top)).max(0.0));
}
@@ -335,7 +353,13 @@ fn confirm(app: &mut VectorcraftApp, d: &Dialog) -> Result<Value, String> {
let mut d = d.clone();
sync(app, &mut d);
if d.bool("__newGroup") {
let colors: Vec<Value> = rows(&d).into_iter().filter(|r| !excluded(r)).map(|r| r["to"].clone()).collect();
// The new colors with the Global Adjust, as `recolor.apply` gives them to a group.
let adjust = global_adjust(adjust_values(&d));
let colors: Vec<Value> = rows(&d)
.into_iter()
.filter(|r| !excluded(r))
.map(|r| to_color(&r).filter(|_| !adjust.is_identity()).map_or_else(|| r["to"].clone(), |c| key(&adjust.apply(c))))
.collect();
return form::commit_preview(app, "swatch.newGroup", json!({"name": d.str("groupName"), "colors": colors, "keepModel": true}));
}
let r = form::commit_preview(app, CMD, apply_params(&d))?;
@@ -529,7 +553,7 @@ fn assign_tab(app: &mut VectorcraftApp, ui: &mut Ui, d: &mut Dialog, rows: &mut
}
}
let target = SliderTarget::of(app, d, rows);
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| mode_menu(ui, d, &target));
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| mode_menu(ui, d, &target, false));
});
ui.add_space(6.0);
changed |= sliders(app, ui, d, rows);
@@ -686,30 +710,84 @@ impl SliderTarget {
}
}
/// The color mode menu of the sliders (HSB, RGB, CMYK, Lab, and Tint for a spot color), drawn
/// right to left: the menu, then its label.
fn mode_menu(ui: &mut Ui, d: &mut Dialog, target: &SliderTarget) {
/// The color mode menu of the sliders (HSB, RGB, CMYK, Lab, Tint for a spot color, and with
/// `global` Global Adjust), drawn right to left: the menu, then its label.
fn mode_menu(ui: &mut Ui, d: &mut Dialog, target: &SliderTarget, global: bool) {
let t = Tokens::get(ui.ctx());
let mut labels: Vec<&str> = SLIDER_MODES.iter().map(|m| m.label()).collect();
if target.spot.is_some() {
labels.push("Tint");
}
let current = target.mode(d).map_or("Tint", Mode::label);
if global {
labels.push(GLOBAL_ADJUST);
}
let adjusting = global && d.str("sliderMode") == GLOBAL;
let current = if adjusting { GLOBAL_ADJUST } else { target.mode(d).map_or("Tint", Mode::label) };
// The label is drawn after the menu (right-to-left), so it cannot name it by adjacency.
let chosen = ui
.add_enabled_ui(target.color.is_some(), |ui| {
widgets::dropdown_named(ui, "recolor-slider-mode", Some(tl!("Color Mode:")), current, &labels, 90.0)
.add_enabled_ui(target.color.is_some() || global, |ui| {
widgets::dropdown_named(ui, "recolor-slider-mode", Some(tl!("Color Mode:")), current, &labels, if global { 110.0 } else { 90.0 })
})
.inner;
if let Some(i) = chosen {
d.fields.insert("sliderMode".into(), json!(SLIDER_MODES.get(i).map_or_else(|| "tint".to_string(), |m| mode_id(*m))));
let id = match SLIDER_MODES.get(i) {
Some(m) => mode_id(*m),
None if labels.get(i) == Some(&GLOBAL_ADJUST) => GLOBAL.to_string(),
None => "tint".to_string(),
};
d.fields.insert("sliderMode".into(), json!(id));
}
ui.label(egui::RichText::new(tl!("Color Mode:")).color(t.text_dim));
}
/// One color slider in the grid: its label, its rail with `track` and its number field, showing
/// `shown` with `suffix`. Returns the whole number in `shown`'s range that a drag, a key or the
/// field sets.
/// The Global Adjust amounts, in percent from -100 to 100, of `ADJUSTS`' keys in the
/// `globalAdjust` field (0 for one it lacks).
fn adjust_values(d: &Dialog) -> [f64; 4] {
let given = d.fields.get("globalAdjust");
ADJUSTS.map(|k| given.and_then(|g| g.get(k)).and_then(Value::as_f64).unwrap_or(0.0).clamp(-100.0, 100.0))
}
/// The Global Adjust of `values` (percent, in the order of `ADJUSTS`).
fn global_adjust(values: [f64; 4]) -> GlobalAdjust {
let [saturation, brightness, temperature, luminosity] = values.map(|v| (v / 100.0) as f32);
GlobalAdjust { saturation, brightness, temperature, luminosity }
}
/// The Global Adjust sliders, two to a line: Saturation, Brightness, Temperature and Luminosity,
/// each from -100% to 100% (0% changes nothing). They change every new color at once, so they
/// edit no row; each rail shows its amounts on `sample`.
fn global_sliders(ui: &mut Ui, d: &mut Dialog, sample: Color) {
let labels = [tl!("Saturation"), tl!("Brightness"), tl!("Temperature"), tl!("Luminosity")];
let values = adjust_values(d);
let mut set: Option<(usize, f64)> = None;
egui::Grid::new("recolor-global-adjust").num_columns(6).spacing([6.0, 4.0]).show(ui, |ui| {
for (n, lbl) in labels.iter().enumerate() {
let shown = widgets::SliderValue::field(values[n], -100.0..=100.0, "%", 0);
// The rail: `sample` with this slider's amount alone.
let track = |x: f32| {
let mut one = [0.0; 4];
one[n] = f64::from(x) * 200.0 - 100.0;
c32(&global_adjust(one).apply(sample))
};
if let Some(v) = slider_cell(ui, ("global", n), lbl, shown, "%", &track, GLOBAL_LABEL_W, GLOBAL_SLIDER_W) {
set = Some((n, v));
}
if n % 2 == 1 {
ui.end_row();
}
}
});
if let Some((n, v)) = set {
let mut values = values;
values[n] = v;
d.fields.insert("globalAdjust".into(), Value::Object(ADJUSTS.iter().zip(values).map(|(k, v)| (k.to_string(), json!(v))).collect()));
}
}
/// One color slider in the grid: its label `label_w` wide, its rail with `track` `slider_w` wide
/// and its number field, showing `shown` with `suffix`. Returns the whole number in `shown`'s
/// range that a drag, a key or the field sets.
#[allow(clippy::too_many_arguments)]
fn slider_cell(
ui: &mut Ui,
id: impl std::hash::Hash + Copy,
@@ -717,13 +795,15 @@ fn slider_cell(
shown: widgets::SliderValue,
suffix: &str,
track: &dyn Fn(f32) -> egui::Color32,
label_w: f32,
slider_w: f32,
) -> Option<f64> {
let t = Tokens::get(ui.ctx());
let (lo, hi) = (*shown.range.start(), *shown.range.end());
let value = shown.value;
ui.add_sized(vec2(12.0, 22.0), egui::Label::new(egui::RichText::new(label).size(12.5).color(t.text)));
ui.add_sized(vec2(label_w, 22.0), egui::Label::new(egui::RichText::new(label).size(12.5).color(t.text)));
let unit = ((value - lo) / (hi - lo)) as f32;
let (dragged, _, keyed) = widgets::color_slider_stepped(ui, ("recolor-slider", id), label, Some(shown), unit, SLIDER_W, track);
let (dragged, _, keyed) = widgets::color_slider_stepped(ui, ("recolor-slider", id), label, Some(shown), unit, slider_w, track);
let typed = widgets::plain_field_named(ui, ("recolor-slider-field", id), Some(label), value, suffix, 0, SLIDER_FIELD_W);
dragged.map(|x| (lo + f64::from(x) * (hi - lo)).round()).or(keyed).or(typed).map(|v| v.clamp(lo, hi)).filter(|v| (v - value).abs() > 1e-6)
}
@@ -746,7 +826,7 @@ fn sliders(app: &VectorcraftApp, ui: &mut Ui, d: &mut Dialog, rows: &mut [Value]
let gray = move |_: f32| input;
for (n, lbl) in mode.labels().iter().enumerate() {
let shown = widgets::SliderValue::field(0.0, f64::from(mode.min(n))..=f64::from(mode.max(n)), mode.suffix(n), 0);
slider_cell(ui, n, lbl, shown, mode.suffix(n), &gray);
slider_cell(ui, n, lbl, shown, mode.suffix(n), &gray, SLIDER_LABEL_W, SLIDER_W);
if n % 2 == 1 {
ui.end_row();
}
@@ -757,7 +837,7 @@ fn sliders(app: &VectorcraftApp, ui: &mut Ui, d: &mut Dialog, rows: &mut [Value]
// Tint of the spot color, and its name.
let tint = f64::from(row_tint(&rows[i])) * 100.0;
let shown = widgets::SliderValue::field(tint, 0.0..=100.0, "%", 0);
if let Some(v) = slider_cell(ui, usize::MAX, "T", shown, "%", &|x| c32(&spot_tinted(app, c, x))) {
if let Some(v) = slider_cell(ui, usize::MAX, "T", shown, "%", &|x| c32(&spot_tinted(app, c, x)), SLIDER_LABEL_W, SLIDER_W) {
rows[i]["tint"] = json!(v);
changed = true;
}
@@ -769,7 +849,8 @@ fn sliders(app: &VectorcraftApp, ui: &mut Ui, d: &mut Dialog, rows: &mut [Value]
for (n, lbl) in mode.labels().iter().enumerate() {
let v = comps.get(n).copied().unwrap_or(0.0);
let shown = widgets::SliderValue::field(f64::from(v), f64::from(mode.min(n))..=f64::from(mode.max(n)), mode.suffix(n), 0);
if let Some(nv) = slider_cell(ui, n, lbl, shown, mode.suffix(n), &|x| c32(&track_color(mode, &comps, n, x))) {
if let Some(nv) = slider_cell(ui, n, lbl, shown, mode.suffix(n), &|x| c32(&track_color(mode, &comps, n, x)), SLIDER_LABEL_W, SLIDER_W)
{
let mut c2 = comps.clone();
c2[n] = nv as f32;
set = Some(c2);
@@ -845,13 +926,14 @@ fn move_color(rows: &mut [Value], i: usize, hsb: [f32; 3], linked: bool) {
const DRAG: &str = "__drag";
/// The Edit tab. Returns true when the rows changed.
fn edit_tab(ui: &mut Ui, d: &mut Dialog, rows: &mut [Value]) -> bool {
fn edit_tab(app: &VectorcraftApp, ui: &mut Ui, d: &mut Dialog, rows: &mut [Value]) -> bool {
let top = ui.cursor().top();
let t = Tokens::get(ui.ctx());
let mut changed = false;
let linked = d.bool("linked");
let active = active_rows(rows);
let base = selection(d).into_iter().find(|i| active.contains(i)).or(active.first().copied());
let target = SliderTarget::of(app, d, rows);
ui.horizontal(|ui| {
ui.label(egui::RichText::new(tl!("Harmony Rules:")).color(t.text_dim));
let rule = Harmony::parse(&d.str("rule")).unwrap_or(Harmony::Complementary);
@@ -870,6 +952,10 @@ fn edit_tab(ui: &mut Ui, d: &mut Dialog, rows: &mut [Value]) -> bool {
if widgets::icon_button(ui, if linked { "link" } else { "link-2-off" }, tip, linked, 24.0).clicked() {
d.fields.insert("linked".into(), json!(!linked));
}
ui.add_space(8.0);
// The mode of the color sliders under the wheel: the selected new color in a color
// mode, or Global Adjust for all of them.
mode_menu(ui, d, &target, true);
});
});
ui.add_space(8.0);
@@ -899,6 +985,12 @@ fn edit_tab(ui: &mut Ui, d: &mut Dialog, rows: &mut [Value]) -> bool {
d.fields.remove(DRAG);
}
ui.add_space(6.0);
if d.str("sliderMode") == GLOBAL {
let sample = base.and_then(|b| to_color(&rows[b])).unwrap_or(Color::rgb(0.8, 0.45, 0.25));
global_sliders(ui, d, sample);
} else {
changed |= sliders(app, ui, d, rows);
}
// The new colours: click one to make it the base colour.
let min_height = (TAB_H - (ui.cursor().top() - top)).max(0.0);
super::modal::scrolled_rows(ui, "recolor-edit-colors", |ui| {
@@ -957,6 +1049,33 @@ mod tests {
assert!(apply_params(d).get("model").is_none());
}
/// `recolor.apply` gets the Global Adjust amounts only when one of them is not 0%, whatever
/// mode the sliders show.
#[test]
fn apply_params_send_global_adjust_only_when_it_changes_colors() {
let mut app = VectorcraftApp::new(vectorcraft_engine::Session::new(), Default::default());
app.run("file.new", json!({"width": 100, "height": 100})).unwrap();
app.run("ui.recolorDialog", json!({"colors": ["#ff8000"]})).unwrap();
let d = app.ui.dialog.as_mut().unwrap();
assert!(apply_params(d).get("globalAdjust").is_none(), "no field");
d.fields.insert("globalAdjust".into(), json!({"saturation": 0, "brightness": 0}));
assert!(apply_params(d).get("globalAdjust").is_none(), "all 0%");
d.fields.insert("globalAdjust".into(), json!({"temperature": 25, "luminosity": 500}));
d.fields.insert("sliderMode".into(), json!("hsb"));
assert_eq!(
apply_params(d)["globalAdjust"],
json!({"saturation": 0.0, "brightness": 0.0, "temperature": 25.0, "luminosity": 100.0}),
"every amount, clamped to 100%"
);
// Colors opened alone become a new color group with the adjustment.
d.fields.insert("globalAdjust".into(), json!({"saturation": -100}));
let d = d.clone();
confirm(&mut app, &d).unwrap();
let g = app.session.doc().unwrap().doc.swatch_groups.last().expect("a new color group");
let c = g.swatches[0].paint.color().unwrap();
assert!(c.to_hsb()[1] < 1e-3, "{c:?}");
}
#[test]
fn merge_and_separate_rows() {
let mut r = rows();
+1
View File
@@ -1244,6 +1244,7 @@ font-technology Variable Променливи
Get Preferences Извличане на предпочитания
Get Text Range Вземане на текстов диапазон
Global Глобален
Global Adjust Глобална корекция
Global Brush Dimensions Глобални размери на четката
Global Process Color Глобален процесен цвят
Global Process Color, CMYK Глобален процесен цвят, CMYK
Can't render this file because it contains an unexpected character in line 2 and column 73.
+2
View File
@@ -2138,6 +2138,7 @@ digits Malayalam Malajalam
Color Mode: Način boja:
Convert Pretvori
Global Globalno
Global Adjust Globalno podešavanje
Green Zelena
Shifts the tints of global and spot colors; other colors stay. Pomjera nijanse globalnih i spot boja; ostale boje ostaju.
Tint Preliv
@@ -4260,6 +4261,7 @@ imageTrace Outline Obris
@msg `library` is a library id or name (see {list}) `library` je ID ili naziv biblioteke (vidi {list})
@msg `mode` applies to freeform gradients `mode` se primjenjuje na gradijente slobodnog oblika
@msg `preserve` must be an object `preserve` mora biti objekat
@msg `globalAdjust` must be an object `globalAdjust` mora biti objekat
@msg `sampleSize` must be 1, 3 or 5 `sampleSize` mora biti 1, 3 ili 5
@msg `stops` must be an array `stops` mora biti niz
@msg `svg` must be an object of SVG options (see document.formats) `svg` mora biti objekat SVG opcija (vidi document.formats)
Can't render this file because it contains an unexpected character in line 2 and column 73.
+2
View File
@@ -1699,6 +1699,7 @@ digits Malayalam Malaiàlam
Glass Vidre
Glass… Vidre…
Global Global
Global Adjust Ajust global
Global Brush Dimensions Dimensions globals del pinzell
Global Edit Options Opcions globals d’edició
Global Process Color Color de procés global
@@ -4776,6 +4777,7 @@ digits Malayalam Malaiàlam
@msg `newName` is empty `newName` és buit
@msg `pathfinder` must be an object `pathfinder` ha de ser un objecte
@msg `preserve` must be an object `preserve` ha de ser un objecte
@msg `globalAdjust` must be an object `globalAdjust` ha de ser un objecte
@msg `sampleSize` must be 1, 3 or 5 `sampleSize` ha de ser 1, 3 o 5
@msg `select` must be true or false `select` ha de ser cert o fals
@msg `stops` must be an array `stops` ha de ser una matriu
Can't render this file because it contains an unexpected character in line 2 and column 73.
+2
View File
@@ -2166,6 +2166,7 @@ digits Malayalam Malajálamské
Color Mode: Barevný režim:
Convert Převést
Global Globální
Global Adjust Globální úprava
Green Zelená
Shifts the tints of global and spot colors; other colors stay. Posune tóny globálních a přímých barev; ostatní barvy zůstanou.
Tint Tón
@@ -4299,6 +4300,7 @@ imageTrace Outline Obrys
@msg `library` is a library id or name (see {list}) `library` je ID nebo název knihovny (viz {list})
@msg `mode` applies to freeform gradients `mode` platí pro volné přechody
@msg `preserve` must be an object `preserve` musí být objekt
@msg `globalAdjust` must be an object `globalAdjust` musí být objekt
@msg `sampleSize` must be 1, 3 or 5 `sampleSize` musí být 1, 3 nebo 5
@msg `stops` must be an array `stops` musí být pole
@msg `svg` must be an object of SVG options (see document.formats) `svg` musí být objekt s možnostmi SVG (viz document.formats)
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -1969,6 +1969,7 @@ digits Malayalam Malayalam
Color Mode: Farvetilstand:
Convert Konvertér
Global Global
Global Adjust Global justering
Green Grøn
Shifts the tints of global and spot colors; other colors stay. Flytter nuancerne af globale farver og spotfarver; andre farver forbliver uændrede.
Tint Nuance
Can't render this file because it contains an unexpected character in line 2 and column 73.
+2
View File
@@ -1268,6 +1268,7 @@ file-filter Templates Vorlagen
Get Preferences Voreinstellungen abrufen
Get Text Range Textbereich abrufen
Global Global
Global Adjust Globale Anpassung
Global Brush Dimensions Globale Pinselabmessungen
Global Process Color Globale Prozessfarbe
Global Process Color, CMYK Globale Prozessfarbe, CMYK
@@ -3906,6 +3907,7 @@ theme Appearance Erscheinungsbild
@msg `library` is a library id or name (see {list}) `library` ist die ID oder der Name einer Bibliothek (siehe {list})
@msg `mode` applies to freeform gradients `mode` gilt für Freiformverläufe
@msg `preserve` must be an object `preserve` muss ein Objekt sein
@msg `globalAdjust` must be an object `globalAdjust` muss ein Objekt sein
@msg `sampleSize` must be 1, 3 or 5 `sampleSize` muss 1, 3 oder 5 sein
@msg `stops` must be an array `stops` muss ein Array sein
@msg `svg` must be an object of SVG options (see document.formats) `svg` muss ein Objekt mit SVG-Optionen sein (siehe document.formats)
Can't render this file because it contains an unexpected character in line 2 and column 73.
+2
View File
@@ -1208,6 +1208,7 @@ font-technology Variable Μεταβλητές
Get Preferences Λήψη προτιμήσεων
Get Text Range Λήψη περιοχής κειμένου
Global Καθολικό
Global Adjust Καθολική ρύθμιση
Global Brush Dimensions Καθολικές διαστάσεις πινέλου
Global Process Color Καθολικό χρώμα τετραχρωμίας
Global Process Color, CMYK Καθολικό χρώμα τετραχρωμίας, CMYK
@@ -2208,6 +2209,7 @@ axis Both Και τα δύο
@msg `library` is a library id or name (see {list}) Το `library` είναι αναγνωριστικό ή όνομα βιβλιοθήκης (δείτε {list})
@msg `mode` applies to freeform gradients Το `mode` ισχύει για ντεγκραντέ ελεύθερης μορφής
@msg `preserve` must be an object Το `preserve` πρέπει να είναι αντικείμενο
@msg `globalAdjust` must be an object Το `globalAdjust` πρέπει να είναι αντικείμενο
@msg `sampleSize` must be 1, 3 or 5 Το `sampleSize` πρέπει να είναι 1, 3 ή 5
@msg `stops` must be an array Το `stops` πρέπει να είναι πίνακας
@msg `svg` must be an object of SVG options (see document.formats) Το `svg` πρέπει να είναι αντικείμενο επιλογών SVG (δείτε document.formats)
Can't render this file because it contains an unexpected character in line 164 and column 5.
+2
View File
@@ -1287,6 +1287,7 @@ font-technology Variable Variables
Get Preferences Obtener preferencias
Get Text Range Obtener rango de texto
Global Global
Global Adjust Ajuste global
Global Brush Dimensions Dimensiones globales de pincel
Global Process Color Color de cuatricromía global
Global Process Color, CMYK Color de cuatricromía global, CMYK
@@ -3992,6 +3993,7 @@ axis Both Ambos
@msg `library` is a library id or name (see {list}) `library` es el id o el nombre de una biblioteca (consulta {list})
@msg `mode` applies to freeform gradients `mode` se aplica a los degradados de forma libre
@msg `preserve` must be an object `preserve` debe ser un objeto
@msg `globalAdjust` must be an object `globalAdjust` debe ser un objeto
@msg `sampleSize` must be 1, 3 or 5 `sampleSize` debe ser 1, 3 o 5
@msg `stops` must be an array `stops` debe ser una lista
@msg `svg` must be an object of SVG options (see document.formats) `svg` debe ser un objeto con opciones de SVG (consulta document.formats)
Can't render this file because it contains an unexpected character in line 2 and column 73.
+2
View File
@@ -2125,6 +2125,7 @@ digits Malayalam Malajalam
Color Mode: Väritila:
Convert Muunna
Global Globaali
Global Adjust Yleissäätö
Green Vihreä
Shifts the tints of global and spot colors; other colors stay. Siirtää globaalien värien ja spottivärien sävyjä; muut värit säilyvät.
Tint Sävy
@@ -4213,6 +4214,7 @@ imageTrace Outline Ääriviiva
@msg `library` is a library id or name (see {list}) `library` on kirjaston tunnus tai nimi (ks. {list})
@msg `mode` applies to freeform gradients `mode` koskee vapaamuotoisia liukuvärejä
@msg `preserve` must be an object `preserve` on oltava objekti
@msg `globalAdjust` must be an object `globalAdjust` on oltava objekti
@msg `sampleSize` must be 1, 3 or 5 `sampleSize` on oltava 1, 3 tai 5
@msg `stops` must be an array `stops` on oltava taulukko
@msg `svg` must be an object of SVG options (see document.formats) `svg` on oltava SVG-asetusten objekti (ks. document.formats)
Can't render this file because it contains an unexpected character in line 2 and column 73.
+2
View File
@@ -1281,6 +1281,7 @@ font-technology Variable Variables
Get Preferences Obtenir les préférences
Get Text Range Obtenir une plage de texte
Global Globale
Global Adjust Réglage global
Global Brush Dimensions Dimensions globales des formes
Global Process Color Couleur quadrichromique globale
Global Process Color, CMYK Couleur quadrichromique globale, CMJN
@@ -3982,6 +3983,7 @@ axis Both Les deux
@msg `library` is a library id or name (see {list}) `library` est l’identifiant ou le nom d’une bibliothèque (voir {list})
@msg `mode` applies to freeform gradients `mode` s’applique aux dégradés de forme libre
@msg `preserve` must be an object `preserve` doit être un objet
@msg `globalAdjust` must be an object `globalAdjust` doit être un objet
@msg `sampleSize` must be 1, 3 or 5 `sampleSize` doit valoir 1, 3 ou 5
@msg `stops` must be an array `stops` doit être un tableau
@msg `svg` must be an object of SVG options (see document.formats) `svg` doit être un objet d’options SVG (voir document.formats)
Can't render this file because it contains an unexpected character in line 2 and column 73.
+2
View File
@@ -1602,6 +1602,7 @@ file-filter Templates Sablonok
Glass Üveg
Glass… Üveg…
Global Globális
Global Adjust Globális beállítás
Global Brush Dimensions Globális ecsetméretek
Global Process Color Globális skálaszín
Global Process Color, CMYK Globális skálaszín, CMYK
@@ -4521,6 +4522,7 @@ imageTrace Outline Körvonal
@msg `library` is a library id or name (see {list}) a `library` könyvtárazonosító vagy -név lehet (lásd: {list})
@msg `mode` applies to freeform gradients a `mode` a szabad formájú színátmenetekre vonatkozik
@msg `preserve` must be an object `preserve`: objektum kell
@msg `globalAdjust` must be an object `globalAdjust`: objektum kell
@msg `sampleSize` must be 1, 3 or 5 `sampleSize`: 1, 3 vagy 5 kell
@msg `stops` must be an array `stops`: tömb kell
@msg `svg` must be an object of SVG options (see document.formats) `svg`: SVG-beállításokat tartalmazó objektum kell (lásd: document.formats)
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -1969,6 +1969,7 @@ digits Malayalam Malayalam
Color Mode: Mode warna:
Convert Konversi
Global Global
Global Adjust Penyesuaian global
Green Hijau
Shifts the tints of global and spot colors; other colors stay. Menggeser tint warna global dan warna spot; warna lainnya tetap.
Tint Tint
1 # Indonesian (id) catalog. Same format as zh-hant.tsv: context <TAB> source <TAB> translation
1969
1970
1971
1972
1973
1974
1975
+2
View File
@@ -1298,6 +1298,7 @@ file-filter Templates Modelli
Get Preferences Ottieni preferenze
Get Text Range Ottieni intervallo di testo
Global Globale
Global Adjust Regolazione globale
Global Brush Dimensions Dimensioni pennello globali
Global Process Color Colore quadricromia globale
Global Process Color, CMYK Colore quadricromia globale, CMYK
@@ -4033,6 +4034,7 @@ axis Both Entrambi
@msg `library` is a library id or name (see {list}) `library` è l'id o il nome di una libreria (vedi {list})
@msg `mode` applies to freeform gradients `mode` si applica alle sfumature a forma libera
@msg `preserve` must be an object `preserve` deve essere un oggetto
@msg `globalAdjust` must be an object `globalAdjust` deve essere un oggetto
@msg `sampleSize` must be 1, 3 or 5 `sampleSize` deve essere 1, 3 o 5
@msg `stops` must be an array `stops` deve essere un array
@msg `svg` must be an object of SVG options (see document.formats) `svg` deve essere un oggetto di opzioni SVG (vedi document.formats)
Can't render this file because it contains an unexpected character in line 2 and column 73.
+2
View File
@@ -1662,6 +1662,7 @@ digits Malayalam マラヤーラム数字
Get Preferences 環境設定を取得
Get Text Range テキスト範囲を取得
Global グローバル
Global Adjust グローバル調整
Global Brush Dimensions ブラシサイズ (共通)
Global Process Color グローバルプロセスカラー
Go To Link リンク元へ移動
@@ -4149,6 +4150,7 @@ swatch Bright Violet ビビッドバイオレット
@msg `groupColors` must be an array `groupColors` には配列を指定してください
@msg `mode` applies to freeform gradients `mode` はフリーフォームグラデーションにのみ適用されます
@msg `preserve` must be an object `preserve` にはオブジェクトを指定してください
@msg `globalAdjust` must be an object `globalAdjust` にはオブジェクトを指定してください
@msg `stops` must be an array `stops` には配列を指定してください
@msg `{k}` must be [x, y] `{k}` は [x, y] の形式で指定してください
@msg a gradient needs at least two stops グラデーションには 2 つ以上の分岐点が必要です
Can't render this file because it contains an unexpected character in line 2 and column 73.
+2
View File
@@ -1870,6 +1870,7 @@ digits Malayalam მალაიალამური
Color Mode: ფერის რეჟიმი:
Convert გარდაქმნა
Global გლობალური
Global Adjust გლობალური კორექცია
Green მწვანე
Shifts the tints of global and spot colors; other colors stay. ცვლის გლობალური და სპოტ-ფერების ტონირებებს; სხვა ფერები უცვლელი რჩება.
Tint ტონირება
@@ -3778,6 +3779,7 @@ font-technology Variable ცვლადი
@msg `library` is a library id or name (see {list}) `library` არის ბიბლიოთეკის id ან სახელი (იხ. {list})
@msg `mode` applies to freeform gradients `mode` ეხება თავისუფალი ფორმის გრადიენტებს
@msg `preserve` must be an object `preserve` უნდა იყოს ობიექტი
@msg `globalAdjust` must be an object `globalAdjust` უნდა იყოს ობიექტი
@msg `sampleSize` must be 1, 3 or 5 `sampleSize` უნდა იყოს 1, 3 ან 5
@msg `stops` must be an array `stops` უნდა იყოს მასივი
@msg `svg` must be an object of SVG options (see document.formats) `svg` უნდა იყოს SVG-ის პარამეტრების ობიექტი (იხ. document.formats)
Can't render this file because it is too large.
+2
View File
@@ -1652,6 +1652,7 @@ digits Malayalam 말라얄람어
Get Preferences 환경 설정 가져오기
Get Text Range 텍스트 범위 가져오기
Global 전역
Global Adjust 전체 조정
Global Brush Dimensions 전역 브러시 치수
Global Process Color 전역 프로세스 색상
Go To Link 링크로 이동
@@ -4076,6 +4077,7 @@ theme Appearance 외관
@msg `groupColors` must be an array `groupColors`는 배열이어야 합니다
@msg `mode` applies to freeform gradients `mode`는 자유형 그레이디언트에 적용됩니다
@msg `preserve` must be an object `preserve`는 개체여야 합니다
@msg `globalAdjust` must be an object `globalAdjust`는 개체여야 합니다
@msg `stops` must be an array `stops`는 배열이어야 합니다
@msg `{k}` must be [x, y] `{k}`은(는) [x, y]여야 합니다
@msg a gradient needs at least two stops 그레이디언트에는 정지점이 두 개 이상 필요합니다
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -1983,6 +1983,7 @@ digits Malayalam Malayalam
Color Mode: Kleurmodus:
Convert Converteren
Global Globaal
Global Adjust Globale aanpassing
Green Groen
Shifts the tints of global and spot colors; other colors stay. Verschuift de tinten van globale kleuren en spotkleuren; andere kleuren blijven ongewijzigd.
Tint Tint
Can't render this file because it contains an unexpected character in line 2 and column 73.
+2
View File
@@ -2083,6 +2083,7 @@ digits Malayalam Malajalam
Color Mode: Tryb koloru:
Convert Konwertuj
Global Globalny
Global Adjust Dopasowanie globalne
Green Zielony
Shifts the tints of global and spot colors; other colors stay. Przesuwa tinty kolorów globalnych i dodatkowych; pozostałe kolory bez zmian.
Tint Tinta
@@ -4122,6 +4123,7 @@ imageTrace Outline Kontur
@msg `library` is a library id or name (see {list}) `library` to identyfikator lub nazwa biblioteki (patrz {list})
@msg `mode` applies to freeform gradients `mode` dotyczy gradientów dowolnych
@msg `preserve` must be an object `preserve` musi być obiektem
@msg `globalAdjust` must be an object `globalAdjust` musi być obiektem
@msg `sampleSize` must be 1, 3 or 5 `sampleSize` musi wynosić 1, 3 lub 5
@msg `stops` must be an array `stops` musi być tablicą
@msg `svg` must be an object of SVG options (see document.formats) `svg` musi być obiektem opcji SVG (patrz document.formats)
Can't render this file because it contains an unexpected character in line 2 and column 73.
+2
View File
@@ -1134,6 +1134,7 @@
Get Preferences Obter Preferências
Get Text Range Obter Intervalo de Texto
Global Global
Global Adjust Ajuste Global
Global Brush Dimensions Dimensões Globais do Pincel
Global Process Color Cor Global de Processo
Glyphs Glifos
@@ -3574,6 +3575,7 @@ axis Both Ambos
@msg `library` is a library id or name (see {list}) `library` é o id ou o nome de uma biblioteca (veja {list})
@msg `mode` applies to freeform gradients `mode` se aplica a gradientes de forma livre
@msg `preserve` must be an object `preserve` deve ser um objeto
@msg `globalAdjust` must be an object `globalAdjust` deve ser um objeto
@msg `sampleSize` must be 1, 3 or 5 `sampleSize` deve ser 1, 3 ou 5
@msg `stops` must be an array `stops` deve ser uma matriz
@msg `svg` must be an object of SVG options (see document.formats) `svg` deve ser um objeto com opções de SVG (veja document.formats)
Can't render this file because it contains an unexpected character in line 6 and column 47.
+2
View File
@@ -1295,6 +1295,7 @@ file-filter Templates Шаблоны
Get Preferences Получить настройки
Get Text Range Получить диапазон текста
Global Глобальный
Global Adjust Глобальная настройка
Global Brush Dimensions Глобальные размеры кисти
Global Process Color Глобальный составной цвет
Global Process Color, CMYK Глобальный составной цвет, CMYK
@@ -4028,6 +4029,7 @@ axis Both Оба
@msg `library` is a library id or name (see {list}) `library` — это идентификатор или имя библиотеки (см. {list})
@msg `mode` applies to freeform gradients `mode` применяется к произвольным градиентам
@msg `preserve` must be an object `preserve` должно быть объектом
@msg `globalAdjust` must be an object `globalAdjust` должно быть объектом
@msg `sampleSize` must be 1, 3 or 5 `sampleSize` должно быть 1, 3 или 5
@msg `stops` must be an array `stops` должно быть массивом
@msg `svg` must be an object of SVG options (see document.formats) `svg` должно быть объектом параметров SVG (см. document.formats)
Can't render this file because it contains an unexpected character in line 2 and column 73.
+2
View File
@@ -1246,6 +1246,7 @@ font-technology Variable Variabla
Get Preferences Hämta inställningar
Get Text Range Hämta textintervall
Global Global
Global Adjust Global justering
Global Brush Dimensions Globala penseldimensioner
Global Process Color Global processfärg
Global Process Color, CMYK Global processfärg, CMYK
@@ -3903,6 +3904,7 @@ axis Both Båda
@msg `library` is a library id or name (see {list}) `library` är ett biblioteks-id eller biblioteksnamn (se {list})
@msg `mode` applies to freeform gradients `mode` gäller för fria gradienter
@msg `preserve` must be an object `preserve` måste vara ett objekt
@msg `globalAdjust` must be an object `globalAdjust` måste vara ett objekt
@msg `sampleSize` must be 1, 3 or 5 `sampleSize` måste vara 1, 3 eller 5
@msg `stops` must be an array `stops` måste vara en array
@msg `svg` must be an object of SVG options (see document.formats) `svg` måste vara ett objekt med SVG-alternativ (se document.formats)
Can't render this file because it contains an unexpected character in line 5 and column 25.
+2
View File
@@ -1224,6 +1224,7 @@ font-technology Variable Değişken
Get Preferences Tercihleri Al
Get Text Range Metin Aralığını Al
Global Küresel
Global Adjust Genel Ayarlama
Global Brush Dimensions Küresel Fırça Boyutları
Global Process Color Global Proses Rengi
Global Process Color, CMYK Küresel Proses Rengi, CMYK
@@ -3786,6 +3787,7 @@ axis Both Her İkisi
@msg `library` is a library id or name (see {list}) 'kitaplık' bir kitaplık kimliği veya adıdır (bkz. {list})
@msg `mode` applies to freeform gradients "mod" serbest biçimli degradeler için geçerlidir
@msg `preserve` must be an object `preserve` bir nesne olmalıdır
@msg `globalAdjust` must be an object `globalAdjust` bir nesne olmalıdır
@msg `sampleSize` must be 1, 3 or 5 "sampleSize" 1, 3 veya 5 olmalıdır
@msg `stops` must be an array "duraklar" bir dizi olmalıdır
@msg `svg` must be an object of SVG options (see document.formats) `svg`, SVG seçeneklerinin bir nesnesi olmalıdır (bkz. document.formats)
Can't render this file because it contains an unexpected character in line 5 and column 25.
+2
View File
@@ -1292,6 +1292,7 @@ file-filter Templates Шаблони
Get Preferences Отримати налаштування
Get Text Range Отримати діапазон тексту
Global Глобальний
Global Adjust Глобальне налаштування
Global Brush Dimensions Глобальні розміри пензля
Global Process Color Глобальний складовий колір
Global Process Color, CMYK Глобальний складовий колір, CMYK
@@ -4017,6 +4018,7 @@ axis Both Обидві
@msg `library` is a library id or name (see {list}) `library` — це ідентифікатор або ім’я бібліотеки (див. {list})
@msg `mode` applies to freeform gradients `mode` застосовується до довільних градієнтів
@msg `preserve` must be an object `preserve` має бути об’єктом
@msg `globalAdjust` must be an object `globalAdjust` має бути об’єктом
@msg `sampleSize` must be 1, 3 or 5 `sampleSize` має бути 1, 3 або 5
@msg `stops` must be an array `stops` має бути масивом
@msg `svg` must be an object of SVG options (see document.formats) `svg` має бути об’єктом параметрів SVG (див. document.formats)
Can't render this file because it contains an unexpected character in line 178 and column 5.
+2
View File
@@ -1503,6 +1503,7 @@ file-filter Templates Mẫu
Glass Kính
Glass… Kính…
Global Toàn cục
Global Adjust Điều chỉnh toàn cục
Global Brush Dimensions Kích thước cọ toàn cục
Global Process Color Màu xử lý toàn cục
Global Process Color, CMYK Màu xử lý toàn cục, CMYK
@@ -4173,6 +4174,7 @@ theme Appearance Giao diện
@msg `library` is a library id or name (see {list}) `library` là id hoặc tên thư viện (xem {list})
@msg `mode` applies to freeform gradients `mode` áp dụng cho chuyển sắc tự do
@msg `preserve` must be an object `preserve` phải là một đối tượng
@msg `globalAdjust` must be an object `globalAdjust` phải là một đối tượng
@msg `sampleSize` must be 1, 3 or 5 `sampleSize` phải là 1, 3 hoặc 5
@msg `stops` must be an array `stops` phải là một mảng
@msg `svg` must be an object of SVG options (see document.formats) `svg` phải là đối tượng tùy chọn SVG (xem document.formats)
Can't render this file because it contains an unexpected character in line 200 and column 5.
+2
View File
@@ -1236,6 +1236,7 @@ file-filter Templates 模板
Get Preferences 获取首选项
Get Text Range 获取文本范围
Global 全局
Global Adjust 全局调整
Global Brush Dimensions 全局画笔尺寸
Global Process Color 全局印刷色
Global Process Color, CMYK 全局印刷色,CMYK
@@ -3878,6 +3879,7 @@ imageTrace Outline 轮廓
@msg `groupColors` must be an array `groupColors` 必须是数组
@msg `mode` applies to freeform gradients `mode` 仅适用于任意形状渐变
@msg `preserve` must be an object `preserve` 必须是对象
@msg `globalAdjust` must be an object `globalAdjust` 必须是对象
@msg `stops` must be an array `stops` 必须是数组
@msg `{k}` must be [x, y] `{k}` 必须是 [x, y]
@msg a gradient needs at least two stops 渐变至少需要两个色标
Can't render this file because it contains an unexpected character in line 2 and column 73.
+2
View File
@@ -1262,6 +1262,7 @@ file-filter Templates 範本
Get Preferences 取得偏好設定
Get Text Range 取得文字範圍
Global 全域
Global Adjust 整體調整
Global Brush Dimensions 整體筆刷尺寸
Global Process Color 整體印刷色
Global Process Color, CMYK 整體印刷色,CMYK
@@ -1801,6 +1802,7 @@ file-filter Templates 範本
@msg `groupColors` must be an array `groupColors` 必須是陣列
@msg `mode` applies to freeform gradients `mode` 只適用於任意形狀漸層
@msg `preserve` must be an object `preserve` 必須是物件
@msg `globalAdjust` must be an object `globalAdjust` 必須是物件
@msg `stops` must be an array `stops` 必須是陣列
@msg `{k}` must be [x, y] `{k}` 必須是 [x, y]
@msg a gradient needs at least two stops 漸層至少需要兩個色標
Can't render this file because it contains an unexpected character in line 6 and column 47.
+66 -1
View File
@@ -572,11 +572,76 @@ fn switching_tabs_or_slider_modes_keeps_the_dialog_height() {
h.ctx.memory(|m| m.area_rect(egui::Id::unique(("dialog", "recolor")))).unwrap().height()
};
let assign = height(&mut h, "sliderMode", json!("cmyk"));
for (k, v) in [("sliderMode", json!("hsb")), ("sliderMode", json!("lab")), ("tab", json!("edit")), ("tab", json!("assign"))] {
for (k, v) in [
("sliderMode", json!("hsb")),
("sliderMode", json!("lab")),
("tab", json!("edit")),
("sliderMode", json!("global")),
("sliderMode", json!("rgb")),
("tab", json!("assign")),
] {
assert_eq!(height(&mut h, k, v.clone()), assign, "{k} {v}");
}
}
/// The fills of the document's objects, in order.
fn fills(app: &VectorcraftApp) -> Vec<Option<Color>> {
let d = &app.session.doc().unwrap().doc;
d.layers[0].children().unwrap().iter().map(|n| d.node(n.id).unwrap().appearance.fill_paint().color()).collect()
}
/// The Edit tab's color mode menu offers Global Adjust: Saturation, Brightness, Temperature and
/// Luminosity open at 0%, which changes nothing; one slider changes every recolored color at once
/// (the rows keep their new colors) and the art previews it; back at 0% the art has its own colors
/// again; the amounts stay while the tab or the mode changes; OK keeps the change as one undo step
/// and Cancel leaves the art as it was.
#[test]
fn global_adjust_changes_every_color_at_once() {
for confirm in [true, false] {
let mut h = edit_harness(&["#ff0000", "#0000ff", "#00ff00"]);
let before = fills(h.state());
let undo = h.state().session.doc().unwrap().history.undo.len();
h.get_by_role_and_label(egui::accesskit::Role::ComboBox, "Color Mode:").click();
h.run_steps(2);
h.get_by_label("Global Adjust").click();
h.run_steps(3);
assert_eq!(field(h.state(), "sliderMode"), json!("global"));
for name in ["Saturation", "Temperature", "Luminosity"] {
assert!(h.query_by_role_and_label(egui::accesskit::Role::Slider, name).is_some(), "{name}");
}
// Global Adjust's Brightness and the wheel's.
assert_eq!(h.get_all_by_role_and_label(egui::accesskit::Role::Slider, "Brightness").count(), 2);
assert_eq!(fills(h.state()), before, "at 0% the art keeps its colors");
let rows = field(h.state(), "rows");
click_slider(&mut h, "Saturation", 0.0);
assert_eq!(field(h.state(), "globalAdjust")["saturation"], json!(-100.0));
assert_eq!(field(h.state(), "rows"), rows, "the rows keep their new colors");
let gray = |app: &VectorcraftApp| fills(app).iter().all(|c| c.is_some_and(|c| c.to_hsb()[1] < 1e-3));
assert!(gray(h.state()), "every color previews gray: {:?}", fills(h.state()));
click_slider(&mut h, "Saturation", 0.5);
assert_eq!(field(h.state(), "globalAdjust")["saturation"], json!(0.0));
assert_eq!(fills(h.state()), before, "back at 0% the art has its own colors");
click_slider(&mut h, "Saturation", 0.0);
for (k, v) in [("tab", json!("assign")), ("tab", json!("edit")), ("sliderMode", json!("hsb")), ("sliderMode", json!("global"))] {
set(h.state_mut(), k, v);
h.run_steps(3);
}
assert_eq!(field(h.state(), "globalAdjust")["saturation"], json!(-100.0), "the amount stays");
assert!(gray(h.state()), "and still applies");
h.get_by_label(if confirm { "OK" } else { "Cancel" }).click();
h.run_steps(2);
assert!(h.state().ui.dialog.is_none());
let app = h.state();
if confirm {
assert!(gray(app), "OK keeps the change: {:?}", fills(app));
assert_eq!(app.session.doc().unwrap().history.undo.len(), undo + 1, "one undo step");
} else {
assert_eq!(fills(app), before, "Cancel leaves the art unchanged");
assert_eq!(app.session.doc().unwrap().history.undo.len(), undo, "and records no undo step");
}
}
}
/// The Assign tab's per-row hex fields are named by their row, not all "Color": a screen reader can
/// tell the second from the first.
#[test]
+6 -1
View File
@@ -259,7 +259,12 @@ as `recolor.reduce` returns them), `colors` (null for Auto), `method`, `preserve
`sliderMode` (the mode of the color sliders under the rows: `hsb`, `rgb`, `cmyk`, `lab`, or `tint` for a row whose new
color is a spot color of the library; unset, they show the new color's own model). The sliders edit the first selected
row's new color and give that row `model: "new"`, so the preview and confirm give only that row's art the color in
the sliders' mode; another edit of the row's new color, Reset and rows reduced again remove it. Setting
the sliders' mode; another edit of the row's new color, Reset and rows reduced again remove it. The Edit tab shows the
same sliders under the wheel, and its mode menu also has Global Adjust (`sliderMode: "global"`): Saturation,
Brightness, Temperature and Luminosity sliders that set `globalAdjust` (`{saturation, brightness, temperature,
luminosity}`, percent from -100 to 100, 0 by default). The dialog sends a non-zero `globalAdjust` as `recolor.apply`'s
`globalAdjust` in every slider mode, so one slider changes every recolored color at once and the rows keep their new
colors; colors opened alone are saved as a group with the change. 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,
+17 -2
View File
@@ -1929,7 +1929,7 @@ the objects `select.all` takes, which leaves out hidden and locked art, and the
and locked objects inside a group are left out too, also in isolation mode.
`recolor.reduce {colors?, method?, preserve?, limitTo?, ids?}` groups those 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?, model?, limitTo?, group?, groupColors?, rename?}`.
`[{from: [keys], to: key}]` to edit and pass to `recolor.apply {map, method?, model?, limitTo?, group?, groupColors?, rename?, globalAdjust?}`.
For scripts and MCP, `model` selects the replacement color model: `keep` (default) retains each
original color's model, `new` takes the replacement's model, and `document` converts replacements
and edited color groups to the document's model. A row's own `model` (the same values) takes the
@@ -1953,11 +1953,26 @@ is linked to a swatch rewritten with another color keeps its row's new color, ev
linked to the swatch, as does art whose color the row converts into the art's own color model. Without `group`,
the swatch keeps its color and the recolored art is no longer linked to it.
`globalAdjust: {saturation?, brightness?, temperature?, luminosity?}` is the dialog's Global Adjust: each amount is a
percentage from -100 to 100 (0 when left out), and all four at 0 change nothing. Every color that a row which is not
excluded recolors takes the same change after its row's new color, in this order: saturation multiplies HSB
saturation by 1 + saturation/100; brightness moves HSB brightness that share of the way to full brightness (above 0)
or to black (below 0); temperature adds 0.2 × temperature/100 to red and subtracts it from blue; luminosity moves Lab
lightness that share of the way to 100 or to 0 and keeps Lab a and b, so the hue stays. Each color keeps its own
model, and a changed color is no longer linked to its swatch. The colors a `group` is rewritten with take the same
change. With `limitTo`, each row's new color takes the change before it snaps to the library, so the art still takes
the library's colors. Colors that no row holds, such as preserved White and Black, and excluded rows stay as they are.
```json
{"name":"run_command","arguments":{"command":"recolor.apply","params":{"map":{"#ff0000":"#ff0000","#0000ff":"#0000ff"},"globalAdjust":{"saturation":-40,"temperature":25}}}}
```
The `recolor.*` commands refuse parameters of the wrong form. A `limitTo` or `method` that is not a string gets the
error for an unknown library or method. Other wrong-form parameters get an error that names them.
`includeImages`, `includePatterns`, a row's `exclude`, `order`, `saturationBrightness` and the `white`, `black` and
`grays` switches of `preserve` must be true or false; `colors` and `seed` must be whole numbers; `group` and `rename`
must be strings, `groupColors` must be an array and `preserve` must be an object. Each refusal leaves the document unchanged and records no undo step. A `null`
must be strings, `groupColors` must be an array, `preserve` must be an object, and `globalAdjust` must be an object
whose amounts are numbers from -100 to 100. Each refusal leaves the document unchanged and records no undo step. A `null`
counts as a value left out except for `recolor.apply`'s `model` and a `map` row's `model`, which must be strings when given.
```json
+1 -1
View File
@@ -123,7 +123,7 @@ dominate; 3D, graphs and automation are specialist). They sum to 105. Feature de
| Selection, transform & align tools | 6 | 85% | Direct Selection segment click/Delete corrected (#1278); open-path anchor Delete keeps gaps; Select › Start Global Edit finds the paths, compound paths and symbol instances of the selected object's shape (Appearance, Size and artboard options) and gives an edit of one to all of them in proportion, as one undo step (#3521), and on a group the groups like it, an edit inside one reaching the same object in the others (#4939); the Align panel menu's Align to Glyph Bounds › Point Text and Area Text measure type by its glyphs' ink for Align and Distribute (#4231); remaining: Free Transform/Scale/Reflect along a rotated box, Global Edit of rotated copies and excluding a match with Shift-click, transform nuances (no-op anchor Align history regression-tested, #1180 follow-up) | 4–6 | estimated; Global Edit regression-tested (`crates/engine/src/tests/globaledit.rs`; Quick Action, menu label and Escape in `panels::properties::tests::start_global_edit_quick_action_and_escape`) |
| Drawing tools (Pen, Curvature, Pencil, shapes, Shaper) | 7 | 85% | Shaper construction widget, Pencil/Pen nuances, Blob Brush options, Touch Type, Dimension tool; see [ui-parity.md](ui-parity.md) | 8–13 | estimated; Curvature point display regression-tested (#1236, in part); Polygon/Star exact-size count persistence regression-covered; Live Corners' Relative rounding done (#3091), score unchanged |
| Path operations, Pathfinder, Shape Builder, Live Paint | 5 | 80% | Shape Builder on the reporter's file (#937: art past 4,096 segments is cut by lines again; unconfirmed on the file itself); gap detection shares Live Paint’s finder, with a 72 pt custom cap and the existing options; Live Paint Gap Options | 4–8 | estimated; Shape Builder Gap Detection shares Live Paint’s spatial gap finder and temporary merge bridges (#2526); Custom is capped at 72 pt in the existing options dialog (lowered from 86%: open issues); compound attributes regression-tested (#1197); single-result source names survive SVG export/import (#1293), with [SVG ID semantics](https://www.w3.org/TR/SVG2/struct.html#IDAttribute) and original-shape tests; Simplify component selection and preview regression-tested (#1272); Simplify of a Direct Selection part of a path regression-tested (`tests/simplify_part.rs`); Divide open-cutting-path geometry and appearance regression-tested (#1307); the Shape Builder's edge-source grid is bounded in memory and files a segment once per cell (`planar.rs` test `rect_index_holds_a_bounded_number_of_references`); live compound shapes (Make, Alt-click, member Shape Modes, Release, Expand, saved and exported) regression-tested (#4522) |
| Colour, swatches, gradients, patterns, mesh, recolor | 7 | 92% | 30.0's gradient additions are done (dither, perceptual blending, presets); freeform gradients in SVG/PDF, mixed spot/process gradients in SVG, and PDF gradients mixing a spot color with RGB or Lab stops | 4–7 | estimated; Recolor model selection is available through the engine and MCP (#2520), and the dialog's color sliders (HSB, RGB, CMYK, Lab, Tint for a spot color) edit the selected row's new color in its mode (lowered from 96%: target moved); unpainted zero-width stroke restoration fixed (#1253), score and hours unchanged; Eyedropper source reads regression-tested (#1251); screen-bounded Recolor Edit chip list (#3416), score and hours unchanged |
| Colour, swatches, gradients, patterns, mesh, recolor | 7 | 92% | 30.0's gradient additions are done (dither, perceptual blending, presets); freeform gradients in SVG/PDF, mixed spot/process gradients in SVG, and PDF gradients mixing a spot color with RGB or Lab stops | 4–7 | estimated; Recolor model selection is available through the engine and MCP (#2520), and the dialog's color sliders (HSB, RGB, CMYK, Lab, Tint for a spot color) edit the selected row's new color in its mode, and the Edit tab's Global Adjust sliders (Saturation, Brightness, Temperature, Luminosity, -100% to 100%) change every recolored color at once (lowered from 96%: target moved); unpainted zero-width stroke restoration fixed (#1253), score and hours unchanged; Eyedropper source reads regression-tested (#1251); screen-bounded Recolor Edit chip list (#3416), score and hours unchanged |
| Strokes, brushes, width profiles | 5 | 88% | Shift-double-click adjoining adjustment is implemented; brush libraries are done (four generated libraries, User Defined, Other Library…, Save Brushes); remaining: Art brush Overlap, Pattern brush auto corners, pen pressure on macOS, tilt/bearing/rotation | 8–15 | estimated; width-point creation and adjoining adjustment regression-tested; dialog layout reviewed; evidence below; finite-arithmetic follow-up regressions and full CI passed (evidence below) |
| Appearance, transparency, graphic styles, masks | 5 | 97% | raster effect reach in preview bounds | 1–2 | estimated |
| 3D and Materials (and Turntable) | 4 | ~25% | live Revolve with automatic smooth vector shading and default-on continuous circular caps, Extrude & Bevel (twist, taper, caps, five bevel shapes), Inflate and Rotate with Position presets (colored Inflate uses smooth membrane lighting; white-light Inflate retains its existing shading; Extrude & Bevel and Rotate retain planar shading; one to ten colored lights, shared neutral ambient, painter visibility); materials, light presets, shadows, ray tracing, 3D Classic, Turntable views, 3D export | 30–55 | estimated (Extrude & Bevel, Inflate and Rotate added 2026-10-12) |