use std::{fmt::Debug, marker::PhantomData};
use crate::{
clipper::{Clipper, Operator},
node::{Node, Role},
Edge, Geometry, IsClose, Operands, Vertex,
};
#[derive(Debug, Clone)]
pub struct Shape<T> {
pub(crate) boundaries: Vec<T>,
}
impl<T> From<T> for Shape<T>
where
T: Geometry,
{
fn from(value: T) -> Self {
Self::new(value)
}
}
impl<T> PartialEq for Shape<T>
where
T: PartialEq + Clone,
{
fn eq(&self, other: &Self) -> bool {
if self.boundaries.len() != other.boundaries.len() {
return false;
}
self.boundaries
.iter()
.all(|a| other.boundaries.iter().any(|b| a.eq(b)))
}
}
impl<T> Shape<T>
where
T: Geometry + Clone + IntoIterator<Item = T::Vertex>,
T::Vertex: Copy + PartialEq + PartialOrd,
<T::Vertex as Vertex>::Scalar: Copy + PartialOrd,
{
pub fn or(self, rhs: Self, tolerance: <T::Vertex as IsClose>::Tolerance) -> Self {
struct OrOperator<T>(PhantomData<T>);
impl<T> Operator<T> for OrOperator<T>
where
T: Geometry,
{
fn is_output<'a>(
ops: Operands<'a, T>,
node: &'a Node<T>,
tolerance: &<T::Vertex as IsClose>::Tolerance,
) -> bool {
match node.role {
Role::Subject(_) => {
!ops.clip.contains(&node.vertex, tolerance)
|| ops.clip.is_boundary(&node.vertex, tolerance)
}
Role::Clip(_) => {
!ops.subject.contains(&node.vertex, tolerance)
|| ops.subject.is_boundary(&node.vertex, tolerance)
}
}
}
}
Clipper::default()
.with_operator::<OrOperator<T>>()
.with_tolerance(tolerance)
.with_subject(self)
.with_clip(rhs)
.execute()
.expect("union should always return a shape")
}
pub fn not(self, rhs: Self, tolerance: <T::Vertex as IsClose>::Tolerance) -> Option<Self> {
struct NotOperator<T>(PhantomData<T>);
impl<T> Operator<T> for NotOperator<T>
where
T: Geometry,
{
fn is_output<'a>(
ops: Operands<'a, T>,
node: &'a Node<T>,
tolerance: &<T::Vertex as IsClose>::Tolerance,
) -> bool {
match node.role {
Role::Subject(_) => {
!ops.clip.contains(&node.vertex, tolerance)
&& !ops.clip.is_boundary(&node.vertex, tolerance)
}
Role::Clip(_) => {
ops.subject.contains(&node.vertex, tolerance)
&& !ops.subject.is_boundary(&node.vertex, tolerance)
}
}
}
}
Clipper::default()
.with_operator::<NotOperator<T>>()
.with_tolerance(tolerance)
.with_clip(rhs.inverted_winding())
.with_subject(self)
.execute()
}
pub fn and(self, rhs: Self, tolerance: <T::Vertex as IsClose>::Tolerance) -> Option<Self> {
struct AndOperator<T>(PhantomData<T>);
impl<T> Operator<T> for AndOperator<T>
where
T: Geometry,
{
fn is_output<'a>(
ops: Operands<'a, T>,
node: &'a Node<T>,
tolerance: &<T::Vertex as IsClose>::Tolerance,
) -> bool {
match node.role {
Role::Subject(_) => {
ops.clip.contains(&node.vertex, tolerance)
|| ops.clip.is_boundary(&node.vertex, tolerance)
}
Role::Clip(_) => {
ops.subject.contains(&node.vertex, tolerance)
|| ops.subject.is_boundary(&node.vertex, tolerance)
}
}
}
}
Clipper::default()
.with_operator::<AndOperator<T>>()
.with_tolerance(tolerance)
.with_subject(self)
.with_clip(rhs)
.execute()
}
}
impl<T> Shape<T>
where
T: Geometry,
T::Vertex: Vertex,
{
fn winding(&self, vertex: &T::Vertex, tolerance: &<T::Vertex as IsClose>::Tolerance) -> isize {
self.boundaries
.iter()
.map(|boundary| boundary.winding(vertex, tolerance))
.sum()
}
pub(crate) fn contains(
&self,
vertex: &T::Vertex,
tolerance: &<T::Vertex as IsClose>::Tolerance,
) -> bool {
self.winding(vertex, tolerance) != 0
}
}
impl<T> Shape<T>
where
T: Geometry,
{
pub fn new(value: impl Into<T>) -> Self {
let boundary = value.into();
Self {
boundaries: vec![if boundary.is_clockwise() {
boundary.reversed()
} else {
boundary
}],
}
}
pub(crate) fn is_boundary(
&self,
vertex: &T::Vertex,
tolerance: &<T::Vertex as IsClose>::Tolerance,
) -> bool {
self.boundaries
.iter()
.flat_map(|boundary| boundary.edges())
.any(|segment| segment.contains(vertex, tolerance))
}
pub(crate) fn total_vertices(&self) -> usize {
self.boundaries
.iter()
.map(|boundary| boundary.total_vertices())
.sum()
}
pub(crate) fn edges(&self) -> impl Iterator<Item = T::Edge<'_>> {
self.boundaries.iter().flat_map(|boundary| boundary.edges())
}
fn inverted_winding(self) -> Self {
Self {
boundaries: self.boundaries.into_iter().map(T::reversed).collect(),
}
}
}