Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

More extensive to_kurbo #132

Draft
wants to merge 10 commits into
base: fix-eq-lints
Choose a base branch
from
Draft
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
234 changes: 191 additions & 43 deletions src/glyph/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<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.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<Point> = 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)
}
}

Expand Down Expand Up @@ -815,3 +895,71 @@ impl From<Color> 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,
]
);
}
}