use crate::types::Line3D;
#[cfg(not(target_arch = "wasm32"))]
use multiversion::multiversion;
use wide::f64x4;
use wide::CmpGe;
use wide::CmpLe;
pub struct SoALines {
pub min_x: Vec<f64>,
pub min_y: Vec<f64>,
pub max_x: Vec<f64>,
pub max_y: Vec<f64>,
}
impl SoALines {
pub fn new(lines: &[Line3D]) -> Self {
let len = lines.len();
let mut min_x = Vec::with_capacity(len + 3);
let mut min_y = Vec::with_capacity(len + 3);
let mut max_x = Vec::with_capacity(len + 3);
let mut max_y = Vec::with_capacity(len + 3);
for line in lines {
min_x.push(line.start.x.min(line.end.x));
min_y.push(line.start.y.min(line.end.y));
max_x.push(line.start.x.max(line.end.x));
max_y.push(line.start.y.max(line.end.y));
}
while min_x.len() % 4 != 0 {
min_x.push(f64::NAN);
min_y.push(f64::NAN);
max_x.push(f64::NAN);
max_y.push(f64::NAN);
}
Self {
min_x,
min_y,
max_x,
max_y,
}
}
pub fn len(&self) -> usize {
self.min_x.len()
}
pub fn is_empty(&self) -> bool {
self.min_x.is_empty()
}
#[inline]
pub fn intersects_bbox_batch(&self, query: Line3D, index: usize) -> u8 {
let q_min_x_val = query.start.x.min(query.end.x);
let q_max_x_val = query.start.x.max(query.end.x);
let q_min_y_val = query.start.y.min(query.end.y);
let q_max_y_val = query.start.y.max(query.end.y);
let q_min_x = f64x4::splat(q_min_x_val);
let q_max_x = f64x4::splat(q_max_x_val);
let q_min_y = f64x4::splat(q_min_y_val);
let q_max_y = f64x4::splat(q_max_y_val);
self.intersects_bbox_batch_splatted(q_min_x, q_max_x, q_min_y, q_max_y, index)
}
#[inline]
pub fn intersects_bbox_batch_splatted(
&self,
q_min_x: f64x4,
q_max_x: f64x4,
q_min_y: f64x4,
q_max_y: f64x4,
index: usize,
) -> u8 {
intersects_bbox_batch_splatted_impl(
&self.min_x,
&self.min_y,
&self.max_x,
&self.max_y,
q_min_x,
q_max_x,
q_min_y,
q_max_y,
index,
)
}
}
#[allow(clippy::too_many_arguments)]
#[cfg_attr(
not(target_arch = "wasm32"),
multiversion(targets(
"x86_64+avx512f+avx512dq",
"x86_64+avx2",
"x86+avx2",
"x86_64+avx",
"x86+avx",
"x86_64+sse2",
"x86+sse2",
))
)]
fn intersects_bbox_batch_splatted_impl(
min_x: &[f64],
min_y: &[f64],
max_x: &[f64],
max_y: &[f64],
q_min_x: f64x4,
q_max_x: f64x4,
q_min_y: f64x4,
q_max_y: f64x4,
index: usize,
) -> u8 {
let t_min_x = f64x4::from(&min_x[index..index + 4]);
let t_min_y = f64x4::from(&min_y[index..index + 4]);
let t_max_x = f64x4::from(&max_x[index..index + 4]);
let t_max_y = f64x4::from(&max_y[index..index + 4]);
let overlap_x = q_min_x.cmp_le(t_max_x) & q_max_x.cmp_ge(t_min_x);
let overlap_y = q_min_y.cmp_le(t_max_y) & q_max_y.cmp_ge(t_min_y);
let overlap = overlap_x & overlap_y;
overlap.move_mask() as u8
}
#[cfg(test)]
mod tests {
use super::*;
use crate::types::{Coord3D, Line3D};
fn make_line(x1: f64, y1: f64, x2: f64, y2: f64) -> Line3D {
Line3D::new(Coord3D::new(x1, y1, 0.0), Coord3D::new(x2, y2, 0.0), 0)
}
#[test]
fn test_soa_bbox_batch_simd() {
let query = make_line(0.0, 0.0, 10.0, 10.0);
let lines = vec![
make_line(1.0, 1.0, 2.0, 2.0),
make_line(12.0, 0.0, 14.0, 10.0),
make_line(10.0, 5.0, 15.0, 5.0),
make_line(0.0, 10.0, 10.0, 0.0),
];
let soa = SoALines::new(&lines);
let mask = soa.intersects_bbox_batch(query, 0);
assert_eq!(mask, 0b1101, "Mask should match expected intersections");
}
#[test]
fn test_soa_padding_safety() {
let query = make_line(0.0, 0.0, 10.0, 10.0);
let lines = vec![make_line(1.0, 1.0, 2.0, 2.0)];
let soa = SoALines::new(&lines);
assert!(soa.min_x.len() >= 4);
let mask = soa.intersects_bbox_batch(query, 0);
assert_eq!(mask, 1, "Padding slots should never return true");
}
#[test]
fn test_empty_soa() {
let lines: Vec<Line3D> = vec![];
let soa = SoALines::new(&lines);
assert_eq!(soa.min_x.len(), 0);
}
#[test]
fn test_crossing_scenario() {
let lines = vec![
make_line(0., 0., 10., 0.), make_line(10., 0., 10., 10.), make_line(10., 10., 0., 10.), make_line(0., 10., 0., 0.), make_line(0., 0., 10., 10.), make_line(0., 10., 10., 0.), ];
let soa = SoALines::new(&lines);
let mask = soa.intersects_bbox_batch(lines[4], 4);
assert_eq!(mask & 2, 2, "Line 4 should intersect Line 5 (bit 1)");
}
}