pub mod iter;
use crate::bitmap::AaBuf;
use crate::types::{AA_SIZE, splash_floor};
use crate::xpath::{XPath, XPathFlags};
#[inline]
fn fp_from_f64(x: f64) -> i32 {
let fp = x * 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 as i32
}
}
}
#[derive(Copy, Clone, Debug)]
pub struct Intersect {
pub x0: i32,
pub x1: i32,
pub count: i32,
}
pub struct XPathScanner {
pub eo: bool,
pub x_min: i32,
pub x_max: i32,
pub y_min: i32,
pub y_max: i32,
row_start: Vec<u32>,
intersects: Vec<Intersect>,
}
impl XPathScanner {
#[must_use]
pub fn new(xpath: &XPath, eo: bool, clip_y_min: i32, clip_y_max: i32) -> Self {
if xpath.segs.is_empty() || clip_y_min > clip_y_max {
return Self::empty(eo);
}
let mut x_min_fp = f64::INFINITY;
let mut x_max_fp = f64::NEG_INFINITY;
let mut y_min_fp = f64::INFINITY;
let mut y_max_fp = f64::NEG_INFINITY;
for seg in &xpath.segs {
if seg.x0.is_nan() || seg.y0.is_nan() || seg.x1.is_nan() || seg.y1.is_nan() {
continue;
}
let sy_min = seg.y0.min(seg.y1);
let sy_max = seg.y0.max(seg.y1);
if sy_min >= f64::from(clip_y_max) + 1.0 || sy_max < f64::from(clip_y_min) {
continue;
}
x_min_fp = x_min_fp.min(seg.x0).min(seg.x1);
x_max_fp = x_max_fp.max(seg.x0).max(seg.x1);
y_min_fp = y_min_fp.min(sy_min);
y_max_fp = y_max_fp.max(sy_max);
}
if x_min_fp > x_max_fp {
return Self::empty(eo);
}
let x_min = splash_floor(x_min_fp);
let x_max = splash_floor(x_max_fp);
let y_min = splash_floor(y_min_fp).max(clip_y_min);
let y_max = splash_floor(y_max_fp).min(clip_y_max);
if y_min > y_max {
return Self::empty(eo);
}
let n_rows = usize::try_from(i64::from(y_max) - i64::from(y_min) + 1)
.expect("n_rows overflows usize; page height is impossibly large");
let mut counts = vec![0u32; n_rows];
count_intersections(xpath, y_min, y_max, &mut counts);
let mut alloc_start = Vec::with_capacity(n_rows + 1);
let mut total = 0u32;
for &c in &counts {
alloc_start.push(total);
total = total
.checked_add(c)
.expect("intersection count exceeds u32::MAX; path has too many segments");
}
alloc_start.push(total);
let total_n = total as usize;
let mut intersects = vec![
Intersect {
x0: 0,
x1: 0,
count: 0
};
total_n
];
counts.fill(0);
fill_intersections(
xpath,
y_min,
y_max,
&alloc_start,
&mut counts,
&mut intersects,
);
let mut row_start = Vec::with_capacity(n_rows + 1);
let mut write_pos = 0usize;
for i in 0..n_rows {
row_start.push(
u32::try_from(write_pos).expect("compacted intersection count exceeds u32::MAX"),
);
let read_base = alloc_start[i] as usize;
let n_written = counts[i] as usize;
intersects[read_base..read_base + n_written].sort_unstable_by_key(|ix| ix.x0);
intersects.copy_within(read_base..read_base + n_written, write_pos);
write_pos += n_written;
}
row_start
.push(u32::try_from(write_pos).expect("compacted intersection count exceeds u32::MAX"));
intersects.truncate(write_pos);
Self {
eo,
x_min,
x_max,
y_min,
y_max,
row_start,
intersects,
}
}
const fn empty(eo: bool) -> Self {
Self {
eo,
x_min: 1,
x_max: 0, y_min: 0,
y_max: -1,
row_start: Vec::new(),
intersects: Vec::new(),
}
}
#[must_use]
pub const fn is_empty(&self) -> bool {
self.x_min > self.x_max || self.y_min > self.y_max
}
pub fn nonempty_rows(&self) -> impl Iterator<Item = i32> + '_ {
self.row_start
.windows(2)
.enumerate()
.filter(|(_, w)| w[0] < w[1])
.map(|(i, _)| {
self.y_min
.saturating_add(i32::try_from(i).unwrap_or(i32::MAX))
})
}
#[must_use]
pub fn row(&self, y: i32) -> &[Intersect] {
if y < self.y_min || y > self.y_max {
return &[];
}
#[expect(
clippy::cast_sign_loss,
reason = "y >= y_min is asserted by the bounds check above; \
the difference is non-negative"
)]
let i = (y - self.y_min) as usize;
debug_assert!(i + 1 < self.row_start.len(), "row_start sentinel missing");
let s = self.row_start[i] as usize;
let e = self.row_start[i + 1] as usize;
debug_assert!(
s <= e && e <= self.intersects.len(),
"row_start invariant violated"
);
&self.intersects[s..e]
}
fn count_crossings(row: &[Intersect], x: i32) -> (i32, bool) {
let mut count = 0i32;
for int in row {
if int.x0 > x {
break;
}
if x <= int.x1 {
return (count, true);
}
count = count.wrapping_add(int.count);
}
(count, false)
}
#[must_use]
pub fn test(&self, x: i32, y: i32) -> bool {
let row = self.row(y);
let eo_mask = if self.eo { 1i32 } else { !0i32 };
let (count, on_span) = Self::count_crossings(row, x);
on_span || (count & eo_mask) != 0
}
#[must_use]
pub fn test_span(&self, x0: i32, x1: i32, y: i32) -> bool {
let row = self.row(y);
let eo_mask = if self.eo { 1i32 } else { !0i32 };
let mut count = 0i32;
let mut i = 0;
let mut xx1 = x0.saturating_sub(1);
while xx1 < x1 {
if i >= row.len() {
return false;
}
if row[i].x0 > xx1.saturating_add(1) && (count & eo_mask) == 0 {
return false;
}
if row[i].x1 > xx1 {
xx1 = row[i].x1;
}
count = count.wrapping_add(row[i].count);
i += 1;
}
true
}
pub fn render_aa_line(&self, aa_buf: &mut AaBuf, x0: &mut i32, x1: &mut i32, y: i32) {
aa_buf.clear();
let aa_width_i32 = i32::try_from(aa_buf.width).unwrap_or(i32::MAX);
let mut xx_min = aa_width_i32;
let mut xx_max = -1i32;
let eo_mask = if self.eo { 1i32 } else { !0i32 };
let aa = AA_SIZE;
for yy in 0..aa {
let scan_y = y * aa + yy;
let row = self.row(scan_y);
let mut count = 0i32;
let mut i = 0usize;
let mut xx = 0i32;
while i < row.len() || (count & eo_mask) != 0 {
if (count & eo_mask) == 0 {
if i >= row.len() {
break;
}
xx = row[i].x0;
count = count.wrapping_add(row[i].count);
i += 1;
continue;
}
let span_x1 = loop {
if i >= row.len() {
break aa_width_i32;
}
count = count.wrapping_add(row[i].count);
let end = row[i].x1 + 1;
i += 1;
if (count & eo_mask) == 0 {
break end;
}
};
let bx0 = usize::try_from(xx.max(0)).unwrap_or(0);
let bx1 = usize::try_from(span_x1.min(aa_width_i32)).unwrap_or(0);
if bx0 < bx1 {
let row_idx = usize::try_from(yy).expect("yy in 0..AA_SIZE, always >= 0");
aa_buf.set_span(row_idx, bx0, bx1);
let bx0_i32 = i32::try_from(bx0).unwrap_or(i32::MAX);
let bx1_i32 = i32::try_from(bx1).unwrap_or(i32::MAX);
if bx0_i32 < xx_min {
xx_min = bx0_i32;
}
if bx1_i32 > xx_max {
xx_max = bx1_i32;
}
}
xx = span_x1;
}
}
if xx_min > xx_max {
*x0 = 0;
*x1 = -1;
return;
}
*x0 = xx_min / aa;
*x1 = (xx_max - 1) / aa;
}
}
fn count_intersections(xpath: &XPath, y_min: i32, y_max: i32, counts: &mut [u32]) {
for seg in &xpath.segs {
if seg.x0.is_nan() || seg.y0.is_nan() {
continue;
}
let seg_y_min = seg.y0.min(seg.y1);
let seg_y_max = seg.y0.max(seg.y1);
let row_y0 = splash_floor(seg_y_min).max(y_min);
let row_y1 = splash_floor(seg_y_max).min(y_max);
if seg.flags.contains(XPathFlags::HORIZ) {
let row = splash_floor(seg_y_min);
if row >= y_min && row <= y_max {
let idx = usize::try_from(row - y_min).expect("row >= y_min");
counts[idx] = counts[idx].saturating_add(1);
}
} else {
for row in row_y0..=row_y1 {
let idx = usize::try_from(row - y_min).expect("row >= y_min");
counts[idx] = counts[idx].saturating_add(1);
}
}
}
}
fn fill_intersections(
xpath: &XPath,
y_min: i32,
y_max: i32,
row_start: &[u32],
cursors: &mut [u32],
intersects: &mut [Intersect],
) {
for seg in &xpath.segs {
if seg.x0.is_nan() || seg.y0.is_nan() {
continue;
}
let seg_y_min = seg.y0.min(seg.y1);
let seg_y_max = seg.y0.max(seg.y1);
let row_y0 = splash_floor(seg_y_min).max(y_min);
let row_y1 = splash_floor(seg_y_max).min(y_max);
if seg.flags.contains(XPathFlags::HORIZ) {
let row = splash_floor(seg_y_min);
if row >= y_min && row <= y_max {
let x0 = splash_floor(seg.x0.min(seg.x1));
let x1 = splash_floor(seg.x0.max(seg.x1));
write_intersect(row, y_min, row_start, cursors, intersects, x0, x1, 0);
}
} else if seg.flags.contains(XPathFlags::VERT) {
let count = if seg.flags.contains(XPathFlags::FLIPPED) {
1
} else {
-1
};
let x = splash_floor(seg.x0);
for row in row_y0..=row_y1 {
let c = if seg_y_min < f64::from(row) { count } else { 0 };
write_intersect(row, y_min, row_start, cursors, intersects, x, x, c);
}
} else {
let count = if seg.flags.contains(XPathFlags::FLIPPED) {
1
} else {
-1
};
let x_base = seg.y0.mul_add(-seg.dxdy, seg.x0);
let sloped_x_min = seg.x0.min(seg.x1);
let sloped_x_max = seg.x0.max(seg.x1);
let fp_clamp_min = fp_from_f64(sloped_x_min);
let fp_clamp_max = fp_from_f64(sloped_x_max);
let xx_init = if f64::from(row_y0) > seg.y0 {
f64::from(row_y0).mul_add(seg.dxdy, x_base)
} else {
seg.x0
};
let mut px0 = splash_floor(xx_init.clamp(sloped_x_min, sloped_x_max));
let mut xx1_fp = fp_from_f64((f64::from(row_y0) + 1.0).mul_add(seg.dxdy, x_base));
for row in row_y0..=row_y1 {
let xx1_fp_clamped = xx1_fp.clamp(fp_clamp_min, fp_clamp_max);
let px1 = xx1_fp_clamped >> 16;
let c = if seg_y_min < f64::from(row) { count } else { 0 };
write_intersect(row, y_min, row_start, cursors, intersects, px0, px1, c);
px0 = px1;
xx1_fp = xx1_fp.saturating_add(seg.dxdy_fp);
}
}
}
}
#[inline]
#[expect(
clippy::too_many_arguments,
reason = "private helper: all params are load-bearing"
)]
fn write_intersect(
row: i32,
y_min: i32,
row_start: &[u32],
cursors: &mut [u32],
intersects: &mut [Intersect],
x0: i32,
x1: i32,
count: i32,
) {
let (lo, hi) = (x0.min(x1), x0.max(x1));
let entry = Intersect {
x0: lo,
x1: hi,
count,
};
let idx = usize::try_from(row - y_min).expect("row >= y_min");
let base = row_start[idx] as usize;
let cur = cursors[idx] as usize;
if cur > 0 {
let last = &mut intersects[base + cur - 1];
if last.x1.saturating_add(1) >= entry.x0 && last.x0 <= entry.x1.saturating_add(1) {
last.count = last.count.wrapping_add(entry.count);
last.x0 = last.x0.min(entry.x0);
last.x1 = last.x1.max(entry.x1);
return;
}
}
debug_assert!(
base + cur < intersects.len(),
"write_intersect: slot out of range (row={row}, base={base}, cur={cur}, len={})",
intersects.len()
);
intersects[base + cur] = entry;
cursors[idx] += 1;
}
#[cfg(test)]
fn add_intersection(row: &mut Vec<Intersect>, x0: i32, x1: i32, count: i32, _is_horiz: bool) {
let (lo, hi) = (x0.min(x1), x0.max(x1));
let entry = Intersect {
x0: lo,
x1: hi,
count,
};
if row.is_empty() {
row.push(entry);
return;
}
let last = row.last_mut().unwrap();
if last.x1.saturating_add(1) < entry.x0 || last.x0 > entry.x1.saturating_add(1) {
row.push(entry);
} else {
last.count = last.count.wrapping_add(entry.count);
last.x0 = last.x0.min(entry.x0);
last.x1 = last.x1.max(entry.x1);
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::path::PathBuilder;
use crate::xpath::XPath;
fn identity() -> [f64; 6] {
[1.0, 0.0, 0.0, 1.0, 0.0, 0.0]
}
fn triangle_xpath() -> XPath {
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();
XPath::new(&b.build(), &identity(), 1.0, true)
}
#[test]
fn empty_on_no_segs() {
let xpath = XPath::empty();
let scanner = XPathScanner::new(&xpath, false, 0, 10);
assert!(scanner.is_empty());
}
#[test]
fn triangle_has_rows() {
let xpath = triangle_xpath();
let scanner = XPathScanner::new(&xpath, false, 0, 4);
assert!(!scanner.is_empty());
assert!(scanner.y_min <= scanner.y_max);
}
#[test]
fn horizontal_segment_count_zero() {
let mut row = Vec::new();
add_intersection(&mut row, 0, 5, 0, true);
assert_eq!(row[0].count, 0);
}
#[test]
fn test_inside_triangle() {
let xpath = triangle_xpath();
let scanner = XPathScanner::new(&xpath, false, 0, 4);
assert!(scanner.test(2, 1));
}
#[test]
fn test_outside_triangle() {
let xpath = triangle_xpath();
let scanner = XPathScanner::new(&xpath, false, 0, 4);
assert!(!scanner.test(10, 2));
assert!(!scanner.test(2, 5));
}
#[test]
fn row_out_of_range_returns_empty() {
let xpath = triangle_xpath();
let scanner = XPathScanner::new(&xpath, false, 0, 4);
assert!(scanner.row(scanner.y_min - 1).is_empty());
assert!(scanner.row(scanner.y_max + 1).is_empty());
assert!(scanner.row(i32::MIN).is_empty());
assert!(scanner.row(i32::MAX).is_empty());
}
#[test]
fn add_intersection_merge_adjacent() {
let mut row = Vec::new();
add_intersection(&mut row, 0, 3, -1, false);
add_intersection(&mut row, 4, 7, 1, false);
assert_eq!(row.len(), 1, "adjacent spans must merge");
assert_eq!(row[0].x0, 0);
assert_eq!(row[0].x1, 7);
assert_eq!(row[0].count, 0); }
#[test]
fn add_intersection_disjoint() {
let mut row = Vec::new();
add_intersection(&mut row, 0, 2, -1, false);
add_intersection(&mut row, 5, 7, -1, false);
assert_eq!(row.len(), 2, "disjoint spans must not merge");
}
#[test]
fn count_crossings_inside_span() {
let row = vec![Intersect {
x0: 2,
x1: 5,
count: -1,
}];
let (count, on_span) = XPathScanner::count_crossings(&row, 3);
assert!(on_span);
assert_eq!(count, 0); }
#[test]
fn count_crossings_after_span() {
let row = vec![Intersect {
x0: 2,
x1: 5,
count: -1,
}];
let (count, on_span) = XPathScanner::count_crossings(&row, 6);
assert!(!on_span);
assert_eq!(count, -1);
}
}