Skip to content

Commit

Permalink
More extensive Contour::to_kurbo
Browse files Browse the repository at this point in the history
  • Loading branch information
madig committed Sep 22, 2022
1 parent 667a109 commit e86a178
Showing 1 changed file with 127 additions and 43 deletions.
170 changes: 127 additions & 43 deletions src/glyph/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -315,55 +315,139 @@ 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<kurbo::BezPath, ConvertContourError> {
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<Vec<kurbo::PathEl>, 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.len() {
// Empty contours cannot be represented by segments.
0 => return Ok(segments),
// Single points are converted to open MoveTos because closed single points of any
// PointType make no sense.
1 => {
segments.push(PathEl::MoveTo(Point::new(points[0].x as f64, points[0].y as f64)));
return Ok(segments);
}
// Contours with two or more points come in three flavors...:
_ => {
// 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 = points[0].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);
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 {
let first = points.first().unwrap();
let last = points.last().unwrap();
implied_oncurve = ContourPoint::new(
0.5 * (last.x + first.x),
0.5 * (last.y + first.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(Point::new(start.x as f64, start.y as f64)));

// Phase 2: Conversion
let mut controls: Vec<Point> = Vec::new();
for point in points {
let p = Point::new(point.x as f64, point.y as f64);
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.len() {
0 => 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.len() {
0 => segments.push(PathEl::LineTo(p)),
1 => {
segments.push(PathEl::QuadTo(controls[0], p));
controls.clear()
}
_ => {
// TODO: make iterator? controls.iter().zip(controls.iter().cycle().skip(1))
for i in 0..=controls.len() - 2 {
let c = controls[i];
let cn = controls[i + 1];
let pi = Point::new(0.5 * (c.x + cn.x), 0.5 * (c.y + cn.y));
segments.push(PathEl::QuadTo(c, pi));
}
segments.push(PathEl::QuadTo(controls[controls.len() - 1], 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.len() {
0 => segments.push(PathEl::LineTo(p)),
1 => {
segments.push(PathEl::QuadTo(controls[0], p));
controls.clear()
}
2 => {
segments.push(PathEl::CurveTo(controls[0], controls[1], 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)
}
}

Expand Down

0 comments on commit e86a178

Please sign in to comment.