use kurbo::{BezPath, PathEl, Point};
use crate::error::PathConversionError;
#[derive(Debug)]
pub struct GlyphPathBuilder {
offcurve: Vec<Point>,
leading_offcurve: Vec<Point>,
path: Vec<PathEl>,
first_oncurve: Option<OnCurve>,
}
#[derive(Debug, Copy, Clone, PartialEq)]
enum OnCurve {
Move(Point),
Line(Point),
Quad(Point),
Cubic(Point),
}
impl OnCurve {
fn point(&self) -> &Point {
match self {
OnCurve::Move(p) => p,
OnCurve::Line(p) => p,
OnCurve::Quad(p) => p,
OnCurve::Cubic(p) => p,
}
}
fn is_move(&self) -> bool {
matches!(self, OnCurve::Move(_))
}
}
impl GlyphPathBuilder {
pub fn new(estimated_num_elements: usize) -> GlyphPathBuilder {
let mut capacity = estimated_num_elements.next_power_of_two();
if capacity == estimated_num_elements {
capacity += 4; }
GlyphPathBuilder {
offcurve: Vec::with_capacity(2),
leading_offcurve: Vec::new(),
path: Vec::with_capacity(capacity),
first_oncurve: None,
}
}
fn check_num_offcurve(
&self,
expected: impl Fn(usize) -> bool,
) -> Result<(), PathConversionError> {
if !expected(self.offcurve.len()) {
return Err(PathConversionError::TooManyOffcurvePoints {
num_offcurve: self.offcurve.len(),
points: self.offcurve.clone(),
});
}
Ok(())
}
fn is_empty(&self) -> bool {
self.first_oncurve.is_none() && self.leading_offcurve.is_empty()
}
fn begin_path(&mut self, oncurve: OnCurve) -> Result<(), PathConversionError> {
assert!(self.first_oncurve.is_none());
self.path.push(PathEl::MoveTo(*oncurve.point()));
self.first_oncurve = Some(oncurve);
Ok(())
}
pub fn move_to(&mut self, p: impl Into<Point>) -> Result<(), PathConversionError> {
if !self.is_empty() {
return Err(PathConversionError::MoveAfterFirstPoint { point: p.into() });
}
self.begin_path(OnCurve::Move(p.into()))
}
pub fn line_to(&mut self, p: impl Into<Point>) -> Result<(), PathConversionError> {
self.check_num_offcurve(|v| v == 0)?;
if self.first_oncurve.is_none() {
self.begin_path(OnCurve::Line(p.into()))?;
} else {
self.path.push(PathEl::LineTo(p.into()));
}
Ok(())
}
pub fn qcurve_to(&mut self, p: impl Into<Point>) -> Result<(), PathConversionError> {
if self.first_oncurve.is_none() {
return self.begin_path(OnCurve::Quad(p.into()));
}
if self.offcurve.is_empty() {
return self.line_to(p);
}
for window in self.offcurve.windows(2) {
let curr = window[0];
let next = window[1];
let implied = Point::new((curr.x + next.x) / 2.0, (curr.y + next.y) / 2.0);
self.path.push(PathEl::QuadTo(curr, implied));
}
self.path
.push(PathEl::QuadTo(*self.offcurve.last().unwrap(), p.into()));
self.offcurve.clear();
Ok(())
}
pub fn curve_to(&mut self, p: impl Into<Point>) -> Result<(), PathConversionError> {
if self.first_oncurve.is_some() {
match self.offcurve.len() {
0 => self.path.push(PathEl::LineTo(p.into())),
1 => self.path.push(PathEl::QuadTo(self.offcurve[0], p.into())),
2 => self.path.push(PathEl::CurveTo(
self.offcurve[0],
self.offcurve[1],
p.into(),
)),
_ => self.check_num_offcurve(|v| v < 3)?,
}
self.offcurve.clear();
} else {
self.begin_path(OnCurve::Cubic(p.into()))?;
}
Ok(())
}
pub fn offcurve(&mut self, p: impl Into<Point>) -> Result<(), PathConversionError> {
if self.first_oncurve.is_some() {
self.offcurve.push(p.into());
} else {
self.leading_offcurve.push(p.into());
}
Ok(())
}
fn end_path(&mut self) -> Result<(), PathConversionError> {
if !self.first_oncurve.is_some_and(|on| on.is_move()) {
self.close_path()?;
}
self.check_num_offcurve(|v| v == 0)?;
self.first_oncurve = None;
Ok(())
}
fn close_path(&mut self) -> Result<(), PathConversionError> {
if !self.leading_offcurve.is_empty() {
self.offcurve.append(&mut self.leading_offcurve);
}
if let Some(first_oncurve) = self.first_oncurve {
match first_oncurve {
OnCurve::Line(pt) => self.line_to(pt)?,
OnCurve::Quad(pt) => self.qcurve_to(pt)?,
OnCurve::Cubic(pt) => self.curve_to(pt)?,
_ => unreachable!(),
}
self.path.push(PathEl::ClosePath);
} else if !self.offcurve.is_empty() {
let first_offcurve = self.offcurve[0];
let last_offcurve = *self.offcurve.last().unwrap();
let implied_oncurve = first_offcurve.midpoint(last_offcurve);
self.begin_path(OnCurve::Quad(implied_oncurve))?;
self.close_path()?;
}
Ok(())
}
pub fn erase_open_corners(&mut self) -> Result<bool, PathConversionError> {
self.end_path()?;
let temp = BezPath::from_vec(std::mem::take(&mut self.path));
let erased = super::erase_open_corners::erase_open_corners(&temp);
let result = erased.is_some();
self.path = erased.unwrap_or(temp).into_iter().collect();
Ok(result)
}
pub fn build(mut self) -> Result<BezPath, PathConversionError> {
self.end_path()?;
Ok(BezPath::from_vec(self.path))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_qcurve_with_no_offcurve_is_a_line_open_contour() {
let mut builder = GlyphPathBuilder::new(0);
builder.move_to((2.0, 2.0)).unwrap(); builder.qcurve_to((4.0, 2.0)).unwrap();
assert_eq!("M2,2 L4,2", builder.build().unwrap().to_svg());
}
#[test]
fn a_qcurve_with_no_offcurve_is_a_line_closed_contour() {
let mut builder = GlyphPathBuilder::new(0);
builder.qcurve_to((2.0, 2.0)).unwrap(); builder.qcurve_to((4.0, 2.0)).unwrap();
assert_eq!("M2,2 L4,2 L2,2 Z", builder.build().unwrap().to_svg());
}
#[test]
fn a_curve_with_no_offcurve_is_a_line_open_contour() {
let mut builder = GlyphPathBuilder::new(0);
builder.move_to((2.0, 2.0)).unwrap(); builder.curve_to((4.0, 2.0)).unwrap();
assert_eq!("M2,2 L4,2", builder.build().unwrap().to_svg());
}
#[test]
fn a_curve_with_no_offcurve_is_a_line_closed_contour() {
let mut builder = GlyphPathBuilder::new(0);
builder.curve_to((2.0, 2.0)).unwrap(); builder.curve_to((4.0, 2.0)).unwrap();
assert_eq!("M2,2 L4,2 L2,2 Z", builder.build().unwrap().to_svg());
}
#[test]
fn a_curve_with_one_offcurve_is_a_single_quad_open_contour() {
let mut builder = GlyphPathBuilder::new(0);
builder.move_to((2.0, 2.0)).unwrap(); builder.offcurve((3.0, 0.0)).unwrap();
builder.curve_to((4.0, 2.0)).unwrap();
assert_eq!("M2,2 Q3,0 4,2", builder.build().unwrap().to_svg());
}
#[test]
fn a_curve_with_one_offcurve_is_a_single_quad_closed_contour() {
let mut builder = GlyphPathBuilder::new(0);
builder.curve_to((2.0, 2.0)).unwrap(); builder.offcurve((3.0, 0.0)).unwrap();
builder.curve_to((4.0, 2.0)).unwrap();
assert_eq!("M2,2 Q3,0 4,2 L2,2 Z", builder.build().unwrap().to_svg());
}
#[test]
fn a_qcurve_with_one_offcurve_is_a_single_quad_to_open_contour() {
let mut builder = GlyphPathBuilder::new(0);
builder.move_to((2.0, 2.0)).unwrap();
builder.offcurve((3.0, 0.0)).unwrap();
builder.qcurve_to((4.0, 2.0)).unwrap();
assert_eq!("M2,2 Q3,0 4,2", builder.build().unwrap().to_svg());
}
#[test]
fn a_qcurve_with_one_offcurve_is_a_single_quad_to_closed_contour() {
let mut builder = GlyphPathBuilder::new(0);
builder.qcurve_to((2.0, 2.0)).unwrap(); builder.offcurve((3.0, 0.0)).unwrap();
builder.qcurve_to((4.0, 2.0)).unwrap();
assert_eq!("M2,2 Q3,0 4,2 L2,2 Z", builder.build().unwrap().to_svg());
}
#[test]
fn a_qcurve_with_two_offcurve_is_two_quad_to_open_contour() {
let mut builder = GlyphPathBuilder::new(0);
builder.move_to((2.0, 2.0)).unwrap();
builder.offcurve((3.0, 0.0)).unwrap();
builder.offcurve((5.0, 4.0)).unwrap();
builder.qcurve_to((6.0, 2.0)).unwrap();
assert_eq!("M2,2 Q3,0 4,2 Q5,4 6,2", builder.build().unwrap().to_svg());
}
#[test]
fn a_qcurve_with_two_offcurve_is_two_quad_to_closed_contour() {
let mut builder = GlyphPathBuilder::new(0);
builder.qcurve_to((2.0, 2.0)).unwrap(); builder.offcurve((3.0, 0.0)).unwrap();
builder.offcurve((5.0, 4.0)).unwrap();
builder.qcurve_to((6.0, 2.0)).unwrap();
assert_eq!(
"M2,2 Q3,0 4,2 Q5,4 6,2 L2,2 Z",
builder.build().unwrap().to_svg()
);
}
#[test]
fn last_line_always_emits_implied_closing_line() {
let mut builder = GlyphPathBuilder::new(0);
builder.line_to((2.0, 2.0)).unwrap();
builder.line_to((4.0, 2.0)).unwrap();
assert_eq!("M2,2 L4,2 L2,2 Z", builder.build().unwrap().to_svg());
}
#[test]
fn last_line_emits_nop_implied_closing_line() {
let mut builder = GlyphPathBuilder::new(0);
builder.line_to((2.0, 2.0)).unwrap();
builder.line_to((4.0, 2.0)).unwrap();
builder.line_to((2.0, 2.0)).unwrap();
assert_eq!("M2,2 L4,2 L2,2 L2,2 Z", builder.build().unwrap().to_svg());
}
#[test]
fn last_quad_equals_move_no_closing_line() {
let mut builder = GlyphPathBuilder::new(0);
builder.offcurve((3.0, 0.0)).unwrap();
builder.qcurve_to((2.0, 2.0)).unwrap();
assert_eq!("M2,2 Q3,0 2,2 Z", builder.build().unwrap().to_svg());
}
#[test]
fn last_cubic_equals_move_no_closing_line() {
let mut builder = GlyphPathBuilder::new(0);
builder.offcurve((3.0, 0.0)).unwrap();
builder.offcurve((0.0, 3.0)).unwrap();
builder.curve_to((2.0, 2.0)).unwrap();
assert_eq!("M2,2 C3,0 0,3 2,2 Z", builder.build().unwrap().to_svg());
}
#[test]
fn last_quad_not_equal_move_do_emit_closing_line() {
let mut builder = GlyphPathBuilder::new(0);
builder.line_to((2.0, 2.0)).unwrap();
builder.offcurve((3.0, 0.0)).unwrap();
builder.qcurve_to((4.0, 2.0)).unwrap();
assert_eq!("M2,2 Q3,0 4,2 L2,2 Z", builder.build().unwrap().to_svg());
}
#[test]
fn last_cubic_not_equal_move_do_emit_closing_line() {
let mut builder = GlyphPathBuilder::new(0);
builder.line_to((2.0, 2.0)).unwrap();
builder.offcurve((3.0, 0.0)).unwrap();
builder.offcurve((0.0, 3.0)).unwrap();
builder.curve_to((4.0, 2.0)).unwrap();
assert_eq!(
"M2,2 C3,0 0,3 4,2 L2,2 Z",
builder.build().unwrap().to_svg()
);
}
#[test]
fn start_on_first_oncurve_irrespective_of_offcurves() {
let expected = "M2,2 C6,0 0,6 4,2 C3,0 0,3 2,2 Z";
let mut builder = GlyphPathBuilder::new(0);
builder.offcurve((3.0, 0.0)).unwrap();
builder.offcurve((0.0, 3.0)).unwrap();
builder.curve_to((2.0, 2.0)).unwrap();
builder.offcurve((6.0, 0.0)).unwrap();
builder.offcurve((0.0, 6.0)).unwrap();
builder.curve_to((4.0, 2.0)).unwrap();
assert_eq!(expected, builder.build().unwrap().to_svg());
let mut builder = GlyphPathBuilder::new(0);
builder.offcurve((0.0, 3.0)).unwrap();
builder.curve_to((2.0, 2.0)).unwrap();
builder.offcurve((6.0, 0.0)).unwrap();
builder.offcurve((0.0, 6.0)).unwrap();
builder.curve_to((4.0, 2.0)).unwrap();
builder.offcurve((3.0, 0.0)).unwrap();
assert_eq!(expected, builder.build().unwrap().to_svg());
let mut builder = GlyphPathBuilder::new(0);
builder.curve_to((2.0, 2.0)).unwrap();
builder.offcurve((6.0, 0.0)).unwrap();
builder.offcurve((0.0, 6.0)).unwrap();
builder.curve_to((4.0, 2.0)).unwrap();
builder.offcurve((3.0, 0.0)).unwrap();
builder.offcurve((0.0, 3.0)).unwrap();
assert_eq!(expected, builder.build().unwrap().to_svg());
}
#[test]
fn closed_quadratic_contour_without_oncurve_points() {
let mut builder = GlyphPathBuilder::new(0);
builder.offcurve((1.0, 1.0)).unwrap();
builder.offcurve((1.0, -1.0)).unwrap();
builder.offcurve((-1.0, -1.0)).unwrap();
builder.offcurve((-1.0, 1.0)).unwrap();
assert_eq!(
"M0,1 Q1,1 1,0 Q1,-1 0,-1 Q-1,-1 -1,0 Q-1,1 0,1 Z",
builder.build().unwrap().to_svg()
);
}
#[test]
fn invalid_move_after_first_point() {
let mut builder = GlyphPathBuilder::new(0);
builder.move_to((2.0, 2.0)).unwrap();
builder.end_path().unwrap();
builder.move_to((3.0, 3.0)).unwrap();
let result = builder.move_to((4.0, 4.0));
assert_eq!(
result,
Err(PathConversionError::MoveAfterFirstPoint {
point: (4.0, 4.0).into()
})
);
builder.end_path().unwrap();
builder.line_to((5.0, 5.0)).unwrap();
let result = builder.move_to((6.0, 6.0));
assert_eq!(
result,
Err(PathConversionError::MoveAfterFirstPoint {
point: (6.0, 6.0).into()
})
);
builder.end_path().unwrap();
builder.offcurve((7.0, 7.0)).unwrap();
let result = builder.move_to((8.0, 8.0));
assert_eq!(
result,
Err(PathConversionError::MoveAfterFirstPoint {
point: (8.0, 8.0).into()
})
);
}
#[test]
fn closed_path_with_trailing_cubic_offcurves() {
let mut builder = GlyphPathBuilder::new(0);
builder.curve_to((10.0, 0.0)).unwrap();
builder.line_to((0.0, 10.0)).unwrap();
builder.offcurve((5.0, 10.0)).unwrap();
builder.offcurve((10.0, 5.0)).unwrap();
let path = builder.build().unwrap();
assert_eq!("M10,0 L0,10 C5,10 10,5 10,0 Z", path.to_svg());
}
#[test]
fn closed_path_with_trailing_quadratic_offcurves() {
let mut builder = GlyphPathBuilder::new(0);
builder.qcurve_to((10.0, 0.0)).unwrap();
builder.line_to((0.0, 10.0)).unwrap();
builder.offcurve((5.0, 10.0)).unwrap();
builder.offcurve((10.0, 5.0)).unwrap();
let path = builder.build().unwrap();
assert_eq!("M10,0 L0,10 Q5,10 7.5,7.5 Q10,5 10,0 Z", path.to_svg());
}
}