use super::corner_class::{angle_between_deg, classify_angle, Class};
use super::tolerance::{ARCLEN_ACCURACY, DEGENERATE_EPS};
use crate::geometry::{
CubicBez, Line, ParamCurve, ParamCurveArclen, ParamCurveDeriv, PathSeg, QuadBez,
};
use crate::geometry::{Point, Vec2};
use crate::path::{Path, PathEl};
use crate::primitives::CornerRounding;
pub fn round_path_corners(path: &Path, opts: CornerRounding) -> Path {
let subpaths = split_subpaths(path);
let mut out = Path::new();
for sp in &subpaths {
round_subpath_into(sp, opts, &mut out);
}
out
}
struct Subpath {
segs: Vec<PathSeg>,
closed: bool,
}
fn split_subpaths(path: &Path) -> Vec<Subpath> {
let mut out = Vec::new();
let mut segs: Vec<PathSeg> = Vec::new();
let mut start = Point::ORIGIN;
let mut pen = Point::ORIGIN;
let mut in_sub = false;
let mut closed = false;
for el in path.elements() {
match el {
PathEl::MoveTo(p) => {
if in_sub {
out.push(Subpath {
segs: std::mem::take(&mut segs),
closed,
});
}
start = *p;
pen = *p;
in_sub = true;
closed = false;
}
PathEl::LineTo(p) => {
segs.push(PathSeg::Line(Line::new(pen, *p)));
pen = *p;
}
PathEl::QuadTo(c, p) => {
segs.push(PathSeg::Quad(QuadBez::new(pen, *c, *p)));
pen = *p;
}
PathEl::CurveTo(c1, c2, p) => {
segs.push(PathSeg::Cubic(CubicBez::new(pen, *c1, *c2, *p)));
pen = *p;
}
PathEl::ClosePath => {
if (pen - start).hypot() > DEGENERATE_EPS {
segs.push(PathSeg::Line(Line::new(pen, start)));
pen = start;
}
closed = true;
}
}
}
if in_sub {
out.push(Subpath { segs, closed });
}
out
}
struct JoinInfo {
t_back: f64,
t_fwd: f64,
c1: Point,
c2: Point,
p_fwd: Point,
}
fn round_subpath_into(sp: &Subpath, opts: CornerRounding, out: &mut Path) {
let n = sp.segs.len();
if n == 0 {
return;
}
let n_joins = if sp.closed { n } else { n - 1 };
if n_joins == 0 {
let s = sp.segs[0];
out.move_to(s.start());
push_seg_continuation(out, &s);
return;
}
let classes: Vec<Class> = (0..n_joins)
.map(|j| classify_join(&sp.segs[j], &sp.segs[(j + 1) % n], opts.max_angle_deg))
.collect();
let mut joins: Vec<Option<JoinInfo>> = (0..n_joins).map(|_| None).collect();
for j in 0..n_joins {
if classes[j] != Class::Corner {
continue;
}
let left = sp.segs[j];
let right = sp.segs[(j + 1) % n];
let left_len = left.arclen(ARCLEN_ACCURACY);
let right_len = right.arclen(ARCLEN_ACCURACY);
if left_len < DEGENERATE_EPS || right_len < DEGENERATE_EPS {
continue;
}
let back_share = if other_end_corner(j, n_joins, sp.closed, &classes, true) {
0.5
} else {
1.0
};
let fwd_share = if other_end_corner(j, n_joins, sp.closed, &classes, false) {
0.5
} else {
1.0
};
let back_dist = (left_len * back_share).min(opts.max_cut);
let fwd_dist = (right_len * fwd_share).min(opts.max_cut);
if back_dist < DEGENERATE_EPS || fwd_dist < DEGENERATE_EPS {
continue;
}
let t_back = left.inv_arclen(left_len - back_dist, ARCLEN_ACCURACY);
let t_fwd = right.inv_arclen(fwd_dist, ARCLEN_ACCURACY);
let p_back = left.eval(t_back);
let p_fwd = right.eval(t_fwd);
let t_back_dir = unit_tangent_at(&left, t_back);
let t_fwd_dir = unit_tangent_at(&right, t_fwd);
const CUBIC_DEG_ELEV: f64 = 2.0 / 3.0;
let c1 = p_back + t_back_dir * (back_dist * CUBIC_DEG_ELEV);
let c2 = p_fwd - t_fwd_dir * (fwd_dist * CUBIC_DEG_ELEV);
joins[j] = Some(JoinInfo {
t_back,
t_fwd,
c1,
c2,
p_fwd,
});
}
let start_pt = if sp.closed {
match joins[n_joins - 1].as_ref() {
Some(j) => j.p_fwd,
None => sp.segs[0].start(),
}
} else {
sp.segs[0].start()
};
out.move_to(start_pt);
for k in 0..n {
let t_start = left_join_idx(k, n_joins, sp.closed)
.and_then(|j| joins[j].as_ref())
.map(|info| info.t_fwd)
.unwrap_or(0.0);
let t_end = right_join_idx(k, n_joins)
.and_then(|j| joins[j].as_ref())
.map(|info| info.t_back)
.unwrap_or(1.0);
if t_end > t_start + DEGENERATE_EPS {
let sub = sp.segs[k].subsegment(t_start..t_end);
push_seg_continuation(out, &sub);
}
if let Some(j) = right_join_idx(k, n_joins) {
if let Some(info) = joins[j].as_ref() {
out.curve_to(info.c1, info.c2, info.p_fwd);
}
}
}
if sp.closed {
out.close_path();
}
}
fn classify_join(left: &PathSeg, right: &PathSeg, max_angle_deg: f64) -> Class {
let t_in = unit_tangent_at(left, 1.0);
let t_out = unit_tangent_at(right, 0.0);
classify_angle(angle_between_deg(-t_in, t_out), max_angle_deg)
}
fn unit_tangent_at(seg: &PathSeg, t: f64) -> Vec2 {
let v = match seg {
PathSeg::Line(l) => l.deriv().eval(t).to_vec2(),
PathSeg::Quad(q) => q.deriv().eval(t).to_vec2(),
PathSeg::Cubic(c) => c.deriv().eval(t).to_vec2(),
};
let len = v.hypot();
if len < DEGENERATE_EPS {
Vec2::new(1.0, 0.0)
} else {
v / len
}
}
fn left_join_idx(k: usize, n_joins: usize, closed: bool) -> Option<usize> {
if k == 0 {
if closed {
Some(n_joins - 1)
} else {
None
}
} else {
Some(k - 1)
}
}
fn right_join_idx(k: usize, n_joins: usize) -> Option<usize> {
if k < n_joins {
Some(k)
} else {
None
}
}
fn other_end_corner(j: usize, n_joins: usize, closed: bool, classes: &[Class], back: bool) -> bool {
let other = if back {
if j == 0 {
if closed {
Some(n_joins - 1)
} else {
None
}
} else {
Some(j - 1)
}
} else {
if j + 1 < n_joins {
Some(j + 1)
} else if closed {
Some(0)
} else {
None
}
};
other.map(|i| classes[i] == Class::Corner).unwrap_or(false)
}
fn push_seg_continuation(path: &mut Path, seg: &PathSeg) {
match seg {
PathSeg::Line(l) => path.line_to(l.p1),
PathSeg::Quad(q) => path.quad_to(q.p1, q.p2),
PathSeg::Cubic(c) => path.curve_to(c.p1, c.p2, c.p3),
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::primitives::{annular_wedge, polygon, rounded_rect, wedge};
use crate::Rect;
use std::f64::consts::PI;
fn count_curves(path: &Path) -> usize {
path.elements()
.iter()
.filter(|el| matches!(el, PathEl::CurveTo(_, _, _)))
.count()
}
fn count_moves(path: &Path) -> usize {
path.elements()
.iter()
.filter(|el| matches!(el, PathEl::MoveTo(_)))
.count()
}
#[test]
fn empty_path_returns_empty() {
let p = Path::new();
let out = round_path_corners(&p, CornerRounding::default());
assert_eq!(out.elements().len(), 0);
}
#[test]
fn unit_square_rounded_yields_four_fillets() {
let sq = [
Point::new(0.0, 0.0),
Point::new(10.0, 0.0),
Point::new(10.0, 10.0),
Point::new(0.0, 10.0),
];
let p = polygon(&[&sq], crate::primitives::PolygonOptions::default());
let out = round_path_corners(&p, CornerRounding::default());
assert_eq!(count_curves(&out), 4);
assert_eq!(count_moves(&out), 1);
}
#[test]
fn wedge_rounds_two_line_to_arc_corners() {
let w = wedge(Point::ORIGIN, 100.0, 0.0, PI / 2.0);
let opts = CornerRounding {
max_cut: 20.0,
..Default::default()
};
let out = round_path_corners(&w, opts);
assert!(count_curves(&out) >= 3);
}
#[test]
fn annular_wedge_rounds_four_corners() {
let a = annular_wedge(Point::ORIGIN, 30.0, 100.0, 0.0, PI / 2.0);
let opts = CornerRounding {
max_cut: 10.0,
..Default::default()
};
let out = round_path_corners(&a, opts);
let curves = count_curves(&out);
assert!(curves >= 4, "expected at least 4 fillets, got {curves}",);
}
#[test]
fn collinear_joins_are_left_alone() {
let mut path = Path::new();
path.move_to(Point::new(0.0, 0.0));
path.line_to(Point::new(5.0, 0.0));
path.line_to(Point::new(10.0, 0.0));
path.line_to(Point::new(10.0, 5.0));
path.line_to(Point::new(10.0, 10.0));
path.line_to(Point::new(5.0, 10.0));
path.line_to(Point::new(0.0, 10.0));
path.line_to(Point::new(0.0, 5.0));
path.close_path();
let out = round_path_corners(&path, CornerRounding::default());
assert_eq!(count_curves(&out), 4);
}
#[test]
fn max_angle_filter_keeps_corner_sharp() {
let sq = [
Point::new(0.0, 0.0),
Point::new(10.0, 0.0),
Point::new(10.0, 10.0),
Point::new(0.0, 10.0),
];
let p = polygon(&[&sq], crate::primitives::PolygonOptions::default());
let out = round_path_corners(
&p,
CornerRounding {
max_angle_deg: 80.0,
..Default::default()
},
);
assert_eq!(count_curves(&out), 0, "above max_angle_deg, no rounding");
let out2 = round_path_corners(
&p,
CornerRounding {
max_angle_deg: 95.0,
..Default::default()
},
);
assert_eq!(count_curves(&out2), 4);
}
#[test]
fn rounded_rect_still_produces_valid_output() {
let p = rounded_rect(Rect::new(0.0, 0.0, 10.0, 10.0), 2.0);
let _ = round_path_corners(&p, CornerRounding::default());
}
}