mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-29 20:27:05 -06:00
Path: Object › Path › Smooth smooths the selected paths with a slider and Auto-Smooth (#4115)
This commit is contained in:
@@ -314,6 +314,55 @@ pub fn smooth(path: &PathData, amount: f64) -> PathData {
|
||||
out
|
||||
}
|
||||
|
||||
/// The Smooth slider's level Auto-Smooth picks, halfway between minimum and maximum smoothing.
|
||||
/// VectorCraft's own rule: the help page says only that Auto-Smooth smooths the path automatically
|
||||
/// (https://helpx.adobe.com/illustrator/desktop/draw-shapes-and-paths/modify-paths/refine-path-segments-with-the-smooth-slider.html).
|
||||
/// [`smooth_level`] already scales with each path's size, so one level suits every path.
|
||||
pub const SMOOTH_AUTO_LEVEL: f64 = 50.0;
|
||||
|
||||
/// Object › Path › Smooth at slider level `level`, from 0 (minimum smoothing: the path stays within
|
||||
/// 0.1 % of its longer side of where it was) to 100 (maximum). The help page names only the two
|
||||
/// ends; the rest is VectorCraft's own rule. Each subpath is refitted with curves as Simplify does,
|
||||
/// but keeping a corner only where the path turns back on itself (so jags and kinks become curves),
|
||||
/// within a tolerance from 0.1 % of the path's longer side at level 0 to 2.5 % at level 100. A refit
|
||||
/// never has more anchor points than the subpath had: a shape whose corners the tolerance can't
|
||||
/// round, such as a rectangle, stays as it is. Closed subpaths stay closed, with no kink left where
|
||||
/// they start, and open ones keep their end points. A level outside 0..=100 counts as the nearer end, and one that is not a number as
|
||||
/// [`SMOOTH_AUTO_LEVEL`].
|
||||
pub fn smooth_level(path: &PathData, level: f64) -> PathData {
|
||||
let level = if level.is_finite() { level.clamp(0.0, 100.0) } else { SMOOTH_AUTO_LEVEL };
|
||||
let size = path.bounds().map_or(0.0, |b| b.width().max(b.height()));
|
||||
if !size.is_finite() {
|
||||
return path.clone();
|
||||
}
|
||||
let opts = SimplifyOptions { tolerance: size * (0.001 + 0.024 * level / 100.0), corner_angle_deg: SMOOTH_CORNER_DEG, straight_lines: false };
|
||||
let smoothed = |sp: &SubPath| {
|
||||
let Some(mut fit) = simplify_subpath(sp, &opts).filter(|f| f != sp) else { return sp.clone() };
|
||||
smooth_seam(&mut fit);
|
||||
fit
|
||||
};
|
||||
PathData::new(path.subpaths.iter().map(smoothed).collect())
|
||||
}
|
||||
|
||||
/// [`smooth_level`] keeps a corner only where the path turns by more than this (turns back).
|
||||
const SMOOTH_CORNER_DEG: f64 = 179.0;
|
||||
|
||||
/// A closed subpath's refit leaves its start anchor with the tangents of the original segments on
|
||||
/// either side of it, a kink when the path had one there: turn it into a smooth anchor with the same
|
||||
/// handle lengths, unless the path turns back there.
|
||||
fn smooth_seam(sp: &mut SubPath) {
|
||||
if !sp.closed {
|
||||
return;
|
||||
}
|
||||
let Some(a) = sp.anchors.first_mut() else { return };
|
||||
let (Some(i), Some(o)) = (crate::fit::unit(a.p - a.h_in), crate::fit::unit(a.h_out - a.p)) else { return };
|
||||
if i.dot(o) < SMOOTH_CORNER_DEG.to_radians().cos() {
|
||||
return;
|
||||
}
|
||||
let Some(dir) = crate::fit::unit(i + o) else { return };
|
||||
*a = Anchor::with_handles(a.p, a.p - dir * a.h_in.distance(a.p), a.p + dir * a.h_out.distance(a.p));
|
||||
}
|
||||
|
||||
/// Remove anchors that can be deleted without changing the shape by more than `tolerance`
|
||||
/// (collinear corner points, duplicate points, and curve joints that one cubic can replace).
|
||||
pub fn remove_redundant_points(path: &PathData, tolerance: f64) -> PathData {
|
||||
|
||||
@@ -20,8 +20,8 @@ pub use boolean::{
|
||||
try_unite_all_with, unite_all, unite_all_with,
|
||||
};
|
||||
pub use edit::{
|
||||
AverageAxis, SIMPLIFY_LEVEL, SimplifiedPart, SimplifyOptions, add_anchor_points, auto_simplify_level, average, join, remove_anchor,
|
||||
remove_redundant_points, simplify, simplify_level_tolerance, simplify_part, simplify_with, smooth, split_into_grid,
|
||||
AverageAxis, SIMPLIFY_LEVEL, SMOOTH_AUTO_LEVEL, SimplifiedPart, SimplifyOptions, add_anchor_points, auto_simplify_level, average, join,
|
||||
remove_anchor, remove_redundant_points, simplify, simplify_level_tolerance, simplify_part, simplify_with, smooth, smooth_level, split_into_grid,
|
||||
};
|
||||
pub use offset::{Cap, Join, offset_path, outline_stroke, stroke_region};
|
||||
pub use pathfinder::{FaceMerger, PathfinderOp, Region, Shape, merge_regions, pathfinder, pathfinder_with, region_at, regions};
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use std::f64::consts::PI;
|
||||
|
||||
use kurbo::{ParamCurve, Point, Rect, Shape as _};
|
||||
use vectorcraft_geom::{FillRule, PathData, SubPath};
|
||||
use vectorcraft_geom::{AnchorKind, FillRule, PathData, SubPath};
|
||||
use vectorcraft_pathops::*;
|
||||
|
||||
const NZ: FillRule = FillRule::NonZero;
|
||||
@@ -430,6 +430,21 @@ fn smooth_rounds_corners() {
|
||||
assert_eq!(same, rect(0.0, 0.0, 10.0, 10.0));
|
||||
}
|
||||
|
||||
/// Object › Path › Smooth: a rectangle, whose corners no curve rounds within the tolerance, stays as
|
||||
/// it is even at maximum smoothing; a polygon of 16 sides becomes a closed curve of fewer anchors once the tolerance
|
||||
/// passes its 0.96 pt sagitta, smooth all round.
|
||||
#[test]
|
||||
fn smooth_level_keeps_corners_it_cannot_round_and_curves_polygons() {
|
||||
let r = rect(0.0, 0.0, 100.0, 50.0);
|
||||
assert_eq!(smooth_level(&r, 100.0), r);
|
||||
let pts: Vec<Point> = (0..16).map(|i| Point::new(50.0 * (f64::from(i) * PI / 8.0).cos(), 50.0 * (f64::from(i) * PI / 8.0).sin())).collect();
|
||||
let polygon = PathData::single(SubPath::polyline(&pts, true));
|
||||
let s = smooth_level(&polygon, SMOOTH_AUTO_LEVEL);
|
||||
assert!(s.subpaths.len() == 1 && s.subpaths[0].closed && s.anchor_count() < 8, "{}", s.anchor_count());
|
||||
assert!(s.subpaths[0].anchors.iter().all(|a| a.kind == AnchorKind::Smooth), "no kink, its start included: {s:?}");
|
||||
assert_eq!(smooth_level(&polygon, 0.0), polygon, "0.1 pt is less than the sagitta");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn remove_redundant_collinear_and_curve() {
|
||||
let p = PathData::single(SubPath::polyline(
|
||||
|
||||
Reference in New Issue
Block a user