use crate::types::{Fixed, Point, Rect};
use alloc::borrow::Cow;
use alloc::vec::Vec;
#[derive(Clone, Debug, PartialEq)]
pub enum PathCmd {
MoveTo(Point),
LineTo(Point),
QuadTo {
ctrl: Point,
end: Point,
},
CubicTo {
ctrl1: Point,
ctrl2: Point,
end: Point,
},
Close,
}
#[derive(Clone, Debug, Default, PartialEq)]
pub struct Path {
pub cmds: Cow<'static, [PathCmd]>,
}
impl Path {
pub fn new() -> Self {
Self {
cmds: Cow::Owned(Vec::new()),
}
}
pub const fn from_static(cmds: &'static [PathCmd]) -> Self {
Self {
cmds: Cow::Borrowed(cmds),
}
}
pub fn from_owned(cmds: Vec<PathCmd>) -> Self {
Self {
cmds: Cow::Owned(cmds),
}
}
pub fn move_to(&mut self, p: Point) -> &mut Self {
self.cmds.to_mut().push(PathCmd::MoveTo(p));
self
}
pub fn line_to(&mut self, p: Point) -> &mut Self {
self.cmds.to_mut().push(PathCmd::LineTo(p));
self
}
pub fn quad_to(&mut self, ctrl: Point, end: Point) -> &mut Self {
self.cmds.to_mut().push(PathCmd::QuadTo { ctrl, end });
self
}
pub fn cubic_to(&mut self, ctrl1: Point, ctrl2: Point, end: Point) -> &mut Self {
self.cmds
.to_mut()
.push(PathCmd::CubicTo { ctrl1, ctrl2, end });
self
}
pub fn close(&mut self) -> &mut Self {
self.cmds.to_mut().push(PathCmd::Close);
self
}
pub fn rect(x: Fixed, y: Fixed, w: Fixed, h: Fixed) -> Self {
let mut p = Self::new();
let tl = Point { x, y };
let tr = Point { x: x + w, y };
let br = Point { x: x + w, y: y + h };
let bl = Point { x, y: y + h };
p.move_to(tl).line_to(tr).line_to(br).line_to(bl).close();
p
}
pub fn rounded_quad(q: &[Point; 4], r: Fixed) -> Self {
let mut min_edge = Fixed::MAX;
let edges: [(Point, Point); 4] = [(q[0], q[1]), (q[1], q[2]), (q[2], q[3]), (q[3], q[0])];
let mut edge_lens = [Fixed::ZERO; 4];
let mut edge_dirs = [Point::ZERO; 4];
for i in 0..4 {
let (a, b) = edges[i];
let dx = b.x - a.x;
let dy = b.y - a.y;
let len = (dx * dx + dy * dy).sqrt();
edge_lens[i] = len;
if len > Fixed::ZERO {
edge_dirs[i] = Point {
x: dx / len,
y: dy / len,
};
}
if len < min_edge {
min_edge = len;
}
}
let r = r.min(min_edge / 2).max(Fixed::ZERO);
if r == Fixed::ZERO {
let mut p = Self::new();
p.move_to(q[0]);
p.line_to(q[1]);
p.line_to(q[2]);
p.line_to(q[3]);
p.close();
return p;
}
let k = r * Fixed::from_f32(0.552_284_8);
let mut seg_start = [Point::ZERO; 4];
let mut seg_end = [Point::ZERO; 4];
for i in 0..4 {
let (a, b) = edges[i];
let ue = edge_dirs[i];
seg_start[i] = Point {
x: a.x + ue.x * r,
y: a.y + ue.y * r,
};
seg_end[i] = Point {
x: b.x - ue.x * r,
y: b.y - ue.y * r,
};
}
let mut p = Self::new();
p.move_to(seg_start[0]);
for i in 0..4 {
p.line_to(seg_end[i]);
let ue_in = edge_dirs[i];
let j = (i + 1) & 3;
let ue_out = edge_dirs[j];
let c1 = Point {
x: seg_end[i].x + ue_in.x * k,
y: seg_end[i].y + ue_in.y * k,
};
let c2 = Point {
x: seg_start[j].x - ue_out.x * k,
y: seg_start[j].y - ue_out.y * k,
};
p.cubic_to(c1, c2, seg_start[j]);
}
p.close();
p
}
pub fn rounded_rect(x: Fixed, y: Fixed, w: Fixed, h: Fixed, r: Fixed) -> Self {
if r == Fixed::ZERO {
return Self::rect(x, y, w, h);
}
let r = r.min(w / 2).min(h / 2);
let k = r * Fixed::from_f32(0.552_284_8);
let mut p = Self::new();
let x1 = x + r;
let x2 = x + w - r;
let y1 = y + r;
let y2 = y + h - r;
p.move_to(Point { x: x1, y });
p.line_to(Point { x: x2, y });
p.cubic_to(
Point { x: x2 + k, y },
Point {
x: x + w,
y: y1 - k,
},
Point { x: x + w, y: y1 },
);
p.line_to(Point { x: x + w, y: y2 });
p.cubic_to(
Point {
x: x + w,
y: y2 + k,
},
Point {
x: x2 + k,
y: y + h,
},
Point { x: x2, y: y + h },
);
p.line_to(Point { x: x1, y: y + h });
p.cubic_to(
Point {
x: x1 - k,
y: y + h,
},
Point { x, y: y2 + k },
Point { x, y: y2 },
);
p.line_to(Point { x, y: y1 });
p.cubic_to(
Point { x, y: y1 - k },
Point { x: x1 - k, y },
Point { x: x1, y },
);
p.close();
p
}
pub fn bbox(&self) -> Option<Rect> {
bbox_of_cmds(&self.cmds)
}
}
pub fn bbox_of_cmds(cmds: &[PathCmd]) -> Option<Rect> {
bbox_of_cmds_transformed(cmds, None)
}
pub fn bbox_of_cmds_transformed(
cmds: &[PathCmd],
transform: Option<&crate::types::Transform>,
) -> Option<Rect> {
let mut xmin = Fixed::MAX;
let mut ymin = Fixed::MAX;
let mut xmax = Fixed::MIN;
let mut ymax = Fixed::MIN;
let mut seen = false;
let mut visit = |p: Point| {
let p = match transform {
Some(tf) => tf.apply_point(p),
None => p,
};
seen = true;
if p.x < xmin {
xmin = p.x;
}
if p.x > xmax {
xmax = p.x;
}
if p.y < ymin {
ymin = p.y;
}
if p.y > ymax {
ymax = p.y;
}
};
for cmd in cmds {
match cmd {
PathCmd::MoveTo(p) | PathCmd::LineTo(p) => visit(*p),
PathCmd::QuadTo { ctrl, end } => {
visit(*ctrl);
visit(*end);
}
PathCmd::CubicTo { ctrl1, ctrl2, end } => {
visit(*ctrl1);
visit(*ctrl2);
visit(*end);
}
PathCmd::Close => {}
}
}
if !seen {
return None;
}
Some(Rect {
x: xmin,
y: ymin,
w: xmax - xmin,
h: ymax - ymin,
})
}
impl Path {
pub fn arc(center: Point, radius: Fixed, start_angle: Fixed, end_angle: Fixed) -> Self {
let mut p = Self::new();
if radius <= Fixed::ZERO {
return p;
}
let sweep = end_angle - start_angle;
if sweep == Fixed::ZERO {
return p;
}
let on_circle = |angle_deg: Fixed| -> Point {
Point {
x: center.x + Fixed::cos_deg(angle_deg) * radius,
y: center.y + Fixed::sin_deg(angle_deg) * radius,
}
};
p.move_to(on_circle(start_angle));
let ninety = Fixed::from_int(90);
let dir = if sweep > Fixed::ZERO {
Fixed::ONE
} else {
-Fixed::ONE
};
let remaining_init = sweep.abs();
let mut a = start_angle;
let mut remaining = remaining_init;
while remaining > Fixed::ZERO {
let step = remaining.min(ninety) * dir;
let a_next = a + step;
let quarter = step / 4;
let k = Fixed::sin_deg(quarter) / Fixed::cos_deg(quarter) * Fixed::from_int(4) / 3;
let t0 = tangent_on_circle(a);
let t1 = tangent_on_circle(a_next);
let p0 = on_circle(a);
let p3 = on_circle(a_next);
let p1 = Point {
x: p0.x + t0.x * k * radius,
y: p0.y + t0.y * k * radius,
};
let p2 = Point {
x: p3.x - t1.x * k * radius,
y: p3.y - t1.y * k * radius,
};
p.cubic_to(p1, p2, p3);
a = a_next;
remaining -= ninety;
}
p
}
}
fn tangent_on_circle(angle_deg: Fixed) -> Point {
Point {
x: -Fixed::sin_deg(angle_deg),
y: Fixed::cos_deg(angle_deg),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn bbox_empty_returns_none() {
let p = Path::new();
assert!(p.bbox().is_none());
}
#[test]
fn bbox_rect_matches_source() {
let p = Path::rect(
Fixed::from_int(10),
Fixed::from_int(20),
Fixed::from_int(30),
Fixed::from_int(40),
);
let bb = p.bbox().unwrap();
assert_eq!(bb.x.to_int(), 10);
assert_eq!(bb.y.to_int(), 20);
assert_eq!(bb.w.to_int(), 30);
assert_eq!(bb.h.to_int(), 40);
}
#[test]
fn bbox_rounded_rect_is_conservative_superset() {
let p = Path::rounded_rect(
Fixed::from_int(0),
Fixed::from_int(0),
Fixed::from_int(20),
Fixed::from_int(20),
Fixed::from_int(4),
);
let bb = p.bbox().unwrap();
assert_eq!(bb.x.to_int(), 0);
assert_eq!(bb.y.to_int(), 0);
assert_eq!(bb.w.to_int(), 20);
assert_eq!(bb.h.to_int(), 20);
}
#[test]
fn arc_empty_for_zero_sweep() {
let p = Path::arc(
Point {
x: Fixed::from_int(50),
y: Fixed::from_int(50),
},
Fixed::from_int(10),
Fixed::from_int(45),
Fixed::from_int(45),
);
assert!(p.cmds.is_empty());
}
#[test]
fn arc_empty_for_zero_radius() {
let p = Path::arc(
Point::ZERO,
Fixed::ZERO,
Fixed::from_int(0),
Fixed::from_int(90),
);
assert!(p.cmds.is_empty());
}
#[test]
fn arc_quarter_circle_starts_and_ends_on_circle() {
let center = Point {
x: Fixed::from_int(50),
y: Fixed::from_int(50),
};
let r = Fixed::from_int(20);
let p = Path::arc(center, r, Fixed::from_int(0), Fixed::from_int(90));
let eps = Fixed::from_f32(0.1);
let PathCmd::MoveTo(start) = &p.cmds[0] else {
panic!("expected MoveTo first")
};
assert!((start.x - (center.x + r)).abs() <= eps);
assert!((start.y - center.y).abs() <= eps);
let PathCmd::CubicTo { end, .. } = p.cmds.last().unwrap() else {
panic!("expected CubicTo last")
};
assert!((end.x - center.x).abs() <= eps);
assert!((end.y - (center.y + r)).abs() <= eps);
}
#[test]
fn arc_full_circle_uses_four_segments() {
let p = Path::arc(
Point::ZERO,
Fixed::from_int(10),
Fixed::from_int(0),
Fixed::from_int(360),
);
assert_eq!(p.cmds.len(), 5);
let cubic_count = p
.cmds
.iter()
.filter(|c| matches!(c, PathCmd::CubicTo { .. }))
.count();
assert_eq!(cubic_count, 4);
}
#[test]
fn arc_negative_sweep_goes_clockwise() {
let p = Path::arc(
Point::ZERO,
Fixed::from_int(10),
Fixed::from_int(0),
Fixed::from_int(-90),
);
assert!(!p.cmds.is_empty());
let PathCmd::CubicTo { end, .. } = p.cmds.last().unwrap() else {
panic!()
};
let eps = Fixed::from_f32(0.1);
assert!(end.x.abs() <= eps);
assert!((end.y - Fixed::from_int(-10)).abs() <= eps);
}
#[test]
fn rounded_quad_zero_radius_is_plain_polygon() {
let q = [
Point {
x: Fixed::from_int(0),
y: Fixed::from_int(0),
},
Point {
x: Fixed::from_int(10),
y: Fixed::from_int(0),
},
Point {
x: Fixed::from_int(10),
y: Fixed::from_int(10),
},
Point {
x: Fixed::from_int(0),
y: Fixed::from_int(10),
},
];
let p = Path::rounded_quad(&q, Fixed::ZERO);
let mut has_cubic = false;
for c in p.cmds.iter() {
if matches!(c, PathCmd::CubicTo { .. }) {
has_cubic = true;
}
}
assert!(!has_cubic);
}
#[test]
fn rounded_quad_axis_aligned_matches_rounded_rect_bbox() {
let q = [
Point {
x: Fixed::from_int(0),
y: Fixed::from_int(0),
},
Point {
x: Fixed::from_int(20),
y: Fixed::from_int(0),
},
Point {
x: Fixed::from_int(20),
y: Fixed::from_int(20),
},
Point {
x: Fixed::from_int(0),
y: Fixed::from_int(20),
},
];
let p = Path::rounded_quad(&q, Fixed::from_int(4));
let bb = p.bbox().unwrap();
assert_eq!(bb.x.to_int(), 0);
assert_eq!(bb.y.to_int(), 0);
assert_eq!(bb.w.to_int(), 20);
assert_eq!(bb.h.to_int(), 20);
}
#[test]
fn rounded_quad_tilted_still_convex_bbox() {
let q = [
Point {
x: Fixed::from_int(10),
y: Fixed::from_int(0),
},
Point {
x: Fixed::from_int(20),
y: Fixed::from_int(10),
},
Point {
x: Fixed::from_int(10),
y: Fixed::from_int(20),
},
Point {
x: Fixed::from_int(0),
y: Fixed::from_int(10),
},
];
let p = Path::rounded_quad(&q, Fixed::from_int(2));
let cubic_count = p
.cmds
.iter()
.filter(|c| matches!(c, PathCmd::CubicTo { .. }))
.count();
assert_eq!(cubic_count, 4);
}
#[test]
fn from_static_stays_borrowed() {
use alloc::borrow::Cow;
static CMDS: &[PathCmd] = &[
PathCmd::MoveTo(Point {
x: Fixed::ZERO,
y: Fixed::ZERO,
}),
PathCmd::Close,
];
let p = Path::from_static(CMDS);
assert!(matches!(p.cmds, Cow::Borrowed(_)));
assert_eq!(p.cmds.iter().count(), 2);
assert!(matches!(p.cmds, Cow::Borrowed(_)));
}
}