use std::cmp::Ordering;
use hyperreal::{Real, ZeroKnowledge as ZeroStatus};
use crate::bbox::{Aabb2, aabb_decided_misses_point, decided_contour_aabb, decided_segment_aabb};
use crate::classify::{classify_oriented_line, compare_reals};
use crate::{
BulgeVertex2, Classification, ContourAreaUnsupportedReason, ContourAreaUnsupportedSegment2,
ContourSignedAreaReport2, CurveError, CurvePolicy, CurveResult, CurveString2, LineSide, Point2,
Segment2, UncertaintyReason,
};
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum FillRule {
NonZero,
EvenOdd,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum ContourPointLocation {
Outside,
Boundary,
Inside,
}
#[derive(Clone, Debug, PartialEq)]
pub struct Contour2 {
curve: CurveString2,
fill_rule: FillRule,
}
impl Contour2 {
pub fn try_new(segments: Vec<Segment2>) -> CurveResult<Self> {
Self::try_new_with_fill_rule(segments, FillRule::NonZero)
}
pub fn try_new_with_fill_rule(
segments: Vec<Segment2>,
fill_rule: FillRule,
) -> CurveResult<Self> {
let curve = CurveString2::try_new(segments)?;
validate_closed_curve_string(&curve)?;
Ok(Self { curve, fill_rule })
}
pub const fn new_unchecked(curve: CurveString2, fill_rule: FillRule) -> Self {
Self { curve, fill_rule }
}
pub fn from_bulge_vertices(vertices: &[BulgeVertex2]) -> CurveResult<Self> {
Self::from_bulge_vertices_with_fill_rule(vertices, FillRule::NonZero)
}
pub fn from_bulge_vertices_with_fill_rule(
vertices: &[BulgeVertex2],
fill_rule: FillRule,
) -> CurveResult<Self> {
if vertices.len() < 2 {
return Err(CurveError::InsufficientVertices);
}
let mut segments = Vec::with_capacity(vertices.len());
for adjacent in vertices.windows(2) {
segments.push(adjacent[0].segment_to(&adjacent[1])?);
}
segments.push(vertices[vertices.len() - 1].segment_to(&vertices[0])?);
Self::try_new_with_fill_rule(segments, fill_rule)
}
pub const fn curve_string(&self) -> &CurveString2 {
&self.curve
}
pub fn segments(&self) -> &[Segment2] {
self.curve.segments()
}
pub fn has_same_exact_boundary(&self, other: &Self) -> bool {
self.fill_rule == other.fill_rule
&& same_exact_segment_cycle(self.segments(), other.segments())
}
pub const fn fill_rule(&self) -> FillRule {
self.fill_rule
}
pub fn signed_area(&self) -> CurveResult<Option<Real>> {
Ok(self.signed_area_report()?.signed_area)
}
pub fn signed_area_report(&self) -> CurveResult<ContourSignedAreaReport2> {
let mut report = ContourSignedAreaReport2::empty();
let mut area = Real::zero();
for (index, segment) in self.segments().iter().enumerate() {
report.segment_count += 1;
match segment {
Segment2::Line(line) => {
report.line_segment_count += 1;
area = &area + &line_signed_area_contribution(line.start(), line.end())?;
}
Segment2::Arc(arc) => match arc_signed_area_contribution(arc)? {
Some(contribution) => {
report.bulge_arc_segment_count += 1;
area = &area + &contribution;
}
None => {
report
.unsupported_segments
.push(ContourAreaUnsupportedSegment2 {
segment_index: index,
reason: ContourAreaUnsupportedReason::CenterOnlyArcSweepAngle,
});
}
},
}
}
report.signed_area = if report.unsupported_segments.is_empty() {
Some(area)
} else {
None
};
Ok(report)
}
pub fn len(&self) -> usize {
self.curve.len()
}
pub fn is_empty(&self) -> bool {
self.curve.is_empty()
}
pub fn winding_number(&self, point: &Point2, policy: &CurvePolicy) -> Classification<i32> {
let contour_box = decided_contour_aabb(self, policy);
let segment_boxes = decided_segment_boxes(self.segments(), policy);
contour_winding_number_with_cached_aabbs(
self,
point,
contour_box.as_ref(),
&segment_boxes,
policy,
)
}
pub fn classify_point(
&self,
point: &Point2,
policy: &CurvePolicy,
) -> Classification<ContourPointLocation> {
let contour_box = decided_contour_aabb(self, policy);
let segment_boxes = decided_segment_boxes(self.segments(), policy);
classify_contour_point_with_cached_aabbs(
self,
point,
contour_box.as_ref(),
&segment_boxes,
policy,
)
}
pub fn point_on_boundary(&self, point: &Point2, policy: &CurvePolicy) -> Classification<bool> {
let contour_box = decided_contour_aabb(self, policy);
let segment_boxes = decided_segment_boxes(self.segments(), policy);
point_on_contour_boundary_with_cached_aabbs(
self,
point,
contour_box.as_ref(),
&segment_boxes,
policy,
)
}
pub fn intersect_contour(
&self,
other: &Self,
policy: &CurvePolicy,
) -> CurveResult<crate::ContourIntersectionSet> {
crate::events::intersect_contours(self, other, policy)
}
pub fn intersect_self(
&self,
policy: &CurvePolicy,
) -> CurveResult<crate::ContourIntersectionSet> {
crate::events::intersect_contour_self(self, policy)
}
pub fn split_at_intersections(
&self,
intersections: &crate::ContourIntersectionSet,
operand: crate::ContourOperand,
policy: &CurvePolicy,
) -> CurveResult<Classification<crate::ContourFragmentSet>> {
crate::fragment::split_contour_at_intersections(self, intersections, operand, policy)
}
pub fn split_at_self_intersections(
&self,
intersections: &crate::ContourIntersectionSet,
policy: &CurvePolicy,
) -> CurveResult<Classification<crate::ContourFragmentSet>> {
crate::fragment::split_contour_at_self_intersections(self, intersections, policy)
}
}
pub(crate) fn classify_contour_point_with_cached_aabbs(
contour: &Contour2,
point: &Point2,
contour_box: Option<&Aabb2>,
segment_boxes: &[Option<Aabb2>],
policy: &CurvePolicy,
) -> Classification<ContourPointLocation> {
if contour_box_misses_point(contour_box, point, policy) {
return Classification::Decided(ContourPointLocation::Outside);
}
match point_on_contour_boundary_with_cached_aabbs(
contour,
point,
contour_box,
segment_boxes,
policy,
) {
Classification::Decided(true) => {
return Classification::Decided(ContourPointLocation::Boundary);
}
Classification::Decided(false) => {}
Classification::Uncertain(reason) => return Classification::Uncertain(reason),
}
let winding = match contour_winding_number_unchecked_with_cached_aabb(
contour,
point,
contour_box,
policy,
) {
Classification::Decided(winding) => winding,
Classification::Uncertain(reason) => return Classification::Uncertain(reason),
};
let inside = match contour.fill_rule {
FillRule::NonZero => winding != 0,
FillRule::EvenOdd => winding.rem_euclid(2) != 0,
};
Classification::Decided(if inside {
ContourPointLocation::Inside
} else {
ContourPointLocation::Outside
})
}
pub(crate) fn contour_winding_number_with_cached_aabbs(
contour: &Contour2,
point: &Point2,
contour_box: Option<&Aabb2>,
segment_boxes: &[Option<Aabb2>],
policy: &CurvePolicy,
) -> Classification<i32> {
if contour_box_misses_point(contour_box, point, policy) {
return Classification::Decided(0);
}
match point_on_contour_boundary_with_cached_aabbs(
contour,
point,
contour_box,
segment_boxes,
policy,
) {
Classification::Decided(true) => {
return Classification::Uncertain(UncertaintyReason::Boundary);
}
Classification::Decided(false) => {}
Classification::Uncertain(reason) => return Classification::Uncertain(reason),
}
contour_winding_number_unchecked_with_cached_aabb(contour, point, contour_box, policy)
}
pub(crate) fn point_on_contour_boundary_with_cached_aabbs(
contour: &Contour2,
point: &Point2,
contour_box: Option<&Aabb2>,
segment_boxes: &[Option<Aabb2>],
policy: &CurvePolicy,
) -> Classification<bool> {
if contour_box_misses_point(contour_box, point, policy) {
return Classification::Decided(false);
}
for (index, segment) in contour.segments().iter().enumerate() {
if segment_boxes
.get(index)
.and_then(Option::as_ref)
.is_some_and(|bbox| aabb_decided_misses_point(bbox, point, policy))
{
continue;
}
match segment.contains_point(point, policy) {
Classification::Decided(true) => return Classification::Decided(true),
Classification::Decided(false) => {}
Classification::Uncertain(reason) => return Classification::Uncertain(reason),
}
}
Classification::Decided(false)
}
fn contour_winding_number_unchecked_with_cached_aabb(
contour: &Contour2,
point: &Point2,
contour_box: Option<&Aabb2>,
policy: &CurvePolicy,
) -> Classification<i32> {
if contour_box_misses_point(contour_box, point, policy) {
return Classification::Decided(0);
}
let mut winding = 0;
for segment in contour.segments() {
let delta = match segment {
Segment2::Line(line) => process_line_winding(line.start(), line.end(), point, policy),
Segment2::Arc(arc) => process_arc_winding(arc, point, policy),
};
let Some(delta) = delta else {
return Classification::Uncertain(UncertaintyReason::Ordering);
};
winding += delta;
}
Classification::Decided(winding)
}
fn contour_box_misses_point(
contour_box: Option<&Aabb2>,
point: &Point2,
policy: &CurvePolicy,
) -> bool {
contour_box.is_some_and(|bbox| aabb_decided_misses_point(bbox, point, policy))
}
fn decided_segment_boxes(segments: &[Segment2], policy: &CurvePolicy) -> Vec<Option<Aabb2>> {
segments
.iter()
.map(|segment| decided_segment_aabb(segment, policy))
.collect()
}
fn line_signed_area_contribution(start: &Point2, end: &Point2) -> CurveResult<Real> {
(((start.x() * end.y()) - (end.x() * start.y())) / Real::from(2_i8)).map_err(CurveError::from)
}
fn arc_signed_area_contribution(arc: &crate::CircularArc2) -> CurveResult<Option<Real>> {
let Some(bulge) = arc.bulge() else {
return Ok(None);
};
let chord = line_signed_area_contribution(arc.start(), arc.end())?;
let b2 = bulge * bulge;
let one_plus_b2 = Real::one() + &b2;
let sin_numerator = (Real::from(4_i8) * bulge) * (Real::one() - &b2);
let sin_denominator = &one_plus_b2 * &one_plus_b2;
let sin_theta = (sin_numerator / sin_denominator)?;
let theta = Real::from(4_i8) * bulge.clone().atan()?;
let segment = (arc.radius_squared() * (theta - sin_theta) / Real::from(2_i8))?;
Ok(Some(chord + segment))
}
fn validate_closed_curve_string(curve: &CurveString2) -> CurveResult<()> {
let start = curve.start().ok_or(CurveError::EmptyCurveString)?;
let end = curve.end().ok_or(CurveError::EmptyCurveString)?;
match start.distance_squared(end).zero_status() {
ZeroStatus::Zero => Ok(()),
ZeroStatus::NonZero => Err(CurveError::DisconnectedCurveString),
ZeroStatus::Unknown => Err(CurveError::AmbiguousCurveStringConnection),
}
}
fn same_exact_segment_cycle(first: &[Segment2], second: &[Segment2]) -> bool {
if first.len() != second.len() {
return false;
}
if first.is_empty() {
return true;
}
same_directed_segment_cycle(first, second) || same_reversed_segment_cycle(first, second)
}
fn same_directed_segment_cycle(first: &[Segment2], second: &[Segment2]) -> bool {
let len = first.len();
(0..len).any(|offset| {
first
.iter()
.enumerate()
.all(|(index, segment)| segment == &second[(index + offset) % len])
})
}
fn same_reversed_segment_cycle(first: &[Segment2], second: &[Segment2]) -> bool {
let len = first.len();
(0..len).any(|offset| {
first.iter().enumerate().all(|(index, segment)| {
let reversed_index = (offset + len - 1 - index) % len;
segment == &second[reversed_index].reversed()
})
})
}
fn process_line_winding(
start: &Point2,
end: &Point2,
point: &Point2,
policy: &CurvePolicy,
) -> Option<i32> {
if le_real(start.y(), point.y(), policy)? {
if gt_real(end.y(), point.y(), policy)? && is_left(start, end, point, policy)? {
Some(1)
} else {
Some(0)
}
} else if le_real(end.y(), point.y(), policy)? && !is_left(start, end, point, policy)? {
Some(-1)
} else {
Some(0)
}
}
fn process_arc_winding(
arc: &crate::CircularArc2,
point: &Point2,
policy: &CurvePolicy,
) -> Option<i32> {
let start = arc.start();
let end = arc.end();
let is_ccw = !arc.is_clockwise();
let point_is_left = if is_ccw {
is_left(start, end, point, policy)?
} else {
is_left_or_equal(start, end, point, policy)?
};
let inside_circle = point_inside_circle(arc, point, policy)?;
if le_real(start.y(), point.y(), policy)? {
if gt_real(end.y(), point.y(), policy)? {
if is_ccw {
if point_is_left || inside_circle {
Some(1)
} else {
Some(0)
}
} else if point_is_left && !inside_circle {
Some(1)
} else {
Some(0)
}
} else if is_ccw
&& !point_is_left
&& lt_real(end.x(), point.x(), policy)?
&& lt_real(point.x(), start.x(), policy)?
&& inside_circle
{
Some(1)
} else if !is_ccw
&& point_is_left
&& lt_real(start.x(), point.x(), policy)?
&& lt_real(point.x(), end.x(), policy)?
&& inside_circle
{
Some(-1)
} else {
Some(0)
}
} else if le_real(end.y(), point.y(), policy)? {
if is_ccw {
if !point_is_left && !inside_circle {
Some(-1)
} else {
Some(0)
}
} else if point_is_left {
if inside_circle { Some(-1) } else { Some(0) }
} else {
Some(-1)
}
} else if is_ccw
&& !point_is_left
&& lt_real(start.x(), point.x(), policy)?
&& lt_real(point.x(), end.x(), policy)?
&& inside_circle
{
Some(1)
} else if !is_ccw
&& point_is_left
&& lt_real(end.x(), point.x(), policy)?
&& lt_real(point.x(), start.x(), policy)?
&& inside_circle
{
Some(-1)
} else {
Some(0)
}
}
fn point_inside_circle(
arc: &crate::CircularArc2,
point: &Point2,
policy: &CurvePolicy,
) -> Option<bool> {
let distance_squared = point.distance_squared(arc.center());
Some(matches!(
compare_reals(&distance_squared, &arc.radius_squared(), policy)?,
Ordering::Less
))
}
fn is_left(start: &Point2, end: &Point2, point: &Point2, policy: &CurvePolicy) -> Option<bool> {
match classify_oriented_line(start, end, point, policy) {
Classification::Decided(side) => Some(side == LineSide::Left),
Classification::Uncertain(_) => None,
}
}
fn is_left_or_equal(
start: &Point2,
end: &Point2,
point: &Point2,
policy: &CurvePolicy,
) -> Option<bool> {
match classify_oriented_line(start, end, point, policy) {
Classification::Decided(side) => Some(matches!(side, LineSide::Left | LineSide::On)),
Classification::Uncertain(_) => None,
}
}
fn le_real(left: &Real, right: &Real, policy: &CurvePolicy) -> Option<bool> {
Some(!matches!(
compare_reals(left, right, policy)?,
Ordering::Greater
))
}
fn lt_real(left: &Real, right: &Real, policy: &CurvePolicy) -> Option<bool> {
Some(matches!(
compare_reals(left, right, policy)?,
Ordering::Less
))
}
fn gt_real(left: &Real, right: &Real, policy: &CurvePolicy) -> Option<bool> {
Some(matches!(
compare_reals(left, right, policy)?,
Ordering::Greater
))
}