use i_overlay::core::fill_rule::FillRule;
use i_overlay::core::overlay_rule::OverlayRule;
use i_overlay::float::single::SingleFloatOverlay;
pub type Ring2D = Vec<[f64; 2]>;
#[derive(Clone, Debug, Default, PartialEq)]
pub struct ContourSet {
pub rings: Vec<Ring2D>,
pub shape_offsets: Vec<usize>,
}
impl ContourSet {
pub fn is_empty(&self) -> bool {
self.rings.is_empty()
}
pub fn shape_count(&self) -> usize {
self.shape_offsets.len()
}
pub fn shape(&self, s: usize) -> Option<&[Ring2D]> {
let start = *self.shape_offsets.get(s)?;
let end = self
.shape_offsets
.get(s + 1)
.copied()
.unwrap_or(self.rings.len());
self.rings.get(start..end)
}
pub fn bounds(&self) -> Option<[f64; 4]> {
let mut b = [
f64::INFINITY,
f64::INFINITY,
f64::NEG_INFINITY,
f64::NEG_INFINITY,
];
for p in self.rings.iter().flatten() {
b[0] = b[0].min(p[0]);
b[1] = b[1].min(p[1]);
b[2] = b[2].max(p[0]);
b[3] = b[3].max(p[1]);
}
(b[0] <= b[2]).then_some(b)
}
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum BooleanOp2D {
Union,
Difference,
Intersection,
}
impl BooleanOp2D {
pub fn from_u8(v: u8) -> Option<Self> {
match v {
0 => Some(BooleanOp2D::Union),
1 => Some(BooleanOp2D::Difference),
2 => Some(BooleanOp2D::Intersection),
_ => None,
}
}
}
fn is_collinear(path: &[[f64; 2]]) -> bool {
let p0 = path[0];
let dir = path.iter().find_map(|p| {
let d = [p[0] - p0[0], p[1] - p0[1]];
(d != [0.0, 0.0]).then_some(d)
});
let Some(dir) = dir else {
return true;
};
path.iter().all(|p| {
let d = [p[0] - p0[0], p[1] - p0[1]];
dir[0] * d[1] - dir[1] * d[0] == 0.0
})
}
pub fn sanitize(rings: &[Ring2D]) -> Vec<Vec<[f64; 2]>> {
rings
.iter()
.filter_map(|ring| {
if ring.iter().any(|p| !p[0].is_finite() || !p[1].is_finite()) {
return None;
}
let mut path = ring.clone();
while path.len() >= 2 && path[path.len() - 1] == path[0] {
path.pop();
}
if path.len() < 3 || is_collinear(&path) {
return None;
}
Some(path)
})
.collect()
}
fn collect(shapes: Vec<Vec<Vec<[f64; 2]>>>) -> ContourSet {
let mut out = ContourSet::default();
for shape in shapes {
match shape.first() {
Some(outer) if outer.len() >= 3 => {}
_ => continue,
}
out.shape_offsets.push(out.rings.len());
for ring in shape {
if ring.len() >= 3 {
out.rings.push(ring);
}
}
}
out
}
fn resolve(subject: &[Vec<[f64; 2]>]) -> ContourSet {
let empty: Vec<Vec<[f64; 2]>> = Vec::new();
collect(subject.overlay(&empty, OverlayRule::Union, FillRule::NonZero))
}
pub fn boolean_2d(subject: &[Ring2D], clip: &[Ring2D], op: BooleanOp2D) -> ContourSet {
let subject = sanitize(subject);
let clip = sanitize(clip);
match op {
BooleanOp2D::Union => {
if subject.is_empty() {
return resolve(&clip);
}
if clip.is_empty() {
return resolve(&subject);
}
collect(subject.overlay(&clip, OverlayRule::Union, FillRule::NonZero))
}
BooleanOp2D::Difference => {
if subject.is_empty() {
return ContourSet::default();
}
if clip.is_empty() {
return resolve(&subject);
}
collect(subject.overlay(&clip, OverlayRule::Difference, FillRule::NonZero))
}
BooleanOp2D::Intersection => {
if subject.is_empty() || clip.is_empty() {
return ContourSet::default();
}
collect(subject.overlay(&clip, OverlayRule::Intersect, FillRule::NonZero))
}
}
}
pub fn resolve_2d(rings: &[Ring2D]) -> ContourSet {
boolean_2d(rings, &[], BooleanOp2D::Union)
}
#[cfg(test)]
#[path = "contour_bool2d_tests.rs"]
mod tests;