use crate::core::affine::Affine2D;
use crate::core::bbox::Bbox;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PathCode {
MoveTo,
LineTo,
CurveTo3,
CurveTo4,
ClosePoly,
}
impl PathCode {
#[must_use]
pub const fn num_vertices(self) -> usize {
match self {
PathCode::MoveTo | PathCode::LineTo | PathCode::ClosePoly => 1,
PathCode::CurveTo3 => 2,
PathCode::CurveTo4 => 3,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum PathSegment {
MoveTo([f64; 2]),
LineTo([f64; 2]),
Quad([f64; 2], [f64; 2]),
Cubic([f64; 2], [f64; 2], [f64; 2]),
Close,
}
const CIRCLE_K: f64 = 0.552_284_749_8;
#[derive(Debug, Clone, PartialEq)]
pub struct Path {
vertices: Vec<[f64; 2]>,
codes: Option<Vec<PathCode>>,
}
impl Path {
#[must_use]
pub fn new(vertices: Vec<[f64; 2]>, codes: Option<Vec<PathCode>>) -> Self {
if let Some(codes) = &codes {
assert_eq!(
codes.len(),
vertices.len(),
"Path: codes length ({}) must match vertices length ({})",
codes.len(),
vertices.len()
);
}
Self { vertices, codes }
}
#[must_use]
pub fn from_polyline(points: &[[f64; 2]]) -> Self {
Self {
vertices: points.to_vec(),
codes: None,
}
}
#[must_use]
pub fn vertices(&self) -> &[[f64; 2]] {
&self.vertices
}
#[must_use]
pub fn codes(&self) -> Option<&[PathCode]> {
self.codes.as_deref()
}
#[must_use]
pub fn unit_rectangle() -> Self {
let vertices = vec![[0.0, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 1.0], [0.0, 0.0]];
let codes = vec![
PathCode::MoveTo,
PathCode::LineTo,
PathCode::LineTo,
PathCode::LineTo,
PathCode::ClosePoly,
];
Self::new(vertices, Some(codes))
}
#[must_use]
pub fn unit_circle() -> Self {
let k = CIRCLE_K;
let vertices = vec![
[1.0, 0.0],
[1.0, k],
[k, 1.0],
[0.0, 1.0],
[-k, 1.0],
[-1.0, k],
[-1.0, 0.0],
[-1.0, -k],
[-k, -1.0],
[0.0, -1.0],
[k, -1.0],
[1.0, -k],
[1.0, 0.0],
];
let codes = vec![
PathCode::MoveTo,
PathCode::CurveTo4,
PathCode::CurveTo4,
PathCode::CurveTo4,
PathCode::CurveTo4,
PathCode::CurveTo4,
PathCode::CurveTo4,
PathCode::CurveTo4,
PathCode::CurveTo4,
PathCode::CurveTo4,
PathCode::CurveTo4,
PathCode::CurveTo4,
PathCode::CurveTo4,
];
Self::new(vertices, Some(codes))
}
#[must_use]
pub fn unit_regular_polygon(num_sides: usize) -> Self {
assert!(num_sides > 0, "unit_regular_polygon: num_sides must be > 0");
let n = num_sides;
let mut vertices = Vec::with_capacity(n + 1);
for i in 0..=n {
let theta =
(2.0 * std::f64::consts::PI / n as f64) * i as f64 + std::f64::consts::FRAC_PI_2;
vertices.push([theta.cos(), theta.sin()]);
}
Self::closed_polyline(vertices)
}
#[must_use]
pub fn unit_regular_star(num_points: usize, inner_circle: f64) -> Self {
assert!(num_points > 0, "unit_regular_star: num_points must be > 0");
let ns2 = num_points * 2;
let mut vertices = Vec::with_capacity(ns2 + 1);
for i in 0..=ns2 {
let theta =
(2.0 * std::f64::consts::PI / ns2 as f64) * i as f64 + std::f64::consts::FRAC_PI_2;
let r = if i % 2 == 1 { inner_circle } else { 1.0 };
vertices.push([r * theta.cos(), r * theta.sin()]);
}
Self::closed_polyline(vertices)
}
fn closed_polyline(vertices: Vec<[f64; 2]>) -> Self {
let n = vertices.len();
let mut codes = Vec::with_capacity(n);
codes.push(PathCode::MoveTo);
for _ in 1..n.saturating_sub(1) {
codes.push(PathCode::LineTo);
}
if n > 1 {
codes.push(PathCode::ClosePoly);
}
Self::new(vertices, Some(codes))
}
pub fn iter_segments(&self) -> impl Iterator<Item = PathSegment> + '_ {
SegmentIter {
path: self,
index: 0,
}
}
#[must_use]
pub fn transformed(&self, transform: &Affine2D) -> Path {
let vertices = self
.vertices
.iter()
.map(|&[x, y]| {
let (tx, ty) = transform.transform_point((x, y));
[tx, ty]
})
.collect();
Path {
vertices,
codes: self.codes.clone(),
}
}
#[must_use]
pub fn get_extents(&self) -> Bbox {
if self.vertices.is_empty() {
return Bbox::null();
}
let mut xmin = f64::INFINITY;
let mut ymin = f64::INFINITY;
let mut xmax = f64::NEG_INFINITY;
let mut ymax = f64::NEG_INFINITY;
for &[x, y] in &self.vertices {
xmin = xmin.min(x);
ymin = ymin.min(y);
xmax = xmax.max(x);
ymax = ymax.max(y);
}
Bbox::from_extents(xmin, ymin, xmax, ymax)
}
#[must_use]
pub fn flatten(&self, tolerance: f64) -> Vec<Vec<[f64; 2]>> {
let tol = tolerance.max(f64::EPSILON);
let mut subpaths: Vec<Vec<[f64; 2]>> = Vec::new();
let mut current: Vec<[f64; 2]> = Vec::new();
let mut start: Option<[f64; 2]> = None;
let mut cur: Option<[f64; 2]> = None;
for seg in self.iter_segments() {
match seg {
PathSegment::MoveTo(p) => {
if !current.is_empty() {
subpaths.push(std::mem::take(&mut current));
}
current.push(p);
start = Some(p);
cur = Some(p);
}
PathSegment::LineTo(p) => {
current.push(p);
cur = Some(p);
}
PathSegment::Quad(c, e) => {
let p0 = cur.unwrap_or(c);
flatten_quad(p0, c, e, tol, &mut current);
cur = Some(e);
}
PathSegment::Cubic(c1, c2, e) => {
let p0 = cur.unwrap_or(c1);
flatten_cubic(p0, c1, c2, e, tol, &mut current);
cur = Some(e);
}
PathSegment::Close => {
if let Some(s) = start {
current.push(s);
cur = Some(s);
}
if !current.is_empty() {
subpaths.push(std::mem::take(&mut current));
}
start = None;
}
}
}
if !current.is_empty() {
subpaths.push(current);
}
subpaths
}
#[must_use]
pub fn contains_point(&self, p: [f64; 2]) -> bool {
let [px, py] = p;
let mut inside = false;
for sub in self.flatten(1e-6) {
if sub.len() < 2 {
continue;
}
let n = sub.len();
for i in 0..n {
let [xi, yi] = sub[i];
let [xj, yj] = sub[(i + 1) % n];
let intersects =
(yi > py) != (yj > py) && px < (xj - xi) * (py - yi) / (yj - yi) + xi;
if intersects {
inside = !inside;
}
}
}
inside
}
}
struct SegmentIter<'a> {
path: &'a Path,
index: usize,
}
impl Iterator for SegmentIter<'_> {
type Item = PathSegment;
fn next(&mut self) -> Option<PathSegment> {
let verts = &self.path.vertices;
if self.index >= verts.len() {
return None;
}
match &self.path.codes {
None => {
let seg = if self.index == 0 {
PathSegment::MoveTo(verts[0])
} else {
PathSegment::LineTo(verts[self.index])
};
self.index += 1;
Some(seg)
}
Some(codes) => {
let code = codes[self.index];
let seg = match code {
PathCode::MoveTo => {
let s = PathSegment::MoveTo(verts[self.index]);
self.index += 1;
s
}
PathCode::LineTo => {
let s = PathSegment::LineTo(verts[self.index]);
self.index += 1;
s
}
PathCode::ClosePoly => {
self.index += 1;
PathSegment::Close
}
PathCode::CurveTo3 => {
let c = verts[self.index];
let e = verts[self.index + 1];
self.index += 2;
PathSegment::Quad(c, e)
}
PathCode::CurveTo4 => {
let c1 = verts[self.index];
let c2 = verts[self.index + 1];
let e = verts[self.index + 2];
self.index += 3;
PathSegment::Cubic(c1, c2, e)
}
};
Some(seg)
}
}
}
}
fn flatten_quad(p0: [f64; 2], c: [f64; 2], p1: [f64; 2], tol: f64, out: &mut Vec<[f64; 2]>) {
if point_line_distance(c, p0, p1) <= tol {
out.push(p1);
return;
}
let p01 = midpoint(p0, c);
let p12 = midpoint(c, p1);
let mid = midpoint(p01, p12);
flatten_quad(p0, p01, mid, tol, out);
flatten_quad(mid, p12, p1, tol, out);
}
fn flatten_cubic(
p0: [f64; 2],
c1: [f64; 2],
c2: [f64; 2],
p1: [f64; 2],
tol: f64,
out: &mut Vec<[f64; 2]>,
) {
let d1 = point_line_distance(c1, p0, p1);
let d2 = point_line_distance(c2, p0, p1);
if d1.max(d2) <= tol {
out.push(p1);
return;
}
let p01 = midpoint(p0, c1);
let p12 = midpoint(c1, c2);
let p23 = midpoint(c2, p1);
let p012 = midpoint(p01, p12);
let p123 = midpoint(p12, p23);
let mid = midpoint(p012, p123);
flatten_cubic(p0, p01, p012, mid, tol, out);
flatten_cubic(mid, p123, p23, p1, tol, out);
}
fn midpoint(a: [f64; 2], b: [f64; 2]) -> [f64; 2] {
[(a[0] + b[0]) * 0.5, (a[1] + b[1]) * 0.5]
}
fn point_line_distance(p: [f64; 2], a: [f64; 2], b: [f64; 2]) -> f64 {
let dx = b[0] - a[0];
let dy = b[1] - a[1];
let len2 = dx * dx + dy * dy;
if len2 == 0.0 {
let ex = p[0] - a[0];
let ey = p[1] - a[1];
return (ex * ex + ey * ey).sqrt();
}
let cross = (p[0] - a[0]) * dy - (p[1] - a[1]) * dx;
cross.abs() / len2.sqrt()
}
#[cfg(feature = "portable")]
fn code_to_u8(code: PathCode) -> u8 {
match code {
PathCode::MoveTo => 1,
PathCode::LineTo => 2,
PathCode::CurveTo3 => 3,
PathCode::CurveTo4 => 4,
PathCode::ClosePoly => 79,
}
}
#[cfg(feature = "portable")]
fn u8_to_code(byte: u8) -> Result<PathCode, crate::portable::PortableError> {
Ok(match byte {
1 => PathCode::MoveTo,
2 => PathCode::LineTo,
3 => PathCode::CurveTo3,
4 => PathCode::CurveTo4,
79 => PathCode::ClosePoly,
other => {
return Err(crate::portable::PortableError::Malformed(format!(
"unknown path code byte {other}"
)));
}
})
}
#[cfg(feature = "portable")]
impl Path {
pub(crate) fn to_portable(
&self,
bank: &mut crate::portable::data::BankWriter,
) -> crate::portable::spec::PathSpec {
crate::portable::spec::PathSpec {
verts: bank.push_pairs(&self.vertices),
codes: self.codes.as_ref().map(|codes| {
let bytes: Vec<u8> = codes.iter().map(|&c| code_to_u8(c)).collect();
bank.push_u8(&bytes)
}),
}
}
pub(crate) fn from_portable(
spec: &crate::portable::spec::PathSpec,
reader: &crate::portable::data::BankReader<'_>,
) -> Result<Path, crate::portable::PortableError> {
let vertices = reader.pairs(&spec.verts)?;
let codes = match &spec.codes {
None => None,
Some(arr) => {
let bytes = reader.u8s(arr)?;
if bytes.len() != vertices.len() {
return Err(crate::portable::PortableError::Malformed(format!(
"path has {} vertices but {} codes",
vertices.len(),
bytes.len()
)));
}
Some(
bytes
.into_iter()
.map(u8_to_code)
.collect::<Result<Vec<_>, _>>()?,
)
}
};
Ok(Path { vertices, codes })
}
}
#[cfg(test)]
mod tests {
use super::*;
fn approx(a: f64, b: f64) {
assert!((a - b).abs() < 1e-9, "expected {b}, got {a}");
}
#[test]
fn unit_rectangle_is_closed_unit_square() {
let p = Path::unit_rectangle();
assert_eq!(p.vertices().len(), 5);
assert_eq!(p.vertices()[0], [0.0, 0.0]);
assert_eq!(p.vertices()[4], [0.0, 0.0]);
let e = p.get_extents();
approx(e.xmin(), 0.0);
approx(e.ymin(), 0.0);
approx(e.xmax(), 1.0);
approx(e.ymax(), 1.0);
}
#[test]
fn from_polyline_iterates_moveto_then_linetos() {
let p = Path::from_polyline(&[[0.0, 0.0], [1.0, 0.0], [1.0, 1.0]]);
let segs: Vec<_> = p.iter_segments().collect();
assert_eq!(
segs,
vec![
PathSegment::MoveTo([0.0, 0.0]),
PathSegment::LineTo([1.0, 0.0]),
PathSegment::LineTo([1.0, 1.0]),
]
);
}
#[test]
fn transformed_by_scale_doubles_extents() {
let p = Path::unit_rectangle();
let scaled = p.transformed(&Affine2D::from_scale(2.0, 2.0));
let e = scaled.get_extents();
approx(e.xmin(), 0.0);
approx(e.ymin(), 0.0);
approx(e.xmax(), 2.0);
approx(e.ymax(), 2.0);
}
#[test]
fn iter_segments_yields_cubic() {
let verts = vec![[0.0, 0.0], [0.0, 1.0], [1.0, 1.0], [1.0, 0.0]];
let codes = vec![
PathCode::MoveTo,
PathCode::CurveTo4,
PathCode::CurveTo4,
PathCode::CurveTo4,
];
let p = Path::new(verts, Some(codes));
let segs: Vec<_> = p.iter_segments().collect();
assert_eq!(
segs,
vec![
PathSegment::MoveTo([0.0, 0.0]),
PathSegment::Cubic([0.0, 1.0], [1.0, 1.0], [1.0, 0.0]),
]
);
}
#[test]
fn iter_segments_yields_quad_and_close() {
let verts = vec![[0.0, 0.0], [1.0, 1.0], [2.0, 0.0], [0.0, 0.0]];
let codes = vec![
PathCode::MoveTo,
PathCode::CurveTo3,
PathCode::LineTo,
PathCode::ClosePoly,
];
let p = Path::new(verts, Some(codes));
let segs: Vec<_> = p.iter_segments().collect();
assert_eq!(
segs,
vec![
PathSegment::MoveTo([0.0, 0.0]),
PathSegment::Quad([1.0, 1.0], [2.0, 0.0]),
PathSegment::Close,
]
);
}
#[test]
fn contains_point_centroid_and_outside() {
let p = Path::unit_rectangle();
assert!(p.contains_point([0.5, 0.5]));
assert!(!p.contains_point([5.0, 5.0]));
assert!(!p.contains_point([-1.0, 0.5]));
}
#[test]
fn unit_circle_extents_are_unit() {
let e = Path::unit_circle().get_extents();
approx(e.xmin(), -1.0);
approx(e.ymin(), -1.0);
approx(e.xmax(), 1.0);
approx(e.ymax(), 1.0);
}
#[test]
fn unit_circle_contains_origin_not_far_point() {
let c = Path::unit_circle();
assert!(c.contains_point([0.0, 0.0]));
assert!(c.contains_point([0.5, 0.5]));
assert!(!c.contains_point([2.0, 0.0]));
}
#[test]
fn unit_regular_polygon_triangle_points_up() {
let p = Path::unit_regular_polygon(3);
assert_eq!(p.vertices().len(), 4);
approx(p.vertices()[0][0], 0.0);
approx(p.vertices()[0][1], 1.0);
assert!(p.contains_point([0.0, 0.0]));
}
#[test]
fn unit_regular_star_inner_radius() {
let s = Path::unit_regular_star(5, 0.5);
approx(s.vertices()[0][0], 0.0);
approx(s.vertices()[0][1], 1.0);
let e = s.get_extents();
approx(e.ymax(), 1.0);
assert!(s.contains_point([0.0, 0.0]));
}
#[test]
#[should_panic(expected = "codes length")]
fn new_rejects_mismatched_codes() {
let _ = Path::new(vec![[0.0, 0.0], [1.0, 1.0]], Some(vec![PathCode::MoveTo]));
}
#[test]
fn flatten_splits_subpaths() {
let verts = vec![[0.0, 0.0], [1.0, 0.0], [2.0, 0.0], [3.0, 0.0]];
let codes = vec![
PathCode::MoveTo,
PathCode::LineTo,
PathCode::MoveTo,
PathCode::LineTo,
];
let p = Path::new(verts, Some(codes));
let subs = p.flatten(1e-3);
assert_eq!(subs.len(), 2);
assert_eq!(subs[0], vec![[0.0, 0.0], [1.0, 0.0]]);
assert_eq!(subs[1], vec![[2.0, 0.0], [3.0, 0.0]]);
}
#[test]
fn flatten_cubic_approximates_endpoint() {
let verts = vec![[0.0, 0.0], [0.0, 1.0], [1.0, 1.0], [1.0, 0.0]];
let codes = vec![
PathCode::MoveTo,
PathCode::CurveTo4,
PathCode::CurveTo4,
PathCode::CurveTo4,
];
let p = Path::new(verts, Some(codes));
let subs = p.flatten(1e-3);
assert_eq!(subs.len(), 1);
let pts = &subs[0];
assert_eq!(pts.first().copied(), Some([0.0, 0.0]));
assert_eq!(pts.last().copied(), Some([1.0, 0.0]));
assert!(pts.len() > 2, "curve should be subdivided");
}
}