use crate::path::adjust::{XPathAdjust, stroke_adjust};
use crate::path::flatten::{CurveData, flatten_curve};
use crate::path::{Path, PathFlags, PathPoint, StrokeAdjustHint};
use crate::types::AA_SIZE;
use bitflags::bitflags;
bitflags! {
#[derive(Copy, Clone, Debug, PartialEq, Eq, Default)]
pub struct XPathFlags: u32 {
const HORIZ = 0x01;
const VERT = 0x02;
const FLIPPED = 0x04;
}
}
#[derive(Clone, Debug)]
pub struct XPathSeg {
pub x0: f64,
pub y0: f64,
pub x1: f64,
pub y1: f64,
pub dxdy: f64,
pub dxdy_fp: i32,
pub flags: XPathFlags,
}
pub struct XPath {
pub segs: Vec<XPathSeg>,
curve_data: Option<Box<CurveData>>,
}
impl XPath {
#[cfg(test)]
pub(crate) const fn empty() -> Self {
Self {
segs: Vec::new(),
curve_data: None,
}
}
#[must_use]
pub fn new(path: &Path, matrix: &[f64; 6], flatness: f64, close_subpaths: bool) -> Self {
let flatness_sq = flatness * flatness;
let mut xpath = Self {
segs: Vec::new(),
curve_data: None,
};
let tpts: Vec<PathPoint> = path
.pts
.iter()
.map(|p| transform(matrix, p.x, p.y))
.collect();
let adjusts = build_adjusts(&path.hints, &tpts, false, 0);
let mut tpts = tpts;
for adj in &adjusts {
debug_assert!(
adj.last_pt < tpts.len(),
"adj.last_pt ({}) out of bounds (tpts.len() = {})",
adj.last_pt,
tpts.len()
);
for pt in &mut tpts[adj.first_pt..=adj.last_pt] {
let (x, y) = (&mut pt.x, &mut pt.y);
stroke_adjust(adj, x, y);
}
}
let n = path.pts.len();
let mut i = 0usize;
while i < n {
if path.flags[i].contains(PathFlags::FIRST) {
let sp_x = tpts[i].x;
let sp_y = tpts[i].y;
let mut cur_x = sp_x;
let mut cur_y = sp_y;
i += 1;
while i < n {
if path.flags[i].contains(PathFlags::CURVE) {
if i + 2 >= n {
break;
}
let p0 = PathPoint::new(cur_x, cur_y);
let p1 = tpts[i];
let p2 = tpts[i + 1];
let p3 = tpts[i + 2];
let mut flat_pts = Vec::new();
flatten_curve(
p0,
p1,
p2,
p3,
flatness_sq,
&mut flat_pts,
&mut xpath.curve_data,
);
for fp in &flat_pts {
xpath.add_segment(cur_x, cur_y, fp.x, fp.y);
cur_x = fp.x;
cur_y = fp.y;
}
i += 3;
} else {
let nx = tpts[i].x;
let ny = tpts[i].y;
xpath.add_segment(cur_x, cur_y, nx, ny);
cur_x = nx;
cur_y = ny;
let is_last = path.flags[i].contains(PathFlags::LAST);
i += 1;
if is_last {
break;
}
}
}
if close_subpaths && ((cur_x - sp_x).abs() > 1e-10 || (cur_y - sp_y).abs() > 1e-10)
{
xpath.add_segment(cur_x, cur_y, sp_x, sp_y);
}
} else {
i += 1;
}
}
xpath
}
pub fn aa_scale(&mut self) {
let s = f64::from(AA_SIZE);
for seg in &mut self.segs {
debug_assert!(
seg.x0.is_finite()
&& seg.y0.is_finite()
&& seg.x1.is_finite()
&& seg.y1.is_finite(),
"aa_scale: segment coordinates must be finite before scaling \
(x0={}, y0={}, x1={}, y1={})",
seg.x0,
seg.y0,
seg.x1,
seg.y1,
);
seg.x0 *= s;
seg.y0 *= s;
seg.x1 *= s;
seg.y1 *= s;
}
}
fn add_segment(&mut self, mut x0: f64, mut y0: f64, mut x1: f64, mut y1: f64) {
let mut flags = XPathFlags::empty();
if y0.to_bits() == y1.to_bits() {
flags.insert(XPathFlags::HORIZ);
if x0.to_bits() == x1.to_bits() {
flags.insert(XPathFlags::VERT);
}
self.segs.push(XPathSeg {
x0,
y0,
x1,
y1,
dxdy: 0.0,
dxdy_fp: 0,
flags,
});
return; }
if x0.to_bits() == x1.to_bits() {
flags.insert(XPathFlags::VERT);
}
let dxdy = if flags.contains(XPathFlags::VERT) {
0.0
} else {
debug_assert_ne!(
y1.to_bits(),
y0.to_bits(),
"add_segment: y0 == y1 must be caught by the HORIZ branch"
);
(x1 - x0) / (y1 - y0)
};
if y0 > y1 {
std::mem::swap(&mut x0, &mut x1);
std::mem::swap(&mut y0, &mut y1);
flags.insert(XPathFlags::FLIPPED);
}
let dxdy_fp = {
let fp = dxdy * 65536.0;
if fp >= f64::from(i32::MAX) {
i32::MAX
} else if fp <= f64::from(i32::MIN) {
i32::MIN
} else {
#[expect(
clippy::cast_possible_truncation,
reason = "fp is bounds-checked to [i32::MIN, i32::MAX] by the branches above"
)]
{
fp.round() as i32
}
}
};
self.segs.push(XPathSeg {
x0,
y0,
x1,
y1,
dxdy,
dxdy_fp,
flags,
});
}
}
#[inline]
#[must_use]
pub const fn transform(m: &[f64; 6], xi: f64, yi: f64) -> PathPoint {
PathPoint::new(
xi.mul_add(m[0], yi.mul_add(m[2], m[4])),
xi.mul_add(m[1], yi.mul_add(m[3], m[5])),
)
}
fn build_adjusts(
hints: &[StrokeAdjustHint],
tpts: &[PathPoint],
adjust_lines: bool,
line_pos_i: i32,
) -> Vec<XPathAdjust> {
debug_assert!(
!adjust_lines && line_pos_i == 0,
"build_adjusts: only the no-op configuration (false, 0) is reachable today; \
got adjust_lines={adjust_lines} line_pos_i={line_pos_i}"
);
let mut adjusts = Vec::with_capacity(hints.len());
for h in hints {
if h.ctrl0 + 1 >= tpts.len() || h.ctrl1 + 1 >= tpts.len() {
continue;
}
let p00 = tpts[h.ctrl0];
let p01 = tpts[h.ctrl0 + 1];
let p10 = tpts[h.ctrl1];
let p11 = tpts[h.ctrl1 + 1];
let vert = (p00.x.to_bits() == p01.x.to_bits()) && (p10.x.to_bits() == p11.x.to_bits());
let horiz = (p00.y.to_bits() == p01.y.to_bits()) && (p10.y.to_bits() == p11.y.to_bits());
if !vert && !horiz {
continue;
}
let (a0, a1) = if vert {
(p00.x.min(p10.x), p00.x.max(p10.x))
} else {
(p00.y.min(p10.y), p00.y.max(p10.y))
};
adjusts.push(XPathAdjust::new(
h.first_pt,
h.last_pt,
vert,
a0,
a1,
adjust_lines,
line_pos_i,
));
}
adjusts
}
#[cfg(test)]
mod tests {
use super::*;
use crate::path::PathBuilder;
fn identity() -> [f64; 6] {
[1.0, 0.0, 0.0, 1.0, 0.0, 0.0]
}
#[test]
fn horizontal_segment_not_flipped() {
let mut xpath = XPath {
segs: Vec::new(),
curve_data: None,
};
xpath.add_segment(0.0, 5.0, 10.0, 5.0);
let s = &xpath.segs[0];
assert!(s.flags.contains(XPathFlags::HORIZ));
assert!(!s.flags.contains(XPathFlags::FLIPPED));
assert!((s.y0 - 5.0).abs() < f64::EPSILON);
assert!((s.y1 - 5.0).abs() < f64::EPSILON);
}
#[test]
fn downward_segment_flipped() {
let mut xpath = XPath {
segs: Vec::new(),
curve_data: None,
};
xpath.add_segment(0.0, 10.0, 0.0, 0.0); let s = &xpath.segs[0];
assert!(s.flags.contains(XPathFlags::FLIPPED));
assert!(s.y0 <= s.y1, "y0={} y1={}", s.y0, s.y1);
}
#[test]
fn aa_scale_multiplies_coords() {
let mut xpath = XPath {
segs: Vec::new(),
curve_data: None,
};
xpath.add_segment(1.0, 0.0, 3.0, 2.0);
let orig_dxdy = xpath.segs[0].dxdy;
xpath.aa_scale();
let s = &xpath.segs[0];
assert!((s.x0 - 4.0).abs() < 1e-10);
assert!((s.y0 - 0.0).abs() < 1e-10);
assert!((s.x1 - 12.0).abs() < 1e-10);
assert!((s.y1 - 8.0).abs() < 1e-10);
assert!(
(s.dxdy - orig_dxdy).abs() < 1e-10,
"dxdy should be unchanged"
);
}
#[test]
fn vertical_segment_dxdy_zero() {
let mut xpath = XPath {
segs: Vec::new(),
curve_data: None,
};
xpath.add_segment(5.0, 0.0, 5.0, 10.0);
let s = &xpath.segs[0];
assert!(s.flags.contains(XPathFlags::VERT));
assert!(s.dxdy.abs() < f64::EPSILON);
}
#[test]
fn triangle_from_path() {
let mut b = PathBuilder::new();
b.move_to(0.0, 0.0).unwrap();
b.line_to(4.0, 0.0).unwrap();
b.line_to(2.0, 4.0).unwrap();
b.close(false).unwrap();
let path = b.build();
let xpath = XPath::new(&path, &identity(), 1.0, false);
assert_eq!(xpath.segs.len(), 3);
}
#[test]
fn degenerate_point_segment() {
let mut xpath = XPath {
segs: Vec::new(),
curve_data: None,
};
xpath.add_segment(3.0, 7.0, 3.0, 7.0);
let s = &xpath.segs[0];
assert!(s.flags.contains(XPathFlags::HORIZ));
assert!(s.flags.contains(XPathFlags::VERT));
assert!(!s.flags.contains(XPathFlags::FLIPPED));
assert_eq!(s.dxdy.to_bits(), 0.0_f64.to_bits());
}
#[test]
fn sloped_segment_dxdy() {
let mut xpath = XPath {
segs: Vec::new(),
curve_data: None,
};
xpath.add_segment(2.0, 1.0, 6.0, 5.0);
let s = &xpath.segs[0];
assert!(!s.flags.contains(XPathFlags::HORIZ));
assert!(!s.flags.contains(XPathFlags::VERT));
assert!(!s.flags.contains(XPathFlags::FLIPPED));
assert!((s.dxdy - 1.0).abs() < f64::EPSILON, "dxdy={}", s.dxdy);
}
#[test]
fn flipped_sloped_segment_dxdy_consistent() {
let mut xpath = XPath {
segs: Vec::new(),
curve_data: None,
};
xpath.add_segment(6.0, 5.0, 2.0, 1.0);
let s = &xpath.segs[0];
assert!(s.flags.contains(XPathFlags::FLIPPED));
assert!(
s.y0 <= s.y1,
"y0 ≤ y1 invariant violated: y0={} y1={}",
s.y0,
s.y1
);
assert!((s.dxdy - 1.0).abs() < f64::EPSILON, "dxdy={}", s.dxdy);
}
#[test]
fn dxdy_fp_matches_dxdy_for_slope_one() {
let mut xpath = XPath {
segs: Vec::new(),
curve_data: None,
};
xpath.add_segment(0.0, 0.0, 4.0, 4.0);
let s = &xpath.segs[0];
assert!((s.dxdy - 1.0).abs() < f64::EPSILON, "dxdy={}", s.dxdy);
assert_eq!(s.dxdy_fp, 65536, "dxdy_fp={}", s.dxdy_fp);
}
#[test]
fn dxdy_fp_matches_dxdy_for_half_slope() {
let mut xpath = XPath {
segs: Vec::new(),
curve_data: None,
};
xpath.add_segment(0.0, 0.0, 2.0, 4.0);
let s = &xpath.segs[0];
assert!((s.dxdy - 0.5).abs() < f64::EPSILON, "dxdy={}", s.dxdy);
assert_eq!(s.dxdy_fp, 32768, "dxdy_fp={}", s.dxdy_fp);
}
#[test]
fn dxdy_fp_zero_for_horizontal() {
let mut xpath = XPath {
segs: Vec::new(),
curve_data: None,
};
xpath.add_segment(0.0, 2.0, 5.0, 2.0);
let s = &xpath.segs[0];
assert!(s.flags.contains(XPathFlags::HORIZ));
assert_eq!(s.dxdy_fp, 0);
}
#[test]
fn dxdy_fp_zero_for_vertical() {
let mut xpath = XPath {
segs: Vec::new(),
curve_data: None,
};
xpath.add_segment(3.0, 0.0, 3.0, 5.0);
let s = &xpath.segs[0];
assert!(s.flags.contains(XPathFlags::VERT));
assert_eq!(s.dxdy_fp, 0);
}
}