use crate::geometry::Shape as _;
use crate::geometry::{Point, Rect, Vec2};
use crate::path::{Path, PathEl};
mod arc_length;
mod clip;
mod clipper;
mod corner;
mod corner_class;
mod end_clip;
mod offset;
mod path_corner;
mod ribbon;
mod tolerance;
pub use arc_length::{ArcLengthWalker, ArcSample, PolylineSampler, TrailingPolicy};
pub use clip::{clip_polylines_to_polygon, intersect_polygons};
pub use corner::round_corners;
pub use end_clip::{clip_polyline, clip_polyline_with_attrs, EndClip};
pub use offset::offset_polygon;
pub use path_corner::round_path_corners;
pub use ribbon::{
polygon_gradient, polygon_ribbon, polygon_ribbon_full, polyline_gradient, polyline_ribbon,
polyline_ribbon_full, ribbon_band_mesh, RibbonOptions,
};
use tolerance::CURVE_APPROX_TOLERANCE;
#[derive(Debug, Clone, Copy)]
pub struct CornerRounding {
pub max_angle_deg: f64,
pub max_cut: f64,
}
impl Default for CornerRounding {
fn default() -> Self {
Self {
max_angle_deg: f64::INFINITY,
max_cut: f64::INFINITY,
}
}
}
#[derive(Debug, Clone, Copy, Default)]
pub struct PolylineOptions {
pub clip_start: Option<EndClip>,
pub clip_end: Option<EndClip>,
}
#[derive(Debug, Clone, Copy)]
pub struct PolygonOptions {
pub offset: f64,
pub miter_limit: f64,
}
impl Default for PolygonOptions {
fn default() -> Self {
Self {
offset: 0.0,
miter_limit: 4.0,
}
}
}
pub fn polyline(points: &[Point], opts: PolylineOptions) -> Path {
if points.len() < 2 {
return Path::new();
}
let pts = clip_polyline(points, opts.clip_start, opts.clip_end);
if pts.len() < 2 {
return Path::new();
}
polyline_path(&pts)
}
pub fn append_polyline_to(path: &mut Path, points: &[Point], closed: bool) {
if points.len() < 2 {
return;
}
path.move_to(points[0]);
for p in &points[1..] {
path.line_to(*p);
}
if closed {
path.close_path();
}
}
pub fn polyline_path(points: &[Point]) -> Path {
let mut path = Path::new();
append_polyline_to(&mut path, points, false);
path
}
pub fn polygon_path(points: &[Point]) -> Path {
let mut path = Path::new();
append_polyline_to(&mut path, points, true);
path
}
pub fn polygon(rings: &[&[Point]], opts: PolygonOptions) -> Path {
if rings.is_empty() || rings[0].len() < 3 {
return Path::new();
}
let offset_rings: Vec<Vec<Point>> = if opts.offset == 0.0 {
rings
.iter()
.filter(|r| r.len() >= 3)
.map(|r| r.to_vec())
.collect()
} else {
offset_polygon(rings, opts.offset, opts.miter_limit)
};
let mut path = Path::new();
for ring in &offset_rings {
if ring.len() < 3 {
continue;
}
append_polyline_to(&mut path, ring, true);
}
path
}
pub fn path_to_rings(path: &Path, tolerance: f64) -> Vec<Vec<Point>> {
let mut rings: Vec<Vec<Point>> = Vec::new();
let mut cur: Vec<Point> = Vec::new();
crate::geometry::flatten(path.iter(), tolerance, |el| match el {
PathEl::MoveTo(p) if !cur.is_empty() => {
rings.push(std::mem::take(&mut cur));
cur.push(p);
}
PathEl::MoveTo(p) => cur.push(p),
PathEl::LineTo(p) => cur.push(p),
PathEl::ClosePath if !cur.is_empty() => {
rings.push(std::mem::take(&mut cur));
}
_ => {}
});
if !cur.is_empty() {
rings.push(cur);
}
rings
}
pub fn rect(r: Rect) -> Path {
r.to_path(CURVE_APPROX_TOLERANCE)
}
pub fn rounded_rect(r: Rect, radius: f64) -> Path {
crate::geometry::RoundedRect::from_rect(r, radius).to_path(CURVE_APPROX_TOLERANCE)
}
pub fn circle(center: Point, radius: f64) -> Path {
crate::geometry::Circle::new(center, radius).to_path(CURVE_APPROX_TOLERANCE)
}
pub fn ellipse(center: Point, radii: Vec2) -> Path {
crate::geometry::Ellipse::new(center, radii, 0.0).to_path(CURVE_APPROX_TOLERANCE)
}
pub fn segment(a: Point, b: Point) -> Path {
let mut p = Path::new();
p.move_to(a);
p.line_to(b);
p
}
pub fn regular_polygon(center: Point, circumradius: f64, n_sides: usize) -> Path {
polygon_path(®ular_polygon_vertices(center, circumradius, n_sides))
}
pub fn regular_polygon_vertices(center: Point, circumradius: f64, n_sides: usize) -> Vec<Point> {
if n_sides < 3 {
return Vec::new();
}
let mut v = Vec::with_capacity(n_sides);
let step = std::f64::consts::TAU / n_sides as f64;
for i in 0..n_sides {
let a = step * i as f64;
v.push(Point::new(
center.x + circumradius * a.cos(),
center.y + circumradius * a.sin(),
));
}
v
}
fn point_on_circle(center: Point, radius: f64, angle: f64) -> Point {
Point::new(
center.x + radius * angle.cos(),
center.y + radius * angle.sin(),
)
}
pub fn arc(center: Point, radius: f64, start_angle: f64, sweep_angle: f64) -> Path {
let arc = crate::geometry::Arc::new(
center,
Vec2::new(radius, radius),
start_angle,
sweep_angle,
0.0,
);
arc.to_path(CURVE_APPROX_TOLERANCE)
}
pub fn wedge(center: Point, radius: f64, start_angle: f64, sweep_angle: f64) -> Path {
let arc_start = point_on_circle(center, radius, start_angle);
let mut path = Path::new();
path.move_to(center);
path.line_to(arc_start);
let arc = crate::geometry::Arc::new(
center,
Vec2::new(radius, radius),
start_angle,
sweep_angle,
0.0,
);
for el in arc.append_iter(CURVE_APPROX_TOLERANCE) {
path.push(el);
}
path.close_path();
path
}
pub fn annular_wedge(
center: Point,
inner_radius: f64,
outer_radius: f64,
start_angle: f64,
sweep_angle: f64,
) -> Path {
let outer_start = point_on_circle(center, outer_radius, start_angle);
let inner_end = point_on_circle(center, inner_radius, start_angle + sweep_angle);
let mut path = Path::new();
path.move_to(outer_start);
let outer_arc = crate::geometry::Arc::new(
center,
Vec2::new(outer_radius, outer_radius),
start_angle,
sweep_angle,
0.0,
);
for el in outer_arc.append_iter(CURVE_APPROX_TOLERANCE) {
path.push(el);
}
path.line_to(inner_end);
let inner_arc = crate::geometry::Arc::new(
center,
Vec2::new(inner_radius, inner_radius),
start_angle + sweep_angle,
-sweep_angle,
0.0,
);
for el in inner_arc.append_iter(CURVE_APPROX_TOLERANCE) {
path.push(el);
}
path.close_path();
path
}
#[cfg(test)]
mod tests {
use super::*;
use crate::geometry::Rect;
use crate::path::PathEl;
fn pt(x: f64, y: f64) -> Point {
Point::new(x, y)
}
#[test]
fn polyline_end_clipped() {
let pts = [pt(0.0, 0.0), pt(5.0, 0.0), pt(5.0, 5.0), pt(10.0, 5.0)];
let opts = PolylineOptions {
clip_start: Some(EndClip::Circle {
center: Point::ORIGIN,
radius: 1.0,
}),
clip_end: Some(EndClip::Rect(Rect::new(9.0, 4.0, 11.0, 6.0))),
};
let path = polyline(&pts, opts);
let mut last_pen: Option<Point> = None;
for el in path.elements() {
match el {
PathEl::MoveTo(p) | PathEl::LineTo(p) => last_pen = Some(*p),
_ => {}
}
}
let first = path
.elements()
.iter()
.find_map(|el| {
if let PathEl::MoveTo(p) = el {
Some(*p)
} else {
None
}
})
.expect("move_to");
assert!((first - Point::ORIGIN).hypot() <= 1.0 + 1e-6);
let last = last_pen.expect("last point");
assert!((last.x - 9.0).abs() < 1e-6);
}
#[test]
fn polyline_then_round_corners_composes() {
let pts = [pt(0.0, 0.0), pt(5.0, 0.0), pt(5.0, 5.0), pt(10.0, 5.0)];
let clipped = clip_polyline(
&pts,
Some(EndClip::Circle {
center: Point::ORIGIN,
radius: 1.0,
}),
Some(EndClip::Rect(Rect::new(9.0, 4.0, 11.0, 6.0))),
);
let path = round_corners(&clipped, false, CornerRounding::default());
let has_curve = path
.elements()
.iter()
.any(|el| matches!(el, PathEl::CurveTo(_, _, _)));
assert!(has_curve);
}
#[test]
fn polygon_with_hole_and_offset() {
let outer = [pt(0.0, 0.0), pt(10.0, 0.0), pt(10.0, 10.0), pt(0.0, 10.0)];
let hole = [pt(3.0, 3.0), pt(7.0, 3.0), pt(7.0, 7.0), pt(3.0, 7.0)];
let rings: [&[Point]; 2] = [&outer, &hole];
let opts = PolygonOptions {
offset: 1.0,
..PolygonOptions::default()
};
let path = polygon(&rings, opts);
let move_count = path
.elements()
.iter()
.filter(|el| matches!(el, PathEl::MoveTo(_)))
.count();
let close_count = path
.elements()
.iter()
.filter(|el| matches!(el, PathEl::ClosePath))
.count();
assert_eq!(move_count, 2, "outer + hole");
assert_eq!(close_count, 2);
}
#[test]
fn polygon_no_offset_returns_input_geometry() {
let sq = [pt(0.0, 0.0), pt(1.0, 0.0), pt(1.0, 1.0), pt(0.0, 1.0)];
let path = polygon(&[&sq], PolygonOptions::default());
let (m, l, q, c) = count_elements(&path);
assert_eq!(m, 1);
assert_eq!(l, 3);
assert_eq!(q, 0);
assert_eq!(c, 1);
}
#[test]
fn offset_then_round_composition() {
let sq = [pt(0.0, 0.0), pt(10.0, 0.0), pt(10.0, 10.0), pt(0.0, 10.0)];
let rings = offset_polygon(&[&sq], 2.0, 4.0);
let mut path = Path::new();
for r in &rings {
let sub = round_corners(r, true, CornerRounding::default());
for el in sub.iter() {
path.push(el);
}
}
let curves = path
.elements()
.iter()
.filter(|el| matches!(el, PathEl::CurveTo(_, _, _)))
.count();
assert!(
curves >= 4,
"rounded inflated square should have at least 4 cubics"
);
}
#[test]
fn polyline_returns_empty_when_under_two_points() {
assert_eq!(
polyline(&[], PolylineOptions::default()).elements().len(),
0
);
assert_eq!(
polyline(&[pt(0.0, 0.0)], PolylineOptions::default())
.elements()
.len(),
0,
);
}
#[test]
fn polygon_returns_empty_when_outer_too_small() {
let r: [&[Point]; 1] = [&[pt(0.0, 0.0), pt(1.0, 0.0)]];
assert_eq!(polygon(&r, PolygonOptions::default()).elements().len(), 0);
}
#[test]
fn shape_constructors_produce_non_empty_paths() {
let r = rect(Rect::new(0.0, 0.0, 10.0, 5.0));
let rr = rounded_rect(Rect::new(0.0, 0.0, 10.0, 5.0), 1.0);
let c = circle(Point::new(0.0, 0.0), 5.0);
let e = ellipse(Point::new(0.0, 0.0), Vec2::new(5.0, 3.0));
for p in [&r, &rr, &c, &e] {
assert!(matches!(p.elements().first(), Some(PathEl::MoveTo(_))));
assert!(
p.elements().len() > 1,
"primitive should have drawing elements past the move_to",
);
}
}
#[test]
fn rect_bounds_match_input() {
let r = Rect::new(1.0, 2.0, 10.0, 8.0);
let p = rect(r);
let b = crate::geometry::Shape::bounding_box(&p);
assert!((b.x0 - 1.0).abs() < 1e-9);
assert!((b.y0 - 2.0).abs() < 1e-9);
assert!((b.x1 - 10.0).abs() < 1e-9);
assert!((b.y1 - 8.0).abs() < 1e-9);
}
#[test]
fn circle_bounds_match_input() {
let p = circle(Point::new(10.0, 20.0), 5.0);
let b = crate::geometry::Shape::bounding_box(&p);
assert!((b.x0 - 5.0).abs() < 0.5);
assert!((b.y0 - 15.0).abs() < 0.5);
assert!((b.x1 - 15.0).abs() < 0.5);
assert!((b.y1 - 25.0).abs() < 0.5);
}
#[test]
fn polyline_path_is_open_and_polygon_path_is_closed() {
let pts = [pt(0.0, 0.0), pt(1.0, 0.0), pt(1.0, 1.0)];
let (m, l, _q, c) = count_elements(&polyline_path(&pts));
assert_eq!((m, l, c), (1, 2, 0));
let (m, l, _q, c) = count_elements(&polygon_path(&pts));
assert_eq!((m, l, c), (1, 2, 1));
}
#[test]
fn path_helpers_ignore_runs_shorter_than_two_vertices() {
assert_eq!(polyline_path(&[]).elements().len(), 0);
assert_eq!(polygon_path(&[pt(0.0, 0.0)]).elements().len(), 0);
let mut path = rect(Rect::new(0.0, 0.0, 1.0, 1.0));
let before = path.elements().len();
append_polyline_to(&mut path, &[pt(5.0, 5.0)], true);
assert_eq!(path.elements().len(), before);
}
#[test]
fn append_polyline_to_adds_one_subpath_per_ring() {
let a = [pt(0.0, 0.0), pt(1.0, 0.0), pt(1.0, 1.0)];
let b = [pt(5.0, 5.0), pt(6.0, 5.0), pt(6.0, 6.0)];
let mut path = Path::new();
append_polyline_to(&mut path, &a, true);
append_polyline_to(&mut path, &b, true);
let (m, l, _q, c) = count_elements(&path);
assert_eq!((m, l, c), (2, 4, 2));
}
#[test]
fn segment_is_move_then_line() {
let p = segment(Point::new(1.0, 2.0), Point::new(3.0, 4.0));
let els: Vec<_> = p.elements().to_vec();
assert_eq!(els.len(), 2);
assert!(matches!(els[0], PathEl::MoveTo(p) if p == Point::new(1.0, 2.0)));
assert!(matches!(els[1], PathEl::LineTo(p) if p == Point::new(3.0, 4.0)));
}
#[test]
fn regular_polygon_vertex_count_matches_n_sides() {
for n in [3, 4, 5, 6, 8, 12] {
let v = regular_polygon_vertices(Point::ORIGIN, 1.0, n);
assert_eq!(v.len(), n);
}
}
#[test]
fn regular_polygon_first_vertex_at_plus_x() {
let v = regular_polygon_vertices(Point::new(10.0, 20.0), 5.0, 6);
assert!((v[0].x - 15.0).abs() < 1e-9);
assert!((v[0].y - 20.0).abs() < 1e-9);
}
#[test]
fn regular_polygon_empty_below_three_sides() {
assert!(regular_polygon_vertices(Point::ORIGIN, 1.0, 2).is_empty());
assert_eq!(regular_polygon(Point::ORIGIN, 1.0, 1).elements().len(), 0);
}
#[test]
fn arc_starts_at_expected_point() {
let path = arc(Point::new(10.0, 0.0), 5.0, 0.0, std::f64::consts::PI);
let first = path
.elements()
.iter()
.find_map(|el| {
if let PathEl::MoveTo(p) = el {
Some(*p)
} else {
None
}
})
.expect("move_to");
assert!((first.x - 15.0).abs() < 1e-9 && first.y.abs() < 1e-9);
}
#[test]
fn wedge_starts_at_center() {
let path = wedge(Point::new(2.0, 3.0), 4.0, 0.0, std::f64::consts::PI / 2.0);
let first = path
.elements()
.iter()
.find_map(|el| {
if let PathEl::MoveTo(p) = el {
Some(*p)
} else {
None
}
})
.expect("move_to");
assert_eq!(first, Point::new(2.0, 3.0));
assert!(path
.elements()
.iter()
.any(|el| matches!(el, PathEl::ClosePath)));
}
#[test]
fn annular_wedge_zero_inner_matches_wedge_bounds() {
let aw = annular_wedge(Point::ORIGIN, 0.0, 5.0, 0.0, std::f64::consts::PI / 2.0);
let w = wedge(Point::ORIGIN, 5.0, 0.0, std::f64::consts::PI / 2.0);
let a = crate::geometry::Shape::bounding_box(&aw);
let b = crate::geometry::Shape::bounding_box(&w);
assert!((a.x0 - b.x0).abs() < 0.1);
assert!((a.y0 - b.y0).abs() < 0.1);
assert!((a.x1 - b.x1).abs() < 0.1);
assert!((a.y1 - b.y1).abs() < 0.1);
}
#[test]
fn full_arc_bounds_match_circle() {
let a = arc(Point::new(0.0, 0.0), 5.0, 0.0, std::f64::consts::TAU);
let b = crate::geometry::Shape::bounding_box(&a);
assert!((b.x0 - (-5.0)).abs() < 0.5);
assert!((b.x1 - 5.0).abs() < 0.5);
assert!((b.y0 - (-5.0)).abs() < 0.5);
assert!((b.y1 - 5.0).abs() < 0.5);
}
#[test]
fn path_to_rings_flattens_polygon_unchanged() {
let sq = [pt(0.0, 0.0), pt(1.0, 0.0), pt(1.0, 1.0), pt(0.0, 1.0)];
let p = polygon(&[&sq], PolygonOptions::default());
let rings = path_to_rings(&p, 0.1);
assert_eq!(rings.len(), 1);
assert_eq!(rings[0].len(), 4);
for (a, b) in rings[0].iter().zip(sq.iter()) {
assert!((a.x - b.x).abs() < 1e-9 && (a.y - b.y).abs() < 1e-9);
}
}
#[test]
fn path_to_rings_flattens_circle_into_many_points() {
let c = circle(Point::ORIGIN, 10.0);
let rings = path_to_rings(&c, 0.1);
assert_eq!(rings.len(), 1);
assert!(rings[0].len() > 8, "circle should flatten to many vertices");
}
#[test]
fn path_to_rings_separates_multi_subpath() {
let outer = [pt(0.0, 0.0), pt(10.0, 0.0), pt(10.0, 10.0), pt(0.0, 10.0)];
let hole = [pt(3.0, 3.0), pt(7.0, 3.0), pt(7.0, 7.0), pt(3.0, 7.0)];
let p = polygon(&[&outer, &hole], PolygonOptions::default());
let rings = path_to_rings(&p, 0.1);
assert_eq!(rings.len(), 2);
}
#[test]
fn path_to_rings_handles_open_polyline() {
let mut p = Path::new();
p.move_to(pt(0.0, 0.0));
p.line_to(pt(1.0, 0.0));
p.line_to(pt(1.0, 1.0));
let rings = path_to_rings(&p, 0.1);
assert_eq!(rings.len(), 1);
assert_eq!(rings[0].len(), 3);
}
#[test]
fn path_to_rings_then_offset_inflates_a_circle() {
let c = circle(Point::ORIGIN, 10.0);
let rings = path_to_rings(&c, 0.5);
let refs: Vec<&[Point]> = rings.iter().map(Vec::as_slice).collect();
let out = offset_polygon(&refs, 5.0, 4.0);
assert_eq!(out.len(), 1);
let (mut x0, mut x1, mut y0, mut y1) = (
f64::INFINITY,
f64::NEG_INFINITY,
f64::INFINITY,
f64::NEG_INFINITY,
);
for p in &out[0] {
x0 = x0.min(p.x);
x1 = x1.max(p.x);
y0 = y0.min(p.y);
y1 = y1.max(p.y);
}
assert!((x0 - (-15.0)).abs() < 0.5);
assert!((x1 - 15.0).abs() < 0.5);
assert!((y0 - (-15.0)).abs() < 0.5);
assert!((y1 - 15.0).abs() < 0.5);
}
#[test]
fn round_corners_works_on_externally_built_vertex_sequence() {
let verts = regular_polygon_vertices(Point::ORIGIN, 10.0, 6);
let path = round_corners(&verts, true, CornerRounding::default());
let curves = path
.elements()
.iter()
.filter(|el| matches!(el, PathEl::CurveTo(_, _, _)))
.count();
assert_eq!(curves, 6, "one cubic per corner of the hexagon");
}
#[test]
fn regular_polygon_composes_with_polygon_for_holes() {
let outer = regular_polygon_vertices(Point::ORIGIN, 10.0, 6);
let hole = regular_polygon_vertices(Point::ORIGIN, 4.0, 6);
let path = polygon(&[&outer, &hole], PolygonOptions::default());
let move_count = path
.elements()
.iter()
.filter(|el| matches!(el, PathEl::MoveTo(_)))
.count();
assert_eq!(move_count, 2, "outer + hole");
}
fn count_elements(path: &Path) -> (usize, usize, usize, usize) {
let mut m = 0;
let mut l = 0;
let mut q = 0;
let mut c = 0;
for el in path.elements() {
match el {
PathEl::MoveTo(_) => m += 1,
PathEl::LineTo(_) => l += 1,
PathEl::QuadTo(_, _) => q += 1,
PathEl::ClosePath => c += 1,
_ => {}
}
}
(m, l, q, c)
}
}