collective

Browse / github.com/storytold/vectorcraft / patch

Image Trace: Expand can group the traced paths by color

Image Trace: Expand can group the traced paths by color ## What and why Expand puts every traced path into one group, so editing one color of a multi-color trace means finding its paths one by one, and Release to Layers splits by object, not by color. `imageTrace.expand` and `imageTrace.makeAndExpand` now take `byColor` (default `false`). With it, each color's paths go in a group of their own, named by the global swatch the color links to (a tint with its percentage, as `Sky 50%`) or as `#rrggbb`; gradient fills share a group named `Gradients`. The groups stack in the order of the lowest path in each and keep their paths' order, in one undo step.

Status
open
Score
0.00
Adopters
0 other operators
Author
dotCooCoo (imported from pull request #4058, open)
Base
bccf4de7223ae78ea24f203b68ef2ffdbcaede74
Size
+411 −2 in 23 files
Review
pending
Shared
2026-10-28
Id
59ec5ddf4b5f2edc10d3be85

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/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/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/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/mcp.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@@ -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, byColor?: false (each color's paths in a group of their own, as imageTrace.expand)} same as imageTrace.make, then expand → {id, paths, anchors, colors}",             has_selection_or_ids,             |s, p| trace_make(s, p, true)         ),@@ -156,7 +156,7 @@ pub fn specs() -> Vec<CommandSpec> {             "Expand",             ["Object", "Image Trace"],             None,-            "{id?, ids?: [id…] (instead of id)} keep only the traced paths (drop the source image) → {ids}",+            "{id?, ids?: [id…] (instead of id), byColor?: false (each color's paths in a group of their own, named by the swatch the color links to or as #rrggbb, gradient fills in one group named Gradients; the groups stack by their lowest path and keep their paths' order; a color whose paths overlap another color's from above and below gets a group for each part, so the art draws the same; not for a trace made with autoGrouping)} keep only the traced paths (drop the source image) → {ids}",             has_selection_or_ids,             trace_expand         ),@@ -1103,10 +1103,98 @@ fn grouped(d: &mut Document, nodes: Vec<Node>, parents: &[Option<usize>]) -> Vec     roots.iter().filter_map(|&r| built.get_mut(r)?.take()).chain(ring).collect() } +/// The `byColor` param of an Expand (false when not given).+fn by_color_param(cmd: &str, p: &Value) -> Result<bool> {+    match p.get("byColor").filter(|v| !v.is_null()) {+        Some(v) => v.as_bool().ok_or_else(|| bad(cmd, "byColor must be true or false")),+        None => Ok(false),+    }+}++/// Whether a trace made with `params` can have paths that overlap, as in [`tr::trace_within`]'s+/// Auto Grouping: stacked layers, Flat Logo's shapes and stroked lines.+fn stacked_trace(params: &tr::TraceParams) -> bool {+    params.method == tr::Method::Overlapping || params.mode == tr::Mode::Logo || params.strokes+}++/// Expand by color: an expanded trace's `items` (bottom-most first) in a group for each paint, a+/// stroked line counted by its stroke. A solid color's group is named by the swatch it links to (a+/// tint with its percentage) or as `#rrggbb`, and gradient fills share one group, "Gradients". The+/// groups stack in the order of the lowest item in each, and each keeps its items' order. When the+/// paths can overlap (`stacked`), an item joins its paint's last group only if no item of a later+/// group overlaps it (by the boxes of everything they paint, effects included); otherwise it starts+/// another group of that paint, so the art draws as before. A group among `items` (Auto Grouping)+/// is refused.+fn color_groups(cmd: &str, d: &mut Document, items: Vec<Arc<Node>>, stacked: bool) -> Result<Vec<Arc<Node>>> {+    use vectorcraft_color::tint_percent;+    // Grouped by the paint: unlinked (its #rrggbb), linked (the swatch and its exact tint), a+    // gradient, or another paint (its label). The name is apart from it: a swatch may be named as+    // a color's `#rrggbb` or as "Gradients".+    type PaintKey = (u8, String, u32);+    struct Run {+        key: PaintKey,+        name: String,+        members: Vec<Arc<Node>>,+        boxes: Vec<Rect>,+        union: Option<Rect>,+    }+    let meet = |a: &Rect, b: &Rect| a.x0 < b.x1 && b.x0 < a.x1 && a.y0 < b.y1 && b.y0 < a.y1;+    let mut groups: Vec<Run> = vec![];+    for item in items {+        if matches!(item.kind, NodeKind::Group { .. }) {+            return Err(bad(cmd, "byColor can't split Auto Grouping groups: trace without Auto Grouping"));+        }+        let paint = match item.appearance.fill_paint() {+            Paint::None => item.appearance.stroke_paint(),+            fill => fill,+        };+        let (key, name) = match &paint {+            Paint::Solid { swatch: Some(n), tint, .. } => {+                let percent = tint_percent(*tint);+                let name = if *tint < 1.0 { format!("{n} {percent}%") } else { n.clone() };+                ((1, n.clone(), tint.to_bits()), name)+            }+            Paint::Solid { color, .. } => ((0, color.to_hex(), 0), color.to_hex()),+            Paint::Gradient(_) => ((2, String::new(), 0), "Gradients".to_string()),+            other => ((3, other.label(), 0), other.label()),+        };+        let bounds = if stacked { vectorcraft_render::painted_bounds(d, &item) } else { None };+        // Joining an earlier group moves the item under every later group's items.+        let hidden_by_later = |at: usize| {+            bounds.is_some_and(|b| groups.iter().skip(at + 1).any(|g| g.union.is_some_and(|u| meet(&u, &b)) && g.boxes.iter().any(|m| meet(m, &b))))+        };+        let at = groups.iter().rposition(|g| g.key == key).filter(|&at| !hidden_by_later(at));+        let mut started = Run { key, name, members: vec![], boxes: vec![], union: None };+        let joins = at.and_then(|at| groups.get_mut(at));+        let new = joins.is_none();+        let g = joins.unwrap_or(&mut started);+        if let Some(b) = bounds {+            g.boxes.push(b);+            g.union = Some(g.union.map_or(b, |u| u.union(b)));+        }+        g.members.push(item);+        if new {+            groups.push(started);+        }+    }+    Ok(groups+        .into_iter()+        .map(|run| {+            let mut g = Node::group(d.alloc_id(), run.members);+            g.name = Some(run.name);+            Arc::new(g)+        })+        .collect())+}+ fn trace_make(s: &mut Session, p: &Value, expand: bool) -> Result<Value> {     let c = if expand { "imageTrace.makeAndExpand" } else { "imageTrace.make" };     let mut params = trace_params(c, p)?;     let view = view_param(c, p)?;+    let by_color = expand && by_color_param(c, p)?;+    if by_color && params.auto_grouping {+        return Err(bad(c, "byColor can't split Auto Grouping groups: trace without Auto Grouping"));+    }     let params_json = serde_json::to_value(&params).map_err(|e| EngineError::Other(e.to_string()))?;     // The preset's name if the parameters are exactly that preset's or its enhanced version's,     // else "Custom".@@ -1193,6 +1281,9 @@ fn trace_make(s: &mut Session, p: &Value, expand: bool) -> Result<Value> {             traced.push(n);         }         children.extend(grouped(d, traced, &res.parents));+        if by_color {+            children = color_groups(c, d, children, stacked_trace(&params))?;+        }         let gid = d.alloc_id();         let mut g = Node::group(gid, children);         g.name = if expand { None } else { Some(TRACE_NAME.into()) };@@ -1291,7 +1382,17 @@ fn trace_release(s: &mut Session, p: &Value) -> Result<Value> { }  fn trace_expand(s: &mut Session, p: &Value) -> Result<Value> {+    const C: &str = "imageTrace.expand";+    let by_color = by_color_param(C, p)?;     let targets = trace_targets(s, p)?;+    if by_color {+        // A trace made with Auto Grouping is refused even when none of its paths were grouped.+        let d = &s.doc()?.doc;+        let grouped = |n: &Node| n.extras().trace.as_deref().and_then(|t| t.pointer("/params/autoGrouping")).and_then(Value::as_bool) == Some(true);+        if targets.iter().any(|g| d.node(*g).is_some_and(grouped)) {+            return Err(bad(C, "byColor can't split Auto Grouping groups: trace without Auto Grouping"));+        }+    }     s.edit("Expand Image Trace", |d, sel| {         for g in &targets {             let n = d.node_mut(*g).ok_or(EngineError::NoNode(*g))?;@@ -1300,6 +1401,15 @@ fn trace_expand(s: &mut Session, p: &Value) -> Result<Value> {             if let Some(ch) = n.children_mut() {                 ch.retain(|c| !matches!(c.kind, NodeKind::Image(_)));             }+            if by_color {+                let items = d.node(*g).and_then(Node::children).cloned().unwrap_or_default();+                // The paths may have been moved or reshaped since the trace was made, so any trace's+                // paths are taken to overlap.+                let groups = color_groups(C, d, items, true)?;+                if let Some(ch) = d.node_mut(*g).and_then(Node::children_mut) {+                    *ch = groups;+                }+            }         }         sel.set(targets.iter().copied());         Ok(())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@@ -2517,6 +2517,256 @@ fn image_trace_release_and_expand() {     assert!(s.execute("imageTrace.release", &json!({})).is_err()); } +/// Expand by color: each color's paths in a group of their own, named by the color, the groups+/// stacked by the lowest path in each and the art drawn as before; Release to Layers then gives a+/// sublayer for each color.+#[test]+fn image_trace_expand_by_color_groups_each_colors_paths() {+    // 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 - 75.0);+        if (34.0..=40.0).contains(&r) {+            [0, 0, 0, 255]+        } else if (130..170).contains(&x) && (55..95).contains(&y) {+            [200, 40, 50, 255]+        } else {+            [240, 240, 220, 255]+        }+    });+    let names = |n: &Node| n.children().unwrap().iter().map(|c| c.name.clone().unwrap_or_default()).collect::<Vec<_>>();+    let hex = |n: &Node| n.appearance.fill_paint().color().unwrap().to_hex();+    let shot = |s: &Session| {+        let img = render_doc(s, false);+        (0..200).flat_map(|x| (0..150).map(move |y| (x, y))).map(|(x, y)| img.pixel(x, y)).collect::<Vec<_>>()+    };+    for method in ["abutting", "overlapping"] {+        let params = json!({"mode": "color", "palette": "limited", "colors": 3, "method": method});+        let expand = |by_color: bool| {+            let mut s = session();+            add_image(&mut s, &art, Affine::IDENTITY);+            let r = s.execute("imageTrace.makeAndExpand", &json!({"params": params, "byColor": by_color})).unwrap();+            let g = NodeId(r["id"].as_u64().unwrap());+            (s, g)+        };+        let (flat, fg) = expand(false);+        let (mut s, g) = expand(true);+        let paths = node(&flat, fg).children().unwrap().clone();+        // The colors in the order they first come in, bottom to top.+        let mut order: Vec<String> = vec![];+        for p in &paths {+            if !order.contains(&hex(p)) {+                order.push(hex(p));+            }+        }+        let mut sorted = order.clone();+        sorted.sort();+        assert_eq!(sorted, ["#000000", "#c82832", "#f0f0dc"], "{method}");+        let n = node(&s, g);+        assert_eq!(names(&n), order, "{method}");+        // Each group holds its color's paths, and in turn they are the paths of the trace without+        // byColor, in their order.+        let groups = n.children().unwrap();+        for c in groups {+            assert!(matches!(c.kind, NodeKind::Group { .. }), "{method}");+            assert!(c.children().unwrap().iter().all(|m| Some(hex(m)) == c.name), "{method}: {:?}", c.name);+        }+        let members: Vec<Arc<Node>> = groups.iter().flat_map(|c| c.children().unwrap().iter().cloned()).collect();+        assert!(members == paths, "{method}");+        assert!(shot(&s) == shot(&flat), "{method}: the art changed");+        // Release to Layers: a sublayer for each color, in the same order.+        let r = s.execute("layer.releaseToLayers", &json!({"id": g.0})).unwrap();+        let subs = ids(&r);+        assert_eq!(subs.len(), 3, "{method}");+        for (sub, name) in subs.iter().zip(&order) {+            let l = node(&s, *sub);+            assert!(matches!(l.kind, NodeKind::Layer { .. }), "{method}");+            assert_eq!(names(&l), std::slice::from_ref(name), "{method}");+        }+    }++    // imageTrace.expand takes it too, as one undo step.+    let mut s = session();+    add_image(&mut s, &art, Affine::IDENTITY);+    let params = json!({"mode": "color", "palette": "limited", "colors": 3});+    let g = NodeId(s.execute("imageTrace.make", &json!({"params": params})).unwrap()["id"].as_u64().unwrap());+    let steps = s.doc().unwrap().history.undo.len();+    s.execute("imageTrace.expand", &json!({"byColor": true})).unwrap();+    assert_eq!(s.doc().unwrap().history.undo.len(), steps + 1);+    let mut got = names(&node(&s, g));+    got.sort();+    assert_eq!(got, ["#000000", "#c82832", "#f0f0dc"]);+    s.execute("edit.undo", &json!({})).unwrap();+    assert_eq!(node(&s, g).name.as_deref(), Some("Image Trace"));++    // Auto Grouping's groups are not split: refused, and the trace stays as it was.+    let mut grouped = params.clone();+    grouped["autoGrouping"] = json!(true);+    let e = s.execute("imageTrace.makeAndExpand", &json!({"id": g.0, "params": grouped, "byColor": true})).unwrap_err().to_string();+    assert!(e.contains("Auto Grouping"), "{e}");+    let g = NodeId(s.execute("imageTrace.make", &json!({"id": g.0, "params": grouped})).unwrap()["id"].as_u64().unwrap());+    let e = s.execute("imageTrace.expand", &json!({"byColor": true})).unwrap_err().to_string();+    assert!(e.contains("Auto Grouping"), "{e}");+    assert_eq!(node(&s, g).name.as_deref(), Some("Image Trace"));+    let e = s.execute("imageTrace.expand", &json!({"byColor": "yes"})).unwrap_err().to_string();+    assert!(e.contains("true or false"), "{e}");+    // Refused too when Auto Grouping grouped nothing, as with side-by-side stripes.+    let mut s = session();+    let stripes = vectorcraft_trace::Raster::from_fn(60, 60, |x, _| match x / 20 {+        0 => [200, 40, 40, 255],+        1 => [40, 160, 60, 255],+        _ => [40, 70, 200, 255],+    });+    add_image(&mut s, &stripes, Affine::IDENTITY);+    let g = NodeId(s.execute("imageTrace.make", &json!({"params": grouped})).unwrap()["id"].as_u64().unwrap());+    assert!(node(&s, g).children().unwrap().iter().all(|c| !matches!(c.kind, NodeKind::Group { .. })));+    let e = s.execute("imageTrace.expand", &json!({"byColor": true})).unwrap_err().to_string();+    assert!(e.contains("Auto Grouping"), "{e}");+    assert_eq!(node(&s, g).name.as_deref(), Some("Image Trace"));++    // A color linked to a global swatch goes by the swatch's name, a tint with its percentage.+    let mut s = session();+    s.execute("swatch.new", &json!({"name": "Ink", "color": "#c82828", "global": true})).unwrap();+    s.execute("swatch.new", &json!({"name": "Sky", "color": "#2846c8", "global": true})).unwrap();+    let r = vectorcraft_trace::Raster::from_fn(60, 60, |x, y| match (x < 30, y < 30) {+        (true, _) => [200, 40, 40, 255],+        (false, true) => [40, 160, 60, 255],+        (false, false) => [40, 70, 200, 255],+    });+    add_image(&mut s, &r, Affine::IDENTITY);+    let limited = json!({"mode": "color", "palette": "limited", "colors": 3});+    let g = NodeId(s.execute("imageTrace.make", &json!({"params": limited})).unwrap()["id"].as_u64().unwrap());+    let paths: Vec<(u64, String)> =+        node(&s, g).children().unwrap().iter().filter(|c| !matches!(c.kind, NodeKind::Image(_))).map(|c| (c.id.0, hex(c))).collect();+    let mut colors: Vec<&str> = paths.iter().map(|(_, h)| h.as_str()).collect();+    colors.sort();+    colors.dedup();+    assert_eq!(colors, ["#2846c8", "#28a03c", "#c82828"]);+    // The red paths link to Ink and the blue ones to Sky at 50%; green stays a plain color.+    let pick = |want: &str| paths.iter().filter(|(_, h)| h == want).map(|(id, _)| *id).collect::<Vec<_>>();+    s.execute("paint.setFill", &json!({"ids": pick("#c82828"), "swatch": "Ink"})).unwrap();+    s.execute("paint.setFill", &json!({"ids": pick("#2846c8"), "swatch": "Sky", "tint": 50})).unwrap();+    s.execute("imageTrace.expand", &json!({"id": g.0, "byColor": true})).unwrap();+    let mut got = names(&node(&s, g));+    got.sort();+    assert_eq!(got, ["#28a03c", "Ink", "Sky 50%"]);+    // A swatch named as another color's #rrggbb still gets a group of its own.+    s.execute("edit.undo", &json!({})).unwrap();+    s.execute("swatch.edit", &json!({"name": "Ink", "newName": "#28a03c"})).unwrap();+    s.execute("imageTrace.expand", &json!({"id": g.0, "byColor": true})).unwrap();+    let mut got = names(&node(&s, g));+    got.sort();+    assert_eq!(got, ["#28a03c", "#28a03c", "Sky 50%"]);++    // Gradient fills share one group, and a stroked line goes by its stroke.+    let ramp = vectorcraft_trace::Raster::from_fn(100, 60, |x, _| {+        let t = (f64::from(x) + 0.5) / 100.0;+        [(20.0 + 200.0 * t) as u8, 40, (220.0 - 180.0 * t) as u8, 255]+    });+    let line =+        vectorcraft_trace::Raster::from_fn(100, 100, |x, y| if (10..90).contains(&x) && (49..52).contains(&y) { [0, 0, 0, 255] } else { [255; 4] });+    for (art, params, want) in [+        (ramp, json!({"preset": "6 Colors", "params": {"gradients": true}, "byColor": true}), "Gradients"),+        (line, json!({"params": {"ignoreWhite": true, "strokes": true, "strokeWidth": 5}, "byColor": true}), "#000000"),+    ] {+        let mut s = session();+        add_image(&mut s, &art, Affine::IDENTITY);+        let res = s.execute("imageTrace.makeAndExpand", &params).unwrap();+        assert_eq!(names(&node(&s, NodeId(res["id"].as_u64().unwrap()))), [want]);+    }++    // Flat Logo stacks its shapes biggest first without cutting holes under white: a black ring's+    // outline, a white disc on it and a black dot on that. Black keeps a group below white and one+    // above it, and the art draws as before, through Make and Expand and through Expand.+    let target = vectorcraft_trace::Raster::from_fn(120, 120, |x, y| {+        let r = (f64::from(x) + 0.5 - 60.0).hypot(f64::from(y) + 0.5 - 60.0);+        if r < 15.0 || (30.0..50.0).contains(&r) {+            [0, 0, 0, 255]+        } else if r < 30.0 {+            [255, 255, 255, 255]+        } else {+            [0, 0, 0, 0]+        }+    });+    let logo = json!({"mode": "logo", "logoColors": ["#000000"]});+    let traced = |by_color: bool, expand_after: bool| {+        let mut s = session();+        add_image(&mut s, &target, Affine::IDENTITY);+        let g = if expand_after {+            let g = s.execute("imageTrace.make", &json!({"params": logo})).unwrap()["id"].as_u64().unwrap();+            s.execute("imageTrace.expand", &json!({"byColor": by_color})).unwrap();+            g+        } else {+            s.execute("imageTrace.makeAndExpand", &json!({"params": logo, "byColor": by_color})).unwrap()["id"].as_u64().unwrap()+        };+        (s, NodeId(g))+    };+    for expand_after in [false, true] {+        let (flat, fg) = traced(false, expand_after);+        let (s, g) = traced(true, expand_after);+        let paths = node(&flat, fg).children().unwrap().clone();+        assert_eq!(paths.iter().map(|p| hex(p)).collect::<Vec<_>>(), ["#000000", "#ffffff", "#000000"]);+        assert_eq!(names(&node(&s, g)), ["#000000", "#ffffff", "#000000"], "expand after: {expand_after}");+        let members: Vec<Arc<Node>> = node(&s, g).children().unwrap().iter().flat_map(|c| c.children().unwrap().iter().cloned()).collect();+        assert!(members == paths, "expand after: {expand_after}");+        assert!(shot(&s) == shot(&flat), "expand after: {expand_after}: the art changed");+    }++    // An Abutting trace's top path, given the bottom path's color and moved onto the middle one (by+    // a move or by a Transform effect), or moved clear of it with a drop shadow cast onto it, before+    // Expand, still draws on top of it.+    let stripes = vectorcraft_trace::Raster::from_fn(90, 30, |x, _| match x / 30 {+        0 => [200, 40, 40, 255],+        1 => [40, 160, 60, 255],+        _ => [40, 70, 200, 255],+    });+    let edited = |by_color: bool, how: &str| {+        let mut s = session();+        add_image(&mut s, &stripes, Affine::IDENTITY);+        let limited = json!({"mode": "color", "palette": "limited", "colors": 3, "method": "abutting"});+        let g = NodeId(s.execute("imageTrace.make", &json!({"params": limited})).unwrap()["id"].as_u64().unwrap());+        let paths: Vec<Arc<Node>> = node(&s, g).children().unwrap().iter().filter(|c| !matches!(c.kind, NodeKind::Image(_))).cloned().collect();+        assert_eq!(paths.len(), 3);+        let (first, middle, last) = (&paths[0], &paths[1], &paths[2]);+        s.execute("paint.setFill", &json!({"ids": [last.id.0], "color": hex(first)})).unwrap();+        let (to, from) = (middle.visual_bounds().unwrap(), last.visual_bounds().unwrap());+        let (dx, dy) = (to.x0 - from.x0, to.y0 - from.y0);+        match how {+            "move" => s.execute("object.move", &json!({"ids": [last.id.0], "dx": dx, "dy": dy})).unwrap(),+            "transform" => {+                s.execute("effect.apply", &json!({"ids": [last.id.0], "effect": "distort.transform", "params": {"moveH": dx, "moveV": dy}})).unwrap()+            }+            _ => {+                s.execute("object.move", &json!({"ids": [last.id.0], "dx": 40, "dy": 0})).unwrap();+                s.execute("effect.apply", &json!({"ids": [last.id.0], "effect": "stylize.dropShadow", "params": {"x": -70, "y": 0, "blur": 0}}))+                    .unwrap()+            }+        };+        s.execute("imageTrace.expand", &json!({"id": g.0, "byColor": by_color})).unwrap();+        (s, g)+    };+    for how in ["move", "transform", "shadow"] {+        let (flat, _) = edited(false, how);+        let (s, g) = edited(true, how);+        assert_eq!(names(&node(&s, g)).len(), 3, "{how}");+        assert!(shot(&s) == shot(&flat), "{how}: the edited art changed");+    }++    // Two tints of one swatch that round to the same percentage are still two colors.+    let mut s = session();+    s.execute("swatch.new", &json!({"name": "Sky", "color": "#2846c8", "global": true})).unwrap();+    add_image(&mut s, &stripes, Affine::IDENTITY);+    let limited = json!({"mode": "color", "palette": "limited", "colors": 3});+    let g = NodeId(s.execute("imageTrace.make", &json!({"params": limited})).unwrap()["id"].as_u64().unwrap());+    let paths: Vec<u64> = node(&s, g).children().unwrap().iter().filter(|c| !matches!(c.kind, NodeKind::Image(_))).map(|c| c.id.0).collect();+    assert_eq!(paths.len(), 3);+    s.execute("paint.setFill", &json!({"ids": [paths[0]], "swatch": "Sky", "tint": 50.1})).unwrap();+    s.execute("paint.setFill", &json!({"ids": [paths[1], paths[2]], "swatch": "Sky", "tint": 50.4})).unwrap();+    s.execute("imageTrace.expand", &json!({"id": g.0, "byColor": true})).unwrap();+    let groups = node(&s, g).children().unwrap().clone();+    assert_eq!(names(&node(&s, g)), ["Sky 50%", "Sky 50%"]);+    assert_eq!(groups.iter().map(|c| c.children().unwrap().len()).collect::<Vec<_>>(), [1, 2]);+}+ #[test] fn image_trace_make_and_expand_and_color_presets() {     let mut s = session();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@@ -5393,6 +5393,8 @@ mark-style	Japanese	Japonské @msg	too many library panels were opened in this session	v této relaci bylo otevřeno příliš mnoho panelů knihoven @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.	Obraz má {_1} × {_2} pixelů, na jedné straně více než {LARGE_TRACE_SIDE}: vektorizuje se, ale pomaleji. Rychleji se vektorizuje s předvolbou nižší věrnosti nebo menší kopie obrazu. @msg	The image was traced at {_1} × {_2} pixels, scaled down from {_3} × {_4} ({_5} megapixels at most).	Obraz byl vektorizován v rozlišení {_1} × {_2} pixelů, zmenšený z {_3} × {_4} (nejvýše {_5} megapixelů).+@msg	byColor must be true or false	byColor musí být true nebo false+@msg	byColor can't split Auto Grouping groups: trace without Auto Grouping	byColor nerozdělí skupiny z Automatického seskupení: vektorizujte bez Automatického seskupení 	Snapping	Přichytávání 	Snap to Tangent	Přichytávat k tečně 	Snap to Perpendicular	Přichytávat ke kolmicidiff --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@@ -4972,6 +4972,8 @@ effect	Soft	Weich @msg	too many library panels were opened in this session	in dieser Sitzung wurden zu viele Bibliotheksbedienfelder geöffnet @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.	Das Bild hat {_1} × {_2} Pixel, mehr als {LARGE_TRACE_SIDE} auf einer Seite: Es wird nachgezeichnet, aber langsamer. Eine Vorgabe mit geringerer Detailtreue oder eine kleinere Kopie des Bildes geht schneller. @msg	The image was traced at {_1} × {_2} pixels, scaled down from {_3} × {_4} ({_5} megapixels at most).	Das Bild wurde mit {_1} × {_2} Pixeln nachgezeichnet, verkleinert von {_3} × {_4} (höchstens {_5} Megapixel).+@msg	byColor must be true or false	byColor muss true oder false sein+@msg	byColor can't split Auto Grouping groups: trace without Auto Grouping	byColor kann Gruppen aus Automatisch gruppieren nicht aufteilen: ohne Automatisch gruppieren nachzeichnen 	Snapping	Einrasten 	Snap to Tangent	An Tangente ausrichten 	Snap to Perpendicular	An Senkrechter ausrichtendiff --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@@ -5345,6 +5345,8 @@ effect	Points	Σημεία @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} megapixel).+@msg	byColor must be true or false	το byColor πρέπει να είναι true ή false+@msg	byColor can't split Auto Grouping groups: trace without Auto Grouping	το byColor δεν μπορεί να χωρίσει τις ομάδες της Αυτόματης ομαδοποίησης: ανιχνεύστε χωρίς Αυτόματη ομαδοποίηση 	Snapping	Προσκόλληση 	Snap to Tangent	Προσκόλληση σε εφαπτομένη 	Snap to Perpendicular	Προσκόλληση σε κάθετο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@@ -5335,6 +5335,8 @@ mark-style	Japanese	Japonesas @msg	too many library panels were opened in this session	se abrieron demasiados paneles de biblioteca en esta sesión @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.	La imagen tiene {_1} × {_2} píxeles, más de {LARGE_TRACE_SIDE} en un lado: se calcará, pero más despacio. Un ajuste preestablecido de menor fidelidad o una copia más pequeña de la imagen se calcan más rápido. @msg	The image was traced at {_1} × {_2} pixels, scaled down from {_3} × {_4} ({_5} megapixels at most).	La imagen se calcó a {_1} × {_2} píxeles, reducida desde {_3} × {_4} ({_5} megapíxeles como máximo).+@msg	byColor must be true or false	byColor debe ser true o false+@msg	byColor can't split Auto Grouping groups: trace without Auto Grouping	byColor no puede dividir los grupos de Agrupación automática: calque sin Agrupación automática 	Snapping	Ajuste 	Snap to Tangent	Ajustar a tangente 	Snap to Perpendicular	Ajustar a perpendiculardiff --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@@ -5357,6 +5357,8 @@ mark-style	Japanese	Japanilainen @msg	too many library panels were opened in this session	tässä istunnossa on avattu liian monta kirjastopaneelia @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.	Kuva on {_1} × {_2} kuvapistettä, yhdeltä sivultaan yli {LARGE_TRACE_SIDE}: se jäljitetään, mutta hitaammin. Pienemmän tarkkuuden esiasetus tai kuvan pienempi kopio jäljittyy nopeammin. @msg	The image was traced at {_1} × {_2} pixels, scaled down from {_3} × {_4} ({_5} megapixels at most).	Kuva jäljitettiin kokoon {_1} × {_2} kuvapistettä pienennettynä koosta {_3} × {_4} (enintään {_5} megapikseliä).+@msg	byColor must be true or false	byColor-arvon on oltava true tai false+@msg	byColor can't split Auto Grouping groups: trace without Auto Grouping	byColor ei voi jakaa Automaattisen ryhmittelyn ryhmiä: jäljitä ilman Automaattista ryhmittelyä 	Snapping	Tartunta 	Snap to Tangent	Kohdista tangenttiin 	Snap to Perpendicular	Kohdista kohtisuoraandiff --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@@ -5392,6 +5392,8 @@ theme	Appearance	Apparence @msg	too many library panels were opened in this session	trop de panneaux de bibliothèque ont été ouverts dans cette 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.	L’image fait {_1} × {_2} pixels, plus de {LARGE_TRACE_SIDE} sur un côté : elle est vectorisée, mais plus lentement. Un paramètre prédéfini moins fidèle ou une copie plus petite de l’image se vectorise plus vite. @msg	The image was traced at {_1} × {_2} pixels, scaled down from {_3} × {_4} ({_5} megapixels at most).	L’image a été vectorisée à {_1} × {_2} pixels, réduite de {_3} × {_4} ({_5} mégapixels au plus).+@msg	byColor must be true or false	byColor doit être true ou false+@msg	byColor can't split Auto Grouping groups: trace without Auto Grouping	byColor ne peut pas scinder les groupes du Groupement automatique : vectorisez sans Groupement automatique 	Snapping	Magnétisme 	Snap to Tangent	Magnétisme de la tangente 	Snap to Perpendicular	Magnétisme de la perpendiculairediff --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@@ -5379,6 +5379,8 @@ imageTrace	Outline	Körvonal @msg	too many library panels were opened in this session	ebben a munkamenetben túl sok könyvtárpanel nyílt meg @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.	A kép {_1} × {_2} képpont, egyik oldalán több mint {LARGE_TRACE_SIDE}: a vektorizálás megtörténik, de lassabban. Kisebb hűségű előbeállítással vagy a kép kisebb másolatával gyorsabb. @msg	The image was traced at {_1} × {_2} pixels, scaled down from {_3} × {_4} ({_5} megapixels at most).	A kép vektorizálása {_1} × {_2} képponton történt, {_3} × {_4} méretről kicsinyítve (legfeljebb {_5} megapixel).+@msg	byColor must be true or false	a byColor értéke true vagy false lehet+@msg	byColor can't split Auto Grouping groups: trace without Auto Grouping	a byColor nem bontja fel az Automatikus csoportosítás csoportjait: vektorizáljon Automatikus csoportosítás nélkül 	Snapping	Illesztés 	Snap to Tangent	Illesztés érintőre 	Snap to Perpendicular	Illesztés merőlegesrediff --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@@ -5006,6 +5006,8 @@ lighting	Light	Luce @msg	too many library panels were opened in this session	in questa sessione sono stati aperti troppi pannelli libreria @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.	L’immagine è di {_1} × {_2} pixel, più di {LARGE_TRACE_SIDE} su un lato: viene vettorializzata, ma più lentamente. Un predefinito a fedeltà più bassa o una copia più piccola dell’immagine è più veloce. @msg	The image was traced at {_1} × {_2} pixels, scaled down from {_3} × {_4} ({_5} megapixels at most).	L’immagine è stata vettorializzata a {_1} × {_2} pixel, ridotta da {_3} × {_4} (al massimo {_5} megapixel).+@msg	byColor must be true or false	byColor deve essere true o false+@msg	byColor can't split Auto Grouping groups: trace without Auto Grouping	byColor non può dividere i gruppi di Raggruppamento automatico: vettorializza senza Raggruppamento automatico 	Snapping	Aggancio 	Snap to Tangent	Aggancia alla tangente 	Snap to Perpendicular	Aggancia alla perpendicolarediff --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@@ -5046,6 +5046,8 @@ lighting	Light	ライト @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).	画像は {_3} × {_4} から縮小した {_1} × {_2} ピクセルでトレースされました (最大 {_5} メガピクセル)。+@msg	byColor must be true or false	byColor は true または false にしてください+@msg	byColor can't split Auto Grouping groups: trace without Auto Grouping	byColor は自動グループ化のグループを分割できません: 自動グループ化なしでトレースしてください 	Snapping	スナップ 	Snap to Tangent	接線にスナップ 	Snap to Perpendicular	垂線にスナップ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@@ -5239,6 +5239,8 @@ imageTrace	Outline	კონტური @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 traced the image at {_1} × {_2} pixels, averaged down from {_3} × {_4}.	ბრტყელი ლოგოს რეჟიმში გამოსახულება ვექტორიზდა {_1} × {_2} პიქსელზე, საშუალოს გამოთვლით შემცირდა {_3} × {_4}-დან.+@msg	byColor must be true or false	byColor უნდა იყოს true ან false+@msg	byColor can't split Auto Grouping groups: trace without Auto Grouping	byColor ვერ დაყოფს ავტომატური დაჯგუფების ჯგუფებს: ვექტორიზება გააკეთეთ ავტომატური დაჯგუფების გარეშე @msg	`{given}` isn't a library panel (see window.swatchLibrary)	`{given}` ბიბლიოთეკის პანელი არ არის (იხ. window.swatchLibrary) @msg	`{key}` must be a #rrggbb color	`{key}` უნდა იყოს #rrggbb ფერი @msg	a key object sits on that point	ამ წერტილზე საკვანძო ობიექტი მდებარეობს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@@ -4969,6 +4969,8 @@ lighting	Light	광원 @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).	이미지를 {_3} × {_4}에서 축소한 {_1} × {_2}픽셀로 추적했습니다(최대 {_5}메가픽셀).+@msg	byColor must be true or false	byColor는 true 또는 false여야 합니다+@msg	byColor can't split Auto Grouping groups: trace without Auto Grouping	byColor는 자동 그룹화 그룹을 나눌 수 없습니다. 자동 그룹화 없이 추적하세요 	Snapping	스냅 	Snap to Tangent	접선에 스냅 	Snap to Perpendicular	수직선에 스냅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@@ -5245,6 +5245,8 @@ mark-style	Japanese	Japońskie @msg	too many library panels were opened in this session	w tej sesji otwarto zbyt wiele paneli bibliotek @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.	Obraz ma {_1} × {_2} pikseli, ponad {LARGE_TRACE_SIDE} na jednym boku: zostanie obrysowany, ale wolniej. Ustawienie wstępne o niższej wierności lub mniejsza kopia obrazu obrysuje się szybciej. @msg	The image was traced at {_1} × {_2} pixels, scaled down from {_3} × {_4} ({_5} megapixels at most).	Obraz obrysowano w rozdzielczości {_1} × {_2} pikseli, po zmniejszeniu z {_3} × {_4} (najwyżej {_5} megapikseli).+@msg	byColor must be true or false	byColor musi mieć wartość true lub false+@msg	byColor can't split Auto Grouping groups: trace without Auto Grouping	byColor nie może podzielić grup Automatycznego grupowania: obrysuj bez Automatycznego grupowania 	Snapping	Przyciąganie 	Snap to Tangent	Przyciągaj do stycznych 	Snap to Perpendicular	Przyciągaj do prostopadłychdiff --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@@ -5310,6 +5310,8 @@ mark-style	Japanese	Japonesas @msg	too many library panels were opened in this session	foram abertos painéis de biblioteca demais nesta sessão @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.	A imagem tem {_1} × {_2} pixels, mais de {LARGE_TRACE_SIDE} em um lado: ela é traçada, mas mais devagar. Uma predefinição de menor fidelidade ou uma cópia menor da imagem traça mais rápido. @msg	The image was traced at {_1} × {_2} pixels, scaled down from {_3} × {_4} ({_5} megapixels at most).	A imagem foi traçada em {_1} × {_2} pixels, reduzida de {_3} × {_4} (no máximo {_5} megapixels).+@msg	byColor must be true or false	byColor deve ser true ou false+@msg	byColor can't split Auto Grouping groups: trace without Auto Grouping	byColor não pode dividir os grupos de Agrupamento Automático: trace sem Agrupamento Automático 	Snapping	Ajuste 	Snap to Tangent	Ajustar à Tangente 	Snap to Perpendicular	Ajustar à Perpendiculardiff --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@@ -5002,6 +5002,8 @@ lighting	Light	Свет @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	byColor must be true or false	byColor должен иметь значение true или false+@msg	byColor can't split Auto Grouping groups: trace without Auto Grouping	byColor не может разделить группы автогруппировки: трассируйте без автогруппировки 	Snapping	Привязка 	Snap to Tangent	Привязать к касательной 	Snap to Perpendicular	Привязать к перпендикуляру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@@ -5416,6 +5416,8 @@ swatch	Yellow Green	Gulgrön @msg	“{name}” is not a file	”{name}” är inte en fil @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.	Bilden är {_1} × {_2} pixlar, mer än {LARGE_TRACE_SIDE} på en sida: den vektoriseras, men långsammare. En förinställning med lägre återgivning eller en mindre kopia av bilden går snabbare. @msg	The image was traced at {_1} × {_2} pixels, scaled down from {_3} × {_4} ({_5} megapixels at most).	Bilden vektoriserades vid {_1} × {_2} pixlar, nedskalad från {_3} × {_4} (högst {_5} megapixlar).+@msg	byColor must be true or false	byColor måste vara true eller false+@msg	byColor can't split Auto Grouping groups: trace without Auto Grouping	byColor kan inte dela upp grupper från Automatisk gruppering: vektorisera utan Automatisk gruppering 	Snapping	Fästning 	Snap to Tangent	Fäst mot tangent 	Snap to Perpendicular	Fäst mot vinkelrätdiff --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@@ -5391,6 +5391,8 @@ theme	Appearance	Görünüm @msg	library panel `{given}` isn't missing its library	`{given}` kitaplık panelinin kitaplığı eksik değil @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).+@msg	byColor must be true or false	byColor true veya false olmalıdır+@msg	byColor can't split Auto Grouping groups: trace without Auto Grouping	byColor, Otomatik Gruplama gruplarını bölemez: Otomatik Gruplama olmadan izleyin 	Gap Length:	Boşluk Uzunluğu: @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 bekliyordiff --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@@ -4986,6 +4986,8 @@ lighting	Light	Світло @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	byColor must be true or false	byColor має мати значення true або false+@msg	byColor can't split Auto Grouping groups: trace without Auto Grouping	byColor не може розділити групи автогрупування: трасуйте без автогрупування 	Snapping	Прив’язка 	Snap to Tangent	Прив’язати до дотичної 	Snap to Perpendicular	Прив’язати до перпендикуляра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@@ -4986,6 +4986,8 @@ theme	Appearance	Giao diện @msg	too many library panels were opened in this session	đã mở quá nhiều bảng thư viện trong phiên này @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.	Ảnh có kích thước {_1} × {_2} điểm ảnh, hơn {LARGE_TRACE_SIDE} ở một cạnh: ảnh vẫn được dò, nhưng chậm hơn. Một thiết lập sẵn có độ trung thực thấp hơn hoặc một bản sao nhỏ hơn của ảnh sẽ dò nhanh hơn. @msg	The image was traced at {_1} × {_2} pixels, scaled down from {_3} × {_4} ({_5} megapixels at most).	Ảnh đã được dò ở {_1} × {_2} điểm ảnh, thu nhỏ từ {_3} × {_4} (tối đa {_5} megapixel).+@msg	byColor must be true or false	byColor phải là true hoặc false+@msg	byColor can't split Auto Grouping groups: trace without Auto Grouping	byColor không thể tách các nhóm của Tự động nhóm: hãy dò mà không bật Tự động nhóm 	Snapping	Hút 	Snap to Tangent	Hút vào tiếp tuyến 	Snap to Perpendicular	Hút vào đường vuông gócdiff --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@@ -5225,6 +5225,8 @@ lighting	Light	光源 @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	byColor must be true or false	byColor 必须是 true 或 false+@msg	byColor can't split Auto Grouping groups: trace without Auto Grouping	byColor 无法拆分自动编组生成的编组:请在不使用自动编组的情况下描摹 	Snapping	对齐 	Snap to Tangent	对齐切线 	Snap to Perpendicular	对齐垂线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@@ -2375,6 +2375,8 @@ file-filter	Templates	範本 @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 traced the image at {_1} × {_2} pixels, averaged down from {_3} × {_4}.	「扁平標誌」模式已以 {_1} × {_2} 像素描圖,由 {_3} × {_4} 平均縮小而來。+@msg	byColor must be true or false	byColor 必須是 true 或 false+@msg	byColor can't split Auto Grouping groups: trace without Auto Grouping	byColor 無法拆分自動群組產生的群組:請不使用自動群組來描圖 # Links and Attributes: files and URLs handed to other apps. @msg	Edit Original opens image and document files only; “{name}” is not one	「編輯原稿」只能開啟影像與文件檔案;「{name}」不屬於這兩類 @msg	only http, https and mailto URLs open in the browser, not “{url}”	只有 http、https 和 mailto URL 會在瀏覽器中開啟,「{url}」不會diff --git a/docs/mcp.md b/docs/mcp.md--- a/docs/mcp.md+++ b/docs/mcp.md@@ -1496,6 +1496,15 @@ 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 `byColor: true`, `imageTrace.expand` and `imageTrace.makeAndExpand` put each color's paths in a group of their+  own, named by the global swatch the color links to (a tint with its percentage, as `Sky 50%`) or as `#rrggbb`;+  gradient fills share a group named `Gradients`. The groups stack in the order of the lowest path in each, and each+  keeps its paths' order. With `imageTrace.expand`, and with `imageTrace.makeAndExpand` when the trace's paths can+  overlap (Overlapping, Flat Logo, Create Strokes), a path that would move under an overlapping path of another color+  (by the bounding boxes of what they paint, effects and shadows included) if it joined its color's group starts+  another group of its color instead, and the renderer draws the art unchanged. In a Flat Logo trace of a black ring+  around a white disc around a black dot, black gets two groups. `layer.releaseToLayers` on the expanded group then+  gives each group a sublayer. A trace made with `autoGrouping` is refused. - 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 run