use crate::{
LineGeometry, PathFillRule, Point, Rect, Stroke, StrokeCap, StrokeJoin, VectorPath,
vector_path::{flatten_cubic_into, quad_as_cubic},
};
#[derive(Clone, Debug, Default, PartialEq)]
pub struct PathContour {
pub points: Vec<Point>,
pub closed: bool,
}
#[derive(Clone, Debug, Default, PartialEq)]
pub struct Path {
contours: Vec<PathContour>,
pen: Point,
}
impl Path {
pub fn new() -> Self {
Self::default()
}
pub fn move_to(&mut self, point: Point) {
self.contours.push(PathContour {
points: vec![point],
closed: false,
});
self.pen = point;
}
pub fn line_to(&mut self, point: Point) {
self.open_contour().push(point);
self.pen = point;
}
pub fn quadratic_to(&mut self, control: Point, end: Point) {
let (first, second) = quad_as_cubic(self.pen, control, end);
self.cubic_to(first, second, end);
}
pub fn cubic_to(&mut self, first: Point, second: Point, end: Point) {
let start = self.pen;
flatten_cubic_into(self.open_contour(), start, first, second, end);
self.pen = end;
}
pub fn close(&mut self) {
if let Some(contour) = self.contours.last_mut().filter(|contour| !contour.closed) {
contour.closed = true;
if let Some(&first) = contour.points.first() {
self.pen = first;
}
}
}
pub fn reset(&mut self) {
self.contours.clear();
self.pen = Point::ZERO;
}
pub fn contours(&self) -> &[PathContour] {
&self.contours
}
pub fn is_empty(&self) -> bool {
!self
.contours
.iter()
.any(|contour| contour.points.len() >= 2)
}
pub fn bounds(&self) -> Rect {
let mut points = self.contours.iter().flat_map(|contour| &contour.points);
let Some(&first) = points.next() else {
return Rect::EMPTY;
};
let (min, max) = points.fold((first, first), |(min, max), point| {
(
Point::new(min.x.min(point.x), min.y.min(point.y)),
Point::new(max.x.max(point.x), max.y.max(point.y)),
)
});
Rect {
x: min.x,
y: min.y,
width: max.x - min.x,
height: max.y - min.y,
}
}
pub fn to_vector_path(&self, fill_rule: PathFillRule) -> VectorPath {
VectorPath::from_subpaths(
self.contours
.iter()
.filter(|contour| contour.points.len() >= 2)
.map(|contour| contour.points.clone())
.collect(),
fill_rule,
)
}
fn open_contour(&mut self) -> &mut Vec<Point> {
if self.contours.last().is_none_or(|contour| contour.closed) {
self.contours.push(PathContour {
points: vec![self.pen],
closed: false,
});
}
let last = self.contours.len() - 1;
&mut self.contours[last].points
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct DashPathEffect {
intervals: [f32; DashPathEffect::MAX_INTERVALS],
count: usize,
phase: f32,
}
impl DashPathEffect {
pub const MAX_INTERVALS: usize = 8;
pub fn new(intervals: &[f32], phase: f32) -> Self {
let mut stored = [0.0; Self::MAX_INTERVALS];
let count = intervals.len().min(Self::MAX_INTERVALS);
stored[..count].copy_from_slice(&intervals[..count]);
Self {
intervals: stored,
count,
phase,
}
}
pub fn intervals(&self) -> &[f32] {
&self.intervals[..self.count]
}
fn period(&self) -> Option<f32> {
let period: f32 = self
.intervals()
.iter()
.map(|interval| interval.max(0.0))
.sum();
(self.count >= 2 && self.count.is_multiple_of(2) && period > 0.0 && period.is_finite())
.then_some(period)
}
pub fn for_each_dash(&self, points: &[Point], mut dash: impl FnMut(&[Point])) {
let Some(period) = self.period() else {
dash(points);
return;
};
let interval = |index: usize| self.intervals[index].max(0.0);
let mut current: Vec<Point> = Vec::new();
let mut into = self.phase.rem_euclid(period);
let mut index = 0;
while into >= interval(index) {
into -= interval(index);
index = (index + 1) % self.count;
}
for pair in points.windows(2) {
let (start, end) = (pair[0], pair[1]);
let length = ((end.x - start.x).powi(2) + (end.y - start.y).powi(2)).sqrt();
let at = |distance: f32| {
let t = if length > 0.0 { distance / length } else { 0.0 };
Point::new(
start.x + (end.x - start.x) * t,
start.y + (end.y - start.y) * t,
)
};
let mut walked = 0.0;
while walked < length {
let on = index.is_multiple_of(2);
let step = (interval(index) - into).min(length - walked);
if on {
if current.is_empty() {
current.push(at(walked));
}
current.push(at(walked + step));
}
walked += step;
into += step;
if into >= interval(index) {
into = 0.0;
if on && current.len() >= 2 {
dash(¤t);
}
current.clear();
index = (index + 1) % self.count;
}
}
}
if current.len() >= 2 {
dash(¤t);
}
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum DrawStyle {
Fill,
Stroke(Stroke),
DashedStroke(Stroke, DashPathEffect),
}
pub fn for_each_stroke_line(
points: &[Point],
closed: bool,
stroke: Stroke,
mut line: impl FnMut(LineGeometry),
) {
let corner = match stroke.join {
StrokeJoin::Miter => StrokeCap::Square,
StrokeJoin::Round | StrokeJoin::Bevel => StrokeCap::Round,
};
let closing = closed
.then(|| points.first().zip(points.last()))
.flatten()
.filter(|(first, last)| first != last)
.map(|(&first, &last)| (last, first));
let edges = points
.windows(2)
.map(|pair| (pair[0], pair[1]))
.chain(closing)
.filter(|(start, end)| start != end);
let count = edges.clone().count();
for (index, (start, end)) in edges.enumerate() {
let start_cap = if closed || index > 0 {
corner
} else {
stroke.cap
};
let end_cap = if closed || index + 1 < count {
corner
} else {
stroke.cap
};
let with_cap = |cap| Stroke { cap, ..stroke };
if start_cap == end_cap {
line(LineGeometry::new(start, end, with_cap(start_cap)));
continue;
}
let half_width = stroke.half_width();
let length = ((end.x - start.x).powi(2) + (end.y - start.y).powi(2)).sqrt();
let grow = |cap: StrokeCap| {
if cap == StrokeCap::Square {
half_width / length
} else {
0.0
}
};
let (back, ahead) = (grow(start_cap), grow(end_cap));
let (dx, dy) = (end.x - start.x, end.y - start.y);
line(LineGeometry::new(
Point::new(start.x - dx * back, start.y - dy * back),
Point::new(end.x + dx * ahead, end.y + dy * ahead),
with_cap(StrokeCap::Butt),
));
for (point, cap) in [(start, start_cap), (end, end_cap)] {
if cap == StrokeCap::Round {
line(LineGeometry::new(point, point, with_cap(StrokeCap::Round)));
}
}
}
}
#[cfg(test)]
#[path = "tests/path_tests.rs"]
mod tests;