Merge pull request #4678 from storytold/feat/addon-artwork-cleanup

Add-ons: Artwork Cleanup — simplify keeping shape, health check, deep ungroup, rejoin
This commit is contained in:
BFlat
2026-10-30 13:34:42 +07:00
committed by GitHub
45 changed files with 3461 additions and 3 deletions
+2
View File
@@ -1,5 +1,6 @@
//! The command registry. Ids follow Illustrator's menu structure.
mod addons;
pub(crate) mod appearance;
mod assets;
mod attributes;
@@ -322,6 +323,7 @@ pub fn command_specs() -> &'static [CommandSpec] {
v.extend(perspgrid::specs());
v.extend(shortcutset::specs());
v.extend(importmatches::specs());
v.extend(addons::specs());
v
})
}
+11
View File
@@ -0,0 +1,11 @@
//! VectorCraft's built-in add-ons: extra tools beyond the standard menus, each a module of its own
//! with commands under `addon.<name>.*` (see `docs/mcp.md`).
mod cleanup;
use super::CommandSpec;
/// The commands of every built-in add-on.
pub fn specs() -> Vec<CommandSpec> {
cleanup::specs()
}
+362
View File
@@ -0,0 +1,362 @@
//! Artwork Cleanup (`addon.cleanup.*`), a built-in add-on: Simplify Anchors (Keep Shape), the
//! Document Health Check (check, show and fix the issues it finds), Deep Ungroup and Rejoin Path
//! Pieces. Its panel and its submenu, Object › Plug-ins › Artwork Cleanup, are the UI's.
mod checks;
#[cfg(test)]
mod tests;
use serde_json::{Map, Value, json};
use vectorcraft_doc::{Document, Node, NodeId, NodeKind};
use vectorcraft_geom::{PathData, SubPath};
use vectorcraft_pathops::addons as po;
use self::checks::{Kind, MAX_OBJECTS, Outcome, Scope, set_path};
use super::super::edit::selected_roots;
use super::super::{CommandSpec, bad, checked_ids_or_selection, cmd, has_doc, has_selection, number_or, switch_or, text_param};
use crate::{EngineError, Result, Session};
/// Simplify Anchors' tolerance by default, and the drift (pt) each step of it allows.
const TOLERANCE: f64 = 20.0;
const PT_PER_TOLERANCE: f64 = 0.02;
/// Rejoin Path Pieces' gap by default, and the widest (pt).
const GAP: f64 = 0.1;
const MAX_GAP: f64 = 6.0;
/// Most anchors Simplify Anchors takes at once.
const MAX_ANCHORS: usize = 2_000_000;
/// Most rounds of fixes `addon.cleanup.fix` runs.
const FIX_ROUNDS: usize = 3;
/// Most ids a finding lists (its count counts them all).
const MAX_LISTED: usize = 1000;
pub fn specs() -> Vec<CommandSpec> {
vec![
cmd!(
"addon.cleanup.simplify",
"Simplify Anchors (Keep Shape)…",
["Object", "Plug-ins", "Artwork Cleanup"],
None,
"{tolerance?: 1..100 (20; higher removes more anchors and lets the outline move further from where it was, up to tolerance × 0.02 pt), protectCorners?: bool (false; anchors where the path turns by more than 30° stay), ids?: [id…] (default: the selection)} remove the anchors each path under the objects can do without, refitting the handles beside each so the outline keeps its shape, as one undo step; open ends stay → {ids, before, after} anchor counts",
has_selection,
simplify
),
cmd!(
query "addon.cleanup.check",
"Health Check",
["Object", "Plug-ins", "Artwork Cleanup"],
None,
"{scope?: \"selection\"|\"document\" (default: the selection, or the document while nothing is selected), kinds?: [kind…] (default: all: emptyText, strayPoints (paths of one anchor), unpainted, zeroOpacity, duplicateObjects (a path stacked on an identical one), offArtboard, duplicateAnchors (neighbours within 0.01 pt), tinyHandles (under 0.01 pt), collinearAnchors (corners on a straight run, turning less than sine 0.005), nearPoints (anchors within 0.01 pt of other paths' anchors), nearlyClosed (ends within 0.01 pt), filledOpenPaths (filled, unstroked open paths), needlessClipGroups (an unpainted clip that clips nothing, hides everything, or is a rectangle round all its art), needlessCompounds (one path or none), needlessGroups (one member or none, nothing of their own), emptyArtboards (the document only))} find issues without changing anything; locked and hidden objects are left out → {scope, total, findings: [{kind, label, count, ids (the objects, at most 1000), artboards (indexes)}]} for the kinds found",
has_doc,
check
),
cmd!(
query "addon.cleanup.show",
"Show Issue",
["Object", "Plug-ins", "Artwork Cleanup"],
None,
"{kind: kind (see addon.cleanup.check), scope?: \"selection\"|\"document\"} select the objects with that issue → {kind, count, ids, artboards}",
has_doc,
show
),
cmd!(
"addon.cleanup.fix",
"Fix All Issues",
["Object", "Plug-ins", "Artwork Cleanup"],
None,
"{scope?: \"selection\"|\"document\", kinds?: [kind…] (default: all; see addon.cleanup.check)} fix the issues addon.cleanup.check finds, in up to three rounds while a round still finds some (a fix can leave issues for another kind), as one undo step: what is empty, unpainted, invisible, duplicated or off the artboards goes, anchors merge, snap or go, tiny handles retract, paths close and needless groups, compound paths and clipping groups give way to their contents → {scope, total, fixed: {kind: count}}",
has_doc,
fix
),
cmd!(
"addon.cleanup.deepUngroup",
"Deep Ungroup…",
["Object", "Plug-ins", "Artwork Cleanup"],
None,
"{includeClipGroups?: bool (false: clipping groups, and the art inside them, stay as they are; true: they are ungrouped too and their clipping paths become plain paths), ids?: [id…] (default: the selection)} ungroup every level of the groups among the objects, as one undo step; each object keeps the opacity of the groups it was in → {ids (the objects left, selected), ungrouped (groups)}",
has_selection,
deep_ungroup
),
cmd!(
"addon.cleanup.rejoin",
"Rejoin Path Pieces…",
["Object", "Plug-ins", "Artwork Cleanup"],
None,
"{gap?: pt 0..6 (0.1), ids?: [id…] (default: the selection)} weld the open paths under the objects whose ends lie within the gap of each other back into paths: an end joins one other end, the one the path turns least to go on into, pieces may run either way, and a chain whose ends meet closes; each path takes the place and paint of its topmost piece, as one undo step → {ids, joined (paths made), removed (pieces merged into them)}",
has_selection,
rejoin
),
]
}
/// The paths (not guides) under `roots`, compound path members included.
fn paths_under(d: &Document, roots: &[NodeId]) -> Vec<NodeId> {
let mut out = vec![];
for n in roots.iter().filter_map(|id| d.node(*id)) {
n.walk(&mut |c| {
if matches!(c.kind, NodeKind::Path { guide: false, .. }) {
out.push(c.id);
}
});
}
out
}
fn simplify(s: &mut Session, p: &Value) -> Result<Value> {
const C: &str = "addon.cleanup.simplify";
let tolerance = number_or(p, "tolerance", TOLERANCE, C)?;
if !(1.0..=100.0).contains(&tolerance) {
return Err(bad(C, "`tolerance` must be from 1 to 100"));
}
let protect = switch_or(p, "protectCorners", false, C)?;
let roots = checked_ids_or_selection(s, p, C)?;
let d = &s.doc()?.doc;
let ids = paths_under(d, &roots);
let paths: Vec<(NodeId, &PathData)> = ids.iter().filter_map(|id| Some((*id, d.node(*id)?.path_data()?))).collect();
let before: usize = paths.iter().map(|(_, p)| p.anchor_count()).sum();
if before > MAX_ANCHORS {
return Err(EngineError::Other("Simplify Anchors: the paths have too many anchors to simplify at once".into()));
}
let changed: Vec<(NodeId, PathData)> = paths
.iter()
.filter_map(|(id, path)| {
let np = po::simplify_keep_shape(path, tolerance * PT_PER_TOLERANCE, protect);
(np != **path).then_some((*id, np))
})
.collect();
let removed: usize = changed.iter().filter_map(|(id, np)| Some(d.node(*id)?.path_data()?.anchor_count().saturating_sub(np.anchor_count()))).sum();
let after = before.saturating_sub(removed);
// When no anchor can go, the document and its undo history stay as they are.
if !changed.is_empty() {
s.edit("Simplify Anchors (Keep Shape)", |d, sel| {
for (id, np) in changed {
// The anchors left are new ones: the path is selected whole.
sel.anchors.remove(&id);
sel.segments.remove(&id);
set_path(d, id, np);
}
Ok(())
})?;
}
Ok(json!({ "ids": ids.iter().map(|i| i.0).collect::<Vec<_>>(), "before": before, "after": after }))
}
/// The scope a check, show or fix request names (by default the selection, or the document while
/// nothing is selected) and its name.
fn scope_of(s: &Session, p: &Value, cmd: &str) -> Result<(Scope, &'static str)> {
let roots = selected_roots(s)?;
match text_param(p, "scope", cmd)? {
Some("document") => Ok((Scope::Document, "document")),
None if roots.is_empty() => Ok((Scope::Document, "document")),
Some("selection") if roots.is_empty() => Err(bad(cmd, "nothing selected")),
Some("selection") | None => Ok((Scope::Objects(roots), "selection")),
Some(other) => Err(bad(cmd, format!("`scope` must be \"selection\" or \"document\", not {other:?}"))),
}
}
/// The kinds a request names (all by default), in the order Fix runs them.
fn kinds_of(p: &Value, cmd: &str) -> Result<Vec<Kind>> {
let given: Vec<Kind> = match p.get("kinds") {
None | Some(Value::Null) => return Ok(Kind::ALL.to_vec()),
Some(Value::Array(a)) => {
a.iter().map(|v| v.as_str().and_then(Kind::parse).ok_or_else(|| bad(cmd, format!("unknown issue kind {v}")))).collect::<Result<_>>()?
}
Some(_) => return Err(bad(cmd, "`kinds` must be a list of issue kinds")),
};
Ok(Kind::ALL.into_iter().filter(|k| given.contains(k)).collect())
}
/// Kind `kind`'s fix run on a copy of the active document: what Fix would change.
fn dry_run(s: &Session, kind: Kind, scope: &Scope) -> Result<Outcome> {
let st = s.doc()?;
let (mut d, mut sel, mut scope) = ((*st.doc).clone(), st.selection.clone(), scope.clone());
kind.fix(&mut d, &mut sel, &mut scope)
}
fn finding(kind: Kind, o: &Outcome) -> Value {
let ids: Vec<u64> = o.ids.iter().take(MAX_LISTED).map(|i| i.0).collect();
json!({ "kind": kind.id(), "label": kind.label(), "count": o.count, "ids": ids, "artboards": o.artboards })
}
fn check(s: &mut Session, p: &Value) -> Result<Value> {
const C: &str = "addon.cleanup.check";
let (scope, name) = scope_of(s, p, C)?;
let mut findings = vec![];
let mut total = 0;
for kind in kinds_of(p, C)? {
let o = dry_run(s, kind, &scope)?;
if o.count > 0 {
total += o.count;
findings.push(finding(kind, &o));
}
}
Ok(json!({ "scope": name, "total": total, "findings": findings }))
}
fn show(s: &mut Session, p: &Value) -> Result<Value> {
const C: &str = "addon.cleanup.show";
let id = text_param(p, "kind", C)?.ok_or_else(|| bad(C, "missing `kind`"))?;
let kind = Kind::parse(id).ok_or_else(|| bad(C, format!("unknown issue kind {id:?}")))?;
let (scope, _) = scope_of(s, p, C)?;
let o = dry_run(s, kind, &scope)?;
if o.count == 0 {
return Err(EngineError::Other("None of these issues are left: check again".into()));
}
// Artboards are no objects to select: the selection stays as it is for them.
let ids = o.ids.clone();
if !ids.is_empty() {
s.select(|_, sel| sel.set(ids))?;
}
Ok(finding(kind, &o))
}
fn fix(s: &mut Session, p: &Value) -> Result<Value> {
const C: &str = "addon.cleanup.fix";
let (mut scope, name) = scope_of(s, p, C)?;
let kinds = kinds_of(p, C)?;
// Fixed on a copy, kind after kind (what one leaves, the next cleans up), then kept as one step.
let st = s.doc()?;
let (mut d, mut sel) = ((*st.doc).clone(), st.selection.clone());
let mut fixed = Map::new();
let mut total = 0;
// A fix can leave issues for a kind that ran before it (groups unwrapped side by side may hold
// duplicates): the kinds run again, a few rounds at most, until a round finds nothing.
for _ in 0..FIX_ROUNDS {
let before = total;
for &kind in &kinds {
let o = kind.fix(&mut d, &mut sel, &mut scope)?;
if o.count > 0 {
total += o.count;
let n = fixed.get(kind.id()).and_then(Value::as_u64).unwrap_or(0);
fixed.insert(kind.id().into(), json!(n + o.count as u64));
}
}
if total == before {
break;
}
}
// With nothing to fix, the document and its undo history stay as they are.
if total > 0 {
sel.prune(&d);
s.edit("Fix Issues", |doc, selection| {
*doc = d;
*selection = sel;
Ok(())
})?;
}
Ok(json!({ "scope": name, "total": total, "fixed": fixed }))
}
/// Is `n` a group Deep Ungroup takes apart (a clipping group only with `clips`)?
fn ungroupable(n: &Node, clips: bool) -> bool {
matches!(n.kind, NodeKind::Group { clip, .. } if clips || !clip)
}
/// `n` with every level of grouping Deep Ungroup takes apart removed, into `out`; `count` counts
/// the groups. Members take on the opacity of the groups they were in, and a clipping path
/// becomes a plain path.
fn flatten(n: Node, clips: bool, out: &mut Vec<Node>, count: &mut usize) {
if !ungroupable(&n, clips) {
out.push(n);
return;
}
let NodeKind::Group { children, clip } = &n.kind else { return };
*count += 1;
for (k, c) in children.iter().enumerate() {
let mut c = (**c).clone();
if *clip
&& k == 0
&& let NodeKind::Path { clipping, .. } = &mut c.kind
{
*clipping = false;
}
c.opacity *= n.opacity;
flatten(c, clips, out, count);
}
}
fn deep_ungroup(s: &mut Session, p: &Value) -> Result<Value> {
const C: &str = "addon.cleanup.deepUngroup";
let clips = switch_or(p, "includeClipGroups", false, C)?;
let roots = checked_ids_or_selection(s, p, C)?;
let d = &s.doc()?.doc;
let (mut items, mut selected, mut count) = (vec![], vec![], 0);
for id in &roots {
match d.node(*id).filter(|n| ungroupable(n, clips)) {
Some(n) => {
let mut out = vec![];
flatten(n.clone(), clips, &mut out, &mut count);
selected.extend(out.iter().map(|n| n.id));
items.push((*id, out));
}
None => selected.push(*id),
}
}
// With no group to take apart, the document and its undo history stay as they are.
if !items.is_empty() {
let ids = selected.clone();
s.edit("Deep Ungroup", |d, sel| {
d.insert_beside(items, true)?;
sel.set(ids);
Ok(())
})?;
}
Ok(json!({ "ids": selected.iter().map(|i| i.0).collect::<Vec<_>>(), "ungrouped": count }))
}
/// The open single-subpath paths under `roots` that Rejoin Path Pieces takes, in paint order
/// (compound path members, clipping paths and guides stay out).
fn pieces(d: &Document, roots: &[NodeId]) -> Vec<(NodeId, SubPath)> {
fn visit(n: &Node, out: &mut Vec<(NodeId, SubPath)>) {
match &n.kind {
NodeKind::Path { path, guide: false, clipping: false, .. } => {
if let [sp] = path.subpaths.as_slice()
&& !sp.closed
{
out.push((n.id, sp.clone()));
}
}
NodeKind::Group { children, .. } | NodeKind::Layer { children, .. } => children.iter().for_each(|c| visit(c, out)),
_ => {}
}
}
let mut out = vec![];
roots.iter().filter_map(|id| d.node(*id)).for_each(|n| visit(n, &mut out));
let editable = d.editable();
out.retain(|(id, _)| editable(*id));
out
}
fn rejoin(s: &mut Session, p: &Value) -> Result<Value> {
const C: &str = "addon.cleanup.rejoin";
let gap = number_or(p, "gap", GAP, C)?;
if !(0.0..=MAX_GAP).contains(&gap) {
return Err(bad(C, "`gap` must be from 0 to 6 pt"));
}
let roots = checked_ids_or_selection(s, p, C)?;
let found = pieces(&s.doc()?.doc, &roots);
if found.len() > MAX_OBJECTS {
return Err(EngineError::Other("Rejoin Path Pieces: too many paths to rejoin at once".into()));
}
let subpaths: Vec<SubPath> = found.iter().map(|(_, sp)| sp.clone()).collect();
// Each chain becomes its topmost piece (the last in paint order); the others go.
let (mut kept, mut gone) = (vec![], vec![]);
for j in po::rejoin(&subpaths, gap) {
let ids: Vec<NodeId> = j.members.iter().filter_map(|&m| found.get(m).map(|f| f.0)).collect();
let Some(&top) = j.members.iter().max().and_then(|&m| found.get(m).map(|f| &f.0)) else { continue };
gone.extend(ids.into_iter().filter(|id| *id != top));
kept.push((top, j.path));
}
if !kept.is_empty() {
let ids: Vec<NodeId> = kept.iter().map(|k| k.0).collect();
s.edit("Rejoin Path Pieces", |d, sel| {
for (id, sp) in &kept {
set_path(d, *id, PathData::single(sp.clone()));
}
d.remove_many(&gone)?;
sel.set(ids);
Ok(())
})?;
}
let ids: Vec<u64> = kept.iter().map(|k| k.0.0).collect();
Ok(json!({ "ids": ids, "joined": kept.len(), "removed": gone.len() }))
}
@@ -0,0 +1,454 @@
//! Document Health Check: the kinds of issue Artwork Cleanup looks for, and how each is fixed. A
//! check runs a kind's fix on a copy of the document, so what a check finds is what Fix changes.
use std::collections::{HashMap, HashSet};
use vectorcraft_color::BlendMode;
use vectorcraft_doc::appearance::AppearanceItem;
use vectorcraft_doc::{Appearance, Document, Knockout, Node, NodeExtras, NodeId, NodeKind, Selection, SymbolBoxes};
use vectorcraft_geom::{PathData, Point, Rect};
use vectorcraft_pathops::addons as po;
use super::super::super::edit::outermost;
use super::super::super::layer::empty_artboards;
use crate::Result;
/// Sine of the turn below which an anchor on a straight run is collinear.
const COLLINEAR: f64 = 0.005;
/// Distance (pt) within which anchors coincide, handles are tiny and path ends meet.
const NEAR: f64 = 0.01;
/// Most objects a check looks at; the rest of a larger document is left as it is.
pub(super) const MAX_OBJECTS: usize = 250_000;
/// Most anchors Near Anchors compares.
const MAX_ANCHORS: usize = 1_000_000;
/// Most rounds of unwrapping nested groups.
const MAX_PASSES: usize = 64;
/// One kind of issue.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(super) enum Kind {
EmptyText,
StrayPoints,
Unpainted,
ZeroOpacity,
DuplicateObjects,
OffArtboard,
DuplicateAnchors,
TinyHandles,
CollinearAnchors,
NearPoints,
NearlyClosed,
FilledOpenPaths,
NeedlessClipGroups,
NeedlessCompounds,
NeedlessGroups,
EmptyArtboards,
}
impl Kind {
/// Every kind, in the order Fix runs them (removing art first can leave groups to unwrap).
pub(super) const ALL: [Kind; 16] = [
Kind::EmptyText,
Kind::StrayPoints,
Kind::Unpainted,
Kind::ZeroOpacity,
Kind::DuplicateObjects,
Kind::OffArtboard,
Kind::DuplicateAnchors,
Kind::TinyHandles,
Kind::CollinearAnchors,
Kind::NearPoints,
Kind::NearlyClosed,
Kind::FilledOpenPaths,
Kind::NeedlessClipGroups,
Kind::NeedlessCompounds,
Kind::NeedlessGroups,
Kind::EmptyArtboards,
];
/// (id, label) of the kind.
fn names(self) -> (&'static str, &'static str) {
match self {
Kind::EmptyText => ("emptyText", "Empty Text"),
Kind::StrayPoints => ("strayPoints", "Stray Points"),
Kind::Unpainted => ("unpainted", "Unpainted Objects"),
Kind::ZeroOpacity => ("zeroOpacity", "Zero-Opacity Objects"),
Kind::DuplicateObjects => ("duplicateObjects", "Duplicate Objects"),
Kind::OffArtboard => ("offArtboard", "Off-Artboard Art"),
Kind::DuplicateAnchors => ("duplicateAnchors", "Duplicate Anchors"),
Kind::TinyHandles => ("tinyHandles", "Tiny Handles"),
Kind::CollinearAnchors => ("collinearAnchors", "Collinear Anchors"),
Kind::NearPoints => ("nearPoints", "Nearly Coincident Anchors"),
Kind::NearlyClosed => ("nearlyClosed", "Nearly Closed Paths"),
Kind::FilledOpenPaths => ("filledOpenPaths", "Filled Open Paths"),
Kind::NeedlessClipGroups => ("needlessClipGroups", "Needless Clipping Groups"),
Kind::NeedlessCompounds => ("needlessCompounds", "Needless Compound Paths"),
Kind::NeedlessGroups => ("needlessGroups", "Needless Groups"),
Kind::EmptyArtboards => ("emptyArtboards", "Empty Artboards"),
}
}
pub(super) fn id(self) -> &'static str {
self.names().0
}
pub(super) fn label(self) -> &'static str {
self.names().1
}
pub(super) fn parse(id: &str) -> Option<Kind> {
Kind::ALL.into_iter().find(|k| k.id() == id)
}
/// Fix this kind of issue in `scope` of `d` → what changed.
pub(super) fn fix(self, d: &mut Document, sel: &mut Selection, scope: &mut Scope) -> Result<Outcome> {
match self {
Kind::DuplicateAnchors => reshape(d, scope, |_, p| p.subpaths.iter_mut().map(|sp| po::merge_duplicate_anchors(sp, NEAR)).sum()),
Kind::TinyHandles => reshape(d, scope, |_, p| p.subpaths.iter_mut().map(|sp| po::retract_tiny_handles(sp, NEAR)).sum()),
Kind::CollinearAnchors => reshape(d, scope, |_, p| p.subpaths.iter_mut().map(|sp| po::remove_collinear_anchors(sp, COLLINEAR)).sum()),
Kind::NearlyClosed => reshape(d, scope, |_, p| p.subpaths.iter_mut().map(|sp| usize::from(po::close_nearly_closed(sp, NEAR))).sum()),
Kind::FilledOpenPaths => reshape(d, scope, close_filled),
Kind::NearPoints => near_points(d, scope),
Kind::EmptyArtboards => artboards(d, sel, scope),
_ => restructure(d, scope, self),
}
}
/// What replaces object `n` (at `place`) when it is this kind of issue: nothing to remove it,
/// its members to unwrap it. `None` when it isn't one.
fn replacement(self, cx: &Context, n: &Node, place: Place) -> Option<Vec<Node>> {
let removable = !place.clips && !is_guide_or_clip(n);
let gone = |is: bool| is.then(Vec::new);
match self {
Kind::EmptyText => gone(matches!(&n.kind, NodeKind::Text(t) if t.plain_text().trim().is_empty())),
Kind::StrayPoints => gone(removable && matches!(&n.kind, NodeKind::Path { path, .. } if path.anchor_count() <= 1)),
Kind::Unpainted => gone(removable && !place.in_compound && is_shape(n) && !is_painted(&n.appearance)),
Kind::ZeroOpacity => gone(removable && !place.in_compound && n.opacity <= 0.0),
Kind::DuplicateObjects => gone(removable && duplicates_the_one_below(cx.d, n)),
Kind::OffArtboard => gone(place.top && !is_guide_or_clip(n) && off_artboards(cx, n)),
Kind::NeedlessGroups => needless_group(n),
Kind::NeedlessCompounds => needless_compound(n),
Kind::NeedlessClipGroups => needless_clip_group(cx, n),
_ => None,
}
}
}
/// What a fix changed (or a check found): how many issues, the objects they are on and the
/// artboards (by index) they are.
#[derive(Clone, Debug, Default)]
pub(super) struct Outcome {
pub count: usize,
pub ids: Vec<NodeId>,
pub artboards: Vec<usize>,
}
/// What a check looks at: the whole document, or the objects given and what is inside them.
#[derive(Clone, Debug)]
pub(super) enum Scope {
Document,
Objects(Vec<NodeId>),
}
/// Where an object sits.
#[derive(Clone, Copy, Default)]
struct Place {
/// A member of a compound path.
in_compound: bool,
/// The clipping path of its group or layer.
clips: bool,
/// A top-level object of the scope (on a layer, or given).
top: bool,
}
/// The document a pass looks at, with its symbols measured once.
struct Context<'a> {
d: &'a Document,
symbols: SymbolBoxes<'a>,
}
/// The visible, unlocked objects in `scope` with where they sit, in paint order: groups, layers,
/// clip groups and compound paths are looked into; live and generated art (blends, envelopes,
/// graphs, symbols…) is taken as it is.
fn objects<'a>(d: &'a Document, scope: &Scope) -> Vec<(&'a Node, Place)> {
let mut out = vec![];
let top = Place { top: true, ..Place::default() };
match scope {
Scope::Document => d.layers.iter().for_each(|l| visit(l, top, &mut out)),
Scope::Objects(ids) => ids.iter().filter_map(|id| d.node(*id)).for_each(|n| visit(n, top, &mut out)),
}
let editable = d.editable();
out.retain(|(n, _)| editable(n.id));
out
}
fn visit<'a>(n: &'a Node, place: Place, out: &mut Vec<(&'a Node, Place)>) {
if !n.visible || n.locked || out.len() >= MAX_OBJECTS {
return;
}
let inner = |clip: bool, i: usize, top: bool| Place { clips: clip && i == 0, top, in_compound: false };
match &n.kind {
// A layer's objects are top-level ones.
NodeKind::Layer { children, clip, .. } => children.iter().enumerate().for_each(|(i, c)| visit(c, inner(*clip, i, true), out)),
NodeKind::Group { children, clip } if !is_generated(n) => {
out.push((n, place));
children.iter().enumerate().for_each(|(i, c)| visit(c, inner(*clip, i, false), out));
}
NodeKind::Compound { children, .. } => {
out.push((n, place));
children.iter().for_each(|c| visit(c, Place { in_compound: true, ..Place::default() }, out));
}
_ => out.push((n, place)),
}
}
/// A group that stands for live or generated art: its members aren't loose art to clean.
fn is_generated(n: &Node) -> bool {
let e = n.extras();
e.graph.is_some() || e.shaper.is_some() || e.dimension.is_some() || e.objects_on_path.is_some() || e.live_paint_gaps.is_some() || e.compound_shape
}
fn is_guide_or_clip(n: &Node) -> bool {
matches!(n.kind, NodeKind::Path { guide: true, .. } | NodeKind::Path { clipping: true, .. })
}
fn is_shape(n: &Node) -> bool {
matches!(n.kind, NodeKind::Path { .. } | NodeKind::Compound { .. })
}
fn is_painted(a: &Appearance) -> bool {
a.items.iter().any(|i| match i {
AppearanceItem::Fill(f) => f.visible && !f.paint.is_none(),
AppearanceItem::Stroke(s) => s.visible && !s.paint.is_none() && s.width > 0.0,
})
}
/// A container with nothing of its own to show: full opacity, normal blending, no fill, stroke,
/// effect, mask, knockout or anything else attached.
fn is_plain(n: &Node) -> bool {
n.opacity >= 1.0
&& n.blend == BlendMode::Normal
&& n.appearance.items.is_empty()
&& n.appearance.effects.is_empty()
&& !n.isolate
&& n.knockout == Knockout::Neutral
&& *n.extras() == NodeExtras::default()
}
/// Is `n` a plain path (full opacity, normal blending) identical to the object just below it?
fn duplicates_the_one_below(d: &Document, n: &Node) -> bool {
if !matches!(n.kind, NodeKind::Path { .. }) || n.opacity < 1.0 || n.blend != BlendMode::Normal {
return false;
}
let below = d.position(n.id).and_then(|(parent, i, _)| d.children(parent)?.get(i.checked_sub(1)?).cloned());
below.is_some_and(|b| {
b.visible && b.kind == n.kind && b.appearance == n.appearance && b.opacity == n.opacity && b.blend == n.blend && b.extras() == n.extras()
})
}
fn inside(a: Rect, b: Rect) -> bool {
a.x0 >= b.x0 && a.y0 >= b.y0 && a.x1 <= b.x1 && a.y1 <= b.y1
}
fn meets(a: Rect, b: Rect) -> bool {
a.x0 <= b.x1 && b.x0 <= a.x1 && a.y0 <= b.y1 && b.y0 <= a.y1
}
/// Does `n` lie wholly off every artboard (a document without artboards has none off them)?
fn off_artboards(cx: &Context, n: &Node) -> bool {
let Some(b) = n.visual_bounds_with(&cx.symbols) else { return false };
// As Delete Empty Artboards measures: a millionth of a point covers rounding at the edges.
!cx.d.artboards.is_empty() && !cx.d.artboards.iter().any(|a| meets(a.rect.inflate(1e-6, 1e-6), b))
}
/// A plain group of one member or none: its member, or nothing.
fn needless_group(n: &Node) -> Option<Vec<Node>> {
let NodeKind::Group { children, clip: false } = &n.kind else { return None };
(children.len() <= 1 && is_plain(n)).then(|| children.iter().map(|c| (**c).clone()).collect())
}
/// A compound path of one path or none: that path, painted as the compound was, or nothing.
fn needless_compound(n: &Node) -> Option<Vec<Node>> {
let NodeKind::Compound { children, rule } = &n.kind else { return None };
let mut path = match children.as_slice() {
[] => return Some(vec![]),
[one] => (**one).clone(),
_ => return None,
};
let NodeKind::Path { rule: r, .. } = &mut path.kind else { return None };
*r = *rule;
path.appearance = n.appearance.clone();
path.opacity = n.opacity;
path.blend = n.blend;
path.extras = n.extras.clone();
Some(vec![path])
}
/// A plain clip group whose unpainted clipping path changes nothing: it clips nothing or lies wholly
/// away from its art (nothing shows: nothing is left), or it is a rectangle around all its art (its
/// art is left).
fn needless_clip_group(cx: &Context, n: &Node) -> Option<Vec<Node>> {
let NodeKind::Group { children, clip: true } = &n.kind else { return None };
if !is_plain(n) {
return None;
}
let Some((clip, art)) = children.split_first() else { return Some(vec![]) };
let paint = clip.clip_paint();
if paint.fill.is_some() || paint.stroke.is_some() {
return None;
}
let clip_box = clip.geometric_bounds()?;
let art_box = art.iter().filter_map(|a| a.visual_bounds_with(&cx.symbols)).reduce(|a, b| a.union(b));
match art_box {
None => Some(vec![]),
Some(b) if !meets(clip_box, b) => Some(vec![]),
Some(b) if is_rectangle(clip) && inside(b, clip_box.inflate(1e-6, 1e-6)) => Some(art.iter().map(|a| (**a).clone()).collect()),
_ => None,
}
}
/// Is `n` a path drawing a square-to-the-page rectangle: four corners on its bounds, no curves?
fn is_rectangle(n: &Node) -> bool {
let NodeKind::Path { path, .. } = &n.kind else { return false };
let (Some(b), [sp]) = (path.bounds(), path.subpaths.as_slice()) else { return false };
let corner = |p: Point| ((p.x - b.x0).abs() < 1e-6 || (p.x - b.x1).abs() < 1e-6) && ((p.y - b.y0).abs() < 1e-6 || (p.y - b.y1).abs() < 1e-6);
sp.closed && sp.anchors.len() == 4 && sp.anchors.iter().all(|a| !a.has_in() && !a.has_out() && corner(a.p))
}
/// Replace the objects with this kind of issue (the outermost ones each round, until none is
/// left) by what [`Kind::replacement`] gives.
fn restructure(d: &mut Document, scope: &mut Scope, kind: Kind) -> Result<Outcome> {
let mut out = Outcome::default();
for _ in 0..MAX_PASSES {
let found: Vec<(NodeId, Vec<Node>)> = {
let d: &Document = d;
let cx = Context { d, symbols: d.symbol_boxes() };
let all: Vec<(NodeId, Vec<Node>)> =
objects(d, scope).into_iter().filter_map(|(n, pl)| Some((n.id, kind.replacement(&cx, n, pl)?))).collect();
let keep: HashSet<NodeId> = outermost(d, all.iter().map(|(id, _)| *id).collect()).into_iter().collect();
all.into_iter().filter(|(id, _)| keep.contains(id)).collect()
};
if found.is_empty() {
break;
}
if let Scope::Objects(roots) = scope {
for (id, nodes) in &found {
if let Some(at) = roots.iter().position(|r| r == id) {
roots.splice(at..=at, nodes.iter().map(|n| n.id));
}
}
}
out.ids.extend(found.iter().map(|(id, _)| *id));
d.insert_beside(found, true)?;
}
out.count = out.ids.len();
Ok(out)
}
/// Run `f` on a copy of each path in scope (not guides), and keep the copies it changed: `f`
/// returns how many issues it fixed.
fn reshape(d: &mut Document, scope: &Scope, f: impl Fn(&Node, &mut PathData) -> usize) -> Result<Outcome> {
let changed: Vec<(NodeId, PathData, usize)> = objects(d, scope)
.into_iter()
.filter_map(|(n, _)| {
let NodeKind::Path { path, guide: false, .. } = &n.kind else { return None };
let mut p = path.clone();
let k = f(n, &mut p);
(k > 0).then_some((n.id, p, k))
})
.collect();
let mut out = Outcome::default();
for (id, p, k) in changed {
set_path(d, id, p);
out.count += k;
out.ids.push(id);
}
Ok(out)
}
/// Give path `id` the geometry `p` (it stops being a live shape).
pub(super) fn set_path(d: &mut Document, id: NodeId, p: PathData) {
if let Some(NodeKind::Path { path, live, .. }) = d.node_mut(id).map(|n| &mut n.kind) {
*path = p;
*live = None;
}
}
/// Filled Open Paths: close the open subpaths of a path with a fill and no stroke.
fn close_filled(n: &Node, p: &mut PathData) -> usize {
let items = &n.appearance.items;
let filled = items.iter().any(|i| matches!(i, AppearanceItem::Fill(f) if f.visible && !f.paint.is_none()));
let stroked = items.iter().any(|i| matches!(i, AppearanceItem::Stroke(s) if s.visible && !s.paint.is_none() && s.width > 0.0));
if !filled || stroked {
return 0;
}
let mut count = 0;
for sp in p.subpaths.iter_mut().filter(|sp| !sp.closed && sp.anchors.len() >= 2) {
sp.closed = true;
count += 1;
}
count
}
/// Nearly Coincident Anchors: anchors of the paths in scope within [`NEAR`] of one another (but
/// not neighbours on one path, which Duplicate Anchors merges) move onto the first of them.
fn near_points(d: &mut Document, scope: &Scope) -> Result<Outcome> {
// Each anchor: (path, subpath, anchor, its subpath's length and closedness).
let mut refs: Vec<(NodeId, usize, usize, usize, bool)> = vec![];
let mut points: Vec<Point> = vec![];
for (n, _) in objects(d, scope) {
let NodeKind::Path { path, guide: false, .. } = &n.kind else { continue };
for (si, sp) in path.subpaths.iter().enumerate() {
for (ai, a) in sp.anchors.iter().enumerate().take(MAX_ANCHORS.saturating_sub(points.len())) {
refs.push((n.id, si, ai, sp.anchors.len(), sp.closed));
points.push(a.p);
}
}
}
let neighbours = |i: usize, j: usize| {
let (Some(a), Some(b)) = (refs.get(i), refs.get(j)) else { return false };
let gap = a.2.abs_diff(b.2);
a.0 == b.0 && a.1 == b.1 && (gap == 1 || (a.4 && gap + 1 == a.3))
};
let mut moves: HashMap<NodeId, Vec<(usize, usize, Point)>> = HashMap::new();
let mut count = 0;
for group in po::near_point_groups(&points, NEAR, neighbours) {
let Some(&to) = group.first().and_then(|&i| points.get(i)) else { continue };
for &i in &group {
if let (Some(&(id, si, ai, ..)), Some(&p)) = (refs.get(i), points.get(i))
&& p != to
{
moves.entry(id).or_default().push((si, ai, to));
count += 1;
}
}
}
let mut ids: Vec<NodeId> = moves.keys().copied().collect();
ids.sort_unstable();
for id in &ids {
let Some(mut p) = d.node(*id).and_then(Node::path_data).cloned() else { continue };
for &(si, ai, to) in moves.get(id).into_iter().flatten() {
if let Some(a) = p.anchor_mut(si, ai) {
a.translate(to - a.p);
}
}
set_path(d, *id, p);
}
Ok(Outcome { count, ids, artboards: vec![] })
}
/// Empty Artboards: the artboards with no art on them (as Delete Empty Artboards finds them),
/// deleted with their guides. Only a check of the whole document looks at artboards.
fn artboards(d: &mut Document, sel: &mut Selection, scope: &Scope) -> Result<Outcome> {
let Scope::Document = scope else { return Ok(Outcome::default()) };
let empty = empty_artboards(d);
// The last first, so the others keep their places.
for &i in empty.iter().rev() {
if i < d.artboards.len() {
let id = d.artboards.remove(i).id;
d.retire_artboard_id(id);
d.retain_guides(sel, |_, g| g.artboard != Some(id));
}
}
Ok(Outcome { count: empty.len(), ids: vec![], artboards: empty })
}
@@ -0,0 +1,336 @@
//! Artwork Cleanup's commands: Simplify Anchors (Keep Shape), the health check's kinds found and
//! fixed one by one, Fix as one undo step, scopes, Show, Deep Ungroup and Rejoin Path Pieces.
use serde_json::{Value, json};
use vectorcraft_color::{Color, Paint};
use vectorcraft_doc::{Appearance, Node, NodeId, NodeKind};
use vectorcraft_geom::{FillRule, Rect};
use crate::Session;
fn session() -> Session {
let mut s = Session::new();
s.execute("file.new", &json!({"width": 400, "height": 300})).unwrap();
s
}
fn id_of(v: &Value) -> NodeId {
NodeId(v["id"].as_u64().unwrap())
}
fn rect(s: &mut Session, x: f64, y: f64, w: f64, h: f64) -> NodeId {
id_of(&s.execute("shape.rectangle", &json!({"x": x, "y": y, "width": w, "height": h})).unwrap())
}
/// A path through `pts` (anchors as `{x, y, out?}` objects).
fn path(s: &mut Session, anchors: Value, closed: bool) -> NodeId {
id_of(&s.execute("path.create", &json!({"anchors": anchors, "closed": closed})).unwrap())
}
fn line(s: &mut Session, pts: &[(f64, f64)]) -> NodeId {
path(s, pts.iter().map(|(x, y)| json!({"x": x, "y": y})).collect(), false)
}
fn node(s: &Session, id: NodeId) -> Option<Node> {
s.doc().unwrap().doc.node(id).cloned()
}
fn change(s: &mut Session, id: NodeId, f: impl FnOnce(&mut Node)) {
s.edit("t", |d, _| {
f(d.node_mut(id).unwrap());
Ok(())
})
.unwrap();
}
/// Put `id` inside a new container that `kind` makes of its members.
fn wrap(s: &mut Session, id: NodeId, kind: fn(Vec<std::sync::Arc<Node>>) -> NodeKind) -> NodeId {
s.edit("t", |d, _| {
let w = d.alloc_id();
Ok(d.wrap(&[id], |c| Node::new(w, kind(c)))?)
})
.unwrap()
}
fn select(s: &mut Session, ids: &[NodeId]) {
s.select(|_, sel| sel.set(ids.iter().copied())).unwrap();
}
fn anchors(s: &Session, id: NodeId) -> usize {
node(s, id).unwrap().path_data().unwrap().anchor_count()
}
/// How many issues of `kind` a check of `scope` finds.
fn found(s: &mut Session, kind: &str, scope: &str) -> usize {
let r = s.execute("addon.cleanup.check", &json!({"kinds": [kind], "scope": scope})).unwrap();
r["findings"].as_array().unwrap().iter().find(|f| f["kind"] == kind).map_or(0, |f| f["count"].as_u64().unwrap() as usize)
}
fn undo_steps(s: &Session) -> usize {
s.doc().unwrap().history.undo.len()
}
#[test]
fn simplify_keeps_the_shape_with_fewer_anchors_in_one_step() {
let mut s = session();
let e = id_of(&s.execute("shape.ellipse", &json!({"x": 50, "y": 50, "width": 100, "height": 100})).unwrap());
s.execute("object.path.addAnchorPoints", &json!({})).unwrap();
s.execute("object.path.addAnchorPoints", &json!({})).unwrap();
assert_eq!(anchors(&s, e), 16);
let bounds = node(&s, e).unwrap().geometric_bounds().unwrap();
let steps = undo_steps(&s);
let r = s.execute("addon.cleanup.simplify", &json!({})).unwrap();
assert_eq!((r["before"].as_u64(), r["after"].as_u64()), (Some(16), Some(anchors(&s, e) as u64)));
assert!(anchors(&s, e) <= 8, "{r}");
let now = node(&s, e).unwrap().geometric_bounds().unwrap();
for (a, b) in [(bounds.x0, now.x0), (bounds.y0, now.y0), (bounds.x1, now.x1), (bounds.y1, now.y1)] {
assert!((a - b).abs() < 0.4, "{bounds:?} → {now:?}");
}
assert_eq!(undo_steps(&s), steps + 1);
s.execute("edit.undo", &json!({})).unwrap();
assert_eq!(anchors(&s, e), 16);
// A rectangle's corners stay; the anchors on its sides go.
let r = rect(&mut s, 200.0, 50.0, 100.0, 60.0);
s.execute("object.path.addAnchorPoints", &json!({})).unwrap();
assert_eq!(anchors(&s, r), 8);
s.execute("addon.cleanup.simplify", &json!({"protectCorners": true})).unwrap();
assert_eq!(anchors(&s, r), 4);
// Nothing left to remove: no undo step.
let steps = undo_steps(&s);
s.execute("addon.cleanup.simplify", &json!({})).unwrap();
assert_eq!(undo_steps(&s), steps);
for bad in [json!({"tolerance": 0}), json!({"tolerance": 101}), json!({"tolerance": "20"}), json!({"protectCorners": 1})] {
assert!(s.execute("addon.cleanup.simplify", &bad).is_err(), "{bad}");
}
}
/// Each kind: what makes the issue in a fresh document (beside a clean rectangle).
fn make_issue(s: &mut Session, kind: &str) {
match kind {
"emptyText" => {
s.execute("text.create", &json!({"x": 20, "y": 20, "text": " "})).unwrap();
}
"strayPoints" => {
path(s, json!([{"x": 30, "y": 30}]), false);
}
"unpainted" => {
let id = rect(s, 20.0, 150.0, 30.0, 30.0);
change(s, id, |n| n.appearance = Appearance::default().into());
}
"zeroOpacity" => {
let id = rect(s, 20.0, 150.0, 30.0, 30.0);
change(s, id, |n| n.opacity = 0.0);
}
"duplicateObjects" => {
let id = rect(s, 20.0, 150.0, 30.0, 30.0);
s.edit("t", |d, _| {
let mut copy = d.node(id).unwrap().clone();
copy.id = d.alloc_id();
let (parent, i, _) = d.position(id).unwrap();
Ok(d.insert(parent, i + 1, copy)?)
})
.unwrap();
}
"offArtboard" => {
rect(s, -500.0, -500.0, 30.0, 30.0);
}
"duplicateAnchors" => {
line(s, &[(10.0, 150.0), (50.0, 150.0), (50.005, 150.0), (50.0, 190.0)]);
}
"tinyHandles" => {
path(s, json!([{"x": 10, "y": 150, "out": [10.004, 150]}, {"x": 60, "y": 190}]), false);
}
"collinearAnchors" => {
line(s, &[(10.0, 150.0), (30.0, 150.0), (50.0, 150.0), (50.0, 190.0)]);
}
"nearPoints" => {
line(s, &[(10.0, 150.0), (50.0, 150.0)]);
line(s, &[(50.005, 150.0), (90.0, 190.0)]);
}
"nearlyClosed" => {
line(s, &[(10.0, 150.0), (50.0, 150.0), (50.0, 190.0), (10.005, 150.0)]);
}
"filledOpenPaths" => {
let id = line(s, &[(10.0, 150.0), (50.0, 150.0), (50.0, 190.0)]);
change(s, id, |n| n.appearance = Appearance::basic(Paint::solid(Color::rgb(1.0, 0.0, 0.0)), Paint::None, 1.0).into());
}
"needlessClipGroups" => {
let inner = rect(s, 30.0, 160.0, 20.0, 20.0);
let clip = rect(s, 20.0, 150.0, 50.0, 50.0);
select(s, &[inner, clip]);
s.execute("object.clippingMask.make", &json!({})).unwrap();
}
"needlessCompounds" => {
// A compound path holds the paint of its paths.
let id = rect(s, 20.0, 150.0, 30.0, 30.0);
let look = node(s, id).unwrap().appearance;
let compound = wrap(s, id, |children| NodeKind::Compound { children, rule: FillRule::NonZero });
change(s, compound, |n| n.appearance = look);
}
"needlessGroups" => {
let id = rect(s, 20.0, 150.0, 30.0, 30.0);
wrap(s, id, |children| NodeKind::Group { children, clip: false });
}
"emptyArtboards" => {
s.edit("t", |d, _| {
let mut a = d.artboards[0].clone();
a.id = d.next_artboard_id();
a.rect = Rect::new(1000.0, 1000.0, 1100.0, 1100.0);
d.artboards.push(a);
Ok(())
})
.unwrap();
}
_ => panic!("no setup for {kind}"),
}
}
const KINDS: [&str; 16] = [
"emptyText",
"strayPoints",
"unpainted",
"zeroOpacity",
"duplicateObjects",
"offArtboard",
"duplicateAnchors",
"tinyHandles",
"collinearAnchors",
"nearPoints",
"nearlyClosed",
"filledOpenPaths",
"needlessClipGroups",
"needlessCompounds",
"needlessGroups",
"emptyArtboards",
];
#[test]
fn each_check_finds_and_fixes_its_issue_and_leaves_clean_art_alone() {
let mut clean = session();
let r = rect(&mut clean, 200.0, 20.0, 80.0, 60.0);
let before = node(&clean, r).unwrap();
assert_eq!(clean.execute("addon.cleanup.check", &json!({})).unwrap()["total"], 0, "clean art has no issues");
assert_eq!(clean.execute("addon.cleanup.fix", &json!({"scope": "document"})).unwrap()["total"], 0);
assert_eq!(node(&clean, r).unwrap(), before);
for kind in KINDS {
let mut s = session();
let r = rect(&mut s, 200.0, 20.0, 80.0, 60.0);
make_issue(&mut s, kind);
select(&mut s, &[]);
assert!(found(&mut s, kind, "document") > 0, "{kind} not found");
let fixed = s.execute("addon.cleanup.fix", &json!({"kinds": [kind], "scope": "document"})).unwrap();
assert!(fixed["fixed"][kind].as_u64().unwrap() > 0, "{kind}: {fixed}");
assert_eq!(found(&mut s, kind, "document"), 0, "{kind} still found after Fix");
assert_eq!(s.execute("addon.cleanup.check", &json!({})).unwrap()["total"], 0, "{kind} left other issues");
assert_eq!(node(&s, r).unwrap(), before, "{kind} changed the clean rectangle");
}
}
#[test]
fn fix_all_is_one_undo_step() {
let mut s = session();
for kind in KINDS {
make_issue(&mut s, kind);
}
select(&mut s, &[]);
let total = s.execute("addon.cleanup.check", &json!({})).unwrap()["total"].as_u64().unwrap();
assert!(total >= 16, "{total}");
let (steps, doc) = (undo_steps(&s), s.doc().unwrap().doc.clone());
let r = s.execute("addon.cleanup.fix", &json!({})).unwrap();
assert_eq!(r["scope"], "document");
assert_eq!(undo_steps(&s), steps + 1);
assert_eq!(s.execute("addon.cleanup.check", &json!({})).unwrap()["total"], 0);
s.execute("edit.undo", &json!({})).unwrap();
assert_eq!(s.doc().unwrap().doc.layers, doc.layers);
assert_eq!(s.doc().unwrap().doc.artboards.len(), doc.artboards.len());
}
#[test]
fn the_scope_is_the_selection_or_the_document() {
let mut s = session();
let a = path(&mut s, json!([{"x": 30, "y": 30}]), false);
let b = path(&mut s, json!([{"x": 60, "y": 30}]), false);
select(&mut s, &[a]);
let r = s.execute("addon.cleanup.check", &json!({"kinds": ["strayPoints"]})).unwrap();
assert_eq!((r["scope"].as_str(), r["total"].as_u64()), (Some("selection"), Some(1)));
assert_eq!(found(&mut s, "strayPoints", "document"), 2);
s.execute("addon.cleanup.fix", &json!({"kinds": ["strayPoints"]})).unwrap();
assert!(node(&s, a).is_none() && node(&s, b).is_some(), "only the selection is fixed");
// Nothing selected: the document.
let r = s.execute("addon.cleanup.check", &json!({})).unwrap();
assert_eq!((r["scope"].as_str(), r["total"].as_u64()), (Some("document"), Some(1)));
assert!(s.execute("addon.cleanup.check", &json!({"scope": "selection"})).is_err(), "nothing selected");
for bad in [json!({"scope": "page"}), json!({"kinds": ["nope"]}), json!({"kinds": "strayPoints"}), json!({"kinds": [1]})] {
assert!(s.execute("addon.cleanup.check", &bad).is_err(), "{bad}");
assert!(s.execute("addon.cleanup.fix", &bad).is_err(), "{bad}");
}
}
#[test]
fn show_selects_the_objects_with_the_issue() {
let mut s = session();
rect(&mut s, 200.0, 20.0, 80.0, 60.0);
let stray = path(&mut s, json!([{"x": 30, "y": 30}]), false);
select(&mut s, &[]);
let r = s.execute("addon.cleanup.show", &json!({"kind": "strayPoints"})).unwrap();
assert_eq!(r["count"], 1);
assert_eq!(s.doc().unwrap().selection.objects, [stray]);
assert!(s.execute("addon.cleanup.show", &json!({"kind": "emptyText", "scope": "document"})).is_err(), "no such issue");
assert!(s.execute("addon.cleanup.show", &json!({})).is_err() && s.execute("addon.cleanup.show", &json!({"kind": "x"})).is_err());
}
#[test]
fn deep_ungroup_takes_apart_every_level_but_clipping_groups() {
let mut s = session();
let a = rect(&mut s, 10.0, 10.0, 20.0, 20.0);
let b = rect(&mut s, 40.0, 10.0, 20.0, 20.0);
let c = rect(&mut s, 70.0, 10.0, 20.0, 20.0);
select(&mut s, &[b, c]);
let inner = id_of(&s.execute("object.group", &json!({})).unwrap());
change(&mut s, inner, |n| n.opacity = 0.5);
let clipped = rect(&mut s, 100.0, 10.0, 10.0, 10.0);
let clip = rect(&mut s, 95.0, 5.0, 30.0, 30.0);
select(&mut s, &[clipped, clip]);
s.execute("object.clippingMask.make", &json!({})).unwrap();
let clip_group = s.doc().unwrap().selection.objects[0];
select(&mut s, &[a, inner, clip_group]);
let outer = id_of(&s.execute("object.group", &json!({})).unwrap());
let steps = undo_steps(&s);
let r = s.execute("addon.cleanup.deepUngroup", &json!({})).unwrap();
assert_eq!(r["ungrouped"], 2);
assert_eq!(undo_steps(&s), steps + 1);
let d = &s.doc().unwrap().doc;
let layer: Vec<NodeId> = d.layers[0].children().unwrap().iter().map(|n| n.id).collect();
assert_eq!(layer, [a, b, c, clip_group]);
assert!(d.node(outer).is_none() && d.node(inner).is_none());
assert_eq!(d.node(b).unwrap().opacity, 0.5, "members keep their groups' opacity");
assert_eq!(s.doc().unwrap().selection.objects.len(), 4);
// Asked to, clipping groups come apart too, their clipping paths kept as plain paths.
select(&mut s, &[clip_group]);
s.execute("addon.cleanup.deepUngroup", &json!({"includeClipGroups": true})).unwrap();
let d = &s.doc().unwrap().doc;
assert!(d.node(clip_group).is_none());
assert!(matches!(d.node(clip).unwrap().kind, NodeKind::Path { clipping: false, .. }));
}
#[test]
fn rejoin_joins_pieces_within_the_gap_only() {
let mut s = session();
let a = line(&mut s, &[(10.0, 10.0), (50.0, 10.0)]);
let b = line(&mut s, &[(50.05, 10.0), (90.0, 50.0)]);
let far = line(&mut s, &[(92.0, 50.0), (120.0, 50.0)]);
select(&mut s, &[a, b, far]);
let r = s.execute("addon.cleanup.rejoin", &json!({})).unwrap();
assert_eq!((r["joined"].as_u64(), r["removed"].as_u64()), (Some(1), Some(1)));
// The topmost piece takes the joined path.
assert!(node(&s, a).is_none());
assert_eq!(anchors(&s, b), 3);
assert_eq!(anchors(&s, far), 2, "beyond the gap");
select(&mut s, &[b, far]);
let r = s.execute("addon.cleanup.rejoin", &json!({"gap": 3})).unwrap();
assert_eq!(r["joined"], 1);
assert_eq!(anchors(&s, far), 4);
for bad in [json!({"gap": -1}), json!({"gap": 7}), json!({"gap": "1"})] {
assert!(s.execute("addon.cleanup.rejoin", &bad).is_err(), "{bad}");
}
}
+14
View File
@@ -0,0 +1,14 @@
//! Geometry for VectorCraft's built-in add-ons, one module per add-on, with the kernels they share.
//!
//! - [`keep_shape`]: remove anchors while refitting their neighbours' handles, so the outline keeps
//! its shape within a tolerance (shared by every add-on that takes anchors away).
//! - [`cleanup`]: Artwork Cleanup's path repairs (collinear, duplicate and near anchors, tiny
//! handles, nearly closed paths) and Rejoin Path Pieces.
pub mod cleanup;
pub mod keep_shape;
pub use cleanup::{
Rejoined, close_nearly_closed, merge_duplicate_anchors, near_point_groups, rejoin, remove_collinear_anchors, retract_tiny_handles,
};
pub use keep_shape::{SHARP_CORNER_DEG, remove_anchor_keep_shape, simplify_keep_shape};
+323
View File
@@ -0,0 +1,323 @@
//! Artwork Cleanup's path repairs, each on one subpath and returning how much it changed (so a
//! check is the repair run on a copy): collinear anchors, duplicate anchors, tiny handles and
//! nearly closed paths; [`near_point_groups`] finds anchors to snap together, and [`rejoin`] welds
//! open pieces whose ends meet back into paths.
use std::collections::HashMap;
use kurbo::{Point, Vec2};
use vectorcraft_geom::{Anchor, AnchorKind, EPS, SubPath};
use crate::fit::{end_tangent, start_tangent};
/// The fewest anchors a subpath keeps: two when open, three when closed.
fn min_anchors(sp: &SubPath) -> usize {
if sp.closed { 3 } else { 2 }
}
/// `a`'s handle offset `h - a.p` moved onto `p`.
fn moved(p: Point, a_p: Point, h: Point) -> Point {
p + (h - a_p)
}
/// Remove the corner anchors (no handles, straight segments both sides) where a straight run goes
/// on in the same direction: the sine of the angle it turns there is at most `tolerance`. Open ends
/// stay, and a subpath keeps two anchors (three when closed) → how many went.
pub fn remove_collinear_anchors(sp: &mut SubPath, tolerance: f64) -> usize {
let mut removed = 0;
let mut i = 0;
while i < sp.anchors.len() && sp.anchors.len() > min_anchors(sp) {
let n = sp.anchors.len();
let inner = sp.closed || (i > 0 && i + 1 < n);
let (Some(prev), Some(cur), Some(next)) = (sp.anchors.get((i + n - 1) % n), sp.anchors.get(i), sp.anchors.get((i + 1) % n)) else { break };
let straight = !prev.has_out() && !cur.has_in() && !cur.has_out() && !next.has_in();
let (a, b) = (cur.p - prev.p, next.p - cur.p);
let len = a.hypot() * b.hypot();
if inner && straight && len > EPS && a.dot(b) > 0.0 && a.cross(b).abs() / len <= tolerance {
sp.anchors.remove(i);
removed += 1;
} else {
i += 1;
}
}
removed
}
/// Merge each anchor lying within `distance` of the one before it (and a closed subpath's last
/// anchor within `distance` of its first) into that one, which keeps its in handle and takes the
/// merged anchor's out handle → how many went.
pub fn merge_duplicate_anchors(sp: &mut SubPath, distance: f64) -> usize {
let before = sp.anchors.len();
let mut out: Vec<Anchor> = Vec::with_capacity(before);
for a in sp.anchors.drain(..) {
match out.last_mut() {
Some(l) if l.p.distance(a.p) <= distance => *l = Anchor::with_handles(l.p, l.h_in, moved(l.p, a.p, a.h_out)),
_ => out.push(a),
}
}
if sp.closed
&& out.len() > 1
&& let (Some(first), Some(last)) = (out.first(), out.last())
&& first.p.distance(last.p) <= distance
&& let Some(last) = out.pop()
&& let Some(f) = out.first_mut()
{
*f = Anchor::with_handles(f.p, moved(f.p, last.p, last.h_in), f.h_out);
}
sp.anchors = out;
before - sp.anchors.len()
}
/// Retract the handles shorter than `length` (but not already on their anchor); an anchor that
/// loses a handle becomes a corner → how many handles were retracted.
pub fn retract_tiny_handles(sp: &mut SubPath, length: f64) -> usize {
let mut count = 0;
for a in &mut sp.anchors {
for out in [false, true] {
let h = if out { &mut a.h_out } else { &mut a.h_in };
let l = h.distance(a.p);
if l > EPS && l < length {
*h = a.p;
a.kind = AnchorKind::Corner;
count += 1;
}
}
}
count
}
/// Close an open subpath of three or more anchors whose ends lie within `distance` of each other:
/// the last anchor merges into the first, which takes its in handle → whether it closed.
pub fn close_nearly_closed(sp: &mut SubPath, distance: f64) -> bool {
let ends_meet = sp.anchors.first().zip(sp.anchors.last()).is_some_and(|(f, l)| f.p.distance(l.p) <= distance);
if sp.closed || sp.anchors.len() < 3 || !ends_meet {
return false;
}
let Some(last) = sp.anchors.pop() else { return false };
if let Some(first) = sp.anchors.first_mut() {
*first = Anchor::with_handles(first.p, moved(first.p, last.p, last.h_in), first.h_out);
}
sp.closed = true;
true
}
/// Groups of `points` to snap together: points within `distance` of one another (and so on, one
/// to the next) that aren't all at the same place, skipping the pairs `linked` names (such as
/// neighbouring anchors of one path). Each group lists its points in order; snapping them all to
/// the first one makes them coincide. Non-finite points and a distance that isn't a positive number
/// find nothing.
pub fn near_point_groups(points: &[Point], distance: f64, linked: impl Fn(usize, usize) -> bool) -> Vec<Vec<usize>> {
if !(distance > 0.0 && distance.is_finite()) {
return vec![];
}
let cell = |p: Point| ((p.x / distance).floor() as i64, (p.y / distance).floor() as i64);
let mut grid: HashMap<(i64, i64), Vec<usize>> = HashMap::new();
for (i, p) in points.iter().enumerate().filter(|(_, p)| p.is_finite()) {
grid.entry(cell(*p)).or_default().push(i);
}
let mut parent: Vec<usize> = (0..points.len()).collect();
fn root(parent: &mut [usize], mut i: usize) -> usize {
while let Some(&p) = parent.get(i).filter(|&&p| p != i) {
let up = parent.get(p).copied().unwrap_or(p);
if let Some(slot) = parent.get_mut(i) {
*slot = up;
}
i = p;
}
i
}
for (&(cx, cy), members) in &grid {
for (dx, dy) in [(0, 0), (1, 0), (-1, 1), (0, 1), (1, 1)] {
let Some(others) = grid.get(&(cx.saturating_add(dx), cy.saturating_add(dy))) else { continue };
for &i in members {
for &j in others.iter().filter(|&&j| (dx, dy) != (0, 0) || j > i) {
let (Some(a), Some(b)) = (points.get(i), points.get(j)) else { continue };
let d = a.distance(*b);
if d <= distance && d > 0.0 && !linked(i.min(j), i.max(j)) {
let (ri, rj) = (root(&mut parent, i), root(&mut parent, j));
if let Some(slot) = parent.get_mut(ri.max(rj)) {
*slot = ri.min(rj);
}
}
}
}
}
}
// Only pairs that aren't at the same place were joined, so no group is all at one place.
let mut groups: HashMap<usize, Vec<usize>> = HashMap::new();
for i in 0..points.len() {
groups.entry(root(&mut parent, i)).or_default().push(i);
}
let mut out: Vec<Vec<usize>> = groups.into_values().filter(|g| g.len() > 1).collect();
out.sort_unstable();
out
}
/// Open pieces welded into one path by [`rejoin`]: `members` index the pieces in the order they
/// run along `path`.
#[derive(Clone, Debug, PartialEq)]
pub struct Rejoined {
pub members: Vec<usize>,
pub path: SubPath,
}
/// One end of a piece: (piece, true for its last anchor).
type End = (usize, bool);
/// The direction pointing out of the piece at `end`.
fn outward(sp: &SubPath, last: bool) -> Option<Vec2> {
let n = sp.segment_count();
Some(match last {
true => end_tangent(&sp.segment(n.checked_sub(1)?)),
false => -start_tangent(&sp.segment(0)),
})
}
/// Weld the open `pieces` whose ends lie within `gap` of each other back into paths: an end meets
/// at most one other, and where an end could meet several, it meets the one the path turns least
/// to go on into (then the nearest). A piece may run either way; a chain whose ends meet closes.
/// Each junction keeps the earlier piece's anchor and the later piece's out handle. Pieces that are
/// closed, have fewer than two anchors or a non-finite coordinate stay out. → the chains of two or
/// more pieces, and the single pieces that closed.
pub fn rejoin(pieces: &[SubPath], gap: f64) -> Vec<Rejoined> {
if !(gap >= 0.0 && gap.is_finite()) {
return vec![];
}
let usable = |sp: &&SubPath| {
!sp.closed && sp.anchors.len() >= 2 && sp.anchors.iter().all(|a| a.p.is_finite() && a.h_in.is_finite() && a.h_out.is_finite())
};
// Each usable end: where it is and the way out of its piece there.
let ends: Vec<(End, Point, Vec2)> = pieces
.iter()
.enumerate()
.filter(|(_, sp)| usable(sp))
.flat_map(|(i, sp)| [(i, sp, false), (i, sp, true)])
.filter_map(|(i, sp, last)| {
let a = if last { sp.anchors.last() } else { sp.anchors.first() }?;
Some(((i, last), a.p, outward(sp, last)?))
})
.collect();
let points: Vec<Point> = ends.iter().map(|e| e.1).collect();
// Every pair of ends within the gap, by the turn across the junction.
let mut pairs: Vec<(f64, f64, End, End)> = vec![];
for (a, b) in candidate_pairs(&points, gap.max(1e-9)) {
let (Some(&(ea, pa, oa)), Some(&(eb, pb, ob))) = (ends.get(a), ends.get(b)) else { continue };
// A piece's own two ends meet only when closing it leaves a shape (three anchors).
if ea.0 == eb.0 && pieces.get(ea.0).is_none_or(|sp| sp.anchors.len() < 3) {
continue;
}
let d = pa.distance(pb);
if d <= gap {
pairs.push((oa.dot(-ob).clamp(-1.0, 1.0).acos(), d, ea, eb));
}
}
pairs.sort_by(|x, y| x.0.total_cmp(&y.0).then(x.1.total_cmp(&y.1)));
let mut link: HashMap<End, End> = HashMap::new();
for (_, _, a, b) in pairs {
if !link.contains_key(&a) && !link.contains_key(&b) {
link.insert(a, b);
link.insert(b, a);
}
}
chains(pieces, &link)
}
/// Pairs (a < b) of `points` that may lie within `gap`: those in neighbouring grid cells.
fn candidate_pairs(points: &[Point], gap: f64) -> Vec<(usize, usize)> {
let cell = |p: Point| ((p.x / gap).floor() as i64, (p.y / gap).floor() as i64);
let mut grid: HashMap<(i64, i64), Vec<usize>> = HashMap::new();
for (i, p) in points.iter().enumerate() {
grid.entry(cell(*p)).or_default().push(i);
}
let mut out = vec![];
for (i, p) in points.iter().enumerate() {
let (cx, cy) = cell(*p);
for dx in -1..=1 {
for dy in -1..=1 {
let Some(others) = grid.get(&(cx.saturating_add(dx), cy.saturating_add(dy))) else { continue };
out.extend(others.iter().filter(|&&j| j > i).map(|&j| (i, j)));
}
}
}
out
}
/// Walk the links between piece ends into chains.
fn chains(pieces: &[SubPath], link: &HashMap<End, End>) -> Vec<Rejoined> {
let mut seen = vec![false; pieces.len()];
let mut out = vec![];
for start in 0..pieces.len() {
if seen.get(start) != Some(&false) || !(link.contains_key(&(start, false)) || link.contains_key(&(start, true))) {
continue;
}
// A member is (piece, reversed): it comes in by end (piece, reversed) and leaves by
// (piece, !reversed). Back up to the chain's first piece, or round to `start` in a loop.
let mut first = (start, false);
let mut closed = false;
for _ in 0..pieces.len() {
let Some(&(p, at_last)) = link.get(&first) else { break };
// The piece before leaves by its end `at_last`.
let prev = (p, !at_last);
if prev.0 == start {
closed = true;
break;
}
first = prev;
}
let members = walk(link, first, closed, &mut seen);
if let Some(path) = assemble(pieces, &members, closed) {
out.push(Rejoined { members: members.iter().map(|m| m.0).collect(), path });
}
}
out
}
/// The pieces of the chain that begins with `first` (piece, reversed), in order.
fn walk(link: &HashMap<End, End>, first: (usize, bool), closed: bool, seen: &mut [bool]) -> Vec<(usize, bool)> {
let mut members = vec![];
let mut cur = first;
loop {
match seen.get_mut(cur.0) {
Some(s) if !*s => *s = true,
_ => break,
}
members.push(cur);
// A piece run forwards leaves by its last anchor.
let Some(&(p, at_last)) = link.get(&(cur.0, !cur.1)) else { break };
let next = (p, at_last);
if closed && next.0 == first.0 {
break;
}
cur = next;
}
members
}
/// The anchors of `pieces` along the chain `members`, each piece's start merged into the end before it.
fn assemble(pieces: &[SubPath], members: &[(usize, bool)], closed: bool) -> Option<SubPath> {
if members.len() < 2 && !closed {
return None;
}
let mut anchors: Vec<Anchor> = vec![];
for &(i, reversed) in members {
let mut sp = pieces.get(i)?.clone();
if reversed {
sp.reverse();
}
let mut rest = sp.anchors.into_iter();
match (anchors.last_mut(), rest.next()) {
(Some(l), Some(f)) => *l = Anchor::with_handles(l.p, l.h_in, moved(l.p, f.p, f.h_out)),
(None, Some(f)) => anchors.push(f),
_ => {}
}
anchors.extend(rest);
}
let mut sp = SubPath::new(anchors, false);
if closed && !close_nearly_closed(&mut sp, f64::INFINITY) {
sp.closed = sp.anchors.len() >= 2;
}
Some(sp)
}
#[cfg(test)]
mod tests;
+134
View File
@@ -0,0 +1,134 @@
use kurbo::{Point, Vec2};
use vectorcraft_geom::{Anchor, AnchorKind, SubPath};
use super::{close_nearly_closed, merge_duplicate_anchors, near_point_groups, rejoin, remove_collinear_anchors, retract_tiny_handles};
fn line(pts: &[(f64, f64)], closed: bool) -> SubPath {
SubPath::polyline(&pts.iter().map(|&p| p.into()).collect::<Vec<Point>>(), closed)
}
fn points(sp: &SubPath) -> Vec<(f64, f64)> {
sp.anchors.iter().map(|a| (a.p.x, a.p.y)).collect()
}
#[test]
fn collinear_anchors_go_and_corners_stay() {
let mut sp = line(&[(0.0, 0.0), (5.0, 0.0), (10.0, 0.0), (10.0, 10.0), (0.0, 10.0), (0.0, 5.0)], true);
assert_eq!(remove_collinear_anchors(&mut sp, 0.005), 2);
assert_eq!(points(&sp), [(0.0, 0.0), (10.0, 0.0), (10.0, 10.0), (0.0, 10.0)]);
assert_eq!(remove_collinear_anchors(&mut sp, 0.005), 0, "clean art is left alone");
// A turn back on itself is not a straight run; an open end stays.
let mut back = line(&[(0.0, 0.0), (10.0, 0.0), (5.0, 0.0)], false);
assert_eq!(remove_collinear_anchors(&mut back, 0.005), 0);
// An anchor with a handle is not a corner on a straight run.
let mut curved = line(&[(0.0, 0.0), (5.0, 0.0), (10.0, 0.0)], false);
curved.anchors[1] = Anchor::smooth((5.0, 0.0).into(), (7.0, 0.0).into());
assert_eq!(remove_collinear_anchors(&mut curved, 0.005), 0);
// A slight bend within the tolerance goes; a larger one stays.
let mut bend = line(&[(0.0, 0.0), (100.0, 0.1), (200.0, 0.0)], false);
assert_eq!(remove_collinear_anchors(&mut bend.clone(), 0.0001), 0);
assert_eq!(remove_collinear_anchors(&mut bend, 0.005), 1);
}
#[test]
fn duplicate_anchors_merge_keeping_the_outer_handles() {
let mut sp = SubPath::new(
vec![
Anchor::with_handles((0.0, 0.0).into(), (0.0, 0.0).into(), (0.0, 0.0).into()),
Anchor::with_handles((10.0, 0.0).into(), (8.0, 0.0).into(), (10.0, 0.0).into()),
Anchor::with_handles((10.005, 0.0).into(), (10.005, 0.0).into(), (10.005, 3.0).into()),
Anchor::corner((10.0, 10.0).into()),
],
false,
);
assert_eq!(merge_duplicate_anchors(&mut sp, 0.01), 1);
assert_eq!(sp.anchors.len(), 3);
let a = sp.anchors[1];
assert_eq!((a.p, a.h_in), (Point::new(10.0, 0.0), Point::new(8.0, 0.0)));
assert!(a.h_out.distance(Point::new(10.0, 3.0)) < 1e-9, "{:?}", a.h_out);
assert_eq!(merge_duplicate_anchors(&mut sp, 0.01), 0);
// A closed path's last anchor on its first merges into it.
let mut ring = line(&[(0.0, 0.0), (10.0, 0.0), (10.0, 10.0), (0.0, 0.0)], true);
assert_eq!(merge_duplicate_anchors(&mut ring, 0.01), 1);
assert_eq!(points(&ring), [(0.0, 0.0), (10.0, 0.0), (10.0, 10.0)]);
}
#[test]
fn tiny_handles_retract() {
let p = Point::new(5.0, 5.0);
let mut sp = SubPath::new(vec![Anchor { p, h_in: p - Vec2::new(0.004, 0.0), h_out: p + Vec2::new(3.0, 0.0), kind: AnchorKind::Smooth }], false);
assert_eq!(retract_tiny_handles(&mut sp, 0.01), 1);
let a = sp.anchors[0];
assert_eq!((a.h_in, a.kind), (p, AnchorKind::Corner));
assert_eq!(a.h_out, p + Vec2::new(3.0, 0.0), "a long handle stays");
assert_eq!(retract_tiny_handles(&mut sp, 0.01), 0);
}
#[test]
fn nearly_closed_paths_close() {
let mut sp = line(&[(0.0, 0.0), (10.0, 0.0), (10.0, 10.0), (0.005, 0.0)], false);
assert!(close_nearly_closed(&mut sp, 0.01));
assert!(sp.closed);
assert_eq!(points(&sp), [(0.0, 0.0), (10.0, 0.0), (10.0, 10.0)]);
let mut apart = line(&[(0.0, 0.0), (10.0, 0.0), (10.0, 10.0), (1.0, 0.0)], false);
assert!(!close_nearly_closed(&mut apart, 0.01));
assert!(!apart.closed);
let mut two = line(&[(0.0, 0.0), (0.0, 0.0)], false);
assert!(!close_nearly_closed(&mut two, 0.01), "two anchors make no shape");
}
#[test]
fn near_points_group_but_linked_and_equal_ones_do_not() {
let pts: Vec<Point> = [(0.0, 0.0), (0.005, 0.0), (5.0, 5.0), (5.0, 5.0), (9.0, 9.0), (9.004, 9.0), (f64::NAN, 0.0)].map(Into::into).to_vec();
let groups = near_point_groups(&pts, 0.01, |a, b| (a, b) == (4, 5));
assert_eq!(groups, [vec![0, 1]], "equal points and linked ones are not snapped");
assert!(near_point_groups(&pts, 0.0, |_, _| false).is_empty());
assert!(near_point_groups(&pts, f64::NAN, |_, _| false).is_empty());
// Far coordinates don't overflow the grid.
let far: Vec<Point> = [(1e300, 1e300), (-1e300, 1e300)].map(Into::into).to_vec();
assert!(near_point_groups(&far, 1e-300, |_, _| false).is_empty());
}
#[test]
fn rejoin_welds_pieces_within_the_gap() {
// Three pieces of one zig-zag, the middle one drawn backwards, plus one too far away.
let pieces = [
line(&[(0.0, 0.0), (10.0, 0.0)], false),
line(&[(20.0, 10.0), (10.05, 0.0)], false),
line(&[(20.0, 10.0), (30.0, 10.0)], false),
line(&[(31.0, 10.0), (40.0, 10.0)], false),
];
let out = rejoin(&pieces, 0.1);
assert_eq!(out.len(), 1, "{out:?}");
assert_eq!(out[0].members, [0, 1, 2]);
assert_eq!(points(&out[0].path), [(0.0, 0.0), (10.0, 0.0), (20.0, 10.0), (30.0, 10.0)]);
assert!(!out[0].path.closed);
// With a wider gap the last piece joins too.
assert_eq!(rejoin(&pieces, 1.0)[0].members, [0, 1, 2, 3]);
// Nothing within the gap: nothing joins.
assert!(rejoin(&pieces[2..], 0.5).is_empty());
}
#[test]
fn rejoin_takes_the_straightest_continuation_and_closes_loops() {
// Piece 0 ends at the origin; piece 1 goes straight on, piece 2 turns back.
let pieces = [line(&[(-10.0, 0.0), (0.0, 0.0)], false), line(&[(0.0, 0.0), (0.0, 10.0)], false), line(&[(0.0, 0.0), (10.0, 0.0)], false)];
let out = rejoin(&pieces, 0.1);
assert_eq!(out.len(), 1);
assert_eq!(out[0].members, [0, 2]);
// Four sides of a square close into one path.
let sides = [
line(&[(0.0, 0.0), (10.0, 0.0)], false),
line(&[(10.0, 0.0), (10.0, 10.0)], false),
line(&[(0.0, 10.0), (10.0, 10.0)], false),
line(&[(0.0, 10.0), (0.0, 0.0)], false),
];
let out = rejoin(&sides, 0.1);
assert_eq!(out.len(), 1);
assert!(out[0].path.closed);
assert_eq!(out[0].path.anchors.len(), 4);
// Closed, single-anchor and non-finite pieces stay out; bad gaps join nothing.
let odd = [line(&[(0.0, 0.0), (1.0, 0.0), (0.0, 1.0)], true), line(&[(1.0, 0.0)], false), line(&[(f64::NAN, 0.0), (0.0, 1.0)], false)];
assert!(rejoin(&odd, 1.0).is_empty());
assert!(rejoin(&sides, f64::NAN).is_empty() && rejoin(&sides, -1.0).is_empty());
}
+305
View File
@@ -0,0 +1,305 @@
//! Shape-keeping anchor removal: an anchor goes, and the handles of its two neighbours that face it
//! are refitted (their directions kept, their lengths fitted) so one segment follows the two that
//! met there. [`remove_anchor_keep_shape`] removes one anchor and says how far the outline moved;
//! [`simplify_keep_shape`] removes as many as it can while every new segment stays within a
//! tolerance of the original outline.
use kurbo::{CubicBez, ParamCurve, ParamCurveNearest};
use vectorcraft_geom::{Anchor, PathData, SubPath};
use crate::edit::remove_anchor;
use crate::fit::{end_tangent, start_tangent};
/// The turn (degrees) at an anchor above which `protect_corners` keeps it.
pub const SHARP_CORNER_DEG: f64 = 30.0;
/// Points sampled along each curve to measure how far two outlines are apart.
const SAMPLES: usize = 8;
/// Nearest-point accuracy (pt) of those measurements.
const ACCURACY: f64 = 1e-6;
/// The segment that takes the place of the two meeting at `cur` once it is removed: from `prev`
/// to `next`, their facing handles refitted ([`remove_anchor`]).
fn merged(prev: &Anchor, cur: &Anchor, next: &Anchor) -> CubicBez {
let mut run = SubPath::new(vec![*prev, *cur, *next], false);
remove_anchor(&mut run, 1);
match run.anchors.as_slice() {
[a, b] => CubicBez::new(prev.p, a.h_out, b.h_in, next.p),
_ => CubicBez::new(prev.p, prev.h_out, next.h_in, next.p),
}
}
/// How far `fitted` strays from the run of segments `original` it replaces, measured both ways
/// (pt): the farthest a sampled point of either lies from the other.
fn deviation(original: &[&CubicBez], fitted: &CubicBez) -> f64 {
let t = |k: usize| k as f64 / SAMPLES as f64;
let mut worst: f64 = 0.0;
for c in original {
for k in 0..=SAMPLES {
worst = worst.max(fitted.nearest(c.eval(t(k)), ACCURACY).distance_sq);
}
}
for k in 1..SAMPLES {
let p = fitted.eval(t(k));
worst = worst.max(original.iter().map(|c| c.nearest(p, ACCURACY).distance_sq).fold(f64::INFINITY, f64::min));
}
worst.sqrt()
}
/// The fewest anchors a subpath keeps: two when open, three when closed.
fn min_anchors(sp: &SubPath) -> usize {
if sp.closed { 3 } else { 2 }
}
/// Indices of anchor `i`'s neighbours in a subpath of `n` anchors (`None` for an open end).
fn neighbours(n: usize, closed: bool, i: usize) -> Option<(usize, usize)> {
match closed {
true => (n > 0).then(|| ((i + n - 1) % n, (i + 1) % n)),
false => (i > 0 && i + 1 < n).then(|| (i - 1, i + 1)),
}
}
fn finite(sp: &SubPath) -> bool {
sp.anchors.iter().all(|a| a.p.is_finite() && a.h_in.is_finite() && a.h_out.is_finite())
}
/// Does the path turn by more than [`SHARP_CORNER_DEG`] at anchor `i` (an inner anchor)?
fn is_sharp(sp: &SubPath, i: usize) -> bool {
let Some((prev, _)) = neighbours(sp.anchors.len(), sp.closed, i) else { return false };
let (into, out) = (end_tangent(&sp.segment(prev)), start_tangent(&sp.segment(i)));
into.dot(out).clamp(-1.0, 1.0).acos().to_degrees() > SHARP_CORNER_DEG
}
/// Give anchors `prev` and `next` the facing handles of segment `c`.
fn apply(sp: &mut SubPath, prev: usize, next: usize, c: &CubicBez) {
if let Some(a) = sp.anchors.get_mut(prev) {
a.h_out = c.p1;
}
if let Some(a) = sp.anchors.get_mut(next) {
a.h_in = c.p2;
}
}
/// Remove anchor `index` of `sp`, refitting the handles of its neighbours that face it so one
/// segment follows the two that met there → (the new subpath, how far its outline strays from the
/// old one, in points). `None` when the anchor can't go: out of range, an end of an open subpath,
/// a non-finite coordinate, or the subpath would keep fewer than two (open) or three (closed)
/// anchors.
pub fn remove_anchor_keep_shape(sp: &SubPath, index: usize) -> Option<(SubPath, f64)> {
let n = sp.anchors.len();
if n <= min_anchors(sp) || index >= n || !finite(sp) {
return None;
}
let (prev, next) = neighbours(n, sp.closed, index)?;
let c = merged(sp.anchors.get(prev)?, sp.anchors.get(index)?, sp.anchors.get(next)?);
let error = deviation(&[&sp.segment(prev), &sp.segment(index)], &c);
let mut out = sp.clone();
apply(&mut out, prev, next, &c);
out.anchors.remove(index);
Some((out, error))
}
/// Remove as many anchors of `path` as can go while its outline stays within `tolerance` points of
/// where it was, refitting the handles beside each ([`remove_anchor_keep_shape`]). Every new segment
/// is measured against the original outline it replaces, so removals don't add up past the
/// tolerance. With `protect_corners`, anchors where the path turns by more than
/// [`SHARP_CORNER_DEG`] stay. Open ends always stay. A subpath with a non-finite coordinate, and a
/// tolerance that isn't a positive number, are left as they are.
pub fn simplify_keep_shape(path: &PathData, tolerance: f64, protect_corners: bool) -> PathData {
if !(tolerance > 0.0 && tolerance.is_finite()) {
return path.clone();
}
PathData::new(path.subpaths.iter().map(|sp| Reducer::new(sp, protect_corners).run(tolerance)).collect())
}
/// One subpath being reduced: what is left of it, and where its anchors were in the original.
struct Reducer<'a> {
original: &'a SubPath,
segments: Vec<CubicBez>,
/// Anchors of the original that stay whatever the tolerance (sharp corners).
kept: Vec<bool>,
cur: SubPath,
/// The original index of each anchor left.
origin: Vec<usize>,
}
impl<'a> Reducer<'a> {
fn new(sp: &'a SubPath, protect_corners: bool) -> Self {
let n = sp.anchors.len();
Reducer {
original: sp,
segments: (0..sp.segment_count()).map(|i| sp.segment(i)).collect(),
kept: (0..n).map(|i| protect_corners && is_sharp(sp, i)).collect(),
cur: sp.clone(),
origin: (0..n).collect(),
}
}
/// Each pass removes the anchors whose removal strays least, no two side by side (so each
/// segment measured is the one that results), until none can go.
fn run(mut self, tolerance: f64) -> SubPath {
if self.cur.anchors.len() <= min_anchors(self.original) || !finite(self.original) {
return self.cur;
}
while self.pass(tolerance) {}
self.cur
}
/// The original segments between original anchors `from` and `to`, round the end of a closed
/// path.
fn span(&self, from: usize, to: usize) -> Option<Vec<&CubicBez>> {
Some(match from < to {
true => self.segments.get(from..to)?.iter().collect(),
false => self.segments.get(from..)?.iter().chain(self.segments.get(..to)?).collect(),
})
}
/// Anchor `i` of what is left, if it may go: (how far its removal strays, the segment that
/// replaces its two).
fn candidate(&self, i: usize, tolerance: f64) -> Option<(f64, CubicBez)> {
if self.origin.get(i).and_then(|&o| self.kept.get(o)) != Some(&false) {
return None;
}
let (prev, next) = neighbours(self.cur.anchors.len(), self.cur.closed, i)?;
let a = &self.cur.anchors;
let c = merged(a.get(prev)?, a.get(i)?, a.get(next)?);
let e = deviation(&self.span(*self.origin.get(prev)?, *self.origin.get(next)?)?, &c);
(e <= tolerance).then_some((e, c))
}
/// One pass: whether any anchor went.
fn pass(&mut self, tolerance: f64) -> bool {
let m = self.cur.anchors.len();
let min = min_anchors(self.original);
if m <= min {
return false;
}
let mut found: Vec<(f64, usize, CubicBez)> = (0..m).filter_map(|i| self.candidate(i, tolerance).map(|(e, c)| (e, i, c))).collect();
found.sort_by(|a, b| a.0.total_cmp(&b.0));
let mut gone = vec![false; m];
let mut left = m - min;
for (_, i, c) in found {
let Some((prev, next)) = neighbours(m, self.cur.closed, i) else { continue };
if left == 0 || gone.get(prev) != Some(&false) || gone.get(next) != Some(&false) {
continue;
}
apply(&mut self.cur, prev, next, &c);
if let Some(g) = gone.get_mut(i) {
*g = true;
}
left -= 1;
}
let mut keep = gone.iter().map(|g| !g);
self.cur.anchors.retain(|_| keep.next().unwrap_or(true));
let mut keep = gone.iter().map(|g| !g);
self.origin.retain(|_| keep.next().unwrap_or(true));
left < m - min
}
}
#[cfg(test)]
mod tests {
use kurbo::{ParamCurveNearest, Point, Vec2};
use vectorcraft_geom::{Anchor, PathData, SubPath};
use super::{remove_anchor_keep_shape, simplify_keep_shape};
/// A circle of radius `r` about the origin drawn with `n` smooth anchors.
fn circle(r: f64, n: usize) -> SubPath {
let step = std::f64::consts::TAU / n as f64;
let k = 4.0 / 3.0 * (step / 4.0).tan() * r;
let anchors = (0..n)
.map(|i| {
let a = step * i as f64;
let p = Point::new(r * a.cos(), r * a.sin());
Anchor::smooth(p, p + Vec2::new(-a.sin(), a.cos()) * k)
})
.collect();
SubPath::new(anchors, true)
}
/// The farthest a point of `sp`'s outline lies from the circle of radius `r`.
fn off_circle(sp: &SubPath, r: f64) -> f64 {
(0..sp.segment_count())
.flat_map(|i| (0..=20).map(move |k| (i, k as f64 / 20.0)))
.map(|(i, t)| (kurbo::ParamCurve::eval(&sp.segment(i), t).to_vec2().hypot() - r).abs())
.fold(0.0, f64::max)
}
#[test]
fn a_circle_keeps_its_shape_with_fewer_anchors() {
let sp = circle(50.0, 16);
let out = simplify_keep_shape(&PathData::single(sp), 0.2, false);
let s = &out.subpaths[0];
assert!(s.anchors.len() <= 8 && s.anchors.len() >= 3, "{} anchors left", s.anchors.len());
assert!(s.closed);
assert!(off_circle(s, 50.0) <= 0.2 + 1e-6, "strays {}", off_circle(s, 50.0));
}
#[test]
fn one_removal_reports_how_far_the_outline_moved() {
let sp = circle(50.0, 16);
let (out, err) = remove_anchor_keep_shape(&sp, 3).unwrap();
assert_eq!(out.anchors.len(), 15);
assert!(err < 0.05, "{err}");
assert!((off_circle(&out, 50.0) - err).abs() < 0.05);
// A corner of a square moves its outline a long way.
let sq = SubPath::polyline(&[(0.0, 0.0).into(), (10.0, 0.0).into(), (10.0, 10.0).into(), (0.0, 10.0).into()], true);
let (_, err) = remove_anchor_keep_shape(&sq, 1).unwrap();
assert!((err - 50f64.sqrt()).abs() < 1e-3, "{err}");
}
#[test]
fn sharp_corners_stay_when_protected() {
// A 90° corner at (1, 0) that a 1 pt tolerance would otherwise take away.
let sp = SubPath::polyline(&[(0.0, 0.0).into(), (1.0, 0.0).into(), (1.0, 1.0).into()], false);
let path = PathData::single(sp);
assert_eq!(simplify_keep_shape(&path, 1.0, false).anchor_count(), 2);
assert_eq!(simplify_keep_shape(&path, 1.0, true).anchor_count(), 3);
// Collinear anchors on a straight edge go either way; the square's corners stay.
let sq = SubPath::polyline(
&[(0.0, 0.0).into(), (5.0, 0.0).into(), (10.0, 0.0).into(), (10.0, 10.0).into(), (0.0, 10.0).into(), (0.0, 5.0).into()],
true,
);
let out = simplify_keep_shape(&PathData::single(sq), 0.01, true);
let pts: Vec<Point> = out.subpaths[0].anchors.iter().map(|a| a.p).collect();
assert_eq!(pts, [(0.0, 0.0).into(), (10.0, 0.0).into(), (10.0, 10.0).into(), (0.0, 10.0).into()] as [Point; 4]);
}
#[test]
fn removals_do_not_add_up_past_the_tolerance() {
// A gentle arc of many anchors: each removal alone is small, together they would flatten it.
let pts: Vec<Point> = (0..=40).map(|i| Point::new(i as f64, (i as f64 / 40.0 * std::f64::consts::PI).sin() * 3.0)).collect();
let sp = SubPath::polyline(&pts, false);
let out = simplify_keep_shape(&PathData::single(sp.clone()), 0.1, false);
let s = &out.subpaths[0];
assert!(s.anchors.len() < 41);
for p in &pts {
let d = (0..s.segment_count()).map(|i| s.segment(i).nearest(*p, 1e-6).distance_sq.sqrt()).fold(f64::INFINITY, f64::min);
assert!(d <= 0.1 + 1e-6, "{p:?} is {d} away");
}
}
#[test]
fn degenerate_input_is_left_alone() {
let line = SubPath::polyline(&[(0.0, 0.0).into(), (1.0, 0.0).into()], false);
assert!(remove_anchor_keep_shape(&line, 0).is_none(), "two anchors stay");
let three = SubPath::polyline(&[(0.0, 0.0).into(), (1.0, 0.0).into(), (2.0, 0.0).into()], false);
assert!(remove_anchor_keep_shape(&three, 0).is_none(), "an open end stays");
assert!(remove_anchor_keep_shape(&three, 9).is_none(), "out of range");
assert_eq!(remove_anchor_keep_shape(&three, 1).unwrap().0.anchors.len(), 2);
let tri = SubPath::polyline(&[(0.0, 0.0).into(), (1.0, 0.0).into(), (0.0, 1.0).into()], true);
assert!(remove_anchor_keep_shape(&tri, 1).is_none(), "a closed path keeps three");
let nan = SubPath::polyline(&[(0.0, 0.0).into(), (f64::NAN, 0.0).into(), (2.0, 0.0).into()], false);
assert!(remove_anchor_keep_shape(&nan, 1).is_none());
for path in [PathData::default(), PathData::single(SubPath::default()), PathData::single(nan.clone()), PathData::single(three.clone())] {
// Compared as text: NaN never equals itself.
for tol in [f64::NAN, -1.0, 0.0, f64::INFINITY] {
assert_eq!(format!("{:?}", simplify_keep_shape(&path, tol, false)), format!("{path:?}"));
}
let _ = simplify_keep_shape(&path, 1.0, true);
}
assert_eq!(format!("{:?}", simplify_keep_shape(&PathData::single(nan.clone()), 1.0, false)), format!("{:?}", PathData::single(nan)));
}
}
+1
View File
@@ -6,6 +6,7 @@
//! anchors. See the crate README for the API overview.
#![forbid(unsafe_code)]
pub mod addons;
mod boolean;
mod budget;
mod edit;
+11
View File
@@ -0,0 +1,11 @@
//! VectorCraft's built-in add-ons in the interface: each has a panel and a submenu at the top of
//! Object › Plug-ins, and acts through its engine commands (`addon.<name>.*`).
pub mod cleanup;
use crate::menus::Item;
/// The add-ons' submenus, then a separator: the head of Object › Plug-ins.
pub fn plugin_menu() -> Vec<Item> {
vec![Item::Sub("Artwork Cleanup", cleanup::menu()), Item::Sep]
}
+191
View File
@@ -0,0 +1,191 @@
//! Artwork Cleanup's panel and submenu (Object › Plug-ins › Artwork Cleanup). The panel runs the
//! Document Health Check on the selection, or the document while nothing is selected, lists what it
//! found, and shows or fixes the issues picked in the list (Fix All: every issue listed), each fix
//! one undo step. Below, the add-on's one-click tools open their options as their menu items do.
use egui::{Align, Layout, Ui};
use serde_json::{Value, json};
use crate::VectorcraftApp;
use crate::menus::Item;
use crate::panels::{empty_state, pstate, set_pstate};
use crate::widgets::{self, dim_label};
/// The panel's id (`window.panel {panel: "artworkCleanup"}`).
pub const ID: &str = "artworkCleanup";
/// Where the panel keeps its last check (panel state).
const STATE: &str = "artwork-cleanup";
/// The one-click tools: (menu label, command, the options their dialog opens with).
fn tools() -> [(&'static str, &'static str, Value); 3] {
[
("Simplify Anchors (Keep Shape)…", "addon.cleanup.simplify", json!({"tolerance": 20, "protectCorners": false})),
("Rejoin Path Pieces…", "addon.cleanup.rejoin", json!({"gap": 0.1})),
("Deep Ungroup…", "addon.cleanup.deepUngroup", json!({"includeClipGroups": false})),
]
}
/// Object › Plug-ins › Artwork Cleanup.
pub fn menu() -> Vec<Item> {
let mut items = vec![Item::Cmd("Document Health Check", "window.panel", json!({ "panel": ID })), Item::Sep];
items.extend(tools().into_iter().map(|(label, id, p)| Item::Cmd(label, id, p)));
items.extend([Item::Sep, Item::Cmd("Fix All Issues", "addon.cleanup.fix", Value::Null)]);
items
}
/// The panel's last check.
#[derive(Clone, Default)]
struct Results {
/// Whether a check has run (for this panel).
checked: bool,
/// The scope it looked at (`selection` or `document`).
scope: String,
/// The document and its revision it looked at.
doc: usize,
revision: u64,
/// What it found: (kind, label, count).
rows: Vec<(String, String, u64)>,
/// The kinds picked in the list.
picked: Vec<String>,
/// What the last fix did.
status: String,
}
/// Run the check (in `scope`, or the default one) → its results.
fn check(app: &mut VectorcraftApp, scope: Option<&str>) -> Results {
let p = scope.map_or_else(|| json!({}), |s| json!({ "scope": s }));
let mut r = Results { checked: true, doc: app.session.active_index().unwrap_or(0), ..Results::default() };
r.revision = app.session.active().map_or(0, |d| d.revision);
match app.session.execute("addon.cleanup.check", &p) {
Ok(v) => {
r.scope = v["scope"].as_str().unwrap_or("document").to_string();
let row = |f: &Value| Some((f["kind"].as_str()?.to_string(), f["label"].as_str()?.to_string(), f["count"].as_u64()?));
r.rows = v["findings"].as_array().into_iter().flatten().filter_map(row).collect();
}
Err(e) => app.status(e.to_string()),
}
r
}
/// Fix the issues of `kinds` in the last check's scope, then check that scope again.
fn fix(app: &mut VectorcraftApp, r: &Results, kinds: Vec<String>) -> Results {
match app.run("addon.cleanup.fix", json!({ "scope": r.scope, "kinds": kinds })) {
Ok(v) => {
let fixed = v["total"].as_u64().unwrap_or(0).to_string();
// A selection fixed away leaves nothing to check again: nothing is left.
let mut again = if app.session.active().is_some_and(|d| d.selection.is_empty()) && r.scope == "selection" {
Results { checked: true, scope: r.scope.clone(), ..Results::default() }
} else {
check(app, Some(&r.scope))
};
again.status = crate::i18n::fmt(tl!("Issues fixed: {count}"), &[("count", &fixed)]);
again
}
Err(e) => {
app.status(e);
r.clone()
}
}
}
/// What the panel's buttons asked for this frame.
enum Action {
Check,
Show(String),
Fix(Vec<String>),
Tool(usize),
}
pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) {
let Some(st) = app.session.active() else {
empty_state(ui, "sparkles", tl!("No document"), tl!("Open a document to check it for issues."));
return;
};
let (selected, revision, doc) = (!st.selection.is_empty(), st.revision, app.session.active_index().unwrap_or(0));
let mut r: Results = pstate(ui.ctx(), STATE);
if r.doc != doc {
r = Results::default();
}
let mut action = None;
let width = ui.available_width();
if widgets::flat_button(ui, if selected { tl!("Check Selection") } else { tl!("Check Document") }, width).clicked() {
action = Some(Action::Check);
}
if r.checked {
ui.add_space(4.0);
results(ui, &mut r, &mut action);
if r.revision != revision {
dim_label(ui, tl!("Changed since the last check."));
}
}
widgets::divider(ui);
ui.add_enabled_ui(selected, |ui| {
for (i, (label, ..)) in tools().iter().enumerate() {
if widgets::flat_button(ui, tl!(label), width).clicked() {
action = Some(Action::Tool(i));
}
}
});
match action {
Some(Action::Check) => r = check(app, None),
Some(Action::Show(kind)) => {
if let Err(e) = app.run("addon.cleanup.show", json!({ "kind": kind, "scope": r.scope })) {
app.status(e);
}
}
Some(Action::Fix(kinds)) => r = fix(app, &r, kinds),
Some(Action::Tool(i)) => {
if let Some((label, id, p)) = tools().into_iter().nth(i) {
let (id, p) = crate::menus::click_target(label, id, &p);
crate::menus::invoke(app, &id, p);
}
}
None => {}
}
set_pstate(ui.ctx(), STATE, r);
}
/// The list of what the check found (click picks a row, Ctrl/Cmd- or Shift-click adds one), and
/// Show, Fix Selected and Fix All.
fn results(ui: &mut Ui, r: &mut Results, action: &mut Option<Action>) {
if r.rows.is_empty() {
dim_label(ui, tl!("No issues found."));
}
for (kind, label, count) in &r.rows {
let on = r.picked.contains(kind);
let clicked = ui
.horizontal(|ui| {
let resp = ui.selectable_label(on, tl!(label));
ui.with_layout(Layout::right_to_left(Align::Center), |ui| dim_label(ui, &count.to_string()));
resp.clicked()
})
.inner;
if clicked {
match ui.input(|i| i.modifiers.command || i.modifiers.shift) {
true if on => r.picked.retain(|k| k != kind),
true => r.picked.push(kind.clone()),
false => r.picked = vec![kind.clone()],
}
}
}
if !r.status.is_empty() {
dim_label(ui, &r.status);
}
ui.horizontal(|ui| {
if ui.add_enabled_ui(r.picked.len() == 1, |ui| widgets::flat_button(ui, tl!("Show"), 56.0)).inner.clicked()
&& let Some(kind) = r.picked.first()
{
*action = Some(Action::Show(kind.clone()));
}
if ui.add_enabled_ui(!r.picked.is_empty(), |ui| widgets::flat_button(ui, tl!("Fix Selected"), 88.0)).inner.clicked() {
*action = Some(Action::Fix(r.picked.clone()));
}
if ui.add_enabled_ui(!r.rows.is_empty(), |ui| widgets::flat_button(ui, tl!("Fix All"), 64.0)).inner.clicked() {
*action = Some(Action::Fix(r.rows.iter().map(|row| row.0.clone()).collect()));
}
});
}
#[cfg(test)]
mod tests;
@@ -0,0 +1,67 @@
use serde_json::json;
use vectorcraft_engine::Session;
use super::{ID, STATE, check, fix, show};
use crate::panels::set_pstate;
use crate::tests_labels::painted_text;
use crate::{VectorcraftApp, menus};
/// An app with a rectangle and a stray point (a path of one anchor), nothing selected.
fn app() -> VectorcraftApp {
let mut app = VectorcraftApp::new(Session::new(), Default::default());
app.run("file.new", json!({"width": 200, "height": 200})).unwrap();
app.run("shape.rectangle", json!({"x": 10, "y": 10, "width": 40, "height": 40})).unwrap();
app.run("path.create", json!({"anchors": [{"x": 80, "y": 80}]})).unwrap();
app.run("select.none", json!({})).unwrap();
app
}
#[test]
fn the_plugins_submenu_opens_the_panel_and_lists_the_tools() {
let mut app = app();
let entries = menus::menu_entries(&app);
let under = |label: &str| entries.iter().find(|e| e.path == ["Object", "Plug-ins", "Artwork Cleanup"] && e.label == label);
let item = under("Document Health Check").expect("listed");
assert_eq!((item.command.as_deref(), &item.params), (Some("window.panel"), &json!({ "panel": ID })));
for (label, id) in [
("Simplify Anchors (Keep Shape)…", "addon.cleanup.simplify"),
("Rejoin Path Pieces…", "addon.cleanup.rejoin"),
("Deep Ungroup…", "addon.cleanup.deepUngroup"),
("Fix All Issues", "addon.cleanup.fix"),
] {
assert_eq!(under(label).and_then(|e| e.command.as_deref()), Some(id), "{label}");
}
// Install Plug-in… still follows.
assert!(entries.iter().any(|e| e.path == ["Object", "Plug-ins"] && e.label == "Install Plug-in…"));
assert!(!crate::panel_docking::showing(&app.ui, ID));
menus::invoke(&mut app, "window.panel", item.params.clone());
assert!(crate::panel_docking::showing(&app.ui, ID), "the panel opens");
// The tools open their options first, as dialogs.
let (target, p) = menus::click_target("Deep Ungroup…", "addon.cleanup.deepUngroup", &json!({"includeClipGroups": false}));
assert_eq!((target.as_str(), &p["command"]), ("ui.paramDialog", &json!("addon.cleanup.deepUngroup")));
}
#[test]
fn check_lists_what_it_found_and_fix_clears_it() {
let mut app = app();
let text = painted_text(&mut app, show);
assert!(text.contains("Check Document") && !text.contains("Stray Points"), "{text}");
let r = check(&mut app, None);
assert_eq!(r.scope, "document");
assert_eq!(r.rows, [("strayPoints".to_string(), "Stray Points".to_string(), 1)]);
let shown = r.clone();
let text = painted_text(&mut app, |app, ui| {
set_pstate(ui.ctx(), STATE, shown.clone());
show(app, ui);
});
assert!(text.contains("Stray Points") && text.contains("Fix Selected") && text.contains("Fix All"), "{text}");
let steps = app.session.doc().unwrap().history.undo.len();
let after = fix(&mut app, &r, vec!["strayPoints".into()]);
assert!(after.rows.is_empty() && after.status.contains('1'), "{}", after.status);
assert_eq!(app.session.doc().unwrap().history.undo.len(), steps + 1, "one undo step");
let text = painted_text(&mut app, |app, ui| {
set_pstate(ui.ctx(), STATE, after.clone());
show(app, ui);
});
assert!(text.contains("No issues found."), "{text}");
}
+47
View File
@@ -5590,3 +5590,50 @@ axis Y Axis Y ос
Average & Join Усредняване и свързване
anchor Points: Точки:
@msg select two open path ends, or two open paths, to average and join изберете два края на отворени пътища или два отворени пътя за усредняване и свързване
# Artwork Cleanup add-on (Object › Plug-ins › Artwork Cleanup): its panel, tools, issue kinds and messages.
Artwork Cleanup Почистване на графиката
Simplify Anchors (Keep Shape)… Опростяване на опорните точки (запазване на формата)…
Health Check Проверка на състоянието
Show Issue Показване на проблема
Fix All Issues Отстраняване на всички проблеми
Fix Issues Отстраняване на проблемите
Deep Ungroup… Разгрупиране на всички нива…
Rejoin Path Pieces… Повторно свързване на части от пътища…
Document Health Check Проверка на състоянието на документа
Check Selection Проверка на селекцията
Check Document Проверка на документа
No issues found. Не са открити проблеми.
Changed since the last check. Променено след последната проверка.
Fix Selected Отстраняване на избраните
Fix All Отстраняване на всички
Issues fixed: {count} Отстранени проблеми: {count}
Open a document to check it for issues. Отворете документ, за да го проверите за проблеми.
Empty Text Празен текст
Unpainted Objects Обекти без цвят
Zero-Opacity Objects Обекти с нулева непрозрачност
Duplicate Objects Дублирани обекти
Off-Artboard Art Графика извън работните области
Duplicate Anchors Дублирани опорни точки
Tiny Handles Миниатюрни дръжки
Collinear Anchors Колинеарни опорни точки
Nearly Coincident Anchors Почти съвпадащи опорни точки
Nearly Closed Paths Почти затворени пътища
Filled Open Paths Запълнени отворени пътища
Needless Clipping Groups Излишни групи за изрязване
Needless Compound Paths Излишни съставни пътища
Needless Groups Излишни групи
Empty Artboards Празни работни области
Protect Corners Защита на ъглите
Gap Разстояние
Include Clip Groups Включване на групите за изрязване
@msg `tolerance` must be from 1 to 100 `tolerance` трябва да е от 1 до 100
@msg `gap` must be from 0 to 6 pt `gap` трябва да е от 0 до 6 pt
@msg Simplify Anchors: the paths have too many anchors to simplify at once Опростяване на опорните точки: пътищата имат твърде много опорни точки, за да се опростят наведнъж
@msg Rejoin Path Pieces: too many paths to rejoin at once Повторно свързване на части от пътища: твърде много пътища за свързване наведнъж
@msg None of these issues are left: check again Не е останал нито един от тези проблеми: проверете отново
@msg `kinds` must be a list of issue kinds `kinds` трябва да е списък с видове проблеми
@msg missing `kind` липсва `kind`
Simplify Anchors (Keep Shape) Опростяване на опорните точки (запазване на формата)
Deep Ungroup Разгрупиране на всички нива
Rejoin Path Pieces Повторно свързване на части от пътища
Can't render this file because it contains an unexpected character in line 2 and column 73.
+47
View File
@@ -5691,3 +5691,50 @@ theme Appearance Aparença
Average & Join Mitjana i unió
anchor Points: Punts:
@msg select two open path ends, or two open paths, to average and join selecciona dos extrems de traçats oberts, o dos traçats oberts, per fer-ne la mitjana i unir-los
# Artwork Cleanup add-on (Object › Plug-ins › Artwork Cleanup): its panel, tools, issue kinds and messages.
Artwork Cleanup Neteja de l’art
Simplify Anchors (Keep Shape)… Simplifica els punts d’ancoratge (mantén la forma)…
Health Check Revisió de l’estat
Show Issue Mostra el problema
Fix All Issues Corregeix tots els problemes
Fix Issues Corregeix els problemes
Deep Ungroup… Desagrupa tots els nivells…
Rejoin Path Pieces… Torna a unir els trossos de traçat…
Document Health Check Revisió de l’estat del document
Check Selection Revisa la selecció
Check Document Revisa el document
No issues found. No s’ha trobat cap problema.
Changed since the last check. Ha canviat des de l’última revisió.
Fix Selected Corregeix els seleccionats
Fix All Corregeix-ho tot
Issues fixed: {count} Problemes corregits: {count}
Open a document to check it for issues. Obriu un document per revisar-ne els problemes.
Empty Text Text buit
Unpainted Objects Objectes sense pintura
Zero-Opacity Objects Objectes amb opacitat zero
Duplicate Objects Objectes duplicats
Off-Artboard Art Art fora dels taulers
Duplicate Anchors Punts d’ancoratge duplicats
Tiny Handles Nanses minúscules
Collinear Anchors Punts d’ancoratge col·lineals
Nearly Coincident Anchors Punts d’ancoratge gairebé coincidents
Nearly Closed Paths Traçats gairebé tancats
Filled Open Paths Traçats oberts amb emplenament
Needless Clipping Groups Grups de retall innecessaris
Needless Compound Paths Traçats compostos innecessaris
Needless Groups Grups innecessaris
Empty Artboards Taulers buits
Protect Corners Protegeix les cantonades
Gap Separació
Include Clip Groups Inclou els grups de retall
@msg `tolerance` must be from 1 to 100 `tolerance` ha d’estar entre 1 i 100
@msg `gap` must be from 0 to 6 pt `gap` ha d’estar entre 0 i 6 pt
@msg Simplify Anchors: the paths have too many anchors to simplify at once Simplifica els punts d’ancoratge: els traçats tenen massa punts d’ancoratge per simplificar-los d’un cop
@msg Rejoin Path Pieces: too many paths to rejoin at once Torna a unir els trossos de traçat: massa traçats per unir-los d’un cop
@msg None of these issues are left: check again Ja no queda cap d’aquests problemes: torneu a revisar
@msg `kinds` must be a list of issue kinds `kinds` ha de ser una llista de tipus de problema
@msg missing `kind` falta `kind`
Simplify Anchors (Keep Shape) Simplifica els punts d’ancoratge (mantén la forma)
Deep Ungroup Desagrupa tots els nivells
Rejoin Path Pieces Torna a unir els trossos de traçat
Can't render this file because it contains an unexpected character in line 2 and column 73.
+47
View File
@@ -5700,3 +5700,50 @@ axis Y Axis osa Y
Average & Join Zprůměrovat a spojit
anchor Points: Body:
@msg select two open path ends, or two open paths, to average and join vyberte dva konce otevřených cest nebo dvě otevřené cesty ke zprůměrování a spojení
# Artwork Cleanup add-on (Object › Plug-ins › Artwork Cleanup): its panel, tools, issue kinds and messages.
Artwork Cleanup Vyčištění kresby
Simplify Anchors (Keep Shape)… Zjednodušit kotevní body (zachovat tvar)…
Health Check Kontrola stavu
Show Issue Zobrazit problém
Fix All Issues Opravit všechny problémy
Fix Issues Opravit problémy
Deep Ungroup… Oddělit všechny úrovně…
Rejoin Path Pieces… Znovu spojit části cest…
Document Health Check Kontrola stavu dokumentu
Check Selection Zkontrolovat výběr
Check Document Zkontrolovat dokument
No issues found. Nebyly nalezeny žádné problémy.
Changed since the last check. Změněno od poslední kontroly.
Fix Selected Opravit vybrané
Fix All Opravit vše
Issues fixed: {count} Opravené problémy: {count}
Open a document to check it for issues. Otevřete dokument a zkontrolujte jeho problémy.
Empty Text Prázdný text
Unpainted Objects Objekty bez barvy
Zero-Opacity Objects Objekty s nulovým krytím
Duplicate Objects Duplicitní objekty
Off-Artboard Art Kresba mimo pracovní plochy
Duplicate Anchors Duplicitní kotevní body
Tiny Handles Drobné úchyty
Collinear Anchors Kolineární kotevní body
Nearly Coincident Anchors Téměř shodné kotevní body
Nearly Closed Paths Téměř uzavřené cesty
Filled Open Paths Vyplněné otevřené cesty
Needless Clipping Groups Zbytečné ořezové skupiny
Needless Compound Paths Zbytečné složené cesty
Needless Groups Zbytečné skupiny
Empty Artboards Prázdné pracovní plochy
Protect Corners Chránit rohy
Gap Mezera
Include Clip Groups Zahrnout ořezové skupiny
@msg `tolerance` must be from 1 to 100 `tolerance` musí být od 1 do 100
@msg `gap` must be from 0 to 6 pt `gap` musí být od 0 do 6 pt
@msg Simplify Anchors: the paths have too many anchors to simplify at once Zjednodušit kotevní body: cesty mají příliš mnoho kotevních bodů, než aby je šlo zjednodušit najednou
@msg Rejoin Path Pieces: too many paths to rejoin at once Znovu spojit části cest: příliš mnoho cest ke spojení najednou
@msg None of these issues are left: check again Žádný z těchto problémů už nezůstal: zkontrolujte znovu
@msg `kinds` must be a list of issue kinds `kinds` musí být seznam druhů problémů
@msg missing `kind` chybí `kind`
Simplify Anchors (Keep Shape) Zjednodušit kotevní body (zachovat tvar)
Deep Ungroup Oddělit všechny úrovně
Rejoin Path Pieces Znovu spojit části cest
Can't render this file because it contains an unexpected character in line 2 and column 73.
+47
View File
@@ -5605,3 +5605,50 @@ axis Y Axis Y-akse
Average & Join Gennemsnit og forbind
anchor Points: Punkter:
@msg select two open path ends, or two open paths, to average and join markér to ender af åbne stier eller to åbne stier, der skal udlignes til gennemsnit og forbindes
# Artwork Cleanup add-on (Object › Plug-ins › Artwork Cleanup): its panel, tools, issue kinds and messages.
Artwork Cleanup Oprydning i grafik
Simplify Anchors (Keep Shape)… Forenkl ankerpunkter (bevar form)…
Health Check Sundhedstjek
Show Issue Vis problem
Fix All Issues Ret alle problemer
Fix Issues Ret problemer
Deep Ungroup… Afgruppér alle niveauer…
Rejoin Path Pieces… Forbind kurvestykker igen…
Document Health Check Sundhedstjek af dokument
Check Selection Kontrollér markering
Check Document Kontrollér dokument
No issues found. Ingen problemer fundet.
Changed since the last check. Ændret siden sidste kontrol.
Fix Selected Ret valgte
Fix All Ret alle
Issues fixed: {count} Rettede problemer: {count}
Open a document to check it for issues. Åbn et dokument for at kontrollere det for problemer.
Empty Text Tom tekst
Unpainted Objects Objekter uden farve
Zero-Opacity Objects Objekter med 0 % opacitet
Duplicate Objects Dublerede objekter
Off-Artboard Art Grafik uden for kunstpladerne
Duplicate Anchors Dublerede ankerpunkter
Tiny Handles Små håndtag
Collinear Anchors Kollineære ankerpunkter
Nearly Coincident Anchors Næsten sammenfaldende ankerpunkter
Nearly Closed Paths Næsten lukkede kurver
Filled Open Paths Udfyldte åbne kurver
Needless Clipping Groups Unødvendige beskæringsgrupper
Needless Compound Paths Unødvendige sammensatte kurver
Needless Groups Unødvendige grupper
Empty Artboards Tomme kunstplader
Protect Corners Beskyt hjørner
Gap Afstand
Include Clip Groups Medtag beskæringsgrupper
@msg `tolerance` must be from 1 to 100 `tolerance` skal være fra 1 til 100
@msg `gap` must be from 0 to 6 pt `gap` skal være fra 0 til 6 pt
@msg Simplify Anchors: the paths have too many anchors to simplify at once Forenkl ankerpunkter: kurverne har for mange ankerpunkter til at blive forenklet på én gang
@msg Rejoin Path Pieces: too many paths to rejoin at once Forbind kurvestykker igen: for mange kurver til at forbinde på én gang
@msg None of these issues are left: check again Ingen af disse problemer er tilbage: kontrollér igen
@msg `kinds` must be a list of issue kinds `kinds` skal være en liste over problemtyper
@msg missing `kind` `kind` mangler
Simplify Anchors (Keep Shape) Forenkl ankerpunkter (bevar form)
Deep Ungroup Afgruppér alle niveauer
Rejoin Path Pieces Forbind kurvestykker igen
Can't render this file because it contains an unexpected character in line 2 and column 73.
+47
View File
@@ -5282,3 +5282,50 @@ axis Y Axis Y-Achse
Average & Join Durchschnitt berechnen und zusammenfügen
anchor Points: Punkte:
@msg select two open path ends, or two open paths, to average and join wählen Sie zwei Enden offener Pfade oder zwei offene Pfade aus, um sie zu mitteln und zusammenzufügen
# Artwork Cleanup add-on (Object › Plug-ins › Artwork Cleanup): its panel, tools, issue kinds and messages.
Artwork Cleanup Bildmaterial-Bereinigung
Simplify Anchors (Keep Shape)… Ankerpunkte vereinfachen (Form erhalten)…
Health Check Zustandsprüfung
Show Issue Problem anzeigen
Fix All Issues Alle Probleme beheben
Fix Issues Probleme beheben
Deep Ungroup… Alle Gruppierungsebenen aufheben…
Rejoin Path Pieces… Pfadstücke wieder zusammenfügen…
Document Health Check Dokument-Zustandsprüfung
Check Selection Auswahl prüfen
Check Document Dokument prüfen
No issues found. Keine Probleme gefunden.
Changed since the last check. Seit der letzten Prüfung geändert.
Fix Selected Ausgewählte beheben
Fix All Alle beheben
Issues fixed: {count} Behobene Probleme: {count}
Open a document to check it for issues. Öffnen Sie ein Dokument, um es auf Probleme zu prüfen.
Empty Text Leerer Text
Unpainted Objects Objekte ohne Farbe
Zero-Opacity Objects Objekte mit 0 % Deckkraft
Duplicate Objects Doppelte Objekte
Off-Artboard Art Bildmaterial außerhalb der Zeichenflächen
Duplicate Anchors Doppelte Ankerpunkte
Tiny Handles Winzige Griffe
Collinear Anchors Kollineare Ankerpunkte
Nearly Coincident Anchors Fast deckungsgleiche Ankerpunkte
Nearly Closed Paths Fast geschlossene Pfade
Filled Open Paths Gefüllte offene Pfade
Needless Clipping Groups Überflüssige Schnittgruppen
Needless Compound Paths Überflüssige zusammengesetzte Pfade
Needless Groups Überflüssige Gruppen
Empty Artboards Leere Zeichenflächen
Protect Corners Ecken schützen
Gap Abstand
Include Clip Groups Schnittgruppen einbeziehen
@msg `tolerance` must be from 1 to 100 `tolerance` muss zwischen 1 und 100 liegen
@msg `gap` must be from 0 to 6 pt `gap` muss zwischen 0 und 6 pt liegen
@msg Simplify Anchors: the paths have too many anchors to simplify at once Ankerpunkte vereinfachen: Die Pfade haben zu viele Ankerpunkte, um sie auf einmal zu vereinfachen
@msg Rejoin Path Pieces: too many paths to rejoin at once Pfadstücke wieder zusammenfügen: zu viele Pfade, um sie auf einmal zusammenzufügen
@msg None of these issues are left: check again Keines dieser Probleme ist mehr vorhanden: Prüfen Sie erneut
@msg `kinds` must be a list of issue kinds `kinds` muss eine Liste von Problemarten sein
@msg missing `kind` `kind` fehlt
Simplify Anchors (Keep Shape) Ankerpunkte vereinfachen (Form erhalten)
Deep Ungroup Alle Gruppierungsebenen aufheben
Rejoin Path Pieces Pfadstücke wieder zusammenfügen
Can't render this file because it contains an unexpected character in line 2 and column 73.
+47
View File
@@ -5652,3 +5652,50 @@ axis Y Axis Άξονας Y
Average & Join Μέσος όρος και ένωση
anchor Points: Σημεία:
@msg select two open path ends, or two open paths, to average and join επιλέξτε δύο άκρα ανοιχτών διαδρομών ή δύο ανοιχτές διαδρομές για μέσο όρο και ένωση
# Artwork Cleanup add-on (Object › Plug-ins › Artwork Cleanup): its panel, tools, issue kinds and messages.
Artwork Cleanup Εκκαθάριση γραφικών
Simplify Anchors (Keep Shape)… Απλοποίηση σημείων αγκύρωσης (διατήρηση σχήματος)…
Health Check Έλεγχος κατάστασης
Show Issue Εμφάνιση προβλήματος
Fix All Issues Διόρθωση όλων των προβλημάτων
Fix Issues Διόρθωση προβλημάτων
Deep Ungroup… Κατάργηση ομαδοποίησης σε όλα τα επίπεδα…
Rejoin Path Pieces… Επανένωση τμημάτων διαδρομών…
Document Health Check Έλεγχος κατάστασης εγγράφου
Check Selection Έλεγχος επιλογής
Check Document Έλεγχος εγγράφου
No issues found. Δεν βρέθηκαν προβλήματα.
Changed since the last check. Άλλαξε μετά τον τελευταίο έλεγχο.
Fix Selected Διόρθωση επιλεγμένων
Fix All Διόρθωση όλων
Issues fixed: {count} Διορθωμένα προβλήματα: {count}
Open a document to check it for issues. Ανοίξτε ένα έγγραφο για να το ελέγξετε για προβλήματα.
Empty Text Κενό κείμενο
Unpainted Objects Αντικείμενα χωρίς χρώμα
Zero-Opacity Objects Αντικείμενα με μηδενική αδιαφάνεια
Duplicate Objects Διπλότυπα αντικείμενα
Off-Artboard Art Γραφικά εκτός πινάκων σχεδίασης
Duplicate Anchors Διπλότυπα σημεία αγκύρωσης
Tiny Handles Μικροσκοπικές λαβές
Collinear Anchors Συγγραμμικά σημεία αγκύρωσης
Nearly Coincident Anchors Σχεδόν συμπίπτοντα σημεία αγκύρωσης
Nearly Closed Paths Σχεδόν κλειστές διαδρομές
Filled Open Paths Γεμισμένες ανοιχτές διαδρομές
Needless Clipping Groups Περιττές ομάδες αποκοπής
Needless Compound Paths Περιττές σύνθετες διαδρομές
Needless Groups Περιττές ομάδες
Empty Artboards Κενοί πίνακες σχεδίασης
Protect Corners Προστασία γωνιών
Gap Απόσταση
Include Clip Groups Συμπερίληψη ομάδων αποκοπής
@msg `tolerance` must be from 1 to 100 Το `tolerance` πρέπει να είναι από 1 έως 100
@msg `gap` must be from 0 to 6 pt Το `gap` πρέπει να είναι από 0 έως 6 pt
@msg Simplify Anchors: the paths have too many anchors to simplify at once Απλοποίηση σημείων αγκύρωσης: οι διαδρομές έχουν πάρα πολλά σημεία αγκύρωσης για να απλοποιηθούν μονομιάς
@msg Rejoin Path Pieces: too many paths to rejoin at once Επανένωση τμημάτων διαδρομών: πάρα πολλές διαδρομές για επανένωση μονομιάς
@msg None of these issues are left: check again Δεν έχει απομείνει κανένα από αυτά τα προβλήματα: ελέγξτε ξανά
@msg `kinds` must be a list of issue kinds Το `kinds` πρέπει να είναι λίστα ειδών προβλημάτων
@msg missing `kind` λείπει το `kind`
Simplify Anchors (Keep Shape) Απλοποίηση σημείων αγκύρωσης (διατήρηση σχήματος)
Deep Ungroup Κατάργηση ομαδοποίησης σε όλα τα επίπεδα
Rejoin Path Pieces Επανένωση τμημάτων διαδρομών
Can't render this file because it contains an unexpected character in line 164 and column 5.
+47
View File
@@ -5642,3 +5642,50 @@ axis Y Axis Eje Y
Average & Join Promediar y unir
anchor Points: Puntos:
@msg select two open path ends, or two open paths, to average and join selecciona dos extremos de trazados abiertos, o dos trazados abiertos, para promediarlos y unirlos
# Artwork Cleanup add-on (Object › Plug-ins › Artwork Cleanup): its panel, tools, issue kinds and messages.
Artwork Cleanup Limpieza de ilustraciones
Simplify Anchors (Keep Shape)… Simplificar puntos de ancla (mantener forma)…
Health Check Revisión de estado
Show Issue Mostrar problema
Fix All Issues Corregir todos los problemas
Fix Issues Corregir problemas
Deep Ungroup… Desagrupar todos los niveles…
Rejoin Path Pieces… Volver a unir fragmentos de trazado…
Document Health Check Revisión de estado del documento
Check Selection Revisar selección
Check Document Revisar documento
No issues found. No se encontraron problemas.
Changed since the last check. Ha cambiado desde la última revisión.
Fix Selected Corregir seleccionados
Fix All Corregir todo
Issues fixed: {count} Problemas corregidos: {count}
Open a document to check it for issues. Abra un documento para buscar problemas en él.
Empty Text Texto vacío
Unpainted Objects Objetos sin pintar
Zero-Opacity Objects Objetos con opacidad cero
Duplicate Objects Objetos duplicados
Off-Artboard Art Ilustración fuera de las mesas de trabajo
Duplicate Anchors Puntos de ancla duplicados
Tiny Handles Manejadores diminutos
Collinear Anchors Puntos de ancla colineales
Nearly Coincident Anchors Puntos de ancla casi coincidentes
Nearly Closed Paths Trazados casi cerrados
Filled Open Paths Trazados abiertos con relleno
Needless Clipping Groups Grupos de recorte innecesarios
Needless Compound Paths Trazados compuestos innecesarios
Needless Groups Grupos innecesarios
Empty Artboards Mesas de trabajo vacías
Protect Corners Proteger esquinas
Gap Separación
Include Clip Groups Incluir grupos de recorte
@msg `tolerance` must be from 1 to 100 `tolerance` debe estar entre 1 y 100
@msg `gap` must be from 0 to 6 pt `gap` debe estar entre 0 y 6 pt
@msg Simplify Anchors: the paths have too many anchors to simplify at once Simplificar puntos de ancla: los trazados tienen demasiados puntos de ancla para simplificarlos de una vez
@msg Rejoin Path Pieces: too many paths to rejoin at once Volver a unir fragmentos de trazado: demasiados trazados para unirlos de una vez
@msg None of these issues are left: check again Ya no queda ninguno de estos problemas: vuelva a revisar
@msg `kinds` must be a list of issue kinds `kinds` debe ser una lista de tipos de problema
@msg missing `kind` falta `kind`
Simplify Anchors (Keep Shape) Simplificar puntos de ancla (mantener forma)
Deep Ungroup Desagrupar todos los niveles
Rejoin Path Pieces Volver a unir fragmentos de trazado
Can't render this file because it contains an unexpected character in line 2 and column 73.
+47
View File
@@ -5731,3 +5731,50 @@ axis Y Axis Y-akseli
Average & Join Keskiarvota ja liitä
anchor Points: Pisteet:
@msg select two open path ends, or two open paths, to average and join valitse kaksi avoimen polun päätä tai kaksi avointa polkua keskiarvotettaviksi ja liitettäviksi
# Artwork Cleanup add-on (Object › Plug-ins › Artwork Cleanup): its panel, tools, issue kinds and messages.
Artwork Cleanup Grafiikan siivous
Simplify Anchors (Keep Shape)… Yksinkertaista ankkuripisteet (säilytä muoto)…
Health Check Kuntotarkistus
Show Issue Näytä ongelma
Fix All Issues Korjaa kaikki ongelmat
Fix Issues Korjaa ongelmat
Deep Ungroup… Pura kaikki ryhmitystasot…
Rejoin Path Pieces… Yhdistä polun palat uudelleen…
Document Health Check Asiakirjan kuntotarkistus
Check Selection Tarkista valinta
Check Document Tarkista asiakirja
No issues found. Ongelmia ei löytynyt.
Changed since the last check. Muuttunut edellisen tarkistuksen jälkeen.
Fix Selected Korjaa valitut
Fix All Korjaa kaikki
Issues fixed: {count} Korjattuja ongelmia: {count}
Open a document to check it for issues. Avaa asiakirja tarkistaaksesi sen ongelmat.
Empty Text Tyhjä teksti
Unpainted Objects Värittömät objektit
Zero-Opacity Objects Täysin läpinäkyvät objektit
Duplicate Objects Objektien kaksoiskappaleet
Off-Artboard Art Piirtoalustojen ulkopuolinen grafiikka
Duplicate Anchors Kaksinkertaiset ankkuripisteet
Tiny Handles Pienet kahvat
Collinear Anchors Samalla suoralla olevat ankkuripisteet
Nearly Coincident Anchors Lähes yhtenevät ankkuripisteet
Nearly Closed Paths Lähes suljetut polut
Filled Open Paths Täytetyt avoimet polut
Needless Clipping Groups Tarpeettomat leikkausryhmät
Needless Compound Paths Tarpeettomat yhdistelmäpolut
Needless Groups Tarpeettomat ryhmät
Empty Artboards Tyhjät piirtoalustat
Protect Corners Suojaa kulmat
Gap Väli
Include Clip Groups Sisällytä leikkausryhmät
@msg `tolerance` must be from 1 to 100 `tolerance`-arvon on oltava 1–100
@msg `gap` must be from 0 to 6 pt `gap`-arvon on oltava 0–6 pt
@msg Simplify Anchors: the paths have too many anchors to simplify at once Yksinkertaista ankkuripisteet: poluissa on liian monta ankkuripistettä yksinkertaistettavaksi kerralla
@msg Rejoin Path Pieces: too many paths to rejoin at once Yhdistä polun palat uudelleen: liian monta polkua yhdistettäväksi kerralla
@msg None of these issues are left: check again Yhtään näistä ongelmista ei ole enää jäljellä: tarkista uudelleen
@msg `kinds` must be a list of issue kinds `kinds`-arvon on oltava luettelo ongelmatyypeistä
@msg missing `kind` `kind` puuttuu
Simplify Anchors (Keep Shape) Yksinkertaista ankkuripisteet (säilytä muoto)
Deep Ungroup Pura kaikki ryhmitystasot
Rejoin Path Pieces Yhdistä polun palat uudelleen
Can't render this file because it contains an unexpected character in line 2 and column 73.
+47
View File
@@ -5699,3 +5699,50 @@ axis Y Axis Axe Y
Average & Join Alignement et jonction
anchor Points: Points :
@msg select two open path ends, or two open paths, to average and join sélectionnez deux extrémités de tracés ouverts, ou deux tracés ouverts, à aligner et joindre
# Artwork Cleanup add-on (Object › Plug-ins › Artwork Cleanup): its panel, tools, issue kinds and messages.
Artwork Cleanup Nettoyage des illustrations
Simplify Anchors (Keep Shape)… Simplifier les points d’ancrage (conserver la forme)…
Health Check Vérification de l’état
Show Issue Afficher le problème
Fix All Issues Corriger tous les problèmes
Fix Issues Corriger les problèmes
Deep Ungroup… Dissocier tous les niveaux…
Rejoin Path Pieces… Rejoindre les morceaux de tracé…
Document Health Check Vérification de l’état du document
Check Selection Vérifier la sélection
Check Document Vérifier le document
No issues found. Aucun problème trouvé.
Changed since the last check. Modifié depuis la dernière vérification.
Fix Selected Corriger la sélection
Fix All Tout corriger
Issues fixed: {count} Problèmes corrigés : {count}
Open a document to check it for issues. Ouvrez un document pour y rechercher des problèmes.
Empty Text Texte vide
Unpainted Objects Objets sans fond ni contour
Zero-Opacity Objects Objets à opacité nulle
Duplicate Objects Objets en double
Off-Artboard Art Illustration hors des plans de travail
Duplicate Anchors Points d’ancrage en double
Tiny Handles Poignées minuscules
Collinear Anchors Points d’ancrage alignés
Nearly Coincident Anchors Points d’ancrage presque confondus
Nearly Closed Paths Tracés presque fermés
Filled Open Paths Tracés ouverts avec fond
Needless Clipping Groups Groupes de découpe inutiles
Needless Compound Paths Tracés transparents inutiles
Needless Groups Groupes inutiles
Empty Artboards Plans de travail vides
Protect Corners Protéger les angles
Gap Écart
Include Clip Groups Inclure les groupes de découpe
@msg `tolerance` must be from 1 to 100 `tolerance` doit être compris entre 1 et 100
@msg `gap` must be from 0 to 6 pt `gap` doit être compris entre 0 et 6 pt
@msg Simplify Anchors: the paths have too many anchors to simplify at once Simplifier les points d’ancrage : les tracés ont trop de points d’ancrage pour être simplifiés en une fois
@msg Rejoin Path Pieces: too many paths to rejoin at once Rejoindre les morceaux de tracé : trop de tracés à rejoindre en une fois
@msg None of these issues are left: check again Aucun de ces problèmes ne subsiste : relancez la vérification
@msg `kinds` must be a list of issue kinds `kinds` doit être une liste de types de problèmes
@msg missing `kind` `kind` manquant
Simplify Anchors (Keep Shape) Simplifier les points d’ancrage (conserver la forme)
Deep Ungroup Dissocier tous les niveaux
Rejoin Path Pieces Rejoindre les morceaux de tracé
Can't render this file because it contains an unexpected character in line 2 and column 73.
+47
View File
@@ -5686,3 +5686,50 @@ axis Y Axis Y tengely
Average & Join Átlagolás és összekapcsolás
anchor Points: Pontok:
@msg select two open path ends, or two open paths, to average and join jelölj ki két nyitott útvonalvéget vagy két nyitott útvonalat az átlagoláshoz és összekapcsoláshoz
# Artwork Cleanup add-on (Object › Plug-ins › Artwork Cleanup): its panel, tools, issue kinds and messages.
Artwork Cleanup Grafika tisztítása
Simplify Anchors (Keep Shape)… Horgonypontok egyszerűsítése (alak megtartása)…
Health Check Állapotellenőrzés
Show Issue Probléma megjelenítése
Fix All Issues Minden probléma javítása
Fix Issues Problémák javítása
Deep Ungroup… Csoportbontás minden szinten…
Rejoin Path Pieces… Útvonaldarabok újraegyesítése…
Document Health Check Dokumentum állapotellenőrzése
Check Selection Kijelölés ellenőrzése
Check Document Dokumentum ellenőrzése
No issues found. Nem található probléma.
Changed since the last check. Módosult a legutóbbi ellenőrzés óta.
Fix Selected Kijelöltek javítása
Fix All Összes javítása
Issues fixed: {count} Javított problémák: {count}
Open a document to check it for issues. Nyisson meg egy dokumentumot a problémák ellenőrzéséhez.
Empty Text Üres szöveg
Unpainted Objects Festetlen objektumok
Zero-Opacity Objects Nulla átlátszatlanságú objektumok
Duplicate Objects Ismétlődő objektumok
Off-Artboard Art Rajztáblákon kívüli grafika
Duplicate Anchors Ismétlődő horgonypontok
Tiny Handles Apró fogantyúk
Collinear Anchors Egy egyenesre eső horgonypontok
Nearly Coincident Anchors Majdnem egybeeső horgonypontok
Nearly Closed Paths Majdnem zárt útvonalak
Filled Open Paths Kitöltött nyitott útvonalak
Needless Clipping Groups Szükségtelen vágócsoportok
Needless Compound Paths Szükségtelen összetett útvonalak
Needless Groups Szükségtelen csoportok
Empty Artboards Üres rajztáblák
Protect Corners Sarkok védelme
Gap Hézag
Include Clip Groups Vágócsoportokkal együtt
@msg `tolerance` must be from 1 to 100 A `tolerance` értéke 1 és 100 között lehet
@msg `gap` must be from 0 to 6 pt A `gap` értéke 0 és 6 pt között lehet
@msg Simplify Anchors: the paths have too many anchors to simplify at once Horgonypontok egyszerűsítése: az útvonalaknak túl sok horgonypontja van ahhoz, hogy egyszerre egyszerűsíthetők legyenek
@msg Rejoin Path Pieces: too many paths to rejoin at once Útvonaldarabok újraegyesítése: túl sok útvonal az egyszerre történő egyesítéshez
@msg None of these issues are left: check again Ezek közül a problémák közül már egy sem maradt: ellenőrizze újra
@msg `kinds` must be a list of issue kinds A `kinds` értékének problémafajták listájának kell lennie
@msg missing `kind` hiányzik a `kind`
Simplify Anchors (Keep Shape) Horgonypontok egyszerűsítése (alak megtartása)
Deep Ungroup Csoportbontás minden szinten
Rejoin Path Pieces Útvonaldarabok újraegyesítése
Can't render this file because it contains an unexpected character in line 2 and column 73.
+47
View File
@@ -5605,3 +5605,50 @@ axis Y Axis Sumbu Y
Average & Join Rata-rata dan sambungkan
anchor Points: Titik:
@msg select two open path ends, or two open paths, to average and join pilih dua ujung jalur terbuka, atau dua jalur terbuka, untuk dirata-ratakan dan disambungkan
# Artwork Cleanup add-on (Object › Plug-ins › Artwork Cleanup): its panel, tools, issue kinds and messages.
Artwork Cleanup Pembersihan karya
Simplify Anchors (Keep Shape)… Sederhanakan titik jangkar (pertahankan bentuk)…
Health Check Pemeriksaan kesehatan
Show Issue Tampilkan masalah
Fix All Issues Perbaiki semua masalah
Fix Issues Perbaiki masalah
Deep Ungroup… Buka grup di semua tingkat…
Rejoin Path Pieces… Sambungkan kembali potongan jalur…
Document Health Check Pemeriksaan kesehatan dokumen
Check Selection Periksa seleksi
Check Document Periksa dokumen
No issues found. Tidak ada masalah yang ditemukan.
Changed since the last check. Berubah sejak pemeriksaan terakhir.
Fix Selected Perbaiki yang dipilih
Fix All Perbaiki semua
Issues fixed: {count} Masalah diperbaiki: {count}
Open a document to check it for issues. Buka dokumen untuk memeriksa masalahnya.
Empty Text Teks kosong
Unpainted Objects Objek tanpa warna
Zero-Opacity Objects Objek dengan opasitas nol
Duplicate Objects Objek duplikat
Off-Artboard Art Karya di luar artboard
Duplicate Anchors Titik jangkar duplikat
Tiny Handles Pegangan sangat kecil
Collinear Anchors Titik jangkar segaris
Nearly Coincident Anchors Titik jangkar yang hampir berimpit
Nearly Closed Paths Jalur yang hampir tertutup
Filled Open Paths Jalur terbuka berisi
Needless Clipping Groups Grup kliping yang tidak perlu
Needless Compound Paths Jalur majemuk yang tidak perlu
Needless Groups Grup yang tidak perlu
Empty Artboards Artboard kosong
Protect Corners Lindungi sudut
Gap Celah
Include Clip Groups Sertakan grup kliping
@msg `tolerance` must be from 1 to 100 `tolerance` harus antara 1 dan 100
@msg `gap` must be from 0 to 6 pt `gap` harus antara 0 dan 6 pt
@msg Simplify Anchors: the paths have too many anchors to simplify at once Sederhanakan titik jangkar: jalur memiliki terlalu banyak titik jangkar untuk disederhanakan sekaligus
@msg Rejoin Path Pieces: too many paths to rejoin at once Sambungkan kembali potongan jalur: terlalu banyak jalur untuk disambungkan sekaligus
@msg None of these issues are left: check again Tidak ada lagi masalah ini yang tersisa: periksa lagi
@msg `kinds` must be a list of issue kinds `kinds` harus berupa daftar jenis masalah
@msg missing `kind` `kind` tidak ada
Simplify Anchors (Keep Shape) Sederhanakan titik jangkar (pertahankan bentuk)
Deep Ungroup Buka grup di semua tingkat
Rejoin Path Pieces Sambungkan kembali potongan jalur
Can't render this file because it contains an unexpected character in line 2169 and column 5.
+47
View File
@@ -5316,3 +5316,50 @@ axis Y Axis Asse Y
Average & Join Media e unisci
anchor Points: Punti:
@msg select two open path ends, or two open paths, to average and join seleziona due estremità di tracciati aperti, o due tracciati aperti, da mediare e unire
# Artwork Cleanup add-on (Object › Plug-ins › Artwork Cleanup): its panel, tools, issue kinds and messages.
Artwork Cleanup Pulizia grafica
Simplify Anchors (Keep Shape)… Semplifica punti di ancoraggio (mantieni forma)…
Health Check Controllo integrità
Show Issue Mostra problema
Fix All Issues Correggi tutti i problemi
Fix Issues Correggi problemi
Deep Ungroup… Separa tutti i livelli…
Rejoin Path Pieces… Ricongiungi parti di tracciato…
Document Health Check Controllo integrità documento
Check Selection Controlla selezione
Check Document Controlla documento
No issues found. Nessun problema trovato.
Changed since the last check. Modificato dall’ultimo controllo.
Fix Selected Correggi selezionati
Fix All Correggi tutto
Issues fixed: {count} Problemi corretti: {count}
Open a document to check it for issues. Apri un documento per controllarne i problemi.
Empty Text Testo vuoto
Unpainted Objects Oggetti senza colore
Zero-Opacity Objects Oggetti con opacità zero
Duplicate Objects Oggetti duplicati
Off-Artboard Art Grafica fuori dalle tavole da disegno
Duplicate Anchors Punti di ancoraggio duplicati
Tiny Handles Maniglie minuscole
Collinear Anchors Punti di ancoraggio allineati
Nearly Coincident Anchors Punti di ancoraggio quasi coincidenti
Nearly Closed Paths Tracciati quasi chiusi
Filled Open Paths Tracciati aperti con riempimento
Needless Clipping Groups Gruppi di ritaglio superflui
Needless Compound Paths Tracciati composti superflui
Needless Groups Gruppi superflui
Empty Artboards Tavole da disegno vuote
Protect Corners Proteggi angoli
Gap Distanza
Include Clip Groups Includi gruppi di ritaglio
@msg `tolerance` must be from 1 to 100 `tolerance` deve essere compreso tra 1 e 100
@msg `gap` must be from 0 to 6 pt `gap` deve essere compreso tra 0 e 6 pt
@msg Simplify Anchors: the paths have too many anchors to simplify at once Semplifica punti di ancoraggio: i tracciati hanno troppi punti di ancoraggio da semplificare in una volta
@msg Rejoin Path Pieces: too many paths to rejoin at once Ricongiungi parti di tracciato: troppi tracciati da ricongiungere in una volta
@msg None of these issues are left: check again Nessuno di questi problemi è ancora presente: controlla di nuovo
@msg `kinds` must be a list of issue kinds `kinds` deve essere un elenco di tipi di problema
@msg missing `kind` `kind` mancante
Simplify Anchors (Keep Shape) Semplifica punti di ancoraggio (mantieni forma)
Deep Ungroup Separa tutti i livelli
Rejoin Path Pieces Ricongiungi parti di tracciato
Can't render this file because it contains an unexpected character in line 2 and column 73.
+47
View File
@@ -5385,3 +5385,50 @@ axis Y Axis Y 軸
Average & Join 平均と連結
anchor Points: ポイント:
@msg select two open path ends, or two open paths, to average and join 平均と連結には、オープンパスの端点を 2 つ、またはオープンパスを 2 つ選択してください
# Artwork Cleanup add-on (Object › Plug-ins › Artwork Cleanup): its panel, tools, issue kinds and messages.
Artwork Cleanup アートワークのクリーンアップ
Simplify Anchors (Keep Shape)… アンカーポイントを単純化(形状を維持)…
Health Check ヘルスチェック
Show Issue 問題を表示
Fix All Issues すべての問題を修正
Fix Issues 問題を修正
Deep Ungroup… すべての階層のグループ解除…
Rejoin Path Pieces… パスの断片を再連結…
Document Health Check ドキュメントのヘルスチェック
Check Selection 選択範囲をチェック
Check Document ドキュメントをチェック
No issues found. 問題は見つかりませんでした。
Changed since the last check. 前回のチェック以降に変更されています。
Fix Selected 選択した問題を修正
Fix All すべて修正
Issues fixed: {count} 修正した問題:{count}
Open a document to check it for issues. ドキュメントを開くと問題をチェックできます。
Empty Text 空のテキスト
Unpainted Objects 塗りも線もないオブジェクト
Zero-Opacity Objects 不透明度 0% のオブジェクト
Duplicate Objects 重複したオブジェクト
Off-Artboard Art アートボード外のアートワーク
Duplicate Anchors 重複したアンカーポイント
Tiny Handles ごく短いハンドル
Collinear Anchors 同一直線上のアンカーポイント
Nearly Coincident Anchors ほぼ重なるアンカーポイント
Nearly Closed Paths ほぼ閉じたパス
Filled Open Paths 塗りのあるオープンパス
Needless Clipping Groups 不要なクリップグループ
Needless Compound Paths 不要な複合パス
Needless Groups 不要なグループ
Empty Artboards 空のアートボード
Protect Corners 角を保護
Gap 間隔
Include Clip Groups クリップグループを含める
@msg `tolerance` must be from 1 to 100 `tolerance` は 1~100 の範囲で指定してください
@msg `gap` must be from 0 to 6 pt `gap` は 0~6 pt の範囲で指定してください
@msg Simplify Anchors: the paths have too many anchors to simplify at once アンカーポイントを単純化:パスのアンカーポイントが多すぎて一度に単純化できません
@msg Rejoin Path Pieces: too many paths to rejoin at once パスの断片を再連結:パスが多すぎて一度に再連結できません
@msg None of these issues are left: check again これらの問題はもう残っていません:もう一度チェックしてください
@msg `kinds` must be a list of issue kinds `kinds` は問題の種類のリストで指定してください
@msg missing `kind` `kind` がありません
Simplify Anchors (Keep Shape) アンカーポイントを単純化(形状を維持)
Deep Ungroup すべての階層のグループ解除
Rejoin Path Pieces パスの断片を再連結
Can't render this file because it contains an unexpected character in line 2 and column 73.
+47
View File
@@ -5689,3 +5689,50 @@ swatch Bright Violet კაშკაშა იისფერი
Average & Join საშუალო და შეერთება
anchor Points: წერტილები:
@msg select two open path ends, or two open paths, to average and join მონიშნეთ ღია ტრაექტორიების ორი ბოლო ან ორი ღია ტრაექტორია საშუალოსა და შეერთებისთვის
# Artwork Cleanup add-on (Object › Plug-ins › Artwork Cleanup): its panel, tools, issue kinds and messages.
Artwork Cleanup გრაფიკის გასუფთავება
Simplify Anchors (Keep Shape)… საყრდენი წერტილების გამარტივება (ფორმის შენარჩუნებით)…
Health Check მდგომარეობის შემოწმება
Show Issue პრობლემის ჩვენება
Fix All Issues ყველა პრობლემის გამოსწორება
Fix Issues პრობლემების გამოსწორება
Deep Ungroup… დაჯგუფების მოხსნა ყველა დონეზე…
Rejoin Path Pieces… ტრაექტორიის ნაწილების ხელახლა შეერთება…
Document Health Check დოკუმენტის მდგომარეობის შემოწმება
Check Selection მონიშნულის შემოწმება
Check Document დოკუმენტის შემოწმება
No issues found. პრობლემები ვერ მოიძებნა.
Changed since the last check. შეიცვალა ბოლო შემოწმების შემდეგ.
Fix Selected მონიშნულის გამოსწორება
Fix All ყველას გამოსწორება
Issues fixed: {count} გამოსწორებული პრობლემები: {count}
Open a document to check it for issues. გახსენით დოკუმენტი, რომ პრობლემებზე შეამოწმოთ.
Empty Text ცარიელი ტექსტი
Unpainted Objects შეუღებავი ობიექტები
Zero-Opacity Objects ნულოვანი გაუმჭვირვალობის ობიექტები
Duplicate Objects დუბლირებული ობიექტები
Off-Artboard Art სამუშაო დაფებს გარეთ მყოფი გრაფიკა
Duplicate Anchors დუბლირებული საყრდენი წერტილები
Tiny Handles პაწაწინა სახელურები
Collinear Anchors კოლინეარული საყრდენი წერტილები
Nearly Coincident Anchors თითქმის დამთხვეული საყრდენი წერტილები
Nearly Closed Paths თითქმის შეკრული ტრაექტორიები
Filled Open Paths შევსებული ღია ტრაექტორიები
Needless Clipping Groups ზედმეტი მომჭრელი ჯგუფები
Needless Compound Paths ზედმეტი შედგენილი ტრაექტორიები
Needless Groups ზედმეტი ჯგუფები
Empty Artboards ცარიელი სამუშაო დაფები
Protect Corners კუთხეების დაცვა
Gap ღრეჩო
Include Clip Groups მომჭრელი ჯგუფების ჩართვა
@msg `tolerance` must be from 1 to 100 `tolerance` უნდა იყოს 1-დან 100-მდე
@msg `gap` must be from 0 to 6 pt `gap` უნდა იყოს 0-დან 6 pt-მდე
@msg Simplify Anchors: the paths have too many anchors to simplify at once საყრდენი წერტილების გამარტივება: ტრაექტორიებს ძალიან ბევრი საყრდენი წერტილი აქვს ერთბაშად გასამარტივებლად
@msg Rejoin Path Pieces: too many paths to rejoin at once ტრაექტორიის ნაწილების ხელახლა შეერთება: ძალიან ბევრი ტრაექტორიაა ერთბაშად შესაერთებლად
@msg None of these issues are left: check again ამ პრობლემებიდან აღარცერთი დარჩა: შეამოწმეთ ხელახლა
@msg `kinds` must be a list of issue kinds `kinds` უნდა იყოს პრობლემის სახეების სია
@msg missing `kind` `kind` აკლია
Simplify Anchors (Keep Shape) საყრდენი წერტილების გამარტივება (ფორმის შენარჩუნებით)
Deep Ungroup დაჯგუფების მოხსნა ყველა დონეზე
Rejoin Path Pieces ტრაექტორიის ნაწილების ხელახლა შეერთება
Can't render this file because it is too large.
+47
View File
@@ -5279,3 +5279,50 @@ axis Y Axis Y축
Average & Join 평균 및 결합
anchor Points: 포인트:
@msg select two open path ends, or two open paths, to average and join 평균 및 결합할 열린 패스의 끝점 두 개 또는 열린 패스 두 개를 선택하세요
# Artwork Cleanup add-on (Object › Plug-ins › Artwork Cleanup): its panel, tools, issue kinds and messages.
Artwork Cleanup 아트워크 정리
Simplify Anchors (Keep Shape)… 앵커 포인트 단순화(모양 유지)…
Health Check 상태 점검
Show Issue 문제 표시
Fix All Issues 모든 문제 수정
Fix Issues 문제 수정
Deep Ungroup… 모든 단계 그룹 해제…
Rejoin Path Pieces… 패스 조각 다시 연결…
Document Health Check 문서 상태 점검
Check Selection 선택 영역 점검
Check Document 문서 점검
No issues found. 문제가 없습니다.
Changed since the last check. 마지막 점검 이후 변경되었습니다.
Fix Selected 선택한 항목 수정
Fix All 모두 수정
Issues fixed: {count} 수정한 문제: {count}
Open a document to check it for issues. 문서를 열면 문제를 점검할 수 있습니다.
Empty Text 빈 텍스트
Unpainted Objects 칠하지 않은 오브젝트
Zero-Opacity Objects 불투명도 0% 오브젝트
Duplicate Objects 중복 오브젝트
Off-Artboard Art 아트보드 밖의 아트워크
Duplicate Anchors 중복 앵커 포인트
Tiny Handles 아주 짧은 핸들
Collinear Anchors 일직선 위의 앵커 포인트
Nearly Coincident Anchors 거의 겹치는 앵커 포인트
Nearly Closed Paths 거의 닫힌 패스
Filled Open Paths 칠한 열린 패스
Needless Clipping Groups 불필요한 클리핑 그룹
Needless Compound Paths 불필요한 컴파운드 패스
Needless Groups 불필요한 그룹
Empty Artboards 빈 아트보드
Protect Corners 모서리 보호
Gap 간격
Include Clip Groups 클리핑 그룹 포함
@msg `tolerance` must be from 1 to 100 `tolerance`는 1에서 100 사이여야 합니다
@msg `gap` must be from 0 to 6 pt `gap`은 0에서 6pt 사이여야 합니다
@msg Simplify Anchors: the paths have too many anchors to simplify at once 앵커 포인트 단순화: 패스의 앵커 포인트가 너무 많아 한 번에 단순화할 수 없습니다
@msg Rejoin Path Pieces: too many paths to rejoin at once 패스 조각 다시 연결: 패스가 너무 많아 한 번에 연결할 수 없습니다
@msg None of these issues are left: check again 이 문제가 더 이상 남아 있지 않습니다. 다시 점검하세요
@msg `kinds` must be a list of issue kinds `kinds`는 문제 종류의 목록이어야 합니다
@msg missing `kind` `kind`가 없습니다
Simplify Anchors (Keep Shape) 앵커 포인트 단순화(모양 유지)
Deep Ungroup 모든 단계 그룹 해제
Rejoin Path Pieces 패스 조각 다시 연결
Can't render this file because it contains an unexpected character in line 2 and column 73.
+47
View File
@@ -5649,3 +5649,50 @@ axis Y Axis Y-as
Average & Join Gemiddelde en verbinden
anchor Points: Punten:
@msg select two open path ends, or two open paths, to average and join selecteer twee uiteinden van open paden, of twee open paden, om te middelen en te verbinden
# Artwork Cleanup add-on (Object › Plug-ins › Artwork Cleanup): its panel, tools, issue kinds and messages.
Artwork Cleanup Illustratie opschonen
Simplify Anchors (Keep Shape)… Ankerpunten vereenvoudigen (vorm behouden)…
Health Check Gezondheidscontrole
Show Issue Probleem tonen
Fix All Issues Alle problemen oplossen
Fix Issues Problemen oplossen
Deep Ungroup… Alle groeperingsniveaus opheffen…
Rejoin Path Pieces… Padstukken opnieuw verbinden…
Document Health Check Gezondheidscontrole van document
Check Selection Selectie controleren
Check Document Document controleren
No issues found. Geen problemen gevonden.
Changed since the last check. Gewijzigd sinds de laatste controle.
Fix Selected Geselecteerde oplossen
Fix All Alles oplossen
Issues fixed: {count} Opgeloste problemen: {count}
Open a document to check it for issues. Open een document om het op problemen te controleren.
Empty Text Lege tekst
Unpainted Objects Objecten zonder verf
Zero-Opacity Objects Objecten met 0% dekking
Duplicate Objects Dubbele objecten
Off-Artboard Art Illustraties buiten de tekenborden
Duplicate Anchors Dubbele ankerpunten
Tiny Handles Piepkleine grepen
Collinear Anchors Collineaire ankerpunten
Nearly Coincident Anchors Bijna samenvallende ankerpunten
Nearly Closed Paths Bijna gesloten paden
Filled Open Paths Gevulde open paden
Needless Clipping Groups Overbodige knipgroepen
Needless Compound Paths Overbodige samengestelde paden
Needless Groups Overbodige groepen
Empty Artboards Lege tekenborden
Protect Corners Hoeken beschermen
Gap Tussenruimte
Include Clip Groups Knipgroepen meenemen
@msg `tolerance` must be from 1 to 100 `tolerance` moet tussen 1 en 100 liggen
@msg `gap` must be from 0 to 6 pt `gap` moet tussen 0 en 6 pt liggen
@msg Simplify Anchors: the paths have too many anchors to simplify at once Ankerpunten vereenvoudigen: de paden hebben te veel ankerpunten om in één keer te vereenvoudigen
@msg Rejoin Path Pieces: too many paths to rejoin at once Padstukken opnieuw verbinden: te veel paden om in één keer te verbinden
@msg None of these issues are left: check again Geen van deze problemen is nog over: controleer opnieuw
@msg `kinds` must be a list of issue kinds `kinds` moet een lijst met soorten problemen zijn
@msg missing `kind` `kind` ontbreekt
Simplify Anchors (Keep Shape) Ankerpunten vereenvoudigen (vorm behouden)
Deep Ungroup Alle groeperingsniveaus opheffen
Rejoin Path Pieces Padstukken opnieuw verbinden
Can't render this file because it contains an unexpected character in line 2 and column 73.
+47
View File
@@ -5554,3 +5554,50 @@ axis Y Axis Oś Y
Average & Join Uśrednij i połącz
anchor Points: Punkty:
@msg select two open path ends, or two open paths, to average and join zaznacz dwa końce otwartych ścieżek lub dwie otwarte ścieżki do uśrednienia i połączenia
# Artwork Cleanup add-on (Object › Plug-ins › Artwork Cleanup): its panel, tools, issue kinds and messages.
Artwork Cleanup Porządkowanie grafiki
Simplify Anchors (Keep Shape)… Uprość punkty kontrolne (zachowaj kształt)…
Health Check Kontrola stanu
Show Issue Pokaż problem
Fix All Issues Napraw wszystkie problemy
Fix Issues Napraw problemy
Deep Ungroup… Rozgrupuj wszystkie poziomy…
Rejoin Path Pieces… Ponownie połącz fragmenty ścieżek…
Document Health Check Kontrola stanu dokumentu
Check Selection Sprawdź zaznaczenie
Check Document Sprawdź dokument
No issues found. Nie znaleziono problemów.
Changed since the last check. Zmieniono od ostatniego sprawdzenia.
Fix Selected Napraw zaznaczone
Fix All Napraw wszystko
Issues fixed: {count} Naprawione problemy: {count}
Open a document to check it for issues. Otwórz dokument, aby sprawdzić go pod kątem problemów.
Empty Text Pusty tekst
Unpainted Objects Obiekty bez koloru
Zero-Opacity Objects Obiekty o zerowym kryciu
Duplicate Objects Zduplikowane obiekty
Off-Artboard Art Grafika poza obszarami kompozycji
Duplicate Anchors Zduplikowane punkty kontrolne
Tiny Handles Bardzo krótkie uchwyty
Collinear Anchors Współliniowe punkty kontrolne
Nearly Coincident Anchors Prawie pokrywające się punkty kontrolne
Nearly Closed Paths Prawie zamknięte ścieżki
Filled Open Paths Wypełnione otwarte ścieżki
Needless Clipping Groups Zbędne grupy przycinające
Needless Compound Paths Zbędne ścieżki złożone
Needless Groups Zbędne grupy
Empty Artboards Puste obszary kompozycji
Protect Corners Chroń narożniki
Gap Odstęp
Include Clip Groups Uwzględnij grupy przycinające
@msg `tolerance` must be from 1 to 100 `tolerance` musi mieścić się w zakresie od 1 do 100
@msg `gap` must be from 0 to 6 pt `gap` musi mieścić się w zakresie od 0 do 6 pt
@msg Simplify Anchors: the paths have too many anchors to simplify at once Uprość punkty kontrolne: ścieżki mają zbyt wiele punktów kontrolnych, aby uprościć je naraz
@msg Rejoin Path Pieces: too many paths to rejoin at once Ponownie połącz fragmenty ścieżek: zbyt wiele ścieżek do połączenia naraz
@msg None of these issues are left: check again Żaden z tych problemów już nie występuje: sprawdź ponownie
@msg `kinds` must be a list of issue kinds `kinds` musi być listą rodzajów problemów
@msg missing `kind` brak `kind`
Simplify Anchors (Keep Shape) Uprość punkty kontrolne (zachowaj kształt)
Deep Ungroup Rozgrupuj wszystkie poziomy
Rejoin Path Pieces Ponownie połącz fragmenty ścieżek
Can't render this file because it contains an unexpected character in line 2 and column 73.
+47
View File
@@ -5617,3 +5617,50 @@ axis Y Axis Eixo Y
Average & Join Média e união
anchor Points: Pontos:
@msg select two open path ends, or two open paths, to average and join selecione duas extremidades de traçados abertos, ou dois traçados abertos, para calcular a média e unir
# Artwork Cleanup add-on (Object › Plug-ins › Artwork Cleanup): its panel, tools, issue kinds and messages.
Artwork Cleanup Limpeza de Arte
Simplify Anchors (Keep Shape)… Simplificar Pontos de Ancoragem (Manter Forma)…
Health Check Verificação de Integridade
Show Issue Mostrar Problema
Fix All Issues Corrigir Todos os Problemas
Fix Issues Corrigir Problemas
Deep Ungroup… Desagrupar Todos os Níveis…
Rejoin Path Pieces… Reunir Partes de Caminho…
Document Health Check Verificação de Integridade do Documento
Check Selection Verificar Seleção
Check Document Verificar Documento
No issues found. Nenhum problema encontrado.
Changed since the last check. Alterado desde a última verificação.
Fix Selected Corrigir Selecionados
Fix All Corrigir Tudo
Issues fixed: {count} Problemas corrigidos: {count}
Open a document to check it for issues. Abra um documento para verificar se há problemas.
Empty Text Texto Vazio
Unpainted Objects Objetos sem Pintura
Zero-Opacity Objects Objetos com Opacidade Zero
Duplicate Objects Objetos Duplicados
Off-Artboard Art Arte Fora das Pranchetas
Duplicate Anchors Pontos de Ancoragem Duplicados
Tiny Handles Alças Minúsculas
Collinear Anchors Pontos de Ancoragem Colineares
Nearly Coincident Anchors Pontos de Ancoragem Quase Coincidentes
Nearly Closed Paths Caminhos Quase Fechados
Filled Open Paths Caminhos Abertos Preenchidos
Needless Clipping Groups Grupos de Recorte Desnecessários
Needless Compound Paths Caminhos Compostos Desnecessários
Needless Groups Grupos Desnecessários
Empty Artboards Pranchetas Vazias
Protect Corners Proteger Cantos
Gap Distância
Include Clip Groups Incluir Grupos de Recorte
@msg `tolerance` must be from 1 to 100 `tolerance` deve estar entre 1 e 100
@msg `gap` must be from 0 to 6 pt `gap` deve estar entre 0 e 6 pt
@msg Simplify Anchors: the paths have too many anchors to simplify at once Simplificar Pontos de Ancoragem: os caminhos têm pontos de ancoragem demais para simplificar de uma vez
@msg Rejoin Path Pieces: too many paths to rejoin at once Reunir Partes de Caminho: caminhos demais para reunir de uma vez
@msg None of these issues are left: check again Nenhum desses problemas permanece: verifique novamente
@msg `kinds` must be a list of issue kinds `kinds` deve ser uma lista de tipos de problema
@msg missing `kind` `kind` ausente
Simplify Anchors (Keep Shape) Simplificar Pontos de Ancoragem (Manter Forma)
Deep Ungroup Desagrupar Todos os Níveis
Rejoin Path Pieces Reunir Partes de Caminho
Can't render this file because it contains an unexpected character in line 6 and column 47.
+47
View File
@@ -5678,3 +5678,50 @@ theme Appearance Оформление
Average & Join Усреднить и соединить
anchor Points: Точки:
@msg select two open path ends, or two open paths, to average and join выделите два конца открытых контуров или два открытых контура для усреднения и соединения
# Artwork Cleanup add-on (Object › Plug-ins › Artwork Cleanup): its panel, tools, issue kinds and messages.
Artwork Cleanup Очистка графики
Simplify Anchors (Keep Shape)… Упростить опорные точки (сохранить форму)…
Health Check Проверка состояния
Show Issue Показать проблему
Fix All Issues Исправить все проблемы
Fix Issues Исправить проблемы
Deep Ungroup… Разгруппировать все уровни…
Rejoin Path Pieces… Соединить части контуров…
Document Health Check Проверка состояния документа
Check Selection Проверить выделение
Check Document Проверить документ
No issues found. Проблем не найдено.
Changed since the last check. Изменено после последней проверки.
Fix Selected Исправить выбранные
Fix All Исправить все
Issues fixed: {count} Исправлено проблем: {count}
Open a document to check it for issues. Откройте документ, чтобы проверить его на проблемы.
Empty Text Пустой текст
Unpainted Objects Объекты без заливки и обводки
Zero-Opacity Objects Объекты с нулевой непрозрачностью
Duplicate Objects Дублирующиеся объекты
Off-Artboard Art Графика за пределами монтажных областей
Duplicate Anchors Дублирующиеся опорные точки
Tiny Handles Крошечные маркеры
Collinear Anchors Коллинеарные опорные точки
Nearly Coincident Anchors Почти совпадающие опорные точки
Nearly Closed Paths Почти замкнутые контуры
Filled Open Paths Открытые контуры с заливкой
Needless Clipping Groups Лишние группы обрезки
Needless Compound Paths Лишние составные контуры
Needless Groups Лишние группы
Empty Artboards Пустые монтажные области
Protect Corners Защищать углы
Gap Зазор
Include Clip Groups Включить группы обрезки
@msg `tolerance` must be from 1 to 100 `tolerance` должно быть от 1 до 100
@msg `gap` must be from 0 to 6 pt `gap` должно быть от 0 до 6 pt
@msg Simplify Anchors: the paths have too many anchors to simplify at once Упростить опорные точки: у контуров слишком много опорных точек, чтобы упростить их за один раз
@msg Rejoin Path Pieces: too many paths to rejoin at once Соединить части контуров: слишком много контуров, чтобы соединить их за один раз
@msg None of these issues are left: check again Ни одной из этих проблем больше нет: проверьте снова
@msg `kinds` must be a list of issue kinds `kinds` должно быть списком видов проблем
@msg missing `kind` не указан `kind`
Simplify Anchors (Keep Shape) Упростить опорные точки (сохранить форму)
Deep Ungroup Разгруппировать все уровни
Rejoin Path Pieces Соединить части контуров
Can't render this file because it contains an unexpected character in line 2 and column 73.
+47
View File
@@ -5724,3 +5724,50 @@ axis Y Axis Y-axel
Average & Join Medelvärde och foga samman
anchor Points: Punkter:
@msg select two open path ends, or two open paths, to average and join markera två ändar av öppna banor, eller två öppna banor, att medelvärdesberäkna och foga samman
# Artwork Cleanup add-on (Object › Plug-ins › Artwork Cleanup): its panel, tools, issue kinds and messages.
Artwork Cleanup Upprensning av grafik
Simplify Anchors (Keep Shape)… Förenkla ankarpunkter (behåll formen)…
Health Check Hälsokontroll
Show Issue Visa problem
Fix All Issues Åtgärda alla problem
Fix Issues Åtgärda problem
Deep Ungroup… Dela upp grupper på alla nivåer…
Rejoin Path Pieces… Foga samman banbitar igen…
Document Health Check Hälsokontroll av dokument
Check Selection Kontrollera markering
Check Document Kontrollera dokument
No issues found. Inga problem hittades.
Changed since the last check. Ändrat sedan den senaste kontrollen.
Fix Selected Åtgärda markerade
Fix All Åtgärda alla
Issues fixed: {count} Åtgärdade problem: {count}
Open a document to check it for issues. Öppna ett dokument för att kontrollera det efter problem.
Empty Text Tom text
Unpainted Objects Ofärgade objekt
Zero-Opacity Objects Objekt med 0 % opacitet
Duplicate Objects Dubblettobjekt
Off-Artboard Art Grafik utanför ritytorna
Duplicate Anchors Dubbletter av ankarpunkter
Tiny Handles Små handtag
Collinear Anchors Kollineära ankarpunkter
Nearly Coincident Anchors Nästan sammanfallande ankarpunkter
Nearly Closed Paths Nästan slutna banor
Filled Open Paths Fyllda öppna banor
Needless Clipping Groups Onödiga klippgrupper
Needless Compound Paths Onödiga sammansatta banor
Needless Groups Onödiga grupper
Empty Artboards Tomma ritytor
Protect Corners Skydda hörn
Gap Mellanrum
Include Clip Groups Ta med klippgrupper
@msg `tolerance` must be from 1 to 100 `tolerance` måste vara från 1 till 100
@msg `gap` must be from 0 to 6 pt `gap` måste vara från 0 till 6 pt
@msg Simplify Anchors: the paths have too many anchors to simplify at once Förenkla ankarpunkter: banorna har för många ankarpunkter för att förenklas på en gång
@msg Rejoin Path Pieces: too many paths to rejoin at once Foga samman banbitar igen: för många banor för att foga samman på en gång
@msg None of these issues are left: check again Inget av dessa problem finns kvar: kontrollera igen
@msg `kinds` must be a list of issue kinds `kinds` måste vara en lista med problemtyper
@msg missing `kind` `kind` saknas
Simplify Anchors (Keep Shape) Förenkla ankarpunkter (behåll formen)
Deep Ungroup Dela upp grupper på alla nivåer
Rejoin Path Pieces Foga samman banbitar igen
Can't render this file because it contains an unexpected character in line 5 and column 25.
+47
View File
@@ -5658,3 +5658,50 @@ axis Y Axis Y ekseni
Average & Join Ortalama ve katıl
anchor Points: Noktalar:
@msg select two open path ends, or two open paths, to average and join ortalaması alınıp katılmak için iki açık yol ucu veya iki açık yol seçin
# Artwork Cleanup add-on (Object › Plug-ins › Artwork Cleanup): its panel, tools, issue kinds and messages.
Artwork Cleanup Çizim Temizliği
Simplify Anchors (Keep Shape)… Bağlantı Noktalarını Basitleştir (Şekli Koru)…
Health Check Sağlık Denetimi
Show Issue Sorunu Göster
Fix All Issues Tüm Sorunları Düzelt
Fix Issues Sorunları Düzelt
Deep Ungroup… Tüm Düzeylerde Grubu Çöz…
Rejoin Path Pieces… Yol Parçalarını Yeniden Birleştir…
Document Health Check Belge Sağlık Denetimi
Check Selection Seçimi Denetle
Check Document Belgeyi Denetle
No issues found. Sorun bulunamadı.
Changed since the last check. Son denetimden bu yana değişti.
Fix Selected Seçilenleri Düzelt
Fix All Tümünü Düzelt
Issues fixed: {count} Düzeltilen sorunlar: {count}
Open a document to check it for issues. Sorunlar için denetlemek üzere bir belge açın.
Empty Text Boş Metin
Unpainted Objects Boyanmamış Nesneler
Zero-Opacity Objects Sıfır Opaklıklı Nesneler
Duplicate Objects Yinelenen Nesneler
Off-Artboard Art Çalışma Yüzeyi Dışındaki Çizimler
Duplicate Anchors Yinelenen Bağlantı Noktaları
Tiny Handles Çok Kısa Kollar
Collinear Anchors Doğrusal Bağlantı Noktaları
Nearly Coincident Anchors Neredeyse Çakışan Bağlantı Noktaları
Nearly Closed Paths Neredeyse Kapalı Yollar
Filled Open Paths Dolgulu Açık Yollar
Needless Clipping Groups Gereksiz Kırpma Grupları
Needless Compound Paths Gereksiz Bileşik Yollar
Needless Groups Gereksiz Gruplar
Empty Artboards Boş Çalışma Yüzeyleri
Protect Corners Köşeleri Koru
Gap Boşluk
Include Clip Groups Kırpma Gruplarını Dahil Et
@msg `tolerance` must be from 1 to 100 `tolerance` 1 ile 100 arasında olmalıdır
@msg `gap` must be from 0 to 6 pt `gap` 0 ile 6 pt arasında olmalıdır
@msg Simplify Anchors: the paths have too many anchors to simplify at once Bağlantı Noktalarını Basitleştir: yollarda bir kerede basitleştirilemeyecek kadar çok bağlantı noktası var
@msg Rejoin Path Pieces: too many paths to rejoin at once Yol Parçalarını Yeniden Birleştir: bir kerede birleştirilemeyecek kadar çok yol var
@msg None of these issues are left: check again Bu sorunların hiçbiri kalmadı: yeniden denetleyin
@msg `kinds` must be a list of issue kinds `kinds` bir sorun türleri listesi olmalıdır
@msg missing `kind` `kind` eksik
Simplify Anchors (Keep Shape) Bağlantı Noktalarını Basitleştir (Şekli Koru)
Deep Ungroup Tüm Düzeylerde Grubu Çöz
Rejoin Path Pieces Yol Parçalarını Yeniden Birleştir
Can't render this file because it contains an unexpected character in line 5 and column 25.
+47
View File
@@ -5296,3 +5296,50 @@ axis Y Axis Вісь Y
Average & Join Усереднити і з’єднати
anchor Points: Точки:
@msg select two open path ends, or two open paths, to average and join виділіть два кінці відкритих контурів або два відкриті контури для усереднення і з’єднання
# Artwork Cleanup add-on (Object › Plug-ins › Artwork Cleanup): its panel, tools, issue kinds and messages.
Artwork Cleanup Очищення графіки
Simplify Anchors (Keep Shape)… Спростити опорні точки (зберегти форму)…
Health Check Перевірка стану
Show Issue Показати проблему
Fix All Issues Виправити всі проблеми
Fix Issues Виправити проблеми
Deep Ungroup… Розгрупувати всі рівні…
Rejoin Path Pieces… З’єднати частини контурів…
Document Health Check Перевірка стану документа
Check Selection Перевірити виділення
Check Document Перевірити документ
No issues found. Проблем не знайдено.
Changed since the last check. Змінено після останньої перевірки.
Fix Selected Виправити вибрані
Fix All Виправити все
Issues fixed: {count} Виправлено проблем: {count}
Open a document to check it for issues. Відкрийте документ, щоб перевірити його на проблеми.
Empty Text Порожній текст
Unpainted Objects Об’єкти без заливки й обведення
Zero-Opacity Objects Об’єкти з нульовою непрозорістю
Duplicate Objects Дублікати об’єктів
Off-Artboard Art Графіка поза монтажними областями
Duplicate Anchors Дублікати опорних точок
Tiny Handles Крихітні маркери
Collinear Anchors Колінеарні опорні точки
Nearly Coincident Anchors Майже збіжні опорні точки
Nearly Closed Paths Майже замкнені контури
Filled Open Paths Відкриті контури із заливкою
Needless Clipping Groups Зайві групи відсікання
Needless Compound Paths Зайві складені контури
Needless Groups Зайві групи
Empty Artboards Порожні монтажні області
Protect Corners Захищати кути
Gap Проміжок
Include Clip Groups Включити групи відсікання
@msg `tolerance` must be from 1 to 100 `tolerance` має бути від 1 до 100
@msg `gap` must be from 0 to 6 pt `gap` має бути від 0 до 6 pt
@msg Simplify Anchors: the paths have too many anchors to simplify at once Спростити опорні точки: контури мають забагато опорних точок, щоб спростити їх за один раз
@msg Rejoin Path Pieces: too many paths to rejoin at once З’єднати частини контурів: забагато контурів, щоб з’єднати їх за один раз
@msg None of these issues are left: check again Жодної з цих проблем більше немає: перевірте знову
@msg `kinds` must be a list of issue kinds `kinds` має бути списком видів проблем
@msg missing `kind` не вказано `kind`
Simplify Anchors (Keep Shape) Спростити опорні точки (зберегти форму)
Deep Ungroup Розгрупувати всі рівні
Rejoin Path Pieces З’єднати частини контурів
Can't render this file because it contains an unexpected character in line 178 and column 5.
+47
View File
@@ -5298,3 +5298,50 @@ axis Y Axis Trục Y
Average & Join Trung bình và nối
anchor Points: Điểm:
@msg select two open path ends, or two open paths, to average and join hãy chọn hai đầu của đường dẫn mở, hoặc hai đường dẫn mở, để lấy trung bình và nối
# Artwork Cleanup add-on (Object › Plug-ins › Artwork Cleanup): its panel, tools, issue kinds and messages.
Artwork Cleanup Dọn dẹp tác phẩm
Simplify Anchors (Keep Shape)… Đơn giản hóa điểm neo (giữ hình dạng)…
Health Check Kiểm tra tình trạng
Show Issue Hiện vấn đề
Fix All Issues Sửa mọi vấn đề
Fix Issues Sửa vấn đề
Deep Ungroup… Bỏ nhóm mọi cấp…
Rejoin Path Pieces… Nối lại các mảnh đường dẫn…
Document Health Check Kiểm tra tình trạng tài liệu
Check Selection Kiểm tra vùng chọn
Check Document Kiểm tra tài liệu
No issues found. Không tìm thấy vấn đề nào.
Changed since the last check. Đã thay đổi kể từ lần kiểm tra trước.
Fix Selected Sửa mục đã chọn
Fix All Sửa tất cả
Issues fixed: {count} Vấn đề đã sửa: {count}
Open a document to check it for issues. Mở một tài liệu để kiểm tra vấn đề.
Empty Text Văn bản trống
Unpainted Objects Đối tượng không tô màu
Zero-Opacity Objects Đối tượng có độ mờ đục bằng 0
Duplicate Objects Đối tượng trùng lặp
Off-Artboard Art Hình vẽ ngoài bảng vẽ
Duplicate Anchors Điểm neo trùng lặp
Tiny Handles Tay nắm rất ngắn
Collinear Anchors Điểm neo thẳng hàng
Nearly Coincident Anchors Điểm neo gần trùng nhau
Nearly Closed Paths Đường dẫn gần khép kín
Filled Open Paths Đường dẫn mở có tô
Needless Clipping Groups Nhóm cắt không cần thiết
Needless Compound Paths Đường dẫn phức không cần thiết
Needless Groups Nhóm không cần thiết
Empty Artboards Bảng vẽ trống
Protect Corners Bảo vệ góc
Gap Khoảng hở
Include Clip Groups Bao gồm nhóm cắt
@msg `tolerance` must be from 1 to 100 `tolerance` phải từ 1 đến 100
@msg `gap` must be from 0 to 6 pt `gap` phải từ 0 đến 6 pt
@msg Simplify Anchors: the paths have too many anchors to simplify at once Đơn giản hóa điểm neo: các đường dẫn có quá nhiều điểm neo để đơn giản hóa một lần
@msg Rejoin Path Pieces: too many paths to rejoin at once Nối lại các mảnh đường dẫn: quá nhiều đường dẫn để nối một lần
@msg None of these issues are left: check again Không còn vấn đề nào trong số này: hãy kiểm tra lại
@msg `kinds` must be a list of issue kinds `kinds` phải là danh sách các loại vấn đề
@msg missing `kind` thiếu `kind`
Simplify Anchors (Keep Shape) Đơn giản hóa điểm neo (giữ hình dạng)
Deep Ungroup Bỏ nhóm mọi cấp
Rejoin Path Pieces Nối lại các mảnh đường dẫn
Can't render this file because it contains an unexpected character in line 200 and column 5.
+47
View File
@@ -5539,3 +5539,50 @@ axis Y Axis Y 轴
Average & Join 平均并连接
anchor Points: 点:
@msg select two open path ends, or two open paths, to average and join 请选择两个开放路径的端点或两条开放路径以进行平均并连接
# Artwork Cleanup add-on (Object › Plug-ins › Artwork Cleanup): its panel, tools, issue kinds and messages.
Artwork Cleanup 图稿清理
Simplify Anchors (Keep Shape)… 简化锚点(保持形状)…
Health Check 健康检查
Show Issue 显示问题
Fix All Issues 修复所有问题
Fix Issues 修复问题
Deep Ungroup… 取消所有层级编组…
Rejoin Path Pieces… 重新连接路径片段…
Document Health Check 文档健康检查
Check Selection 检查所选对象
Check Document 检查文档
No issues found. 未发现问题。
Changed since the last check. 自上次检查后已更改。
Fix Selected 修复所选
Fix All 全部修复
Issues fixed: {count} 已修复的问题:{count}
Open a document to check it for issues. 打开文档以检查其中的问题。
Empty Text 空文本
Unpainted Objects 无上色对象
Zero-Opacity Objects 不透明度为零的对象
Duplicate Objects 重复对象
Off-Artboard Art 画板外的图稿
Duplicate Anchors 重复锚点
Tiny Handles 极短手柄
Collinear Anchors 共线锚点
Nearly Coincident Anchors 几乎重合的锚点
Nearly Closed Paths 几乎闭合的路径
Filled Open Paths 填色的开放路径
Needless Clipping Groups 多余的剪切组
Needless Compound Paths 多余的复合路径
Needless Groups 多余的编组
Empty Artboards 空白画板
Protect Corners 保护尖角
Gap 间隙
Include Clip Groups 包括剪切组
@msg `tolerance` must be from 1 to 100 `tolerance` 必须介于 1 到 100 之间
@msg `gap` must be from 0 to 6 pt `gap` 必须介于 0 到 6 pt 之间
@msg Simplify Anchors: the paths have too many anchors to simplify at once 简化锚点:路径的锚点太多,无法一次简化
@msg Rejoin Path Pieces: too many paths to rejoin at once 重新连接路径片段:路径太多,无法一次连接
@msg None of these issues are left: check again 这些问题都已不存在:请重新检查
@msg `kinds` must be a list of issue kinds `kinds` 必须是问题类型的列表
@msg missing `kind` 缺少 `kind`
Simplify Anchors (Keep Shape) 简化锚点(保持形状)
Deep Ungroup 取消所有层级编组
Rejoin Path Pieces 重新连接路径片段
Can't render this file because it contains an unexpected character in line 2 and column 73.
+47
View File
@@ -5335,3 +5335,50 @@ axis Y Axis Y 軸
Average & Join 平均並連接
anchor Points: 點:
@msg select two open path ends, or two open paths, to average and join 請選取兩個開放路徑的端點或兩條開放路徑以進行平均並連接
# Artwork Cleanup add-on (Object › Plug-ins › Artwork Cleanup): its panel, tools, issue kinds and messages.
Artwork Cleanup 圖稿清理
Simplify Anchors (Keep Shape)… 簡化錨點(保持形狀)…
Health Check 健康檢查
Show Issue 顯示問題
Fix All Issues 修正所有問題
Fix Issues 修正問題
Deep Ungroup… 解散所有層級群組…
Rejoin Path Pieces… 重新連接路徑片段…
Document Health Check 文件健康檢查
Check Selection 檢查選取範圍
Check Document 檢查文件
No issues found. 未發現問題。
Changed since the last check. 自上次檢查後已變更。
Fix Selected 修正選取項目
Fix All 全部修正
Issues fixed: {count} 已修正的問題:{count}
Open a document to check it for issues. 開啟文件以檢查其中的問題。
Empty Text 空白文字
Unpainted Objects 未上色物件
Zero-Opacity Objects 不透明度為零的物件
Duplicate Objects 重複物件
Off-Artboard Art 工作區域外的圖稿
Duplicate Anchors 重複錨點
Tiny Handles 極短控制把手
Collinear Anchors 共線錨點
Nearly Coincident Anchors 幾乎重合的錨點
Nearly Closed Paths 幾乎封閉的路徑
Filled Open Paths 有填色的開放路徑
Needless Clipping Groups 多餘的剪裁群組
Needless Compound Paths 多餘的複合路徑
Needless Groups 多餘的群組
Empty Artboards 空白工作區域
Protect Corners 保護尖角
Gap 間隙
Include Clip Groups 包含剪裁群組
@msg `tolerance` must be from 1 to 100 `tolerance` 必須介於 1 到 100 之間
@msg `gap` must be from 0 to 6 pt `gap` 必須介於 0 到 6 pt 之間
@msg Simplify Anchors: the paths have too many anchors to simplify at once 簡化錨點:路徑的錨點太多,無法一次簡化
@msg Rejoin Path Pieces: too many paths to rejoin at once 重新連接路徑片段:路徑太多,無法一次連接
@msg None of these issues are left: check again 這些問題都已不存在:請重新檢查
@msg `kinds` must be a list of issue kinds `kinds` 必須是問題類型的清單
@msg missing `kind` 未指定 `kind`
Simplify Anchors (Keep Shape) 簡化錨點(保持形狀)
Deep Ungroup 解散所有層級群組
Rejoin Path Pieces 重新連接路徑片段
Can't render this file because it contains an unexpected character in line 6 and column 47.
+1
View File
@@ -22,6 +22,7 @@ macro_rules! tl {
};
}
pub mod addons;
pub mod background;
mod brand;
pub mod canvas;
+1 -1
View File
@@ -367,7 +367,7 @@ pub fn menu_tree_named(english_names: bool) -> Vec<(&'static str, Vec<Item>)> {
],
),
Sep,
sub("Plug-ins", crate::dialogs::plugin::object_menu()),
sub("Plug-ins", [crate::addons::plugin_menu(), crate::dialogs::plugin::object_menu()].concat()),
],
),
(
+1
View File
@@ -179,6 +179,7 @@ pub fn show_icon_panel(app: &mut VectorcraftApp, ui: &mut Ui, id: &str) {
links::ID => links::show(app, ui),
asset_export::ID => asset_export::show(app, ui),
css_properties::ID => css_properties::show(app, ui),
crate::addons::cleanup::ID => crate::addons::cleanup::show(app, ui),
id if library_panel::parse(id).is_some() => library_panel::show(app, ui, id),
_ => {
dim_label(ui, tl!("This panel is on the roadmap (see the parity plan)."));
+1
View File
@@ -197,6 +197,7 @@ pub const ICON_PANELS: &[(&str, &str, &str)] = &[
(crate::panels::asset_export::ID, "Asset Export", "share-2"),
(crate::panels::css_properties::ID, "CSS Properties", "globe"),
(crate::panels::variables::ID, "Variables", "chart-column"),
(crate::addons::cleanup::ID, "Artwork Cleanup", "sparkles"),
];
/// Groups of icon panels separated by dividers in the collapsed column.
+56
View File
@@ -5652,3 +5652,59 @@ the entry names and decodes the codes that VectorCraft's default shortcuts agree
{"name":"run_command","arguments":{"command":"shortcutSet.read","params":{"path":"/Users/me/Studio.kys"}}}
{"name":"run_command","arguments":{"command":"importMatches.set","params":{"matches":[{"kind":"name","name":"Group","target":"object.group"},{"kind":"key","name":"901","target":"G"}]}}}
```
## Artwork Cleanup add-on
Artwork Cleanup is a built-in add-on: its commands are `addon.cleanup.*`, and in the desktop app it lives in Object ›
Plug-ins › Artwork Cleanup and in its panel (`window.panel {panel: "artworkCleanup"}`).
`addon.cleanup.simplify {tolerance?, protectCorners?, ids?}` removes the anchors each selected path can do without and
refits the handles beside each one, so the outline keeps its shape. `tolerance` runs from 1 to 100 (20 by default); the
outline stays within tolerance × 0.02 pt of where it was, measured against the original outline, so removals don't add
up. With `protectCorners`, anchors where the path turns by more than 30° stay. Open ends always stay. It reports the
anchor counts `before` and `after`, as one undo step.
The health check looks for issues in the selection, or in the whole document while nothing is selected (`scope:
"selection"|"document"` picks one). Locked and hidden objects, and the insides of live or generated art (blends,
envelopes, graphs, symbols), are left out.
- `addon.cleanup.check {scope?, kinds?}` changes nothing and lists `findings`: for each kind found, its `count`, the
`ids` of the objects it is on (at most 1,000) and, for empty artboards, their indexes.
- `addon.cleanup.show {kind, scope?}` selects the objects with that issue.
- `addon.cleanup.fix {scope?, kinds?}` fixes the issues as one undo step and reports how many of each kind it fixed.
The kinds run in a fixed order, so art removed by one kind can leave a group that a later kind unwraps, and they
run again (three rounds at most) while a round still fixes something.
| Kind | Finds | Fix |
|---|---|---|
| `emptyText` | Text with no characters, or only spaces | Removes it |
| `strayPoints` | Paths of one anchor (not clipping paths) | Removes them |
| `unpainted` | Paths and compound paths with no fill and no stroke | Removes them |
| `zeroOpacity` | Objects at 0 % opacity | Removes them |
| `duplicateObjects` | A path stacked directly on an identical one (geometry, paint, opacity) | Removes the upper one |
| `offArtboard` | Top-level objects that touch no artboard | Removes them |
| `duplicateAnchors` | Neighbouring anchors within 0.01 pt | Merges them |
| `tinyHandles` | Handles shorter than 0.01 pt | Retracts them |
| `collinearAnchors` | Corner anchors on a straight run (turning by a sine of 0.005 or less) | Removes them |
| `nearPoints` | Anchors within 0.01 pt of anchors of other paths | Snaps them together |
| `nearlyClosed` | Open paths whose ends lie within 0.01 pt | Closes them |
| `filledOpenPaths` | Open paths with a fill and no stroke | Closes them |
| `needlessClipGroups` | Clipping groups whose unpainted clip clips nothing, hides all their art, or is a rectangle round all of it | Removes the group (and the art it hides), or leaves the art |
| `needlessCompounds` | Compound paths of one path or none | Leaves the path, painted as the compound was |
| `needlessGroups` | Groups of one member or none, with no paint, effects or transparency of their own | Leaves the member |
| `emptyArtboards` | Artboards with no art (document scope only; one artboard always stays) | Deletes them |
`addon.cleanup.deepUngroup {includeClipGroups?, ids?}` takes apart every level of grouping in the selection. Clipping
groups stay whole unless `includeClipGroups` is true; their clipping paths then become plain paths. Objects keep the
opacity of the groups they were in.
`addon.cleanup.rejoin {gap?, ids?}` welds the selected open paths whose ends lie within `gap` (0 to 6 pt, 0.1 by
default) back into paths. Each end joins one other end: where several are in reach, the one the path turns least to go
on into. Pieces may run either way, and a chain whose ends meet closes. Each new path takes the place and paint of its
topmost piece.
```json
{"name":"run_command","arguments":{"command":"addon.cleanup.check","params":{"scope":"document"}}}
{"name":"run_command","arguments":{"command":"addon.cleanup.fix","params":{"kinds":["strayPoints","needlessGroups"]}}}
{"name":"run_command","arguments":{"command":"addon.cleanup.simplify","params":{"tolerance":40,"protectCorners":true}}}
```
+12 -1
View File
@@ -1,6 +1,6 @@
# Parity with Adobe Illustrator
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-30 · **Change:** minor (Hardware: a second finger cancels the tool press the first finger began, so a pinch only navigates (#4198); scores unchanged) · **Target:** Adobe Illustrator 2026 (30.x; public release notes through 30.7, August 2026)
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-30 · **Change:** minor (Artwork Cleanup, a built-in add-on beyond Illustrator; scores unchanged) · **Target:** Adobe Illustrator 2026 (30.x; public release notes through 30.7, August 2026)
## Feature submission evidence
@@ -1206,10 +1206,21 @@ applies to fills). Tests: `color::gradient::tests` (perceptual midpoint, expansi
`pdf::tests::dithered_perceptual_gradients_export_as_images_with_noise_on_the_fill` and the Gradient panel's
`the_method_menu_makes_the_gradient_perceptual`. Scores and hours unchanged.
## Artwork Cleanup add-on (2026-10-30)
A VectorCraft addition, not an Illustrator feature, so no area score moves. Object › Plug-ins › Artwork Cleanup and its
panel run `addon.cleanup.*`: Simplify Anchors (Keep Shape) on the shared kernel
`vectorcraft_pathops::addons::keep_shape`, a health check of 16 kinds of issue on the selection or the document with
Show, Fix Selected and Fix All (each one undo step), Deep Ungroup and Rejoin Path Pieces. Tests: `addons::` in
`vectorcraft-pathops`, `cmd::addons::cleanup::tests` in `vectorcraft-engine` (each kind found, fixed and clean art left
alone; Fix All as one step; scopes; rejoin within the gap only) and `addons::cleanup::tests` in `vectorcraft-ui-egui`
(the submenu opens the panel; Check lists what it finds). Commands: `docs/mcp.md` › Artwork Cleanup add-on.
## Revision history
| Date | Change | Summary |
|---|---|---|
| 2026-10-30 | minor | Artwork Cleanup add-on (a VectorCraft addition): evidence added; scores unchanged. |
| 2026-10-30 | minor | Hardware: a second finger cancels the tool press the first finger began, and a finger landing while another is down presses nothing, so a pinch only navigates (#4198); evidence added; scores unchanged (estimated). |
| 2026-10-30 | minor | File formats: SVG export writes axis-aligned rectangles as `<rect>`, circles and ellipses as `<circle>`/`<ellipse>`, straight lines as `<line>` and other straight-segment paths as `<polygon>`/`<polyline>`, keeping `<path>` where a shape element would draw differently (#4652); evidence added; scores and hours unchanged (estimated). |
| 2026-10-30 | minor | UI chrome: with Preferences › Performance › Real-time Drawing and Editing off, a ruler guide dragged out of a ruler or moved shows while it is dragged (#4646); evidence added; score and hours unchanged (estimated). |
+3 -1
View File
@@ -1,6 +1,6 @@
# UI and interaction parity
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-30 · **Change:** minor (Spinners: a held arrow repeats after a pause (#4267); scores unchanged) · **Target:** Adobe Illustrator 2026 (30.x)
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-30 · **Change:** minor (Artwork Cleanup add-on: its panel and Object › Plug-ins submenu; scores unchanged) · **Target:** Adobe Illustrator 2026 (30.x)
## Feature submission evidence
@@ -321,6 +321,7 @@ next unit in the order a right-click on a ruler lists them, from Feet back to Po
| Preferences › General › Constrain Angle | done | arrow keys and Shift drags along the turned axes; the Rectangle, Rounded Rectangle, Ellipse and grid tools, point and area type and the shape size dialogs draw along them with the bounding box turned; Shift-drawn polygons and stars keep the turned base orientation; the Line Segment tool's Shift steps count from it (#394, <https://helpx.adobe.com/illustrator/desktop/manage-objects/arrange-objects/rotate-objects-by-specific-angles.html>); arcs, spirals, flares and Pen paths are not turned |
| Tabs panel: a stop drags from where the press landed, a stop just added included | done | #1458 |
| Outline view keeps template layers in Preview, dimmed | done | #1486; a picture placed to be traced stays visible under the outlines |
| Artwork Cleanup add-on (VectorCraft addition): Object › Plug-ins › Artwork Cleanup opens its panel, which checks the selection or the document for 16 kinds of issue, lists them with counts, and shows or fixes the picked rows or all of them, each fix one undo step; Simplify Anchors (Keep Shape), Rejoin Path Pieces and Deep Ungroup open their options first | done | `addon.cleanup.*`, `crates/ui-egui/src/addons/cleanup.rs` and its tests; not an Illustrator feature, so it is not scored |
## Look and feel
@@ -361,6 +362,7 @@ separate. Scores and hours unchanged. [Behavior and command details](development
| Date | Change | Summary |
|---|---|---|
| 2026-10-30 | minor | Artwork Cleanup add-on (a VectorCraft addition): its panel and Object › Plug-ins › Artwork Cleanup; scores and estimates unchanged |
| 2026-10-30 | minor | Spinners: a held arrow repeats after a pause (#4267); scores and estimates unchanged |
| 2026-10-30 | minor | Control bar: a Replace menu swaps the selected symbol instances' symbol (#4121); scores and estimates unchanged |
| 2026-10-30 | minor | Color Mixer popover: the Color panel's menu with its color modes (#4249); scores and estimates unchanged |