use tinyvec::TinyVec;
use std::f64;
use std::f64::consts::*;
use std::slice;
use crate::float_eq_cairo::ApproxEqCairo;
use crate::path_parser::{ParseError, PathParser};
use crate::util::clamp;
#[derive(Debug, Copy, Clone, PartialEq)]
pub struct LargeArc(pub bool);
#[derive(Debug, Copy, Clone, PartialEq)]
pub enum Sweep {
Negative,
Positive,
}
#[derive(Debug, Clone, PartialEq, Default)]
pub struct CubicBezierCurve {
pub pt1: (f64, f64),
pub pt2: (f64, f64),
pub to: (f64, f64),
}
impl CubicBezierCurve {
fn from_coords(coords: &mut slice::Iter<'_, f64>) -> CubicBezierCurve {
let pt1 = take_two(coords);
let pt2 = take_two(coords);
let to = take_two(coords);
CubicBezierCurve { pt1, pt2, to }
}
fn to_packed_and_coords(&self, coords: &mut Vec<f64>) -> PackedCommand {
coords.push(self.pt1.0);
coords.push(self.pt1.1);
coords.push(self.pt2.0);
coords.push(self.pt2.1);
coords.push(self.to.0);
coords.push(self.to.1);
PackedCommand::CurveTo
}
}
pub enum ArcParameterization {
CenterParameters {
center: (f64, f64),
radii: (f64, f64),
theta1: f64,
delta_theta: f64,
},
LineTo,
Omit,
}
#[derive(Debug, Clone, PartialEq)]
pub struct EllipticalArc {
pub r: (f64, f64),
pub x_axis_rotation: f64,
pub large_arc: LargeArc,
pub sweep: Sweep,
pub from: (f64, f64),
pub to: (f64, f64),
}
impl EllipticalArc {
pub(crate) fn center_parameterization(&self) -> ArcParameterization {
let Self {
r: (mut rx, mut ry),
x_axis_rotation,
large_arc,
sweep,
from: (x1, y1),
to: (x2, y2),
} = *self;
if rx * rx < f64::EPSILON || ry * ry < f64::EPSILON {
return ArcParameterization::LineTo;
}
let is_large_arc = large_arc.0;
let is_positive_sweep = sweep == Sweep::Positive;
let phi = x_axis_rotation * PI / 180.0;
let (sin_phi, cos_phi) = phi.sin_cos();
rx = rx.abs();
ry = ry.abs();
let mid_x = (x1 - x2) / 2.0;
let mid_y = (y1 - y2) / 2.0;
let x1_ = cos_phi * mid_x + sin_phi * mid_y;
let y1_ = -sin_phi * mid_x + cos_phi * mid_y;
let lambda = (x1_ / rx).powi(2) + (y1_ / ry).powi(2);
if lambda > 1.0 {
rx *= lambda.sqrt();
ry *= lambda.sqrt();
}
let d = (rx * y1_).powi(2) + (ry * x1_).powi(2);
if d == 0.0 {
return ArcParameterization::Omit;
}
let k = {
let mut k = ((rx * ry).powi(2) / d - 1.0).abs().sqrt();
if is_positive_sweep == is_large_arc {
k = -k;
}
k
};
let cx_ = k * rx * y1_ / ry;
let cy_ = -k * ry * x1_ / rx;
let cx = cos_phi * cx_ - sin_phi * cy_ + (x1 + x2) / 2.0;
let cy = sin_phi * cx_ + cos_phi * cy_ + (y1 + y2) / 2.0;
let ux = (x1_ - cx_) / rx;
let uy = (y1_ - cy_) / ry;
let u_len = (ux * ux + uy * uy).abs().sqrt();
if u_len == 0.0 {
return ArcParameterization::Omit;
}
let cos_theta1 = clamp(ux / u_len, -1.0, 1.0);
let theta1 = {
let mut theta1 = cos_theta1.acos();
if uy < 0.0 {
theta1 = -theta1;
}
theta1
};
let vx = (-x1_ - cx_) / rx;
let vy = (-y1_ - cy_) / ry;
let v_len = (vx * vx + vy * vy).abs().sqrt();
if v_len == 0.0 {
return ArcParameterization::Omit;
}
let dp_uv = ux * vx + uy * vy;
let cos_delta_theta = clamp(dp_uv / (u_len * v_len), -1.0, 1.0);
let delta_theta = {
let mut delta_theta = cos_delta_theta.acos();
if ux * vy - uy * vx < 0.0 {
delta_theta = -delta_theta;
}
if is_positive_sweep && delta_theta < 0.0 {
delta_theta += PI * 2.0;
} else if !is_positive_sweep && delta_theta > 0.0 {
delta_theta -= PI * 2.0;
}
delta_theta
};
ArcParameterization::CenterParameters {
center: (cx, cy),
radii: (rx, ry),
theta1,
delta_theta,
}
}
fn from_coords(
large_arc: LargeArc,
sweep: Sweep,
coords: &mut slice::Iter<'_, f64>,
) -> EllipticalArc {
let r = take_two(coords);
let x_axis_rotation = take_one(coords);
let from = take_two(coords);
let to = take_two(coords);
EllipticalArc {
r,
x_axis_rotation,
large_arc,
sweep,
from,
to,
}
}
fn to_packed_and_coords(&self, coords: &mut Vec<f64>) -> PackedCommand {
coords.push(self.r.0);
coords.push(self.r.1);
coords.push(self.x_axis_rotation);
coords.push(self.from.0);
coords.push(self.from.1);
coords.push(self.to.0);
coords.push(self.to.1);
match (self.large_arc, self.sweep) {
(LargeArc(false), Sweep::Negative) => PackedCommand::ArcSmallNegative,
(LargeArc(false), Sweep::Positive) => PackedCommand::ArcSmallPositive,
(LargeArc(true), Sweep::Negative) => PackedCommand::ArcLargeNegative,
(LargeArc(true), Sweep::Positive) => PackedCommand::ArcLargePositive,
}
}
}
pub(crate) fn arc_segment(
c: (f64, f64),
r: (f64, f64),
x_axis_rotation: f64,
th0: f64,
th1: f64,
) -> CubicBezierCurve {
let (cx, cy) = c;
let (rx, ry) = r;
let phi = x_axis_rotation * PI / 180.0;
let (sin_phi, cos_phi) = phi.sin_cos();
let (sin_th0, cos_th0) = th0.sin_cos();
let (sin_th1, cos_th1) = th1.sin_cos();
let th_half = 0.5 * (th1 - th0);
let t = (8.0 / 3.0) * (th_half * 0.5).sin().powi(2) / th_half.sin();
let x1 = rx * (cos_th0 - t * sin_th0);
let y1 = ry * (sin_th0 + t * cos_th0);
let x3 = rx * cos_th1;
let y3 = ry * sin_th1;
let x2 = x3 + rx * (t * sin_th1);
let y2 = y3 + ry * (-t * cos_th1);
CubicBezierCurve {
pt1: (
cx + cos_phi * x1 - sin_phi * y1,
cy + sin_phi * x1 + cos_phi * y1,
),
pt2: (
cx + cos_phi * x2 - sin_phi * y2,
cy + sin_phi * x2 + cos_phi * y2,
),
to: (
cx + cos_phi * x3 - sin_phi * y3,
cy + sin_phi * x3 + cos_phi * y3,
),
}
}
#[derive(Clone, Default, Debug, PartialEq)]
pub enum PathCommand {
MoveTo(f64, f64),
LineTo(f64, f64),
CurveTo(CubicBezierCurve),
Arc(EllipticalArc),
#[default]
ClosePath,
}
impl PathCommand {
fn num_coordinates(&self) -> usize {
match *self {
PathCommand::MoveTo(..) => 2,
PathCommand::LineTo(..) => 2,
PathCommand::CurveTo(_) => 6,
PathCommand::Arc(_) => 7,
PathCommand::ClosePath => 0,
}
}
fn to_packed(&self, coords: &mut Vec<f64>) -> PackedCommand {
match *self {
PathCommand::MoveTo(x, y) => {
coords.push(x);
coords.push(y);
PackedCommand::MoveTo
}
PathCommand::LineTo(x, y) => {
coords.push(x);
coords.push(y);
PackedCommand::LineTo
}
PathCommand::CurveTo(ref c) => c.to_packed_and_coords(coords),
PathCommand::Arc(ref a) => a.to_packed_and_coords(coords),
PathCommand::ClosePath => PackedCommand::ClosePath,
}
}
fn from_packed(packed: PackedCommand, coords: &mut slice::Iter<'_, f64>) -> PathCommand {
match packed {
PackedCommand::MoveTo => {
let x = take_one(coords);
let y = take_one(coords);
PathCommand::MoveTo(x, y)
}
PackedCommand::LineTo => {
let x = take_one(coords);
let y = take_one(coords);
PathCommand::LineTo(x, y)
}
PackedCommand::CurveTo => PathCommand::CurveTo(CubicBezierCurve::from_coords(coords)),
PackedCommand::ClosePath => PathCommand::ClosePath,
PackedCommand::ArcSmallNegative => PathCommand::Arc(EllipticalArc::from_coords(
LargeArc(false),
Sweep::Negative,
coords,
)),
PackedCommand::ArcSmallPositive => PathCommand::Arc(EllipticalArc::from_coords(
LargeArc(false),
Sweep::Positive,
coords,
)),
PackedCommand::ArcLargeNegative => PathCommand::Arc(EllipticalArc::from_coords(
LargeArc(true),
Sweep::Negative,
coords,
)),
PackedCommand::ArcLargePositive => PathCommand::Arc(EllipticalArc::from_coords(
LargeArc(true),
Sweep::Positive,
coords,
)),
}
}
}
#[derive(Default)]
pub struct PathBuilder {
path_commands: TinyVec<[PathCommand; 32]>,
}
#[derive(Default)]
pub struct Path {
commands: Box<[PackedCommand]>,
coords: Box<[f64]>,
}
#[repr(u8)]
#[derive(Debug, Clone, Copy)]
enum PackedCommand {
MoveTo,
LineTo,
CurveTo,
ArcSmallNegative,
ArcSmallPositive,
ArcLargeNegative,
ArcLargePositive,
ClosePath,
}
impl PackedCommand {
fn num_coordinates(&self) -> usize {
match *self {
PackedCommand::MoveTo => 2,
PackedCommand::LineTo => 2,
PackedCommand::CurveTo => 6,
PackedCommand::ArcSmallNegative
| PackedCommand::ArcSmallPositive
| PackedCommand::ArcLargeNegative
| PackedCommand::ArcLargePositive => 7,
PackedCommand::ClosePath => 0,
}
}
}
impl PathBuilder {
pub fn parse(&mut self, path_str: &str) -> Result<(), ParseError> {
let mut parser = PathParser::new(self, path_str);
parser.parse()
}
pub fn into_path(self) -> Path {
let num_coords = self
.path_commands
.iter()
.map(PathCommand::num_coordinates)
.sum();
let mut coords = Vec::with_capacity(num_coords);
let packed_commands: Vec<_> = self
.path_commands
.iter()
.map(|cmd| cmd.to_packed(&mut coords))
.collect();
Path {
commands: packed_commands.into_boxed_slice(),
coords: coords.into_boxed_slice(),
}
}
pub fn move_to(&mut self, x: f64, y: f64) {
self.path_commands.push(PathCommand::MoveTo(x, y));
}
pub fn line_to(&mut self, x: f64, y: f64) {
self.path_commands.push(PathCommand::LineTo(x, y));
}
pub fn curve_to(&mut self, x2: f64, y2: f64, x3: f64, y3: f64, x4: f64, y4: f64) {
let curve = CubicBezierCurve {
pt1: (x2, y2),
pt2: (x3, y3),
to: (x4, y4),
};
self.path_commands.push(PathCommand::CurveTo(curve));
}
pub fn arc(
&mut self,
x1: f64,
y1: f64,
rx: f64,
ry: f64,
x_axis_rotation: f64,
large_arc: LargeArc,
sweep: Sweep,
x2: f64,
y2: f64,
) {
let arc = EllipticalArc {
r: (rx, ry),
x_axis_rotation,
large_arc,
sweep,
from: (x1, y1),
to: (x2, y2),
};
self.path_commands.push(PathCommand::Arc(arc));
}
pub fn close_path(&mut self) {
self.path_commands.push(PathCommand::ClosePath);
}
}
pub struct SubPathIter<'a> {
path: &'a Path,
commands_start: usize,
coords_start: usize,
}
pub struct SubPath<'a> {
commands: &'a [PackedCommand],
coords: &'a [f64],
}
pub struct SubPathCommandsIter<'a> {
commands_iter: slice::Iter<'a, PackedCommand>,
coords_iter: slice::Iter<'a, f64>,
}
impl<'a> SubPath<'a> {
pub fn iter_commands(&self) -> SubPathCommandsIter<'_> {
SubPathCommandsIter {
commands_iter: self.commands.iter(),
coords_iter: self.coords.iter(),
}
}
pub fn origin(&self) -> (f64, f64) {
let first = *self.commands.first().unwrap();
assert!(matches!(first, PackedCommand::MoveTo));
let command = PathCommand::from_packed(first, &mut self.coords.iter());
match command {
PathCommand::MoveTo(x, y) => (x, y),
_ => unreachable!(),
}
}
pub fn is_zero_length(&self) -> bool {
let (cur_x, cur_y) = self.origin();
for cmd in self.iter_commands().skip(1) {
let (end_x, end_y) = match cmd {
PathCommand::MoveTo(_, _) => unreachable!(
"A MoveTo cannot appear in a subpath if it's not the first element"
),
PathCommand::LineTo(x, y) => (x, y),
PathCommand::CurveTo(curve) => curve.to,
PathCommand::Arc(arc) => arc.to,
PathCommand::ClosePath => return true,
};
if !end_x.approx_eq_cairo(cur_x) || !end_y.approx_eq_cairo(cur_y) {
return false;
}
}
true
}
}
impl<'a> Iterator for SubPathIter<'a> {
type Item = SubPath<'a>;
fn next(&mut self) -> Option<Self::Item> {
if self.commands_start >= self.path.commands.len() {
return None;
}
let commands = &self.path.commands[self.commands_start..];
assert!(matches!(commands.first().unwrap(), PackedCommand::MoveTo));
let mut num_coords = PackedCommand::MoveTo.num_coordinates();
for (i, cmd) in commands.iter().enumerate().skip(1) {
if let PackedCommand::MoveTo = cmd {
let subpath_coords_start = self.coords_start;
self.commands_start += i;
self.coords_start += num_coords;
return Some(SubPath {
commands: &commands[..i],
coords: &self.path.coords
[subpath_coords_start..subpath_coords_start + num_coords],
});
} else {
num_coords += cmd.num_coordinates();
}
}
self.commands_start = self.path.commands.len();
let subpath_coords_start = self.coords_start;
assert!(subpath_coords_start + num_coords == self.path.coords.len());
self.coords_start = self.path.coords.len();
Some(SubPath {
commands,
coords: &self.path.coords[subpath_coords_start..],
})
}
}
impl<'a> Iterator for SubPathCommandsIter<'a> {
type Item = PathCommand;
fn next(&mut self) -> Option<Self::Item> {
self.commands_iter
.next()
.map(|packed| PathCommand::from_packed(*packed, &mut self.coords_iter))
}
}
impl Path {
pub fn iter_subpath(&self) -> SubPathIter<'_> {
SubPathIter {
path: self,
commands_start: 0,
coords_start: 0,
}
}
pub fn iter(&self) -> impl Iterator<Item = PathCommand> + '_ {
let commands = self.commands.iter();
let mut coords = self.coords.iter();
commands.map(move |cmd| PathCommand::from_packed(*cmd, &mut coords))
}
pub fn is_empty(&self) -> bool {
self.commands.is_empty()
}
}
fn take_one(iter: &mut slice::Iter<'_, f64>) -> f64 {
*iter.next().unwrap()
}
fn take_two(iter: &mut slice::Iter<'_, f64>) -> (f64, f64) {
(take_one(iter), take_one(iter))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn empty_builder() {
let builder = PathBuilder::default();
let path = builder.into_path();
assert!(path.is_empty());
assert_eq!(path.iter().count(), 0);
}
#[test]
fn empty_path() {
let path = Path::default();
assert!(path.is_empty());
assert_eq!(path.iter().count(), 0);
}
#[test]
fn all_commands() {
let mut builder = PathBuilder::default();
builder.move_to(42.0, 43.0);
builder.line_to(42.0, 43.0);
builder.curve_to(42.0, 43.0, 44.0, 45.0, 46.0, 47.0);
builder.arc(
42.0,
43.0,
44.0,
45.0,
46.0,
LargeArc(true),
Sweep::Positive,
47.0,
48.0,
);
builder.close_path();
let path = builder.into_path();
assert!(path.iter().eq(vec![
PathCommand::MoveTo(42.0, 43.0),
PathCommand::LineTo(42.0, 43.0),
PathCommand::CurveTo(CubicBezierCurve {
pt1: (42.0, 43.0),
pt2: (44.0, 45.0),
to: (46.0, 47.0),
}),
PathCommand::Arc(EllipticalArc {
from: (42.0, 43.0),
r: (44.0, 45.0),
to: (47.0, 48.0),
x_axis_rotation: 46.0,
large_arc: LargeArc(true),
sweep: Sweep::Positive,
}),
PathCommand::ClosePath,
]));
}
#[test]
fn subpath_iter() {
let mut builder = PathBuilder::default();
builder.move_to(42.0, 43.0);
builder.line_to(42.0, 43.0);
builder.close_path();
builder.move_to(22.0, 22.0);
builder.curve_to(22.0, 22.0, 44.0, 45.0, 46.0, 47.0);
builder.move_to(69.0, 69.0);
builder.line_to(42.0, 43.0);
let path = builder.into_path();
let subpaths = path
.iter_subpath()
.map(|subpath| {
(
subpath.origin(),
subpath.iter_commands().collect::<Vec<PathCommand>>(),
)
})
.collect::<Vec<((f64, f64), Vec<PathCommand>)>>();
assert_eq!(
subpaths,
vec![
(
(42.0, 43.0),
vec![
PathCommand::MoveTo(42.0, 43.0),
PathCommand::LineTo(42.0, 43.0),
PathCommand::ClosePath
]
),
(
(22.0, 22.0),
vec![
PathCommand::MoveTo(22.0, 22.0),
PathCommand::CurveTo(CubicBezierCurve {
pt1: (22.0, 22.0),
pt2: (44.0, 45.0),
to: (46.0, 47.0)
})
]
),
(
(69.0, 69.0),
vec![
PathCommand::MoveTo(69.0, 69.0),
PathCommand::LineTo(42.0, 43.0)
]
)
]
);
}
#[test]
fn zero_length_subpaths() {
let mut builder = PathBuilder::default();
builder.move_to(42.0, 43.0);
builder.move_to(44.0, 45.0);
builder.close_path();
builder.move_to(46.0, 47.0);
builder.line_to(48.0, 49.0);
let path = builder.into_path();
let subpaths = path
.iter_subpath()
.map(|subpath| (subpath.is_zero_length(), subpath.origin()))
.collect::<Vec<(bool, (f64, f64))>>();
assert_eq!(
subpaths,
vec![
(true, (42.0, 43.0)),
(true, (44.0, 45.0)),
(false, (46.0, 47.0)),
]
);
}
}