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 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)]
mod tests {
use super::*;
#[test]
fn test_compute_signed_area_ccw() {
let contour = vec![
Point2::new(0.0, 0.0),
Point2::new(1.0, 0.0),
Point2::new(1.0, 1.0),
Point2::new(0.0, 1.0),
];
let area = compute_signed_area(&contour);
assert!((area - 1.0).abs() < EPSILON_2D);
}
#[test]
fn test_compute_signed_area_cw() {
let contour = vec![
Point2::new(0.0, 0.0),
Point2::new(0.0, 1.0),
Point2::new(1.0, 1.0),
Point2::new(1.0, 0.0),
];
let area = compute_signed_area(&contour);
assert!((area + 1.0).abs() < EPSILON_2D);
}
#[test]
fn test_ensure_ccw() {
let cw = vec![
Point2::new(0.0, 0.0),
Point2::new(0.0, 1.0),
Point2::new(1.0, 1.0),
Point2::new(1.0, 0.0),
];
let ccw = ensure_ccw(&cw);
assert!(compute_signed_area(&ccw) > 0.0);
}
#[test]
fn test_subtract_2d_simple() {
let profile = Profile2D::new(vec![
Point2::new(0.0, 0.0),
Point2::new(10.0, 0.0),
Point2::new(10.0, 10.0),
Point2::new(0.0, 10.0),
]);
let void_contour = vec![
Point2::new(4.0, 4.0),
Point2::new(6.0, 4.0),
Point2::new(6.0, 6.0),
Point2::new(4.0, 6.0),
];
let result = subtract_2d(&profile, &void_contour).unwrap();
assert_eq!(result.holes.len(), 1);
assert_eq!(result.outer.len(), 4);
}
#[test]
fn test_subtract_multiple_2d() {
let profile = Profile2D::new(vec![
Point2::new(0.0, 0.0),
Point2::new(10.0, 0.0),
Point2::new(10.0, 10.0),
Point2::new(0.0, 10.0),
]);
let voids = vec![
vec![
Point2::new(2.0, 2.0),
Point2::new(3.0, 2.0),
Point2::new(3.0, 3.0),
Point2::new(2.0, 3.0),
],
vec![
Point2::new(7.0, 7.0),
Point2::new(8.0, 7.0),
Point2::new(8.0, 8.0),
Point2::new(7.0, 8.0),
],
];
let result = subtract_multiple_2d(&profile, &voids).unwrap();
assert_eq!(result.holes.len(), 2);
}
#[test]
fn test_point_in_contour() {
let contour = vec![
Point2::new(0.0, 0.0),
Point2::new(10.0, 0.0),
Point2::new(10.0, 10.0),
Point2::new(0.0, 10.0),
];
assert!(point_in_contour(&Point2::new(5.0, 5.0), &contour));
assert!(!point_in_contour(&Point2::new(15.0, 5.0), &contour));
assert!(!point_in_contour(&Point2::new(-1.0, 5.0), &contour));
}
#[test]
fn test_is_valid_contour() {
let valid = vec![
Point2::new(0.0, 0.0),
Point2::new(1.0, 0.0),
Point2::new(1.0, 1.0),
Point2::new(0.0, 1.0),
];
assert!(is_valid_contour(&valid));
let degenerate = vec![
Point2::new(0.0, 0.0),
Point2::new(1.0, 0.0),
Point2::new(2.0, 0.0),
];
assert!(!is_valid_contour(°enerate));
let too_few = vec![Point2::new(0.0, 0.0), Point2::new(1.0, 0.0)];
assert!(!is_valid_contour(&too_few));
}
}