mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-31 13:17:19 -06:00
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:
@@ -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
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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}");
|
||||
}
|
||||
}
|
||||
@@ -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};
|
||||
@@ -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;
|
||||
@@ -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());
|
||||
}
|
||||
@@ -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)));
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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]
|
||||
}
|
||||
@@ -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}");
|
||||
}
|
||||
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -22,6 +22,7 @@ macro_rules! tl {
|
||||
};
|
||||
}
|
||||
|
||||
pub mod addons;
|
||||
pub mod background;
|
||||
mod brand;
|
||||
pub mod canvas;
|
||||
|
||||
@@ -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()),
|
||||
],
|
||||
),
|
||||
(
|
||||
|
||||
@@ -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)."));
|
||||
|
||||
@@ -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
@@ -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}}}
|
||||
```
|
||||
|
||||
@@ -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
@@ -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 |
|
||||
|
||||
Reference in New Issue
Block a user