use glam::Vec2;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum FillRule {
#[default]
NonZero,
EvenOdd,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Verb {
MoveTo,
LineTo,
QuadTo,
CubicTo,
Close,
}
impl Verb {
pub const fn point_count(self) -> usize {
match self {
Self::MoveTo | Self::LineTo => 1,
Self::QuadTo => 2,
Self::CubicTo => 3,
Self::Close => 0,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Rect {
pub min: Vec2,
pub max: Vec2,
}
impl Rect {
pub fn new(min: Vec2, max: Vec2) -> Self {
Self { min, max }
}
pub fn empty() -> Self {
Self {
min: Vec2::splat(f32::INFINITY),
max: Vec2::splat(f32::NEG_INFINITY),
}
}
pub fn is_empty(&self) -> bool {
self.min.x > self.max.x || self.min.y > self.max.y
}
pub fn union_point(&mut self, p: Vec2) {
self.min = self.min.min(p);
self.max = self.max.max(p);
}
pub fn contains(&self, p: Vec2) -> bool {
p.x >= self.min.x && p.x <= self.max.x && p.y >= self.min.y && p.y <= self.max.y
}
pub fn width(&self) -> f32 {
(self.max.x - self.min.x).max(0.0)
}
pub fn height(&self) -> f32 {
(self.max.y - self.min.y).max(0.0)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Convexity {
Convex,
Concave,
}
#[derive(Debug, Clone, Default, PartialEq)]
pub struct Path {
verbs: Vec<Verb>,
points: Vec<Vec2>,
fill_rule: FillRule,
rounded_rect: Option<(Rect, f32)>,
}
pub const MAX_COORDINATE: f32 = 16_777_216.0;
impl Path {
pub fn builder() -> PathBuilder {
PathBuilder::new()
}
pub fn as_rounded_rect(&self) -> Option<(Rect, f32)> {
self.rounded_rect
}
pub fn verbs(&self) -> &[Verb] {
&self.verbs
}
pub fn points(&self) -> &[Vec2] {
&self.points
}
pub fn is_finite(&self) -> bool {
self.points.iter().all(|p| p.is_finite())
}
pub fn is_within_tessellation_range(&self) -> bool {
self.points
.iter()
.all(|p| p.x.abs() <= MAX_COORDINATE && p.y.abs() <= MAX_COORDINATE)
}
pub fn fill_rule(&self) -> FillRule {
self.fill_rule
}
pub fn is_empty(&self) -> bool {
self.verbs.is_empty()
}
pub fn bounds(&self) -> Rect {
let mut r = Rect::empty();
for p in &self.points {
r.union_point(*p);
}
r
}
pub fn segments(&self) -> impl Iterator<Item = (Verb, &[Vec2])> {
let mut cursor = 0usize;
self.verbs.iter().map(move |verb| {
let n = verb.point_count();
let slice = &self.points[cursor..cursor + n];
cursor += n;
(*verb, slice)
})
}
pub fn convexity(&self) -> Convexity {
if self
.verbs
.iter()
.any(|v| matches!(v, Verb::QuadTo | Verb::CubicTo))
{
return Convexity::Concave;
}
if self.verbs.iter().filter(|v| **v == Verb::MoveTo).count() > 1 {
return Convexity::Concave;
}
polygon_convexity(&self.points)
}
}
fn quadrant(v: Vec2) -> i32 {
match (v.x >= 0.0, v.y >= 0.0) {
(true, true) => 0,
(false, true) => 1,
(false, false) => 2,
(true, false) => 3,
}
}
pub fn polygon_convexity(points: &[Vec2]) -> Convexity {
if points.len() < 3 {
return Convexity::Convex;
}
let n = points.len();
let mut sign = 0i32;
let (mut counter_clockwise, mut clockwise) = (0i32, 0i32);
for i in 0..n {
let a = points[i];
let b = points[(i + 1) % n];
if a == b {
continue;
}
let incoming = b - a;
let mut k = (i + 2) % n;
let mut skipped = 0;
while points[k] == b && skipped < n {
k = (k + 1) % n;
skipped += 1;
}
let c = points[k];
if c == b {
return Convexity::Convex;
}
let outgoing = c - b;
let cross = incoming.perp_dot(outgoing);
let advance = (quadrant(outgoing) - quadrant(incoming)).rem_euclid(4);
counter_clockwise += advance;
clockwise += (-advance).rem_euclid(4);
if cross.abs() <= f32::EPSILON {
continue;
}
let s = if cross > 0.0 { 1 } else { -1 };
if sign == 0 {
sign = s;
} else if sign != s {
return Convexity::Concave;
}
}
if sign == 0 {
return Convexity::Convex;
}
let turned = if sign > 0 {
counter_clockwise
} else {
clockwise
};
if turned > 4 {
return Convexity::Concave;
}
Convexity::Convex
}
#[derive(Debug, Clone, Default)]
pub struct PathBuilder {
verbs: Vec<Verb>,
points: Vec<Vec2>,
fill_rule: FillRule,
subpath_start: Option<Vec2>,
current: Option<Vec2>,
rounded_rect: Option<(Rect, f32)>,
}
impl PathBuilder {
pub fn new() -> Self {
Self::default()
}
pub fn with_fill_rule(mut self, rule: FillRule) -> Self {
self.fill_rule = rule;
self
}
pub fn move_to(&mut self, p: Vec2) -> &mut Self {
self.verbs.push(Verb::MoveTo);
self.points.push(p);
self.subpath_start = Some(p);
self.current = Some(p);
self
}
pub fn line_to(&mut self, p: Vec2) -> &mut Self {
self.ensure_started();
self.verbs.push(Verb::LineTo);
self.points.push(p);
self.current = Some(p);
self
}
pub fn quad_to(&mut self, ctrl: Vec2, to: Vec2) -> &mut Self {
self.ensure_started();
self.verbs.push(Verb::QuadTo);
self.points.push(ctrl);
self.points.push(to);
self.current = Some(to);
self
}
pub fn conic_to(&mut self, ctrl: Vec2, to: Vec2, weight: f32) -> &mut Self {
self.ensure_started();
let from = self.current.unwrap_or(ctrl);
if !weight.is_finite() || weight <= 0.0 || !ctrl.is_finite() || !to.is_finite() {
return self.line_to(to);
}
self.push_conic(from, ctrl, to, weight, 0);
self
}
fn push_conic(&mut self, from: Vec2, ctrl: Vec2, to: Vec2, weight: f32, depth: u32) {
const NEAR_ONE: f32 = 1e-3;
const MAX_DEPTH: u32 = 6;
const RELATIVE_ERROR: f32 = 1e-3;
if (weight - 1.0).abs() <= NEAR_ONE || depth >= MAX_DEPTH {
self.quad_to(ctrl, to);
return;
}
let conic_mid = (from + ctrl * (2.0 * weight) + to) / (2.0 * (1.0 + weight));
let quad_mid = (from + ctrl * 2.0 + to) * 0.25;
let chord = (to - from).length();
if chord > 0.0 && (conic_mid - quad_mid).length() <= RELATIVE_ERROR * chord {
self.quad_to(ctrl, to);
return;
}
let scale = 1.0 / (1.0 + weight);
let left_ctrl = (from + ctrl * weight) * scale;
let right_ctrl = (ctrl * weight + to) * scale;
let mid = (from + ctrl * (2.0 * weight) + to) * (0.5 * scale);
let split_weight = ((1.0 + weight) * 0.5).sqrt();
self.push_conic(from, left_ctrl, mid, split_weight, depth + 1);
self.push_conic(mid, right_ctrl, to, split_weight, depth + 1);
}
pub fn cubic_to(&mut self, c0: Vec2, c1: Vec2, to: Vec2) -> &mut Self {
self.ensure_started();
self.verbs.push(Verb::CubicTo);
self.points.push(c0);
self.points.push(c1);
self.points.push(to);
self.current = Some(to);
self
}
pub fn arc(&mut self, center: Vec2, radii: Vec2, start: f32, sweep: f32) -> &mut Self {
if !center.is_finite() || !radii.is_finite() || !start.is_finite() || !sweep.is_finite() {
return self;
}
let point_at = |angle: f32| {
Vec2::new(
center.x + radii.x * angle.cos(),
center.y + radii.y * angle.sin(),
)
};
let first = point_at(start);
match self.current {
Some(_) => {
self.line_to(first);
}
None => {
self.move_to(first);
}
}
let turn = std::f32::consts::TAU;
let sweep = sweep.clamp(-turn, turn);
if sweep == 0.0 {
return self;
}
let segments = (sweep.abs() / std::f32::consts::FRAC_PI_2).ceil().max(1.0);
let step = sweep / segments;
let k = (4.0 / 3.0) * (step / 4.0).tan();
let mut angle = start;
for _ in 0..segments as u32 {
let next = angle + step;
let (from, to) = (point_at(angle), point_at(next));
let d_from = Vec2::new(-radii.x * angle.sin(), radii.y * angle.cos());
let d_to = Vec2::new(-radii.x * next.sin(), radii.y * next.cos());
self.cubic_to(from + d_from * k, to - d_to * k, to);
angle = next;
}
self
}
pub fn close(&mut self) -> &mut Self {
if self.verbs.is_empty() {
return self;
}
self.verbs.push(Verb::Close);
self.current = self.subpath_start;
self
}
pub fn current_point(&self) -> Option<Vec2> {
self.current
}
fn ensure_started(&mut self) {
if self.verbs.is_empty() {
self.verbs.push(Verb::MoveTo);
self.points.push(Vec2::ZERO);
self.subpath_start = Some(Vec2::ZERO);
self.current = Some(Vec2::ZERO);
}
}
pub fn as_rounded_rect(&mut self, rect: Rect, radius: f32) -> &mut Self {
let finite = rect.min.x.is_finite()
&& rect.min.y.is_finite()
&& rect.max.x.is_finite()
&& rect.max.y.is_finite()
&& radius.is_finite();
self.rounded_rect = finite.then(|| (rect, radius.max(0.0)));
self
}
pub fn build(self) -> Path {
Path {
verbs: self.verbs,
points: self.points,
fill_rule: self.fill_rule,
rounded_rect: self.rounded_rect,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn verb_point_counts_match_what_the_builder_pushes() {
let mut b = PathBuilder::new();
b.move_to(Vec2::ZERO)
.line_to(Vec2::new(1.0, 0.0))
.quad_to(Vec2::new(2.0, 1.0), Vec2::new(3.0, 0.0))
.cubic_to(
Vec2::new(4.0, 1.0),
Vec2::new(5.0, -1.0),
Vec2::new(6.0, 0.0),
)
.close();
let path = b.build();
let expected: usize = path.verbs().iter().map(|v| v.point_count()).sum();
assert_eq!(expected, path.points().len());
}
#[test]
fn segments_walk_verbs_and_points_in_step() {
let mut b = PathBuilder::new();
b.move_to(Vec2::ZERO)
.quad_to(Vec2::new(1.0, 1.0), Vec2::new(2.0, 0.0));
let path = b.build();
let collected: Vec<_> = path.segments().collect();
assert_eq!(collected.len(), 2);
assert_eq!(collected[0].0, Verb::MoveTo);
assert_eq!(collected[0].1, &[Vec2::ZERO]);
assert_eq!(collected[1].0, Verb::QuadTo);
assert_eq!(collected[1].1, &[Vec2::new(1.0, 1.0), Vec2::new(2.0, 0.0)]);
}
#[test]
fn line_without_move_starts_at_origin() {
let mut b = PathBuilder::new();
b.line_to(Vec2::new(1.0, 1.0));
let path = b.build();
assert_eq!(path.verbs()[0], Verb::MoveTo);
assert_eq!(path.points()[0], Vec2::ZERO);
}
#[test]
fn closing_an_empty_builder_is_a_no_op() {
let path = {
let mut b = PathBuilder::new();
b.close();
b.build()
};
assert!(path.is_empty());
}
#[test]
fn close_returns_the_pen_to_the_subpath_start() {
let mut b = PathBuilder::new();
b.move_to(Vec2::new(5.0, 5.0))
.line_to(Vec2::new(9.0, 5.0))
.close();
assert_eq!(b.current_point(), Some(Vec2::new(5.0, 5.0)));
}
#[test]
fn bounds_cover_every_control_point() {
let mut b = PathBuilder::new();
b.move_to(Vec2::new(0.0, 0.0)).cubic_to(
Vec2::new(0.0, 10.0),
Vec2::new(10.0, 10.0),
Vec2::new(10.0, 0.0),
);
let path = b.build();
let bounds = path.bounds();
for p in path.points() {
assert!(bounds.contains(*p), "bounds must contain {p:?}");
}
assert_eq!(bounds.max.y, 10.0);
}
#[test]
fn empty_path_has_empty_bounds() {
let path = Path::default();
assert!(path.bounds().is_empty());
assert_eq!(path.bounds().width(), 0.0);
}
#[test]
fn square_is_convex_and_chevron_is_not() {
let square = [
Vec2::new(0.0, 0.0),
Vec2::new(1.0, 0.0),
Vec2::new(1.0, 1.0),
Vec2::new(0.0, 1.0),
];
assert_eq!(polygon_convexity(&square), Convexity::Convex);
let chevron = [
Vec2::new(0.0, 0.0),
Vec2::new(2.0, 1.0),
Vec2::new(4.0, 0.0),
Vec2::new(2.0, 4.0),
];
assert_eq!(polygon_convexity(&chevron), Convexity::Concave);
}
#[test]
fn collinear_points_do_not_break_convexity() {
let with_collinear = [
Vec2::new(0.0, 0.0),
Vec2::new(1.0, 0.0),
Vec2::new(2.0, 0.0),
Vec2::new(2.0, 2.0),
Vec2::new(0.0, 2.0),
];
assert_eq!(polygon_convexity(&with_collinear), Convexity::Convex);
}
#[test]
fn convexity_is_conservative_about_curves_and_subpaths() {
let mut b = PathBuilder::new();
b.move_to(Vec2::ZERO)
.quad_to(Vec2::new(1.0, 1.0), Vec2::new(2.0, 0.0));
assert_eq!(b.build().convexity(), Convexity::Concave);
let mut b = PathBuilder::new();
b.move_to(Vec2::ZERO)
.line_to(Vec2::new(1.0, 0.0))
.close()
.move_to(Vec2::new(5.0, 5.0))
.line_to(Vec2::new(6.0, 5.0));
assert_eq!(b.build().convexity(), Convexity::Concave);
}
#[test]
fn degenerate_polygons_are_trivially_convex() {
assert_eq!(polygon_convexity(&[]), Convexity::Convex);
assert_eq!(polygon_convexity(&[Vec2::ZERO]), Convexity::Convex);
assert_eq!(
polygon_convexity(&[Vec2::ZERO, Vec2::new(1.0, 1.0)]),
Convexity::Convex
);
}
#[test]
fn a_repeated_point_does_not_make_a_convex_contour_concave() {
let squared_off = [
Vec2::new(0.0, 0.0),
Vec2::new(10.0, 0.0),
Vec2::new(10.0, 0.0),
Vec2::new(10.0, 10.0),
Vec2::new(0.0, 10.0),
Vec2::new(0.0, 10.0),
];
assert_eq!(polygon_convexity(&squared_off), Convexity::Convex);
let repeated_at_the_wrap = [
Vec2::new(0.0, 0.0),
Vec2::new(10.0, 0.0),
Vec2::new(10.0, 10.0),
Vec2::new(0.0, 10.0),
Vec2::new(0.0, 0.0),
];
assert_eq!(polygon_convexity(&repeated_at_the_wrap), Convexity::Convex);
}
#[test]
fn a_repeated_point_does_not_hide_a_concavity() {
let doubled_back = [
Vec2::new(50.0, 70.0),
Vec2::new(0.0, 70.0),
Vec2::new(0.0, 0.0),
Vec2::new(40.0, 70.0),
Vec2::new(40.0, 70.0),
];
assert_eq!(polygon_convexity(&doubled_back), Convexity::Concave);
}
fn points_of(path: &Path) -> Vec<Vec2> {
crate::flatten::flatten(path, 0.01)
.into_iter()
.flatten()
.collect()
}
#[test]
fn an_arc_stays_on_its_circle() {
for sweep in [
std::f32::consts::FRAC_PI_6,
std::f32::consts::FRAC_PI_2,
2.0,
std::f32::consts::PI,
-std::f32::consts::PI,
std::f32::consts::TAU,
] {
let mut builder = PathBuilder::new();
builder.arc(Vec2::new(50.0, 50.0), Vec2::splat(40.0), 0.3, sweep);
let path = builder.build();
let worst = points_of(&path)
.iter()
.map(|p| ((*p - Vec2::new(50.0, 50.0)).length() - 40.0).abs())
.fold(0.0f32, f32::max);
assert!(
worst < 0.05,
"a sweep of {sweep} strays {worst} from a radius of forty"
);
}
}
#[test]
fn an_arc_begins_and_ends_where_it_was_asked_to() {
let center = Vec2::new(10.0, 20.0);
let radii = Vec2::new(30.0, 30.0);
let (start, sweep) = (0.5f32, 1.7f32);
let mut builder = PathBuilder::new();
builder.arc(center, radii, start, sweep);
let points = points_of(&builder.build());
let want_first = center + Vec2::new(radii.x * start.cos(), radii.y * start.sin());
let end = start + sweep;
let want_last = center + Vec2::new(radii.x * end.cos(), radii.y * end.sin());
assert!((points[0] - want_first).length() < 0.01, "{:?}", points[0]);
assert!(
(*points.last().unwrap() - want_last).length() < 0.01,
"{:?}",
points.last()
);
}
#[test]
fn a_negative_sweep_travels_the_other_way() {
let center = Vec2::ZERO;
let arc_of = |sweep: f32| {
let mut builder = PathBuilder::new();
builder.arc(center, Vec2::splat(10.0), 0.0, sweep);
points_of(&builder.build())
};
let forward = arc_of(std::f32::consts::FRAC_PI_2);
let backward = arc_of(-std::f32::consts::FRAC_PI_2);
assert!(forward.iter().all(|p| p.y >= -0.01), "forward dipped");
assert!(backward.iter().all(|p| p.y <= 0.01), "backward rose");
}
#[test]
fn an_elliptical_arc_follows_the_ellipse() {
let (center, radii) = (Vec2::new(0.0, 0.0), Vec2::new(60.0, 20.0));
let mut builder = PathBuilder::new();
builder.arc(center, radii, 0.0, std::f32::consts::TAU);
let worst = points_of(&builder.build())
.iter()
.map(|p| {
let (x, y) = (p.x / radii.x, p.y / radii.y);
(x * x + y * y - 1.0).abs()
})
.fold(0.0f32, f32::max);
assert!(worst < 0.002, "off the ellipse by {worst}");
}
#[test]
fn an_arc_after_a_move_is_joined_by_a_line() {
let mut builder = PathBuilder::new();
builder.move_to(Vec2::ZERO);
builder.arc(Vec2::ZERO, Vec2::splat(10.0), 0.0, 1.0);
builder.close();
let path = builder.build();
assert_eq!(path.verbs()[0], Verb::MoveTo);
assert_eq!(
path.verbs()[1],
Verb::LineTo,
"the arc did not join the current point"
);
assert!(points_of(&path).iter().any(|p| p.length() < 0.01));
}
#[test]
fn an_arc_on_an_empty_builder_starts_with_a_move() {
let mut builder = PathBuilder::new();
builder.arc(Vec2::ZERO, Vec2::splat(10.0), 0.0, 1.0);
assert_eq!(builder.build().verbs()[0], Verb::MoveTo);
}
#[test]
fn a_degenerate_arc_adds_no_curve() {
let mut builder = PathBuilder::new();
builder.arc(Vec2::ZERO, Vec2::splat(10.0), 0.0, 0.0);
let path = builder.build();
assert_eq!(path.verbs(), &[Verb::MoveTo]);
let mut builder = PathBuilder::new();
builder.arc(Vec2::ZERO, Vec2::splat(f32::NAN), 0.0, 1.0);
assert!(builder.build().is_empty());
}
#[test]
fn a_sweep_past_a_full_turn_is_one_turn() {
let count = |sweep| {
let mut builder = PathBuilder::new();
builder.arc(Vec2::ZERO, Vec2::splat(10.0), 0.0, sweep);
builder.build().verbs().len()
};
assert_eq!(
count(std::f32::consts::TAU),
count(std::f32::consts::TAU * 3.0)
);
}
}
#[cfg(test)]
mod conic_tests {
use super::*;
fn points_of(path: &Path) -> Vec<Vec2> {
crate::flatten::flatten(path, 0.01)
.into_iter()
.flatten()
.collect()
}
#[test]
fn a_conic_of_weight_one_is_the_quadratic_it_already_was() {
let mut conic = PathBuilder::new();
conic.move_to(Vec2::new(0.0, 0.0)).conic_to(
Vec2::new(50.0, 100.0),
Vec2::new(100.0, 0.0),
1.0,
);
let mut quad = PathBuilder::new();
quad.move_to(Vec2::new(0.0, 0.0))
.quad_to(Vec2::new(50.0, 100.0), Vec2::new(100.0, 0.0));
assert_eq!(points_of(&conic.build()), points_of(&quad.build()));
}
#[test]
fn a_conic_of_the_circular_weight_traces_a_circle() {
const R: f32 = 100.0;
let mut b = PathBuilder::new();
b.move_to(Vec2::new(R, 0.0)).conic_to(
Vec2::new(R, R),
Vec2::new(0.0, R),
std::f32::consts::FRAC_1_SQRT_2,
);
let points = points_of(&b.build());
assert!(points.len() > 8, "a quarter turn should not be two lines");
let worst = points
.iter()
.map(|p| (p.length() - R).abs())
.fold(0.0f32, f32::max);
assert!(
worst < 0.05,
"the arc strays {worst} from the circle it is supposed to be"
);
}
#[test]
fn a_weight_that_describes_no_curve_gives_the_line_between_the_ends() {
for weight in [0.0, -1.0, f32::NAN, f32::INFINITY] {
let mut b = PathBuilder::new();
b.move_to(Vec2::new(0.0, 0.0)).conic_to(
Vec2::new(50.0, 100.0),
Vec2::new(100.0, 0.0),
weight,
);
let points = points_of(&b.build());
assert_eq!(
points,
vec![Vec2::new(0.0, 0.0), Vec2::new(100.0, 0.0)],
"a weight of {weight} should give a straight line"
);
}
}
#[test]
fn a_hyperbolic_conic_still_converges_and_stays_inside_its_hull() {
let (a, c, e) = (
Vec2::new(0.0, 0.0),
Vec2::new(50.0, 100.0),
Vec2::new(100.0, 0.0),
);
let mut b = PathBuilder::new();
b.move_to(a).conic_to(c, e, 8.0);
for p in points_of(&b.build()) {
assert!(
p.y >= -0.01 && p.y <= c.y + 0.01 && p.x >= -0.01 && p.x <= e.x + 0.01,
"{p:?} is outside the triangle its own control points make"
);
}
}
}