You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

412 lines
15 KiB

use euclid::Angle;
use lyon_geom::{
ArcFlags, CubicBezierSegment, Line, LineSegment, Point, Scalar, SvgArc, Transform, Vector,
};
pub enum ArcOrLineSegment<S> {
Arc(SvgArc<S>),
Line(LineSegment<S>),
}
fn arc_from_endpoints_and_tangents<S: Scalar>(
from: Point<S>,
from_tangent: Vector<S>,
to: Point<S>,
to_tangent: Vector<S>,
) -> Option<SvgArc<S>> {
let from_to = (from - to).length();
let incenter = {
let from_tangent = Line {
point: from,
vector: from_tangent,
};
let to_tangent = Line {
point: to,
vector: to_tangent,
};
let intersection = from_tangent.intersection(&to_tangent)?;
let from_intersection = (from - intersection).length();
let to_intersection = (to - intersection).length();
(((from * to_intersection).to_vector()
+ (to * from_intersection).to_vector()
+ (intersection * from_to).to_vector())
/ (from_intersection + to_intersection + from_to))
.to_point()
};
let get_perpendicular_bisector = |a, b| {
let vector: Vector<S> = a - b;
let perpendicular_vector = Vector::from([-vector.y, vector.x]).normalize();
Line {
point: LineSegment { from: a, to: b }.sample(S::HALF),
vector: perpendicular_vector,
}
};
let from_incenter_bisector = get_perpendicular_bisector(from, incenter);
let to_incenter_bisector = get_perpendicular_bisector(to, incenter);
let center = from_incenter_bisector.intersection(&to_incenter_bisector)?;
let radius = (from - center).length();
// Use the 2D determinant + dot product to identify winding direction
// See https://www.w3.org/TR/SVG/paths.html#PathDataEllipticalArcCommands for
// a nice visual explanation of large arc and sweep
let flags = {
let from_center = (from - center).normalize();
let to_center = (to - center).normalize();
let det = from_center.x * to_center.y - from_center.y * to_center.x;
let dot = from_center.dot(to_center);
let atan2 = det.atan2(dot);
ArcFlags {
large_arc: atan2.abs() >= S::PI(),
sweep: atan2.is_sign_positive(),
}
};
Some(SvgArc {
from,
to,
radii: Vector::splat(radius),
// This is a circular arc
x_rotation: Angle::zero(),
flags,
})
}
pub trait FlattenWithArcs<S> {
fn flattened(&self, tolerance: S) -> Vec<ArcOrLineSegment<S>>;
}
impl<S> FlattenWithArcs<S> for CubicBezierSegment<S>
where
S: Scalar + Copy,
{
/// Implementation of [Modeling of Bézier Curves Using a Combination of Linear and Circular Arc Approximations](https://sci-hub.st/https://doi.org/10.1109/CGIV.2012.20)
///
/// There are some slight deviations like using monotonic ranges instead of bounding by inflection points.
///
/// Kaewsaiha, P., & Dejdumrong, N. (2012). Modeling of Bézier Curves Using a Combination of Linear and Circular Arc Approximations. 2012 Ninth International Conference on Computer Graphics, Imaging and Visualization. doi:10.1109/cgiv.2012.20
///
fn flattened(&self, tolerance: S) -> Vec<ArcOrLineSegment<S>> {
if (self.to - self.from).square_length() < S::EPSILON {
return vec![];
} else if self.is_linear(tolerance) {
return vec![ArcOrLineSegment::Line(self.baseline())];
}
let mut acc = vec![];
self.for_each_monotonic_range(&mut |range| {
let inner_bezier = self.split_range(range);
if (inner_bezier.to - inner_bezier.from).square_length() < S::EPSILON {
return;
} else if inner_bezier.is_linear(tolerance) {
acc.push(ArcOrLineSegment::Line(inner_bezier.baseline()));
return;
}
if let Some(svg_arc) = arc_from_endpoints_and_tangents(
inner_bezier.from,
inner_bezier.derivative(S::ZERO),
inner_bezier.to,
inner_bezier.derivative(S::ONE),
)
.filter(|svg_arc| {
let arc = svg_arc.to_arc();
let mut max_deviation = S::ZERO;
// TODO: find a better way to check tolerance
// Ideally: derivative of |f(x) - g(x)| and look at 0 crossings
for i in 1..20 {
let t = S::from(i).unwrap() / S::from(20).unwrap();
max_deviation =
max_deviation.max((arc.sample(t) - inner_bezier.sample(t)).length());
}
max_deviation < tolerance
}) {
acc.push(ArcOrLineSegment::Arc(svg_arc));
} else {
let (left, right) = inner_bezier.split(S::HALF);
acc.append(&mut FlattenWithArcs::flattened(&left, tolerance));
acc.append(&mut FlattenWithArcs::flattened(&right, tolerance));
}
});
acc
}
}
impl<S> FlattenWithArcs<S> for SvgArc<S>
where
S: Scalar,
{
fn flattened(&self, tolerance: S) -> Vec<ArcOrLineSegment<S>> {
if (self.to - self.from).square_length() < S::EPSILON {
return vec![];
} else if self.is_straight_line() {
return vec![ArcOrLineSegment::Line(LineSegment {
from: self.from,
to: self.to,
})];
} else if (self.radii.x.abs() - self.radii.y.abs()).abs() < S::EPSILON {
return vec![ArcOrLineSegment::Arc(*self)];
}
let self_arc = self.to_arc();
if let Some(svg_arc) = arc_from_endpoints_and_tangents(
self.from,
self_arc.sample_tangent(S::ZERO),
self.to,
self_arc.sample_tangent(S::ONE),
)
.filter(|approx_svg_arc| {
let approx_arc = approx_svg_arc.to_arc();
let mut max_deviation = S::ZERO;
// TODO: find a better way to check tolerance
// Ideally: derivative of |f(x) - g(x)| and look at 0 crossings
for i in 1..20 {
let t = S::from(i).unwrap() / S::from(20).unwrap();
max_deviation =
max_deviation.max((approx_arc.sample(t) - self_arc.sample(t)).length());
}
max_deviation < tolerance
}) {
vec![ArcOrLineSegment::Arc(svg_arc)]
} else {
let (left, right) = self_arc.split(S::HALF);
let mut acc = FlattenWithArcs::flattened(&left.to_svg_arc(), tolerance);
acc.append(&mut FlattenWithArcs::flattened(
&right.to_svg_arc(),
tolerance,
));
acc
}
}
}
pub trait Transformed<S> {
fn transformed(&self, transform: &Transform<S>) -> Self;
}
impl<S: Scalar> Transformed<S> for SvgArc<S> {
/// A lot of the math here is heavily borrowed from [Vitaly Putzin's svgpath](https://github.com/fontello/svgpath).
///
/// The code is Rust-ified with only one or two changes, but I plan to understand the math here and
/// merge changes upstream to lyon-geom.
fn transformed(&self, transform: &Transform<S>) -> Self {
let from = transform.transform_point(self.from);
let to = transform.transform_point(self.to);
// Translation does not affect rotation, radii, or flags
let [a, b, c, d, _tx, _ty] = transform.to_array();
let (x_rotation, radii) = {
let (sin, cos) = self.x_rotation.sin_cos();
// Radii are axis-aligned -- rotate & transform
let ma = [
self.radii.x * (a * cos + c * sin),
self.radii.x * (b * cos + d * sin),
self.radii.y * (-a * sin + c * cos),
self.radii.y * (-b * sin + d * cos),
];
// ma * transpose(ma) = [ J L ]
// [ L K ]
// L is calculated later (if the image is not a circle)
let J = ma[0].powi(2) + ma[2].powi(2);
let K = ma[1].powi(2) + ma[3].powi(2);
// the discriminant of the characteristic polynomial of ma * transpose(ma)
let D = ((ma[0] - ma[3]).powi(2) + (ma[2] + ma[1]).powi(2))
* ((ma[0] + ma[3]).powi(2) + (ma[2] - ma[1]).powi(2));
// the "mean eigenvalue"
let JK = (J + K) / S::TWO;
// check if the image is (almost) a circle
if D < S::EPSILON * JK {
// if it is
(Angle::zero(), Vector::splat(JK.sqrt()))
} else {
// if it is not a circle
let L = ma[0] * ma[1] + ma[2] * ma[3];
let D = D.sqrt();
// {l1,l2} = the two eigen values of ma * transpose(ma)
let l1 = JK + D / S::TWO;
let l2 = JK - D / S::TWO;
// the x - axis - rotation angle is the argument of the l1 - eigenvector
let ax = if L.abs() < S::EPSILON && (l1 - K).abs() < S::EPSILON {
Angle::frac_pi_2()
} else {
Angle::radians(
(if L.abs() > (l1 - K).abs() {
(l1 - J) / L
} else {
L / (l1 - K)
})
.atan(),
)
};
(ax, Vector::from([l1.sqrt(), l2.sqrt()]))
}
};
// A mirror transform causes this flag to be flipped
let invert_sweep = { (a * d) - (b * c) < S::ZERO };
let flags = ArcFlags {
sweep: if invert_sweep {
!self.flags.sweep
} else {
self.flags.sweep
},
large_arc: self.flags.large_arc,
};
Self {
from,
to,
radii,
x_rotation,
flags,
}
}
}
#[cfg(test)]
mod tests {
use cairo::{Context, SvgSurface};
use lyon_geom::{point, vector, CubicBezierSegment, Point, Vector};
use std::path::PathBuf;
use svgtypes::PathParser;
use crate::arc::{ArcOrLineSegment, FlattenWithArcs};
#[test]
fn flatten_returns_expected_arcs() {
const PATH: &'static str = "M 8.0549,11.9023
c
0.13447,1.69916 8.85753,-5.917903 7.35159,-6.170957
z";
let mut surf =
SvgSurface::new(128., 128., Some(PathBuf::from("approx_circle.svg"))).unwrap();
surf.set_document_unit(cairo::SvgUnit::Mm);
let ctx = Context::new(&surf).unwrap();
ctx.set_line_width(0.2);
let mut current_position = Point::zero();
let mut acc = 0;
for path in PathParser::from(PATH) {
use svgtypes::PathSegment::*;
match path.unwrap() {
MoveTo { x, y, abs } => {
if abs {
ctx.move_to(x, y);
current_position = point(x, y);
} else {
ctx.rel_move_to(x, y);
current_position += vector(x, y);
}
}
LineTo { x, y, abs } => {
if abs {
ctx.line_to(x, y);
current_position = point(x, y);
} else {
ctx.rel_line_to(x, y);
current_position += vector(x, y);
}
}
ClosePath { .. } => ctx.close_path(),
CurveTo {
x1,
y1,
x2,
y2,
x,
y,
abs,
} => {
ctx.set_dash(&[], 0.);
match acc {
0 => ctx.set_source_rgb(1., 0., 0.),
1 => ctx.set_source_rgb(0., 1., 0.),
2 => ctx.set_source_rgb(0., 0., 1.),
3 => ctx.set_source_rgb(0., 0., 0.),
_ => unreachable!(),
}
let curve = CubicBezierSegment {
from: current_position,
ctrl1: (vector(x1, y1)
+ if !abs {
current_position.to_vector()
} else {
Vector::zero()
})
.to_point(),
ctrl2: (vector(x2, y2)
+ if !abs {
current_position.to_vector()
} else {
Vector::zero()
})
.to_point(),
to: (vector(x, y)
+ if !abs {
current_position.to_vector()
} else {
Vector::zero()
})
.to_point(),
};
for segment in FlattenWithArcs::flattened(&curve, 0.02) {
match segment {
ArcOrLineSegment::Arc(svg_arc) => {
let arc = svg_arc.to_arc();
if svg_arc.flags.sweep {
ctx.arc(
arc.center.x,
arc.center.y,
arc.radii.x,
arc.start_angle.radians,
(arc.start_angle + arc.sweep_angle).radians,
)
} else {
ctx.arc_negative(
arc.center.x,
arc.center.y,
arc.radii.x,
arc.start_angle.radians,
(arc.start_angle + arc.sweep_angle).radians,
)
}
}
ArcOrLineSegment::Line(line) => ctx.line_to(line.to.x, line.to.y),
}
}
ctx.stroke().unwrap();
current_position = curve.to;
ctx.set_dash(&[0.1], 0.);
ctx.move_to(curve.from.x, curve.from.y);
ctx.curve_to(
curve.ctrl1.x,
curve.ctrl1.y,
curve.ctrl2.x,
curve.ctrl2.y,
curve.to.x,
curve.to.y,
);
ctx.stroke().unwrap();
acc += 1;
}
other => unimplemented!("{:?}", other),
}
}
}
}