mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-30 12:47:13 -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 {
|
||||
|
||||
Reference in New Issue
Block a user