From f118cf6aa654721c8ff3a11edbfe34e7ed605932 Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Sun, 8 Nov 2026 13:44:44 +0700 Subject: [PATCH] Image Trace: Expand can group the traced paths by color Squashed from #4058 onto main. --- crates/engine/src/cmd/buildcmds.rs | 4 +- crates/engine/src/cmd/buildcmds/trace.rs | 347 ++++++++++++++++++++++- crates/engine/src/tests/build.rs | 310 ++++++++++++++++++++ crates/ui-egui/src/i18n/ca.tsv | 3 + crates/ui-egui/src/i18n/cs.tsv | 3 + crates/ui-egui/src/i18n/de.tsv | 3 + crates/ui-egui/src/i18n/el.tsv | 3 + crates/ui-egui/src/i18n/es.tsv | 3 + crates/ui-egui/src/i18n/fi.tsv | 3 + crates/ui-egui/src/i18n/fr.tsv | 3 + crates/ui-egui/src/i18n/hu.tsv | 3 + crates/ui-egui/src/i18n/it.tsv | 3 + crates/ui-egui/src/i18n/ja.tsv | 3 + crates/ui-egui/src/i18n/ka.tsv | 3 + crates/ui-egui/src/i18n/ko.tsv | 3 + crates/ui-egui/src/i18n/pl.tsv | 3 + crates/ui-egui/src/i18n/pt-br.tsv | 3 + crates/ui-egui/src/i18n/ru.tsv | 3 + crates/ui-egui/src/i18n/sv.tsv | 3 + crates/ui-egui/src/i18n/tr.tsv | 3 + crates/ui-egui/src/i18n/uk.tsv | 3 + crates/ui-egui/src/i18n/vi.tsv | 3 + crates/ui-egui/src/i18n/zh-hans.tsv | 3 + crates/ui-egui/src/i18n/zh-hant.tsv | 3 + docs/mcp.md | 11 + 25 files changed, 732 insertions(+), 3 deletions(-) diff --git a/crates/engine/src/cmd/buildcmds.rs b/crates/engine/src/cmd/buildcmds.rs index f54752632..1b37ca96f 100644 --- a/crates/engine/src/cmd/buildcmds.rs +++ b/crates/engine/src/cmd/buildcmds.rs @@ -153,7 +153,7 @@ pub fn specs() -> Vec { "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) ), @@ -171,7 +171,7 @@ pub fn specs() -> Vec { "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; refused for a trace made with autoGrouping and for a trace with a live Pathfinder effect)} keep only the traced paths (drop the source image) → {ids}", has_selection_or_ids, trace_expand ), diff --git a/crates/engine/src/cmd/buildcmds/trace.rs b/crates/engine/src/cmd/buildcmds/trace.rs index 00bcc1c83..1924ed405 100644 --- a/crates/engine/src/cmd/buildcmds/trace.rs +++ b/crates/engine/src/cmd/buildcmds/trace.rs @@ -4,10 +4,16 @@ use vectorcraft_color::Swatch; use vectorcraft_doc::ImageObject; use super::{ - Appearance, AppearanceItem, Arc, Beside, Color, Document, EngineError, LineCap, LineJoin, LiveShape, Node, NodeId, NodeKind, Paint, Point, + Appearance, AppearanceItem, Arc, Beside, Color, Document, EngineError, LineCap, LineJoin, LiveShape, Node, NodeId, NodeKind, Paint, Point, Rect, Result, Session, StrokeLayer, TRACE_NAME, TraceView, Value, bad, fill_only, ids_json, json, root_ids, shape_node, str_param, switch_or, tr, }; +#[cfg(test)] +thread_local! { + static BOX_TESTS: std::cell::Cell = const { std::cell::Cell::new(0) }; + static CANDIDATE_VISITS: std::cell::Cell = const { std::cell::Cell::new(0) }; +} + // ---------- Image Trace ---------- fn is_trace_group(n: &Node) -> bool { @@ -16,6 +22,8 @@ fn is_trace_group(n: &Node) -> bool { && n.children().is_some_and(|c| c.first().is_some_and(|i| matches!(i.kind, NodeKind::Image(_)))) } +const LIVE_PATHFINDER_BY_COLOR: &str = "byColor can't regroup a trace with a live Pathfinder effect: expand without byColor"; + /// The `view` param ([`TraceView`] id), if given. fn view_param(cmd: &str, p: &Value) -> Result> { let Some(v) = p.get("view").filter(|v| !v.is_null()) else { return Ok(None) }; @@ -368,6 +376,156 @@ fn grouped(d: &mut Document, nodes: Vec, parents: &[Option]) -> 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 { + 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), + } +} + +/// 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 meet, an item joins its paint's last group only if no item of a later group meets it +/// (by the boxes of everything they paint, effects included, within one point); otherwise it +/// starts another group of that paint, which keeps the stacking order of those items. Items +/// farther apart can still share antialiased pixels at small view or raster-export scales, so +/// their edge pixels may change. A group among `items` (Auto Grouping) is refused. +fn color_groups(cmd: &str, d: &mut Document, items: Vec>) -> Result>> { + use std::collections::{BTreeMap, HashMap}; + use std::ops::Bound::{Excluded, Unbounded}; + 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 { + name: String, + members: Vec>, + } + // Boxes closer than a point count as overlapping: their antialiased edges can share pixels. + let meet = |a: &Rect, b: &Rect| { + #[cfg(test)] + BOX_TESTS.with(|n| n.set(n.get() + 1)); + let b = b.inflate(1.0, 1.0); + a.x0 < b.x1 && b.x0 < a.x1 && a.y0 < b.y1 && b.y0 < a.y1 + }; + // Choose a cell size from the painted extent and path count. A box over more than eight cells + // across but at most eight down (a stripe) is indexed by its rows, one the other way round by + // its columns, and one over more than eight both ways stays in a separate list, keeping the + // index bounded even when a path covers most of the document. + let items: Vec<_> = items + .into_iter() + .map(|item| { + let bounds = vectorcraft_render::painted_bounds(d, &item); + (item, bounds) + }) + .collect(); + let extent = items.iter().filter_map(|(_, b)| *b).reduce(|a, b| a.union(b)); + let cell_size = extent.map_or(1.0, |r| ((r.x1 - r.x0).abs() * (r.y1 - r.y0).abs() / items.len().max(1) as f64).sqrt().max(1.0)); + // The first and last column and row of the cells `r` covers; `None` when it isn't finite. + let span = |r: Rect| -> Option<(i64, i64, i64, i64)> { + let coord = |v: f64| (v / cell_size).floor() as i64; + [r.x0, r.y0, r.x1, r.y1].iter().all(|v| v.is_finite()).then(|| (coord(r.x0), coord(r.y0), coord(r.x1), coord(r.y1))) + }; + let count = |first: i64, last: i64| i128::from(last) - i128::from(first) + 1; + // A box over more cells than this is checked against every box. + const MAX_CELLS: i128 = 1024; + let mut groups: Vec = vec![]; + let mut latest: HashMap = HashMap::new(); + // An item can join an earlier group, so insertion order is not group-index order. Index + // boxes by group to skip older groups without scanning their entries. + let mut spatial: HashMap<(i64, i64), BTreeMap>> = HashMap::new(); + let mut rows: HashMap>> = HashMap::new(); + let mut columns: HashMap>> = HashMap::new(); + let mut large: BTreeMap> = BTreeMap::new(); + let mut all_boxes: BTreeMap> = BTreeMap::new(); + for (item, bounds) 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()), + }; + // Joining an earlier group moves the item under every later group's items. + let hidden_by_later = |at: usize| { + bounds.is_some_and(|b| { + let overlaps_later = |boxes: &BTreeMap>| { + boxes.range((Excluded(at), Unbounded)).rev().any(|(_, group_boxes)| { + group_boxes.iter().rev().any(|box_bounds| { + #[cfg(test)] + CANDIDATE_VISITS.with(|n| n.set(n.get() + 1)); + meet(box_bounds, &b) + }) + }) + }; + match span(b) { + Some((x0, y0, x1, y1)) if count(x0, x1) * count(y0, y1) <= MAX_CELLS => { + (y0..=y1).flat_map(|y| (x0..=x1).map(move |x| (x, y))).filter_map(|c| spatial.get(&c)).any(overlaps_later) + || (y0..=y1).filter_map(|y| rows.get(&y)).any(overlaps_later) + || (x0..=x1).filter_map(|x| columns.get(&x)).any(overlaps_later) + || overlaps_later(&large) + } + _ => overlaps_later(&all_boxes), + } + }) + }; + let at = latest.get(&key).copied().filter(|&at| !hidden_by_later(at)); + let mut started = Run { name, members: vec![] }; + let joins = at.and_then(|at| groups.get_mut(at)); + let new = joins.is_none(); + let g = joins.unwrap_or(&mut started); + g.members.push(item); + if new { + groups.push(started); + latest.insert(key, groups.len() - 1); + } + if let Some(b) = bounds { + let index = if new { groups.len() - 1 } else { at.unwrap_or_default() }; + all_boxes.entry(index).or_default().push(b); + match span(b.inflate(1.0, 1.0)) { + Some((x0, y0, x1, y1)) if count(x0, x1) <= 8 && count(y0, y1) <= 8 => { + for place in (y0..=y1).flat_map(|y| (x0..=x1).map(move |x| (x, y))) { + spatial.entry(place).or_default().entry(index).or_default().push(b); + } + } + Some((_, y0, _, y1)) if count(y0, y1) <= 8 => { + for y in y0..=y1 { + rows.entry(y).or_default().entry(index).or_default().push(b); + } + } + Some((x0, _, x1, _)) if count(x0, x1) <= 8 => { + for x in x0..=x1 { + columns.entry(x).or_default().entry(index).or_default().push(b); + } + } + _ => large.entry(index).or_default().push(b), + } + } + } + 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()) +} + /// What a trace depends on: the object traced (an image, or an Image Trace object traced again), /// its image, and the settings it is traced with. The tracer makes the same paths of the same /// inputs, so a trace made away from the command ([`TraceRun`]) stands in for the command's own. @@ -398,6 +556,10 @@ struct Prepared { fn prepare(s: &Session, c: &str, p: &Value) -> Result { let mut params = trace_params(s, c, p)?; let view = view_param(c, p)?; + let by_color = c == "imageTrace.makeAndExpand" && 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(¶ms).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 (a // saved preset's under its saved name), else "Custom". @@ -428,6 +590,9 @@ fn prepare(s: &Session, c: &str, p: &Value) -> Result { }) .ok_or_else(|| bad(c, "select an image (or an Image Trace group)"))? }; + if by_color && s.doc()?.doc.node(target).is_some_and(vectorcraft_render::effects::has_pathfinder) { + return Err(bad(c, LIVE_PATHFINDER_BY_COLOR)); + } let blob = s.doc()?.doc.images.get(&image.key).ok_or_else(|| EngineError::Other(format!("image data `{}` is missing", image.key)))?; let bytes = Arc::clone(&blob.bytes); let library = trace_library(c, s, &mut params)?; @@ -533,6 +698,7 @@ impl TraceRun { pub(super) fn trace_make(s: &mut Session, p: &Value, expand: bool) -> Result { let c = if expand { "imageTrace.makeAndExpand" } else { "imageTrace.make" }; let Prepared { inputs, trace, view, params_json, preset_label, library, bytes } = prepare(s, c, p)?; + let group_by_color = expand && by_color_param(c, p)?; // A saved preset's settings join the journal entry, so a replay traces as this run did after // the preset is changed, renamed or deleted. let named = str_param(p, "preset").unwrap_or("Default"); @@ -608,6 +774,9 @@ pub(super) fn trace_make(s: &mut Session, p: &Value, expand: bool) -> Result Result { } pub(super) fn trace_expand(s: &mut Session, p: &Value) -> Result { + 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")); + } + if targets.iter().any(|g| d.node(*g).is_some_and(vectorcraft_render::effects::has_pathfinder)) { + return Err(bad(C, LIVE_PATHFINDER_BY_COLOR)); + } + } s.edit("Expand Image Trace", |d, sel| { for g in &targets { let n = d.node_mut(*g).ok_or(EngineError::NoNode(*g))?; @@ -715,6 +897,15 @@ pub(super) fn trace_expand(s: &mut Session, p: &Value) -> Result { 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)?; + if let Some(ch) = d.node_mut(*g).and_then(Node::children_mut) { + *ch = groups; + } + } } sel.set(targets.iter().copied()); Ok(()) @@ -722,6 +913,160 @@ pub(super) fn trace_expand(s: &mut Session, p: &Value) -> Result { Ok(json!({ "ids": ids_json(&targets) })) } +#[cfg(test)] +mod color_group_tests { + use super::*; + use vectorcraft_geom::{PathData, SubPath}; + + fn area(d: &mut Document, x0: f64, y0: f64, x1: f64, y1: f64, color: Color) -> Arc { + let path = PathData::single(SubPath::polyline(&[Point::new(x0, y0), Point::new(x1, y0), Point::new(x1, y1), Point::new(x0, y1)], true)); + Arc::new(Node::path(d.alloc_id(), path, fill_only(Paint::solid(color)))) + } + + #[test] + fn abutting_color_groups_preserve_order_at_shared_edges() { + let mut d = Document::new(100.0, 100.0); + let a = Color::rgb(1.0, 0.0, 0.0); + let b = Color::rgb(0.0, 0.0, 1.0); + let items = vec![area(&mut d, 0.0, 0.0, 20.0, 20.0, a), area(&mut d, 20.0, 0.0, 40.0, 20.0, b), area(&mut d, 40.0, 0.0, 60.0, 20.0, a)]; + let original: Vec<_> = items.iter().map(|n| n.id).collect(); + let groups = color_groups("imageTrace.makeAndExpand", &mut d, items).unwrap(); + let actual: Vec<_> = groups.iter().flat_map(|g| g.children().unwrap().iter().map(|n| n.id)).collect(); + assert_eq!(actual, original, "shared-edge paths changed stacking order"); + } + + #[test] + fn dense_color_groups_use_bounded_box_tests() { + let mut d = Document::new(200.0, 200.0); + let a = Color::rgb(1.0, 0.0, 0.0); + let b = Color::rgb(0.0, 0.0, 1.0); + let items = (0..4000) + .map(|i| { + if i % 2 == 0 { + let x = 100.0 + (i % 40) as f64 * 0.01; + area(&mut d, x, 100.0, x + 0.1, 100.1, a) + } else { + let x = if i % 4 == 1 { 0.0 } else { 198.0 }; + area(&mut d, x, x, x + 2.0, x + 2.0, b) + } + }) + .collect(); + BOX_TESTS.with(|n| n.set(0)); + let groups = color_groups("imageTrace.makeAndExpand", &mut d, items).unwrap(); + let tests = BOX_TESTS.with(std::cell::Cell::get); + assert_eq!(groups.iter().map(|g| g.children().map_or(0, Vec::len)).sum::(), 4000); + assert!(tests < 4000 * 32, "{tests} box tests for 4000 paths"); + } + + #[test] + fn overlapping_alternating_colors_visit_bounded_candidates() { + const COUNT: usize = 3000; + let mut d = Document::new(100.0, 100.0); + let colors = [Color::rgb(1.0, 0.0, 0.0), Color::rgb(0.0, 0.0, 1.0)]; + let items = (0..COUNT).map(|i| area(&mut d, 10.0, 10.0, 20.0, 20.0, colors[i % 2])).collect(); + CANDIDATE_VISITS.with(|n| n.set(0)); + let groups = color_groups("imageTrace.makeAndExpand", &mut d, items).unwrap(); + let visits = CANDIDATE_VISITS.with(std::cell::Cell::get); + assert_eq!(groups.len(), COUNT); + assert!(visits < COUNT * 16, "{visits} candidate visits for {COUNT} paths"); + } + + #[test] + fn separate_stripes_of_alternating_colors_visit_bounded_candidates() { + const COUNT: usize = 2000; + let mut d = Document::new(1000.0, 4000.0); + let colors = [Color::rgb(1.0, 0.0, 0.0), Color::rgb(0.0, 0.0, 1.0)]; + for tall in [false, true] { + let items = (0..COUNT) + .map(|i| { + let at = i as f64 * 2.0; + if tall { area(&mut d, at, 0.0, at + 0.5, 1000.0, colors[i % 2]) } else { area(&mut d, 0.0, at, 1000.0, at + 0.5, colors[i % 2]) } + }) + .collect(); + CANDIDATE_VISITS.with(|n| n.set(0)); + let groups = color_groups("imageTrace.makeAndExpand", &mut d, items).unwrap(); + let visits = CANDIDATE_VISITS.with(std::cell::Cell::get); + assert_eq!(groups.len(), 2, "tall {tall}"); + assert!(visits < COUNT * 32, "tall {tall}: {visits} candidate visits for {COUNT} stripes"); + } + } + + #[test] + fn spatial_color_groups_match_simple_scan() { + let colors = [Color::rgb(1.0, 0.0, 0.0), Color::rgb(0.0, 0.0, 1.0), Color::rgb(0.0, 1.0, 0.0)]; + for layout in 0..6 { + let mut d = Document::new(200.0, 200.0); + let items: Vec<_> = (0..90) + .map(|i| { + let x = match layout { + 0 => i as f64 * 2.0, + 1 => (i % 10) as f64 * 5.0, + 2 => (i % 7) as f64 * 3.0, + 3 => (i % 13) as f64 * 9.0, + 4 => (i % 4) as f64 * 10.0, + _ => i as f64 * 1.5, + }; + let y = match layout { + 0 => 0.0, + 1 => (i / 10) as f64 * 5.0, + 2 => (i / 7) as f64 * 3.0, + 3 => (i / 13) as f64 * 9.0, + 4 => i as f64 * 1.5, + _ => (i % 4) as f64 * 10.0, + }; + // Layouts 4 and 5: wide and tall stripes, some meeting their neighbors. + let (w, h) = match layout { + 4 => (150.0, 1.0), + 5 => (1.0, 150.0), + _ => (3.0, 3.0), + }; + area(&mut d, x, y, x + w, y + h, colors[i % 3]) + }) + .collect(); + let mut expected: Vec<(String, Vec<(NodeId, Rect)>)> = vec![]; + for item in &items { + let key = item.appearance.fill_paint().color().unwrap().to_hex(); + let bounds = vectorcraft_render::painted_bounds(&d, item).unwrap(); + let previous = expected.iter().rposition(|(k, _)| *k == key); + let can_join = previous.filter(|&at| { + !expected.iter().skip(at + 1).any(|(_, boxes)| { + boxes.iter().any(|(_, m)| { + let b = bounds.inflate(1.0, 1.0); + m.x0 < b.x1 && b.x0 < m.x1 && m.y0 < b.y1 && b.y0 < m.y1 + }) + }) + }); + match can_join.and_then(|at| expected.get_mut(at)) { + Some((_, members)) => members.push((item.id, bounds)), + None => expected.push((key, vec![(item.id, bounds)])), + } + } + let actual = color_groups("imageTrace.makeAndExpand", &mut d, items).unwrap(); + let actual: Vec> = actual.iter().map(|g| g.children().unwrap().iter().map(|n| n.id).collect()).collect(); + let expected: Vec> = expected.into_iter().map(|(_, members)| members.into_iter().map(|(id, _)| id).collect()).collect(); + assert_eq!(actual, expected, "layout {layout}"); + } + } + + #[test] + fn an_abutting_gradient_stays_above_the_solid_area_it_borders() { + use vectorcraft_color::{Gradient, GradientKind, GradientPaint, GradientStop}; + let stops = vec![GradientStop::new(0.0, Color::rgb(1.0, 0.0, 0.0)), GradientStop::new(1.0, Color::rgb(0.0, 0.0, 1.0))]; + let ramp = || Paint::Gradient(Box::new(GradientPaint::new(Gradient::new(GradientKind::Linear, stops.clone())))); + let mut d = Document::new(100.0, 100.0); + let mut area = |x0: f64, x1: f64, paint: Paint| { + let path = + PathData::single(SubPath::polyline(&[Point::new(x0, 0.0), Point::new(x1, 0.0), Point::new(x1, 60.0), Point::new(x0, 60.0)], true)); + Arc::new(Node::path(d.alloc_id(), path, fill_only(paint))) + }; + // A gradient, a solid area beside it and a second gradient beside that, bottom to top. + let items = vec![area(0.0, 50.0, ramp()), area(50.0, 80.0, Paint::solid(Color::rgb(0.2, 0.6, 0.2))), area(80.0, 100.0, ramp())]; + let groups = color_groups("imageTrace.makeAndExpand", &mut d, items).unwrap(); + let names: Vec<_> = groups.iter().map(|g| g.name.clone().unwrap_or_default()).collect(); + assert_eq!(names, ["Gradients", "#339933", "Gradients"]); + } +} + #[cfg(test)] mod trace_limit_tests { use super::{MAX_TRACE_ANCHORS, trace_warnings}; diff --git a/crates/engine/src/tests/build.rs b/crates/engine/src/tests/build.rs index 9e397975f..a94696d16 100644 --- a/crates/engine/src/tests/build.rs +++ b/crates/engine/src/tests/build.rs @@ -3221,6 +3221,316 @@ 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] +#[allow(clippy::cognitive_complexity)] // Exercises several color and stacking cases in one trace fixture. +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::>(); + 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::>() + }; + 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(); + // Without byColor (the default) the paths sit in the trace's group, with no group of their own. + assert!(paths.iter().all(|p| !matches!(p.kind, NodeKind::Group { .. })), "{method}"); + let (default, dg) = { + let mut s = session(); + add_image(&mut s, &art, Affine::IDENTITY); + let r = s.execute("imageTrace.makeAndExpand", &json!({"params": params})).unwrap(); + (s, NodeId(r["id"].as_u64().unwrap())) + }; + assert!(*node(&default, dg).children().unwrap() == paths, "{method}: byColor is false when not given"); + // The colors in the order they first come in, bottom to top. + let mut order: Vec = 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> = 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")); + + // A trace of one color gives one group holding every path. + let mut one = session(); + let bar = + vectorcraft_trace::Raster::from_fn(80, 40, |x, y| if (10..70).contains(&x) && (10..30).contains(&y) { [0, 0, 0, 255] } else { [255; 4] }); + add_image(&mut one, &bar, Affine::IDENTITY); + let r = one.execute("imageTrace.makeAndExpand", &json!({"params": {"ignoreWhite": true}, "byColor": true})).unwrap(); + let og = node(&one, NodeId(r["id"].as_u64().unwrap())); + assert_eq!(names(&og), ["#000000"]); + let only = &og.children().unwrap()[0]; + assert!(!only.children().unwrap().is_empty() && only.children().unwrap().iter().all(|m| hex(m) == "#000000")); + + // 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::>(); + 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", ¶ms).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::>(), ["#000000", "#ffffff", "#000000"]); + assert_eq!(names(&node(&s, g)), ["#000000", "#ffffff", "#000000"], "expand after: {expand_after}"); + let members: Vec> = 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), moved clear of it with a drop shadow cast onto it, or moved + // to share a pixel at its edge, 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> = 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 { + "beside" => { + // The middle path and the moved top path end and start inside one pixel. + s.execute("object.move", &json!({"ids": [middle.id.0], "dx": -0.5, "dy": 0})).unwrap(); + s.execute("object.move", &json!({"ids": [last.id.0], "dx": -0.3, "dy": 0})).unwrap() + } + "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", "beside"] { + 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 = 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::>(), [1, 2]); +} + +#[test] +fn image_trace_expand_by_color_preserves_live_pathfinder_art() { + let mut s = session(); + let art = vectorcraft_trace::Raster::from_fn(100, 60, |x, y| { + if (10..40).contains(&y) && ((10..35).contains(&x) || (65..90).contains(&x)) { [0, 0, 0, 255] } else { [255, 255, 255, 255] } + }); + add_image(&mut s, &art, Affine::IDENTITY); + let g = NodeId(s.execute("imageTrace.make", &json!({"params": {"ignoreWhite": true}})).unwrap()["id"].as_u64().unwrap()); + let paths: Vec = node(&s, g).children().unwrap().iter().filter(|c| c.path_data().is_some()).map(|c| c.id).collect(); + assert_eq!(paths.len(), 2); + s.execute("select.set", &json!({"ids": [paths[1].0]})).unwrap(); + s.execute("object.move", &json!({"dx": -45, "dy": 0})).unwrap(); + s.execute("effect.apply", &json!({"ids": [g.0], "effect": "pathfinder.subtract"})).unwrap(); + let before = render_doc(&s, false); + let before_pixels: Vec<_> = (0..100).flat_map(|x| (0..60).map(move |y| (x, y))).map(|(x, y)| before.pixel(x, y)).collect(); + let original = node(&s, g); + let steps = s.doc().unwrap().history.undo.len(); + let result = s.execute("imageTrace.expand", &json!({"id": g.0, "byColor": true})); + if let Err(e) = &result { + assert!(e.to_string().contains("Pathfinder"), "{e}"); + assert_eq!(node(&s, g), original); + assert_eq!(s.doc().unwrap().history.undo.len(), steps); + } + let after = render_doc(&s, false); + let after_pixels: Vec<_> = (0..100).flat_map(|x| (0..60).map(move |y| (x, y))).map(|(x, y)| after.pixel(x, y)).collect(); + let changed = after_pixels.iter().zip(&before_pixels).filter(|(a, b)| a != b).count(); + assert_eq!(changed, 0, "byColor changed {changed} live Pathfinder pixels: {result:?}"); + assert!(result.is_err(), "byColor must refuse a live Pathfinder operand group"); + let error = s.execute("imageTrace.makeAndExpand", &json!({"id": g.0, "params": {"ignoreWhite": true}, "byColor": true})).unwrap_err(); + assert!(error.to_string().contains("Pathfinder"), "{error}"); + assert_eq!(node(&s, g), original); + assert_eq!(s.doc().unwrap().history.undo.len(), steps); +} + #[test] fn image_trace_make_and_expand_and_color_presets() { let mut s = session(); diff --git a/crates/ui-egui/src/i18n/ca.tsv b/crates/ui-egui/src/i18n/ca.tsv index 68c1e81b0..0f39585bb 100644 --- a/crates/ui-egui/src/i18n/ca.tsv +++ b/crates/ui-egui/src/i18n/ca.tsv @@ -6127,6 +6127,9 @@ anchor Points: Punts: CMYK Images Imatges CMYK Grayscale Images Imatges en escala de grisos @msg `kind` must be spotColors, colorEffects, rgbImages, cmykImages or grayImages `kind` ha de ser spotColors, colorEffects, rgbImages, cmykImages o grayImages +@msg byColor must be true or false byColor ha de ser true o false +@msg byColor can't split Auto Grouping groups: trace without Auto Grouping byColor no pot separar els grups de l'agrupació automàtica: traça sense l'agrupació automàtica +@msg byColor can't regroup a trace with a live Pathfinder effect: expand without byColor byColor no pot tornar a agrupar un traçat amb un efecte Pathfinder actiu: expandeix-lo sense byColor # Color Effects add-on: Vector Halftone (Effect › Color Effects). Dot Color from Art Color dels punts de la il·lustració diff --git a/crates/ui-egui/src/i18n/cs.tsv b/crates/ui-egui/src/i18n/cs.tsv index e74e240d7..13700edfa 100644 --- a/crates/ui-egui/src/i18n/cs.tsv +++ b/crates/ui-egui/src/i18n/cs.tsv @@ -5711,6 +5711,9 @@ 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í +@msg byColor can't regroup a trace with a live Pathfinder effect: expand without byColor byColor nemůže přeskupit trasování s živým efektem Pathfinder: rozbalte bez byColor Snapping Přichytávání Snap to Tangent Přichytávat k tečně Snap to Perpendicular Přichytávat ke kolmici diff --git a/crates/ui-egui/src/i18n/de.tsv b/crates/ui-egui/src/i18n/de.tsv index 2da424151..9938513a8 100644 --- a/crates/ui-egui/src/i18n/de.tsv +++ b/crates/ui-egui/src/i18n/de.tsv @@ -5290,6 +5290,9 @@ 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 +@msg byColor can't regroup a trace with a live Pathfinder effect: expand without byColor byColor kann eine Nachzeichnung mit einem aktiven Pathfinder-Effekt nicht neu gruppieren: ohne byColor umwandeln Snapping Einrasten Snap to Tangent An Tangente ausrichten Snap to Perpendicular An Senkrechter ausrichten diff --git a/crates/ui-egui/src/i18n/el.tsv b/crates/ui-egui/src/i18n/el.tsv index 4d713dfed..c2330c4bf 100644 --- a/crates/ui-egui/src/i18n/el.tsv +++ b/crates/ui-egui/src/i18n/el.tsv @@ -5662,6 +5662,9 @@ 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 δεν μπορεί να χωρίσει τις ομάδες της Αυτόματης ομαδοποίησης: ανιχνεύστε χωρίς Αυτόματη ομαδοποίηση +@msg byColor can't regroup a trace with a live Pathfinder effect: expand without byColor το byColor δεν μπορεί να αναομαδοποιήσει μια ανίχνευση με ενεργό εφέ Pathfinder: επεκτείνετε χωρίς 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 index 8c00c78d2..e4807fae6 100644 --- a/crates/ui-egui/src/i18n/es.tsv +++ b/crates/ui-egui/src/i18n/es.tsv @@ -5652,6 +5652,9 @@ 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 +@msg byColor can't regroup a trace with a live Pathfinder effect: expand without byColor byColor no puede reagrupar un calco con un efecto Pathfinder activo: expanda sin byColor Snapping Ajuste Snap to Tangent Ajustar a tangente Snap to Perpendicular Ajustar a perpendicular diff --git a/crates/ui-egui/src/i18n/fi.tsv b/crates/ui-egui/src/i18n/fi.tsv index 3491ca30a..3970072a3 100644 --- a/crates/ui-egui/src/i18n/fi.tsv +++ b/crates/ui-egui/src/i18n/fi.tsv @@ -5674,6 +5674,9 @@ 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ä +@msg byColor can't regroup a trace with a live Pathfinder effect: expand without byColor byColor ei voi ryhmitellä uudelleen jäljitystä, jossa on aktiivinen Pathfinder-tehoste: laajenna ilman byColor-asetusta Snapping Tartunta Snap to Tangent Kohdista tangenttiin Snap to Perpendicular Kohdista kohtisuoraan diff --git a/crates/ui-egui/src/i18n/fr.tsv b/crates/ui-egui/src/i18n/fr.tsv index 539c0f3ae..1dd0177b6 100644 --- a/crates/ui-egui/src/i18n/fr.tsv +++ b/crates/ui-egui/src/i18n/fr.tsv @@ -5706,6 +5706,9 @@ 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 +@msg byColor can't regroup a trace with a live Pathfinder effect: expand without byColor byColor ne peut pas regrouper un tracé avec un effet Pathfinder actif : développez sans byColor Snapping Magnétisme Snap to Tangent Magnétisme de la tangente Snap to Perpendicular Magnétisme de la perpendiculaire diff --git a/crates/ui-egui/src/i18n/hu.tsv b/crates/ui-egui/src/i18n/hu.tsv index 247ffc64e..fe27d894f 100644 --- a/crates/ui-egui/src/i18n/hu.tsv +++ b/crates/ui-egui/src/i18n/hu.tsv @@ -5694,6 +5694,9 @@ 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 +@msg byColor can't regroup a trace with a live Pathfinder effect: expand without byColor a byColor nem csoportosíthatja át az élő Pathfinder-effektussal rendelkező vektorizálást: bővítse ki byColor nélkül Snapping Illesztés Snap to Tangent Illesztés érintőre Snap to Perpendicular Illesztés merőlegesre diff --git a/crates/ui-egui/src/i18n/it.tsv b/crates/ui-egui/src/i18n/it.tsv index 44da4c994..cf88a606a 100644 --- a/crates/ui-egui/src/i18n/it.tsv +++ b/crates/ui-egui/src/i18n/it.tsv @@ -5324,6 +5324,9 @@ 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 +@msg byColor can't regroup a trace with a live Pathfinder effect: expand without byColor byColor non può raggruppare di nuovo una traccia con un effetto Pathfinder attivo: espandi senza byColor Snapping Aggancio Snap to Tangent Aggancia alla tangente Snap to Perpendicular Aggancia alla perpendicolare diff --git a/crates/ui-egui/src/i18n/ja.tsv b/crates/ui-egui/src/i18n/ja.tsv index 07577badf..d4ee9ab66 100644 --- a/crates/ui-egui/src/i18n/ja.tsv +++ b/crates/ui-egui/src/i18n/ja.tsv @@ -5367,6 +5367,9 @@ 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 は自動グループ化のグループを分割できません: 自動グループ化なしでトレースしてください +@msg byColor can't regroup a trace with a live Pathfinder effect: expand without byColor byColor はライブ Pathfinder 効果のあるトレースを再グループ化できません: 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 index 6a5581ed4..f46a2eb14 100644 --- a/crates/ui-egui/src/i18n/ka.tsv +++ b/crates/ui-egui/src/i18n/ka.tsv @@ -5557,6 +5557,9 @@ imageTrace Outline კონტური @msg `colorGroup` needs `mode: "color"` `colorGroup`-ს სჭირდება `mode: "color"` @msg no color group `{name}` in the document (see swatch.list) დოკუმენტში ფერების ჯგუფი `{name}` არ არის (იხ. swatch.list) @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 byColor can't regroup a trace with a live Pathfinder effect: expand without byColor byColor ვერ გადააჯგუფებს ტრასირებას აქტიური Pathfinder ეფექტით: გააფართოვეთ 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 index cce6ce736..9b6c84093 100644 --- a/crates/ui-egui/src/i18n/ko.tsv +++ b/crates/ui-egui/src/i18n/ko.tsv @@ -5285,6 +5285,9 @@ 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는 자동 그룹화 그룹을 나눌 수 없습니다. 자동 그룹화 없이 추적하세요 +@msg byColor can't regroup a trace with a live Pathfinder effect: expand without byColor byColor는 라이브 Pathfinder 효과가 있는 추적을 다시 그룹화할 수 없습니다. 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 index adfb669e8..f00ae7858 100644 --- a/crates/ui-egui/src/i18n/pl.tsv +++ b/crates/ui-egui/src/i18n/pl.tsv @@ -5564,6 +5564,9 @@ 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 +@msg byColor can't regroup a trace with a live Pathfinder effect: expand without byColor byColor nie może ponownie zgrupować obrysu z aktywnym efektem Pathfinder: rozwiń bez byColor Snapping Przyciąganie Snap to Tangent Przyciągaj do stycznych Snap to Perpendicular Przyciągaj do prostopadłych diff --git a/crates/ui-egui/src/i18n/pt-br.tsv b/crates/ui-egui/src/i18n/pt-br.tsv index 183d41dc1..5d277fab5 100644 --- a/crates/ui-egui/src/i18n/pt-br.tsv +++ b/crates/ui-egui/src/i18n/pt-br.tsv @@ -5626,6 +5626,9 @@ 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 +@msg byColor can't regroup a trace with a live Pathfinder effect: expand without byColor byColor não pode reagrupar um traçado com um efeito Pathfinder ativo: expanda sem byColor Snapping Ajuste Snap to Tangent Ajustar à Tangente Snap to Perpendicular Ajustar à Perpendicular diff --git a/crates/ui-egui/src/i18n/ru.tsv b/crates/ui-egui/src/i18n/ru.tsv index 544050c9a..42d30eadd 100644 --- a/crates/ui-egui/src/i18n/ru.tsv +++ b/crates/ui-egui/src/i18n/ru.tsv @@ -5319,6 +5319,9 @@ 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 не может разделить группы автогруппировки: трассируйте без автогруппировки +@msg byColor can't regroup a trace with a live Pathfinder effect: expand without byColor byColor не может перегруппировать трассировку с действующим эффектом Pathfinder: разверните без 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 index f007400d9..896b59c2e 100644 --- a/crates/ui-egui/src/i18n/sv.tsv +++ b/crates/ui-egui/src/i18n/sv.tsv @@ -5733,6 +5733,9 @@ 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 +@msg byColor can't regroup a trace with a live Pathfinder effect: expand without byColor byColor kan inte gruppera om en kalkering med en aktiv Pathfinder-effekt: expandera utan byColor Snapping Fäst Snap to Tangent Fäst mot tangent Snap to Perpendicular Fäst mot vinkelrät diff --git a/crates/ui-egui/src/i18n/tr.tsv b/crates/ui-egui/src/i18n/tr.tsv index 3c28f018c..5cf7665d4 100644 --- a/crates/ui-egui/src/i18n/tr.tsv +++ b/crates/ui-egui/src/i18n/tr.tsv @@ -5712,6 +5712,9 @@ 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 +@msg byColor can't regroup a trace with a live Pathfinder effect: expand without byColor byColor, canlı Pathfinder efekti olan bir izlemeyi yeniden gruplayamaz: byColor olmadan genişletin Gap Length: Boşluk Uzunluğu: @msg `colorGroup` needs `library: "document"` `colorGroup` için `library: "document"` gerekir @msg `colorGroup` needs `palette: "documentLibrary"` `colorGroup` için `palette: "documentLibrary"` gerekir diff --git a/crates/ui-egui/src/i18n/uk.tsv b/crates/ui-egui/src/i18n/uk.tsv index 7de17b0fc..e7055639b 100644 --- a/crates/ui-egui/src/i18n/uk.tsv +++ b/crates/ui-egui/src/i18n/uk.tsv @@ -5304,6 +5304,9 @@ 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 не може розділити групи автогрупування: трасуйте без автогрупування +@msg byColor can't regroup a trace with a live Pathfinder effect: expand without byColor byColor не може перегрупувати трасування з активним ефектом Pathfinder: розгорніть без 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 index 678eddbce..4b9de70d4 100644 --- a/crates/ui-egui/src/i18n/vi.tsv +++ b/crates/ui-egui/src/i18n/vi.tsv @@ -5305,6 +5305,9 @@ 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 +@msg byColor can't regroup a trace with a live Pathfinder effect: expand without byColor byColor không thể nhóm lại bản dò có hiệu ứng Pathfinder đang hoạt động: mở rộng mà không dùng byColor 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óc diff --git a/crates/ui-egui/src/i18n/zh-hans.tsv b/crates/ui-egui/src/i18n/zh-hans.tsv index 9a3346839..225ca1112 100644 --- a/crates/ui-egui/src/i18n/zh-hans.tsv +++ b/crates/ui-egui/src/i18n/zh-hans.tsv @@ -5542,6 +5542,9 @@ 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 无法拆分自动编组生成的编组:请在不使用自动编组的情况下描摹 +@msg byColor can't regroup a trace with a live Pathfinder effect: expand without byColor byColor 无法重新编组带有实时 Pathfinder 效果的描摹:请不使用 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 index 4b7107054..e18a0fc58 100644 --- a/crates/ui-egui/src/i18n/zh-hant.tsv +++ b/crates/ui-egui/src/i18n/zh-hant.tsv @@ -2553,6 +2553,9 @@ file-filter Templates 範本 @msg `colorGroup` needs `mode: "color"` `colorGroup` 需要 `mode: "color"` @msg no color group `{name}` in the document (see swatch.list) 文件中沒有顏色群組 `{name}` (請參閱 swatch.list) @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 byColor can't regroup a trace with a live Pathfinder effect: expand without byColor byColor 無法重新編組帶有即時 Pathfinder 效果的描圖:請不使用 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 index 6b4a6bbeb..426fb7d88 100644 --- a/docs/mcp.md +++ b/docs/mcp.md @@ -2015,6 +2015,17 @@ 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. For either Expand command, a path that would move under a path of another + color whose painted bounds, effects and shadows included, come within one document point of it starts another group + of its color instead of joining an earlier one. Paths whose painted bounds come within one document point of an + intervening color stay in separate groups to preserve their stacking order; at small view or raster-export scales, + regrouped paths farther apart can still share antialiased pixels, so edge pixels may differ. 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. Expanding by color also refuses a trace + with a live Pathfinder effect. `imageTrace.presets.save {name?, newName?, preset?, enhanced?, params?}` saves tracing settings as a preset kept with the preferences: `params` over `preset`'s (default: the saved preset `name`, else Default), checked as `imageTrace.make` checks them; without `name` it is a new "Tracing Preset N", and `newName` renames a saved preset (the reply's