From 1757c69b32107cc3d1eb963ac40b1611a86f40a8 Mon Sep 17 00:00:00 2001 From: Colin Rofls Date: Wed, 11 Dec 2024 10:24:48 -0500 Subject: [PATCH] [fontir] Add ir::path_builder module And move all the GlyphPathBuilder stuff there. No functional change. I'd like to hang the 'erase open corners' code off of this, and the ir module is already huge. --- fontir/src/ir.rs | 513 +--------------------------------- fontir/src/ir/path_builder.rs | 512 +++++++++++++++++++++++++++++++++ 2 files changed, 518 insertions(+), 507 deletions(-) create mode 100644 fontir/src/ir/path_builder.rs 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()); + } +}