diff --git a/fontir/src/ir.rs b/fontir/src/ir.rs index 57ba9748..5f27dd9b 100644 --- a/fontir/src/ir.rs +++ b/fontir/src/ir.rs @@ -25,14 +25,15 @@ use fontdrasil::{ }; use crate::{ - error::{ - BadAnchor, BadAnchorReason, BadGlyph, BadGlyphKind, PathConversionError, - VariationModelError, - }, + error::{BadAnchor, BadAnchorReason, BadGlyph, BadGlyphKind, VariationModelError}, orchestration::{IdAware, Persistable, WorkId}, variations::VariationModel, }; +mod path_builder; + +pub use path_builder::GlyphPathBuilder; + pub const DEFAULT_VENDOR_ID: &str = "NONE"; const DEFAULT_VENDOR_ID_TAG: Tag = Tag::new(b"NONE"); @@ -1858,239 +1859,6 @@ pub struct Component { pub transform: Affine, } -#[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(_)) - } -} - -/// Helps convert points-of-type to a bezier path. -/// -/// Source formats tend to use streams of point-of-type. Curve manipulation is -/// often easier on bezier path, so provide a mechanism to convert. -/// While kurbo::BezPath can contain multiple subpaths, and this builder could be -/// used to convert multiple contours (i.e. list of points) into a single BezPath, -/// our GlyphInstance.contours is defined as a `Vec`, so frontends should -/// convert one contour at a time. -#[derive(Debug)] -pub struct GlyphPathBuilder { - offcurve: Vec, - leading_offcurve: Vec, - path: Vec, - first_oncurve: Option, -} - -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; // close path often adds a few - } - 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(()) - } - - /// Lifts the "pen" to Point `p` and marks the beginning of an open contour. - /// - /// A point of this type can only be the first point in a contour. - /// Cf. "move" in - pub fn move_to(&mut self, p: impl Into) -> Result<(), PathConversionError> { - if !self.is_empty() { - return Err(PathConversionError::MoveAfterFirstPoint { point: p.into() }); - } - self.begin_path(OnCurve::Move(p.into())) - } - - /// Draws a line from the previous point to Point `p`. - /// - /// The previous point cannot be an off-curve point. - /// If this is the first point in a contour, the contour is assumed to be closed, - /// i.e. a cyclic list of points with no predominant start point. - /// Cf. "line" in - pub fn line_to(&mut self, p: impl Into) -> 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(()) - } - - /// Draws a quadratic curve/spline from the last non-offcurve point to Point `p`. - /// - /// This uses the TrueType "implied on-curve point" principle. - /// The number of preceding off-curve points can be n >= 0. When n=0, a straight line is - /// implied. If n=1, a single quadratic Bezier curve is drawn. If n>=2, a sequence of - /// quadratic Bezier curves is drawn, with the implied on-curve points at the midpoints - /// between pairs of successive off-curve points. - /// If this is the first point in a contour, the contour is assumed to be closed. - /// Cf. "qcurve" in - pub fn qcurve_to(&mut self, p: impl Into) -> Result<(), PathConversionError> { - // https://github.com/googlefonts/fontmake-rs/issues/110 - // Guard clauses: degenerate cases - if self.first_oncurve.is_none() { - return self.begin_path(OnCurve::Quad(p.into())); - } - if self.offcurve.is_empty() { - return self.line_to(p); - } - - // Insert an implied oncurve between every pair of offcurve points - for window in self.offcurve.windows(2) { - let curr = window[0]; - let next = window[1]; - // current offcurve to halfway to the next one - let implied = Point::new((curr.x + next.x) / 2.0, (curr.y + next.y) / 2.0); - self.path.push(PathEl::QuadTo(curr, implied)); - } - // last but not least, the last offcurve to the provided point - self.path - .push(PathEl::QuadTo(*self.offcurve.last().unwrap(), p.into())); - self.offcurve.clear(); - - Ok(()) - } - - /// Draws a cubic curve from the previous non-offcurve point to Point `p`. - /// - /// Type of curve depends on the number of accumulated off-curves: 0 (straight line), - /// 1 (quadratic Bezier) or 2 (cubic Bezier). - /// If this is the first point in a contour, the contour is assumed to be closed. - /// Cf. "curve" in - pub fn curve_to(&mut self, p: impl Into) -> 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(()) - } - - /// Append off-curve point `p` to the following curve segment. - /// - /// The type of curve is defined by following on-curve point, which can be either a - /// (cubic) "curve" or (quadratic) "qcurve". - /// If offcurve is the first point in a contour, the contour is assumed to be closed. - /// Cf. "offcurve" in - pub fn offcurve(&mut self, p: impl Into) -> Result<(), PathConversionError> { - if self.first_oncurve.is_some() { - self.offcurve.push(p.into()); - } else { - self.leading_offcurve.push(p.into()); - } - Ok(()) - } - - /// Ends the current sub-path. - /// - /// It's called automatically by `build()` thus can be - /// omitted when building one BezPath per contour, but can be called manually in - /// order to build multiple contours into a single BezPath. - pub fn end_path(&mut self) -> Result<(), PathConversionError> { - // a contour that does *not* start with a move is assumed to be closed - // https://unifiedfontobject.org/versions/ufo3/glyphs/glif/#point-types - 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> { - // Flush any leading off-curves to the end. This matches fontTools' PointToSegmentPen - // always starting/ending a closed contour on the first on-curve point: - // https://github.com/fonttools/fonttools/blob/57fb47/Lib/fontTools/pens/pointPen.py#L147-L155 - if !self.leading_offcurve.is_empty() { - self.offcurve.append(&mut self.leading_offcurve); - } - // Take dangling off-curves to imply a curve back to sub-path start. - // For closed paths we explicitly output the implied closing line - // equivalent to fontTools' PointToSegmentPen(outputImpliedClosingLine=True) - 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() { - // special TrueType oncurve-less quadratic contour, we assume the path - // starts at midpoint between the first and last offcurves - 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(()) - } - - /// Builds the kurbo::BezPath from the accumulated points. - pub fn build(mut self) -> Result { - self.end_path()?; - Ok(BezPath::from_vec(self.path)) - } -} - #[cfg(test)] mod tests { @@ -2104,7 +1872,7 @@ mod tests { use fontdrasil::coords::{CoordConverter, NormalizedCoord, UserCoord}; use write_fonts::{tables::os2::SelectionFlags, types::NameId}; - use crate::{error::PathConversionError, ir::Axis, variations::VariationModel}; + use crate::{ir::Axis, variations::VariationModel}; use pretty_assertions::assert_eq; @@ -2234,275 +2002,6 @@ mod tests { assert_bincode_round_trip(test_static_metadata()); } - #[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(); // open contour - 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(); // closed, ie not starting with 'move' - 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(); // open contour - 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(); // closed - 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(); // open - 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(); // closed - 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(); // closed - 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(); // closed - 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(); - // a closing line is implied by Z, but emit it nonetheless - 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(); - // duplicate last point, not to be confused with the closing line implied by Z - 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() { - // if last curve point is equal to move, there's no need to disambiguate it from - // the implicit closing line, so we don't emit one - 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() { - // if last point is different from move, then emit the implied 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() { - // the following three closed contours are all equivalent and get normalized - // to the same path, which begins/ends on the first on-curve point i.e. (2,2). - 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.qcurve_to((0.0, 1.0)).unwrap(); // implied - 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() { - // A point of type 'move' must be the first point in an (open) contour. - let mut builder = GlyphPathBuilder::new(0); - builder.move_to((2.0, 2.0)).unwrap(); - builder.end_path().unwrap(); - // move_to after ending the current subpath is OK - builder.move_to((3.0, 3.0)).unwrap(); - // but it's an error if we try to do move_to again - 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(); - // can't move_to in the middle of a closed (not starting with move_to) subpath - 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(); - // can't move_to after an offcurve point - 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()); - } - // from // #[test] diff --git a/fontir/src/ir/path_builder.rs b/fontir/src/ir/path_builder.rs new file mode 100644 index 00000000..ef1bdb16 --- /dev/null +++ b/fontir/src/ir/path_builder.rs @@ -0,0 +1,512 @@ +//! common utility for building a glyph, shared between backends + +use kurbo::{BezPath, PathEl, Point}; + +use crate::error::PathConversionError; + +/// Helps convert points-of-type to a bezier path. +/// +/// Source formats tend to use streams of point-of-type. Curve manipulation is +/// often easier on bezier path, so provide a mechanism to convert. +/// While kurbo::BezPath can contain multiple subpaths, and this builder could be +/// used to convert multiple contours (i.e. list of points) into a single BezPath, +/// our GlyphInstance.contours is defined as a `Vec`, so frontends should +/// convert one contour at a time. +#[derive(Debug)] +pub struct GlyphPathBuilder { + offcurve: Vec, + leading_offcurve: Vec, + path: Vec, + first_oncurve: Option, +} + +#[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; // close path often adds a few + } + 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(()) + } + + /// Lifts the "pen" to Point `p` and marks the beginning of an open contour. + /// + /// A point of this type can only be the first point in a contour. + /// Cf. "move" in + pub fn move_to(&mut self, p: impl Into) -> Result<(), PathConversionError> { + if !self.is_empty() { + return Err(PathConversionError::MoveAfterFirstPoint { point: p.into() }); + } + self.begin_path(OnCurve::Move(p.into())) + } + + /// Draws a line from the previous point to Point `p`. + /// + /// The previous point cannot be an off-curve point. + /// If this is the first point in a contour, the contour is assumed to be closed, + /// i.e. a cyclic list of points with no predominant start point. + /// Cf. "line" in + pub fn line_to(&mut self, p: impl Into) -> 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(()) + } + + /// Draws a quadratic curve/spline from the last non-offcurve point to Point `p`. + /// + /// This uses the TrueType "implied on-curve point" principle. + /// The number of preceding off-curve points can be n >= 0. When n=0, a straight line is + /// implied. If n=1, a single quadratic Bezier curve is drawn. If n>=2, a sequence of + /// quadratic Bezier curves is drawn, with the implied on-curve points at the midpoints + /// between pairs of successive off-curve points. + /// If this is the first point in a contour, the contour is assumed to be closed. + /// Cf. "qcurve" in + pub fn qcurve_to(&mut self, p: impl Into) -> Result<(), PathConversionError> { + // https://github.com/googlefonts/fontmake-rs/issues/110 + // Guard clauses: degenerate cases + if self.first_oncurve.is_none() { + return self.begin_path(OnCurve::Quad(p.into())); + } + if self.offcurve.is_empty() { + return self.line_to(p); + } + + // Insert an implied oncurve between every pair of offcurve points + for window in self.offcurve.windows(2) { + let curr = window[0]; + let next = window[1]; + // current offcurve to halfway to the next one + let implied = Point::new((curr.x + next.x) / 2.0, (curr.y + next.y) / 2.0); + self.path.push(PathEl::QuadTo(curr, implied)); + } + // last but not least, the last offcurve to the provided point + self.path + .push(PathEl::QuadTo(*self.offcurve.last().unwrap(), p.into())); + self.offcurve.clear(); + + Ok(()) + } + + /// Draws a cubic curve from the previous non-offcurve point to Point `p`. + /// + /// Type of curve depends on the number of accumulated off-curves: 0 (straight line), + /// 1 (quadratic Bezier) or 2 (cubic Bezier). + /// If this is the first point in a contour, the contour is assumed to be closed. + /// Cf. "curve" in + pub fn curve_to(&mut self, p: impl Into) -> 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(()) + } + + /// Append off-curve point `p` to the following curve segment. + /// + /// The type of curve is defined by following on-curve point, which can be either a + /// (cubic) "curve" or (quadratic) "qcurve". + /// If offcurve is the first point in a contour, the contour is assumed to be closed. + /// Cf. "offcurve" in + pub fn offcurve(&mut self, p: impl Into) -> Result<(), PathConversionError> { + if self.first_oncurve.is_some() { + self.offcurve.push(p.into()); + } else { + self.leading_offcurve.push(p.into()); + } + Ok(()) + } + + /// Ends the current sub-path. + /// + /// It's called automatically by `build()` thus can be + /// omitted when building one BezPath per contour, but can be called manually in + /// order to build multiple contours into a single BezPath. + pub fn end_path(&mut self) -> Result<(), PathConversionError> { + // a contour that does *not* start with a move is assumed to be closed + // https://unifiedfontobject.org/versions/ufo3/glyphs/glif/#point-types + 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> { + // Flush any leading off-curves to the end. This matches fontTools' PointToSegmentPen + // always starting/ending a closed contour on the first on-curve point: + // https://github.com/fonttools/fonttools/blob/57fb47/Lib/fontTools/pens/pointPen.py#L147-L155 + if !self.leading_offcurve.is_empty() { + self.offcurve.append(&mut self.leading_offcurve); + } + // Take dangling off-curves to imply a curve back to sub-path start. + // For closed paths we explicitly output the implied closing line + // equivalent to fontTools' PointToSegmentPen(outputImpliedClosingLine=True) + 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() { + // special TrueType oncurve-less quadratic contour, we assume the path + // starts at midpoint between the first and last offcurves + 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(()) + } + + /// Builds the kurbo::BezPath from the accumulated points. + pub fn build(mut self) -> Result { + 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(); // open contour + 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(); // closed, ie not starting with 'move' + 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(); // open contour + 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(); // closed + 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(); // open + 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(); // closed + 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(); // closed + 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(); // closed + 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(); + // a closing line is implied by Z, but emit it nonetheless + 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(); + // duplicate last point, not to be confused with the closing line implied by Z + 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() { + // if last curve point is equal to move, there's no need to disambiguate it from + // the implicit closing line, so we don't emit one + 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() { + // if last point is different from move, then emit the implied 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() { + // the following three closed contours are all equivalent and get normalized + // to the same path, which begins/ends on the first on-curve point i.e. (2,2). + 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.qcurve_to((0.0, 1.0)).unwrap(); // implied + 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() { + // A point of type 'move' must be the first point in an (open) contour. + let mut builder = GlyphPathBuilder::new(0); + builder.move_to((2.0, 2.0)).unwrap(); + builder.end_path().unwrap(); + // move_to after ending the current subpath is OK + builder.move_to((3.0, 3.0)).unwrap(); + // but it's an error if we try to do move_to again + 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(); + // can't move_to in the middle of a closed (not starting with move_to) subpath + 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(); + // can't move_to after an offcurve point + 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()); + } +}