diff --git a/src/glyph/mod.rs b/src/glyph/mod.rs index a018c3d4..01ad841e 100644 --- a/src/glyph/mod.rs +++ b/src/glyph/mod.rs @@ -315,55 +315,135 @@ impl Contour { self.points.first().map_or(true, |v| v.typ != PointType::Move) } - /// Converts the `Contour` to a [`kurbo::BezPath`]. + /// Converts the `Contour` to a Vec of [`kurbo::PathEl`]. #[cfg(feature = "kurbo")] - pub fn to_kurbo(&self) -> Result { - let mut path = kurbo::BezPath::new(); - let mut offs = std::collections::VecDeque::new(); - let mut points = if self.is_closed() { - // Add end-of-contour offcurves to queue - let rotate = self - .points - .iter() - .rev() - .position(|pt| pt.typ != PointType::OffCurve) - .map(|idx| self.points.len() - 1 - idx); - self.points.iter().cycle().skip(rotate.unwrap_or(0)).take(self.points.len() + 1) - } else { - self.points.iter().cycle().skip(0).take(self.points.len()) - }; - if let Some(start) = points.next() { - path.move_to(start.to_kurbo()); - } - for pt in points { - let kurbo_point = pt.to_kurbo(); - match pt.typ { - PointType::Move => path.move_to(kurbo_point), - PointType::Line => path.line_to(kurbo_point), - PointType::OffCurve => offs.push_back(kurbo_point), - PointType::Curve => { - match offs.make_contiguous() { - [] => return Err(ConvertContourError::new(ErrorKind::BadPoint)), - [p1] => path.quad_to(*p1, kurbo_point), - [p1, p2] => path.curve_to(*p1, *p2, kurbo_point), - _ => return Err(ConvertContourError::new(ErrorKind::TooManyOffCurves)), - }; - offs.clear(); + pub fn to_kurbo(&self) -> Result, ConvertContourError> { + use kurbo::{PathEl, Point}; + + let mut points: Vec<&ContourPoint> = self.points.iter().collect(); + let mut segments = Vec::new(); + + let closed; + let start: &ContourPoint; + let implied_oncurve: ContourPoint; + + // Phase 1: Preparation + match *points.as_slice() { + // Empty contours cannot be represented by segments. + [] => return Ok(segments), + // Single points are converted to open MoveTos because closed single points of any + // PointType make no sense. + [p0] => { + segments.push(PathEl::MoveTo(p0.to_kurbo())); + return Ok(segments); + } + // Contours with two or more points come in three flavors...: + [p0, .., pn] => { + // 1. ... Open contours begin with a Move. Start the segment on the first point + // and don't close it. Note: Trailing off-curves are an error. + if let PointType::Move = p0.typ { + closed = false; + // Pop off the Move here so the segmentation loop below can just error out on + // encountering any other Move. + start = points.remove(0); + } else { + closed = true; + // 2. ... Closed contours begin with anything else. Locate the first on-curve + // point and rotate the point list so that it _ends_ with that point. The first + // point could be a curve with its off-curves at the end; moving the point + // makes always makes all associated off-curves reachable in a single pass + // without wrapping around. Start the segment on the last point. + if let Some(first_oncurve) = + points.iter().position(|e| e.typ != PointType::OffCurve) + { + points.rotate_left(first_oncurve + 1); + // Recompute `last` after rotation: + start = points.last().unwrap(); + // 3. ... Closed all-offcurve quadratic contours: Rare special case of + // TrueType's “implied on-curve points” principle. Compute the last implied + // on-curve point and append it, so we can handle this normally in the loop + // below. Start the segment on the last, computed point. + } else { + implied_oncurve = ContourPoint::new( + 0.5 * (pn.x + p0.x), + 0.5 * (pn.y + p0.y), + PointType::QCurve, + false, + None, + None, + None, + ); + points.push(&implied_oncurve); + start = &implied_oncurve; + } } - PointType::QCurve => { - while let Some(pt) = offs.pop_front() { - if let Some(next) = offs.front() { - let implied_point = pt.midpoint(*next); - path.quad_to(pt, implied_point); - } else { - path.quad_to(pt, kurbo_point); + } + } + + // Phase 1.5: Always need a MoveTo as the first element. + segments.push(PathEl::MoveTo(start.to_kurbo())); + + // Phase 2: Conversion + let mut controls: Vec = Vec::new(); + for point in points { + let p = point.to_kurbo(); + match point.typ { + PointType::OffCurve => controls.push(p), + // The first Move is removed from the points above, any other Move we encounter is illegal. + PointType::Move => return Err(ConvertContourError::new(ErrorKind::UnexpectedMove)), + // A line must have 0 off-curves preceeding it. + PointType::Line => match *controls.as_slice() { + [] => segments.push(PathEl::LineTo(p)), + _ => { + return Err(ConvertContourError::new( + ErrorKind::UnexpectedPointAfterOffCurve, + )) + } + }, + // A quadratic curve can have any number of off-curves preceeding it. Zero means it's + // a line, numbers > 1 mean we must expand “implied on-curve points”. + PointType::QCurve => match *controls.as_slice() { + [] => segments.push(PathEl::LineTo(p)), + [c0] => { + segments.push(PathEl::QuadTo(c0, p)); + controls.clear() + } + [.., cn] => { + // Insert a computed on-curve point between each control point. + for (c0, c1) in controls.iter().zip(controls.iter().skip(1)) { + segments.push(PathEl::QuadTo(*c0, c0.midpoint(*c1))); } + segments.push(PathEl::QuadTo(cn, p)); + controls.clear() } - offs.clear(); - } + }, + // A curve can have 0, 1 or 2 off-curves preceeding it according to the UFO specification. + // Zero means it's a line, one means it's a quadratic curve, two means it's a cubic curve. + PointType::Curve => match *controls.as_slice() { + [] => segments.push(PathEl::LineTo(p)), + [c0] => { + segments.push(PathEl::QuadTo(c0, p)); + controls.clear() + } + [c0, c1] => { + segments.push(PathEl::CurveTo(c0, c1, p)); + controls.clear() + } + _ => return Err(ConvertContourError::new(ErrorKind::TooManyOffCurves)), + }, } } - Ok(path) + // If we have control points left at this point, we are an open contour, which must end on + // an on-curve point. + if !controls.is_empty() { + debug_assert!(!closed); + return Err(ConvertContourError::new(ErrorKind::TrailingOffCurves)); + } + if closed { + segments.push(PathEl::ClosePath); + } + + Ok(segments) } } @@ -815,3 +895,71 @@ impl From for druid::piet::Color { druid::piet::Color::rgba(red, green, blue, alpha) } } + +#[cfg(all(test, feature = "kurbo"))] +mod kurbo_tests { + use super::*; + + #[test] + fn many_control_quads() { + let c1 = Contour::new( + vec![ + ContourPoint::new(0.0, 0.0, PointType::OffCurve, false, None, None, None), + ContourPoint::new(2.0, 2.0, PointType::OffCurve, false, None, None, None), + ContourPoint::new(4.0, 4.0, PointType::OffCurve, false, None, None, None), + ContourPoint::new(100.0, 100.0, PointType::QCurve, false, None, None, None), + ], + None, + None, + ); + + assert_eq!( + c1.to_kurbo().unwrap(), + vec![ + kurbo::PathEl::MoveTo((100.0, 100.0).into()), + kurbo::PathEl::QuadTo((0.0, 0.0).into(), (1.0, 1.0).into(),), + kurbo::PathEl::QuadTo((2.0, 2.0).into(), (3.0, 3.0).into(),), + kurbo::PathEl::QuadTo((4.0, 4.0).into(), (100.0, 100.0).into(),), + kurbo::PathEl::ClosePath, + ] + ); + + let c2 = Contour::new( + vec![ + ContourPoint::new(0.0, 0.0, PointType::OffCurve, false, None, None, None), + ContourPoint::new(2.0, 2.0, PointType::OffCurve, false, None, None, None), + ContourPoint::new(100.0, 100.0, PointType::QCurve, false, None, None, None), + ], + None, + None, + ); + + assert_eq!( + c2.to_kurbo().unwrap(), + vec![ + kurbo::PathEl::MoveTo((100.0, 100.0).into()), + kurbo::PathEl::QuadTo((0.0, 0.0).into(), (1.0, 1.0).into(),), + kurbo::PathEl::QuadTo((2.0, 2.0).into(), (100.0, 100.0).into(),), + kurbo::PathEl::ClosePath, + ] + ); + + let c3 = Contour::new( + vec![ + ContourPoint::new(0.0, 0.0, PointType::OffCurve, false, None, None, None), + ContourPoint::new(100.0, 100.0, PointType::QCurve, false, None, None, None), + ], + None, + None, + ); + + assert_eq!( + c3.to_kurbo().unwrap(), + vec![ + kurbo::PathEl::MoveTo((100.0, 100.0).into()), + kurbo::PathEl::QuadTo((0.0, 0.0).into(), (100.0, 100.0).into(),), + kurbo::PathEl::ClosePath, + ] + ); + } +}