Files
vectorcraft/crates/trace/src/fit.rs
T

157 lines
5.1 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! Loop → polygon → Bézier curves.
use vectorcraft_geom::{AnchorKind, CubicBez, ParamCurve, PathData, Point, SubPath};
use vectorcraft_pathops::{SimplifyOptions, simplify_with};
use crate::contour::Loop;
#[derive(Clone, Copy, Debug)]
pub(crate) struct FitOptions {
pub polygon_tol: f64,
pub fit_tol: f64,
pub corner_angle: f64,
pub snap_lines: bool,
}
fn seg_dist(p: Point, a: Point, b: Point) -> f64 {
let ab = b - a;
let l2 = ab.hypot2();
if l2 < 1e-18 {
return p.distance(a);
}
let t = ((p - a).dot(ab) / l2).clamp(0.0, 1.0);
p.distance(a + ab * t)
}
/// Douglas–Peucker on `pts[a..=b]`, marking kept indices.
fn rdp(pts: &[Point], a: usize, b: usize, tol: f64, keep: &mut [bool]) {
let mut stack = vec![(a, b)];
while let Some((a, b)) = stack.pop() {
if b <= a + 1 {
continue;
}
let (pa, pb) = (pts[a], pts[b % pts.len()]);
let mut best = (0.0, a);
for (i, &q) in pts.iter().enumerate().take(b).skip(a + 1) {
let d = seg_dist(q, pa, pb);
if d > best.0 {
best = (d, i);
}
}
if best.0 > tol {
keep[best.1] = true;
stack.push((a, best.1));
stack.push((best.1, b));
}
}
}
/// The polygon approximating a closed loop of pixel-corner vertices within `tol`.
pub(crate) fn polygon(loop_pts: &[(i32, i32)], tol: f64) -> Vec<Point> {
let raw: Vec<Point> = loop_pts.iter().map(|&(x, y)| Point::new(x as f64, y as f64)).collect();
if raw.len() < 4 {
return raw;
}
let pts = dejag(loop_pts);
let pts = if pts.len() < 3 { raw.clone() } else { pts };
let n = pts.len();
if n < 4 {
return pts;
}
// Split the closed loop at vertex 0 and the vertex farthest from it.
let far = (1..n).max_by(|&i, &j| pts[i].distance_squared(pts[0]).total_cmp(&pts[j].distance_squared(pts[0]))).unwrap_or(n / 2);
let mut ext = pts.clone();
ext.push(pts[0]);
let mut keep = vec![false; n + 1];
keep[0] = true;
keep[far] = true;
rdp(&ext, 0, far, tol, &mut keep);
rdp(&ext, far, n, tol, &mut keep);
let mut out: Vec<Point> = (0..n).filter(|&i| keep[i]).map(|i| pts[i]).collect();
// Vertex 0 was forced; drop it when it's not needed.
if out.len() > 3 {
let (p, a, b) = (out[0], out[out.len() - 1], out[1]);
if seg_dist(p, a, b) <= tol * 0.5 {
out.remove(0);
}
}
if out.len() < 3 { raw } else { out }
}
/// Replace pixel-staircase jogs by their midpoints.
///
/// A unit-length edge whose two ends turn in opposite directions (an S-shaped step) is a jog of
/// a staircase approximating a sloped line or curve: its midpoint lies on the ideal boundary, its
/// end vertices don't. Vertices not adjacent to any jog (true corners, e.g. of rectangles, or the
/// U-turns at the end of one-pixel-wide lines) are kept.
fn dejag(v: &[(i32, i32)]) -> Vec<Point> {
let n = v.len();
let edge = |i: usize| {
let (a, b) = (v[i % n], v[(i + 1) % n]);
(b.0 - a.0, b.1 - a.1)
};
// Turn sign at vertex i (between edge i-1 and edge i).
let turn = |i: usize| {
let (a, b) = (edge((i + n - 1) % n), edge(i));
(a.0 as i64 * b.1 as i64 - a.1 as i64 * b.0 as i64).signum()
};
let jog: Vec<bool> = (0..n)
.map(|i| {
let (dx, dy) = edge(i);
dx.abs() + dy.abs() == 1 && turn(i) * turn(i + 1) < 0
})
.collect();
let mut out = Vec::with_capacity(n);
for i in 0..n {
if !jog[i] && !jog[(i + n - 1) % n] {
out.push(Point::new(v[i].0 as f64, v[i].1 as f64));
}
if jog[i] {
let (a, b) = (v[i], v[(i + 1) % n]);
out.push(Point::new((a.0 + b.0) as f64 / 2.0, (a.1 + b.1) as f64 / 2.0));
}
}
out
}
/// Fit one loop into a closed Bézier subpath (pixel coordinates).
pub(crate) fn fit_loop(l: &Loop, o: &FitOptions) -> Option<SubPath> {
let poly = polygon(&l.pts, o.polygon_tol);
if poly.len() < 3 {
return None;
}
let sp = SubPath::polyline(&poly, true);
let fitted =
simplify_with(&PathData::single(sp), &SimplifyOptions { tolerance: o.fit_tol, corner_angle_deg: o.corner_angle, straight_lines: false });
let mut sp = fitted.subpaths.into_iter().next()?;
if sp.anchors.len() < 2 {
return None;
}
if o.snap_lines {
snap_to_lines(&mut sp, (o.fit_tol * 2.0).max(1.5));
}
Some(sp)
}
/// Retract the handles of curves that stay within `tol` of their chord.
fn snap_to_lines(sp: &mut SubPath, tol: f64) {
let n = sp.anchors.len();
for i in 0..sp.segment_count() {
let j = (i + 1) % n;
let (a, b) = (sp.anchors[i], sp.anchors[j]);
if !a.has_out() && !b.has_in() {
continue;
}
let c = CubicBez::new(a.p, a.h_out, b.h_in, b.p);
if (1..8).all(|k| seg_dist(c.eval(k as f64 / 8.0), a.p, b.p) <= tol) {
sp.anchors[i].h_out = a.p;
sp.anchors[j].h_in = b.p;
}
}
for a in &mut sp.anchors {
if !a.has_in() || !a.has_out() {
a.kind = AnchorKind::Corner;
}
}
}