collective

Browse / github.com/storytold/vectorcraft / patch

Image Trace: keepColors traces only the listed palette colors

Image Trace: keepColors traces only the listed palette colors ## What and why Image Trace cannot trace some of an image's colors and leave the rest out. Ignore Color drops one color, so tracing only the line work of a multi-color image takes a full trace and then deleting the other colors' paths by hand. A new trace parameter, `keepColors` (default empty, so today's results do not change), lists the colors to trace. With `"keepColors": ["#rrggbb", …]` in the params of `imageTrace.make` or `imageTrace.makeAndExpand`, only the palette colors within about 30 levels of a listed color are traced, the distance Ignore Color matches by. With Method Abutting, e

Status
open
Score
0.00
Adopters
0 other operators
Author
dotCooCoo (imported from pull request #3968, open)
Base
f05c33cdfc0de6bd3f7d71e2a1628622af9a50f8
Size
+451 −14 in 29 files
Review
pending
Shared
2026-10-28
Id
4a7a50a6f08c23e6e07257b7

How it went

No other operator has reported using it yet.

Diff

raw · crates/engine/src/cmd/buildcmds.rs crates/engine/src/tests_build.rs crates/trace/src/lib.rs crates/trace/src/tests.rs crates/ui-egui/src/i18n/cs.tsv crates/ui-egui/src/i18n/de.tsv crates/ui-egui/src/i18n/el.tsv crates/ui-egui/src/i18n/es.tsv crates/ui-egui/src/i18n/fi.tsv crates/ui-egui/src/i18n/fr.tsv crates/ui-egui/src/i18n/hu.tsv crates/ui-egui/src/i18n/it.tsv crates/ui-egui/src/i18n/ja.tsv crates/ui-egui/src/i18n/ka.tsv crates/ui-egui/src/i18n/ko.tsv crates/ui-egui/src/i18n/nl.tsv crates/ui-egui/src/i18n/pl.tsv crates/ui-egui/src/i18n/pt-br.tsv crates/ui-egui/src/i18n/ru.tsv crates/ui-egui/src/i18n/sv.tsv crates/ui-egui/src/i18n/tests.rs crates/ui-egui/src/i18n/tr.tsv crates/ui-egui/src/i18n/uk.tsv crates/ui-egui/src/i18n/vi.tsv crates/ui-egui/src/i18n/zh-hans.tsv crates/ui-egui/src/i18n/zh-hant.tsv docs/image-trace-adjustments.md docs/mcp.md docs/target-app-parity.md

diff --git a/crates/engine/src/cmd/buildcmds.rs b/crates/engine/src/cmd/buildcmds.rs--- a/crates/engine/src/cmd/buildcmds.rs+++ b/crates/engine/src/cmd/buildcmds.rs@@ -129,7 +129,7 @@ pub fn specs() -> Vec<CommandSpec> {             "Make",             ["Object", "Image Trace"],             None,-            "{id?: image or Image Trace group (default: selection), preset?: name (imageTrace.presets), enhanced?: false (the preset's enhanced version: with Auto Grouping, and with Shapes in all but High Fidelity Photo; Flat Logo has none and is traced as it is), params?: {mode: \"blackAndWhite\"|\"grayscale\"|\"color\"|\"logo\", threshold: 0-255, palette: \"limited\"|\"fullTone\"|\"automatic\"|\"documentLibrary\" (color mode; limited: the `colors` best colours, fullTone: as many as the tones need, more with colorDetail, automatic: flat art with its own colours, else fullTone, documentLibrary: at most `colors` of the library's colours, as they are), colors: 2-256, colorDetail: 0-100 (fullTone, automatic), library: \"document\" (the document's swatches) or a swatch library id or name (swatch.library.list; documentLibrary), paths: 0-100, corners: 0-100, noise: px, method: \"abutting\"|\"overlapping\", ignoreWhite (leave out white areas), ignoreColor?: \"#rrggbb\" (with ignoreWhite: the colour left out instead of white), snapCurvesToLines, shapes: false (filled outlines that are a circle, an ellipse, a rectangle or a regular polygon come out as that live shape), autoGrouping: false (each path that has others inside it is grouped with them), gradients: false (grayscale and color: an area whose colour changes evenly along one direction is one path with a linear gradient fill), fills: true, strokes: false (Create: lines up to strokeWidth wide become stroked centre lines; without fills, wider areas become stroked outlines), strokeWidth: px (10), logoColors?: [\"#rrggbb\"…] (mode logo: flat-colour logos only; colours found automatically or named here, white is always its own colour, at most ten; noise = areas under noise/10 px ignored; edges to a fraction of a pixel, sharp corners restored, lines and circles snapped; enclosed transparent areas are painted white unless ignoreWhite makes them holes; fills/strokes are ignored; gradients and photos are refused)}, view?: as imageTrace.setView (default: the traced object's own, else tracingResult), ids?: [id…] (instead of id)} → {id, paths, anchors, colors, warnings: [text…] (the image is over 1500 px on a side, so it traces more slowly; or it was traced scaled down; Flat Logo names only the size it traced a larger image at)}",+            "{id?: image or Image Trace group (default: selection), preset?: name (imageTrace.presets), enhanced?: false (the preset's enhanced version: with Auto Grouping, and with Shapes in all but High Fidelity Photo; Flat Logo has none and is traced as it is), params?: {mode: \"blackAndWhite\"|\"grayscale\"|\"color\"|\"logo\", threshold: 0-255, palette: \"limited\"|\"fullTone\"|\"automatic\"|\"documentLibrary\" (color mode; limited: the `colors` best colours, fullTone: as many as the tones need, more with colorDetail, automatic: flat art with its own colours, else fullTone, documentLibrary: at most `colors` of the library's colours, as they are), colors: 2-256, colorDetail: 0-100 (fullTone, automatic), library: \"document\" (the document's swatches) or a swatch library id or name (swatch.library.list; documentLibrary), paths: 0-100, corners: 0-100, noise: px, method: \"abutting\"|\"overlapping\", ignoreWhite (leave out white areas), ignoreColor?: \"#rrggbb\" (with ignoreWhite: the colour left out instead of white), keepColors?: [\"#rrggbb\"…] (not mode logo: only the palette colors within about 30 levels of a listed color are traced; empty: every color), snapCurvesToLines, shapes: false (filled outlines that are a circle, an ellipse, a rectangle or a regular polygon come out as that live shape), autoGrouping: false (each path that has others inside it is grouped with them), gradients: false (grayscale and color: an area whose colour changes evenly along one direction is one path with a linear gradient fill), fills: true, strokes: false (Create: lines up to strokeWidth wide become stroked centre lines; without fills, wider areas become stroked outlines), strokeWidth: px (10), logoColors?: [\"#rrggbb\"…] (mode logo: flat-colour logos only; colours found automatically or named here, white is always its own colour, at most ten; noise = areas under noise/10 px ignored; edges to a fraction of a pixel, sharp corners restored, lines and circles snapped; enclosed transparent areas are painted white unless ignoreWhite makes them holes; fills/strokes are ignored; gradients and photos are refused)}, view?: as imageTrace.setView (default: the traced object's own, else tracingResult), ids?: [id…] (instead of id)} → {id, paths, anchors, colors, warnings: [text…] (the image is over 1500 px on a side, so it traces more slowly; or it was traced scaled down; Flat Logo names only the size it traced a larger image at; or a keepColors color matches none the trace found, or nothing was traced)}",             has_selection_or_ids,             |s, p| trace_make(s, p, false)         ),@@ -138,7 +138,7 @@ pub fn specs() -> Vec<CommandSpec> {             "Make and Expand",             ["Object", "Image Trace"],             None,-            "{…as imageTrace.make} same as imageTrace.make, then expand → {id, paths, anchors, colors}",+            "{…as imageTrace.make} same as imageTrace.make, then expand → {id, paths, anchors, colors, warnings: [text…] (as imageTrace.make)}",             has_selection_or_ids,             |s, p| trace_make(s, p, true)         ),@@ -1137,7 +1137,8 @@ fn trace_make(s: &mut Session, p: &Value, expand: bool) -> Result<Value> {     let raster = tr::Raster::decode_to_fit(&blob.bytes).map_err(|e| EngineError::Other(e.to_string()))?;     let library = trace_library(c, s, &mut params)?;     let res = tr::trace_within(&raster, &params, MAX_TRACE_ANCHORS).map_err(|e| EngineError::Other(e.to_string()))?;-    let warnings = trace_warnings((img.width, img.height), (raster.width, raster.height), res.worked_at);+    let mut warnings = keep_warnings(&params, &res);+    warnings.extend(trace_warnings((img.width, img.height), (raster.width, raster.height), res.worked_at));     // Pixel space of the decoded raster → the image object's pixel space → document.     let sx = img.width.max(1) as f64 / raster.width.max(1) as f64;     let sy = img.height.max(1) as f64 / raster.height.max(1) as f64;@@ -1240,6 +1241,30 @@ pub(crate) fn trace_warnings(size: (u32, u32), traced: (u32, u32), worked: Optio     out } +/// What a trace with Keep Colors tells the user: that nothing was traced because no color the+/// trace found matches a listed one, because Ignore Color left out the ones that match, or because+/// the ones that match produce no paths; each listed color that matches none of them; and that Flat+/// Logo mode does not use the list.+pub(crate) fn keep_warnings(params: &tr::TraceParams, res: &tr::TraceResult) -> Vec<String> {+    if params.keep_colors.is_empty() {+        return vec![];+    }+    if params.mode == tr::Mode::Logo {+        return vec!["Flat Logo mode does not use keepColors: it traces the colors named in logoColors, or the colors it finds.".to_string()];+    }+    if res.paths.is_empty() && res.keep_unmatched.len() == params.keep_colors.len() {+        return vec![format!("Nothing was traced: none of the colors the trace found matches keepColors ({}).", params.keep_colors.join(", "))];+    }+    let mut out = vec![];+    if res.paths.is_empty() && res.keep_ignored {+        out.push("Nothing was traced: Ignore Color leaves out every area of the colors keepColors lists.".to_string());+    } else if res.paths.is_empty() {+        out.push("Nothing was traced: the colors that match keepColors produce no paths.".to_string());+    }+    out.extend(res.keep_unmatched.iter().map(|c| format!("keepColors: none of the colors the trace found matches {c}, so nothing of it is traced.")));+    out+}+ fn trace_targets(s: &Session, p: &Value) -> Result<Vec<NodeId>> {     let d = &s.doc()?.doc;     let v: Vec<NodeId> = root_ids(s, p)?.into_iter().filter(|i| d.node(*i).is_some_and(is_trace_group)).collect();@@ -1309,7 +1334,25 @@ fn trace_expand(s: &mut Session, p: &Value) -> Result<Value> {  #[cfg(test)] mod trace_limit_tests {-    use super::{MAX_TRACE_ANCHORS, trace_warnings};+    use super::{MAX_TRACE_ANCHORS, keep_warnings, trace_warnings};+    use vectorcraft_trace as tr;++    #[test]+    fn keep_warnings_name_matched_colors_that_produce_no_paths() {+        let params = tr::TraceParams { keep_colors: vec!["#ff0000".into()], ..tr::TraceParams::default() };+        // A listed color was traced, and produced no paths: the palette lists no color without paths.+        let traced = tr::TraceResult::default();+        let none = vec!["Nothing was traced: the colors that match keepColors produce no paths.".to_string()];+        assert_eq!(keep_warnings(&params, &traced), none);+        // With Ignore Color, too: Ignore Color did not leave that color out.+        let ignoring = tr::TraceParams { ignore_white: true, ..params.clone() };+        assert_eq!(keep_warnings(&ignoring, &traced), none);+        // Ignore Color left out every color that matches: none was traced.+        assert_eq!(+            keep_warnings(&ignoring, &tr::TraceResult { keep_ignored: true, ..tr::TraceResult::default() }),+            vec!["Nothing was traced: Ignore Color leaves out every area of the colors keepColors lists.".to_string()]+        );+    }      #[test]     fn trace_warnings_name_a_large_or_scaled_down_image() {diff --git a/crates/engine/src/tests_build.rs b/crates/engine/src/tests_build.rs--- a/crates/engine/src/tests_build.rs+++ b/crates/engine/src/tests_build.rs@@ -2323,6 +2323,91 @@ fn image_trace_make_places_paths_over_the_image() {     assert_eq!(paths[0].appearance.fill_paint(), Paint::solid(Color::rgb8(0, 0, 0))); } +#[test]+fn image_trace_keep_colors_traces_only_the_colors_listed() {+    // A light field with a black ring, a red square and a blue bar on it.+    let art = vectorcraft_trace::Raster::from_fn(200, 150, |x, y| {+        let r = (f64::from(x) + 0.5 - 70.0).hypot(f64::from(y) + 0.5 - 60.0);+        if (34.0..=40.0).contains(&r) {+            [0, 0, 0, 255]+        } else if (130..170).contains(&x) && (30..70).contains(&y) {+            [200, 40, 50, 255]+        } else if (30..170).contains(&x) && (110..130).contains(&y) {+            [40, 80, 200, 255]+        } else {+            [240, 240, 220, 255]+        }+    });+    let traced = |keep: Value| {+        let mut s = session();+        add_image(&mut s, &art, Affine::IDENTITY);+        let params = json!({"params": {"mode": "color", "palette": "limited", "colors": 4, "keepColors": keep}});+        let r = s.execute("imageTrace.makeAndExpand", &params)?;+        let g = node(&s, NodeId(r["id"].as_u64().unwrap()));+        let fills: Vec<Paint> = g.children().unwrap().iter().map(|n| n.appearance.fill_paint()).collect();+        Ok::<_, EngineError>((r["colors"].clone(), fills))+    };+    let (black, red, blue, light) = (Color::rgb8(0, 0, 0), Color::rgb8(200, 40, 50), Color::rgb8(40, 80, 200), Color::rgb8(240, 240, 220));+    let has = |fills: &[Paint], c: Color| fills.contains(&Paint::solid(c));+    // Nothing listed: every color.+    let (colors, fills) = traced(json!([])).unwrap();+    assert_eq!(colors, 4);+    assert!([black, red, blue, light].into_iter().all(|c| has(&fills, c)), "{fills:?}");+    // The black and the red only.+    let (colors, fills) = traced(json!(["#000000", "#c8283c"])).unwrap();+    assert_eq!(colors, 2);+    assert!(has(&fills, black) && has(&fills, red), "{fills:?}");+    assert!(fills.iter().all(|f| *f == Paint::solid(black) || *f == Paint::solid(red)), "{fills:?}");+    // A color that is none is refused.+    let e = traced(json!(["#000000", "black"])).unwrap_err().to_string();+    assert!(e.contains("\"black\"") && e.contains("#rrggbb"), "{e}");+    assert!(find_command("imageTrace.make").unwrap().params.contains("keepColors?: [\"#rrggbb\"…]"));+    assert!(find_command("imageTrace.makeAndExpand").unwrap().params.contains("warnings: [text…] (as imageTrace.make)"));+}++#[test]+fn image_trace_keep_colors_warns_about_colors_that_match_nothing() {+    // A light field with a black ring and a red square on it.+    let art = vectorcraft_trace::Raster::from_fn(200, 150, |x, y| {+        let r = (f64::from(x) + 0.5 - 70.0).hypot(f64::from(y) + 0.5 - 60.0);+        if (34.0..=40.0).contains(&r) {+            [0, 0, 0, 255]+        } else if (130..170).contains(&x) && (30..70).contains(&y) {+            [200, 40, 50, 255]+        } else {+            [240, 240, 220, 255]+        }+    });+    let traced = |params: Value| {+        let mut s = session();+        add_image(&mut s, &art, Affine::IDENTITY);+        let r = s.execute("imageTrace.makeAndExpand", &json!({ "params": params })).unwrap();+        (r["paths"].as_u64().unwrap(), r["warnings"].clone())+    };+    let color = |keep: Value| traced(json!({"mode": "color", "palette": "limited", "colors": 3, "keepColors": keep}));+    // Every color listed is in the image: no warning.+    let (paths, warnings) = color(json!(["#000000", "#c8283c"]));+    assert!(paths > 0);+    assert_eq!(warnings, json!([]));+    // No color matches: nothing is traced, and the reply says why.+    assert_eq!(+        color(json!(["#ff00ff", "#00ffff"])),+        (0, json!(["Nothing was traced: none of the colors the trace found matches keepColors (#ff00ff, #00ffff)."]))+    );+    // One color matches nothing: the others are traced, and that one is named.+    let (paths, warnings) = color(json!(["#000000", "#00ffff"]));+    assert!(paths > 0);+    assert_eq!(warnings, json!(["keepColors: none of the colors the trace found matches #00ffff, so nothing of it is traced."]));+    // Ignore Color leaves out the only color listed.+    let ignored =+        traced(json!({"mode": "color", "palette": "limited", "colors": 3, "keepColors": ["#000000"], "ignoreWhite": true, "ignoreColor": "#000000"}));+    assert_eq!(ignored, (0, json!(["Nothing was traced: Ignore Color leaves out every area of the colors keepColors lists."])));+    // Flat Logo mode does not use the list.+    let (paths, warnings) = traced(json!({"mode": "logo", "keepColors": ["#ff00ff"]}));+    assert!(paths > 0);+    assert_eq!(warnings, json!(["Flat Logo mode does not use keepColors: it traces the colors named in logoColors, or the colors it finds."]));+}+ /// Shapes: the disc of the test image comes out as a live ellipse where the image lies, the same /// circle as its traced path; without the option it is a plain path. #[test]diff --git a/crates/trace/src/lib.rs b/crates/trace/src/lib.rs--- a/crates/trace/src/lib.rs+++ b/crates/trace/src/lib.rs@@ -295,6 +295,11 @@ pub struct TraceParams {     /// The colour Ignore Color leaves out (`#rrggbb`): the palette colours within about 30 levels     /// of it. `None` is white. (Flat Logo mode leaves out white only.)     pub ignore_color: Option<String>,+    /// Keep Colors: the colors to trace (`#rrggbb`). Only the palette colors within about 30+    /// levels of one of them (as Ignore Color matches a color) are traced, a gradient by its middle+    /// color, and their paths are the ones the trace of every color has. Empty traces every color.+    /// (Flat Logo mode does not use it: it traces the colors named in `logo_colors`.)+    pub keep_colors: Vec<String>,     /// Replace nearly-straight curves with straight lines. In Logo mode this also snaps tight arcs     /// to true circles.     pub snap_curves_to_lines: bool,@@ -336,6 +341,7 @@ impl Default for TraceParams {             method: Method::Abutting,             ignore_white: false,             ignore_color: None,+            keep_colors: Vec::new(),             snap_curves_to_lines: false,             shapes: false,             auto_grouping: false,@@ -484,6 +490,12 @@ pub struct TraceResult {     /// option). Each path drawn together with those inside it, in the order they have here, paints     /// what `paths` paints in its order.     pub parents: Vec<Option<usize>>,+    /// Keep Colors: the colors listed in [`TraceParams::keep_colors`] (as written) that no color+    /// the trace found matches, so that nothing of them is traced. (Flat Logo mode: empty.)+    pub keep_unmatched: Vec<String>,+    /// Keep Colors: whether Ignore Color left out every color the trace found that a listed color+    /// matches, so that none of them was traced. (Flat Logo mode: false.)+    pub keep_ignored: bool,     /// Flat Logo: the size in pixels the image was traced at when it was averaged down first;     /// `None` when it was traced at its own size.     pub worked_at: Option<(u32, u32)>,@@ -507,6 +519,18 @@ fn is_ignored(c: [u8; 3], target: Option<[u8; 3]>) -> bool {     } } +/// Whether Keep Colors traces palette color `c`: every color when `keep` is empty, else those+/// Ignore Color would match to one of `keep`.+fn is_kept(c: [u8; 3], keep: &[[u8; 3]]) -> bool {+    keep.is_empty() || keep.iter().any(|&k| is_ignored(c, Some(k)))+}++/// For each color of `palette`, whether it is left out of the trace: Ignore Color's (`ignore`,+/// `None` for white) and those Keep Colors does not list (`keep`).+fn left_out(palette: &[[u8; 3]], params: &TraceParams, ignore: Option<[u8; 3]>, keep: &[[u8; 3]]) -> Vec<bool> {+    palette.iter().map(|&c| (params.ignore_white && is_ignored(c, ignore)) || !is_kept(c, keep)).collect()+}+ /// Trace `img` with `params`. Failures (an image Logo mode refuses, say) give an empty result: use /// [`trace_within`] to see why. pub fn trace(img: &Raster, params: &TraceParams) -> TraceResult {@@ -533,6 +557,12 @@ pub fn trace_within(img: &Raster, params: &TraceParams, max_anchors: usize) -> R  fn trace_paths(img: &Raster, params: &TraceParams, max_anchors: usize) -> Result<TraceResult, TraceError> {     let (w, h) = (img.width as usize, img.height as usize);+    // The colors Keep Colors traces (checked in every mode).+    let listed = params+        .keep_colors+        .iter()+        .map(|name| logo::parse_hex(name).ok_or_else(|| TraceError::BadPalette(name.clone())))+        .collect::<Result<Vec<_>, _>>()?;     if params.mode == Mode::Logo {         return logo::trace_logo(img, params, max_anchors);     }@@ -618,14 +648,27 @@ fn trace_paths(img: &Raster, params: &TraceParams, max_anchors: usize) -> Result     for (r, &i) in order.iter().enumerate() {         rank[i] = r;     }-    let layers: Vec<(usize, usize)> =-        order.iter().copied().enumerate().filter(|&(_, ci)| !(params.ignore_white && is_ignored(q.palette[ci], ignore))).collect();+    // Layers left out: Ignore Color's, and those Keep Colors does not list. Abutting, the others+    // are traced as they are in the trace of every color; stacked, they do not reach under the+    // colors left out (see `Layers::ignored`).+    let left_out = left_out(&q.palette, params, ignore, &listed);+    let layers: Vec<(usize, usize)> = order.iter().copied().enumerate().filter(|&(_, ci)| !left_out.get(ci).copied().unwrap_or(false)).collect();+    // The listed colors that match none of the colors found.+    let keep_unmatched = params+        .keep_colors+        .iter()+        .zip(&listed)+        .filter(|&(_, &k)| !order.iter().any(|&ci| is_kept(q.palette[ci], &[k])))+        .map(|(name, _)| name.clone())+        .collect();+    // Every layer is left out although a listed color matches one: Ignore Color left those out.+    let keep_ignored = !listed.is_empty() && layers.is_empty() && order.iter().any(|&ci| is_kept(q.palette[ci], &listed));     let job = Layers {         q: &q,         rank: &rank,         params,         img,-        ignore,+        left_out,         boxes,         gradients: &gradients,         bw_tones: bw_tones(img, &q),@@ -635,7 +678,7 @@ fn trace_paths(img: &Raster, params: &TraceParams, max_anchors: usize) -> Result         anchors: AtomicUsize::new(0),         max_anchors,     };-    let mut out = TraceResult::default();+    let mut out = TraceResult { keep_unmatched, keep_ignored, ..TraceResult::default() };     // A layer takes about ten bytes a pixel of its box while it is traced (its mask, the labels     // of its components, the stack that fills them, its edges): as many layers at a time as fit     // in half a gigabyte, so that a large image is not traced at the cost of gigabytes.@@ -691,8 +734,8 @@ struct Layers<'a> {     rank: &'a [usize],     params: &'a TraceParams,     img: &'a Raster,-    /// The colour Ignore Color leaves out, if not white.-    ignore: Option<[u8; 3]>,+    /// For each palette entry, whether Ignore Color or Keep Colors leaves it out (see [`left_out`]).+    left_out: Vec<bool>,     /// The box each palette entry's pixels lie in: `(x0, y0, x1, y1)`, the far sides excluded.     boxes: Vec<Option<(usize, usize, usize, usize)>>,     /// Gradients: the gradient each palette entry is, if it is one.@@ -709,10 +752,11 @@ struct Layers<'a> { }  impl Layers<'_> {-    /// Ignore White: white is no part of any layer (stacked layers reach under the layers above-    /// them, and must not reach under white that is left out).+    /// Ignore White, and the colors Keep Colors does not list: such a color is no part of any+    /// layer (stacked layers reach under the layers above them, and must not reach under a color+    /// that is left out).     fn ignored(&self, label: u16) -> bool {-        self.params.ignore_white && self.q.palette.get(label as usize).is_some_and(|c| is_ignored(*c, self.ignore))+        self.left_out.get(label as usize).copied().unwrap_or(false)     }     /// The region across a crack, for cutting outlines at their junctions: the outside pixel's     /// colour. Stacked layers end on many colours at once and share no edge: one neighbour.@@ -1084,7 +1128,7 @@ pub(crate) fn shift_probe(img: &Raster, params: &TraceParams) -> Vec<[f64; 4]> {         rank: &rank,         params,         img,-        ignore: None,+        left_out: left_out(&q.palette, params, None, &[]),         boxes: vec![None; n],         gradients: &gradients,         bw_tones: bw_tones(img, &q),diff --git a/crates/trace/src/tests.rs b/crates/trace/src/tests.rs--- a/crates/trace/src/tests.rs+++ b/crates/trace/src/tests.rs@@ -1217,6 +1217,113 @@ fn ignore_color_leaves_out_the_colour_picked() {     assert_eq!(serde_json::to_value(&params).unwrap()["ignoreColor"], serde_json::Value::Null); } +/// A navy field with a yellow disc, a green bar and a red square on it, hard-edged.+fn four_color_art() -> Raster {+    Raster::from_fn(160, 120, |x, y| {+        let (fx, fy) = (f64::from(x) + 0.5, f64::from(y) + 0.5);+        if Point::new(fx, fy).distance(Point::new(50.0, 45.0)) <= 30.0 {+            [250, 200, 40, 255]+        } else if (20.0..140.0).contains(&fx) && (90.0..108.0).contains(&fy) {+            [40, 160, 90, 255]+        } else if (100.0..140.0).contains(&fx) && (20.0..60.0).contains(&fy) {+            [200, 40, 50, 255]+        } else {+            [20, 40, 90, 255]+        }+    })+}++#[test]+fn keep_colors_traces_only_the_colors_listed() {+    let near = |c: [u8; 3], want: [u8; 3]| (0..3).all(|k| c[k].abs_diff(want[k]) <= 3);+    let (yellow, red, navy) = ([250, 200, 40], [200, 40, 50], [20, 40, 90]);+    let img = four_color_art();+    let pixels = |res: &TraceResult, c: [u8; 3]| res.paths.iter().filter(|p| near(p.color, c)).map(|p| p.pixels).sum::<usize>();+    let abutting = TraceParams { mode: Mode::Color, palette: Palette::Limited, colors: 4, ..TraceParams::default() };+    let abutting_all = trace(&img, &abutting);+    for method in [Method::Abutting, Method::Overlapping] {+        let params = TraceParams { method, ..abutting.clone() };+        let all = trace(&img, &params);+        assert_eq!(all.palette.len(), 4, "{method:?}: {:?}", all.palette);+        // The yellow and the red, each picked a little off.+        let res = trace(&img, &TraceParams { keep_colors: vec!["#f0c832".into(), "#c8283c".into()], ..params.clone() });+        assert_eq!(res.palette.len(), 2, "{method:?}: {:?}", res.palette);+        if method == Method::Abutting {+            // Their paths as the whole trace has them.+            let want: Vec<TracedPath> = all.paths.iter().filter(|p| near(p.color, yellow) || near(p.color, red)).cloned().collect();+            assert!(want.iter().any(|p| near(p.color, yellow)) && want.iter().any(|p| near(p.color, red)));+            assert_eq!(res.paths, want);+        } else {+            // Stacked, the red reaches under the yellow above it, not under the green left out.+            assert!(res.paths.iter().all(|p| near(p.color, yellow) || near(p.color, red)));+            assert_eq!(pixels(&res, red), pixels(&abutting_all, red) + pixels(&abutting_all, yellow));+            assert_eq!(pixels(&res, yellow), pixels(&abutting_all, yellow));+            // The navy field alone, at the bottom of the stack: no part of the colors left out.+            let field = trace(&img, &TraceParams { keep_colors: vec!["#14285a".into()], ..params.clone() });+            assert!(!field.paths.is_empty());+            assert_eq!(pixels(&field, navy), pixels(&abutting_all, navy));+            assert!(pixels(&all, navy) > pixels(&abutting_all, navy));+        }+        // Every color listed: the trace of every color, as with none listed.+        let every = vec!["#fac828".into(), "#28a05a".into(), "#c82832".into(), "#14285a".into()];+        assert_eq!(trace(&img, &TraceParams { keep_colors: every, ..params.clone() }), all, "{method:?}");+        // A color the image does not have: nothing is traced.+        assert!(trace(&img, &TraceParams { keep_colors: vec!["#ff00ff".into()], ..params.clone() }).paths.is_empty(), "{method:?}");+        // With Ignore Color as well, a kept color it matches is left out.+        let both = TraceParams {+            keep_colors: vec!["#fac828".into(), "#14285a".into()],+            ignore_white: true,+            ignore_color: Some("#14285a".into()),+            ..params.clone()+        };+        let res = trace(&img, &both);+        assert!(!res.paths.is_empty() && res.paths.iter().all(|p| near(p.color, yellow)), "{method:?}");+    }+    // A color that is none is refused, in Flat Logo mode too.+    for mode in [Mode::Color, Mode::Logo] {+        let bad = trace_within(&img, &TraceParams { mode, keep_colors: vec!["#c8283c".into(), "red".into()], ..TraceParams::default() }, usize::MAX);+        assert!(matches!(&bad, Err(TraceError::BadPalette(name)) if name == "red"), "{mode:?}");+    }+    // Old documents have no such setting: every color.+    let old: TraceParams = serde_json::from_str(r#"{"mode":"color"}"#).unwrap();+    assert!(old.keep_colors.is_empty());+    let on: TraceParams = serde_json::from_str(r##"{"keepColors":["#c8283c"]}"##).unwrap();+    assert_eq!(on.keep_colors, ["#c8283c"]);+}++#[test]+fn keep_colors_names_the_colors_nothing_matches() {+    let img = four_color_art();+    let params = TraceParams { mode: Mode::Color, palette: Palette::Limited, colors: 4, ..TraceParams::default() };+    let unmatched = |keep: &[&str], params: &TraceParams| {+        let res = trace(&img, &TraceParams { keep_colors: keep.iter().map(|k| k.to_string()).collect(), ..params.clone() });+        (res.keep_unmatched, res.paths.len())+    };+    // Every color listed matches one the trace found.+    assert_eq!(unmatched(&["#f0c832", "#c8283c"], &params).0, Vec::<String>::new());+    // A color the image does not have is named, as written, beside the ones traced.+    let (names, paths) = unmatched(&["#f0c832", "#FF00FF", "#c8283c"], &params);+    assert_eq!(names, ["#FF00FF"]);+    assert!(paths > 0);+    // None matches: every one is named, and nothing is traced.+    assert_eq!(unmatched(&["#ff00ff", "#00ffff"], &params), (vec!["#ff00ff".to_string(), "#00ffff".to_string()], 0));+    // A color Ignore Color leaves out still matches one the trace found.+    let ignoring = TraceParams { ignore_white: true, ignore_color: Some("#14285a".into()), ..params.clone() };+    assert_eq!(unmatched(&["#14285a"], &ignoring), (Vec::new(), 0));+    // The result says that Ignore Color left out every color that matches, and only then.+    let keep = |keep: &[&str], params: &TraceParams| {+        trace(&img, &TraceParams { keep_colors: keep.iter().map(|k| k.to_string()).collect(), ..params.clone() })+    };+    assert!(keep(&["#14285a"], &ignoring).keep_ignored);+    assert!(!keep(&["#14285a", "#f0c832"], &ignoring).keep_ignored);+    assert!(!keep(&["#14285a"], &params).keep_ignored);+    assert!(!keep(&["#ff00ff"], &ignoring).keep_ignored);+    // With none listed, none is named.+    assert!(trace(&img, &params).keep_unmatched.is_empty());+    // Flat Logo mode does not use the list.+    assert!(trace(&img, &TraceParams { mode: Mode::Logo, keep_colors: vec!["#ff00ff".into()], ..params }).keep_unmatched.is_empty());+}+ #[test] fn snap_curves_to_lines_levels_a_rectangle_a_degree_askew() {     // 120 × 60, turned 1.5° about its centre.diff --git a/crates/ui-egui/src/i18n/cs.tsv b/crates/ui-egui/src/i18n/cs.tsv--- a/crates/ui-egui/src/i18n/cs.tsv+++ b/crates/ui-egui/src/i18n/cs.tsv@@ -5401,6 +5401,11 @@ mark-style	Japanese	Japonské 	Snap to Perpendicular	Přichytávat ke kolmici @msg	unknown snap option `{key}` (use {options})	neznámá možnost přichytávání `{key}` (použijte {options}) 	Gap Length:	Délka mezery:+@msg	Flat Logo mode does not use keepColors: it traces the colors named in logoColors, or the colors it finds.	Režim Ploché logo nepoužívá keepColors: vektorizuje barvy uvedené v logoColors nebo barvy, které najde.+@msg	Nothing was traced: none of the colors the trace found matches keepColors ({_1}).	Nic nebylo vektorizováno: žádná z barev, které vektorizace našla, neodpovídá keepColors ({_1}).+@msg	Nothing was traced: Ignore Color leaves out every area of the colors keepColors lists.	Nic nebylo vektorizováno: Ignorovat barvu vynechává všechny oblasti barev uvedených v keepColors.+@msg	Nothing was traced: the colors that match keepColors produce no paths.	Nic nebylo vektorizováno: z barev odpovídajících keepColors nevznikly žádné cesty.+@msg	keepColors: none of the colors the trace found matches {c}, so nothing of it is traced.	keepColors: žádná z barev, které vektorizace našla, neodpovídá {c}, takže z ní není nic vektorizováno. @msg	Flat Logo traced the image at {_1} × {_2} pixels, averaged down from {_3} × {_4}.	Režim Ploché logo vektorizoval obraz v rozlišení {_1} × {_2} pixelů, zmenšený průměrováním z {_3} × {_4}. 	Artboard {n} of {total}	Pracovní plocha {n} z {total} 	File {n} of {total}	Soubor {n} z {total}diff --git a/crates/ui-egui/src/i18n/de.tsv b/crates/ui-egui/src/i18n/de.tsv--- a/crates/ui-egui/src/i18n/de.tsv+++ b/crates/ui-egui/src/i18n/de.tsv@@ -4980,6 +4980,11 @@ effect	Soft	Weich 	Snap to Perpendicular	An Senkrechter ausrichten @msg	unknown snap option `{key}` (use {options})	unbekannte Einrastoption `{key}` (möglich: {options}) 	Gap Length:	Lückenlänge:+@msg	Flat Logo mode does not use keepColors: it traces the colors named in logoColors, or the colors it finds.	Der Modus „Flaches Logo“ verwendet keepColors nicht: Er zeichnet die in logoColors genannten Farben nach oder die Farben, die er findet.+@msg	Nothing was traced: none of the colors the trace found matches keepColors ({_1}).	Nichts wurde nachgezeichnet: Keine der beim Nachzeichnen gefundenen Farben entspricht keepColors ({_1}).+@msg	Nothing was traced: Ignore Color leaves out every area of the colors keepColors lists.	Nichts wurde nachgezeichnet: „Farbe ignorieren“ lässt jeden Bereich der in keepColors aufgeführten Farben aus.+@msg	Nothing was traced: the colors that match keepColors produce no paths.	Nichts wurde nachgezeichnet: Die Farben, die keepColors entsprechen, ergeben keine Pfade.+@msg	keepColors: none of the colors the trace found matches {c}, so nothing of it is traced.	keepColors: Keine der beim Nachzeichnen gefundenen Farben entspricht {c}, daher wird davon nichts nachgezeichnet. @msg	Flat Logo traced the image at {_1} × {_2} pixels, averaged down from {_3} × {_4}.	Der Modus „Flaches Logo“ hat das Bild mit {_1} × {_2} Pixeln nachgezeichnet, durch Mittelung verkleinert von {_3} × {_4}. 	Artboard {n} of {total}	Zeichenfläche {n} von {total} 	File {n} of {total}	Datei {n} von {total}diff --git a/crates/ui-egui/src/i18n/el.tsv b/crates/ui-egui/src/i18n/el.tsv--- a/crates/ui-egui/src/i18n/el.tsv+++ b/crates/ui-egui/src/i18n/el.tsv@@ -5353,6 +5353,11 @@ effect	Points	Σημεία 	Snap to Perpendicular	Προσκόλληση σε κάθετο @msg	unknown snap option `{key}` (use {options})	άγνωστη επιλογή προσκόλλησης `{key}` (χρησιμοποιήστε {options}) 	Gap Length:	Μήκος κενού:+@msg	Flat Logo mode does not use keepColors: it traces the colors named in logoColors, or the colors it finds.	Η λειτουργία Επίπεδο λογότυπο δεν χρησιμοποιεί το keepColors: ανιχνεύει τα χρώματα που ορίζονται στο logoColors ή τα χρώματα που βρίσκει.+@msg	Nothing was traced: none of the colors the trace found matches keepColors ({_1}).	Δεν ανιχνεύτηκε τίποτα: κανένα από τα χρώματα που βρήκε η ανίχνευση δεν αντιστοιχεί στο keepColors ({_1}).+@msg	Nothing was traced: Ignore Color leaves out every area of the colors keepColors lists.	Δεν ανιχνεύτηκε τίποτα: η Παράβλεψη χρώματος παραλείπει κάθε περιοχή των χρωμάτων που ορίζει το keepColors.+@msg	Nothing was traced: the colors that match keepColors produce no paths.	Δεν ανιχνεύτηκε τίποτα: τα χρώματα που ταιριάζουν με το keepColors δεν δίνουν καμία διαδρομή.+@msg	keepColors: none of the colors the trace found matches {c}, so nothing of it is traced.	keepColors: κανένα από τα χρώματα που βρήκε η ανίχνευση δεν αντιστοιχεί στο {c}, οπότε τίποτα από αυτό δεν ανιχνεύεται. @msg	Flat Logo traced the image at {_1} × {_2} pixels, averaged down from {_3} × {_4}.	Η λειτουργία Επίπεδο λογότυπο ανίχνευσε την εικόνα σε {_1} × {_2} εικονοστοιχεία, σμικρυμένη με μέσο όρο από {_3} × {_4}. 	Artboard {n} of {total}	Πίνακας σχεδίασης {n} από {total} 	File {n} of {total}	Αρχείο {n} από {total}diff --git a/crates/ui-egui/src/i18n/es.tsv b/crates/ui-egui/src/i18n/es.tsv--- a/crates/ui-egui/src/i18n/es.tsv+++ b/crates/ui-egui/src/i18n/es.tsv@@ -5343,6 +5343,11 @@ mark-style	Japanese	Japonesas 	Snap to Perpendicular	Ajustar a perpendicular @msg	unknown snap option `{key}` (use {options})	opción de ajuste desconocida `{key}` (use {options}) 	Gap Length:	Longitud del hueco:+@msg	Flat Logo mode does not use keepColors: it traces the colors named in logoColors, or the colors it finds.	El modo Logotipo plano no usa keepColors: calca los colores indicados en logoColors o los colores que encuentra.+@msg	Nothing was traced: none of the colors the trace found matches keepColors ({_1}).	No se calcó nada: ninguno de los colores que encontró el calco coincide con keepColors ({_1}).+@msg	Nothing was traced: Ignore Color leaves out every area of the colors keepColors lists.	No se calcó nada: Ignorar color omite todas las áreas de los colores que indica keepColors.+@msg	Nothing was traced: the colors that match keepColors produce no paths.	No se calcó nada: los colores que coinciden con keepColors no producen ningún trazado.+@msg	keepColors: none of the colors the trace found matches {c}, so nothing of it is traced.	keepColors: ninguno de los colores que encontró el calco coincide con {c}, así que no se calca nada de él. @msg	Flat Logo traced the image at {_1} × {_2} pixels, averaged down from {_3} × {_4}.	El modo Logotipo plano calcó la imagen a {_1} × {_2} píxeles, reducida promediando desde {_3} × {_4}. 	Artboard {n} of {total}	Mesa de trabajo {n} de {total} 	File {n} of {total}	Archivo {n} de {total}diff --git a/crates/ui-egui/src/i18n/fi.tsv b/crates/ui-egui/src/i18n/fi.tsv--- a/crates/ui-egui/src/i18n/fi.tsv+++ b/crates/ui-egui/src/i18n/fi.tsv@@ -5365,6 +5365,11 @@ mark-style	Japanese	Japanilainen 	Snap to Perpendicular	Kohdista kohtisuoraan @msg	unknown snap option `{key}` (use {options})	tuntematon tartuntavalinta `{key}` (käytä: {options}) 	Gap Length:	Aukon pituus:+@msg	Flat Logo mode does not use keepColors: it traces the colors named in logoColors, or the colors it finds.	Tasainen logo -tila ei käytä keepColors-asetusta: se jäljittää logoColors-asetuksessa nimetyt värit tai löytämänsä värit.+@msg	Nothing was traced: none of the colors the trace found matches keepColors ({_1}).	Mitään ei jäljitetty: mikään jäljityksen löytämistä väreistä ei vastaa keepColors-asetusta ({_1}).+@msg	Nothing was traced: Ignore Color leaves out every area of the colors keepColors lists.	Mitään ei jäljitetty: Ohita väri jättää pois kaikki keepColors-asetuksen värien alueet.+@msg	Nothing was traced: the colors that match keepColors produce no paths.	Mitään ei jäljitetty: keepColors-asetusta vastaavista väreistä ei synny polkuja.+@msg	keepColors: none of the colors the trace found matches {c}, so nothing of it is traced.	keepColors: mikään jäljityksen löytämistä väreistä ei vastaa väriä {c}, joten siitä ei jäljitetä mitään. @msg	Flat Logo traced the image at {_1} × {_2} pixels, averaged down from {_3} × {_4}.	Tasainen logo -tila jäljitti kuvan kokoon {_1} × {_2} kuvapistettä keskiarvoistamalla pienennettynä koosta {_3} × {_4}. 	Artboard {n} of {total}	Piirtoalusta {n}/{total} 	File {n} of {total}	Tiedosto {n}/{total}diff --git a/crates/ui-egui/src/i18n/fr.tsv b/crates/ui-egui/src/i18n/fr.tsv--- a/crates/ui-egui/src/i18n/fr.tsv+++ b/crates/ui-egui/src/i18n/fr.tsv@@ -5400,6 +5400,11 @@ theme	Appearance	Apparence 	Snap to Perpendicular	Magnétisme de la perpendiculaire @msg	unknown snap option `{key}` (use {options})	option de magnétisme inconnue `{key}` (utilisez {options}) 	Gap Length:	Longueur de lacune :+@msg	Flat Logo mode does not use keepColors: it traces the colors named in logoColors, or the colors it finds.	Le mode Logo en aplats n’utilise pas keepColors : il vectorise les couleurs indiquées dans logoColors ou celles qu’il trouve.+@msg	Nothing was traced: none of the colors the trace found matches keepColors ({_1}).	Rien n’a été vectorisé : aucune des couleurs trouvées par la vectorisation ne correspond à keepColors ({_1}).+@msg	Nothing was traced: Ignore Color leaves out every area of the colors keepColors lists.	Rien n’a été vectorisé : Ignorer la couleur omet toutes les zones des couleurs indiquées dans keepColors.+@msg	Nothing was traced: the colors that match keepColors produce no paths.	Rien n’a été vectorisé : les couleurs correspondant à keepColors ne produisent aucun tracé.+@msg	keepColors: none of the colors the trace found matches {c}, so nothing of it is traced.	keepColors : aucune des couleurs trouvées par la vectorisation ne correspond à {c}, donc rien n’en est vectorisé. @msg	Flat Logo traced the image at {_1} × {_2} pixels, averaged down from {_3} × {_4}.	Le mode Logo en aplats a vectorisé l’image à {_1} × {_2} pixels, réduite par moyennage de {_3} × {_4}. 	Artboard {n} of {total}	Plan de travail {n} sur {total} 	File {n} of {total}	Fichier {n} sur {total}diff --git a/crates/ui-egui/src/i18n/hu.tsv b/crates/ui-egui/src/i18n/hu.tsv--- a/crates/ui-egui/src/i18n/hu.tsv+++ b/crates/ui-egui/src/i18n/hu.tsv@@ -5387,6 +5387,11 @@ imageTrace	Outline	Körvonal 	Snap to Perpendicular	Illesztés merőlegesre @msg	unknown snap option `{key}` (use {options})	ismeretlen illesztési beállítás: `{key}` (használható: {options}) 	Gap Length:	Rés hossza:+@msg	Flat Logo mode does not use keepColors: it traces the colors named in logoColors, or the colors it finds.	Az Egyszínű logó mód nem használja a keepColors beállítást: a logoColors beállításban megadott színeket vagy az általa talált színeket vektorizálja.+@msg	Nothing was traced: none of the colors the trace found matches keepColors ({_1}).	Semmi sem lett vektorizálva: a vektorizálás által talált színek egyike sem felel meg a keepColors beállításnak ({_1}).+@msg	Nothing was traced: Ignore Color leaves out every area of the colors keepColors lists.	Semmi sem lett vektorizálva: a Szín figyelmen kívül hagyása kihagyja a keepColors beállításban felsorolt színek minden területét.+@msg	Nothing was traced: the colors that match keepColors produce no paths.	Semmi sem lett vektorizálva: a keepColors beállításnak megfelelő színekből nem jön létre útvonal.+@msg	keepColors: none of the colors the trace found matches {c}, so nothing of it is traced.	keepColors: a vektorizálás által talált színek egyike sem felel meg ennek: {c}, így belőle semmi sem lesz vektorizálva. @msg	Flat Logo traced the image at {_1} × {_2} pixels, averaged down from {_3} × {_4}.	Az Egyszínű logó mód {_1} × {_2} képponton vektorizálta a képet, {_3} × {_4} méretről átlagolással kicsinyítve. 	Artboard {n} of {total}	Rajztábla: {n} / {total} 	File {n} of {total}	Fájl: {n} / {total}diff --git a/crates/ui-egui/src/i18n/it.tsv b/crates/ui-egui/src/i18n/it.tsv--- a/crates/ui-egui/src/i18n/it.tsv+++ b/crates/ui-egui/src/i18n/it.tsv@@ -5014,6 +5014,11 @@ lighting	Light	Luce 	Snap to Perpendicular	Aggancia alla perpendicolare @msg	unknown snap option `{key}` (use {options})	opzione di aggancio sconosciuta `{key}` (usare {options}) 	Gap Length:	Lunghezza lacuna:+@msg	Flat Logo mode does not use keepColors: it traces the colors named in logoColors, or the colors it finds.	La modalità Logo piatto non usa keepColors: vettorializza i colori indicati in logoColors o i colori che trova.+@msg	Nothing was traced: none of the colors the trace found matches keepColors ({_1}).	Non è stato vettorializzato nulla: nessuno dei colori trovati dalla vettorializzazione corrisponde a keepColors ({_1}).+@msg	Nothing was traced: Ignore Color leaves out every area of the colors keepColors lists.	Non è stato vettorializzato nulla: Ignora colore esclude ogni area dei colori elencati in keepColors.+@msg	Nothing was traced: the colors that match keepColors produce no paths.	Non è stato vettorializzato nulla: i colori che corrispondono a keepColors non producono tracciati.+@msg	keepColors: none of the colors the trace found matches {c}, so nothing of it is traced.	keepColors: nessuno dei colori trovati dalla vettorializzazione corrisponde a {c}, quindi non ne viene vettorializzato nulla. @msg	Flat Logo traced the image at {_1} × {_2} pixels, averaged down from {_3} × {_4}.	La modalità Logo piatto ha vettorializzato l’immagine a {_1} × {_2} pixel, ridotta per media da {_3} × {_4}. 	Artboard {n} of {total}	Tavola da disegno {n} di {total} 	File {n} of {total}	File {n} di {total}diff --git a/crates/ui-egui/src/i18n/ja.tsv b/crates/ui-egui/src/i18n/ja.tsv--- a/crates/ui-egui/src/i18n/ja.tsv+++ b/crates/ui-egui/src/i18n/ja.tsv@@ -5054,6 +5054,11 @@ lighting	Light	ライト 	Snap to Perpendicular	垂線にスナップ @msg	unknown snap option `{key}` (use {options})	不明なスナップオプション `{key}` ({options} を使用してください) 	Gap Length:	ギャップの長さ:+@msg	Flat Logo mode does not use keepColors: it traces the colors named in logoColors, or the colors it finds.	フラットロゴモードは keepColors を使いません。logoColors で指定したカラー、または検出したカラーをトレースします。+@msg	Nothing was traced: none of the colors the trace found matches keepColors ({_1}).	何もトレースされませんでした。トレースで検出したカラーのどれも keepColors ({_1}) に一致しません。+@msg	Nothing was traced: Ignore Color leaves out every area of the colors keepColors lists.	何もトレースされませんでした。「カラーを無視」が keepColors に指定したカラーの領域をすべて除外します。+@msg	Nothing was traced: the colors that match keepColors produce no paths.	何もトレースされませんでした。keepColors に一致するカラーからパスができません。+@msg	keepColors: none of the colors the trace found matches {c}, so nothing of it is traced.	keepColors: トレースで検出したカラーのどれも {c} に一致しないため、このカラーは何もトレースされません。 @msg	Flat Logo traced the image at {_1} × {_2} pixels, averaged down from {_3} × {_4}.	フラットロゴモードは画像を {_3} × {_4} から平均化して縮小した {_1} × {_2} ピクセルでトレースしました。 	Artboard {n} of {total}	アートボード {n} / {total} 	File {n} of {total}	ファイル {n} / {total}diff --git a/crates/ui-egui/src/i18n/ka.tsv b/crates/ui-egui/src/i18n/ka.tsv--- a/crates/ui-egui/src/i18n/ka.tsv+++ b/crates/ui-egui/src/i18n/ka.tsv@@ -5240,6 +5240,11 @@ imageTrace	Outline	კონტური @msg	Save writes {_1}, not {_2}: use file.saveAsTemplate to write it	„შენახვა“ წერს ფორმატს {_1} და არა {_2}: ამისთვის გამოიყენეთ file.saveAsTemplate @msg	The image is {_1} × {_2} pixels, more than {LARGE_TRACE_SIDE} on a side: it is traced, but more slowly. A lower-fidelity preset or a smaller copy of the image traces faster.	გამოსახულება არის {_1} × {_2} პიქსელი, ანუ ერთ მხარეზე {LARGE_TRACE_SIDE}-ზე მეტი: ის ვექტორიზდება, მაგრამ უფრო ნელა. ნაკლები სიზუსტის პრესეტი ან გამოსახულების უფრო პატარა ასლი უფრო სწრაფად ვექტორიზდება. @msg	The image was traced at {_1} × {_2} pixels, scaled down from {_3} × {_4} ({_5} megapixels at most).	გამოსახულება ვექტორიზდა {_1} × {_2} პიქსელზე, შემცირდა {_3} × {_4}-დან (მაქსიმუმ {_5} მეგაპიქსელი).+@msg	Flat Logo mode does not use keepColors: it traces the colors named in logoColors, or the colors it finds.	ბრტყელი ლოგოს რეჟიმი keepColors-ს არ იყენებს: ის ვექტორიზებს logoColors-ში მითითებულ ფერებს ან მის მიერ ნაპოვნ ფერებს.+@msg	Nothing was traced: none of the colors the trace found matches keepColors ({_1}).	არაფერი ვექტორიზდა: ვექტორიზაციის მიერ ნაპოვნი არცერთი ფერი არ შეესაბამება keepColors-ს ({_1}).+@msg	Nothing was traced: Ignore Color leaves out every area of the colors keepColors lists.	არაფერი ვექტორიზდა: ფერის იგნორირება გამოტოვებს keepColors-ში ჩამოთვლილი ფერების ყველა არეს.+@msg	Nothing was traced: the colors that match keepColors produce no paths.	არაფერი ვექტორიზდა: keepColors-ის შესაბამისი ფერებიდან არცერთი კონტური არ მიიღება.+@msg	keepColors: none of the colors the trace found matches {c}, so nothing of it is traced.	keepColors: ვექტორიზაციის მიერ ნაპოვნი არცერთი ფერი არ შეესაბამება {c}-ს, ამიტომ მისგან არაფერი ვექტორიზდება. @msg	Flat Logo traced the image at {_1} × {_2} pixels, averaged down from {_3} × {_4}.	ბრტყელი ლოგოს რეჟიმში გამოსახულება ვექტორიზდა {_1} × {_2} პიქსელზე, საშუალოს გამოთვლით შემცირდა {_3} × {_4}-დან. @msg	`{given}` isn't a library panel (see window.swatchLibrary)	`{given}` ბიბლიოთეკის პანელი არ არის (იხ. window.swatchLibrary) @msg	`{key}` must be a #rrggbb color	`{key}` უნდა იყოს #rrggbb ფერიdiff --git a/crates/ui-egui/src/i18n/ko.tsv b/crates/ui-egui/src/i18n/ko.tsv--- a/crates/ui-egui/src/i18n/ko.tsv+++ b/crates/ui-egui/src/i18n/ko.tsv@@ -4977,6 +4977,11 @@ lighting	Light	광원 	Snap to Perpendicular	수직선에 스냅 @msg	unknown snap option `{key}` (use {options})	알 수 없는 스냅 옵션 `{key}` ({options} 중에서 사용하세요) 	Gap Length:	간격 길이:+@msg	Flat Logo mode does not use keepColors: it traces the colors named in logoColors, or the colors it finds.	단색 로고 모드는 keepColors를 사용하지 않습니다. logoColors에 지정한 색상 또는 찾은 색상을 추적합니다.+@msg	Nothing was traced: none of the colors the trace found matches keepColors ({_1}).	아무것도 추적하지 않았습니다. 추적에서 찾은 색상 중 keepColors({_1})와 일치하는 색상이 없습니다.+@msg	Nothing was traced: Ignore Color leaves out every area of the colors keepColors lists.	아무것도 추적하지 않았습니다. 색상 무시가 keepColors에 나열된 색상의 모든 영역을 제외합니다.+@msg	Nothing was traced: the colors that match keepColors produce no paths.	아무것도 추적하지 않았습니다. keepColors와 일치하는 색상에서 패스가 만들어지지 않습니다.+@msg	keepColors: none of the colors the trace found matches {c}, so nothing of it is traced.	keepColors: 추적에서 찾은 색상 중 {c}와 일치하는 색상이 없어 이 색상은 추적하지 않습니다. @msg	Flat Logo traced the image at {_1} × {_2} pixels, averaged down from {_3} × {_4}.	단색 로고 모드에서 이미지를 {_3} × {_4}에서 평균을 내어 축소한 {_1} × {_2}픽셀로 추적했습니다. 	Artboard {n} of {total}	아트보드 {n} / {total} 	File {n} of {total}	파일 {n} / {total}diff --git a/crates/ui-egui/src/i18n/nl.tsv b/crates/ui-egui/src/i18n/nl.tsv--- a/crates/ui-egui/src/i18n/nl.tsv+++ b/crates/ui-egui/src/i18n/nl.tsv@@ -5384,6 +5384,11 @@ swatch	Bright Violet	Felviolet @msg	too many library panels were opened in this session	er zijn in deze sessie te veel bibliotheekpanelen geopend @msg	The image is {_1} × {_2} pixels, more than {LARGE_TRACE_SIDE} on a side: it is traced, but more slowly. A lower-fidelity preset or a smaller copy of the image traces faster.	De afbeelding is {_1} × {_2} pixels, meer dan {LARGE_TRACE_SIDE} aan één zijde: ze wordt getraceerd, maar langzamer. Een voorinstelling met minder detail of een kleinere kopie van de afbeelding traceert sneller. @msg	The image was traced at {_1} × {_2} pixels, scaled down from {_3} × {_4} ({_5} megapixels at most).	De afbeelding is getraceerd op {_1} × {_2} pixels, verkleind van {_3} × {_4} (maximaal {_5} megapixels).+@msg	Flat Logo mode does not use keepColors: it traces the colors named in logoColors, or the colors it finds.	De modus Plat logo gebruikt keepColors niet: hij traceert de kleuren die in logoColors staan, of de kleuren die hij vindt.+@msg	Nothing was traced: none of the colors the trace found matches keepColors ({_1}).	Er is niets getraceerd: geen van de kleuren die het traceren vond, komt overeen met keepColors ({_1}).+@msg	Nothing was traced: Ignore Color leaves out every area of the colors keepColors lists.	Er is niets getraceerd: Kleur negeren laat elk gebied van de kleuren in keepColors weg.+@msg	Nothing was traced: the colors that match keepColors produce no paths.	Er is niets getraceerd: de kleuren die overeenkomen met keepColors leveren geen paden op.+@msg	keepColors: none of the colors the trace found matches {c}, so nothing of it is traced.	keepColors: geen van de kleuren die het traceren vond, komt overeen met {c}, dus daarvan wordt niets getraceerd. @msg	Flat Logo traced the image at {_1} × {_2} pixels, averaged down from {_3} × {_4}.	De modus Plat logo heeft de afbeelding getraceerd op {_1} × {_2} pixels, door middeling verkleind van {_3} × {_4}. 	Artboard {n} of {total}	Tekengebied {n} van {total} 	File {n} of {total}	Bestand {n} van {total}diff --git a/crates/ui-egui/src/i18n/pl.tsv b/crates/ui-egui/src/i18n/pl.tsv--- a/crates/ui-egui/src/i18n/pl.tsv+++ b/crates/ui-egui/src/i18n/pl.tsv@@ -5253,6 +5253,11 @@ mark-style	Japanese	Japońskie 	Snap to Perpendicular	Przyciągaj do prostopadłych @msg	unknown snap option `{key}` (use {options})	nieznana opcja przyciągania `{key}` (użyj: {options}) 	Gap Length:	Długość przerwy:+@msg	Flat Logo mode does not use keepColors: it traces the colors named in logoColors, or the colors it finds.	Tryb Płaskie logo nie używa keepColors: obrysowuje kolory podane w logoColors lub kolory, które znajdzie.+@msg	Nothing was traced: none of the colors the trace found matches keepColors ({_1}).	Nic nie zostało obrysowane: żaden z kolorów znalezionych podczas obrysowania nie pasuje do keepColors ({_1}).+@msg	Nothing was traced: Ignore Color leaves out every area of the colors keepColors lists.	Nic nie zostało obrysowane: Ignoruj kolor pomija każdy obszar kolorów wymienionych w keepColors.+@msg	Nothing was traced: the colors that match keepColors produce no paths.	Nic nie zostało obrysowane: kolory pasujące do keepColors nie dają żadnych ścieżek.+@msg	keepColors: none of the colors the trace found matches {c}, so nothing of it is traced.	keepColors: żaden z kolorów znalezionych podczas obrysowania nie pasuje do {c}, więc nic z niego nie jest obrysowywane. @msg	Flat Logo traced the image at {_1} × {_2} pixels, averaged down from {_3} × {_4}.	Tryb Płaskie logo obrysował obraz w rozdzielczości {_1} × {_2} pikseli, po zmniejszeniu przez uśrednienie z {_3} × {_4}. 	Artboard {n} of {total}	Obszar kompozycji {n} z {total} 	File {n} of {total}	Plik {n} z {total}diff --git a/crates/ui-egui/src/i18n/pt-br.tsv b/crates/ui-egui/src/i18n/pt-br.tsv--- a/crates/ui-egui/src/i18n/pt-br.tsv+++ b/crates/ui-egui/src/i18n/pt-br.tsv@@ -5318,6 +5318,11 @@ mark-style	Japanese	Japonesas 	Snap to Perpendicular	Ajustar à Perpendicular @msg	unknown snap option `{key}` (use {options})	opção de ajuste desconhecida `{key}` (use {options}) 	Gap Length:	Comprimento da Lacuna:+@msg	Flat Logo mode does not use keepColors: it traces the colors named in logoColors, or the colors it finds.	O modo Logotipo Plano não usa keepColors: ele traça as cores indicadas em logoColors ou as cores que encontra.+@msg	Nothing was traced: none of the colors the trace found matches keepColors ({_1}).	Nada foi traçado: nenhuma das cores que o traçado encontrou corresponde a keepColors ({_1}).+@msg	Nothing was traced: Ignore Color leaves out every area of the colors keepColors lists.	Nada foi traçado: Ignorar Cor deixa de fora todas as áreas das cores listadas em keepColors.+@msg	Nothing was traced: the colors that match keepColors produce no paths.	Nada foi traçado: as cores que correspondem a keepColors não geram nenhum caminho.+@msg	keepColors: none of the colors the trace found matches {c}, so nothing of it is traced.	keepColors: nenhuma das cores que o traçado encontrou corresponde a {c}, então nada dela é traçado. @msg	Flat Logo traced the image at {_1} × {_2} pixels, averaged down from {_3} × {_4}.	O modo Logotipo Plano traçou a imagem em {_1} × {_2} pixels, reduzida por média de {_3} × {_4}. 	Artboard {n} of {total}	Mesa de Trabalho {n} de {total} 	File {n} of {total}	Arquivo {n} de {total}diff --git a/crates/ui-egui/src/i18n/ru.tsv b/crates/ui-egui/src/i18n/ru.tsv--- a/crates/ui-egui/src/i18n/ru.tsv+++ b/crates/ui-egui/src/i18n/ru.tsv@@ -5010,6 +5010,11 @@ lighting	Light	Свет 	Snap to Perpendicular	Привязать к перпендикуляру @msg	unknown snap option `{key}` (use {options})	неизвестный параметр привязки `{key}` (допустимо: {options}) 	Gap Length:	Длина зазора:+@msg	Flat Logo mode does not use keepColors: it traces the colors named in logoColors, or the colors it finds.	Режим «Плоский логотип» не использует keepColors: он трассирует цвета, указанные в logoColors, или найденные им цвета.+@msg	Nothing was traced: none of the colors the trace found matches keepColors ({_1}).	Ничего не трассировано: ни один из найденных при трассировке цветов не соответствует keepColors ({_1}).+@msg	Nothing was traced: Ignore Color leaves out every area of the colors keepColors lists.	Ничего не трассировано: «Игнорировать цвет» исключает все области цветов, перечисленных в keepColors.+@msg	Nothing was traced: the colors that match keepColors produce no paths.	Ничего не трассировано: цвета, соответствующие keepColors, не дают ни одного контура.+@msg	keepColors: none of the colors the trace found matches {c}, so nothing of it is traced.	keepColors: ни один из найденных при трассировке цветов не соответствует {c}, поэтому из него ничего не трассируется. @msg	Flat Logo traced the image at {_1} × {_2} pixels, averaged down from {_3} × {_4}.	Режим «Плоский логотип» трассировал изображение в размере {_1} × {_2} пикселей, уменьшенное усреднением с {_3} × {_4}. 	Artboard {n} of {total}	Монтажная область {n} из {total} 	File {n} of {total}	Файл {n} из {total}diff --git a/crates/ui-egui/src/i18n/sv.tsv b/crates/ui-egui/src/i18n/sv.tsv--- a/crates/ui-egui/src/i18n/sv.tsv+++ b/crates/ui-egui/src/i18n/sv.tsv@@ -5424,6 +5424,11 @@ swatch	Yellow Green	Gulgrön 	Snap to Perpendicular	Fäst mot vinkelrät @msg	unknown snap option `{key}` (use {options})	okänt fästalternativ `{key}` (använd {options}) 	Gap Length:	Lucklängd:+@msg	Flat Logo mode does not use keepColors: it traces the colors named in logoColors, or the colors it finds.	Läget Platt logotyp använder inte keepColors: det vektoriserar färgerna som anges i logoColors eller färgerna det hittar.+@msg	Nothing was traced: none of the colors the trace found matches keepColors ({_1}).	Ingenting vektoriserades: ingen av färgerna som vektoriseringen hittade motsvarar keepColors ({_1}).+@msg	Nothing was traced: Ignore Color leaves out every area of the colors keepColors lists.	Ingenting vektoriserades: Ignorera färg utelämnar alla områden med färgerna som keepColors anger.+@msg	Nothing was traced: the colors that match keepColors produce no paths.	Ingenting vektoriserades: färgerna som matchar keepColors ger inga banor.+@msg	keepColors: none of the colors the trace found matches {c}, so nothing of it is traced.	keepColors: ingen av färgerna som vektoriseringen hittade motsvarar {c}, så ingenting av den vektoriseras. @msg	Flat Logo traced the image at {_1} × {_2} pixels, averaged down from {_3} × {_4}.	Läget Platt logotyp vektoriserade bilden vid {_1} × {_2} pixlar, nedskalad genom medelvärdesbildning från {_3} × {_4}. 	Artboard {n} of {total}	Rityta {n} av {total} 	File {n} of {total}	Fil {n} av {total}diff --git a/crates/ui-egui/src/i18n/tests.rs b/crates/ui-egui/src/i18n/tests.rs--- a/crates/ui-egui/src/i18n/tests.rs+++ b/crates/ui-egui/src/i18n/tests.rs@@ -1390,6 +1390,42 @@ fn width(p: &Value, cmd: &str, key: &str) -> Result<f64> {     assert_eq!(c.message("`width` must be a number").map(|m| m.1), Some(vec![("key", "width")])); } +/// The warnings of an Image Trace with keepColors, which the status bar shows and the scan above+/// doesn't read, are translated in each language of [`COMPLETE_MESSAGES`], with the colors kept.+#[test]+fn the_keep_colors_trace_warnings_are_translated() {+    use serde_json::json;+    let mut s = vectorcraft_engine::Session::new();+    s.execute("file.new", &json!({"width": 100, "height": 100})).unwrap();+    // A white field with a black square in it.+    let img = image::RgbaImage::from_fn(40, 40, |x, y| {+        image::Rgba(if (10..30).contains(&x) && (10..30).contains(&y) { [0, 0, 0, 255] } else { [255, 255, 255, 255] })+    });+    let mut png = vec![];+    img.write_to(&mut std::io::Cursor::new(&mut png), image::ImageFormat::Png).unwrap();+    let data = vectorcraft_format::base64_encode(&png);+    let mut warnings = vec![];+    for (params, shown) in [+        (json!({"mode": "color", "palette": "limited", "colors": 2, "keepColors": ["#ff00ff", "#00ffff"]}), "#ff00ff, #00ffff"),+        (json!({"mode": "color", "palette": "limited", "colors": 2, "keepColors": ["#000000", "#00ffff"]}), "#00ffff"),+        (json!({"mode": "color", "palette": "limited", "colors": 2, "keepColors": ["#000000"], "ignoreWhite": true, "ignoreColor": "#000000"}), ""),+        (json!({"mode": "logo", "keepColors": ["#000000"]}), ""),+    ] {+        s.execute("file.place", &json!({"name": "square.png", "dataBase64": data, "link": false})).unwrap();+        let r = s.execute("imageTrace.makeAndExpand", &json!({ "params": params })).unwrap();+        let w = r["warnings"].as_array().and_then(|w| w.first()).and_then(|w| w.as_str()).unwrap_or_else(|| panic!("{params}: {r}"));+        warnings.push((w.to_string(), shown));+    }+    // Listed colors that were traced and produced no paths (no square image gives that).+    warnings.push(("Nothing was traced: the colors that match keepColors produce no paths.".to_string(), ""));+    for (warning, shown) in &warnings {+        for code in COMPLETE_MESSAGES {+            let tr = message(Lang::from_code(code).unwrap(), warning);+            assert!(tr != *warning && tr.contains(shown) && tr.contains("keepColors"), "{code}: {tr}");+        }+    }+}+ /// The parameter errors of the blend spine commands (Move Spine Point, Reshape Spine Segment, Add /// Spine Point and Delete Spine Point, which Direct Selection, the Pen and the Anchor Point tool /// send) say what is wrong.diff --git a/crates/ui-egui/src/i18n/tr.tsv b/crates/ui-egui/src/i18n/tr.tsv--- a/crates/ui-egui/src/i18n/tr.tsv+++ b/crates/ui-egui/src/i18n/tr.tsv@@ -5395,6 +5395,11 @@ theme	Appearance	Görünüm @msg	The image is {_1} × {_2} pixels, more than {LARGE_TRACE_SIDE} on a side: it is traced, but more slowly. A lower-fidelity preset or a smaller copy of the image traces faster.	Görüntü {_1} × {_2} piksel, bir kenarı {LARGE_TRACE_SIDE} değerinden büyük: izlenir, ancak daha yavaş. Daha düşük doğruluklu bir ön ayar veya görüntünün daha küçük bir kopyası daha hızlı izlenir. @msg	The image was traced at {_1} × {_2} pixels, scaled down from {_3} × {_4} ({_5} megapixels at most).	Görüntü {_3} × {_4} boyutundan küçültülerek {_1} × {_2} pikselde izlendi (en fazla {_5} megapiksel). 	Gap Length:	Boşluk Uzunluğu:+@msg	Flat Logo mode does not use keepColors: it traces the colors named in logoColors, or the colors it finds.	Düz Logo modu keepColors kullanmaz: logoColors içinde adı verilen renkleri veya bulduğu renkleri izler.+@msg	Nothing was traced: none of the colors the trace found matches keepColors ({_1}).	Hiçbir şey izlenmedi: izlemenin bulduğu renklerin hiçbiri keepColors ({_1}) ile eşleşmiyor.+@msg	Nothing was traced: Ignore Color leaves out every area of the colors keepColors lists.	Hiçbir şey izlenmedi: Rengi Yoksay, keepColors içinde listelenen renklerin tüm alanlarını dışarıda bırakıyor.+@msg	Nothing was traced: the colors that match keepColors produce no paths.	Hiçbir şey izlenmedi: keepColors ile eşleşen renkler hiçbir yol oluşturmuyor.+@msg	keepColors: none of the colors the trace found matches {c}, so nothing of it is traced.	keepColors: izlemenin bulduğu renklerin hiçbiri {c} ile eşleşmiyor, bu yüzden bu renkten hiçbir şey izlenmiyor. @msg	Flat Logo traced the image at {_1} × {_2} pixels, averaged down from {_3} × {_4}.	Düz Logo modu görüntüyü {_3} × {_4} boyutundan ortalama alarak küçültüp {_1} × {_2} pikselde izledi. @plural	{n} more export waiting|{n} more exports waiting	{n} dışa aktarma daha bekliyor|{n} dışa aktarma daha bekliyor @msg	Export cancelled	Dışa aktarma iptal edildidiff --git a/crates/ui-egui/src/i18n/uk.tsv b/crates/ui-egui/src/i18n/uk.tsv--- a/crates/ui-egui/src/i18n/uk.tsv+++ b/crates/ui-egui/src/i18n/uk.tsv@@ -4994,6 +4994,11 @@ lighting	Light	Світло 	Snap to Perpendicular	Прив’язати до перпендикуляра @msg	unknown snap option `{key}` (use {options})	невідомий параметр прив’язки `{key}` (використовуйте {options}) 	Gap Length:	Довжина проміжку:+@msg	Flat Logo mode does not use keepColors: it traces the colors named in logoColors, or the colors it finds.	Режим «Плоский логотип» не використовує keepColors: він трасує кольори, вказані в logoColors, або знайдені ним кольори.+@msg	Nothing was traced: none of the colors the trace found matches keepColors ({_1}).	Нічого не трасовано: жоден із кольорів, знайдених під час трасування, не відповідає keepColors ({_1}).+@msg	Nothing was traced: Ignore Color leaves out every area of the colors keepColors lists.	Нічого не трасовано: «Ігнорувати колір» пропускає всі області кольорів, перелічених у keepColors.+@msg	Nothing was traced: the colors that match keepColors produce no paths.	Нічого не трасовано: кольори, що відповідають keepColors, не дають жодного контуру.+@msg	keepColors: none of the colors the trace found matches {c}, so nothing of it is traced.	keepColors: жоден із кольорів, знайдених під час трасування, не відповідає {c}, тому з нього нічого не трасується. @msg	Flat Logo traced the image at {_1} × {_2} pixels, averaged down from {_3} × {_4}.	Режим «Плоский логотип» трасував зображення в розмірі {_1} × {_2} пікселів, зменшене усередненням з {_3} × {_4}. 	Artboard {n} of {total}	Монтажна область {n} з {total} 	File {n} of {total}	Файл {n} з {total}diff --git a/crates/ui-egui/src/i18n/vi.tsv b/crates/ui-egui/src/i18n/vi.tsv--- a/crates/ui-egui/src/i18n/vi.tsv+++ b/crates/ui-egui/src/i18n/vi.tsv@@ -4994,6 +4994,11 @@ theme	Appearance	Giao diện 	Snap to Perpendicular	Hút vào đường vuông góc @msg	unknown snap option `{key}` (use {options})	tùy chọn hút không hợp lệ `{key}` (dùng {options}) 	Gap Length:	Độ dài khoảng hở:+@msg	Flat Logo mode does not use keepColors: it traces the colors named in logoColors, or the colors it finds.	Chế độ Logo phẳng không dùng keepColors: chế độ này dò các màu được nêu trong logoColors hoặc các màu nó tìm thấy.+@msg	Nothing was traced: none of the colors the trace found matches keepColors ({_1}).	Không có gì được dò: không màu nào mà quá trình dò tìm thấy khớp với keepColors ({_1}).+@msg	Nothing was traced: Ignore Color leaves out every area of the colors keepColors lists.	Không có gì được dò: Bỏ qua màu bỏ đi mọi vùng của các màu mà keepColors liệt kê.+@msg	Nothing was traced: the colors that match keepColors produce no paths.	Không có gì được dò: các màu khớp với keepColors không tạo ra đường dẫn nào.+@msg	keepColors: none of the colors the trace found matches {c}, so nothing of it is traced.	keepColors: không màu nào mà quá trình dò tìm thấy khớp với {c}, nên không có gì của màu đó được dò. @msg	Flat Logo traced the image at {_1} × {_2} pixels, averaged down from {_3} × {_4}.	Chế độ Logo phẳng đã dò ảnh ở {_1} × {_2} điểm ảnh, thu nhỏ bằng cách lấy trung bình từ {_3} × {_4}. 	Artboard {n} of {total}	Bảng vẽ {n} / {total} 	File {n} of {total}	Tệp {n} / {total}diff --git a/crates/ui-egui/src/i18n/zh-hans.tsv b/crates/ui-egui/src/i18n/zh-hans.tsv--- a/crates/ui-egui/src/i18n/zh-hans.tsv+++ b/crates/ui-egui/src/i18n/zh-hans.tsv@@ -5233,6 +5233,11 @@ lighting	Light	光源 	Snap to Perpendicular	对齐垂线 @msg	unknown snap option `{key}` (use {options})	未知的对齐选项 `{key}`(可用:{options}) 	Gap Length:	间隙长度:+@msg	Flat Logo mode does not use keepColors: it traces the colors named in logoColors, or the colors it finds.	“扁平徽标”模式不使用 keepColors:它描摹 logoColors 中指定的颜色,或它找到的颜色。+@msg	Nothing was traced: none of the colors the trace found matches keepColors ({_1}).	未描摹任何内容:描摹找到的颜色中没有一种与 keepColors ({_1}) 匹配。+@msg	Nothing was traced: Ignore Color leaves out every area of the colors keepColors lists.	未描摹任何内容:“忽略颜色”排除了 keepColors 所列颜色的所有区域。+@msg	Nothing was traced: the colors that match keepColors produce no paths.	未描摹任何内容:与 keepColors 匹配的颜色未生成任何路径。+@msg	keepColors: none of the colors the trace found matches {c}, so nothing of it is traced.	keepColors:描摹找到的颜色中没有一种与 {c} 匹配,因此该颜色未被描摹。 @msg	Flat Logo traced the image at {_1} × {_2} pixels, averaged down from {_3} × {_4}.	“扁平徽标”模式已按 {_1} × {_2} 像素描摹图像,由 {_3} × {_4} 平均缩小而来。 # Links and Attributes: files and URLs handed to other apps. @msg	Edit Original opens image and document files only; “{name}” is not one	编辑原稿只能打开图像和文档文件;“{name}” 不是其中之一diff --git a/crates/ui-egui/src/i18n/zh-hant.tsv b/crates/ui-egui/src/i18n/zh-hant.tsv--- a/crates/ui-egui/src/i18n/zh-hant.tsv+++ b/crates/ui-egui/src/i18n/zh-hant.tsv@@ -2377,6 +2377,11 @@ file-filter	Templates	範本 @msg	too many library panels were opened in this session	本次工作階段開啟的資料庫面板過多 @msg	The image is {_1} × {_2} pixels, more than {LARGE_TRACE_SIDE} on a side: it is traced, but more slowly. A lower-fidelity preset or a smaller copy of the image traces faster.	影像為 {_1} × {_2} 像素,有一邊超過 {LARGE_TRACE_SIDE}:仍會描圖,但速度較慢。使用精確度較低的預設集或較小的影像拷貝,描圖會較快。 @msg	The image was traced at {_1} × {_2} pixels, scaled down from {_3} × {_4} ({_5} megapixels at most).	影像已以 {_1} × {_2} 像素描圖,由 {_3} × {_4} 縮小而來 (最多 {_5} 百萬像素)。+@msg	Flat Logo mode does not use keepColors: it traces the colors named in logoColors, or the colors it finds.	「扁平標誌」模式不使用 keepColors:它會描圖 logoColors 中指定的顏色,或它找到的顏色。+@msg	Nothing was traced: none of the colors the trace found matches keepColors ({_1}).	未描圖任何內容:描圖找到的顏色中沒有一種與 keepColors ({_1}) 相符。+@msg	Nothing was traced: Ignore Color leaves out every area of the colors keepColors lists.	未描圖任何內容:「忽略顏色」排除了 keepColors 所列顏色的所有區域。+@msg	Nothing was traced: the colors that match keepColors produce no paths.	未描圖任何內容:與 keepColors 相符的顏色未產生任何路徑。+@msg	keepColors: none of the colors the trace found matches {c}, so nothing of it is traced.	keepColors:描圖找到的顏色中沒有一種與 {c} 相符,因此該顏色未被描圖。 @msg	Flat Logo traced the image at {_1} × {_2} pixels, averaged down from {_3} × {_4}.	「扁平標誌」模式已以 {_1} × {_2} 像素描圖,由 {_3} × {_4} 平均縮小而來。 # Links and Attributes: files and URLs handed to other apps. @msg	Edit Original opens image and document files only; “{name}” is not one	「編輯原稿」只能開啟影像與文件檔案;「{name}」不屬於這兩類diff --git a/docs/image-trace-adjustments.md b/docs/image-trace-adjustments.md--- a/docs/image-trace-adjustments.md+++ b/docs/image-trace-adjustments.md@@ -32,6 +32,20 @@ white beside a cream), at most six; scattered specks of a colour add none. Adobe its own palettes are chosen; this is VectorCraft's own method (see [image-trace-quality.md](image-trace-quality.md#auto-color-keeps-rare-colours)). +**Keep Colors.** `"keepColors": ["#rrggbb", …]` in the params of `imageTrace.make` or+`imageTrace.makeAndExpand` traces only the palette colors within about 30 levels of a listed color,+the distance Ignore Color matches by, and leaves the other colors out. With Method Abutting, the paths+of the kept colors are the same as in a trace of every color. With Method Overlapping, a kept color's+paths reach under the kept colors above it in the stack and not under the colors left out, as with+Ignore Color. An empty list traces every color, and a value that is not a six-digit hex color is+refused. With Ignore Color as well, a kept color that Ignore Color matches is still left out. Flat+Logo does not use `keepColors`: it traces the colors named in `logoColors`, or the colors it finds.+The reply's `warnings` name each listed color that matches none of the colors the trace found. When+nothing is traced because no listed color matches, because Ignore Color leaves out every area of the+ones that match, or because the ones that match produce no paths, the first warning says so, and the+status bar shows it. In Flat Logo mode the+warning says that `keepColors` is not used.+ **Abutting and Overlapping.** Abutting is the default Method, and the Default, Auto Color, 3 Colors, 6 Colors and 16 Colors presets use it. With Abutting, two neighboring colors share one exact edge. The canvas smooths the edges of each path on its own, so each of the two paths covers only part of thediff --git a/docs/mcp.md b/docs/mcp.md--- a/docs/mcp.md+++ b/docs/mcp.md@@ -1504,6 +1504,8 @@ already walks Image Trace. Short agent entry points: - Image Trace: `imageTrace.presets`, `imageTrace.make {id?, preset?, params?, view?}` (or `imageTrace.makeAndExpand`),   `imageTrace.setView`, `imageTrace.expand`, `imageTrace.release`. A key inside `params` that is not one of the trace   settings, such as `colours`, is an error that names it and the nearest setting (`colors`), as for a command's own keys.+  With `keepColors` in `params`, the reply's `warnings` name each listed color that no traced color matches, and say+  when nothing was traced. - Live Paint: `livePaint.make`, `livePaint.merge`, `livePaint.fill {group?, point}`, `livePaint.strokeEdge`,   `livePaint.info {group}`, `livePaint.release`, `livePaint.expand`, and `livePaint.gapOptions {ids?, detect?, size?,   color?, closeGaps?}` (Object › Live Paint › Gap Options). With `detect: true`, paint stops at the gaps that rundiff --git a/docs/target-app-parity.md b/docs/target-app-parity.md--- a/docs/target-app-parity.md+++ b/docs/target-app-parity.md@@ -802,6 +802,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur - **Image Trace, measured:** `crates/trace/tests/quality` traces drawings made from known vector art with every preset and [`image-trace-quality.md`](image-trace-quality.md) records the numbers against the baseline. Black and White, Grayscale and Color place each edge by the blend of the pixels across it (to a fraction of a pixel on anti-aliased and blurred images; hard-edged images are read as staircases) and fit outlines as Flat Logo does. *Measured* on each preset's own inputs, all sizes and variants: Default's outlines went from 0.31 px off on average to 0.09 (largest 1.51 to 1.02), Black and White Logo's from 0.27 to 0.06 with 0.3 kinks per trace where it had 15, and the gaps between abutting paths from 64 px to 22. On clean curved shapes Black and White Logo is 0.02 px off (the open tracer measured beside it: 0.20). The palette is made of the picture's own colours (blends along edges take none of it, colours that noise made of one colour merge, a blend pixel goes to one of the two colours it lies between; #1504): 16 Colors on flat art, all variants, went from 70 paths to 4.2 and from 5.1 px off to 0.13, with no path left in a colour the art doesn't have (5.9 before). Photos are smoothed within their tones before they are quantised and stacked layers go by lightness (#1516): High Fidelity Photo went from 3,653 paths to 121 with its colour error down from 2.30 to 1.75, and a 16 megapixel photo-like image is traced in 3.5 s where 4 megapixels were refused. Since then, by the same harness: outlines are cut at their junctions and each shared piece is fitted once (#1535: gaps and overlaps between abutting paths 22 to 50 px of area down to under 2); layers are traced within the box of their pixels and a few at a time (#1546: a 16 megapixel photo 3.9 GB to 0.9 GB); black and white are read against the image's own black and the paper near each edge (#1563, for thin lines in blurred and scanned art); Ignore Color and the Auto Color and Outline presets (#1574); Snap Curves To Lines levels lines within 2.5 degrees of level or upright (#1593, the angle our choice); Create: Strokes puts centre lines in the middle of their ink and makes them as wide as it (#1595: Outline 0.295 to 0.052 px from the true centre line, 18.4 to 2.7 anchors a stroke); sharp corners whose pixel is mostly outside the shape stay corners (#1609: Default 69% to 76% kept). Three options, each off unless asked for as the legacy presets have them: Shapes (#1609: 57 to 62% of plain circles, ellipses, rectangles and regular polygons come out as live shapes, rounded rectangles rarely), Auto Grouping (#1618: paths grouped with what lies inside them, the same picture) and Gradients (#1627: linear gradients traced as one gradient-filled path; on gradient inputs 6 Colors' colour error 2.12 to 0.62 with 6.4 to 3.4 paths, none found in flat art). Snap Curves To Lines moves no end of a line by more than 1.6 px, so long lines a degree or two off stay as the image has them (#1647: Technical Drawing's largest distance 4.21 px to 1.46). Flat Logo takes the tones a scan's uneven paper makes of one colour for one colour (#1657: on scans 470 paths and 21 px off to 8.5 paths and 0.09 px). Shapes finds rounded rectangles and shapes in aliased, blurred and scanned art (#1845: 57 to 62% of the plain shapes up to 98 to 100%, no false shape but one line 2 px wide). Stroked lines get square caps where their ink is cut square, and a wide end that turned towards a corner goes back on its line (#1846: Outline's largest distance 2.94 px to 0.88, 14 anchors to 10). Each of the 14 documented presets has an enhanced version with Auto Grouping and, in all but High Fidelity Photo, Shapes (#1847), measured beside its preset: equal or better within 0.005 px and 1% of edge ΔE on every preset's own inputs (Default 0.091 to 0.083 px); Auto Grouping keeps traced order in its groups, which removed a light seam along the counters of letters. Gradients grows a gradient over the pixels noise and soft rims took off it (#1848: blurred and noisy linear gradients 3 to 11 paths beside the gradient down to 0.5 with 6 Colors, 24 to 56 down to 0.5 to 1.5 with High Fidelity Photo). Silhouettes is 0.52 px off because its threshold is 200 (0.11 px at 128). Still weak, by the same numbers: rounded rectangles and other line-and-arc outlines take 12 to 16 anchors (the fitter does not split lines from arcs), blurred and scanned polygons lose corners, hairlines under half a pixel are lost to the threshold, aliased art is about 0.25 px off, a scan's uneven paper splits into tones when more colours are asked for than the art has, stroke junctions stay at their skeleton pixels, a three-stop gradient in a small or scanned disc is not found, radial gradients stay bands (a draft, #1849, traces them; the documented option is for linear ones, so it waits for a decision), near-regular polygons a few dozen pixels across are made regular with Shapes (up to 0.5 px off), and aliased stroked lines are 0.8 px off at worst. - **Gradient mesh from an image:** Object › Create Gradient Mesh (`object.mesh.create`) on an embedded image makes a mesh of 1 to 50 rows and columns over the picture's rectangle, placed with the image's transform; each point takes the alpha-weighted average colour and the opacity of the area around it, and Appearance and Highlight apply. A linked image is refused with a message to embed it (#1676). Scores and hours unchanged. - **Image Trace Flat Logo mode:** a fourth mode beside Black and White, Grayscale and Color, for flat-colour logos. Each pixel is read as a blend of the logo's colours (found automatically or named), the blend is interpolated smoothly and every outline is placed where two colours are equally present, so edges are accurate to a fraction of a pixel. Outlines are then cleaned: corners the blur rounded are put back, near-straight runs and near-circular arcs become exact lines and arcs, the rest is fitted with as few Béziers as the tolerance allows, touching colours overlap by a hair so no gap opens, and an outline that strays from the raw contour is fitted plainly instead. Gradients and photos are refused with advice to use Color. Line art whose only solid area is white paper (strokes a pixel or two wide) takes its colours from the strokes' cores, with the lighter shades along a stroke's anti-aliased edge folded into its ink; a page whose ink (a channel at least 48 below white) covers under 1% of it traces to nothing, as blank paper does. *Measured* (a local experiment, not run in CI): a flat-colour logo's vector original was rendered down to a soft 199 × 100 px transparent PNG, traced, and the result compared with the original at 1600 px wide. The built-in colour presets (6 Colors, 16 Colors, High Fidelity Photo, Low Fidelity Photo) had a mean edge error of 2.5 to 3.4 px and region overlap of 0.88 to 0.91; Flat Logo had 0.78 px and 0.966 (0.48 px and 0.975 from a crisp source), in about 0.1 s. Limits: flat-colour art only; needle-thin sharp tips are shortened by about 1.7 source pixels because the blur erases them; it works at no more than 1024 px on the long side (larger images are averaged down first); only one logo family has been scored against a vector original.+- **Image Trace, Keep Colors:** `"keepColors": ["#rrggbb", …]` traces only the palette colors near a listed color (Abutting: each with the paths the trace of every color gives it; Overlapping: not reaching under the colors left out), in Black and White, Grayscale and Color; code-drawn tests in `crates/trace` and `crates/engine`. - **Advanced art:** live Blends (steps/distance/smooth colour, editable spine, anchor-targeted Blend tool, faithful interpolation, knockout), Envelope Distort (warp/mesh/top object, Reset, full Envelope Options, mesh handles, Edit Contents; type, images, symbols, gradients, patterns and appearance distorted everywhere), Liquify tools with options and pen pressure, Puppet Warp with rotating pins, Perspective Grid with presets, movable planes and projected type (choosing a perspective tool shows the grid; Hide Grid hides it with the tool still chosen), Gradient Mesh, Shape Builder (stroke-only open paths and lines cut the regions they cross and enclose regions of their own, and regions they bound merge without seams where the lines cross at rounded coordinates (#893); filled open paths count as closed; erase mode while Alt/Option is held: a minus pointer, red highlights of what will go, a click or drag deletes the regions touched, the pieces of open lines and the edges (outline pieces between the points where paths meet, opening their paths there) it passes over, one undo step, `shapeBuilder.merge {erase}` and `shapeBuilder.regions` edges over MCP), Live Paint (every path an edge, open lines included), Image Trace (12 presets, colour layers traced in parallel; a trace past two million anchor points is refused with advice rather than running out of memory, an image over 64 megapixels is traced scaled down to that size (aspect kept, paths at the image's own size) and one over 192 megapixels is refused, and a selection too large to outline shows its bounds (#525); Object › Image Trace › Make and Make and Expand trace at once with the Default preset, #543; with an image selected the Control bar and the Properties panel show Image Trace, whose arrow traces with any preset, Mask (`object.maskImage`: a clipping path on the image's outline, selected so its handles crop), Crop Image (a crop box on the image, starting on its part over the artboard, dragged by its handles or inside it, the outside dimmed, with X/Y/W/H, Apply and Cancel in the Control bar, Enter and Escape, #734; `object.cropImage {rect}` for agents, `{trim: true}` cuts an image to its pixels that aren't fully transparent, pixel for pixel) and Embed or Unembed in place of Fill and Stroke; with an Image Trace object selected, its Preset (choosing another traces again), View, the Image Trace panel and Expand, plus Release in Properties; the five View modes (Tracing Result, with Outlines, Outlines, Outlines with Source Image, Source Image) redraw a trace on screen without tracing again, are saved with it and `imageTrace.setView` sets them, while exports and Expand keep the result; the panel's Advanced section is a header that stays open or closed as it was left (part of #641); Color mode's Palette: Limited, Full Tone (as many colours as the tones need, Less … More), Automatic (flat art with its own colours, else Full Tone; a heuristic) and Document Library (the most used colours of the document's swatches or a swatch library, filled with the swatch colours as they are), part of #641; Advanced › Create: Fills and/or Strokes, where lines up to the Stroke width trace as stroked centre lines (distance transform, thinning, a skeleton graph with spurs pruned, curve fitting; one width per line; without Fills, wider areas become stroked outlines), part of #641), pattern swatches with pattern editing mode, live Repeat (radial/grid/mirror). A single selected mirror repeat shows a prominent axis with a center move handle and two rotation handles. Drag the line or center to change spacing, or an end to change angle; Shift snaps rotation to 45°. The original artwork stays in place, the reflected copy previews live, Escape cancels and each drag takes one undo step through `object.repeat.options`. Controls follow zoom/view rotation and hide with edges or the bounding box. Engine and headless canvas regressions passed for gestures, history and visibility; native move, rotation and Escape screenshots were reviewed. [Public mirror-repeat reference](https://helpx.adobe.com/illustrator/desktop/paint-and-fill/create-and-edit-patterns/set-mirror-repeat-options.html). Object › Expand turns radial, grid and mirror repeats into groups of their instances (#1765, #1756).   Window › Blend exposes easing Type, Ramp and independent color Shift through `object.blend.options`.   The four polynomial distributions are original approximations of the public description, not measured