use std::sync::{Arc, Mutex, OnceLock};
use kurbo::Shape as _;
use crate::{Pivot, Point, Rect, Transform, ValidationError, Vector2};
mod construction;
mod editing;
mod metrics;
mod morph;
#[cfg(test)]
mod tests;
mod validation;
pub use morph::MorphPath;
pub const MAX_PATH_VERBS: usize = 16_384;
pub const MAX_SVG_PATH_BYTES: usize = 1_048_576;
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum PathVerb {
MoveTo(Point),
LineTo(Point),
QuadraticTo(Point, Point),
CubicTo(Point, Point, Point),
Close,
}
impl PathVerb {
pub fn points(self) -> [Option<Point>; 3] {
match self {
Self::MoveTo(point) | Self::LineTo(point) => [Some(point), None, None],
Self::QuadraticTo(control, end) => [Some(control), Some(end), None],
Self::CubicTo(first, second, end) => [Some(first), Some(second), Some(end)],
Self::Close => [None, None, None],
}
}
}
#[derive(Clone, Debug, Default)]
pub struct Path2D {
data: Arc<PathData>,
}
type StrokeCache = Arc<Mutex<Vec<([u64; 4], PathData)>>>;
#[derive(Clone, Debug, Default)]
struct PathData {
error: Option<ValidationError>,
verbs: Arc<Vec<PathVerb>>,
hull: Option<Rect>,
current: Option<Point>,
start: Option<Point>,
bounds: OnceLock<Rect>,
fingerprint: OnceLock<u64>,
metrics: OnceLock<Result<Arc<metrics::Metrics>, ValidationError>>,
strokes: OnceLock<StrokeCache>,
}
impl PartialEq for Path2D {
fn eq(&self, other: &Self) -> bool {
self.verbs() == other.verbs() && self.data.error == other.data.error
}
}
impl Path2D {
pub fn new() -> Self {
Self::default()
}
pub fn verbs(&self) -> &[PathVerb] {
&self.data.verbs
}
pub fn bounding_box(&self) -> Rect {
*self.data.bounds.get_or_init(|| self.compute_bounds())
}
pub fn transformed(&self, transform: &Transform) -> Result<Self, ValidationError> {
self.validate()?;
if !transform.is_finite() {
return Err(invalid("transform", "Path transform must be finite"));
}
let mut result = Self::new();
for verb in self.verbs() {
let mapped = match *verb {
PathVerb::MoveTo(point) => PathVerb::MoveTo(transform.apply(point)),
PathVerb::LineTo(point) => PathVerb::LineTo(transform.apply(point)),
PathVerb::QuadraticTo(control, end) => {
PathVerb::QuadraticTo(transform.apply(control), transform.apply(end))
}
PathVerb::CubicTo(first, second, end) => PathVerb::CubicTo(
transform.apply(first),
transform.apply(second),
transform.apply(end),
),
PathVerb::Close => PathVerb::Close,
};
result.push(mapped)?;
}
Ok(result)
}
pub fn shared_verbs(&self) -> Arc<Vec<PathVerb>> {
self.data.verbs.clone()
}
pub fn axis_aligned_rectangle(&self) -> Option<Rect> {
if self.data.error.is_some() {
return None;
}
let [
PathVerb::MoveTo(first),
PathVerb::LineTo(second),
PathVerb::LineTo(third),
PathVerb::LineTo(fourth),
PathVerb::Close,
] = self.verbs()
else {
return None;
};
let horizontal = first.y == second.y
&& second.x == third.x
&& third.y == fourth.y
&& fourth.x == first.x;
let vertical = first.x == second.x
&& second.y == third.y
&& third.x == fourth.x
&& fourth.y == first.y;
if !(horizontal || vertical) || first.x == third.x || first.y == third.y {
return None;
}
Some(Rect::new(
first.x.min(third.x),
first.y.min(third.y),
first.x.max(third.x),
first.y.max(third.y),
))
}
pub fn fingerprint(&self) -> u64 {
*self.data.fingerprint.get_or_init(|| {
let mut value = 14695981039346656037_u64;
let mut write = |bytes: &[u8]| {
for byte in bytes {
value = (value ^ u64::from(*byte)).wrapping_mul(1099511628211);
}
};
write(&(self.verbs().len() as u64).to_le_bytes());
for verb in self.verbs() {
write(&[match verb {
PathVerb::MoveTo(_) => 0,
PathVerb::LineTo(_) => 1,
PathVerb::QuadraticTo(..) => 2,
PathVerb::CubicTo(..) => 3,
PathVerb::Close => 4,
}]);
for point in verb.points().into_iter().flatten() {
write(&point.x.to_bits().to_le_bytes());
write(&point.y.to_bits().to_le_bytes());
}
}
value
})
}
fn compute_bounds(&self) -> Rect {
let Some(hull) = self.data.hull else {
return Rect::default();
};
let normalize = |point: Point| {
(
if hull.width() == 0.0 {
0.0
} else {
(point.x - hull.left) / hull.width()
},
if hull.height() == 0.0 {
0.0
} else {
(point.y - hull.top) / hull.height()
},
)
};
let mut path = kurbo::BezPath::new();
for verb in self.verbs() {
match *verb {
PathVerb::MoveTo(point) => path.move_to(normalize(point)),
PathVerb::LineTo(point) => path.line_to(normalize(point)),
PathVerb::QuadraticTo(control, end) => {
path.quad_to(normalize(control), normalize(end))
}
PathVerb::CubicTo(first, second, end) => {
path.curve_to(normalize(first), normalize(second), normalize(end))
}
PathVerb::Close => path.close_path(),
}
}
let bounds = path.bounding_box();
if ![bounds.x0, bounds.y0, bounds.x1, bounds.y1]
.iter()
.all(|value| value.is_finite())
{
return hull;
}
let first = hull.anchor(Pivot::Relative(
bounds.x0.clamp(0.0, 1.0),
bounds.y0.clamp(0.0, 1.0),
));
let last = hull.anchor(Pivot::Relative(
bounds.x1.clamp(0.0, 1.0),
bounds.y1.clamp(0.0, 1.0),
));
let mut result = Rect::new(first.x, first.y, last.x, last.y);
for verb in self.verbs() {
if let PathVerb::MoveTo(point) = verb {
result = Rect::new(
result.left.min(point.x),
result.top.min(point.y),
result.right.max(point.x),
result.bottom.max(point.y),
);
}
}
result
}
pub(crate) fn cached_stroke(&self, key: [u64; 4]) -> Option<Path2D> {
let mut entries = self.data.strokes.get()?.lock().ok()?;
let index = entries.iter().position(|entry| entry.0 == key)?;
let entry = entries.remove(index);
let result = Self {
data: Arc::new(entry.1.clone()),
};
entries.push(entry);
Some(result)
}
pub(crate) fn retain_stroke(&self, key: [u64; 4], outline: &Path2D) {
let bytes = |data: &PathData| {
data.verbs.len() * std::mem::size_of::<PathVerb>()
+ std::mem::size_of::<([u64; 4], PathData)>()
};
let cost = bytes(&outline.data);
if cost > 1024 * 1024 {
return;
}
outline.bounding_box();
outline.fingerprint();
let mut snapshot = (*outline.data).clone();
snapshot.metrics.take();
snapshot.strokes.take();
let Ok(mut entries) = self
.data
.strokes
.get_or_init(|| Arc::new(Mutex::new(Vec::new())))
.lock()
else {
return;
};
if entries.iter().any(|entry| entry.0 == key) {
return;
}
while !entries.is_empty()
&& (entries.len() >= 4
|| entries.iter().map(|entry| bytes(&entry.1)).sum::<usize>() + cost > 1024 * 1024)
{
entries.remove(0);
}
if entries.try_reserve(1).is_ok() {
entries.push((key, snapshot));
}
}
}
fn invalid(field: &str, message: &str) -> ValidationError {
ValidationError::invalid("path2d", field, message)
}