diff --git a/crates/engine/src/cmd/colorcmds.rs b/crates/engine/src/cmd/colorcmds.rs index 221521efb..1d009a6b0 100644 --- a/crates/engine/src/cmd/colorcmds.rs +++ b/crates/engine/src/cmd/colorcmds.rs @@ -30,7 +30,7 @@ pub fn specs() -> Vec { "Invert Colors", ["Edit", "Edit Colors"], None, - "{fill?: true, stroke?: true, includeImages?: true, includePatterns?: true, ids?} replace each colour by its RGB inverse (keeps the colour model) in fills, strokes, text, gradient meshes (with fills), colorized image fills (their pixels stay unchanged), other embedded images (a recoloured copy) and pattern tiles (a new pattern swatch; the original stays), as one undo step → {changed}", + "{fill?: true, stroke?: true, includeImages?: true, includePatterns?: true, ids?} replace each colour by its RGB inverse (keeps the colour model) in fills, strokes, text, gradient meshes (with fills), colorized image fills (their pixels stay unchanged), other embedded images (a recoloured copy) and pattern tiles (a new pattern swatch; the original stays), as one undo step → {changed, notChanged: {locked, hidden} (when the selection is everything select.all takes: the locked and hidden objects it left out whose colors would have changed; an object both hidden and locked counts as hidden), warning?: set when notChanged counts any object}", has_selection, |s, p| recolor(s, p, "edit.colors.invert", "Invert Colors", &invert) ), @@ -39,7 +39,7 @@ pub fn specs() -> Vec { "Convert to CMYK", ["Edit", "Edit Colors"], None, - "{fill?, stroke?, includeImages?, includePatterns?, ids?} (as edit.colors.invert) → {changed}", + "{fill?, stroke?, includeImages?, includePatterns?, ids?} (as edit.colors.invert) → {changed, notChanged, warning?}", has_selection, |s, p| recolor(s, p, "edit.colors.toCMYK", "Convert to CMYK", &to_cmyk) ), @@ -48,7 +48,7 @@ pub fn specs() -> Vec { "Convert to Grayscale", ["Edit", "Edit Colors"], None, - "{fill?, stroke?, includeImages?, includePatterns?, ids?} (as edit.colors.invert) → {changed}", + "{fill?, stroke?, includeImages?, includePatterns?, ids?} (as edit.colors.invert) → {changed, notChanged, warning?}", has_selection, |s, p| recolor(s, p, "edit.colors.toGrayscale", "Convert to Grayscale", &to_gray) ), @@ -57,7 +57,7 @@ pub fn specs() -> Vec { "Convert to RGB", ["Edit", "Edit Colors"], None, - "{fill?, stroke?, includeImages?, includePatterns?, ids?} (as edit.colors.invert) → {changed}", + "{fill?, stroke?, includeImages?, includePatterns?, ids?} (as edit.colors.invert) → {changed, notChanged, warning?}", has_selection, |s, p| { recolor(s, p, "edit.colors.toRGB", "Convert to RGB", &|c| { @@ -71,7 +71,7 @@ pub fn specs() -> Vec { "Saturate…", ["Edit", "Edit Colors"], None, - "{intensity: -100..100 (%; a number outside the range counts as its nearest end), fill?, stroke?, includeImages?, includePatterns?, ids?} scale saturation by (1 + intensity/100), reaching what edit.colors.invert does → {changed}", + "{intensity: -100..100 (%; a number outside the range counts as its nearest end), fill?, stroke?, includeImages?, includePatterns?, ids?} scale saturation by (1 + intensity/100), reaching what edit.colors.invert does → {changed, notChanged, warning?} (as edit.colors.invert)", has_selection, saturate ), @@ -80,7 +80,7 @@ pub fn specs() -> Vec { "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? | 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}", + "{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, notChanged, warning?} (as edit.colors.invert)", has_selection, adjust_balance ), @@ -213,6 +213,48 @@ impl<'a> Recolor<'a> { Self { links: Some(links), ..Self::new(scope, f) } } + /// A pass that maps colors as this one does, with nothing recolored yet. + fn again(&self) -> Self { + Self { links: self.links, ..Self::new(self.scope, self.f) } + } + + /// The entries of `objects` (ids, each with a flag) whose object this pass would change, other + /// than by swatch links alone, its patterns' tiles included. The document stays as it is. + pub(crate) fn would_change(mut self, d: &Document, objects: &[(NodeId, T)]) -> Vec<(NodeId, T)> { + let tried: Vec<(&Node, Node, T)> = + objects.iter().filter_map(|&(id, flag)| d.node(id).and_then(|n| Some((n, self.subtree(d, n)?, flag)))).collect(); + let same = self.link_only_copies(d); + tried.into_iter().filter_map(|(n, m, flag)| (without_links(m, &same) != without_links(n.clone(), &same)).then_some((n.id, flag))).collect() + } + + /// The patterns this pass has copied whose copy differs from the original only by swatch + /// links: the copy's name → the original's. A copy whose tile uses other copies counts when + /// those copies do. + fn link_only_copies(&self, d: &Document) -> HashMap { + let art = |v: &[Arc], same: &HashMap| v.iter().map(|n| without_links((**n).clone(), same)).collect::>(); + let copies: HashMap<&str, &PatternDef> = self.new_patterns.iter().map(|p| (p.name.as_str(), p)).collect(); + let pairs: Vec<(&String, &String, &PatternDef, &PatternDef)> = self + .patterns + .iter() + .filter_map(|(old, new)| { + let new = new.as_ref()?; + Some((new, old, d.pattern(old)?, *copies.get(new.as_str())?)) + }) + .collect(); + let mut same = HashMap::new(); + loop { + let found: Vec<(String, String)> = pairs + .iter() + .filter(|(new, _, original, copy)| !same.contains_key(*new) && art(&original.art, &same) == art(©.art, &same)) + .map(|(new, old, _, _)| ((*new).clone(), (*old).clone())) + .collect(); + if found.is_empty() { + return same; + } + same.extend(found); + } + } + /// Recolour the objects `ids` and everything inside them (each once), add the new images and /// pattern swatches, and return the number of paints, meshes and images changed. pub(crate) fn run(mut self, d: &mut Document, ids: &[NodeId]) -> usize { @@ -313,6 +355,29 @@ impl<'a> Recolor<'a> { } } +/// `n` with the swatch links of its paints and its children's removed, each color kept as it +/// shows, and each pattern named in `same` (a copy → its original) by its original's name. +fn without_links(mut n: Node, same: &HashMap) -> Node { + for p in vectorcraft_doc::swatches::node_paints_mut(&mut n) { + map_links(p, &|_, link, _| link.take().is_some()); + match p { + Paint::Gradient(g) => g.swatch = None, + Paint::Pattern { pattern, .. } => { + if let Some(original) = same.get(pattern.as_str()) { + *pattern = original.clone(); + } + } + _ => {} + } + } + if let Some(children) = n.children_mut() { + for c in children.iter_mut() { + *c = Arc::new(without_links((**c).clone(), same)); + } + } + n +} + /// What a pass over document `d` does to each colour the shared visitor meets. struct Visit<'p, 'a> { pass: &'p mut Recolor<'a>, @@ -342,19 +407,107 @@ pub(crate) fn recolor_links(s: &mut Session, p: &Value, cmd: &str, label: &str, } /// Run `pass` on the `ids` param's objects (default: the selection) as one undo step named `label`; -/// errors name the command `cmd`. +/// errors name the command `cmd`. The reply's `notChanged` counts the locked and hidden objects the +/// selection left out ([`left_out`]) that the pass would have changed. fn run_pass(s: &mut Session, p: &Value, cmd: &str, label: &str, pass: Recolor) -> Result { - let ids = match ids_param(p, "ids", cmd)? { - Some(v) => v, - None => selected_roots(s)?, + let (ids, left) = match ids_param(p, "ids", cmd)? { + Some(v) => (v, vec![]), + None => { + let roots = selected_roots(s)?; + let st = s.doc()?; + let left = left_out(&st.doc, st.isolation, &roots); + (roots, left) + } }; let colorized_ok = super::paint::check_colorized(&s.doc()?.doc).is_ok(); - let n = s.edit(label, |d, _| { + let (n, not_changed) = s.edit(label, |d, _| { + let not_changed = NotChanged::of(pass.again().would_change(d, &left).into_iter().map(|(_, hidden)| hidden)); let n = pass.run(d, &ids); super::paint::check_colorized_kept(colorized_ok, d).map_err(|e| bad(cmd, e))?; - Ok(n) + Ok((n, not_changed)) })?; - Ok(json!({ "changed": n })) + let mut out = json!({ "changed": n }); + not_changed.report(&mut out); + Ok(out) +} + +/// The objects a recolor of `roots` leaves out of the art the user meant to recolor, each with +/// whether it is hidden (otherwise it is locked). When `roots` are exactly the objects Select All +/// takes (in isolation mode, those of the isolated group), these are the objects beside them that +/// Select All skips: hidden or locked objects, the objects of hidden or locked layers and +/// sublayers, and, outside isolation mode, objects on locked artboards (counted as locked). The art +/// of template layers and the objects inside a group are not listed. With any other `roots`, none. +pub(crate) fn left_out(d: &Document, iso: Option, roots: &[NodeId]) -> Vec<(NodeId, bool)> { + fn visit(n: &Node, hidden: bool, locked: bool, all: &HashSet, board_locked: &dyn Fn(&Node) -> bool, out: &mut Vec<(NodeId, bool)>) { + for c in n.children().into_iter().flatten() { + if c.is_template() { + continue; + } + let (hidden, locked) = (hidden || !c.visible, locked || c.locked); + if c.is_layer() { + visit(c, hidden, locked, all, board_locked, out); + } else if !all.contains(&c.id) && (hidden || locked || board_locked(c)) { + out.push((c.id, hidden)); + } + } + } + let all: HashSet = super::select::selectable(d, iso).into_iter().collect(); + let picked: HashSet = roots.iter().copied().collect(); + if picked.is_empty() || picked != all { + return vec![]; + } + let mut out = vec![]; + match iso.and_then(|i| d.node(i)) { + Some(c) => visit(c, false, false, &all, &|_| false, &mut out), + None => { + let boards = d.locked_artboards(); + let board_locked = |n: &Node| !boards.is_empty() && boards.holds(n); + for l in d.layers.iter().filter(|l| !l.is_template()) { + visit(l, !l.visible, l.locked, &all, &board_locked, &mut out); + } + } + } + out +} + +/// How many locked and hidden objects a recolor left out ([`left_out`]) although it would have +/// changed their colors. An object both hidden and locked counts as hidden. +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +pub(crate) struct NotChanged { + pub locked: usize, + pub hidden: usize, +} + +impl NotChanged { + /// The counts of objects given by whether each is hidden. + pub(crate) fn of(hidden: impl IntoIterator) -> Self { + hidden.into_iter().fold(Self::default(), |n, h| if h { Self { hidden: n.hidden + 1, ..n } } else { Self { locked: n.locked + 1, ..n } }) + } + + /// Add `notChanged` to reply `out`, and a `warning`, which the app shows in its status bar, + /// when any object is counted. + pub(crate) fn report(self, out: &mut Value) { + out["notChanged"] = json!({ "locked": self.locked, "hidden": self.hidden }); + if let Some(w) = self.warning() { + out["warning"] = json!(w); + } + } + + fn warning(self) -> Option { + let part = |n: usize, kind: &str| match n { + 0 => None, + 1 => Some(format!("1 {kind} object")), + n => Some(format!("{n} {kind} objects")), + }; + match (part(self.locked, "locked"), part(self.hidden, "hidden")) { + (Some(locked), Some(hidden)) => Some(format!("Recolor completed; {locked} and {hidden} were left unchanged")), + (Some(objects), None) | (None, Some(objects)) if self.locked + self.hidden == 1 => { + Some(format!("Recolor completed; {objects} was left unchanged")) + } + (Some(objects), None) | (None, Some(objects)) => Some(format!("Recolor completed; {objects} were left unchanged")), + (None, None) => None, + } + } } fn saturate(s: &mut Session, p: &Value) -> Result { diff --git a/crates/engine/src/cmd/recolor.rs b/crates/engine/src/cmd/recolor.rs index 00b87c649..6dcdc9dc4 100644 --- a/crates/engine/src/cmd/recolor.rs +++ b/crates/engine/src/cmd/recolor.rs @@ -12,7 +12,7 @@ use vectorcraft_color::{Color, Paint, Swatch}; use vectorcraft_doc::swatches::{node_colors, node_paints, override_paints}; use vectorcraft_doc::{Document, Node, NodeId, NodeKind}; -use super::colorcmds::{Recolor, Scope}; +use super::colorcmds::{NotChanged, Recolor, Scope, left_out}; use super::{ CommandSpec, Result, Session, always, bad, check_switches, checked_ids_param, cmd, color_value, has_doc, index, str_param, switch_or, text_param, }; @@ -33,7 +33,7 @@ pub fn specs() -> Vec { "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 (in from, to and groupColors, #rrggbbaa and #rgba are refused), 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)}", + "{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 (in from, to and groupColors, #rrggbbaa and #rgba are refused), 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), notChanged: {locked, hidden} (as edit.colors.invert), warning?: set when notChanged counts any object}", has_doc, apply ), @@ -118,6 +118,38 @@ fn used_colors(d: &Document, ids: &[NodeId], only: Option<&BTreeSet>) -> used.into_iter().map(|(_, u)| u).collect() } +/// The colors `n` shows, in paint order ([`node_colors`]), then the colors of the tiles of the +/// patterns it uses as fills or strokes, and of the patterns those tiles use (each pattern once). +fn shown_colors<'a>(d: &'a Document, n: &'a Node) -> Vec { + let mut colors = vec![]; + node_colors(n, &mut |c, _| colors.push(*c)); + let mut patterns: Vec<&str> = vec![]; + pattern_names(n, &mut patterns); + let mut next = 0; + while let Some(&name) = patterns.get(next) { + next += 1; + for a in d.pattern(name).map(|def| def.art.as_slice()).unwrap_or_default() { + node_colors(a, &mut |c, _| colors.push(*c)); + pattern_names(a, &mut patterns); + } + } + colors +} + +/// Adds the names of the patterns `n` and the objects inside it use as fills or strokes to `names`, +/// leaving out names already there. +fn pattern_names<'a>(n: &'a Node, names: &mut Vec<&'a str>) { + n.walk(&mut |m| { + for p in node_paints(m) { + if let Paint::Pattern { pattern, .. } = p + && !names.contains(&pattern.as_str()) + { + names.push(pattern); + } + } + }); +} + /// The objects whose colors `recolor.colors` and `recolor.reduce` read, for [`used_colors`]: `ids` /// (each checked) or else the selection, with everything they hold; with nothing selected, the /// objects Select All takes and the objects inside them, leaving out every hidden or locked object @@ -526,6 +558,10 @@ fn apply(s: &mut Session, p: &Value) -> Result { // `selected_roots` refuses it. super::edit::refuse_symbol_part(s)?; let ids = s.doc()?.selection.objects.clone(); + let left = { + let st = s.doc()?; + left_out(&st.doc, st.isolation, &super::edit::roots_of(&st.doc, ids.clone())) + }; let group = text_param(p, "group", C)?; if ids.is_empty() && group.is_none() { return Err(bad(C, "select artwork to recolour, or give a colour `group`")); @@ -563,7 +599,7 @@ fn apply(s: &mut Session, p: &Value) -> Result { }; let rename = text_param(p, "rename", C)?; let colorized_ok = super::paint::check_colorized(&s.doc()?.doc).is_ok(); - let (changed, edit, added, converted) = s.edit(label, |d, _| { + let (changed, edit, added, converted, not_changed) = s.edit(label, |d, _| { // The rows' library swatches join the document (one it has with that name and colour is // used as it is). let mut added = vec![]; @@ -601,12 +637,27 @@ fn apply(s: &mut Session, p: &Value) -> Result { 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, tint: &mut f32| map.map_link(c, link, tint); + // The objects left out whose colors the pass would change, with the colors they show now. + // The trial pass does not count towards the note on color models. + let before: Vec<(NodeId, Vec, bool)> = Recolor::with_links(scope, &f, &lf) + .would_change(d, &left) + .into_iter() + .filter_map(|(id, hidden)| Some((id, shown_colors(d, d.node(id)?), hidden))) + .collect(); + map.converted.set(false); let changed = Recolor::with_links(scope, &f, &lf).run(d, &ids); let edit = group.map(|g| super::swatch::rewrite_group(d, g, &new, rename, C)).transpose()?; super::paint::check_colorized_kept(colorized_ok, d).map_err(|e| bad(C, e))?; - Ok((changed, edit, added, map.converted.get())) + // Rewriting the group's swatches can change the colors of art linked to them, left out or + // not, and of the patterns it uses. Art that only loses its link keeps its colors and is + // counted. + let not_changed = NotChanged::of( + before.iter().filter(|(id, colors, _)| d.node(*id).is_some_and(|now| shown_colors(d, now) == *colors)).map(|(_, _, hidden)| *hidden), + ); + Ok((changed, edit, added, map.converted.get(), not_changed)) })?; let mut out = json!({ "changed": changed, "swatches": added }); + not_changed.report(&mut out); if let Some(e) = edit { e.defaults(s); out["relinked"] = json!(e.relinked()); diff --git a/crates/engine/src/tests/colorize.rs b/crates/engine/src/tests/colorize.rs index 40152af74..2dba3f6e0 100644 --- a/crates/engine/src/tests/colorize.rs +++ b/crates/engine/src/tests/colorize.rs @@ -179,7 +179,7 @@ fn fill_proxy_invert_recolors_a_colorized_gray_image_and_undo_restores_it() { let id = image(&mut s, "gray", gray(false)); s.execute("paint.setFill", &json!({"color": "#336699"})).unwrap(); let before = colorize(&s, id).unwrap(); - assert_eq!(s.execute("paint.invert", &json!({})).unwrap(), json!({"changed": 1})); + assert_eq!(s.execute("paint.invert", &json!({})).unwrap(), json!({"changed": 1, "notChanged": {"locked": 0, "hidden": 0}})); assert_eq!(colorize(&s, id).and_then(|p| p.color()).map(|c| c.to_hex()), Some("#cc9966".into())); assert_eq!(s.proxy_paints().0.color().map(|c| c.to_hex()), Some("#cc9966".into())); s.execute("edit.undo", &json!({})).unwrap(); @@ -193,7 +193,7 @@ fn fill_proxy_complement_recolors_a_colorized_gray_image_and_undo_restores_it() s.execute("paint.setFill", &json!({"color": "#ff0000"})).unwrap(); let before = colorize(&s, id).unwrap(); let expected = Paint::solid(Color::rgb8(0, 255, 255)); - assert_eq!(s.execute("paint.complement", &json!({})).unwrap(), json!({"changed": 1})); + assert_eq!(s.execute("paint.complement", &json!({})).unwrap(), json!({"changed": 1, "notChanged": {"locked": 0, "hidden": 0}})); assert_eq!(colorize(&s, id), Some(expected.clone())); assert_eq!(s.proxy_paints().0, expected); s.execute("edit.undo", &json!({})).unwrap(); @@ -209,7 +209,7 @@ fn stroke_proxy_invert_leaves_a_colorized_image_fill_unchanged() { let key = image_key(&s, id); s.execute("paint.setStroke", &json!({"color": "#ff0000"})).unwrap(); assert_eq!(s.doc().unwrap().doc.node(id).unwrap().appearance.stroke().unwrap().paint.color(), Some(Color::rgb8(255, 0, 0))); - assert_eq!(s.execute("paint.invert", &json!({"stroke": true})).unwrap(), json!({"changed": 1})); + assert_eq!(s.execute("paint.invert", &json!({"stroke": true})).unwrap(), json!({"changed": 1, "notChanged": {"locked": 0, "hidden": 0}})); assert_eq!(s.doc().unwrap().doc.node(id).unwrap().appearance.stroke().unwrap().paint.color(), Some(Color::rgb8(0, 255, 255))); assert_eq!(colorize(&s, id), before); assert_eq!(image_key(&s, id), key); @@ -223,7 +223,10 @@ fn edit_colors_invert_without_fill_recolors_a_colorized_images_stroke() { s.execute("paint.setStroke", &json!({"color": "#ff0000"})).unwrap(); let before = colorize(&s, id); let key = image_key(&s, id); - assert_eq!(s.execute("edit.colors.invert", &json!({"fill": false, "stroke": true})).unwrap(), json!({"changed": 1})); + assert_eq!( + s.execute("edit.colors.invert", &json!({"fill": false, "stroke": true})).unwrap(), + json!({"changed": 1, "notChanged": {"locked": 0, "hidden": 0}}) + ); assert_eq!(s.doc().unwrap().doc.node(id).unwrap().appearance.stroke().unwrap().paint.color(), Some(Color::rgb8(0, 255, 255))); assert_eq!(colorize(&s, id), before); assert_eq!(image_key(&s, id), key); @@ -251,7 +254,10 @@ fn edit_colors_recolors_an_image_colorize_with_pixels_excluded() { let id = image(&mut s, "gray", gray(false)); s.execute("paint.setFill", &json!({"color": "#ff0000"})).unwrap(); let before = colorize(&s, id); - assert_eq!(s.execute("edit.colors.invert", &json!({"includeImages": false})).unwrap(), json!({"changed": 1})); + assert_eq!( + s.execute("edit.colors.invert", &json!({"includeImages": false})).unwrap(), + json!({"changed": 1, "notChanged": {"locked": 0, "hidden": 0}}) + ); assert_eq!(colorize(&s, id), Some(Paint::solid(Color::rgb8(0, 255, 255)))); s.execute("edit.undo", &json!({})).unwrap(); assert_eq!(colorize(&s, id), before); @@ -264,7 +270,7 @@ fn edit_colors_invert_recolors_colorize_without_changing_pixels_and_undo_restore s.execute("paint.setFill", &json!({"color": "#336699"})).unwrap(); let before = colorize(&s, id); let key = image_key(&s, id); - assert_eq!(s.execute("edit.colors.invert", &json!({})).unwrap(), json!({"changed": 1})); + assert_eq!(s.execute("edit.colors.invert", &json!({})).unwrap(), json!({"changed": 1, "notChanged": {"locked": 0, "hidden": 0}})); assert_eq!(colorize(&s, id).and_then(|p| p.color()).map(|c| c.to_hex()), Some("#cc9966".into())); assert_eq!(image_key(&s, id), key); s.execute("edit.undo", &json!({})).unwrap(); @@ -292,7 +298,7 @@ fn edit_colors_invert_without_fill_leaves_colorized_image_unchanged() { s.execute("paint.setFill", &json!({"color": "#336699"})).unwrap(); let before = colorize(&s, id); let key = image_key(&s, id); - assert_eq!(s.execute("edit.colors.invert", &json!({"fill": false})).unwrap(), json!({"changed": 0})); + assert_eq!(s.execute("edit.colors.invert", &json!({"fill": false})).unwrap(), json!({"changed": 0, "notChanged": {"locked": 0, "hidden": 0}})); assert_eq!(colorize(&s, id), before); assert_eq!(image_key(&s, id), key); } @@ -326,7 +332,7 @@ fn proxy_invert_recolors_a_colorized_image_and_another_fill_in_one_undo_step() { s.execute("paint.setFill", &json!({"color": "#0000ff"})).unwrap(); s.execute("select.set", &json!({"ids": [id.0, shape.0]})).unwrap(); let undo = s.doc().unwrap().history.undo.len(); - assert_eq!(s.execute("paint.invert", &json!({})).unwrap(), json!({"changed": 2})); + assert_eq!(s.execute("paint.invert", &json!({})).unwrap(), json!({"changed": 2, "notChanged": {"locked": 0, "hidden": 0}})); assert_eq!(s.doc().unwrap().history.undo.len(), undo + 1); assert_eq!(colorize(&s, id), Some(Paint::solid(Color::rgb8(0, 255, 255)))); assert_eq!(s.doc().unwrap().doc.node(shape).unwrap().appearance.fill().unwrap().paint.color(), Some(Color::rgb8(255, 255, 0))); diff --git a/crates/engine/src/tests/editcolors.rs b/crates/engine/src/tests/editcolors.rs index 1f8e6bd40..bfd57a7aa 100644 --- a/crates/engine/src/tests/editcolors.rs +++ b/crates/engine/src/tests/editcolors.rs @@ -430,3 +430,47 @@ fn recolor_writes_a_new_pattern_swatch() { s.execute("edit.colors.invert", &json!({})).unwrap(); assert_eq!((fill_pattern(&s, r), tile_fill(&s, "Dots 2")), ("Dots 2".to_string(), "#00ffff".to_string())); } + +#[test] +fn edit_colors_of_everything_count_the_hidden_objects_left_out() { + let mut s = session(); + let shown = rect(&mut s, json!("#ff0000")); + let hidden = rect(&mut s, json!("#ff0000")); + // A hidden object with no paint would not change. + let unpainted = rect(&mut s, json!("#ff0000")); + s.execute("paint.setFill", &json!({"ids": [unpainted.0], "none": true})).unwrap(); + s.execute("paint.setStroke", &json!({"ids": [unpainted.0], "none": true})).unwrap(); + s.execute("layer.setProps", &json!({"ids": [hidden.0, unpainted.0], "visible": false})).unwrap(); + // A locked member of a selected group is recolored with it. + let member = rect(&mut s, json!("#ff0000")); + let locked_member = rect(&mut s, json!("#ff0000")); + s.execute("select.set", &json!({"ids": [member.0, locked_member.0]})).unwrap(); + s.execute("object.group", &json!({})).unwrap(); + s.execute("layer.setProps", &json!({"ids": [locked_member.0], "locked": true})).unwrap(); + s.execute("select.all", &json!({})).unwrap(); + let r = s.execute("edit.colors.invert", &json!({})).unwrap(); + assert_eq!(r["notChanged"], json!({"locked": 0, "hidden": 1}), "{r}"); + assert_eq!(r["warning"], "Recolor completed; 1 hidden object was left unchanged"); + assert_eq!((fill_of(&s, shown).to_hex(), fill_of(&s, hidden).to_hex()), ("#00ffff".to_string(), "#ff0000".to_string())); + assert_eq!(fill_of(&s, locked_member).to_hex(), "#00ffff"); + // Objects given by `ids` are the whole scope. + let r = s.execute("edit.colors.toGrayscale", &json!({"ids": [shown.0]})).unwrap(); + assert_eq!(r["notChanged"], json!({"locked": 0, "hidden": 0}), "{r}"); + assert!(r.get("warning").is_none(), "{r}"); +} + +#[test] +fn edit_colors_in_isolation_mode_count_the_locked_members_left_out() { + let mut s = session(); + let a = rect(&mut s, json!("#ff0000")); + let b = rect(&mut s, json!("#ff0000")); + let c = rect(&mut s, json!("#ff0000")); + s.execute("select.set", &json!({"ids": [a.0, b.0, c.0]})).unwrap(); + let g = id_of(&s.execute("object.group", &json!({})).unwrap()); + s.execute("layer.setProps", &json!({"ids": [b.0, c.0], "locked": true})).unwrap(); + s.execute("object.isolate", &json!({"id": g.0})).unwrap(); + s.execute("select.all", &json!({})).unwrap(); + let r = s.execute("edit.colors.saturate", &json!({"intensity": -50})).unwrap(); + assert_eq!(r["notChanged"], json!({"locked": 2, "hidden": 0}), "{r}"); + assert_eq!(r["warning"], "Recolor completed; 2 locked objects were left unchanged"); +} diff --git a/crates/engine/src/tests/recolor.rs b/crates/engine/src/tests/recolor.rs index 541f961b8..37464de71 100644 --- a/crates/engine/src/tests/recolor.rs +++ b/crates/engine/src/tests/recolor.rs @@ -630,6 +630,215 @@ fn model_param_must_be_a_string_when_given() { assert_eq!(fill(&s, art).to_hex(), "#1a1a1a", "nothing changed"); } +/// Sets the Layers panel's lock and visibility of `ids` (objects or layers). +fn set_props(s: &mut Session, ids: &[NodeId], props: Value) { + let mut p = props; + p["ids"] = json!(ids.iter().map(|i| i.0).collect::>()); + run(s, "layer.setProps", p); +} + +#[test] +fn recolor_of_everything_counts_the_locked_and_hidden_objects_it_left_out() { + let mut s = session(); + let open = rect(&mut s, json!("#ff0000")); + let locked = rect(&mut s, json!("#ff0000")); + let hidden = rect(&mut s, json!("#ff0000")); + let both = rect(&mut s, json!("#ff0000")); + // A locked object the map leaves as it is was not left out of anything. + let other = rect(&mut s, json!("#00ff00")); + set_props(&mut s, &[locked, other], json!({"locked": true})); + set_props(&mut s, &[hidden], json!({"visible": false})); + set_props(&mut s, &[both], json!({"visible": false, "locked": true})); + // The objects of a locked layer count as locked; a template layer's art is not counted. + let layer = NodeId(run(&mut s, "layer.new", json!({}))["id"].as_u64().unwrap()); + let on_locked_layer = rect(&mut s, json!("#ff0000")); + set_props(&mut s, &[layer], json!({"locked": true})); + let template = NodeId(run(&mut s, "layer.new", json!({}))["id"].as_u64().unwrap()); + rect(&mut s, json!("#ff0000")); + set_props(&mut s, &[template], json!({"template": true})); + select_all(&mut s); + let r = run(&mut s, "recolor.apply", json!({"map": {"#ff0000": "#0000ff"}})); + assert_eq!(r["notChanged"], json!({"locked": 2, "hidden": 2}), "{r}"); + assert_eq!(r["warning"], "Recolor completed; 2 locked objects and 2 hidden objects were left unchanged"); + assert_eq!(fill(&s, open).to_hex(), "#0000ff"); + for id in [locked, hidden, both, on_locked_layer] { + assert_eq!(fill(&s, id).to_hex(), "#ff0000", "locked and hidden art keeps its colors"); + } +} + +#[test] +fn recolor_of_everything_counts_locked_art_that_only_loses_its_group_swatch_link() { + let mut s = session(); + run(&mut s, "swatch.newGroup", json!({"name": "Brand", "colors": ["#2878f0", "#ff0000"]})); + let names: Vec = + s.doc().unwrap().doc.swatch_groups.iter().find(|g| g.name == "Brand").unwrap().swatches.iter().map(|w| w.name.clone()).collect(); + for name in &names { + run(&mut s, "swatch.edit", json!({"name": name, "global": true})); + } + let open = rect(&mut s, json!("#000000")); + let locked = rect(&mut s, json!("#000000")); + run(&mut s, "paint.setFill", json!({"ids": [open.0, locked.0], "swatch": names[1]})); + set_props(&mut s, &[locked], json!({"locked": true})); + select_all(&mut s); + // The group is rewritten with one color, so its second swatch goes and the locked art only + // loses its link to it. + let r = run(&mut s, "recolor.apply", json!({"map": {"#ff0000": "#00ff00"}, "group": "Brand", "groupColors": ["#123456"]})); + assert_eq!(fill(&s, open).to_hex(), "#00ff00"); + assert_eq!(fill(&s, locked).to_hex(), "#ff0000", "the locked art keeps its color"); + assert_eq!(r["notChanged"], json!({"locked": 1, "hidden": 0}), "{r}"); +} + +#[test] +fn recolor_of_everything_does_not_count_locked_art_whose_pattern_the_group_rewrite_recolors() { + let mut s = session(); + run(&mut s, "swatch.newGroup", json!({"name": "Brand", "colors": ["#2878f0", "#ff0000"]})); + let names: Vec = + s.doc().unwrap().doc.swatch_groups.iter().find(|g| g.name == "Brand").unwrap().swatches.iter().map(|w| w.name.clone()).collect(); + for name in &names { + run(&mut s, "swatch.edit", json!({"name": name, "global": true})); + } + // "Dots": a tile linked to the group's second swatch. + let tile = rect(&mut s, json!("#000000")); + run(&mut s, "paint.setFill", json!({"ids": [tile.0], "swatch": names[1]})); + run(&mut s, "select.set", json!({"ids": [tile.0]})); + run(&mut s, "object.pattern.make", json!({"name": "Dots", "width": 20, "height": 20})); + run(&mut s, "object.pattern.done", json!({})); + let tile_hex = |s: &Session| s.doc().unwrap().doc.pattern("Dots").unwrap().art[0].appearance.fill_paint().color().unwrap().to_hex(); + assert_eq!(tile_hex(&s), "#ff0000"); + let locked = rect(&mut s, json!("#000000")); + run(&mut s, "paint.setFill", json!({"ids": [locked.0], "swatch": "Dots"})); + set_props(&mut s, &[locked], json!({"locked": true})); + let open = rect(&mut s, json!("#000000")); + run(&mut s, "paint.setFill", json!({"ids": [open.0], "swatch": names[1]})); + select_all(&mut s); + // The group's second swatch is rewritten with the row's new color, and the tile follows it. + let r = run(&mut s, "recolor.apply", json!({"map": {"#ff0000": "#00ff00"}, "group": "Brand", "groupColors": ["#2878f0", "#00ff00"]})); + assert_eq!(tile_hex(&s), "#00ff00", "{r}"); + assert_eq!(r["notChanged"], json!({"locked": 0, "hidden": 0}), "{r}"); +} + +#[test] +fn recolor_of_everything_does_not_count_locked_art_whose_nested_pattern_the_group_rewrite_recolors() { + let mut s = session(); + run(&mut s, "swatch.newGroup", json!({"name": "Brand", "colors": ["#2878f0", "#ff0000"]})); + let names: Vec = + s.doc().unwrap().doc.swatch_groups.iter().find(|g| g.name == "Brand").unwrap().swatches.iter().map(|w| w.name.clone()).collect(); + for name in &names { + run(&mut s, "swatch.edit", json!({"name": name, "global": true})); + } + // "Dots": a tile linked to the group's second swatch; "Rings": a tile filled with "Dots". + let tile = rect(&mut s, json!("#000000")); + run(&mut s, "paint.setFill", json!({"ids": [tile.0], "swatch": names[1]})); + run(&mut s, "select.set", json!({"ids": [tile.0]})); + run(&mut s, "object.pattern.make", json!({"name": "Dots", "width": 20, "height": 20})); + run(&mut s, "object.pattern.done", json!({})); + let outer = rect(&mut s, json!("#000000")); + run(&mut s, "paint.setFill", json!({"ids": [outer.0], "swatch": "Dots"})); + run(&mut s, "select.set", json!({"ids": [outer.0]})); + run(&mut s, "object.pattern.make", json!({"name": "Rings", "width": 40, "height": 40})); + run(&mut s, "object.pattern.done", json!({})); + let tile_hex = |s: &Session| s.doc().unwrap().doc.pattern("Dots").unwrap().art[0].appearance.fill_paint().color().unwrap().to_hex(); + let locked = rect(&mut s, json!("#000000")); + run(&mut s, "paint.setFill", json!({"ids": [locked.0], "swatch": "Rings"})); + set_props(&mut s, &[locked], json!({"locked": true})); + let open = rect(&mut s, json!("#000000")); + run(&mut s, "paint.setFill", json!({"ids": [open.0], "swatch": names[1]})); + select_all(&mut s); + // The rewrite recolors the inner tile, which the locked art shows through "Rings". + let r = run(&mut s, "recolor.apply", json!({"map": {"#ff0000": "#00ff00"}, "group": "Brand", "groupColors": ["#2878f0", "#00ff00"]})); + assert_eq!(tile_hex(&s), "#00ff00", "{r}"); + assert_eq!(r["notChanged"], json!({"locked": 0, "hidden": 0}), "{r}"); +} + +#[test] +fn recolor_of_everything_does_not_count_locked_art_that_would_only_take_a_swatch_link() { + let mut s = session(); + run(&mut s, "swatch.new", json!({"name": "Red", "color": "#ff0000", "global": true})); + // "Dots": an unlinked red tile. + let tile = rect(&mut s, json!("#ff0000")); + run(&mut s, "select.set", json!({"ids": [tile.0]})); + run(&mut s, "object.pattern.make", json!({"name": "Dots", "width": 20, "height": 20})); + run(&mut s, "object.pattern.done", json!({})); + let open = rect(&mut s, json!("#ff0000")); + let locked = rect(&mut s, json!("#ff0000")); + let dots = rect(&mut s, json!("#000000")); + run(&mut s, "paint.setFill", json!({"ids": [dots.0], "swatch": "Dots"})); + set_props(&mut s, &[locked, dots], json!({"locked": true})); + select_all(&mut s); + // The row keeps the color and snaps it to the document's global swatch of the same color, so + // the open art only takes the link. + let r = run(&mut s, "recolor.apply", json!({"map": {"#ff0000": "#ff0000"}, "limitTo": "document"})); + let link = |s: &Session, id: NodeId| match s.doc().unwrap().doc.node(id).unwrap().appearance.fill_paint() { + vectorcraft_color::Paint::Solid { swatch, .. } => swatch, + p => panic!("{p:?}"), + }; + assert!(link(&s, open).is_some(), "{r}"); + assert_eq!(fill(&s, locked).to_hex(), "#ff0000"); + assert_eq!(r["notChanged"], json!({"locked": 0, "hidden": 0}), "{r}"); + assert!(r.get("warning").is_none(), "{r}"); +} + +#[test] +fn recolor_of_everything_does_not_count_locked_art_whose_nested_pattern_would_only_take_a_swatch_link() { + let mut s = session(); + run(&mut s, "swatch.new", json!({"name": "Red", "color": "#ff0000", "global": true})); + // "Dots": an unlinked red tile; "Rings": a tile filled with "Dots". + let tile = rect(&mut s, json!("#ff0000")); + run(&mut s, "select.set", json!({"ids": [tile.0]})); + run(&mut s, "object.pattern.make", json!({"name": "Dots", "width": 20, "height": 20})); + run(&mut s, "object.pattern.done", json!({})); + let outer = rect(&mut s, json!("#000000")); + run(&mut s, "paint.setFill", json!({"ids": [outer.0], "swatch": "Dots"})); + run(&mut s, "select.set", json!({"ids": [outer.0]})); + run(&mut s, "object.pattern.make", json!({"name": "Rings", "width": 40, "height": 40})); + run(&mut s, "object.pattern.done", json!({})); + rect(&mut s, json!("#ff0000")); + let rings = rect(&mut s, json!("#000000")); + run(&mut s, "paint.setFill", json!({"ids": [rings.0], "swatch": "Rings"})); + set_props(&mut s, &[rings], json!({"locked": true})); + select_all(&mut s); + // The inner tile would only take the link to the document's red swatch. + let r = run(&mut s, "recolor.apply", json!({"map": {"#ff0000": "#ff0000"}, "limitTo": "document"})); + assert_eq!(r["notChanged"], json!({"locked": 0, "hidden": 0}), "{r}"); + assert!(r.get("warning").is_none(), "{r}"); +} + +#[test] +fn locked_and_hidden_members_of_a_selected_group_are_recolored_and_not_counted() { + let mut s = session(); + let ids = [rect(&mut s, json!("#ff0000")), rect(&mut s, json!("#ff0000")), rect(&mut s, json!("#ff0000"))]; + select_all(&mut s); + run(&mut s, "object.group", json!({})); + set_props(&mut s, &ids[1..2], json!({"locked": true})); + set_props(&mut s, &ids[2..3], json!({"visible": false})); + let node = |s: &Session, id: NodeId| s.doc().unwrap().doc.node(id).unwrap().clone(); + assert!(node(&s, ids[1]).locked && !node(&s, ids[2]).visible); + select_all(&mut s); + let r = run(&mut s, "recolor.apply", json!({"map": {"#ff0000": "#0000ff"}})); + assert_eq!(r["notChanged"], json!({"locked": 0, "hidden": 0}), "{r}"); + assert!(r.get("warning").is_none(), "{r}"); + assert!(ids.iter().all(|id| fill(&s, *id).to_hex() == "#0000ff")); +} + +#[test] +fn recolor_of_part_of_the_art_counts_nothing_left_out() { + let mut s = session(); + let picked = rect(&mut s, json!("#ff0000")); + let locked = rect(&mut s, json!("#ff0000")); + rect(&mut s, json!("#ff0000")); + set_props(&mut s, &[locked], json!({"locked": true})); + run(&mut s, "select.set", json!({"ids": [picked.0]})); + let r = run(&mut s, "recolor.apply", json!({"map": {"#ff0000": "#0000ff"}})); + assert_eq!(r["notChanged"], json!({"locked": 0, "hidden": 0}), "{r}"); + assert!(r.get("warning").is_none(), "{r}"); + // Everything selected, with nothing locked or hidden. + run(&mut s, "object.unlockAll", json!({})); + select_all(&mut s); + let r = run(&mut s, "recolor.apply", json!({"map": {"#ff0000": "#00ff00"}})); + assert_eq!(r["notChanged"], json!({"locked": 0, "hidden": 0}), "{r}"); + assert!(r.get("warning").is_none(), "{r}"); +} + /// A row's own `model` takes the place of the command's for that row's colors; a bad one fails /// before editing; `recolor.randomize` moves it with its new color. #[test] diff --git a/crates/ui-egui/src/i18n/ca.tsv b/crates/ui-egui/src/i18n/ca.tsv index ccaf8d995..b0bf6998d 100644 --- a/crates/ui-egui/src/i18n/ca.tsv +++ b/crates/ui-egui/src/i18n/ca.tsv @@ -5627,6 +5627,13 @@ digits Malayalam Malaiàlam @msg select an object in perspective selecciona un objecte en perspectiva @msg select an Objects on Path group selecciona un grup d’objectes sobre un traçat @msg select artwork (or a colour group) to recolor selecciona la il·lustració (o un grup de colors) per recolorejar +@msg Recolor completed; {locked} and {hidden} were left unchanged Recoloració completada; sense canvis: {locked} i {hidden} +@msg Recolor completed; {objects} were left unchanged Recoloració completada; sense canvis: {objects} +@msg Recolor completed; {objects} was left unchanged Recoloració completada; sense canvis: {objects} +@msg 1 locked object 1 objecte bloquejat +@msg {n} locked objects {n} objectes bloquejats +@msg 1 hidden object 1 objecte ocult +@msg {n} hidden objects {n} objectes ocults @msg select at least two objects selecciona almenys dos objectes @msg select objects, not layers selecciona objectes, no capes @msg select one object selecciona un objecte diff --git a/crates/ui-egui/src/i18n/cs.tsv b/crates/ui-egui/src/i18n/cs.tsv index ce5bcd370..5b283d741 100644 --- a/crates/ui-egui/src/i18n/cs.tsv +++ b/crates/ui-egui/src/i18n/cs.tsv @@ -4839,6 +4839,13 @@ imageTrace Outline Obrys @msg select an image vyberte obraz @msg select an object in perspective vyberte objekt v perspektivě @msg select artwork (or a colour group) to recolor vyberte kresbu (nebo skupinu barev) k přebarvení +@msg Recolor completed; {locked} and {hidden} were left unchanged Přebarvení dokončeno; beze změny: {locked} a {hidden} +@msg Recolor completed; {objects} were left unchanged Přebarvení dokončeno; beze změny: {objects} +@msg Recolor completed; {objects} was left unchanged Přebarvení dokončeno; beze změny: {objects} +@msg 1 locked object zamčený objekt (1) +@msg {n} locked objects zamčené objekty ({n}) +@msg 1 hidden object skrytý objekt (1) +@msg {n} hidden objects skryté objekty ({n}) @msg select at least two objects vyberte alespoň dva objekty @msg exit isolation mode first nejprve ukončete izolovaný režim @msg select objects, not layers vyberte objekty, ne vrstvy diff --git a/crates/ui-egui/src/i18n/de.tsv b/crates/ui-egui/src/i18n/de.tsv index 83a721a0f..21e806ebc 100644 --- a/crates/ui-egui/src/i18n/de.tsv +++ b/crates/ui-egui/src/i18n/de.tsv @@ -4446,6 +4446,13 @@ theme Appearance Erscheinungsbild @msg select an image wählen Sie ein Bild aus @msg select an object in perspective wählen Sie ein Objekt in der Perspektive aus @msg select artwork (or a colour group) to recolor wählen Sie eine Grafik (oder eine Farbgruppe) zum Neufärben aus +@msg Recolor completed; {locked} and {hidden} were left unchanged Neufärben abgeschlossen; unverändert: {locked} und {hidden} +@msg Recolor completed; {objects} were left unchanged Neufärben abgeschlossen; unverändert: {objects} +@msg Recolor completed; {objects} was left unchanged Neufärben abgeschlossen; unverändert: {objects} +@msg 1 locked object 1 gesperrtes Objekt +@msg {n} locked objects {n} gesperrte Objekte +@msg 1 hidden object 1 ausgeblendetes Objekt +@msg {n} hidden objects {n} ausgeblendete Objekte @msg select at least two objects wählen Sie mindestens zwei Objekte aus @msg exit isolation mode first beenden Sie zuerst den Isolationsmodus @msg select objects, not layers wählen Sie Objekte aus, keine Ebenen diff --git a/crates/ui-egui/src/i18n/el.tsv b/crates/ui-egui/src/i18n/el.tsv index 905694778..cccf88b15 100644 --- a/crates/ui-egui/src/i18n/el.tsv +++ b/crates/ui-egui/src/i18n/el.tsv @@ -4479,6 +4479,13 @@ digits Malayalam Μαλαγιάλαμ @msg select an image επιλέξτε εικόνα @msg select an object in perspective επιλέξτε αντικείμενο σε προοπτική @msg select artwork (or a colour group) to recolor επιλέξτε σχέδιο (ή ομάδα χρωμάτων) για επαναχρωματισμό +@msg Recolor completed; {locked} and {hidden} were left unchanged Ο επαναχρωματισμός ολοκληρώθηκε· χωρίς αλλαγή: {locked} και {hidden} +@msg Recolor completed; {objects} were left unchanged Ο επαναχρωματισμός ολοκληρώθηκε· χωρίς αλλαγή: {objects} +@msg Recolor completed; {objects} was left unchanged Ο επαναχρωματισμός ολοκληρώθηκε· χωρίς αλλαγή: {objects} +@msg 1 locked object 1 κλειδωμένο αντικείμενο +@msg {n} locked objects {n} κλειδωμένα αντικείμενα +@msg 1 hidden object 1 κρυφό αντικείμενο +@msg {n} hidden objects {n} κρυφά αντικείμενα @msg select at least two objects επιλέξτε τουλάχιστον δύο αντικείμενα @msg exit isolation mode first βγείτε πρώτα από τη λειτουργία απομόνωσης @msg select objects, not layers επιλέξτε αντικείμενα, όχι επίπεδα diff --git a/crates/ui-egui/src/i18n/es.tsv b/crates/ui-egui/src/i18n/es.tsv index 44c6aa8a5..e90cf58f3 100644 --- a/crates/ui-egui/src/i18n/es.tsv +++ b/crates/ui-egui/src/i18n/es.tsv @@ -4530,6 +4530,13 @@ axis Both Ambos @msg select an image selecciona una imagen @msg select an object in perspective selecciona un objeto en perspectiva @msg select artwork (or a colour group) to recolor selecciona la ilustración (o un grupo de colores) que quieres volver a colorear +@msg Recolor completed; {locked} and {hidden} were left unchanged Recoloreado completado; sin cambios: {locked} y {hidden} +@msg Recolor completed; {objects} were left unchanged Recoloreado completado; sin cambios: {objects} +@msg Recolor completed; {objects} was left unchanged Recoloreado completado; sin cambios: {objects} +@msg 1 locked object 1 objeto bloqueado +@msg {n} locked objects {n} objetos bloqueados +@msg 1 hidden object 1 objeto oculto +@msg {n} hidden objects {n} objetos ocultos @msg select at least two objects selecciona al menos dos objetos @msg exit isolation mode first sal primero del modo de aislamiento @msg select objects, not layers selecciona objetos, no capas diff --git a/crates/ui-egui/src/i18n/fi.tsv b/crates/ui-egui/src/i18n/fi.tsv index 14f7e8d6b..08bc88ade 100644 --- a/crates/ui-egui/src/i18n/fi.tsv +++ b/crates/ui-egui/src/i18n/fi.tsv @@ -4741,6 +4741,13 @@ imageTrace Outline Ääriviiva @msg select an image valitse kuva @msg select an object in perspective valitse perspektiivissä oleva objekti @msg select artwork (or a colour group) to recolor valitse uudelleenväritettävä kuvitus (tai väriryhmä) +@msg Recolor completed; {locked} and {hidden} were left unchanged Uudelleenväritys valmis; muuttamatta jäi: {locked} ja {hidden} +@msg Recolor completed; {objects} were left unchanged Uudelleenväritys valmis; muuttamatta jäi: {objects} +@msg Recolor completed; {objects} was left unchanged Uudelleenväritys valmis; muuttamatta jäi: {objects} +@msg 1 locked object 1 lukittu objekti +@msg {n} locked objects {n} lukittua objektia +@msg 1 hidden object 1 piilotettu objekti +@msg {n} hidden objects {n} piilotettua objektia @msg select at least two objects valitse vähintään kaksi objektia @msg exit isolation mode first poistu ensin eristystilasta @msg select objects, not layers valitse objekteja, ei tasoja diff --git a/crates/ui-egui/src/i18n/fr.tsv b/crates/ui-egui/src/i18n/fr.tsv index c2e59da2a..02235699b 100644 --- a/crates/ui-egui/src/i18n/fr.tsv +++ b/crates/ui-egui/src/i18n/fr.tsv @@ -4520,6 +4520,13 @@ axis Both Les deux @msg select an image sélectionnez une image @msg select an object in perspective sélectionnez un objet en perspective @msg select artwork (or a colour group) to recolor sélectionnez l’illustration (ou un groupe de couleurs) dont redéfinir les couleurs +@msg Recolor completed; {locked} and {hidden} were left unchanged Recoloration terminée ; sans changement : {locked} et {hidden} +@msg Recolor completed; {objects} were left unchanged Recoloration terminée ; sans changement : {objects} +@msg Recolor completed; {objects} was left unchanged Recoloration terminée ; sans changement : {objects} +@msg 1 locked object 1 objet verrouillé +@msg {n} locked objects {n} objets verrouillés +@msg 1 hidden object 1 objet masqué +@msg {n} hidden objects {n} objets masqués @msg select at least two objects sélectionnez au moins deux objets @msg exit isolation mode first quittez d’abord le mode Isolation @msg select objects, not layers sélectionnez des objets, pas des calques diff --git a/crates/ui-egui/src/i18n/hi.tsv b/crates/ui-egui/src/i18n/hi.tsv index a9c7f4f0d..f999c8d31 100644 --- a/crates/ui-egui/src/i18n/hi.tsv +++ b/crates/ui-egui/src/i18n/hi.tsv @@ -7226,6 +7226,13 @@ antiAlias Sharp शार्प @msg select area type and closed paths (at least one area type object) क्षेत्र प्रकार और बंद पाथ (कम से कम एक क्षेत्र प्रकार ऑब्जेक्ट) सिलेक्ट करें @msg select area type whose first line can fit its frame क्षेत्र के प्रकार सिलेक्ट करें जिनकी पहली पंक्ति अपने फ्रेम को फिट कर सकती है @msg select artwork (or a colour group) to recolor फिर से रंग करने के लिए आर्टवर्क (या एक रंग समूह) सिलेक्ट करें +@msg Recolor completed; {locked} and {hidden} were left unchanged रीकलर पूरा हुआ; ये अपरिवर्तित रहे: {locked} और {hidden} +@msg Recolor completed; {objects} were left unchanged रीकलर पूरा हुआ; ये अपरिवर्तित रहे: {objects} +@msg Recolor completed; {objects} was left unchanged रीकलर पूरा हुआ; यह अपरिवर्तित रहा: {objects} +@msg 1 locked object 1 लॉक किया गया ऑब्जेक्ट +@msg {n} locked objects {n} लॉक किए गए ऑब्जेक्ट +@msg 1 hidden object 1 छिपा हुआ ऑब्जेक्ट +@msg {n} hidden objects {n} छिपे हुए ऑब्जेक्ट @msg select artwork to add its colours अपने रंगों को जोड़ने के लिए आर्टवर्क सिलेक्ट करें @msg select artwork to make a group of its colours अपने रंगों का एक समूह बनाने के लिए आर्टवर्क सिलेक्ट करें @msg select artwork to recolour, or give a colour `group` रंग बदलने के लिए आर्टवर्क चुनें या रंग `group` दें diff --git a/crates/ui-egui/src/i18n/hu.tsv b/crates/ui-egui/src/i18n/hu.tsv index 70f2c779d..bcb46aa3c 100644 --- a/crates/ui-egui/src/i18n/hu.tsv +++ b/crates/ui-egui/src/i18n/hu.tsv @@ -5277,6 +5277,13 @@ imageTrace Outline Körvonal @msg select an Image Trace object jelölj ki egy képvektorizálási objektumot @msg select an object in perspective jelölj ki egy perspektivikus objektumot @msg select artwork (or a colour group) to recolor jelölj ki átszínezendő grafikát (vagy színcsoportot) +@msg Recolor completed; {locked} and {hidden} were left unchanged Átszínezés kész; változatlan maradt: {locked} és {hidden} +@msg Recolor completed; {objects} were left unchanged Átszínezés kész; változatlan maradt: {objects} +@msg Recolor completed; {objects} was left unchanged Átszínezés kész; változatlan maradt: {objects} +@msg 1 locked object 1 zárolt objektum +@msg {n} locked objects {n} zárolt objektum +@msg 1 hidden object 1 rejtett objektum +@msg {n} hidden objects {n} rejtett objektum @msg select at least two objects jelölj ki legalább két objektumot @msg exit isolation mode first előbb lépj ki az elkülönítési módból @msg select objects, not layers objektumokat jelölj ki, rétegeket ne diff --git a/crates/ui-egui/src/i18n/it.tsv b/crates/ui-egui/src/i18n/it.tsv index 0b34752a7..0c3b98b89 100644 --- a/crates/ui-egui/src/i18n/it.tsv +++ b/crates/ui-egui/src/i18n/it.tsv @@ -4572,6 +4572,13 @@ axis Both Entrambi @msg select an image seleziona un'immagine @msg select an object in perspective seleziona un oggetto in prospettiva @msg select artwork (or a colour group) to recolor seleziona la grafica (o un gruppo colori) da ricolorare +@msg Recolor completed; {locked} and {hidden} were left unchanged Ricolorazione completata; senza modifiche: {locked} e {hidden} +@msg Recolor completed; {objects} were left unchanged Ricolorazione completata; senza modifiche: {objects} +@msg Recolor completed; {objects} was left unchanged Ricolorazione completata; senza modifiche: {objects} +@msg 1 locked object 1 oggetto bloccato +@msg {n} locked objects {n} oggetti bloccati +@msg 1 hidden object 1 oggetto nascosto +@msg {n} hidden objects {n} oggetti nascosti @msg select at least two objects seleziona almeno due oggetti @msg exit isolation mode first esci prima dalla modalità isolamento @msg select objects, not layers seleziona oggetti, non livelli diff --git a/crates/ui-egui/src/i18n/ja.tsv b/crates/ui-egui/src/i18n/ja.tsv index 72352dc1f..b7beb7356 100644 --- a/crates/ui-egui/src/i18n/ja.tsv +++ b/crates/ui-egui/src/i18n/ja.tsv @@ -4696,6 +4696,13 @@ swatch Bright Violet ビビッドバイオレット @msg print settings are not an object プリント設定がオブジェクト形式ではありません @msg no colour group `{g}` カラーグループ `{g}` が見つかりません @msg select artwork (or a colour group) to recolor 再配色するアートワーク (またはカラーグループ) を選択してください +@msg Recolor completed; {locked} and {hidden} were left unchanged 再配色が完了しました。変更されなかったもの: {locked}、{hidden} +@msg Recolor completed; {objects} were left unchanged 再配色が完了しました。変更されなかったもの: {objects} +@msg Recolor completed; {objects} was left unchanged 再配色が完了しました。変更されなかったもの: {objects} +@msg 1 locked object ロックされたオブジェクト 1 個 +@msg {n} locked objects ロックされたオブジェクト {n} 個 +@msg 1 hidden object 非表示のオブジェクト 1 個 +@msg {n} hidden objects 非表示のオブジェクト {n} 個 @msg Preview in Browser needs the desktop app (the web downloads files instead) 「ブラウザーでプレビュー」にはデスクトップアプリが必要です (Web 版では代わりにファイルをダウンロードします) @msg no file system here この環境にはファイルシステムがありません @msg nothing to preview プレビューするものがありません diff --git a/crates/ui-egui/src/i18n/ka.tsv b/crates/ui-egui/src/i18n/ka.tsv index 43787fc72..d9c7e32f4 100644 --- a/crates/ui-egui/src/i18n/ka.tsv +++ b/crates/ui-egui/src/i18n/ka.tsv @@ -4281,6 +4281,13 @@ font-technology Variable ცვლადი @msg select an image მონიშნეთ გამოსახულება @msg select an object in perspective მონიშნეთ ობიექტი პერსპექტივაში @msg select artwork (or a colour group) to recolor მონიშნეთ ხელახლა შესაფერადებელი ნახატი (ან ფერთა ჯგუფი) +@msg Recolor completed; {locked} and {hidden} were left unchanged ხელახლა შეფერადება დასრულდა; უცვლელი დარჩა: {locked} და {hidden} +@msg Recolor completed; {objects} were left unchanged ხელახლა შეფერადება დასრულდა; უცვლელი დარჩა: {objects} +@msg Recolor completed; {objects} was left unchanged ხელახლა შეფერადება დასრულდა; უცვლელი დარჩა: {objects} +@msg 1 locked object 1 ჩაკეტილი ობიექტი +@msg {n} locked objects {n} ჩაკეტილი ობიექტი +@msg 1 hidden object 1 დამალული ობიექტი +@msg {n} hidden objects {n} დამალული ობიექტი @msg select at least two objects მონიშნეთ ორი ობიექტი მაინც @msg exit isolation mode first ჯერ გამოდით იზოლაციის რეჟიმიდან @msg select objects, not layers მონიშნეთ ობიექტები და არა შრეები diff --git a/crates/ui-egui/src/i18n/ko.tsv b/crates/ui-egui/src/i18n/ko.tsv index ae35dfc68..36cbe846e 100644 --- a/crates/ui-egui/src/i18n/ko.tsv +++ b/crates/ui-egui/src/i18n/ko.tsv @@ -4623,6 +4623,13 @@ theme Appearance 외관 @msg print settings are not an object 인쇄 설정이 개체가 아닙니다 @msg no colour group `{g}` 색상 그룹 `{g}`이(가) 없습니다 @msg select artwork (or a colour group) to recolor 리컬러할 아트웍(또는 색상 그룹)을 선택하세요 +@msg Recolor completed; {locked} and {hidden} were left unchanged 색상 재지정이 완료되었습니다. 변경되지 않음: {locked}, {hidden} +@msg Recolor completed; {objects} were left unchanged 색상 재지정이 완료되었습니다. 변경되지 않음: {objects} +@msg Recolor completed; {objects} was left unchanged 색상 재지정이 완료되었습니다. 변경되지 않음: {objects} +@msg 1 locked object 잠긴 개체 1개 +@msg {n} locked objects 잠긴 개체 {n}개 +@msg 1 hidden object 숨겨진 개체 1개 +@msg {n} hidden objects 숨겨진 개체 {n}개 @msg Preview in Browser needs the desktop app (the web downloads files instead) 브라우저에서 미리 보기는 데스크톱 앱이 필요합니다(웹에서는 파일을 다운로드합니다) @msg no file system here 여기에는 파일 시스템이 없습니다 @msg nothing to preview 미리 볼 항목이 없습니다 diff --git a/crates/ui-egui/src/i18n/pl.tsv b/crates/ui-egui/src/i18n/pl.tsv index 717646ed4..e9e25a7f7 100644 --- a/crates/ui-egui/src/i18n/pl.tsv +++ b/crates/ui-egui/src/i18n/pl.tsv @@ -4664,6 +4664,13 @@ imageTrace Outline Kontur @msg select an image zaznacz obraz @msg select an object in perspective zaznacz obiekt w perspektywie @msg select artwork (or a colour group) to recolor zaznacz kompozycję (lub grupę kolorów), aby zmienić kolory +@msg Recolor completed; {locked} and {hidden} were left unchanged Zmiana kolorów zakończona; bez zmian: {locked} i {hidden} +@msg Recolor completed; {objects} were left unchanged Zmiana kolorów zakończona; bez zmian: {objects} +@msg Recolor completed; {objects} was left unchanged Zmiana kolorów zakończona; bez zmian: {objects} +@msg 1 locked object zablokowany obiekt (1) +@msg {n} locked objects zablokowane obiekty ({n}) +@msg 1 hidden object ukryty obiekt (1) +@msg {n} hidden objects ukryte obiekty ({n}) @msg select at least two objects zaznacz co najmniej dwa obiekty @msg exit isolation mode first najpierw wyjdź z trybu izolacji @msg select objects, not layers zaznacz obiekty, nie warstwy diff --git a/crates/ui-egui/src/i18n/pt-br.tsv b/crates/ui-egui/src/i18n/pt-br.tsv index 780f556e0..fde3c95b6 100644 --- a/crates/ui-egui/src/i18n/pt-br.tsv +++ b/crates/ui-egui/src/i18n/pt-br.tsv @@ -4101,6 +4101,13 @@ axis Both Ambos @msg select an image selecione uma imagem @msg select an object in perspective selecione um objeto em perspectiva @msg select artwork (or a colour group) to recolor selecione uma arte-final (ou um grupo de cores) para recolorir +@msg Recolor completed; {locked} and {hidden} were left unchanged Recoloração concluída; sem alteração: {locked} e {hidden} +@msg Recolor completed; {objects} were left unchanged Recoloração concluída; sem alteração: {objects} +@msg Recolor completed; {objects} was left unchanged Recoloração concluída; sem alteração: {objects} +@msg 1 locked object 1 objeto bloqueado +@msg {n} locked objects {n} objetos bloqueados +@msg 1 hidden object 1 objeto oculto +@msg {n} hidden objects {n} objetos ocultos @msg select at least two objects selecione pelo menos dois objetos @msg exit isolation mode first saia do modo de isolamento primeiro @msg select objects, not layers selecione objetos, não camadas diff --git a/crates/ui-egui/src/i18n/ru.tsv b/crates/ui-egui/src/i18n/ru.tsv index addb45f58..507e663a7 100644 --- a/crates/ui-egui/src/i18n/ru.tsv +++ b/crates/ui-egui/src/i18n/ru.tsv @@ -4567,6 +4567,13 @@ axis Both Оба @msg select an image выделите изображение @msg select an object in perspective выделите объект в перспективе @msg select artwork (or a colour group) to recolor выделите рисунок (или цветовую группу) для перекрашивания +@msg Recolor completed; {locked} and {hidden} were left unchanged Перекрашивание завершено; без изменений: {locked} и {hidden} +@msg Recolor completed; {objects} were left unchanged Перекрашивание завершено; без изменений: {objects} +@msg Recolor completed; {objects} was left unchanged Перекрашивание завершено; без изменений: {objects} +@msg 1 locked object заблокированный объект (1) +@msg {n} locked objects заблокированные объекты ({n}) +@msg 1 hidden object скрытый объект (1) +@msg {n} hidden objects скрытые объекты ({n}) @msg select at least two objects выделите как минимум два объекта @msg exit isolation mode first сначала выйдите из режима изоляции @msg select objects, not layers выделите объекты, а не слои diff --git a/crates/ui-egui/src/i18n/sv.tsv b/crates/ui-egui/src/i18n/sv.tsv index 01c52040d..13dce154f 100644 --- a/crates/ui-egui/src/i18n/sv.tsv +++ b/crates/ui-egui/src/i18n/sv.tsv @@ -4429,6 +4429,13 @@ axis Both Båda @msg select an image markera en bild @msg select an object in perspective markera ett objekt i perspektiv @msg select artwork (or a colour group) to recolor markera grafik (eller en färggrupp) att färga om +@msg Recolor completed; {locked} and {hidden} were left unchanged Omfärgning klar; utan ändring: {locked} och {hidden} +@msg Recolor completed; {objects} were left unchanged Omfärgning klar; utan ändring: {objects} +@msg Recolor completed; {objects} was left unchanged Omfärgning klar; utan ändring: {objects} +@msg 1 locked object 1 låst objekt +@msg {n} locked objects {n} låsta objekt +@msg 1 hidden object 1 dolt objekt +@msg {n} hidden objects {n} dolda objekt @msg select at least two objects markera minst två objekt @msg exit isolation mode first avsluta isoleringsläget först @msg select objects, not layers markera objekt, inte lager diff --git a/crates/ui-egui/src/i18n/tests.rs b/crates/ui-egui/src/i18n/tests.rs index 447f086be..c9b118843 100644 --- a/crates/ui-egui/src/i18n/tests.rs +++ b/crates/ui-egui/src/i18n/tests.rs @@ -1597,6 +1597,28 @@ fn messages_translate_by_template_and_their_parts() { assert_eq!(message(Lang::EN, nested), nested); } +/// The note `recolor.apply` and the Edit Colors commands give when they leave out locked or hidden +/// objects is translated whole, its counts included, in every complete-message language. +#[test] +fn recolor_notes_on_objects_left_unchanged_translate_with_their_counts() { + let notes = [ + ("Recolor completed; 2 locked objects and 1 hidden object were left unchanged", ["2", "1"]), + ("Recolor completed; 1 locked object and 3 hidden objects were left unchanged", ["1", "3"]), + ("Recolor completed; 1 locked object was left unchanged", ["1", "1"]), + ("Recolor completed; 5 hidden objects were left unchanged", ["5", "5"]), + ]; + for code in COMPLETE_MESSAGES { + let lang = Lang::from_code(code).expect("registered"); + for (note, counts) in notes { + let shown = message(lang, note); + // Whole words, so that a translation such as Catalan's "objecte" is not read as English. + let english = ["completed", "locked", "hidden", "object", "objects", "unchanged"]; + assert!(shown.split(|c: char| !c.is_alphabetic()).all(|w| !english.contains(&w)), "{code}: {shown}"); + assert!(counts.iter().all(|n| shown.contains(n)), "{code}: {shown}"); + } + } +} + #[test] fn package_status_message_templates_parse() { let c = Catalog::parse(concat!( diff --git a/crates/ui-egui/src/i18n/tr.tsv b/crates/ui-egui/src/i18n/tr.tsv index 11a579549..a9c500eda 100644 --- a/crates/ui-egui/src/i18n/tr.tsv +++ b/crates/ui-egui/src/i18n/tr.tsv @@ -4282,6 +4282,13 @@ axis Both Her İkisi @msg select an image bir resim seç @msg select an object in perspective perspektifte bir nesne seç @msg select artwork (or a colour group) to recolor yeniden renklendirilecek resmi (veya bir renk grubunu) seçin +@msg Recolor completed; {locked} and {hidden} were left unchanged Yeniden renklendirme tamamlandı; değiştirilmeden kalan: {locked} ve {hidden} +@msg Recolor completed; {objects} were left unchanged Yeniden renklendirme tamamlandı; değiştirilmeden kalan: {objects} +@msg Recolor completed; {objects} was left unchanged Yeniden renklendirme tamamlandı; değiştirilmeden kalan: {objects} +@msg 1 locked object 1 kilitli nesne +@msg {n} locked objects {n} kilitli nesne +@msg 1 hidden object 1 gizli nesne +@msg {n} hidden objects {n} gizli nesne @msg select at least two objects en az iki nesne seç @msg exit isolation mode first önce izolasyon modundan çık @msg select objects, not layers katmanları değil nesneleri seçin diff --git a/crates/ui-egui/src/i18n/uk.tsv b/crates/ui-egui/src/i18n/uk.tsv index 2e354aded..e0089d18e 100644 --- a/crates/ui-egui/src/i18n/uk.tsv +++ b/crates/ui-egui/src/i18n/uk.tsv @@ -4556,6 +4556,13 @@ axis Both Обидві @msg select an image виділіть зображення @msg select an object in perspective виділіть об’єкт у перспективі @msg select artwork (or a colour group) to recolor виділіть малюнок (або колірну групу) для перефарбовування +@msg Recolor completed; {locked} and {hidden} were left unchanged Перефарбовування завершено; без змін: {locked} і {hidden} +@msg Recolor completed; {objects} were left unchanged Перефарбовування завершено; без змін: {objects} +@msg Recolor completed; {objects} was left unchanged Перефарбовування завершено; без змін: {objects} +@msg 1 locked object заблокований об’єкт (1) +@msg {n} locked objects заблоковані об’єкти ({n}) +@msg 1 hidden object прихований об’єкт (1) +@msg {n} hidden objects приховані об’єкти ({n}) @msg select at least two objects виділіть як мінімум два об’єкти @msg exit isolation mode first спочатку вийдіть з режиму ізоляції @msg select objects, not layers виділіть об’єкти, а не шари diff --git a/crates/ui-egui/src/i18n/vi.tsv b/crates/ui-egui/src/i18n/vi.tsv index 5e135a108..b1ce3d82c 100644 --- a/crates/ui-egui/src/i18n/vi.tsv +++ b/crates/ui-egui/src/i18n/vi.tsv @@ -4931,6 +4931,13 @@ theme Appearance Giao diện @msg select an Image Trace object hãy chọn đối tượng vector hóa ảnh @msg select an object in perspective hãy chọn đối tượng trong phối cảnh @msg select artwork (or a colour group) to recolor hãy chọn tác phẩm (hoặc nhóm màu) để tô lại +@msg Recolor completed; {locked} and {hidden} were left unchanged Đã tô màu lại xong; không thay đổi: {locked} và {hidden} +@msg Recolor completed; {objects} were left unchanged Đã tô màu lại xong; không thay đổi: {objects} +@msg Recolor completed; {objects} was left unchanged Đã tô màu lại xong; không thay đổi: {objects} +@msg 1 locked object 1 đối tượng bị khóa +@msg {n} locked objects {n} đối tượng bị khóa +@msg 1 hidden object 1 đối tượng bị ẩn +@msg {n} hidden objects {n} đối tượng bị ẩn @msg select at least two objects hãy chọn ít nhất hai đối tượng @msg exit isolation mode first hãy thoát chế độ cô lập trước @msg select objects, not layers hãy chọn đối tượng, không phải lớp diff --git a/crates/ui-egui/src/i18n/zh-hans.tsv b/crates/ui-egui/src/i18n/zh-hans.tsv index f69bfcfdb..32781e7ef 100644 --- a/crates/ui-egui/src/i18n/zh-hans.tsv +++ b/crates/ui-egui/src/i18n/zh-hans.tsv @@ -4407,6 +4407,13 @@ imageTrace Outline 轮廓 @msg print settings are not an object 打印设置不是对象 @msg no colour group `{g}` 没有颜色组 `{g}` @msg select artwork (or a colour group) to recolor 请选择要重新着色的图稿 (或颜色组) +@msg Recolor completed; {locked} and {hidden} were left unchanged 重新着色已完成;未更改:{locked}和{hidden} +@msg Recolor completed; {objects} were left unchanged 重新着色已完成;未更改:{objects} +@msg Recolor completed; {objects} was left unchanged 重新着色已完成;未更改:{objects} +@msg 1 locked object 1 个锁定的对象 +@msg {n} locked objects {n} 个锁定的对象 +@msg 1 hidden object 1 个隐藏的对象 +@msg {n} hidden objects {n} 个隐藏的对象 @msg Preview in Browser needs the desktop app (the web downloads files instead) “在浏览器中预览”需要桌面应用 (网页版会改为下载文件) @msg no file system here 此处没有文件系统 @msg nothing to preview 没有可预览的内容 diff --git a/crates/ui-egui/src/i18n/zh-hant.tsv b/crates/ui-egui/src/i18n/zh-hant.tsv index c0116e8bc..57aeca5f8 100644 --- a/crates/ui-egui/src/i18n/zh-hant.tsv +++ b/crates/ui-egui/src/i18n/zh-hant.tsv @@ -2252,6 +2252,13 @@ file-filter Templates 範本 @msg print settings are not an object 列印設定不是物件 @msg no colour group `{g}` 沒有顏色群組 `{g}` @msg select artwork (or a colour group) to recolor 請選取要重新上色的圖稿 (或顏色群組) +@msg Recolor completed; {locked} and {hidden} were left unchanged 重新上色已完成;未變更:{locked}和{hidden} +@msg Recolor completed; {objects} were left unchanged 重新上色已完成;未變更:{objects} +@msg Recolor completed; {objects} was left unchanged 重新上色已完成;未變更:{objects} +@msg 1 locked object 1 個鎖定的物件 +@msg {n} locked objects {n} 個鎖定的物件 +@msg 1 hidden object 1 個隱藏的物件 +@msg {n} hidden objects {n} 個隱藏的物件 @msg Preview in Browser needs the desktop app (the web downloads files instead) 「在瀏覽器中預覽」需要桌面應用程式 (網頁版會改為下載檔案) @msg no file system here 此處沒有檔案系統 @msg nothing to preview 沒有可預覽的內容 diff --git a/docs/mcp.md b/docs/mcp.md index 28a6a3be0..ea8f91260 100644 --- a/docs/mcp.md +++ b/docs/mcp.md @@ -2437,6 +2437,15 @@ For `edit.colors.*`, `fill: false` leaves colorized image fills unchanged. `reco `edit.colors.saturate` needs `intensity`, a number from -100 to 100 (a number outside the range counts as its nearest end). Without one, or with a value that is not a number, it answers `` missing number `intensity` `` and changes nothing. +The reply of `recolor.apply` and of every command above except the Blend commands has `notChanged: {locked, hidden}`. +When the selection is exactly what `select.all` takes (in isolation mode, what it takes inside the isolated group), it +counts the objects `select.all` left out whose colors the command would have changed: locked or hidden objects, the +objects of locked or hidden layers and sublayers, and objects on locked artboards (counted as locked). An object that +is both hidden and locked counts as hidden. Template layers' art is not counted. Locked and hidden objects inside a +selected group are recolored and not counted. With any other selection, or with `ids`, both counts are 0. When a count +is above 0 the reply also has a `warning`, which the app shows in the status bar, for example +`Recolor completed; 3 locked objects and 1 hidden object were left unchanged`. + ```json {"name":"run_command","arguments":{"command":"recolor.colors","params":{}}} {"name":"run_command","arguments":{"command":"recolor.apply","params":{"map":{"#ff0000":"#0055ff"},"includeImages":false}}}