use kurbo::{ParamCurve, ParamCurveArclen, ParamCurveExtrema};
use crate::{FuzzyZero, PathBbox, Rect, Transform};
#[allow(missing_docs)]
#[derive(Clone, Copy, PartialEq, Debug)]
pub enum PathCommand {
MoveTo,
LineTo,
CurveTo,
ClosePath,
}
#[allow(missing_docs)]
#[derive(Clone, Copy, Debug)]
pub enum PathSegment {
MoveTo {
x: f64,
y: f64,
},
LineTo {
x: f64,
y: f64,
},
CurveTo {
x1: f64,
y1: f64,
x2: f64,
y2: f64,
x: f64,
y: f64,
},
ClosePath,
}
#[derive(Clone, Default, Debug)]
pub struct PathData {
commands: Vec<PathCommand>,
points: Vec<f64>,
}
impl PathData {
#[inline]
pub fn new() -> Self {
PathData::default()
}
#[inline]
pub fn is_empty(&self) -> bool {
self.commands.is_empty()
}
#[inline]
pub fn len(&self) -> usize {
self.commands.len()
}
#[inline]
pub fn commands(&self) -> &[PathCommand] {
&self.commands
}
#[inline]
pub fn points(&self) -> &[f64] {
&self.points
}
pub fn clear(&mut self) {
self.commands.clear();
self.points.clear();
}
pub fn shrink_to_fit(&mut self) {
self.commands.shrink_to_fit();
self.points.shrink_to_fit();
}
#[inline]
pub fn from_rect(rect: Rect) -> Self {
let mut path = PathData::default();
path.push_rect(rect);
path
}
#[inline]
pub fn push_move_to(&mut self, x: f64, y: f64) {
self.commands.push(PathCommand::MoveTo);
self.points.push(x);
self.points.push(y);
}
#[inline]
pub fn push_line_to(&mut self, x: f64, y: f64) {
self.commands.push(PathCommand::LineTo);
self.points.push(x);
self.points.push(y);
}
#[inline]
pub fn push_quad_to(&mut self, x1: f64, y1: f64, x: f64, y: f64) {
#[inline]
fn calc(n1: f64, n2: f64) -> f64 {
(n1 + n2 * 2.0) / 3.0
}
let (px, py) = self.last_pos();
self.push_curve_to(calc(px, x1), calc(py, y1), calc(x, x1), calc(y, y1), x, y)
}
#[inline]
pub fn push_curve_to(&mut self, x1: f64, y1: f64, x2: f64, y2: f64, x: f64, y: f64) {
self.commands.push(PathCommand::CurveTo);
self.points.push(x1);
self.points.push(y1);
self.points.push(x2);
self.points.push(y2);
self.points.push(x);
self.points.push(y);
}
pub fn push_arc_to(
&mut self,
rx: f64,
ry: f64,
x_axis_rotation: f64,
large_arc: bool,
sweep: bool,
x: f64,
y: f64,
) {
let (prev_x, prev_y) = self.last_pos();
let svg_arc = kurbo::SvgArc {
from: kurbo::Point::new(prev_x, prev_y),
to: kurbo::Point::new(x, y),
radii: kurbo::Vec2::new(rx, ry),
x_rotation: x_axis_rotation.to_radians(),
large_arc,
sweep,
};
match kurbo::Arc::from_svg_arc(&svg_arc) {
Some(arc) => {
arc.to_cubic_beziers(0.1, |p1, p2, p| {
self.push_curve_to(p1.x, p1.y, p2.x, p2.y, p.x, p.y);
});
}
None => {
self.push_line_to(x, y);
}
}
}
#[inline]
pub fn push_close_path(&mut self) {
self.commands.push(PathCommand::ClosePath);
}
#[inline]
pub fn push_rect(&mut self, rect: Rect) {
self.push_move_to(rect.x(), rect.y());
self.push_line_to(rect.right(), rect.y());
self.push_line_to(rect.right(), rect.bottom());
self.push_line_to(rect.x(), rect.bottom());
self.push_close_path();
}
#[inline]
pub fn push_path(&mut self, path: &PathData) {
self.commands.extend_from_slice(&path.commands);
self.points.extend_from_slice(&path.points);
}
#[inline]
fn last_pos(&self) -> (f64, f64) {
let seg = self.commands.last().expect("path must not be empty");
match seg {
PathCommand::ClosePath => {
panic!("the previous segment must be M/L/C")
}
_ => {
let index = self.points.len() - 2;
(self.points[index], self.points[index + 1])
}
}
}
#[inline]
pub fn bbox(&self) -> Option<PathBbox> {
calc_bbox(self)
}
#[inline]
pub fn bbox_with_transform(
&self,
ts: Transform,
stroke: Option<&super::Stroke>,
) -> Option<PathBbox> {
calc_bbox_with_transform(self, ts, stroke)
}
#[inline]
pub fn has_bbox(&self) -> bool {
has_bbox(self)
}
#[inline]
pub fn length(&self) -> f64 {
calc_length(self)
}
#[inline]
pub fn transform(&mut self, ts: Transform) {
transform_path(&mut self.points, ts);
}
#[inline]
pub fn transform_from(&mut self, offset: usize, ts: Transform) {
let mut points_offset = 0;
for command in self.commands().iter().take(offset) {
match command {
PathCommand::MoveTo | PathCommand::LineTo => points_offset += 2,
PathCommand::CurveTo => points_offset += 6,
PathCommand::ClosePath => {}
}
}
transform_path(&mut self.points[points_offset..], ts);
}
#[inline]
pub fn segments(&self) -> PathSegmentsIter {
PathSegmentsIter {
path: self,
cmd_index: 0,
points_index: 0,
}
}
}
#[allow(missing_debug_implementations)]
#[derive(Clone)]
pub struct PathSegmentsIter<'a> {
path: &'a PathData,
cmd_index: usize,
points_index: usize,
}
impl<'a> Iterator for PathSegmentsIter<'a> {
type Item = PathSegment;
fn next(&mut self) -> Option<Self::Item> {
if self.cmd_index < self.path.commands.len() {
let verb = self.path.commands[self.cmd_index];
self.cmd_index += 1;
match verb {
PathCommand::MoveTo => {
self.points_index += 2;
Some(PathSegment::MoveTo {
x: self.path.points[self.points_index - 2],
y: self.path.points[self.points_index - 1],
})
}
PathCommand::LineTo => {
self.points_index += 2;
Some(PathSegment::LineTo {
x: self.path.points[self.points_index - 2],
y: self.path.points[self.points_index - 1],
})
}
PathCommand::CurveTo => {
self.points_index += 6;
Some(PathSegment::CurveTo {
x1: self.path.points[self.points_index - 6],
y1: self.path.points[self.points_index - 5],
x2: self.path.points[self.points_index - 4],
y2: self.path.points[self.points_index - 3],
x: self.path.points[self.points_index - 2],
y: self.path.points[self.points_index - 1],
})
}
PathCommand::ClosePath => Some(PathSegment::ClosePath),
}
} else {
None
}
}
}
fn calc_bbox(path: &PathData) -> Option<PathBbox> {
if path.is_empty() {
return None;
}
let mut prev_x = path.points[0];
let mut prev_y = path.points[1];
let mut minx = prev_x;
let mut miny = prev_y;
let mut maxx = prev_x;
let mut maxy = prev_y;
for seg in path.segments() {
match seg {
PathSegment::MoveTo { x, y } | PathSegment::LineTo { x, y } => {
prev_x = x;
prev_y = y;
if x > maxx {
maxx = x;
} else if x < minx {
minx = x;
}
if y > maxy {
maxy = y;
} else if y < miny {
miny = y;
}
}
PathSegment::CurveTo {
x1,
y1,
x2,
y2,
x,
y,
} => {
let curve = kurbo::CubicBez::from_points(prev_x, prev_y, x1, y1, x2, y2, x, y);
let r = curve.bounding_box();
if r.x0 < minx {
minx = r.x0;
}
if r.x1 > maxx {
maxx = r.x1;
}
if r.y0 < miny {
miny = r.y0;
}
if r.y1 > maxy {
maxy = r.y1;
}
prev_x = x;
prev_y = y;
}
PathSegment::ClosePath => {}
}
}
let width = maxx - minx;
let height = maxy - miny;
PathBbox::new(minx, miny, width, height)
}
fn calc_bbox_with_transform(
path: &PathData,
ts: Transform,
stroke: Option<&super::Stroke>,
) -> Option<PathBbox> {
if path.is_empty() {
return None;
}
let mut prev_x = 0.0;
let mut prev_y = 0.0;
let mut minx = 0.0;
let mut miny = 0.0;
let mut maxx = 0.0;
let mut maxy = 0.0;
if let Some(PathSegment::MoveTo { x, y }) = TransformedPath::new(path, ts).next() {
prev_x = x;
prev_y = y;
minx = x;
miny = y;
maxx = x;
maxy = y;
}
for seg in TransformedPath::new(path, ts) {
match seg {
PathSegment::MoveTo { x, y } | PathSegment::LineTo { x, y } => {
prev_x = x;
prev_y = y;
if x > maxx {
maxx = x;
} else if x < minx {
minx = x;
}
if y > maxy {
maxy = y;
} else if y < miny {
miny = y;
}
}
PathSegment::CurveTo {
x1,
y1,
x2,
y2,
x,
y,
} => {
let curve = kurbo::CubicBez::from_points(prev_x, prev_y, x1, y1, x2, y2, x, y);
let r = curve.bounding_box();
if r.x0 < minx {
minx = r.x0;
}
if r.x1 > maxx {
maxx = r.x1;
}
if r.y0 < miny {
miny = r.y0;
}
if r.y1 > maxy {
maxy = r.y1;
}
prev_x = x;
prev_y = y;
}
PathSegment::ClosePath => {}
}
}
if let Some(stroke) = stroke {
let w = stroke.width.get()
/ if ts.is_default() {
2.0
} else {
2.0 / (ts.a * ts.d - ts.b * ts.c).abs().sqrt()
};
minx -= w;
miny -= w;
maxx += w;
maxy += w;
}
let width = maxx - minx;
let height = maxy - miny;
PathBbox::new(minx, miny, width, height)
}
fn has_bbox(path: &PathData) -> bool {
if path.is_empty() {
return false;
}
let mut prev_x = path.points[0];
let mut prev_y = path.points[1];
let mut minx = prev_x;
let mut miny = prev_y;
let mut maxx = prev_x;
let mut maxy = prev_y;
for seg in path.segments() {
match seg {
PathSegment::MoveTo { x, y } | PathSegment::LineTo { x, y } => {
prev_x = x;
prev_y = y;
if x > maxx {
maxx = x;
} else if x < minx {
minx = x;
}
if y > maxy {
maxy = y;
} else if y < miny {
miny = y;
}
}
PathSegment::CurveTo {
x1,
y1,
x2,
y2,
x,
y,
} => {
let curve = kurbo::CubicBez::from_points(prev_x, prev_y, x1, y1, x2, y2, x, y);
let r = curve.bounding_box();
if r.x0 < minx {
minx = r.x0;
}
if r.x1 > maxx {
maxx = r.x1;
}
if r.x0 < miny {
miny = r.y0;
}
if r.y1 > maxy {
maxy = r.y1;
}
prev_x = x;
prev_y = y;
}
PathSegment::ClosePath => {}
}
let width = maxx - minx;
let height = maxy - miny;
if !(width.is_fuzzy_zero() || height.is_fuzzy_zero()) {
return true;
}
}
false
}
fn calc_length(path: &PathData) -> f64 {
if path.is_empty() {
return 0.0;
}
let mut prev_mx = path.points[0];
let mut prev_my = path.points[1];
let mut prev_x = prev_mx;
let mut prev_y = prev_my;
fn create_curve_from_line(px: f64, py: f64, x: f64, y: f64) -> kurbo::CubicBez {
let line = kurbo::Line::new(kurbo::Point::new(px, py), kurbo::Point::new(x, y));
let p1 = line.eval(0.33);
let p2 = line.eval(0.66);
kurbo::CubicBez::from_points(px, py, p1.x, p1.y, p2.x, p2.y, x, y)
}
let mut length = 0.0;
for seg in path.segments() {
let curve = match seg {
PathSegment::MoveTo { x, y } => {
prev_mx = x;
prev_my = y;
prev_x = x;
prev_y = y;
continue;
}
PathSegment::LineTo { x, y } => create_curve_from_line(prev_x, prev_y, x, y),
PathSegment::CurveTo {
x1,
y1,
x2,
y2,
x,
y,
} => kurbo::CubicBez::from_points(prev_x, prev_y, x1, y1, x2, y2, x, y),
PathSegment::ClosePath => create_curve_from_line(prev_x, prev_y, prev_mx, prev_my),
};
length += curve.arclen(0.5);
prev_x = curve.p3.x;
prev_y = curve.p3.y;
}
length
}
fn transform_path(points: &mut [f64], ts: Transform) {
if points.is_empty() {
return;
}
if ts.is_default() {
return;
}
for p in points.chunks_exact_mut(2) {
let (x, y) = ts.apply(p[0], p[1]);
p[0] = x;
p[1] = y;
}
}
#[allow(missing_debug_implementations)]
pub struct TransformedPath<'a> {
iter: PathSegmentsIter<'a>,
ts: Transform,
}
impl<'a> TransformedPath<'a> {
#[inline]
pub fn new(path: &'a PathData, ts: Transform) -> Self {
TransformedPath {
iter: path.segments(),
ts,
}
}
}
impl<'a> Iterator for TransformedPath<'a> {
type Item = PathSegment;
fn next(&mut self) -> Option<Self::Item> {
self.iter.next().map(|segment| match segment {
PathSegment::MoveTo { x, y } => {
let (x, y) = self.ts.apply(x, y);
PathSegment::MoveTo { x, y }
}
PathSegment::LineTo { x, y } => {
let (x, y) = self.ts.apply(x, y);
PathSegment::LineTo { x, y }
}
PathSegment::CurveTo {
x1,
y1,
x2,
y2,
x,
y,
} => {
let (x1, y1) = self.ts.apply(x1, y1);
let (x2, y2) = self.ts.apply(x2, y2);
let (x, y) = self.ts.apply(x, y);
PathSegment::CurveTo {
x1,
y1,
x2,
y2,
x,
y,
}
}
PathSegment::ClosePath => PathSegment::ClosePath,
})
}
}
pub(crate) trait CubicBezExt {
fn from_points(px: f64, py: f64, x1: f64, y1: f64, x2: f64, y2: f64, x: f64, y: f64) -> Self;
}
impl CubicBezExt for kurbo::CubicBez {
fn from_points(px: f64, py: f64, x1: f64, y1: f64, x2: f64, y2: f64, x: f64, y: f64) -> Self {
kurbo::CubicBez {
p0: kurbo::Point::new(px, py),
p1: kurbo::Point::new(x1, y1),
p2: kurbo::Point::new(x2, y2),
p3: kurbo::Point::new(x, y),
}
}
}