use crate::error::{Error, Result};
use crate::profile::Profile2D;
use i_overlay::core::fill_rule::FillRule;
use i_overlay::core::overlay_rule::OverlayRule;
use i_overlay::float::single::SingleFloatOverlay;
use nalgebra::Point2;
#[cfg(test)]
const EPSILON_2D: f64 = 1e-9;
const MIN_AREA_THRESHOLD: f64 = 1e-10;
pub fn subtract_2d(profile: &Profile2D, void_contour: &[Point2<f64>]) -> Result<Profile2D> {
if void_contour.len() < 3 {
return Err(Error::InvalidProfile(
"Void contour must have at least 3 vertices".to_string(),
));
}
if profile.outer.len() < 3 {
return Err(Error::InvalidProfile(
"Profile must have at least 3 vertices".to_string(),
));
}
let subject = profile_to_paths(profile);
let clip = vec![contour_to_path(void_contour)];
let result = subject.overlay(&clip, OverlayRule::Difference, FillRule::EvenOdd);
shapes_to_profile(&result)
}
pub fn subtract_multiple_2d(
profile: &Profile2D,
void_contours: &[Vec<Point2<f64>>],
) -> Result<Profile2D> {
if void_contours.is_empty() {
return Ok(profile.clone());
}
let valid_contours: Vec<_> = void_contours.iter().filter(|c| c.len() >= 3).collect();
if valid_contours.is_empty() {
return Ok(profile.clone());
}
let subject = profile_to_paths(profile);
let clip: Vec<Vec<[f64; 2]>> = valid_contours.iter().map(|c| contour_to_path(c)).collect();
let result = subject.overlay(&clip, OverlayRule::Difference, FillRule::EvenOdd);
shapes_to_profile(&result)
}
pub fn subtract_multiple_2d_counted(
profile: &Profile2D,
void_contours: &[Vec<Point2<f64>>],
) -> Result<(Profile2D, usize)> {
let valid_contours: Vec<_> = void_contours.iter().filter(|c| c.len() >= 3).collect();
if valid_contours.is_empty() {
return Ok((profile.clone(), 1));
}
let subject = profile_to_paths(profile);
let clip: Vec<Vec<[f64; 2]>> = valid_contours.iter().map(|c| contour_to_path(c)).collect();
let result = subject.overlay(&clip, OverlayRule::Difference, FillRule::EvenOdd);
let shapes = result
.iter()
.filter(|s| s.first().is_some_and(|outer| outer.len() >= 3))
.count();
Ok((shapes_to_profile(&result)?, shapes))
}
pub fn union_contours_to_shapes(contours: &[Vec<Point2<f64>>]) -> Vec<Profile2D> {
let subject: Vec<Vec<[f64; 2]>> = contours
.iter()
.filter(|c| c.len() >= 3)
.map(|c| contour_to_path(&ensure_ccw(c)))
.collect();
if subject.is_empty() {
return Vec::new();
}
let empty: Vec<Vec<[f64; 2]>> = Vec::new();
let result = subject.overlay(&empty, OverlayRule::Union, FillRule::NonZero);
let mut out = Vec::new();
for shape in &result {
let Some(outer_raw) = shape.first() else {
continue;
};
let outer: Vec<Point2<f64>> = outer_raw.iter().map(|p| Point2::new(p[0], p[1])).collect();
if !is_valid_contour(&outer) {
continue;
}
let outer = ensure_ccw(&outer);
let mut holes = Vec::new();
for c in shape.iter().skip(1) {
let hole: Vec<Point2<f64>> = c.iter().map(|p| Point2::new(p[0], p[1])).collect();
if is_valid_contour(&hole) {
holes.push(ensure_cw(&hole));
}
}
out.push(Profile2D { outer, holes });
}
out
}
pub fn is_valid_contour(contour: &[Point2<f64>]) -> bool {
if contour.len() < 3 {
return false;
}
let area = compute_signed_area(contour).abs();
area > MIN_AREA_THRESHOLD
}
pub fn compute_signed_area(contour: &[Point2<f64>]) -> f64 {
if contour.len() < 3 {
return 0.0;
}
let mut area = 0.0;
let n = contour.len();
for i in 0..n {
let j = (i + 1) % n;
area += contour[i].x * contour[j].y;
area -= contour[j].x * contour[i].y;
}
area * 0.5
}
pub fn ensure_ccw(contour: &[Point2<f64>]) -> Vec<Point2<f64>> {
let area = compute_signed_area(contour);
if area < 0.0 {
contour.iter().rev().cloned().collect()
} else {
contour.to_vec()
}
}
pub fn ensure_cw(contour: &[Point2<f64>]) -> Vec<Point2<f64>> {
let area = compute_signed_area(contour);
if area > 0.0 {
contour.iter().rev().cloned().collect()
} else {
contour.to_vec()
}
}
pub fn point_in_contour(point: &Point2<f64>, contour: &[Point2<f64>]) -> bool {
if contour.len() < 3 {
return false;
}
let mut inside = false;
let n = contour.len();
let mut j = n - 1;
for i in 0..n {
let pi = &contour[i];
let pj = &contour[j];
if ((pi.y > point.y) != (pj.y > point.y))
&& (point.x < (pj.x - pi.x) * (point.y - pi.y) / (pj.y - pi.y) + pi.x)
{
inside = !inside;
}
j = i;
}
inside
}
fn profile_to_paths(profile: &Profile2D) -> Vec<Vec<[f64; 2]>> {
let mut paths = Vec::with_capacity(1 + profile.holes.len());
let outer = ensure_ccw(&profile.outer);
paths.push(contour_to_path(&outer));
for hole in &profile.holes {
let hole_cw = ensure_cw(hole);
paths.push(contour_to_path(&hole_cw));
}
paths
}
fn contour_to_path(contour: &[Point2<f64>]) -> Vec<[f64; 2]> {
contour.iter().map(|p| [p.x, p.y]).collect()
}
fn shapes_to_profile(shapes: &[Vec<Vec<[f64; 2]>>]) -> Result<Profile2D> {
if shapes.is_empty() {
return Err(Error::InvalidProfile(
"Boolean operation resulted in empty geometry".to_string(),
));
}
let mut best_shape_idx = 0;
let mut largest_area = 0.0f64;
for (idx, shape) in shapes.iter().enumerate() {
if shape.is_empty() {
continue;
}
let outer_contour: Vec<Point2<f64>> =
shape[0].iter().map(|p| Point2::new(p[0], p[1])).collect();
let area = compute_signed_area(&outer_contour).abs();
if area > largest_area {
largest_area = area;
best_shape_idx = idx;
}
}
let best_shape = &shapes[best_shape_idx];
if best_shape.is_empty() {
return Err(Error::InvalidProfile(
"Selected shape has no contours".to_string(),
));
}
let outer: Vec<Point2<f64>> = best_shape[0]
.iter()
.map(|p| Point2::new(p[0], p[1]))
.collect();
let outer = ensure_ccw(&outer);
let mut holes = Vec::new();
for contour in best_shape.iter().skip(1) {
let hole: Vec<Point2<f64>> = contour.iter().map(|p| Point2::new(p[0], p[1])).collect();
if is_valid_contour(&hole) {
holes.push(ensure_cw(&hole));
}
}
Ok(Profile2D { outer, holes })
}
#[cfg(test)]
#[path = "bool2d_tests.rs"]
mod tests;