Merge pull request #4771 from storytold/feat/addon-path-effects

Add-ons: Path Precision Tools — live path effects
This commit is contained in:
BFlat
2026-10-31 06:26:41 +07:00
committed by GitHub
48 changed files with 2071 additions and 11 deletions
+3
View File
@@ -6,7 +6,10 @@
//! with one to four inks.
//! - [`halftone`]: Color Effects' live Vector Halftone, and the screens Object › Vector Halftone
//! builds its dots with.
//! - [`path`]: Path Precision Tools' live Path Effects, geometry effects built on the add-on's path
//! kernels.
pub mod color;
pub mod duotone;
pub mod halftone;
pub mod path;
+243
View File
@@ -0,0 +1,243 @@
//! Path Effects (Path Precision Tools): live geometry effects that rewrite an object's path with the
//! add-on's path kernels (Effect › Path Effects). Each is evaluated with the other geometry effects
//! ([`crate::apply_geometry`]), so it draws, measures, exports and expands as they do; the geometry
//! is written only by Expand Appearance.
//!
//! Parameters come from documents and scripts, so each is clamped to its documented range, and a
//! path with a coordinate past [`MAX_COORDINATE`] or not finite is left as it is.
use serde_json::{Value, json};
use vectorcraft_doc::Dash;
use vectorcraft_geom::{PathData, Point, SubPath};
use vectorcraft_pathops::addons::pathtools::{self as tools, AnchorSpacing, CloseMode, ExtendMode, SMOOTH_RATIO, SegmentKind};
use vectorcraft_pathops::addons::{PT_PER_TOLERANCE, retract_tiny_handles, simplify_keep_shape};
use crate::catalog::{EffectInfo, Lengths};
use crate::stroke;
use crate::util::{flag, num, text};
/// What every Path Effect's id starts with.
pub const PREFIX: &str = "addon.path.";
pub const DASHES: &str = "addon.path.dashes";
pub const ADD_ANCHORS: &str = "addon.path.addAnchors";
pub const EXTEND: &str = "addon.path.extend";
pub const CLOSE: &str = "addon.path.close";
pub const SMOOTH: &str = "addon.path.smooth";
pub const RETRACT: &str = "addon.path.retract";
pub const REVERSE: &str = "addon.path.reverse";
pub const SIMPLIFY: &str = "addon.path.simplify";
pub const SPLIT: &str = "addon.path.split";
const MENU: &[&str] = &["Effect", "Path Effects"];
/// The dash and gap parameters of [`DASHES`], pair by pair.
const DASH_PAIRS: [(&str, &str); 5] = [("dash1", "gap1"), ("dash2", "gap2"), ("dash3", "gap3"), ("dash4", "gap4"), ("dash5", "gap5")];
/// Longest dash or gap, and the farthest offset (pt).
const MAX_DASH: f64 = 10_000.0;
/// Longest stretch [`EXTEND`] adds or takes off an end, in points and in percent of the path.
const MAX_EXTEND: f64 = 100_000.0;
const MAX_EXTEND_PERCENT: f64 = 1000.0;
/// Most anchors [`ADD_ANCHORS`] adds to one segment.
const MAX_PER_SEGMENT: f64 = 1000.0;
/// Closest and farthest apart [`ADD_ANCHORS`]' anchors are (pt).
const DISTANCE_RANGE: (f64, f64) = (0.01, 100_000.0);
/// The range of [`SMOOTH`]'s ratio.
const RATIO_RANGE: (f64, f64) = (0.01, 0.5);
/// Longest handle length [`RETRACT`] takes (pt).
const MAX_HANDLE: f64 = 100_000.0;
/// Highest tolerance of [`SIMPLIFY`].
const MAX_TOLERANCE: f64 = 200.0;
// shortcut: simplifying measures every anchor on every draw, so a larger path draws as it is; lift
// this if the kernel gets an incremental pass.
/// Most anchors [`SIMPLIFY`] takes in one path.
const MAX_SIMPLIFY_ANCHORS: usize = 50_000;
/// A path with a coordinate past this, or one that isn't finite, is left as it is: lengths measured
/// on it would overflow.
const MAX_COORDINATE: f64 = 1e12;
/// The effects' catalogue entries.
pub(crate) fn catalog() -> Vec<EffectInfo> {
let entry = |id, label, params, defaults, always| EffectInfo {
id,
label,
menu: MENU,
params,
defaults,
raster: false,
lengths: Lengths { always, absolute: &[] },
};
vec![
entry(
DASHES,
"Dashes to Geometry…",
"{dash1: pt 0..10000 (12), gap1: pt 0..10000 (6), dash2: pt 0..10000 (0), gap2: pt 0..10000 (0), dash3: pt 0..10000 (0), gap3: pt 0..10000 (0), dash4: pt 0..10000 (0), gap4: pt 0..10000 (0), dash5: pt 0..10000 (0), gap5: pt 0..10000 (0), offset: pt -10000..10000 (0; how far into the pattern the path starts; not with alignCorners), alignCorners: bool (false; every run between corners and path ends holds whole periods, stretched or squeezed to fit, with a dash centred on each corner and end)} cuts the path into its dashes, open subpaths along it that the stroke paints as it paints any path. A dash of 0 is a point its stroke's round or projecting cap paints; a pair of two zeros is left out, and the pattern repeats. A pattern of nothing, or of more than 200000 dashes, leaves the path as it is; a dash longer than a closed subpath keeps it closed",
json!({"dash1": 12.0, "gap1": 6.0, "dash2": 0.0, "gap2": 0.0, "dash3": 0.0, "gap3": 0.0, "dash4": 0.0, "gap4": 0.0, "dash5": 0.0, "gap5": 0.0, "offset": 0.0, "alignCorners": false}),
&["dash1", "gap1", "dash2", "gap2", "dash3", "gap3", "dash4", "gap4", "dash5", "gap5", "offset"],
),
entry(
ADD_ANCHORS,
"Add Anchors…",
"{target: \"all\"|\"curved\"|\"straight\" (all; the segments that get anchors), spacing: \"count\"|\"distance\" (count), count: 1..1000 (1; anchors on each segment, evenly spaced by length), distance: pt 0.01..100000 (20; an anchor every so far along each segment from its start), maxCount: 1..1000 (100; most anchors added to one segment)} adds anchors without changing the shape; past 2 million anchors in all the path stays as it is",
json!({"target": "all", "spacing": "count", "count": 1, "distance": 20.0, "maxCount": 100}),
&["distance"],
),
entry(
EXTEND,
"Extend Path…",
"{startLength: pt -100000..100000 (10), startPercent: % of the path's length -1000..1000 (0), startShape: \"straight\"|\"arc\" (straight), endLength: pt -100000..100000 (10), endPercent: % of the path's length -1000..1000 (0), endShape: \"straight\"|\"arc\" (straight)} lengthens each end of an open subpath by its length and percent together, or shortens it back along itself when they come to less than 0. Straight runs on along the end's direction; arc follows the circle of the end's curvature, at most half way round. Closed subpaths stay as they are, and so does an end that would be shortened to nothing",
json!({"startLength": 10.0, "startPercent": 0.0, "startShape": "straight", "endLength": 10.0, "endPercent": 0.0, "endShape": "straight"}),
&["startLength", "endLength"],
),
entry(
CLOSE,
"Close Path…",
"{mode: \"straight\"|\"smooth\" (straight; smooth: the closing segment leaves both ends smoothly)} closes every open subpath of two anchors or more",
json!({"mode": "straight"}),
&[],
),
entry(
SMOOTH,
"Smooth Anchors…",
"{ratio: 0.01..0.5 (0.3905; a handle's length as a share of the distance to the nearer neighbour; 0.3905 makes a square's corners a circle)} makes every anchor smooth: a lone handle is mirrored, two are brought into line keeping their lengths, and an anchor without handles gets them at right angles to the bisector of its neighbours",
json!({"ratio": SMOOTH_RATIO}),
&[],
),
entry(
RETRACT,
"Retract Handles…",
"{handles: \"all\"|\"shorter\" (all; shorter: only those shorter than length), length: pt 0..100000 (5)} takes the handles back to their anchors; an anchor that loses one becomes a corner",
json!({"handles": "all", "length": 5.0}),
&["length"],
),
entry(
REVERSE,
"Reverse Direction",
"{} runs every subpath the other way: the shape stays, and arrowheads, dashes and the effects after it swap ends",
json!({}),
&[],
),
entry(
SIMPLIFY,
"Simplify Anchors…",
"{tolerance: 0..200 (10; higher removes more anchors and lets the outline move further, up to tolerance × 0.02 pt), protectCorners: bool (true; anchors where the path turns by more than 30° stay)} removes the anchors a path can do without, refitting the handles beside each; open ends stay; a path of more than 50000 anchors stays as it is",
json!({"tolerance": 10.0, "protectCorners": true}),
&[],
),
entry(
SPLIT,
"Split at Anchors…",
"{anchors: \"corner\"|\"smooth\"|\"both\" (both; smooth: where the handles stand in line, corner: the others)} cuts subpaths into open pieces at those anchors; the shape stays, and an open path's ends aren't cuts",
json!({"anchors": "both"}),
&[],
),
]
}
/// Path Effect `id` with parameters `p` on `path`, which keeps to `max` anchors where anchors are
/// added one by one. `path` as it is when `id` is no such effect or the path can't be measured.
pub(crate) fn apply(id: &str, p: &Value, path: &PathData, max: usize) -> PathData {
if !measurable(path) {
return path.clone();
}
let edit = |change: &dyn Fn(&mut SubPath)| {
let mut out = path.clone();
out.subpaths.iter_mut().for_each(change);
out
};
match id {
DASHES => dashes(path, p),
ADD_ANCHORS => add_anchors(path, p, max).unwrap_or_else(|| path.clone()),
EXTEND => PathData::new(path.subpaths.iter().map(|sp| extend(sp, p)).collect()),
CLOSE => {
let mode = if text(p, "mode", "straight") == "smooth" { CloseMode::Smooth } else { CloseMode::Straight };
edit(&|sp| {
tools::close(sp, mode, SMOOTH_RATIO);
})
}
SMOOTH => {
let ratio = num(p, "ratio", SMOOTH_RATIO).clamp(RATIO_RANGE.0, RATIO_RANGE.1);
edit(&|sp| tools::smooth_anchors(sp, ratio))
}
RETRACT if text(p, "handles", "all") == "shorter" => {
let length = num(p, "length", 5.0).clamp(0.0, MAX_HANDLE);
edit(&|sp| {
retract_tiny_handles(sp, length);
})
}
RETRACT => edit(&|sp| sp.anchors.iter_mut().for_each(|a| a.retract())),
REVERSE => edit(&SubPath::reverse),
SIMPLIFY if path.anchor_count() <= MAX_SIMPLIFY_ANCHORS => {
let tolerance = num(p, "tolerance", 10.0).clamp(0.0, MAX_TOLERANCE) * PT_PER_TOLERANCE;
simplify_keep_shape(path, tolerance, flag(p, "protectCorners", true))
}
SPLIT => {
let at = text(p, "anchors", "both");
let (corners, smooth) = (at != "smooth", at != "corner");
PathData::new(path.subpaths.iter().flat_map(|sp| tools::split_at_anchors(sp, corners, smooth)).collect())
}
_ => path.clone(),
}
}
/// Is every coordinate of `path` within [`MAX_COORDINATE`]?
fn measurable(path: &PathData) -> bool {
let near = |q: Point| q.x.abs() <= MAX_COORDINATE && q.y.abs() <= MAX_COORDINATE;
path.anchors().all(|(_, _, a)| near(a.p) && near(a.h_in) && near(a.h_out))
}
/// [`DASHES`]: the dashes of `path` as open subpaths, those of no length as subpaths that stay on
/// their point; `path` as it is when the pattern draws no dashes.
fn dashes(path: &PathData, p: &Value) -> PathData {
let length = |key| num(p, key, 0.0).clamp(0.0, MAX_DASH);
let pairs = DASH_PAIRS.iter().map(|(dash, gap)| (length(dash), length(gap)));
let pattern = pairs.filter(|(d, g)| *d > 0.0 || *g > 0.0).flat_map(|(d, g)| [d, g]).collect();
let dash = Dash { pattern, offset: num(p, "offset", 0.0).clamp(-MAX_DASH, MAX_DASH), align_corners: flag(p, "alignCorners", false) };
let Some(dashed) = stroke::dash(&path.to_bezpath(), &dash) else { return path.clone() };
let mut out = PathData::from_bezpath(&dashed.path);
out.subpaths.extend(dashed.dots.iter().map(|d| SubPath::polyline(&[d.at, d.at], false)));
out
}
/// [`ADD_ANCHORS`] within `max` anchors in all; `None` past them.
fn add_anchors(path: &PathData, p: &Value, max: usize) -> Option<PathData> {
let which = match text(p, "target", "all") {
"curved" => SegmentKind::Curved,
"straight" => SegmentKind::Straight,
_ => SegmentKind::All,
};
let spacing = match text(p, "spacing", "count") {
"distance" => AnchorSpacing::Distance(num(p, "distance", 20.0).clamp(DISTANCE_RANGE.0, DISTANCE_RANGE.1)),
_ => AnchorSpacing::Count(num(p, "count", 1.0).clamp(1.0, MAX_PER_SEGMENT) as usize),
};
let cap = num(p, "maxCount", 100.0).clamp(1.0, MAX_PER_SEGMENT) as usize;
let mut room = max.saturating_sub(path.anchor_count());
let mut out = Vec::with_capacity(path.subpaths.len());
for sp in &path.subpaths {
let added = tools::add_anchors(sp, which, spacing, cap, room)?;
room = room.saturating_sub(added.anchors.len().saturating_sub(sp.anchors.len()));
out.push(added);
}
Some(PathData::new(out))
}
/// [`EXTEND`] on one subpath, the start then the end; what can't be done stays as it is.
fn extend(sp: &SubPath, p: &Value) -> SubPath {
let total = sp.anchor_arc_positions().last().copied().unwrap_or(0.0);
let by = |length, percent| {
num(p, length, 0.0).clamp(-MAX_EXTEND, MAX_EXTEND) + num(p, percent, 0.0).clamp(-MAX_EXTEND_PERCENT, MAX_EXTEND_PERCENT) / 100.0 * total
};
let shape = |key| if text(p, key, "straight") == "arc" { ExtendMode::Arc } else { ExtendMode::Straight };
let ends = [(true, by("startLength", "startPercent"), shape("startShape")), (false, by("endLength", "endPercent"), shape("endShape"))];
let mut out = sp.clone();
for (at_start, by, shape) in ends {
if by != 0.0
&& let Some(changed) = tools::extend_open(&out, at_start, by, shape)
{
out = changed;
}
}
out
}
#[cfg(test)]
mod tests;
+278
View File
@@ -0,0 +1,278 @@
use serde_json::{Value, json};
use vectorcraft_doc::Effect;
use vectorcraft_geom::{Anchor, PathData, Point, Rect, SubPath};
use super::*;
use crate::{apply_geometry, effect_info, is_geometry, is_length, is_raster, length_range};
fn pt(x: f64, y: f64) -> Point {
Point::new(x, y)
}
fn line(pts: &[(f64, f64)], closed: bool) -> PathData {
PathData::single(SubPath::polyline(&pts.iter().map(|&(x, y)| pt(x, y)).collect::<Vec<_>>(), closed))
}
fn square() -> PathData {
line(&[(0.0, 0.0), (100.0, 0.0), (100.0, 100.0), (0.0, 100.0)], true)
}
/// A curve: a smooth anchor between two ends.
fn curve() -> PathData {
let mid = Anchor::smooth(pt(50.0, -30.0), pt(70.0, -30.0));
PathData::single(SubPath::new(
vec![
Anchor { h_out: pt(10.0, -20.0), ..Anchor::corner(pt(0.0, 0.0)) },
mid,
Anchor { h_in: pt(90.0, -20.0), ..Anchor::corner(pt(100.0, 0.0)) },
],
false,
))
}
fn run(id: &str, params: Value, path: &PathData) -> PathData {
let e = Effect { id: id.into(), params, visible: true };
apply_geometry(&[e], path, path.bounds().unwrap_or(Rect::ZERO))
}
fn near(a: f64, b: f64) -> bool {
(a - b).abs() < 1e-3
}
const ALL: [&str; 9] = [DASHES, ADD_ANCHORS, EXTEND, CLOSE, SMOOTH, RETRACT, REVERSE, SIMPLIFY, SPLIT];
#[test]
fn the_effects_are_geometry_effects_of_the_path_effects_menu() {
for id in ALL {
let info = effect_info(id).unwrap();
assert!(is_geometry(id) && !is_raster(id), "{id}");
assert_eq!(info.menu, ["Effect", "Path Effects"], "{id}");
assert!(id.starts_with(PREFIX));
}
assert!(is_length(DASHES, "gap5", false) && is_length(EXTEND, "endLength", false) && !is_length(EXTEND, "endPercent", false));
assert_eq!(length_range(DASHES, "offset"), Some(-10_000.0..=10_000.0));
}
#[test]
fn dashes_become_open_subpaths_along_the_path() {
let out = run(DASHES, json!({"dash1": 10, "gap1": 5}), &line(&[(0.0, 0.0), (100.0, 0.0)], false));
assert_eq!(out.subpaths.len(), 7, "periods of 15 on 100");
assert!(out.subpaths.iter().all(|s| !s.closed));
assert!(near(out.subpaths[1].anchors[0].p.x, 15.0) && near(out.subpaths[1].anchors[1].p.x, 25.0));
// Two pairs, and a pair of zeros between them left out.
let out = run(DASHES, json!({"dash1": 10, "gap1": 5, "dash3": 20, "gap3": 5}), &line(&[(0.0, 0.0), (100.0, 0.0)], false));
assert!(near(out.subpaths[1].anchors[0].p.x, 15.0) && near(out.subpaths[1].anchors[1].p.x, 35.0));
assert!(near(out.subpaths[2].anchors[0].p.x, 40.0));
// The offset starts part way into the pattern.
let out = run(DASHES, json!({"dash1": 10, "gap1": 5, "offset": 5}), &line(&[(0.0, 0.0), (100.0, 0.0)], false));
assert!(near(out.subpaths[0].anchors[1].p.x, 5.0));
}
#[test]
fn aligned_dashes_centre_on_corners_and_ends() {
let open = line(&[(0.0, 0.0), (100.0, 0.0), (100.0, 100.0)], false);
let out = run(DASHES, json!({"dash1": 10, "gap1": 10, "alignCorners": true}), &open);
let on = |p: Point| out.subpaths.iter().any(|s| (0..s.segment_count()).any(|i| s.anchors[i].p == p || s.anchors[i + 1].p == p));
assert!(on(pt(0.0, 0.0)) && on(pt(100.0, 100.0)), "a dash on each end");
let corner = out.subpaths.iter().find(|s| s.anchors.iter().any(|a| a.p == pt(100.0, 0.0))).expect("a dash over the corner");
assert!(near(corner.anchors[0].p.distance(pt(100.0, 0.0)), corner.anchors[2].p.distance(pt(100.0, 0.0))), "centred: {corner:?}");
}
#[test]
fn zero_dashes_are_points_and_patterns_of_nothing_leave_the_path() {
let path = line(&[(0.0, 0.0), (100.0, 0.0)], false);
let dots = run(DASHES, json!({"dash1": 0, "gap1": 25}), &path);
assert_eq!(dots.subpaths.len(), 5);
assert!(dots.subpaths.iter().all(|s| s.anchors.len() == 2 && s.anchors[0].p == s.anchors[1].p));
for params in [
json!({"dash1": 0, "gap1": 0}),
json!({"dash1": -5, "gap1": -5}),
json!({"dash1": 0.0001, "gap1": 0.0001}),
json!({"dash1": "x", "gap1": null}),
] {
assert_eq!(run(DASHES, params.clone(), &path), path, "{params}");
}
}
#[test]
fn add_anchors_spaces_them_and_keeps_to_the_caps() {
let square = square();
assert_eq!(run(ADD_ANCHORS, json!({}), &square).anchor_count(), 8, "one on each segment");
assert_eq!(run(ADD_ANCHORS, json!({"count": 3}), &square).anchor_count(), 16);
assert_eq!(run(ADD_ANCHORS, json!({"count": 3, "maxCount": 2}), &square).anchor_count(), 12);
let out = run(ADD_ANCHORS, json!({"spacing": "distance", "distance": 30}), &square);
assert_eq!(out.anchor_count(), 16, "at 30, 60 and 90 on each side");
assert!(near(out.subpaths[0].anchors[2].p.x, 60.0));
assert_eq!(run(ADD_ANCHORS, json!({"spacing": "distance", "distance": 25}), &square).anchor_count(), 16, "none on the segment's end");
assert_eq!(run(ADD_ANCHORS, json!({"spacing": "distance", "distance": 1e-9, "maxCount": 2}), &square).anchor_count(), 12);
// Only the segments asked for.
let mixed = PathData::new(vec![curve().subpaths[0].clone(), square.subpaths[0].clone()]);
assert_eq!(run(ADD_ANCHORS, json!({"target": "curved"}), &mixed).anchor_count(), 7 + 2);
assert_eq!(run(ADD_ANCHORS, json!({"target": "straight"}), &mixed).anchor_count(), 7 + 4);
// The shape stays.
let out = run(ADD_ANCHORS, json!({"count": 4}), &curve());
assert_eq!(out.anchor_count(), 3 + 8);
assert!(near(out.length(), curve().length()));
assert_eq!(out.bounds().map(|b| (b.y0 * 100.0).round()), curve().bounds().map(|b| (b.y0 * 100.0).round()));
}
#[test]
fn add_anchors_past_the_budget_leaves_the_path() {
let path = square();
let most = json!({"count": 1000, "maxCount": 1000});
assert_eq!(super::apply(ADD_ANCHORS, &most, &path, 4003), path);
assert_eq!(super::apply(ADD_ANCHORS, &most, &path, 4004).anchor_count(), 4004);
// Many segments, each with the most anchors: refused before they are built.
let long = line(&(0..4000).map(|i| (i as f64, 0.0)).collect::<Vec<_>>(), false);
assert_eq!(run(ADD_ANCHORS, json!({"count": 1000, "maxCount": 1000}), &long).anchor_count(), 4000, "4 million would pass the budget");
}
#[test]
fn extend_lengthens_and_shortens_each_end() {
let path = line(&[(0.0, 0.0), (100.0, 0.0)], false);
let out = run(EXTEND, json!({"startLength": 20, "endLength": 10}), &path);
let xs: Vec<f64> = out.subpaths[0].anchors.iter().map(|a| a.p.x).collect();
assert_eq!(xs, [-20.0, 0.0, 100.0, 110.0]);
// Percent of the path's length, adding to the length; negative shortens.
let out = run(EXTEND, json!({"startLength": 0, "endLength": 5, "endPercent": 10}), &path);
assert!(out.subpaths[0].anchors.last().is_some_and(|a| near(a.p.x, 115.0)));
let out = run(EXTEND, json!({"startLength": -10, "endLength": 0, "endPercent": -50}), &path);
let xs: Vec<f64> = out.subpaths[0].anchors.iter().map(|a| a.p.x).collect();
assert!(near(xs[0], 10.0) && near(xs[1], 50.0), "{xs:?}");
// An arc carries on round the end's curve.
let arc = run(EXTEND, json!({"startLength": 0, "endLength": 40, "endShape": "arc"}), &curve());
let straight = run(EXTEND, json!({"startLength": 0, "endLength": 40}), &curve());
assert!(arc.anchor_count() >= straight.anchor_count());
assert!((arc.length() - straight.length()).abs() < 0.1);
assert_ne!(arc.subpaths[0].anchors.last().map(|a| a.p), straight.subpaths[0].anchors.last().map(|a| a.p));
}
#[test]
fn extend_leaves_what_it_cannot_change() {
let closed = square();
assert_eq!(run(EXTEND, json!({"endLength": 50}), &closed), closed);
let path = line(&[(0.0, 0.0), (100.0, 0.0)], false);
assert_eq!(run(EXTEND, json!({"startLength": 0, "endLength": -500}), &path), path, "nothing would be left");
assert_eq!(run(EXTEND, json!({"startLength": 0, "endLength": 0, "endPercent": -100}), &path), path);
let one = PathData::single(SubPath::polyline(&[pt(1.0, 1.0)], false));
assert_eq!(run(EXTEND, json!({}), &one), one);
}
#[test]
fn close_joins_open_subpaths_straight_or_smooth() {
let open = line(&[(0.0, 0.0), (100.0, 0.0), (100.0, 100.0)], false);
let out = run(CLOSE, json!({}), &open);
assert!(out.subpaths[0].closed && out.subpaths[0].segment_is_line(2));
let out = run(CLOSE, json!({"mode": "smooth"}), &open);
assert!(out.subpaths[0].closed && !out.subpaths[0].segment_is_line(2));
assert_eq!(run(CLOSE, json!({}), &square()), square());
let one = PathData::single(SubPath::polyline(&[pt(1.0, 1.0)], false));
assert_eq!(run(CLOSE, json!({}), &one), one);
}
#[test]
fn smooth_makes_a_square_round_and_clamps_its_ratio() {
let out = run(SMOOTH, json!({}), &square());
assert!(out.subpaths[0].anchors.iter().all(|a| near(a.h_out.distance(a.p), 39.05) && a.has_in()));
let at = |ratio: f64| run(SMOOTH, json!({ "ratio": ratio }), &square()).subpaths[0].anchors[0];
assert!(near(at(9.0).h_out.distance(pt(0.0, 0.0)), 50.0));
assert!(near(at(-9.0).h_out.distance(pt(0.0, 0.0)), 1.0));
}
#[test]
fn retract_takes_all_handles_or_the_short_ones() {
let out = run(RETRACT, json!({}), &curve());
assert!(out.subpaths[0].anchors.iter().all(|a| !a.has_in() && !a.has_out()));
// The ends' handles are about 22 pt long and the middle anchor's 20.
let out = run(RETRACT, json!({"handles": "shorter", "length": 21}), &curve());
let a = &out.subpaths[0].anchors;
assert!(!a[1].has_in() && !a[1].has_out() && a[0].has_out() && a[2].has_in());
assert_eq!(run(RETRACT, json!({"handles": "shorter", "length": 1}), &curve()), curve());
}
#[test]
fn reverse_runs_the_other_way_and_twice_is_as_it_was() {
let out = run(REVERSE, json!({}), &curve());
assert_eq!(out.subpaths[0].anchors[0].p, pt(100.0, 0.0));
assert_eq!(run(REVERSE, json!({}), &out), curve());
}
#[test]
fn simplify_takes_anchors_within_the_tolerance_and_stops_at_its_cap() {
let pts: Vec<(f64, f64)> = (0..=40).map(|i| (i as f64, (i as f64 / 40.0 * std::f64::consts::PI).sin() * 3.0)).collect();
let path = line(&pts, false);
let out = run(SIMPLIFY, json!({"tolerance": 200}), &path);
assert!(out.anchor_count() < 20, "{}", out.anchor_count());
assert_eq!(run(SIMPLIFY, json!({"tolerance": 0}), &path), path);
// Corners stay unless told otherwise.
let corner = line(&[(0.0, 0.0), (1.0, 0.0), (1.0, 1.0)], false);
assert_eq!(run(SIMPLIFY, json!({"tolerance": 200}), &corner), corner);
assert_eq!(run(SIMPLIFY, json!({"tolerance": 200, "protectCorners": false}), &corner).anchor_count(), 2);
let long = line(&(0..=MAX_SIMPLIFY_ANCHORS).map(|i| (i as f64, 0.0)).collect::<Vec<_>>(), false);
assert_eq!(run(SIMPLIFY, json!({}), &long).anchor_count(), long.anchor_count());
}
#[test]
fn split_cuts_at_the_anchors_asked_for() {
let mixed = PathData::single(SubPath::new(
vec![
Anchor::corner(pt(0.0, 0.0)),
Anchor::smooth(pt(50.0, 0.0), pt(60.0, 0.0)),
Anchor::corner(pt(100.0, 0.0)),
Anchor::corner(pt(100.0, 50.0)),
],
false,
));
assert_eq!(run(SPLIT, json!({}), &mixed).subpaths.len(), 3);
assert_eq!(run(SPLIT, json!({"anchors": "smooth"}), &mixed).subpaths.len(), 2);
assert_eq!(run(SPLIT, json!({"anchors": "corner"}), &mixed).subpaths.len(), 2);
let out = run(SPLIT, json!({}), &square());
assert_eq!(out.subpaths.len(), 4);
assert!(out.subpaths.iter().all(|s| !s.closed && s.anchors.len() == 2));
assert!(near(out.length(), 400.0));
assert_eq!(run(SPLIT, json!({"anchors": "smooth"}), &square()), square(), "no smooth anchors");
}
/// Whatever a file holds: every effect takes wrong types, huge numbers and paths of nothing, of one
/// anchor or past measuring, and keeps to the budget.
#[test]
fn junk_params_and_paths_are_harmless() {
let junk = [
json!({}),
json!(null),
json!([1, 2]),
json!("x"),
json!({"dash1": 1e308, "gap1": 1e308, "dash2": -1e308, "gap2": "x", "offset": 1e308, "alignCorners": "yes"}),
json!({"dash1": 1e-300, "gap1": 1e-300, "offset": -1e308, "alignCorners": true}),
json!({"target": 3, "spacing": ["distance"], "count": 1e308, "distance": 1e-308, "maxCount": -1e308}),
json!({"spacing": "distance", "distance": -1e308, "maxCount": 1e308}),
json!({"startLength": 1e308, "startPercent": 1e308, "startShape": 7, "endLength": -1e308, "endPercent": -1e308, "endShape": "arc"}),
json!({"mode": {}, "ratio": 1e308, "handles": "shorter", "length": 1e308, "tolerance": 1e308, "protectCorners": "no", "anchors": 9}),
json!({"ratio": -1e308, "length": -1e308, "tolerance": -1e308, "anchors": "none"}),
];
let far = line(&[(1e308, -1e308), (-1e308, 1e308), (1e300, 0.0)], false);
let nan = line(&[(f64::NAN, 0.0), (f64::INFINITY, 1.0)], false);
let tiny = line(&[(0.0, 0.0), (1e-300, 0.0), (1e-300, 1e-300)], true);
let same = line(&[(5.0, 5.0), (5.0, 5.0), (5.0, 5.0)], true);
let empty = PathData::single(SubPath::default());
let one = PathData::single(SubPath::polyline(&[pt(1.0, 1.0)], true));
let paths = [square(), curve(), far.clone(), nan.clone(), tiny, same, empty, one, PathData::default()];
for id in ALL {
for params in &junk {
for path in &paths {
let out = super::apply(id, &crate::merged_params(id, params), path, 100_000);
assert!(out.anchor_count() <= 100_000.max(path.anchor_count()) + 8 * path.subpaths.len(), "{id} {params}");
}
}
// Compared as text: NaN never equals itself.
assert_eq!(format!("{:?}", super::apply(id, &json!({}), &nan, 100)), format!("{nan:?}"), "{id}");
assert_eq!(super::apply(id, &json!({}), &far, 100), far, "{id}");
}
}
/// The dash generator's limit holds: a pattern too fine for the path leaves it solid.
#[test]
fn too_many_dashes_leave_the_path() {
let long = line(&[(0.0, 0.0), (1e9, 0.0)], false);
assert_eq!(run(DASHES, json!({"dash1": 1, "gap1": 1}), &long), long);
}
+1
View File
@@ -793,6 +793,7 @@ pub fn effect_catalog() -> Vec<EffectInfo> {
}
v.extend(crate::addons::color::catalog());
v.extend(crate::addons::halftone::catalog());
v.extend(crate::addons::path::catalog());
v.push(g(TRAP, "Trap…", PATHFINDER, trap::EFFECT_PARAMS_DOC, trap::effect_defaults()));
v.push(g(
CROP_MARKS,
+1
View File
@@ -265,6 +265,7 @@ fn apply_unchecked(id: &str, p: &Value, path: &PathData, b: Rect, ctx: &GeomCont
"convertToShape.rectangle" | "convertToShape.roundedRectangle" | "convertToShape.ellipse" => stylize::convert_to_shape(id, b, p),
"stylize.roundCorners" => stylize::round_corners(path, num(p, "radius", 10.0)),
"stylize.scribble" => stylize::scribble(path, b, p),
_ if id.starts_with(crate::addons::path::PREFIX) => crate::addons::path::apply(id, p, path, max),
_ => match id.strip_prefix("warp.").and_then(WarpStyle::from_id) {
Some(style) => warp::warp(path, b, style, p, max),
None => path.clone(),
+2 -3
View File
@@ -18,9 +18,8 @@ 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.
/// Simplify Anchors' tolerance by default.
const TOLERANCE: f64 = 20.0;
pub(super) 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;
@@ -123,7 +122,7 @@ fn simplify(s: &mut Session, p: &Value) -> Result<Value> {
let changed: Vec<(NodeId, PathData)> = paths
.iter()
.filter_map(|(id, path)| {
let np = po::simplify_keep_shape(path, tolerance * PT_PER_TOLERANCE, protect);
let np = po::simplify_keep_shape(path, tolerance * po::PT_PER_TOLERANCE, protect);
(np != **path).then_some((*id, np))
})
.collect();
+3 -2
View File
@@ -6,6 +6,8 @@
//! and the UI's.
mod corners;
#[cfg(test)]
mod effect_tests;
mod shape;
#[cfg(test)]
mod tests;
@@ -22,7 +24,6 @@ use super::super::pathops::Beside;
use super::super::{
CommandSpec, bad, checked_id, checked_ids_or_selection, cmd, finite_numbers, has_doc, has_selection, number_or, switch_or, text_param,
};
use super::cleanup::PT_PER_TOLERANCE;
use crate::{EngineError, Result, Session};
/// The Anchor Eraser Brush's tolerance by default (the scale Simplify Anchors (Keep Shape) uses).
@@ -256,7 +257,7 @@ fn erase(s: &mut Session, p: &Value) -> Result<Value> {
let ids = paths_under(d, &roots);
let paths: Vec<(NodeId, &PathData)> = ids.iter().filter_map(|id| Some((*id, d.node(*id)?.path_data()?))).collect();
check_size(paths.iter().map(|(_, p)| *p), C)?;
let tol = if ignore { f64::INFINITY } else { tolerance * PT_PER_TOLERANCE };
let tol = if ignore { f64::INFINITY } else { tolerance * po::PT_PER_TOLERANCE };
let mut removed = 0;
let mut changed = vec![];
for (id, path) in paths {
@@ -0,0 +1,186 @@
//! Path Effects (`addon.path.*`) through the session: live on the canvas, the document's path left
//! alone, and Expand Appearance and a save keeping what they draw.
use serde_json::{Value, json};
use vectorcraft_doc::{Document, Effect, Node, NodeId};
use vectorcraft_geom::{BezPath, Rect};
use vectorcraft_render::Renderer;
use vectorcraft_render::effects::{GeomContext, apply_geometry_bez, effect_catalog, new_effect, preview, scale_effect};
use crate::Session;
/// A black 4 pt stroke along `anchors`, unfilled.
fn stroked(anchors: Value, closed: bool) -> (Session, NodeId) {
let mut s = Session::new();
s.execute("file.new", &json!({"width": 200, "height": 200})).unwrap();
let id = NodeId(s.execute("path.create", &json!({"anchors": anchors, "closed": closed})).unwrap()["id"].as_u64().unwrap());
s.select(|_, sel| sel.set([id])).unwrap();
s.execute("paint.setFill", &json!({"none": true})).unwrap();
s.execute("paint.setStroke", &json!({"color": "#000000"})).unwrap();
s.execute("stroke.set", &json!({"weight": 4})).unwrap();
(s, id)
}
fn wave() -> (Session, NodeId) {
let anchors = json!([
{"x": 20, "y": 100},
{"x": 60, "y": 60, "in": [40, 60], "out": [80, 60]},
{"x": 100, "y": 100},
{"x": 140, "y": 140},
{"x": 180, "y": 100}
]);
stroked(anchors, false)
}
/// Every Path Effect, with parameters that change the wave.
fn effects() -> Vec<(&'static str, Value)> {
vec![
("addon.path.dashes", json!({"dash1": 14, "gap1": 6, "dash2": 3, "gap2": 6})),
("addon.path.dashes", json!({"dash1": 14, "gap1": 6, "alignCorners": true})),
("addon.path.addAnchors", json!({"count": 3})),
("addon.path.addAnchors", json!({"spacing": "distance", "distance": 15, "target": "curved"})),
("addon.path.extend", json!({"startLength": 10, "endPercent": 10, "endShape": "arc"})),
("addon.path.extend", json!({"startLength": -10, "endLength": -20})),
("addon.path.close", json!({})),
("addon.path.close", json!({"mode": "smooth"})),
("addon.path.smooth", json!({"ratio": 0.3})),
("addon.path.retract", json!({})),
("addon.path.reverse", json!({})),
("addon.path.simplify", json!({"tolerance": 200, "protectCorners": false})),
("addon.path.split", json!({"anchors": "both"})),
]
}
fn node(s: &Session, id: NodeId) -> Node {
s.doc().unwrap().doc.node(id).cloned().unwrap()
}
fn picture_of(d: &Document) -> Vec<u8> {
let r = Renderer::new().render_region(d, Rect::new(0.0, 0.0, 200.0, 200.0), 1.0, true);
(0..200).flat_map(|y| (0..200).map(move |x| (x, y))).flat_map(|(x, y)| r.pixel(x, y)).collect()
}
fn picture(s: &Session) -> Vec<u8> {
picture_of(&s.doc().unwrap().doc)
}
/// The outline of `n`: its path, or those of the compound path it became.
fn outline(n: &Node) -> BezPath {
let mut out = n.path_data().map(|p| p.to_bezpath()).unwrap_or_default();
n.children().into_iter().flatten().for_each(|c| out.extend(outline(c)));
out
}
/// How many bytes of two pictures differ by more than antialiasing noise.
fn differing(a: &[u8], b: &[u8]) -> usize {
a.iter().zip(b).filter(|(x, y)| x.abs_diff(**y) > 8).count()
}
#[test]
fn a_path_effect_draws_live_and_leaves_the_document_path() {
let (mut s, id) = stroked(json!([{"x": 10, "y": 50}, {"x": 190, "y": 50}]), false);
let solid = picture(&s);
let path = node(&s, id).path_data().cloned();
s.execute("effect.apply", &json!({"effect": "addon.path.dashes", "params": {"dash1": 20, "gap1": 20}})).unwrap();
assert_eq!(node(&s, id).path_data().cloned(), path, "only the effect holds the geometry");
let dashed = picture(&s);
let at = |pic: &[u8], x: usize| pic[(50 * 200 + x) * 4];
assert!(at(&solid, 45) < 40 && at(&dashed, 45) > 200, "a gap: {}", at(&dashed, 45));
assert!(at(&dashed, 15) < 40, "a dash");
// Hiding it draws the path again.
s.execute("effect.setParams", &json!({"index": 0, "visible": false})).unwrap();
assert_eq!(picture(&s), solid);
}
#[test]
fn a_path_effect_moves_the_bounds_the_object_is_measured_by() {
let (mut s, id) = stroked(json!([{"x": 50, "y": 50}, {"x": 150, "y": 50}]), false);
s.execute("effect.apply", &json!({"effect": "addon.path.extend", "params": {"startLength": 0, "endLength": 30}})).unwrap();
let drawn = preview::bounds_of(&s.doc().unwrap().doc, &[id], true).unwrap();
assert!((drawn.x1 - 180.0).abs() < 3.0, "{drawn:?}");
let pic = picture(&s);
assert!(pic[(50 * 200 + 170) * 4] < 40, "and draws there");
}
#[test]
fn expand_appearance_writes_the_outline_the_effect_draws() {
for (effect, params) in effects() {
let (mut s, id) = wave();
s.execute("effect.apply", &json!({"effect": effect, "params": params})).unwrap();
let n = node(&s, id);
let before = n.path_data().cloned().unwrap();
let live = apply_geometry_bez(&n.appearance.effects, &before.to_bezpath(), before.bounds().unwrap(), &GeomContext::of(&n));
let pic = picture(&s);
s.execute("effect.expandAppearance", &json!({})).unwrap();
let expanded = node(&s, id);
assert!(expanded.appearance.effects.is_empty(), "{effect} {params}");
let (out, want) = (outline(&expanded), live);
assert_eq!(out.elements().len(), want.elements().len(), "{effect} {params}");
for (a, b) in out.elements().iter().zip(want.elements()) {
assert!(a.is_finite() && format!("{a:.4?}") == format!("{b:.4?}"), "{effect} {params}: {a:?} {b:?}");
}
assert_eq!(differing(&pic, &picture(&s)), 0, "{effect} {params}");
}
}
#[test]
fn path_effects_round_trip_through_a_save_and_scale_their_distances() {
for (effect, params) in effects() {
let (mut s, id) = wave();
s.execute("effect.apply", &json!({"effect": effect, "params": params})).unwrap();
let restored = vectorcraft_format::load(&vectorcraft_format::save(&s.doc().unwrap().doc, false)).unwrap();
assert_eq!(restored.node(id).unwrap().appearance.effects, node(&s, id).appearance.effects, "{effect} {params}");
assert_eq!(picture_of(&restored), picture(&s), "{effect} {params}");
}
let mut e = new_effect("addon.path.dashes", &json!({"dash1": 10})).unwrap();
scale_effect(&mut e, 2.0);
assert_eq!((e.params["dash1"].as_f64(), e.params["gap1"].as_f64()), (Some(20.0), Some(12.0)));
}
#[test]
fn undo_takes_a_path_effect_off_in_one_step() {
let (mut s, id) = wave();
s.execute("effect.apply", &json!({"effect": "addon.path.reverse"})).unwrap();
assert_eq!(node(&s, id).appearance.effects.len(), 1);
s.execute("edit.undo", &json!({})).unwrap();
assert!(node(&s, id).appearance.effects.is_empty());
}
/// A file's effects skip the checks `effect.apply` makes: whatever they hold, the object draws and
/// expands.
#[test]
fn path_effects_with_junk_params_from_a_file_draw_and_expand() {
let junk = [
json!(null),
json!("x"),
json!({"dash1": 1e308, "gap1": -1e308, "offset": "x", "alignCorners": 3}),
json!({"count": 1e308, "maxCount": 1e308, "distance": -1e308, "spacing": "distance", "target": []}),
json!({"startLength": 1e308, "endPercent": -1e308, "endShape": {}, "mode": 1, "ratio": "x", "handles": "shorter", "length": 1e308}),
json!({"tolerance": 1e308, "protectCorners": "no", "anchors": null, "startPercent": 1e308, "startShape": "arc"}),
];
let ids = effect_catalog().into_iter().map(|e| e.id).filter(|id| id.starts_with("addon.path."));
let stack: Vec<Effect> =
ids.flat_map(|id| junk.iter().map(move |params| Effect { id: id.into(), params: params.clone(), visible: true })).collect();
let (mut s, id) = wave();
s.edit("Junk", |d, _| {
if let Some(n) = d.node_mut(id) {
n.appearance.effects = stack;
}
Ok(())
})
.unwrap();
let saved = vectorcraft_format::save(&s.doc().unwrap().doc, false);
picture_of(&vectorcraft_format::load(&saved).unwrap());
s.execute("effect.expandAppearance", &json!({})).unwrap();
assert!(node(&s, id).appearance.effects.is_empty());
}
#[test]
fn exports_write_the_dashes_as_paths() {
let (mut s, _) = wave();
s.execute("effect.apply", &json!({"effect": "addon.path.dashes", "params": {"dash1": 10, "gap1": 10}})).unwrap();
let v = s.execute("document.export", &json!({"format": "svg"})).unwrap();
let svg = vectorcraft_format::base64_decode(v["dataBase64"].as_str().unwrap()).unwrap();
let moves = svg.iter().filter(|c| **c == b'M').count();
assert!(moves > 5, "the exported art holds the dashes: {moves}");
}
+12
View File
@@ -419,6 +419,18 @@ fn structured_junk() {
"addon.color.filter",
json!({"effect": "addon.color.duotone", "params": {"tones": [{"color": "#ff0000", "swatch": "[Registration]"}], "skipSpot": true}}),
),
("effect.apply", json!({"effect": "addon.path.dashes", "params": {"dash1": 1e308, "gap1": 1e308, "offset": -1e308, "alignCorners": true}})),
("effect.apply", json!({"effect": "addon.path.dashes", "params": {"dash1": -1e308, "gap1": 1e-308, "dash5": 1e-9, "gap5": 0}})),
("effect.apply", json!({"effect": "addon.path.addAnchors", "params": {"target": "x", "spacing": "x", "count": 1e308, "maxCount": 1e308}})),
("effect.apply", json!({"effect": "addon.path.addAnchors", "params": {"spacing": "distance", "distance": 1e-308, "maxCount": -1e308}})),
("effect.apply", json!({"effect": "addon.path.extend", "params": {"startLength": 1e308, "endLength": -1e308, "startPercent": -1e308}})),
("effect.apply", json!({"effect": "addon.path.extend", "params": {"endPercent": 1e308, "endShape": "arc", "startShape": "x"}})),
("effect.apply", json!({"effect": "addon.path.close", "params": {"mode": "x"}})),
("effect.apply", json!({"effect": "addon.path.smooth", "params": {"ratio": 1e308}})),
("effect.apply", json!({"effect": "addon.path.retract", "params": {"handles": "shorter", "length": 1e308}})),
("effect.apply", json!({"effect": "addon.path.reverse"})),
("effect.apply", json!({"effect": "addon.path.simplify", "params": {"tolerance": 1e308, "protectCorners": false}})),
("effect.apply", json!({"effect": "addon.path.split", "params": {"anchors": "x"}})),
("addon.color.printFixes", json!({"inkLimit": 1e308, "overprintBlack": -1e308})),
("addon.color.printFixes", json!({"inkLimit": 1, "overprintBlack": 1, "removeWhiteOverprint": true, "scope": "document", "textOnly": true})),
("addon.color.printFixes", json!({"inkLimit": [300], "scope": 3, "ids": "x"})),
+21
View File
@@ -171,6 +171,27 @@ fn rich_document_roundtrips() {
assert_eq!(v["version"], vectorcraft_format::VERSION);
}
/// A file's Path Effects are its own business: whatever their parameters hold, they load and save
/// as they are (the engine's tests draw them).
#[test]
fn path_effects_with_junk_params_round_trip() {
let mut d = rich_doc();
let mut id = None;
d.walk(|n| id = id.or(n.path_data().map(|_| n.id)));
let id = id.unwrap();
let effects: Vec<vectorcraft_doc::Effect> = [
("addon.path.dashes", json!({"dash1": 1e308, "gap1": -1e308, "offset": "x", "alignCorners": 3})),
("addon.path.addAnchors", json!({"count": 1e308, "maxCount": 1e308, "distance": -1e308, "spacing": "distance", "target": []})),
("addon.path.extend", json!(null)),
]
.into_iter()
.map(|(id, params)| vectorcraft_doc::Effect { id: id.into(), params, visible: true })
.collect();
d.node_mut(id).unwrap().appearance.effects = effects.clone();
let back = load(&save(&d, true)).unwrap();
assert_eq!(back.node(id).unwrap().appearance.effects, effects);
}
#[test]
fn sniff_distinguishes_formats() {
assert!(sniff(&save(&Document::new(1.0, 1.0), false)));
+1 -1
View File
@@ -20,5 +20,5 @@ pub mod segment_reshape;
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_anchors_keep_shape, simplify_keep_shape};
pub use keep_shape::{PT_PER_TOLERANCE, SHARP_CORNER_DEG, remove_anchor_keep_shape, simplify_anchors_keep_shape, simplify_keep_shape};
pub use segment_reshape::{ReshapeMode, reshape_segment};
+3
View File
@@ -13,6 +13,9 @@ 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;
/// The drift (pt) each step of the tolerance the add-ons offer allows.
pub const PT_PER_TOLERANCE: f64 = 0.02;
/// Points sampled along each curve to measure how far two outlines are apart.
const SAMPLES: usize = 8;
+66 -2
View File
@@ -1,5 +1,6 @@
//! Path Precision Tools' geometry: anchor edits for the Anchor Inspector (smooth by the auto-smooth
//! rule, equalise handles, add evenly spaced anchors, close), the readouts the Measure & Inspect
//! rule, equalise handles, add evenly spaced anchors, close), what its live path effects add to them
//! ([`add_anchors`], [`smooth_anchors`], [`split_at_anchors`]), the readouts the Measure & Inspect
//! tool shows over a path, open paths lengthened or shortened at an end ([`extend`]), the places
//! where paths cross ([`crossings`]), anchors slid along their path ([`slide`]) and the Parametric
//! Shape Tool's shapes ([`shapes`]).
@@ -14,7 +15,7 @@ mod tests;
use std::collections::BTreeSet;
use kurbo::{CubicBez, ParamCurve, ParamCurveArclen, ParamCurveNearest, Point, Vec2};
use vectorcraft_geom::{AnchorKind, PathData, SubPath};
use vectorcraft_geom::{Anchor, AnchorKind, PathData, SubPath};
pub use crossings::{MAX_CROSSING_SEGMENTS, crossings, insert_at};
pub use extend::{ExtendMode, extend_length_to, extend_open};
@@ -151,6 +152,69 @@ pub fn add_points(sp: &SubPath, segments: Option<&BTreeSet<usize>>, count: usize
insert_at(sp, &hits)
}
/// The segments [`add_anchors`] adds to.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum SegmentKind {
All,
Curved,
Straight,
}
/// How [`add_anchors`] spaces its anchors along a segment.
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum AnchorSpacing {
/// This many, evenly spaced by length.
Count(usize),
/// One every this many points from the segment's start, none on its end.
Distance(f64),
}
/// `sp` with anchors added on the segments `which` picks, spaced as `spacing` says and at most `cap`
/// on one segment; the shape doesn't change. `None` when that would add more than `room` anchors.
pub fn add_anchors(sp: &SubPath, which: SegmentKind, spacing: AnchorSpacing, cap: usize, mut room: usize) -> Option<SubPath> {
let mut hits = vec![];
for seg in 0..sp.segment_count() {
let picked = match which {
SegmentKind::All => true,
SegmentKind::Curved => !sp.segment_is_line(seg),
SegmentKind::Straight => sp.segment_is_line(seg),
};
let c = sp.segment(seg);
let len = c.arclen(ACCURACY);
if !picked || !(len > TINY && len.is_finite()) {
continue;
}
let (count, step) = match spacing {
AnchorSpacing::Count(n) => (n, len / (n as f64 + 1.0)),
AnchorSpacing::Distance(d) if d > 0.0 => (((len / d).ceil() as usize).saturating_sub(1), d),
AnchorSpacing::Distance(_) => (0, 0.0),
};
let count = count.min(cap);
room = room.checked_sub(count)?;
hits.extend((1..=count).map(|k| (seg, c.inv_arclen(step * k as f64, ACCURACY))));
}
Some(insert_at(sp, &hits).0)
}
/// Make every anchor of `sp` smooth by the auto-smooth rule ([`auto_smooth`]). An open path's ends
/// come last, so each aims at the handle its neighbour ended up with, whichever way the path runs.
pub fn smooth_anchors(sp: &mut SubPath, ratio: f64) {
let (n, closed) = (sp.anchors.len(), sp.closed);
let ends = if closed { vec![] } else { vec![0, n.saturating_sub(1)] };
for i in (0..n).filter(|&i| closed || (i > 0 && i + 1 < n)).chain(ends) {
auto_smooth(sp, i, ratio);
}
}
/// `sp` cut at its `corners`, its `smooth` anchors, or both ([`SubPath::cut_at`]); the shape doesn't
/// change. An anchor is smooth when its handles stand in line, as the outline shows it, whatever
/// kind it was given.
pub fn split_at_anchors(sp: &SubPath, corners: bool, smooth: bool) -> Vec<SubPath> {
let is_smooth = |a: &Anchor| Anchor::with_handles(a.p, a.h_in, a.h_out).kind == AnchorKind::Smooth;
let at = sp.anchors.iter().enumerate().filter(|(_, a)| if is_smooth(a) { smooth } else { corners }).map(|(i, _)| i).collect();
sp.cut_at(&at)
}
/// How [`close`] joins an open subpath's ends.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum CloseMode {
@@ -359,3 +359,78 @@ fn the_slide_a_pointer_asks_for_and_where_the_tangent_stops() {
assert_eq!(stops.len(), 4, "{stops:?}");
assert!(stops.windows(2).all(|w| w[0] < w[1]));
}
#[test]
fn add_anchors_spaces_them_by_count_or_distance() {
let sq = line(&[(0.0, 0.0), (90.0, 0.0), (90.0, 90.0), (0.0, 90.0)], true);
let out = add_anchors(&sq, SegmentKind::All, AnchorSpacing::Count(2), 100, 100).unwrap();
assert_eq!(out.anchors.len(), 12);
assert!(near(out.anchors[1].p.x, 30.0, 1e-6) && near(out.anchors[2].p.x, 60.0, 1e-6));
assert!(out.closed);
let out = add_anchors(&sq, SegmentKind::All, AnchorSpacing::Distance(40.0), 100, 100).unwrap();
assert_eq!(out.anchors.len(), 12, "at 40 and 80, none past the segment");
assert!(near(out.anchors[2].p.x, 80.0, 1e-6));
assert_eq!(add_anchors(&sq, SegmentKind::All, AnchorSpacing::Distance(1.0), 3, 100).unwrap().anchors.len(), 16, "the cap");
assert_eq!(add_anchors(&sq, SegmentKind::All, AnchorSpacing::Distance(0.0), 3, 100).unwrap(), sq);
assert_eq!(add_anchors(&sq, SegmentKind::Curved, AnchorSpacing::Count(1), 3, 100).unwrap(), sq, "no curves");
assert_eq!(add_anchors(&sq, SegmentKind::All, AnchorSpacing::Count(3), 9, 12).unwrap().anchors.len(), 16, "room for all of them");
assert!(add_anchors(&sq, SegmentKind::All, AnchorSpacing::Count(3), 9, 11).is_none(), "no room");
// A curve keeps its shape, and a zero-length segment gets nothing.
let bent = SubPath::new(
vec![
Anchor { h_out: pt(0.0, 50.0), ..Anchor::corner(pt(0.0, 0.0)) },
Anchor { h_in: pt(100.0, 50.0), ..Anchor::corner(pt(100.0, 0.0)) },
Anchor::corner(pt(100.0, 0.0)),
],
false,
);
let out = add_anchors(&bent, SegmentKind::Curved, AnchorSpacing::Count(3), 9, 9).unwrap();
assert_eq!(out.anchors.len(), 6);
assert!(near(length(&out), length(&bent), 1e-4));
assert_eq!(add_anchors(&bent, SegmentKind::Straight, AnchorSpacing::Count(3), 9, 9).unwrap(), bent);
}
#[test]
fn smoothing_every_anchor_reads_the_same_either_way_along_the_path() {
let zig = line(&[(0.0, 0.0), (30.0, 40.0), (70.0, 0.0), (100.0, 50.0)], false);
let mut there = zig.clone();
smooth_anchors(&mut there, SMOOTH_RATIO);
let mut back = zig.clone();
back.reverse();
smooth_anchors(&mut back, SMOOTH_RATIO);
back.reverse();
for (a, b) in there.anchors.iter().zip(&back.anchors) {
assert!(a.h_in.distance(b.h_in) < 1e-9 && a.h_out.distance(b.h_out) < 1e-9, "{a:?} {b:?}");
}
assert!(there.anchors[1].kind == AnchorKind::Smooth && there.anchors[0].has_out() && there.anchors[3].has_in());
// Nothing to smooth.
for mut sp in [SubPath::default(), line(&[(1.0, 1.0)], false), line(&[(1.0, 1.0)], true)] {
let before = sp.clone();
smooth_anchors(&mut sp, SMOOTH_RATIO);
assert_eq!(sp, before);
}
}
#[test]
fn splitting_reads_an_anchors_kind_from_its_handles() {
// Smooth by its handles though it is called a corner, as a path read back from an outline is.
let run = SubPath::new(
vec![
Anchor::corner(pt(0.0, 0.0)),
Anchor { h_in: pt(40.0, 0.0), h_out: pt(60.0, 0.0), ..Anchor::corner(pt(50.0, 0.0)) },
Anchor::corner(pt(100.0, 0.0)),
Anchor::corner(pt(100.0, 50.0)),
],
false,
);
assert_eq!(split_at_anchors(&run, false, true).len(), 2);
assert_eq!(split_at_anchors(&run, true, false).len(), 2);
assert_eq!(split_at_anchors(&run, true, true).len(), 3);
assert_eq!(split_at_anchors(&run, false, false), std::slice::from_ref(&run));
let pieces = split_at_anchors(&run, true, true);
assert!(near(pieces.iter().map(length).sum::<f64>(), length(&run), 1e-6), "the shape stays");
// A closed subpath opens at its first cut.
let sq = line(&[(0.0, 0.0), (10.0, 0.0), (10.0, 10.0)], true);
assert_eq!(split_at_anchors(&sq, true, false).len(), 3);
assert!(split_at_anchors(&sq, true, false).iter().all(|p| !p.closed));
}
+14
View File
@@ -30,6 +30,20 @@ pub const KIND: &str = "pathToolOptions";
const CLOSE_MODES: [(&str, &str); 3] = [("handles", "Keep Handles"), ("straight", "Straight"), ("smooth", "Smooth")];
const LABEL_W: f32 = 64.0;
/// The parameters of the add-on's Path Effects that pick one of some values, as (label, value).
pub(crate) fn effect_choices(effect: &str, key: &str) -> Option<&'static [(&'static str, &'static str)]> {
use vectorcraft_effects::addons::path as fx;
match (effect, key) {
(fx::ADD_ANCHORS, "target") => Some(&[("All", "all"), ("Curved", "curved"), ("Straight", "straight")]),
(fx::ADD_ANCHORS, "spacing") => Some(&[("By Count", "count"), ("By Distance", "distance")]),
(fx::EXTEND, "startShape" | "endShape") => Some(&[("Straight", "straight"), ("Arc", "arc")]),
(fx::CLOSE, "mode") => Some(&[("Straight", "straight"), ("Smooth", "smooth")]),
(fx::RETRACT, "handles") => Some(&[("All", "all"), ("Shorter Than Length", "shorter")]),
(fx::SPLIT, "anchors") => Some(&[("Corner", "corner"), ("Smooth", "smooth"), ("Both", "both")]),
_ => None,
}
}
/// Object › Plug-ins › Path Precision Tools.
pub fn menu() -> Vec<Item> {
let tool = |label, id: &str| Item::Cmd(label, "tool.select", json!({ "tool": id }));
@@ -161,3 +161,51 @@ fn the_corner_section_shows_the_tools_values_and_applies_them_to_the_selection()
assert_eq!(app.session.doc().unwrap().doc.node(id).unwrap().path_data().unwrap().anchor_count(), 6);
assert!(painted_text(&mut app, show).contains("Corners selected: 2"), "still selected");
}
#[test]
fn effect_path_effects_lists_the_live_effects_and_their_dialogs_pick_from_their_values() {
let mut app = app();
let entries = menus::menu_entries(&app);
let at = |label: &str| entries.iter().find(|e| e.path == ["Effect", "Path Effects"] && e.label == label);
for label in [
"Dashes to Geometry…",
"Add Anchors…",
"Extend Path…",
"Close Path…",
"Smooth Anchors…",
"Retract Handles…",
"Simplify Anchors…",
"Split at Anchors…",
] {
assert_eq!(at(label).expect(label).command.as_deref(), Some("effect.dialog"), "{label}");
}
// No options: applied at once.
assert_eq!(at("Reverse Direction").expect("listed").command.as_deref(), Some("effect.apply"));
assert_eq!(entries.iter().filter(|e| e.path == ["Effect", "Path Effects"]).count(), 9);
// The dialog opens with the effect's parameters, each choice on a list.
app.run("shape.rectangle", json!({"x": 10, "y": 10, "width": 40, "height": 40})).unwrap();
menus::invoke(&mut app, "effect.dialog", json!({"effect": "addon.path.addAnchors"}));
assert_eq!(app.ui.dialog.as_ref().map(|d| d.kind.as_str()), Some("effect"));
let text = painted_text(&mut app, |app, ui| crate::dialogs::show(app, ui.ctx()));
assert!(text.contains("Add Anchors") && text.contains("Max Count:") && text.contains("All"), "{text}");
app.ui.dialog = None;
let _ = app.session.cancel_interaction();
menus::invoke(&mut app, "effect.dialog", json!({"effect": "addon.path.dashes"}));
let text = painted_text(&mut app, |app, ui| crate::dialogs::show(app, ui.ctx()));
assert!(text.contains("Dash 1:") && text.contains("Gap 5:") && text.contains("Align to Corners and Ends:"), "{text}");
for (effect, key) in [
("addon.path.addAnchors", "target"),
("addon.path.addAnchors", "spacing"),
("addon.path.extend", "startShape"),
("addon.path.extend", "endShape"),
("addon.path.close", "mode"),
("addon.path.retract", "handles"),
("addon.path.split", "anchors"),
] {
let doc = vectorcraft_effects::effect_info(effect).unwrap();
let options = super::effect_choices(effect, key).expect(key);
let default = doc.defaults[key].as_str().unwrap();
assert!(options.iter().any(|(_, value)| *value == default), "{effect} {key}");
assert!(options.iter().all(|(_, value)| doc.params.contains(&format!("\"{value}\""))), "{effect} {key}");
}
}
+1 -1
View File
@@ -329,7 +329,7 @@ fn choices(effect: &str, key: &str) -> Option<form::Choices> {
("stylize.dropShadow", "shade") => Some(SHADES),
("path.offsetPath", "joins") => Some(&[("Miter", "miter"), ("Round", "round"), ("Bevel", "bevel")]),
("distort.roughen" | "distort.zigZag", "points") => Some(&[("Smooth", "smooth"), ("Corner", "corner")]),
_ => None,
_ => crate::addons::pathtools::effect_choices(effect, key),
}
}
+11
View File
@@ -647,6 +647,17 @@ pub(crate) fn humanize(k: &str) -> String {
"Sy" => "Vertical %".into(),
"Radius1" => "Radius 1".into(),
"Radius2" => "Radius 2".into(),
"Dash1" => "Dash 1:".into(),
"Dash2" => "Dash 2:".into(),
"Dash3" => "Dash 3:".into(),
"Dash4" => "Dash 4:".into(),
"Dash5" => "Dash 5:".into(),
"Gap1" => "Gap 1:".into(),
"Gap2" => "Gap 2:".into(),
"Gap3" => "Gap 3:".into(),
"Gap4" => "Gap 4:".into(),
"Gap5" => "Gap 5:".into(),
"Align Corners" => "Align to Corners and Ends:".into(),
"Max Radius" => "Max. Radius:".into(),
"Blur Mode" => "Mode:".into(),
"Channel1" => "Channel 1:".into(),
+41
View File
@@ -5828,3 +5828,44 @@ breakText Glyphs Глифи
@msg `by` must be lines, paragraphs, words or glyphs, not {other} `by` трябва да е lines, paragraphs, words или glyphs, а не {other}
@msg `kind` must be "line" or "paragraph", not {other} `kind` трябва да е "line" или "paragraph", а не {other}
@msg `align` must be "left", "center" or "right", not {other} `align` трябва да е "left", "center" или "right", а не {other}
# Path Precision Tools add-on: Path Effects (Effect › Path Effects).
Mode Режим
Path Effects Ефекти на пътя
Reverse Direction Обръщане на посоката
Align to Corners and Ends Подравняване към ъглите и краищата
Count Брой
Max Count Максимален брой
Start Length Дължина в началото
Start Percent Процент в началото
Start Shape Форма в началото
End Length Дължина в края
End Percent Процент в края
End Shape Форма в края
By Count По брой
By Distance По разстояние
Both И двете
Shorter Than Length По-къси от дължината
Dashes to Geometry Тирета в геометрия
Dashes to Geometry… Тирета в геометрия…
Add Anchors Добавяне на опорни точки
Add Anchors… Добавяне на опорни точки…
Smooth Anchors Заглаждане на опорните точки
Smooth Anchors… Заглаждане на опорните точки…
Simplify Anchors Опростяване на опорните точки
Simplify Anchors… Опростяване на опорните точки…
Split at Anchors Разделяне в опорните точки
Split at Anchors… Разделяне в опорните точки…
Extend Path… Удължаване на път…
Close Path… Затваряне на пътя…
Retract Handles… Прибиране на дръжките…
Dash 1 Тире 1
Gap 1 Разстояние 1
Dash 2 Тире 2
Gap 2 Разстояние 2
Dash 3 Тире 3
Gap 3 Разстояние 3
Dash 4 Тире 4
Gap 4 Разстояние 4
Dash 5 Тире 5
Gap 5 Разстояние 5
Can't render this file because it contains an unexpected character in line 2 and column 73.
+42
View File
@@ -5852,6 +5852,48 @@ swatch Yellow Green Žuto-zelena
Position: Položaj:
Font Size: Veličina fonta:
shape Open Otvoren
# Path Precision Tools add-on: Path Effects (Effect › Path Effects).
Retract Handles Uvuci ručke
Curved Zakrivljeno
Path Effects Efekti putanje
Reverse Direction Obrni smjer
Align to Corners and Ends Poravnaj s uglovima i krajevima
Count Broj
Max Count Najveći broj
Start Length Dužina na početku
Start Percent Postotak na početku
Start Shape Oblik na početku
End Length Dužina na kraju
End Percent Postotak na kraju
End Shape Oblik na kraju
By Count Po broju
By Distance Po udaljenosti
Both Oba
Shorter Than Length Kraće od dužine
Dashes to Geometry Crtice u geometriju
Dashes to Geometry… Crtice u geometriju…
Add Anchors Dodaj sidra
Add Anchors… Dodaj sidra…
Smooth Anchors Zagladi sidra
Smooth Anchors… Zagladi sidra…
Simplify Anchors Pojednostavi sidra
Simplify Anchors… Pojednostavi sidra…
Split at Anchors Razdvoji na sidrima
Split at Anchors… Razdvoji na sidrima…
Extend Path… Produži putanju…
Close Path… Zatvori putanju…
Retract Handles… Uvuci ručke…
Dash 1 Crtica 1
Gap 1 Razmak 1
Dash 2 Crtica 2
Gap 2 Razmak 2
Dash 3 Crtica 3
Gap 3 Razmak 3
Dash 4 Crtica 4
Gap 4 Razmak 4
Dash 5 Crtica 5
Gap 5 Razmak 5
# Path Precision Tools add-on: the Corner Tool and Slide Anchor Along Path.
Corner Tool Alat za uglove
Slide Anchor Along Path Klizanje sidra po putanji
Can't render this file because it contains an unexpected character in line 2 and column 73.
+40
View File
@@ -5929,3 +5929,43 @@ breakText Glyphs Glifs
@msg `by` must be lines, paragraphs, words or glyphs, not {other} `by` ha de ser lines, paragraphs, words o glyphs, no {other}
@msg `kind` must be "line" or "paragraph", not {other} `kind` ha de ser "line" o "paragraph", no {other}
@msg `align` must be "left", "center" or "right", not {other} `align` ha de ser "left", "center" o "right", no {other}
# Path Precision Tools add-on: Path Effects (Effect › Path Effects).
Path Effects Efectes de traçat
Reverse Direction Inverteix la direcció
Align to Corners and Ends Alinea a les cantonades i als extrems
Count Nombre
Max Count Nombre màxim
Start Length Longitud a l’inici
Start Percent Percentatge a l’inici
Start Shape Forma a l’inici
End Length Longitud al final
End Percent Percentatge al final
End Shape Forma al final
By Count Per nombre
By Distance Per distància
Both Tots dos
Shorter Than Length Més curts que la longitud
Dashes to Geometry Traços a geometria
Dashes to Geometry… Traços a geometria…
Add Anchors Afegeix punts d’ancoratge
Add Anchors… Afegeix punts d’ancoratge…
Smooth Anchors Suavitza els punts d’ancoratge
Smooth Anchors… Suavitza els punts d’ancoratge…
Simplify Anchors Simplifica els punts d’ancoratge
Simplify Anchors… Simplifica els punts d’ancoratge…
Split at Anchors Divideix als punts d’ancoratge
Split at Anchors… Divideix als punts d’ancoratge…
Extend Path… Allarga el traçat…
Close Path… Tanca el traçat…
Retract Handles… Retrau les nanses…
Dash 1 Guió 1
Gap 1 Separació 1
Dash 2 Guió 2
Gap 2 Separació 2
Dash 3 Guió 3
Gap 3 Separació 3
Dash 4 Guió 4
Gap 4 Separació 4
Dash 5 Guió 5
Gap 5 Separació 5
Can't render this file because it contains an unexpected character in line 2 and column 73.
+40
View File
@@ -5938,3 +5938,43 @@ breakText Glyphs Glyfy
@msg `by` must be lines, paragraphs, words or glyphs, not {other} `by` musí být lines, paragraphs, words nebo glyphs, ne {other}
@msg `kind` must be "line" or "paragraph", not {other} `kind` musí být "line" nebo "paragraph", ne {other}
@msg `align` must be "left", "center" or "right", not {other} `align` musí být "left", "center" nebo "right", ne {other}
# Path Precision Tools add-on: Path Effects (Effect › Path Effects).
Path Effects Efekty cesty
Reverse Direction Obrátit směr
Align to Corners and Ends Zarovnat k rohům a koncům
Count Počet
Max Count Maximální počet
Start Length Délka na začátku
Start Percent Procenta na začátku
Start Shape Tvar na začátku
End Length Délka na konci
End Percent Procenta na konci
End Shape Tvar na konci
By Count Podle počtu
By Distance Podle vzdálenosti
Both Obojí
Shorter Than Length Kratší než délka
Dashes to Geometry Čárky na geometrii
Dashes to Geometry… Čárky na geometrii…
Add Anchors Přidat kotevní body
Add Anchors… Přidat kotevní body…
Smooth Anchors Vyhladit kotevní body
Smooth Anchors… Vyhladit kotevní body…
Simplify Anchors Zjednodušit kotevní body
Simplify Anchors… Zjednodušit kotevní body…
Split at Anchors Rozdělit v kotevních bodech
Split at Anchors… Rozdělit v kotevních bodech…
Extend Path… Prodloužit cestu…
Close Path… Uzavřít cestu…
Retract Handles… Zasunout úchyty…
Dash 1 Čárka 1
Gap 1 Mezera 1
Dash 2 Čárka 2
Gap 2 Mezera 2
Dash 3 Čárka 3
Gap 3 Mezera 3
Dash 4 Čárka 4
Gap 4 Mezera 4
Dash 5 Čárka 5
Gap 5 Mezera 5
Can't render this file because it contains an unexpected character in line 2 and column 73.
+40
View File
@@ -5843,3 +5843,43 @@ breakText Glyphs Glyffer
@msg `by` must be lines, paragraphs, words or glyphs, not {other} `by` skal være lines, paragraphs, words eller glyphs, ikke {other}
@msg `kind` must be "line" or "paragraph", not {other} `kind` skal være "line" eller "paragraph", ikke {other}
@msg `align` must be "left", "center" or "right", not {other} `align` skal være "left", "center" eller "right", ikke {other}
# Path Precision Tools add-on: Path Effects (Effect › Path Effects).
Path Effects Kurveeffekter
Reverse Direction Omvend retning
Align to Corners and Ends Juster til hjørner og ender
Count Antal
Max Count Største antal
Start Length Startlængde
Start Percent Startprocent
Start Shape Startform
End Length Slutlængde
End Percent Slutprocent
End Shape Slutform
By Count Efter antal
By Distance Efter afstand
Both Begge
Shorter Than Length Kortere end længden
Dashes to Geometry Streger til geometri
Dashes to Geometry… Streger til geometri…
Add Anchors Tilføj ankerpunkter
Add Anchors… Tilføj ankerpunkter…
Smooth Anchors Udglat ankerpunkter
Smooth Anchors… Udglat ankerpunkter…
Simplify Anchors Forenkl ankerpunkter
Simplify Anchors… Forenkl ankerpunkter…
Split at Anchors Opdel ved ankerpunkter
Split at Anchors… Opdel ved ankerpunkter…
Extend Path… Forlæng sti…
Close Path… Luk sti…
Retract Handles… Træk håndtag ind…
Dash 1 Streg 1
Gap 1 Mellemrum 1
Dash 2 Streg 2
Gap 2 Mellemrum 2
Dash 3 Streg 3
Gap 3 Mellemrum 3
Dash 4 Streg 4
Gap 4 Mellemrum 4
Dash 5 Streg 5
Gap 5 Mellemrum 5
Can't render this file because it contains an unexpected character in line 2 and column 73.
+41
View File
@@ -5523,3 +5523,44 @@ breakText Glyphs Glyphen
@msg `by` must be lines, paragraphs, words or glyphs, not {other} `by` muss lines, paragraphs, words oder glyphs sein, nicht {other}
@msg `kind` must be "line" or "paragraph", not {other} `kind` muss "line" oder "paragraph" sein, nicht {other}
@msg `align` must be "left", "center" or "right", not {other} `align` muss "left", "center" oder "right" sein, nicht {other}
# Path Precision Tools add-on: Path Effects (Effect › Path Effects).
Path Effects Pfadeffekte
Reverse Direction Richtung umkehren
Align to Corners and Ends An Ecken und Enden ausrichten
Count Anzahl
Max Count Höchstanzahl
Start Length Startlänge
Start Percent Start in Prozent
Start Shape Startform
End Length Endlänge
End Percent Ende in Prozent
End Shape Endform
By Count Nach Anzahl
By Distance Nach Abstand
Both Beide
Shorter Than Length Kürzer als Länge
Anchors Ankerpunkte
Dashes to Geometry Striche in Geometrie
Dashes to Geometry… Striche in Geometrie…
Add Anchors Ankerpunkte hinzufügen
Add Anchors… Ankerpunkte hinzufügen…
Smooth Anchors Ankerpunkte glätten
Smooth Anchors… Ankerpunkte glätten…
Simplify Anchors Ankerpunkte vereinfachen
Simplify Anchors… Ankerpunkte vereinfachen…
Split at Anchors An Ankerpunkten teilen
Split at Anchors… An Ankerpunkten teilen…
Extend Path… Pfad verlängern…
Close Path… Pfad schließen…
Retract Handles… Griffe einziehen…
Dash 1 Strich 1
Gap 1 Lücke 1
Dash 2 Strich 2
Gap 2 Lücke 2
Dash 3 Strich 3
Gap 3 Lücke 3
Dash 4 Strich 4
Gap 4 Lücke 4
Dash 5 Strich 5
Gap 5 Lücke 5
Can't render this file because it contains an unexpected character in line 2 and column 73.
+41
View File
@@ -5890,3 +5890,44 @@ breakText Glyphs Γλύφοι
@msg `by` must be lines, paragraphs, words or glyphs, not {other} το `by` πρέπει να είναι lines, paragraphs, words ή glyphs, όχι {other}
@msg `kind` must be "line" or "paragraph", not {other} το `kind` πρέπει να είναι "line" ή "paragraph", όχι {other}
@msg `align` must be "left", "center" or "right", not {other} το `align` πρέπει να είναι "left", "center" ή "right", όχι {other}
# Path Precision Tools add-on: Path Effects (Effect › Path Effects).
Mode Λειτουργία
Path Effects Εφέ διαδρομής
Reverse Direction Αντιστροφή κατεύθυνσης
Align to Corners and Ends Στοίχιση σε γωνίες και άκρα
Count Πλήθος
Max Count Μέγιστο πλήθος
Start Length Μήκος αρχής
Start Percent Ποσοστό αρχής
Start Shape Σχήμα αρχής
End Length Μήκος τέλους
End Percent Ποσοστό τέλους
End Shape Σχήμα τέλους
By Count Κατά πλήθος
By Distance Κατά απόσταση
Both Και τα δύο
Shorter Than Length Μικρότερες από το μήκος
Dashes to Geometry Παύλες σε γεωμετρία
Dashes to Geometry… Παύλες σε γεωμετρία…
Add Anchors Προσθήκη σημείων αγκύρωσης
Add Anchors… Προσθήκη σημείων αγκύρωσης…
Smooth Anchors Εξομάλυνση σημείων αγκύρωσης
Smooth Anchors… Εξομάλυνση σημείων αγκύρωσης…
Simplify Anchors Απλοποίηση σημείων αγκύρωσης
Simplify Anchors… Απλοποίηση σημείων αγκύρωσης…
Split at Anchors Διαχωρισμός στα σημεία αγκύρωσης
Split at Anchors… Διαχωρισμός στα σημεία αγκύρωσης…
Extend Path… Επέκταση διαδρομής…
Close Path… Κλείσιμο διαδρομής…
Retract Handles… Σύμπτυξη λαβών…
Dash 1 Παύλα 1
Gap 1 Διάκενο 1
Dash 2 Παύλα 2
Gap 2 Διάκενο 2
Dash 3 Παύλα 3
Gap 3 Διάκενο 3
Dash 4 Παύλα 4
Gap 4 Διάκενο 4
Dash 5 Παύλα 5
Gap 5 Διάκενο 5
Can't render this file because it contains an unexpected character in line 164 and column 5.
+40
View File
@@ -5881,3 +5881,43 @@ breakText Glyphs Glifos
@msg `by` must be lines, paragraphs, words or glyphs, not {other} `by` debe ser lines, paragraphs, words o glyphs, no {other}
@msg `kind` must be "line" or "paragraph", not {other} `kind` debe ser "line" o "paragraph", no {other}
@msg `align` must be "left", "center" or "right", not {other} `align` debe ser "left", "center" o "right", no {other}
# Path Precision Tools add-on: Path Effects (Effect › Path Effects).
Path Effects Efectos de trazado
Reverse Direction Invertir dirección
Align to Corners and Ends Alinear con esquinas y extremos
Count Cantidad
Max Count Cantidad máxima
Start Length Longitud inicial
Start Percent Porcentaje inicial
Start Shape Forma inicial
End Length Longitud final
End Percent Porcentaje final
End Shape Forma final
By Count Por cantidad
By Distance Por distancia
Both Ambos
Shorter Than Length Más cortos que la longitud
Dashes to Geometry Guiones a geometría
Dashes to Geometry… Guiones a geometría…
Add Anchors Agregar puntos de ancla
Add Anchors… Agregar puntos de ancla…
Smooth Anchors Suavizar puntos de ancla
Smooth Anchors… Suavizar puntos de ancla…
Simplify Anchors Simplificar puntos de ancla
Simplify Anchors… Simplificar puntos de ancla…
Split at Anchors Dividir en puntos de ancla
Split at Anchors… Dividir en puntos de ancla…
Extend Path… Prolongar trazado…
Close Path… Cerrar trazado…
Retract Handles… Retraer manejadores…
Dash 1 Guion 1
Gap 1 Hueco 1
Dash 2 Guion 2
Gap 2 Hueco 2
Dash 3 Guion 3
Gap 3 Hueco 3
Dash 4 Guion 4
Gap 4 Hueco 4
Dash 5 Guion 5
Gap 5 Hueco 5
Can't render this file because it contains an unexpected character in line 2 and column 73.
+40
View File
@@ -5969,3 +5969,43 @@ breakText Glyphs Merkit
@msg `by` must be lines, paragraphs, words or glyphs, not {other} `by` on oltava lines, paragraphs, words tai glyphs, ei {other}
@msg `kind` must be "line" or "paragraph", not {other} `kind` on oltava "line" tai "paragraph", ei {other}
@msg `align` must be "left", "center" or "right", not {other} `align` on oltava "left", "center" tai "right", ei {other}
# Path Precision Tools add-on: Path Effects (Effect › Path Effects).
Path Effects Polkutehosteet
Reverse Direction Käännä suunta
Align to Corners and Ends Tasaa kulmiin ja päihin
Count Määrä
Max Count Enimmäismäärä
Start Length Alun pituus
Start Percent Alun prosentti
Start Shape Alun muoto
End Length Lopun pituus
End Percent Lopun prosentti
End Shape Lopun muoto
By Count Määrän mukaan
By Distance Etäisyyden mukaan
Both Molemmat
Shorter Than Length Pituutta lyhyemmät
Dashes to Geometry Katkoviivat geometriaksi
Dashes to Geometry… Katkoviivat geometriaksi…
Add Anchors Lisää ankkuripisteitä
Add Anchors… Lisää ankkuripisteitä…
Smooth Anchors Pyöristä ankkuripisteet
Smooth Anchors… Pyöristä ankkuripisteet…
Simplify Anchors Yksinkertaista ankkuripisteet
Simplify Anchors… Yksinkertaista ankkuripisteet…
Split at Anchors Jaa ankkuripisteistä
Split at Anchors… Jaa ankkuripisteistä…
Extend Path… Jatka polkua…
Close Path… Sulje polku…
Retract Handles… Vedä kahvat sisään…
Dash 1 Viiva 1
Gap 1 Väli 1
Dash 2 Viiva 2
Gap 2 Väli 2
Dash 3 Viiva 3
Gap 3 Väli 3
Dash 4 Viiva 4
Gap 4 Väli 4
Dash 5 Viiva 5
Gap 5 Väli 5
Can't render this file because it contains an unexpected character in line 2 and column 73.
+40
View File
@@ -5937,3 +5937,43 @@ breakText Glyphs Glyphes
@msg `by` must be lines, paragraphs, words or glyphs, not {other} `by` doit valoir lines, paragraphs, words ou glyphs, et non {other}
@msg `kind` must be "line" or "paragraph", not {other} `kind` doit valoir "line" ou "paragraph", et non {other}
@msg `align` must be "left", "center" or "right", not {other} `align` doit valoir "left", "center" ou "right", et non {other}
# Path Precision Tools add-on: Path Effects (Effect › Path Effects).
Path Effects Effets de tracé
Reverse Direction Inverser le sens
Align to Corners and Ends Aligner sur les angles et extrémités
Count Nombre
Max Count Nombre maximal
Start Length Longueur au début
Start Percent Pourcentage au début
Start Shape Forme au début
End Length Longueur à la fin
End Percent Pourcentage à la fin
End Shape Forme à la fin
By Count Par nombre
By Distance Par distance
Both Les deux
Shorter Than Length Plus courtes que la longueur
Dashes to Geometry Tirets en géométrie
Dashes to Geometry… Tirets en géométrie…
Add Anchors Ajouter des points d’ancrage
Add Anchors… Ajouter des points d’ancrage…
Smooth Anchors Lisser les points d’ancrage
Smooth Anchors… Lisser les points d’ancrage…
Simplify Anchors Simplifier les points d’ancrage
Simplify Anchors… Simplifier les points d’ancrage…
Split at Anchors Scinder aux points d’ancrage
Split at Anchors… Scinder aux points d’ancrage…
Extend Path… Prolonger le tracé…
Close Path… Fermer le tracé…
Retract Handles… Rétracter les poignées…
Dash 1 Tiret 1
Gap 1 Écart 1
Dash 2 Tiret 2
Gap 2 Écart 2
Dash 3 Tiret 3
Gap 3 Écart 3
Dash 4 Tiret 4
Gap 4 Écart 4
Dash 5 Tiret 5
Gap 5 Écart 5
Can't render this file because it contains an unexpected character in line 2 and column 73.
+40
View File
@@ -5925,3 +5925,43 @@ breakText Glyphs Glifák
@msg `by` must be lines, paragraphs, words or glyphs, not {other} a `by` értéke lines, paragraphs, words vagy glyphs lehet, nem {other}
@msg `kind` must be "line" or "paragraph", not {other} a `kind` értéke "line" vagy "paragraph" lehet, nem {other}
@msg `align` must be "left", "center" or "right", not {other} az `align` értéke "left", "center" vagy "right" lehet, nem {other}
# Path Precision Tools add-on: Path Effects (Effect › Path Effects).
Path Effects Útvonaleffektusok
Reverse Direction Irány megfordítása
Align to Corners and Ends Igazítás a sarkokhoz és végekhez
Count Darabszám
Max Count Legnagyobb darabszám
Start Length Kezdő hossz
Start Percent Kezdő százalék
Start Shape Kezdő alak
End Length Záró hossz
End Percent Záró százalék
End Shape Záró alak
By Count Darabszám szerint
By Distance Távolság szerint
Both Mindkettő
Shorter Than Length A hossznál rövidebbek
Dashes to Geometry Vonalak geometriává
Dashes to Geometry… Vonalak geometriává…
Add Anchors Horgonypontok hozzáadása
Add Anchors… Horgonypontok hozzáadása…
Smooth Anchors Horgonypontok simítása
Smooth Anchors… Horgonypontok simítása…
Simplify Anchors Horgonypontok egyszerűsítése
Simplify Anchors… Horgonypontok egyszerűsítése…
Split at Anchors Felosztás a horgonypontoknál
Split at Anchors… Felosztás a horgonypontoknál…
Extend Path… Útvonal meghosszabbítása…
Close Path… Útvonal lezárása…
Retract Handles… Fogantyúk behúzása…
Dash 1 Vonal 1
Gap 1 Hézag 1
Dash 2 Vonal 2
Gap 2 Hézag 2
Dash 3 Vonal 3
Gap 3 Hézag 3
Dash 4 Vonal 4
Gap 4 Hézag 4
Dash 5 Vonal 5
Gap 5 Hézag 5
Can't render this file because it contains an unexpected character in line 2 and column 73.
+40
View File
@@ -5843,3 +5843,43 @@ breakText Glyphs Glif
@msg `by` must be lines, paragraphs, words or glyphs, not {other} `by` harus lines, paragraphs, words, atau glyphs, bukan {other}
@msg `kind` must be "line" or "paragraph", not {other} `kind` harus "line" atau "paragraph", bukan {other}
@msg `align` must be "left", "center" or "right", not {other} `align` harus "left", "center", atau "right", bukan {other}
# Path Precision Tools add-on: Path Effects (Effect › Path Effects).
Path Effects Efek jalur
Reverse Direction Balikkan arah
Align to Corners and Ends Sejajarkan ke sudut dan ujung
Count Jumlah
Max Count Jumlah maksimum
Start Length Panjang awal
Start Percent Persen awal
Start Shape Bentuk awal
End Length Panjang akhir
End Percent Persen akhir
End Shape Bentuk akhir
By Count Menurut jumlah
By Distance Menurut jarak
Both Keduanya
Shorter Than Length Lebih pendek dari panjang
Dashes to Geometry Garis putus ke geometri
Dashes to Geometry… Garis putus ke geometri…
Add Anchors Tambahkan titik jangkar
Add Anchors… Tambahkan titik jangkar…
Smooth Anchors Haluskan titik jangkar
Smooth Anchors… Haluskan titik jangkar…
Simplify Anchors Sederhanakan titik jangkar
Simplify Anchors… Sederhanakan titik jangkar…
Split at Anchors Pisahkan di titik jangkar
Split at Anchors… Pisahkan di titik jangkar…
Extend Path… Perpanjang jalur…
Close Path… Tutup jalur…
Retract Handles… Tarik masuk pegangan…
Dash 1 Garis 1
Gap 1 Jarak 1
Dash 2 Garis 2
Gap 2 Jarak 2
Dash 3 Garis 3
Gap 3 Jarak 3
Dash 4 Garis 4
Gap 4 Jarak 4
Dash 5 Garis 5
Gap 5 Jarak 5
Can't render this file because it contains an unexpected character in line 2169 and column 5.
+41
View File
@@ -5557,3 +5557,44 @@ breakText Glyphs Glifi
@msg `by` must be lines, paragraphs, words or glyphs, not {other} `by` deve essere lines, paragraphs, words o glyphs, non {other}
@msg `kind` must be "line" or "paragraph", not {other} `kind` deve essere "line" o "paragraph", non {other}
@msg `align` must be "left", "center" or "right", not {other} `align` deve essere "left", "center" o "right", non {other}
# Path Precision Tools add-on: Path Effects (Effect › Path Effects).
Path Effects Effetti tracciato
Reverse Direction Inverti direzione
Align to Corners and Ends Allinea ad angoli ed estremità
Count Numero
Max Count Numero massimo
Start Length Lunghezza iniziale
Start Percent Percentuale iniziale
Start Shape Forma iniziale
End Length Lunghezza finale
End Percent Percentuale finale
End Shape Forma finale
By Count Per numero
By Distance Per distanza
Both Entrambi
Shorter Than Length Più corte della lunghezza
Anchors Punti di ancoraggio
Dashes to Geometry Tratti in geometria
Dashes to Geometry… Tratti in geometria…
Add Anchors Aggiungi punti di ancoraggio
Add Anchors… Aggiungi punti di ancoraggio…
Smooth Anchors Smussa punti di ancoraggio
Smooth Anchors… Smussa punti di ancoraggio…
Simplify Anchors Semplifica punti di ancoraggio
Simplify Anchors… Semplifica punti di ancoraggio…
Split at Anchors Dividi ai punti di ancoraggio
Split at Anchors… Dividi ai punti di ancoraggio…
Extend Path… Estendi tracciato…
Close Path… Chiudi tracciato…
Retract Handles… Ritira maniglie…
Dash 1 Tratto 1
Gap 1 Spazio 1
Dash 2 Tratto 2
Gap 2 Spazio 2
Dash 3 Tratto 3
Gap 3 Spazio 3
Dash 4 Tratto 4
Gap 4 Spazio 4
Dash 5 Tratto 5
Gap 5 Spazio 5
Can't render this file because it contains an unexpected character in line 2 and column 73.
+41
View File
@@ -5626,3 +5626,44 @@ breakText Glyphs 字形
@msg `by` must be lines, paragraphs, words or glyphs, not {other} `by` は lines、paragraphs、words、glyphs のいずれかにしてください({other} は使用できません)
@msg `kind` must be "line" or "paragraph", not {other} `kind` は "line" または "paragraph" にしてください({other} は使用できません)
@msg `align` must be "left", "center" or "right", not {other} `align` は "left"、"center"、"right" のいずれかにしてください({other} は使用できません)
# Path Precision Tools add-on: Path Effects (Effect › Path Effects).
Path Effects パス効果
Reverse Direction 方向を反転
Align to Corners and Ends コーナーと先端に整列
Count 個数
Max Count 最大個数
Start Length 始点の長さ
Start Percent 始点のパーセント
Start Shape 始点の形状
End Length 終点の長さ
End Percent 終点のパーセント
End Shape 終点の形状
By Count 個数で指定
By Distance 距離で指定
Both 両方
Shorter Than Length 長さより短いもの
Anchors アンカーポイント
Dashes to Geometry 破線をジオメトリに
Dashes to Geometry… 破線をジオメトリに…
Add Anchors アンカーポイントを追加
Add Anchors… アンカーポイントを追加…
Smooth Anchors アンカーポイントをスムーズに
Smooth Anchors… アンカーポイントをスムーズに…
Simplify Anchors アンカーポイントを単純化
Simplify Anchors… アンカーポイントを単純化…
Split at Anchors アンカーポイントで分割
Split at Anchors… アンカーポイントで分割…
Extend Path… パスを延長…
Close Path… パスを閉じる…
Retract Handles… ハンドルを収める…
Dash 1 線分 1
Gap 1 間隔 1
Dash 2 線分 2
Gap 2 間隔 2
Dash 3 線分 3
Gap 3 間隔 3
Dash 4 線分 4
Gap 4 間隔 4
Dash 5 線分 5
Gap 5 間隔 5
Can't render this file because it contains an unexpected character in line 2 and column 73.
+40
View File
@@ -5928,3 +5928,43 @@ breakText Glyphs გლიფები
@msg `by` must be lines, paragraphs, words or glyphs, not {other} `by` უნდა იყოს lines, paragraphs, words ან glyphs და არა {other}
@msg `kind` must be "line" or "paragraph", not {other} `kind` უნდა იყოს "line" ან "paragraph" და არა {other}
@msg `align` must be "left", "center" or "right", not {other} `align` უნდა იყოს "left", "center" ან "right" და არა {other}
# Path Precision Tools add-on: Path Effects (Effect › Path Effects).
Path Effects ტრაექტორიის ეფექტები
Reverse Direction მიმართულების შებრუნება
Align to Corners and Ends კუთხეებთან და ბოლოებთან სწორება
Count რაოდენობა
Max Count მაქსიმალური რაოდენობა
Start Length საწყისი სიგრძე
Start Percent საწყისი პროცენტი
Start Shape საწყისი ფორმა
End Length საბოლოო სიგრძე
End Percent საბოლოო პროცენტი
End Shape საბოლოო ფორმა
By Count რაოდენობით
By Distance მანძილით
Both ორივე
Shorter Than Length სიგრძეზე მოკლე
Dashes to Geometry ტირეები გეომეტრიად
Dashes to Geometry… ტირეები გეომეტრიად…
Add Anchors საყრდენი წერტილების დამატება
Add Anchors… საყრდენი წერტილების დამატება…
Smooth Anchors საყრდენი წერტილების მოგლუვება
Smooth Anchors… საყრდენი წერტილების მოგლუვება…
Simplify Anchors საყრდენი წერტილების გამარტივება
Simplify Anchors… საყრდენი წერტილების გამარტივება…
Split at Anchors დაყოფა საყრდენ წერტილებზე
Split at Anchors… დაყოფა საყრდენ წერტილებზე…
Extend Path… ტრაექტორიის გაგრძელება…
Close Path… ტრაექტორიის შეკვრა…
Retract Handles… სახელურების შეკეცვა…
Dash 1 ტირე 1
Gap 1 შუალედი 1
Dash 2 ტირე 2
Gap 2 შუალედი 2
Dash 3 ტირე 3
Gap 3 შუალედი 3
Dash 4 ტირე 4
Gap 4 შუალედი 4
Dash 5 ტირე 5
Gap 5 შუალედი 5
Can't render this file because it is too large.
+41
View File
@@ -5520,3 +5520,44 @@ breakText Glyphs 글리프
@msg `by` must be lines, paragraphs, words or glyphs, not {other} `by`는 lines, paragraphs, words 또는 glyphs여야 합니다({other}는 사용할 수 없음)
@msg `kind` must be "line" or "paragraph", not {other} `kind`는 "line" 또는 "paragraph"여야 합니다({other}는 사용할 수 없음)
@msg `align` must be "left", "center" or "right", not {other} `align`은 "left", "center" 또는 "right"여야 합니다({other}는 사용할 수 없음)
# Path Precision Tools add-on: Path Effects (Effect › Path Effects).
Path Effects 패스 효과
Reverse Direction 방향 반전
Align to Corners and Ends 모퉁이와 끝에 정렬
Count 개수
Max Count 최대 개수
Start Length 시작 길이
Start Percent 시작 퍼센트
Start Shape 시작 모양
End Length 끝 길이
End Percent 끝 퍼센트
End Shape 끝 모양
By Count 개수 기준
By Distance 거리 기준
Both 둘 다
Shorter Than Length 길이보다 짧은 핸들
Anchors 앵커 포인트
Dashes to Geometry 대시를 도형으로
Dashes to Geometry… 대시를 도형으로…
Add Anchors 앵커 포인트 추가
Add Anchors… 앵커 포인트 추가…
Smooth Anchors 앵커 포인트 매끄럽게
Smooth Anchors… 앵커 포인트 매끄럽게…
Simplify Anchors 앵커 포인트 단순화
Simplify Anchors… 앵커 포인트 단순화…
Split at Anchors 앵커 포인트에서 분할
Split at Anchors… 앵커 포인트에서 분할…
Extend Path… 패스 연장…
Close Path… 패스 닫기…
Retract Handles… 핸들 접기…
Dash 1 대시 1
Gap 1 간격 1
Dash 2 대시 2
Gap 2 간격 2
Dash 3 대시 3
Gap 3 간격 3
Dash 4 대시 4
Gap 4 간격 4
Dash 5 대시 5
Gap 5 간격 5
Can't render this file because it contains an unexpected character in line 2 and column 73.
+40
View File
@@ -5887,3 +5887,43 @@ breakText Glyphs Glyphs
@msg `by` must be lines, paragraphs, words or glyphs, not {other} `by` moet lines, paragraphs, words of glyphs zijn, niet {other}
@msg `kind` must be "line" or "paragraph", not {other} `kind` moet "line" of "paragraph" zijn, niet {other}
@msg `align` must be "left", "center" or "right", not {other} `align` moet "left", "center" of "right" zijn, niet {other}
# Path Precision Tools add-on: Path Effects (Effect › Path Effects).
Path Effects Padeffecten
Reverse Direction Richting omkeren
Align to Corners and Ends Uitlijnen op hoeken en uiteinden
Count Aantal
Max Count Maximumaantal
Start Length Beginlengte
Start Percent Beginpercentage
Start Shape Beginvorm
End Length Eindlengte
End Percent Eindpercentage
End Shape Eindvorm
By Count Op aantal
By Distance Op afstand
Both Beide
Shorter Than Length Korter dan lengte
Dashes to Geometry Streepjes naar geometrie
Dashes to Geometry… Streepjes naar geometrie…
Add Anchors Ankerpunten toevoegen
Add Anchors… Ankerpunten toevoegen…
Smooth Anchors Ankerpunten gladmaken
Smooth Anchors… Ankerpunten gladmaken…
Simplify Anchors Ankerpunten vereenvoudigen
Simplify Anchors… Ankerpunten vereenvoudigen…
Split at Anchors Splitsen bij ankerpunten
Split at Anchors… Splitsen bij ankerpunten…
Extend Path… Pad verlengen…
Close Path… Pad sluiten…
Retract Handles… Grepen intrekken…
Dash 1 Streepje 1
Gap 1 Tussenruimte 1
Dash 2 Streepje 2
Gap 2 Tussenruimte 2
Dash 3 Streepje 3
Gap 3 Tussenruimte 3
Dash 4 Streepje 4
Gap 4 Tussenruimte 4
Dash 5 Streepje 5
Gap 5 Tussenruimte 5
Can't render this file because it contains an unexpected character in line 2 and column 73.
+41
View File
@@ -5793,3 +5793,44 @@ breakText Glyphs Glify
@msg `by` must be lines, paragraphs, words or glyphs, not {other} `by` musi mieć wartość lines, paragraphs, words lub glyphs, a nie {other}
@msg `kind` must be "line" or "paragraph", not {other} `kind` musi mieć wartość "line" lub "paragraph", a nie {other}
@msg `align` must be "left", "center" or "right", not {other} `align` musi mieć wartość "left", "center" lub "right", a nie {other}
# Path Precision Tools add-on: Path Effects (Effect › Path Effects).
Path Effects Efekty ścieżki
Reverse Direction Odwróć kierunek
Align to Corners and Ends Wyrównaj do narożników i końców
Count Liczba
Max Count Maksymalna liczba
Start Length Długość na początku
Start Percent Procent na początku
Start Shape Kształt na początku
End Length Długość na końcu
End Percent Procent na końcu
End Shape Kształt na końcu
By Count Według liczby
By Distance Według odległości
Both Oba
Shorter Than Length Krótsze niż długość
Anchors Punkty kontrolne
Dashes to Geometry Kreski jako geometria
Dashes to Geometry… Kreski jako geometria…
Add Anchors Dodaj punkty kontrolne
Add Anchors… Dodaj punkty kontrolne…
Smooth Anchors Wygładź punkty kontrolne
Smooth Anchors… Wygładź punkty kontrolne…
Simplify Anchors Uprość punkty kontrolne
Simplify Anchors… Uprość punkty kontrolne…
Split at Anchors Podziel w punktach kontrolnych
Split at Anchors… Podziel w punktach kontrolnych…
Extend Path… Przedłuż ścieżkę…
Close Path… Zamknij ścieżkę…
Retract Handles… Schowaj uchwyty…
Dash 1 Kreska 1
Gap 1 Odstęp 1
Dash 2 Kreska 2
Gap 2 Odstęp 2
Dash 3 Kreska 3
Gap 3 Odstęp 3
Dash 4 Kreska 4
Gap 4 Odstęp 4
Dash 5 Kreska 5
Gap 5 Odstęp 5
Can't render this file because it contains an unexpected character in line 2 and column 73.
+42
View File
@@ -5856,3 +5856,45 @@ breakText Glyphs Glifos
@msg `by` must be lines, paragraphs, words or glyphs, not {other} `by` deve ser lines, paragraphs, words ou glyphs, não {other}
@msg `kind` must be "line" or "paragraph", not {other} `kind` deve ser "line" ou "paragraph", não {other}
@msg `align` must be "left", "center" or "right", not {other} `align` deve ser "left", "center" ou "right", não {other}
# Path Precision Tools add-on: Path Effects (Effect › Path Effects).
Distance Distância
Mode Modo
Path Effects Efeitos de Caminho
Reverse Direction Inverter Direção
Align to Corners and Ends Alinhar aos Cantos e Extremidades
Count Quantidade
Max Count Quantidade Máxima
Start Length Comprimento Inicial
Start Percent Porcentagem Inicial
Start Shape Forma Inicial
End Length Comprimento Final
End Percent Porcentagem Final
End Shape Forma Final
By Count Por Quantidade
By Distance Por Distância
Both Ambos
Shorter Than Length Mais Curtas que o Comprimento
Dashes to Geometry Traços em Geometria
Dashes to Geometry… Traços em Geometria…
Add Anchors Adicionar Pontos de Ancoragem
Add Anchors… Adicionar Pontos de Ancoragem…
Smooth Anchors Suavizar Pontos de Ancoragem
Smooth Anchors… Suavizar Pontos de Ancoragem…
Simplify Anchors Simplificar Pontos de Ancoragem
Simplify Anchors… Simplificar Pontos de Ancoragem…
Split at Anchors Dividir nos Pontos de Ancoragem
Split at Anchors… Dividir nos Pontos de Ancoragem…
Extend Path… Estender Caminho…
Close Path… Fechar Caminho…
Retract Handles… Recolher Alças…
Dash 1 Traço 1
Gap 1 Lacuna 1
Dash 2 Traço 2
Gap 2 Lacuna 2
Dash 3 Traço 3
Gap 3 Lacuna 3
Dash 4 Traço 4
Gap 4 Lacuna 4
Dash 5 Traço 5
Gap 5 Lacuna 5
Can't render this file because it contains an unexpected character in line 6 and column 47.
+40
View File
@@ -5916,3 +5916,43 @@ breakText Glyphs Глифы
@msg `by` must be lines, paragraphs, words or glyphs, not {other} `by` должен быть lines, paragraphs, words или glyphs, а не {other}
@msg `kind` must be "line" or "paragraph", not {other} `kind` должен быть "line" или "paragraph", а не {other}
@msg `align` must be "left", "center" or "right", not {other} `align` должен быть "left", "center" или "right", а не {other}
# Path Precision Tools add-on: Path Effects (Effect › Path Effects).
Path Effects Эффекты контура
Reverse Direction Обратить направление
Align to Corners and Ends Выровнять по углам и концам
Count Количество
Max Count Наибольшее количество
Start Length Длина в начале
Start Percent Проценты в начале
Start Shape Форма в начале
End Length Длина в конце
End Percent Проценты в конце
End Shape Форма в конце
By Count По количеству
By Distance По расстоянию
Both Оба
Shorter Than Length Короче длины
Dashes to Geometry Штрихи в геометрию
Dashes to Geometry… Штрихи в геометрию…
Add Anchors Добавить опорные точки
Add Anchors… Добавить опорные точки…
Smooth Anchors Сгладить опорные точки
Smooth Anchors… Сгладить опорные точки…
Simplify Anchors Упростить опорные точки
Simplify Anchors… Упростить опорные точки…
Split at Anchors Разрезать в опорных точках
Split at Anchors… Разрезать в опорных точках…
Extend Path… Продлить контур…
Close Path… Замкнуть контур…
Retract Handles… Убрать маркеры…
Dash 1 Штрих 1
Gap 1 Зазор 1
Dash 2 Штрих 2
Gap 2 Зазор 2
Dash 3 Штрих 3
Gap 3 Зазор 3
Dash 4 Штрих 4
Gap 4 Зазор 4
Dash 5 Штрих 5
Gap 5 Зазор 5
Can't render this file because it contains an unexpected character in line 2 and column 73.
+40
View File
@@ -5994,3 +5994,43 @@ breakText Glyphs Glyfer
@msg `by` must be lines, paragraphs, words or glyphs, not {other} `by` måste vara lines, paragraphs, words eller glyphs, inte {other}
@msg `kind` must be "line" or "paragraph", not {other} `kind` måste vara "line" eller "paragraph", inte {other}
@msg `align` must be "left", "center" or "right", not {other} `align` måste vara "left", "center" eller "right", inte {other}
# Path Precision Tools add-on: Path Effects (Effect › Path Effects).
Path Effects Baneffekter
Reverse Direction Vänd riktning
Align to Corners and Ends Justera mot hörn och ändar
Count Antal
Max Count Högsta antal
Start Length Startlängd
Start Percent Startprocent
Start Shape Startform
End Length Slutlängd
End Percent Slutprocent
End Shape Slutform
By Count Efter antal
By Distance Efter avstånd
Both Båda
Shorter Than Length Kortare än längden
Dashes to Geometry Streck till geometri
Dashes to Geometry… Streck till geometri…
Add Anchors Lägg till ankarpunkter
Add Anchors… Lägg till ankarpunkter…
Smooth Anchors Jämna ut ankarpunkter
Smooth Anchors… Jämna ut ankarpunkter…
Simplify Anchors Förenkla ankarpunkter
Simplify Anchors… Förenkla ankarpunkter…
Split at Anchors Dela vid ankarpunkter
Split at Anchors… Dela vid ankarpunkter…
Extend Path… Förläng bana…
Close Path… Stäng bana…
Retract Handles… Dra in handtag…
Dash 1 Streck 1
Gap 1 Mellanrum 1
Dash 2 Streck 2
Gap 2 Mellanrum 2
Dash 3 Streck 3
Gap 3 Mellanrum 3
Dash 4 Streck 4
Gap 4 Mellanrum 4
Dash 5 Streck 5
Gap 5 Mellanrum 5
Can't render this file because it contains an unexpected character in line 5 and column 25.
+40
View File
@@ -5896,3 +5896,43 @@ breakText Glyphs Glifler
@msg `by` must be lines, paragraphs, words or glyphs, not {other} `by`, lines, paragraphs, words veya glyphs olmalı, {other} değil
@msg `kind` must be "line" or "paragraph", not {other} `kind`, "line" veya "paragraph" olmalı, {other} değil
@msg `align` must be "left", "center" or "right", not {other} `align`, "left", "center" veya "right" olmalı, {other} değil
# Path Precision Tools add-on: Path Effects (Effect › Path Effects).
Path Effects Yol Efektleri
Reverse Direction Yönü Ters Çevir
Align to Corners and Ends Köşelere ve Uçlara Hizala
Count Sayı
Max Count En Fazla Sayı
Start Length Başlangıç Uzunluğu
Start Percent Başlangıç Yüzdesi
Start Shape Başlangıç Şekli
End Length Bitiş Uzunluğu
End Percent Bitiş Yüzdesi
End Shape Bitiş Şekli
By Count Sayıya Göre
By Distance Mesafeye Göre
Both Her İkisi
Shorter Than Length Uzunluktan Kısa Olanlar
Dashes to Geometry Çizgileri Geometriye Dönüştür
Dashes to Geometry… Çizgileri Geometriye Dönüştür…
Add Anchors Bağlantı Noktaları Ekle
Add Anchors… Bağlantı Noktaları Ekle…
Smooth Anchors Bağlantı Noktalarını Yumuşat
Smooth Anchors… Bağlantı Noktalarını Yumuşat…
Simplify Anchors Bağlantı Noktalarını Basitleştir
Simplify Anchors… Bağlantı Noktalarını Basitleştir…
Split at Anchors Bağlantı Noktalarından Böl
Split at Anchors… Bağlantı Noktalarından Böl…
Extend Path… Yolu Uzat…
Close Path… Yolu Kapat…
Retract Handles… Kolları Geri Çek…
Dash 1 Çizgi 1
Gap 1 Boşluk 1
Dash 2 Çizgi 2
Gap 2 Boşluk 2
Dash 3 Çizgi 3
Gap 3 Boşluk 3
Dash 4 Çizgi 4
Gap 4 Boşluk 4
Dash 5 Çizgi 5
Gap 5 Boşluk 5
Can't render this file because it contains an unexpected character in line 5 and column 25.
+41
View File
@@ -5537,3 +5537,44 @@ breakText Glyphs Гліфи
@msg `by` must be lines, paragraphs, words or glyphs, not {other} `by` має бути lines, paragraphs, words або glyphs, а не {other}
@msg `kind` must be "line" or "paragraph", not {other} `kind` має бути "line" або "paragraph", а не {other}
@msg `align` must be "left", "center" or "right", not {other} `align` має бути "left", "center" або "right", а не {other}
# Path Precision Tools add-on: Path Effects (Effect › Path Effects).
Path Effects Ефекти контуру
Reverse Direction Змінити напрям
Align to Corners and Ends Вирівняти за кутами й кінцями
Count Кількість
Max Count Найбільша кількість
Start Length Довжина на початку
Start Percent Відсотки на початку
Start Shape Форма на початку
End Length Довжина в кінці
End Percent Відсотки в кінці
End Shape Форма в кінці
By Count За кількістю
By Distance За відстанню
Both Обидва
Shorter Than Length Коротші за довжину
Anchors Опорні точки
Dashes to Geometry Штрихи в геометрію
Dashes to Geometry… Штрихи в геометрію…
Add Anchors Додати опорні точки
Add Anchors… Додати опорні точки…
Smooth Anchors Згладити опорні точки
Smooth Anchors… Згладити опорні точки…
Simplify Anchors Спростити опорні точки
Simplify Anchors… Спростити опорні точки…
Split at Anchors Розрізати в опорних точках
Split at Anchors… Розрізати в опорних точках…
Extend Path… Подовжити контур…
Close Path… Замкнути контур…
Retract Handles… Прибрати маркери…
Dash 1 Штрих 1
Gap 1 Проміжок 1
Dash 2 Штрих 2
Gap 2 Проміжок 2
Dash 3 Штрих 3
Gap 3 Проміжок 3
Dash 4 Штрих 4
Gap 4 Проміжок 4
Dash 5 Штрих 5
Gap 5 Проміжок 5
Can't render this file because it contains an unexpected character in line 178 and column 5.
+41
View File
@@ -5539,3 +5539,44 @@ breakText Glyphs Glyph
@msg `by` must be lines, paragraphs, words or glyphs, not {other} `by` phải là lines, paragraphs, words hoặc glyphs, không phải {other}
@msg `kind` must be "line" or "paragraph", not {other} `kind` phải là "line" hoặc "paragraph", không phải {other}
@msg `align` must be "left", "center" or "right", not {other} `align` phải là "left", "center" hoặc "right", không phải {other}
# Path Precision Tools add-on: Path Effects (Effect › Path Effects).
Path Effects Hiệu ứng đường dẫn
Reverse Direction Đảo ngược hướng
Align to Corners and Ends Căn theo góc và đầu mút
Count Số lượng
Max Count Số lượng tối đa
Start Length Độ dài đầu
Start Percent Phần trăm đầu
Start Shape Hình dạng đầu
End Length Độ dài cuối
End Percent Phần trăm cuối
End Shape Hình dạng cuối
By Count Theo số lượng
By Distance Theo khoảng cách
Both Cả hai
Shorter Than Length Ngắn hơn độ dài
Anchors Điểm neo
Dashes to Geometry Nét đứt thành hình học
Dashes to Geometry… Nét đứt thành hình học…
Add Anchors Thêm các điểm neo
Add Anchors… Thêm các điểm neo…
Smooth Anchors Làm mượt điểm neo
Smooth Anchors… Làm mượt điểm neo…
Simplify Anchors Đơn giản hóa điểm neo
Simplify Anchors… Đơn giản hóa điểm neo…
Split at Anchors Tách tại điểm neo
Split at Anchors… Tách tại điểm neo…
Extend Path… Kéo dài đường dẫn…
Close Path… Đóng đường dẫn…
Retract Handles… Thu tay nắm…
Dash 1 Nét 1
Gap 1 Khoảng hở 1
Dash 2 Nét 2
Gap 2 Khoảng hở 2
Dash 3 Nét 3
Gap 3 Khoảng hở 3
Dash 4 Nét 4
Gap 4 Khoảng hở 4
Dash 5 Nét 5
Gap 5 Khoảng hở 5
Can't render this file because it contains an unexpected character in line 200 and column 5.
+40
View File
@@ -5780,3 +5780,43 @@ breakText Glyphs 字形
@msg `by` must be lines, paragraphs, words or glyphs, not {other} `by` 必须是 lines、paragraphs、words 或 glyphs,而不是 {other}
@msg `kind` must be "line" or "paragraph", not {other} `kind` 必须是 "line" 或 "paragraph",而不是 {other}
@msg `align` must be "left", "center" or "right", not {other} `align` 必须是 "left"、"center" 或 "right",而不是 {other}
# Path Precision Tools add-on: Path Effects (Effect › Path Effects).
Path Effects 路径效果
Reverse Direction 反转方向
Align to Corners and Ends 对齐转角和端点
Count 数量
Max Count 最大数量
Start Length 起点长度
Start Percent 起点百分比
Start Shape 起点形状
End Length 终点长度
End Percent 终点百分比
End Shape 终点形状
By Count 按数量
By Distance 按距离
Both 两者
Shorter Than Length 短于长度
Dashes to Geometry 虚线转为几何图形
Dashes to Geometry… 虚线转为几何图形…
Add Anchors 添加锚点
Add Anchors… 添加锚点…
Smooth Anchors 平滑锚点
Smooth Anchors… 平滑锚点…
Simplify Anchors 简化锚点
Simplify Anchors… 简化锚点…
Split at Anchors 在锚点处分割
Split at Anchors… 在锚点处分割…
Extend Path… 延长路径…
Close Path… 闭合路径…
Retract Handles… 收回手柄…
Dash 1 虚线 1
Gap 1 间隔 1
Dash 2 虚线 2
Gap 2 间隔 2
Dash 3 虚线 3
Gap 3 间隔 3
Dash 4 虚线 4
Gap 4 间隔 4
Dash 5 虚线 5
Gap 5 间隔 5
Can't render this file because it contains an unexpected character in line 2 and column 73.
+41
View File
@@ -5576,3 +5576,44 @@ breakText Glyphs 字符
@msg `by` must be lines, paragraphs, words or glyphs, not {other} `by` 必須是 lines、paragraphs、words 或 glyphs,而不是 {other}
@msg `kind` must be "line" or "paragraph", not {other} `kind` 必須是 "line" 或 "paragraph",而不是 {other}
@msg `align` must be "left", "center" or "right", not {other} `align` 必須是 "left"、"center" 或 "right",而不是 {other}
# Path Precision Tools add-on: Path Effects (Effect › Path Effects).
Path Effects 路徑效果
Reverse Direction 反轉方向
Align to Corners and Ends 對齊轉角和端點
Count 數量
Max Count 最大數量
Start Length 起點長度
Start Percent 起點百分比
Start Shape 起點形狀
End Length 終點長度
End Percent 終點百分比
End Shape 終點形狀
By Count 依數量
By Distance 依距離
Both 兩者
Shorter Than Length 短於長度
Anchors 錨點
Dashes to Geometry 虛線轉為幾何圖形
Dashes to Geometry… 虛線轉為幾何圖形…
Add Anchors 增加錨點
Add Anchors… 增加錨點…
Smooth Anchors 平滑錨點
Smooth Anchors… 平滑錨點…
Simplify Anchors 簡化錨點
Simplify Anchors… 簡化錨點…
Split at Anchors 在錨點處分割
Split at Anchors… 在錨點處分割…
Extend Path… 延長路徑…
Close Path… 封閉路徑…
Retract Handles… 收回控制把手…
Dash 1 虛線 1
Gap 1 間隙 1
Dash 2 虛線 2
Gap 2 間隙 2
Dash 3 虛線 3
Gap 3 間隙 3
Dash 4 虛線 4
Gap 4 間隙 4
Dash 5 虛線 5
Gap 5 間隙 5
Can't render this file because it contains an unexpected character in line 6 and column 47.
+1
View File
@@ -139,6 +139,7 @@ pub(super) fn effect_menu() -> Vec<Item> {
"Crop Marks",
"Distort & Transform",
"Path",
"Path Effects",
"Pathfinder",
"Rasterize…",
"Stylize",
+31
View File
@@ -5903,6 +5903,37 @@ plain path from then on.
{"name":"run_command","arguments":{"command":"addon.pathtools.shape.set","params":{"id":14,"kind":"gear","count":16,"radius":2}}}
```
### Path Effects (live)
Effect › Path Effects holds the add-on's live geometry effects, applied with `effect.apply {effect, params}` like any
other effect, with no shortcuts. Each draws, measures, exports and stacks as the other geometry effects do; the object's
own path stays as it is until Expand Appearance (`effect.expandAppearance`) writes the result. `effect.list` documents
every parameter. Out-of-range values are clamped, and an effect whose result would pass the geometry effects' budget
(2 million anchors) leaves the path as it is.
| Id | Menu item | Parameters |
| --- | --- | --- |
| `addon.path.dashes` | Dashes to Geometry… | `dash1`…`dash5`, `gap1`…`gap5` (pt 0 to 10000; 12 and 6, then 0), `offset` (pt), `alignCorners` (bool, false) |
| `addon.path.addAnchors` | Add Anchors… | `target` (`all`, `curved`, `straight`), `spacing` (`count`, `distance`), `count` (1 to 1000, 1), `distance` (pt, 20), `maxCount` (1 to 1000, 100) |
| `addon.path.extend` | Extend Path… | `startLength`, `endLength` (pt, 10; negative shortens), `startPercent`, `endPercent` (% of the path's length, 0), `startShape`, `endShape` (`straight`, `arc`) |
| `addon.path.close` | Close Path… | `mode` (`straight`, `smooth`) |
| `addon.path.smooth` | Smooth Anchors… | `ratio` (0.01 to 0.5, 0.3905) |
| `addon.path.retract` | Retract Handles… | `handles` (`all`, `shorter`), `length` (pt, 5) |
| `addon.path.reverse` | Reverse Direction | none |
| `addon.path.simplify` | Simplify Anchors… | `tolerance` (0 to 200, 10; each step lets the outline drift 0.02 pt), `protectCorners` (bool, true) |
| `addon.path.split` | Split at Anchors… | `anchors` (`corner`, `smooth`, `both`) |
Dashes to Geometry turns the path into open subpaths, one per dash; with `alignCorners`, every run between corners and
ends holds whole periods with a dash centred on each corner and end, and the offset doesn't apply. A dash of 0 becomes a
point that a round or projecting cap paints. Extend Path adds an end's length and percent together; closed subpaths stay
as they are, and so does an end that would be shortened to nothing. An arc follows the end's curvature for at most half
a circle. Split at Anchors tells smooth anchors by their handles standing in line. Simplify Anchors leaves a path of more
than 50000 anchors as it is.
```json
{"id": 1, "method": "engine.execute", "params": {"command": "effect.apply", "params": {"effect": "addon.path.dashes", "params": {"dash1": 8, "gap1": 4, "dash2": 2, "gap2": 4, "alignCorners": true}}}}
```
## Smart Pen add-on
Smart Pen is a built-in add-on: one tool that draws paths and edits their points, handles and segments without
+12 -1
View File
@@ -1,6 +1,6 @@
# Parity with Adobe Illustrator
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-31 · **Change:** minor (Path Precision Tools add-on: Corner Tool and Slide Anchor Along Path; 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-31 · **Change:** minor (Path Precision Tools add-on: live Path Effects; scores unchanged) · **Target:** Adobe Illustrator 2026 (30.x; public release notes through 30.7, August 2026)
## Feature submission evidence
@@ -1251,6 +1251,16 @@ Kernels in `vectorcraft_pathops::addons::pathtools`. Tests: `addons::pathtools`
undo step, bad params refused, the brush's stroke one step) and `edit_toolbar::tests::addon_tools_are_in_the_drawer_not_the_toolbars`.
Commands: `docs/mcp.md` › Path Precision Tools add-on.
The add-on's live Path Effects (2026-10-31, Effect › Path Effects, `addon.path.*`) are geometry effects, so they draw,
measure, export and expand as the others do: Dashes to Geometry (up to five dash and gap pairs, offset, fitted to corners
and ends), Add Anchors (all, curved or straight segments, by count or distance, capped per segment), Extend Path (each
end by length and percent, shortening when negative, straight or arc), Close Path (straight or smooth), Smooth Anchors,
Retract Handles (all or the short ones), Reverse Direction, Simplify Anchors and Split at Anchors. Code in
`vectorcraft_effects::addons::path` on the kernels above. Tests: `addons::path::tests` in `vectorcraft-effects` (geometry,
caps, junk params and paths), `cmd::addons::pathtools::effect_tests` in `vectorcraft-engine` (drawn live, Expand
Appearance matches the outline and the pixels, saves keep it, a file's junk params), `addons::pathtools::tests` in
`vectorcraft-pathops` and `vectorcraft-ui-egui`, and the command sweep's junk rows.
The add-on's Parametric Shape Tool (2026-10-30) draws ten kinds of shape that stay editable as numbers (rectangle,
ellipse, polygon and star with 3 to 1,000 sides or points, ring, arrow, cross, heart, gear, speech bubble; corner radius
limited to the middle of a side): a drag from the centre sizes and turns one, the arrow keys change its count and values
@@ -1328,6 +1338,7 @@ add-on.
| 2026-10-30 | minor | File formats: `.ai`/EPS editing data: a blend takes the transparency (opacity, blend mode, knockout) in its state block after its record, so its steps knock each other out as the page draws them; a user's file comes in 0.6% from its page instead of 1.3%. Scores and estimates unchanged. |
| 2026-10-31 | minor | Path Precision Tools add-on: the Corner Tool and Slide Anchor Along Path; evidence added; scores unchanged. |
| 2026-10-30 | minor | File formats: `.ai`/EPS editing-data fidelity from an audit of a user's files: All Caps and Small Caps (#4708); placed files at their box's corner, kept where they draw closer to the page (#4664, #4723, #4742); pages cropped to the art, with `UserUnit` (#4658, #4727); gradient mesh corners with handle weights of 0 and the state written after a mesh (#4738, #4744, #4748); type on a path spaced as the file lays it out (#4743); synthetic regressions in `crates/eps`; evidence added; scores unchanged. |
| 2026-10-31 | minor | Path Precision Tools add-on (a VectorCraft addition): live Path Effects; evidence added; scores unchanged. |
| 2026-10-31 | minor | Artwork Cleanup add-on (a VectorCraft addition): text tools; evidence added; scores unchanged. |
| 2026-10-30 | minor | Color Effects add-on: live Duotone (one to four inks, spot tones kept) and Print Ink Fixes; scores and estimates unchanged |
| 2026-10-30 | minor | 3D Extrude & Bevel, Inflate and Rotate apply to type, which stays editable (#4715); scores and estimates unchanged |
+4 -1
View File
@@ -1,6 +1,6 @@
# UI and interaction parity
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-31 · **Change:** minor (Path Precision Tools add-on: Corner Tool and Slide Anchor Along Path; scores unchanged) · **Target:** Adobe Illustrator 2026 (30.x)
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-31 · **Change:** minor (Path Precision Tools add-on: live Path Effects; scores unchanged) · **Target:** Adobe Illustrator 2026 (30.x)
## Feature submission evidence
@@ -326,6 +326,8 @@ next unit in the order a right-click on a ruler lists them, from Feet back to Po
| 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 |
| Smart Pen add-on (VectorCraft addition): Object › Plug-ins › Smart Pen selects the Smart Pen Tool, which the All Tools drawer lists under its own heading but neither toolbar shows as shipped (no shortcut), and opens the Smart Pen panel (next segment straight or curved; Remove Points Keep Shape, Make Smooth, Make Corner, Retract Handles, Close or Join). The tool draws (click: straight or curved segment, a curve after a straight one tangent; drag: smooth anchor, Alt breaks the handle, Shift 45°, Space moves it; Tab switches a segment; rubber band; close, join, continue) and edits without switching tools (click selects a point, drag moves it, Alt-click removes it keeping the shape, Shift+Alt-click deletes it, double-click toggles smooth/corner; handle drags with Shift and Alt; segment drags reshape, Alt-click adds a point) | done | `addon.pen.*`, `crates/tools/src/addons/smart_pen.rs`, `crates/ui-egui/src/addons/pen.rs` and their tests; not an Illustrator feature, so it is not scored |
| Path Precision Tools add-on (VectorCraft addition): Effect › Path Effects lists its live effects (Dashes to Geometry, Add Anchors, Extend Path, Close Path, Smooth Anchors, Retract Handles, Reverse Direction, Simplify Anchors, Split at Anchors), each with the effect dialog's live Preview except Reverse Direction, which has no options, and listed in the Appearance panel like any effect | done | `addon.path.*`, `crates/effects/src/addons/path.rs`, `crates/ui-egui/src/addons/pathtools.rs` and their tests; not an Illustrator feature, so it is not scored |
| Path Precision Tools add-on (VectorCraft addition): Object › Plug-ins › Path Precision Tools selects its four tools (Anchor Eraser Brush, Extend Path, Measure & Inspect, Parametric Shape), which the All Tools drawer lists under their own heading but neither toolbar shows as shipped, opens the Anchor Inspector panel (the selected anchor's X/Y and handle lengths and angles as fields; Make Smooth, Make Corner, Equalize Handles, Remove Points Keep Shape, Add Points, the three ways to close) and the Parametric Shape panel (kind, count, W/H, rotation, the kind's own values and corner radius of the selected parametric shape, or of the tool's next one), and runs Add Points at / Split at Crossings and Convert to Parametric Shape; the Parametric Shape Tool draws ten re-editable kinds from the centre (↑/↓ and ←/→ change their numbers while dragging) and edits a selected one with handles; a double-click on the brush or Extend Path tool opens its options | done | `addon.pathtools.*`, `crates/tools/src/addons/`, `crates/ui-egui/src/addons/pathtools.rs`, `crates/ui-egui/src/addons/pathtools/shape.rs` and their tests; not an Illustrator feature, so it is not scored |
| Path Precision Tools add-on (VectorCraft addition): Object › Plug-ins › Path Precision Tools selects its six tools (Anchor Eraser Brush, Extend Path, Measure & Inspect, Parametric Shape, Corner Tool, Slide Anchor Along Path), which the All Tools drawer lists under their own heading but neither toolbar shows as shipped, opens the Anchor Inspector panel (the selected anchor's X/Y and handle lengths and angles as fields; Make Smooth, Make Corner, Equalize Handles, Remove Points Keep Shape, Add Points, the three ways to close) and the Parametric Shape panel (kind, count, W/H, rotation, the kind's own values and corner radius of the selected parametric shape, or of the tool's next one), and runs Add Points at / Split at Crossings and Convert to Parametric Shape; the Parametric Shape Tool draws ten re-editable kinds from the centre (↑/↓ and ←/→ change their numbers while dragging) and edits a selected one with handles; the Corner Tool cuts editable corners on any path (hover shows the anchor, a click uses its radius and kind, a drag sizes the corner, Shift whole values; a corner's marker selects it and loads its values, drags to resize, to 0 or double-clicked removes it; never past what fits; Alt-marquee cuts every anchor inside) with a Corners section in the Anchor Inspector (radius, kind, selected count, Apply to Selected Anchors); Slide Anchor Along Path drags an anchor along its curve with the segments beside it refitted, Shift snapping where the tangent is a multiple of 45°; a double-click on the brush or Extend Path tool opens its options | done | `addon.pathtools.*`, `crates/tools/src/addons/`, `crates/ui-egui/src/addons/pathtools.rs`, `crates/ui-egui/src/addons/pathtools/` and their tests; not an Illustrator feature, so it is not scored |
| Smart Pen add-on (VectorCraft addition): Object › Plug-ins › Smart Pen selects the Smart Pen Tool, which the All Tools drawer lists under its own heading but neither toolbar shows as shipped (no shortcut), and opens the Smart Pen panel (next segment straight or curved; Remove Points Keep Shape, Make Smooth, Make Corner, Retract Handles, Close or Join). The tool draws (click: straight or curved segment, a curve after a straight one tangent; drag: smooth anchor, Alt breaks the handle, Shift 45°, Space moves it; Tab switches a segment; rubber band; close, join, continue) and edits without switching tools (click selects a point, drag moves it, Alt-click removes it keeping the shape, Shift+Alt-click deletes it, double-click toggles smooth/corner; handle drags with Shift and Alt; segment drags reshape, Alt-click adds a point); an Alt-drag away from paths selects the points inside a marquee | done | `addon.pen.*`, `crates/tools/src/addons/smart_pen.rs`, `crates/ui-egui/src/addons/pen.rs` and their tests; not an Illustrator feature, so it is not scored |
| Path Precision Tools add-on (VectorCraft addition): Object › Plug-ins › Path Precision Tools selects its three tools (Anchor Eraser Brush, Extend Path, Measure & Inspect), which the All Tools drawer lists under their own heading but neither toolbar shows as shipped, opens the Anchor Inspector panel (the selected anchor's X/Y and handle lengths and angles as fields; Make Smooth, Make Corner, Equalize Handles, Remove Points Keep Shape, Add Points, the three ways to close) and runs Add Points at / Split at Crossings; a double-click on the brush or Extend Path tool opens its options | done | `addon.pathtools.*`, `crates/tools/src/addons/`, `crates/ui-egui/src/addons/pathtools.rs` and their tests; not an Illustrator feature, so it is not scored |
@@ -372,6 +374,7 @@ separate. Scores and hours unchanged. [Behavior and command details](development
| Date | Change | Summary |
|---|---|---|
| 2026-10-31 | minor | Path Precision Tools add-on: live Path Effects in Effect › Path Effects; scores and estimates unchanged |
| 2026-10-31 | minor | Path Precision Tools add-on: the Corner Tool (editable corners, panel section) and Slide Anchor Along Path; scores and estimates unchanged |
| 2026-10-31 | minor | Artwork Cleanup add-on (a VectorCraft addition): its text tools in the panel and Object › Plug-ins › Artwork Cleanup; scores and estimates unchanged |
| 2026-10-30 | minor | Color Effects add-on: live Duotone (one to four inks, spot tones kept) and Print Ink Fixes; scores and estimates unchanged |