pub mod adjust;
pub mod flatten;
use bitflags::bitflags;
#[derive(Copy, Clone, Debug, PartialEq)]
pub struct PathPoint {
pub x: f64,
pub y: f64,
}
impl PathPoint {
#[must_use]
pub const fn new(x: f64, y: f64) -> Self {
Self { x, y }
}
}
impl From<(f64, f64)> for PathPoint {
fn from((x, y): (f64, f64)) -> Self {
Self { x, y }
}
}
impl From<PathPoint> for (f64, f64) {
fn from(p: PathPoint) -> Self {
(p.x, p.y)
}
}
bitflags! {
#[derive(Copy, Clone, Debug, PartialEq, Eq, Default)]
pub struct PathFlags: u8 {
const FIRST = 0x01;
const LAST = 0x02;
const CLOSED = 0x04;
const CURVE = 0x08;
}
}
impl PathFlags {
#[must_use]
pub const fn is_first(self) -> bool {
self.contains(Self::FIRST)
}
#[must_use]
pub const fn is_last(self) -> bool {
self.contains(Self::LAST)
}
#[must_use]
pub const fn is_closed(self) -> bool {
self.contains(Self::CLOSED)
}
#[must_use]
pub const fn is_curve(self) -> bool {
self.contains(Self::CURVE)
}
}
#[derive(Copy, Clone, Debug)]
pub struct StrokeAdjustHint {
pub ctrl0: usize,
pub ctrl1: usize,
pub first_pt: usize,
pub last_pt: usize,
}
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum PathError {
NoCurPt,
BogusPath,
}
impl std::fmt::Display for PathError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::NoCurPt => f.write_str(
"path error: no current point \
(call move_to before line_to, curve_to, or close)",
),
Self::BogusPath => f.write_str(
"path error: consecutive moveTo without a drawing operator \
(a one-point subpath is already active)",
),
}
}
}
impl std::error::Error for PathError {}
#[derive(Clone, Debug, Default)]
pub struct Path {
pub pts: Vec<PathPoint>,
pub flags: Vec<PathFlags>,
pub hints: Vec<StrokeAdjustHint>,
pub cur_subpath: usize,
}
impl Path {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[inline]
#[must_use]
pub const fn no_current_point(&self) -> bool {
self.cur_subpath == self.pts.len()
}
#[inline]
#[must_use]
pub const fn one_point_subpath(&self) -> bool {
!self.pts.is_empty() && self.cur_subpath == self.pts.len() - 1
}
#[inline]
#[must_use]
pub const fn open_subpath(&self) -> bool {
!self.pts.is_empty() && self.cur_subpath < self.pts.len() - 1
}
#[must_use]
pub fn current_point(&self) -> Option<PathPoint> {
if self.no_current_point() {
None
} else {
self.pts.last().copied()
}
}
pub fn offset(&mut self, dx: f64, dy: f64) {
for p in &mut self.pts {
p.x += dx;
p.y += dy;
}
}
pub fn append(&mut self, other: &Self) {
debug_assert!(
other.cur_subpath <= other.pts.len(),
"append: other.cur_subpath ({}) exceeds other.pts.len() ({}); invariant broken",
other.cur_subpath,
other.pts.len()
);
let base = self.pts.len();
self.cur_subpath = base + other.cur_subpath;
self.pts.extend_from_slice(&other.pts);
self.flags.extend_from_slice(&other.flags);
for h in &other.hints {
self.hints.push(StrokeAdjustHint {
ctrl0: h.ctrl0 + base,
ctrl1: h.ctrl1 + base,
first_pt: h.first_pt + base,
last_pt: h.last_pt + base,
});
}
}
}
pub struct PathBuilder {
path: Path,
}
impl PathBuilder {
#[must_use]
pub fn new() -> Self {
Self { path: Path::new() }
}
pub fn move_to(&mut self, x: f64, y: f64) -> Result<(), PathError> {
if self.path.one_point_subpath() {
return Err(PathError::BogusPath);
}
let len = self.path.pts.len();
self.path.pts.push(PathPoint::new(x, y));
self.path.flags.push(PathFlags::FIRST | PathFlags::LAST);
self.path.cur_subpath = len;
Ok(())
}
pub fn line_to(&mut self, x: f64, y: f64) -> Result<(), PathError> {
if self.path.no_current_point() {
return Err(PathError::NoCurPt);
}
let last = self.path.flags.last_mut().unwrap();
last.remove(PathFlags::LAST);
self.path.pts.push(PathPoint::new(x, y));
self.path.flags.push(PathFlags::LAST);
Ok(())
}
pub fn curve_to(
&mut self,
x1: f64,
y1: f64,
x2: f64,
y2: f64,
x3: f64,
y3: f64,
) -> Result<(), PathError> {
if self.path.no_current_point() {
return Err(PathError::NoCurPt);
}
let last = self.path.flags.last_mut().unwrap();
last.remove(PathFlags::LAST);
self.path.pts.push(PathPoint::new(x1, y1));
self.path.flags.push(PathFlags::CURVE);
self.path.pts.push(PathPoint::new(x2, y2));
self.path.flags.push(PathFlags::CURVE);
self.path.pts.push(PathPoint::new(x3, y3));
self.path.flags.push(PathFlags::LAST);
Ok(())
}
pub fn close(&mut self, force: bool) -> Result<(), PathError> {
if self.path.no_current_point() {
return Err(PathError::NoCurPt);
}
let sp = self.path.cur_subpath;
let last_idx = self.path.pts.len() - 1;
let first = self.path.pts[sp];
let last = self.path.pts[last_idx];
if force || (sp != last_idx && first != last) {
self.line_to(first.x, first.y)?;
}
debug_assert_eq!(
self.path.pts.len(),
self.path.flags.len(),
"close: pts/flags length invariant violated"
);
let new_last = self.path.pts.len() - 1;
self.path.flags[sp].insert(PathFlags::CLOSED);
self.path.flags[new_last].insert(PathFlags::CLOSED);
self.path.cur_subpath = self.path.pts.len();
Ok(())
}
pub fn add_stroke_adjust_hint(
&mut self,
ctrl0: usize,
ctrl1: usize,
first_pt: usize,
last_pt: usize,
) {
self.path.hints.push(StrokeAdjustHint {
ctrl0,
ctrl1,
first_pt,
last_pt,
});
}
#[must_use]
pub fn cur_pt(&self) -> Option<PathPoint> {
self.path.current_point()
}
#[must_use]
pub const fn pts_len(&self) -> usize {
self.path.pts.len()
}
pub fn offset(&mut self, dx: f64, dy: f64) {
self.path.offset(dx, dy);
}
#[must_use]
pub fn build(self) -> Path {
self.path
}
}
impl Default for PathBuilder {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn initial_state() {
let p = Path::new();
assert!(p.no_current_point());
assert!(!p.one_point_subpath());
assert!(!p.open_subpath());
}
#[test]
fn move_to_gives_one_point() {
let mut b = PathBuilder::new();
b.move_to(1.0, 2.0).unwrap();
assert!(b.path.one_point_subpath());
assert!(!b.path.open_subpath());
}
#[test]
fn line_to_opens_subpath() {
let mut b = PathBuilder::new();
b.move_to(0.0, 0.0).unwrap();
b.line_to(10.0, 0.0).unwrap();
assert!(b.path.open_subpath());
assert_eq!(b.path.pts.len(), 2);
}
#[test]
fn curve_to_adds_three_points() {
let mut b = PathBuilder::new();
b.move_to(0.0, 0.0).unwrap();
b.curve_to(1.0, 2.0, 3.0, 4.0, 5.0, 0.0).unwrap();
assert_eq!(b.path.pts.len(), 4);
assert!(b.path.flags[1].is_curve());
assert!(b.path.flags[2].is_curve());
assert!(b.path.flags[3].is_last());
assert!(!b.path.flags[3].is_curve());
}
#[test]
fn close_sets_closed_flag() {
let mut b = PathBuilder::new();
b.move_to(0.0, 0.0).unwrap();
b.line_to(10.0, 0.0).unwrap();
b.line_to(5.0, 5.0).unwrap();
b.close(false).unwrap();
assert!(b.path.flags[0].is_closed());
assert!(b.path.flags.last().unwrap().is_closed());
assert!(b.path.no_current_point());
}
#[test]
fn after_close_current_point_is_none() {
let mut b = PathBuilder::new();
b.move_to(0.0, 0.0).unwrap();
b.line_to(1.0, 0.0).unwrap();
b.close(false).unwrap();
assert_eq!(b.path.current_point(), None);
}
#[test]
fn close_one_point_subpath_sets_closed_flag() {
let mut b = PathBuilder::new();
b.move_to(3.0, 4.0).unwrap();
assert!(b.path.one_point_subpath());
b.close(false).unwrap();
assert!(b.path.flags[0].is_closed());
assert!(b.path.no_current_point());
assert_eq!(b.path.pts.len(), 1);
}
#[test]
fn no_cur_pt_errors() {
let mut b = PathBuilder::new();
assert_eq!(b.line_to(1.0, 1.0), Err(PathError::NoCurPt));
assert_eq!(
b.curve_to(1.0, 1.0, 2.0, 2.0, 3.0, 3.0),
Err(PathError::NoCurPt)
);
assert_eq!(b.close(false), Err(PathError::NoCurPt));
}
#[test]
fn bogus_path_on_double_moveto() {
let mut b = PathBuilder::new();
b.move_to(0.0, 0.0).unwrap();
assert_eq!(b.move_to(1.0, 1.0), Err(PathError::BogusPath));
}
#[test]
fn path_error_display() {
let no_pt = PathError::NoCurPt.to_string();
assert!(
no_pt.contains("no current point"),
"NoCurPt display should mention 'no current point', got: {no_pt}"
);
let bogus = PathError::BogusPath.to_string();
assert!(
bogus.contains("consecutive moveTo"),
"BogusPath display should mention 'consecutive moveTo', got: {bogus}"
);
}
#[test]
#[expect(
clippy::float_cmp,
reason = "testing exact round-trip identity through From impls, not approximate equality"
)]
fn from_tuple_pathpoint() {
let p: PathPoint = (1.5_f64, 2.5_f64).into();
assert_eq!(p.x, 1.5);
assert_eq!(p.y, 2.5);
let t: (f64, f64) = p.into();
assert_eq!(t, (1.5, 2.5));
}
#[test]
fn path_flags_helpers() {
let f = PathFlags::FIRST | PathFlags::LAST | PathFlags::CLOSED | PathFlags::CURVE;
assert!(f.is_first());
assert!(f.is_last());
assert!(f.is_closed());
assert!(f.is_curve());
let empty = PathFlags::empty();
assert!(!empty.is_first());
assert!(!empty.is_last());
assert!(!empty.is_closed());
assert!(!empty.is_curve());
}
#[test]
fn append_adjusts_hints() {
let mut a = PathBuilder::new();
a.move_to(0.0, 0.0).unwrap();
a.line_to(1.0, 0.0).unwrap();
let pa = a.build();
let mut b_path = pa.clone();
let mut other = pa;
other.hints.push(StrokeAdjustHint {
ctrl0: 0,
ctrl1: 1,
first_pt: 0,
last_pt: 1,
});
b_path.append(&other);
assert_eq!(b_path.hints[0].ctrl0, 2);
assert_eq!(b_path.hints[0].first_pt, 2);
}
#[test]
fn append_empty_other_is_safe() {
let mut base = PathBuilder::new();
base.move_to(0.0, 0.0).unwrap();
base.line_to(1.0, 0.0).unwrap();
let mut p = base.build();
let original_len = p.pts.len();
p.append(&Path::new());
assert_eq!(p.pts.len(), original_len);
assert!(p.no_current_point());
}
}