use crate::boolean::{
BooleanFragmentClassification, validate_boolean_fragment_classification_boundary_action,
};
use crate::classify::is_zero;
use crate::{
Classification, Contour2, CurveError, CurvePolicy, CurveResult, FillRule, RegionContourKey,
RegionContourRole, RegionSide, Segment2,
};
#[derive(Clone, Debug, PartialEq)]
pub struct DirectedBooleanFragment {
pub key: crate::RegionContourKey,
pub fragment_index: usize,
pub segment: Segment2,
}
#[derive(Clone, Debug, Default, PartialEq)]
pub struct BooleanBoundaryFragmentSet {
directed_fragments: Vec<DirectedBooleanFragment>,
unresolved_boundaries: Vec<BooleanFragmentClassification>,
}
impl BooleanBoundaryFragmentSet {
pub fn new(
directed_fragments: Vec<DirectedBooleanFragment>,
unresolved_boundaries: Vec<BooleanFragmentClassification>,
) -> CurveResult<Self> {
validate_boolean_boundary_fragment_set(&directed_fragments, &unresolved_boundaries)?;
Ok(Self {
directed_fragments,
unresolved_boundaries,
})
}
pub fn directed_fragments(&self) -> &[DirectedBooleanFragment] {
&self.directed_fragments
}
pub fn unresolved_boundaries(&self) -> &[BooleanFragmentClassification] {
&self.unresolved_boundaries
}
pub fn is_empty(&self) -> bool {
self.directed_fragments.is_empty() && self.unresolved_boundaries.is_empty()
}
pub fn is_ready_for_traversal(&self) -> bool {
self.unresolved_boundaries.is_empty()
}
pub fn directed_len(&self) -> usize {
self.directed_fragments.len()
}
pub fn unresolved_len(&self) -> usize {
self.unresolved_boundaries.len()
}
pub fn assemble_chains(&self, policy: &CurvePolicy) -> Classification<BooleanBoundaryChainSet> {
if !self.unresolved_boundaries.is_empty() {
return Classification::Uncertain(crate::UncertaintyReason::Boundary);
}
let (successors, predecessors) = match endpoint_adjacency(&self.directed_fragments, policy)
{
Classification::Decided(adjacency) => adjacency,
Classification::Uncertain(reason) => return Classification::Uncertain(reason),
};
let mut used = vec![false; self.directed_fragments.len()];
let mut chains = Vec::new();
for index in 0..self.directed_fragments.len() {
if predecessors[index].is_none() && !used[index] {
let chain =
match follow_chain(index, &self.directed_fragments, &successors, &mut used) {
Classification::Decided(chain) => chain,
Classification::Uncertain(reason) => {
return Classification::Uncertain(reason);
}
};
chains.push(chain);
}
}
for index in 0..self.directed_fragments.len() {
if !used[index] {
let chain =
match follow_chain(index, &self.directed_fragments, &successors, &mut used) {
Classification::Decided(chain) => chain,
Classification::Uncertain(reason) => {
return Classification::Uncertain(reason);
}
};
chains.push(chain);
}
}
decided_boolean_boundary_chain_set(chains)
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct BooleanBoundaryChain {
fragments: Vec<DirectedBooleanFragment>,
closed: bool,
}
impl BooleanBoundaryChain {
pub fn new(fragments: Vec<DirectedBooleanFragment>, closed: bool) -> CurveResult<Self> {
validate_directed_boolean_fragments(&fragments, "boolean boundary chain")?;
validate_boolean_boundary_chain_geometry(&fragments, closed)?;
Ok(Self { fragments, closed })
}
pub fn fragments(&self) -> &[DirectedBooleanFragment] {
&self.fragments
}
pub fn into_fragments(self) -> Vec<DirectedBooleanFragment> {
self.fragments
}
pub const fn is_closed(&self) -> bool {
self.closed
}
pub fn is_empty(&self) -> bool {
self.fragments.is_empty()
}
pub fn len(&self) -> usize {
self.fragments.len()
}
}
#[derive(Clone, Debug, Default, PartialEq)]
pub struct BooleanBoundaryChainSet {
chains: Vec<BooleanBoundaryChain>,
}
impl BooleanBoundaryChainSet {
pub fn new(chains: Vec<BooleanBoundaryChain>) -> CurveResult<Self> {
validate_boolean_boundary_chains(&chains)?;
Ok(Self { chains })
}
pub fn chains(&self) -> &[BooleanBoundaryChain] {
&self.chains
}
pub fn into_chains(self) -> Vec<BooleanBoundaryChain> {
self.chains
}
pub fn is_empty(&self) -> bool {
self.chains.is_empty()
}
pub fn len(&self) -> usize {
self.chains.len()
}
pub fn closed_count(&self) -> usize {
self.chains.iter().filter(|chain| chain.is_closed()).count()
}
pub fn closed_loops(&self) -> Classification<BooleanBoundaryLoopSet> {
if self.chains.iter().any(|chain| !chain.is_closed()) {
return Classification::Uncertain(crate::UncertaintyReason::Unsupported);
}
let loops = match self
.chains
.iter()
.map(|chain| BooleanBoundaryLoop::new(chain.fragments.clone()))
.collect::<CurveResult<Vec<_>>>()
{
Ok(loops) => loops,
Err(_) => return Classification::Uncertain(crate::UncertaintyReason::Unsupported),
};
decided_boolean_boundary_loop_set(loops)
}
pub fn into_closed_loops(self) -> Classification<BooleanBoundaryLoopSet> {
if self.chains.iter().any(|chain| !chain.is_closed()) {
return Classification::Uncertain(crate::UncertaintyReason::Unsupported);
}
let loops = match self
.chains
.into_iter()
.map(|chain| BooleanBoundaryLoop::new(chain.fragments))
.collect::<CurveResult<Vec<_>>>()
{
Ok(loops) => loops,
Err(_) => return Classification::Uncertain(crate::UncertaintyReason::Unsupported),
};
decided_boolean_boundary_loop_set(loops)
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct BooleanBoundaryLoop {
fragments: Vec<DirectedBooleanFragment>,
}
impl BooleanBoundaryLoop {
pub fn new(fragments: Vec<DirectedBooleanFragment>) -> CurveResult<Self> {
validate_directed_boolean_fragments(&fragments, "boolean boundary loop")?;
validate_boolean_boundary_loop_geometry(&fragments)?;
Ok(Self { fragments })
}
pub fn fragments(&self) -> &[DirectedBooleanFragment] {
&self.fragments
}
pub fn into_fragments(self) -> Vec<DirectedBooleanFragment> {
self.fragments
}
pub fn is_empty(&self) -> bool {
self.fragments.is_empty()
}
pub fn len(&self) -> usize {
self.fragments.len()
}
pub fn to_contour(&self, fill_rule: FillRule) -> CurveResult<Contour2> {
Contour2::try_new_with_fill_rule(
self.fragments
.iter()
.map(|fragment| fragment.segment.clone())
.collect(),
fill_rule,
)
}
pub fn into_contour(self, fill_rule: FillRule) -> CurveResult<Contour2> {
Contour2::try_new_with_fill_rule(
self.fragments
.into_iter()
.map(|fragment| fragment.segment)
.collect(),
fill_rule,
)
}
}
#[derive(Clone, Debug, Default, PartialEq)]
pub struct BooleanBoundaryLoopSet {
loops: Vec<BooleanBoundaryLoop>,
}
impl BooleanBoundaryLoopSet {
pub fn new(loops: Vec<BooleanBoundaryLoop>) -> CurveResult<Self> {
validate_boolean_boundary_loops(&loops)?;
Ok(Self { loops })
}
pub fn from_contours(contours: Vec<Contour2>) -> CurveResult<Self> {
let mut loops = Vec::with_capacity(contours.len());
for (index, contour) in contours.into_iter().enumerate() {
let fragments = contour
.segments()
.iter()
.enumerate()
.map(|(fragment_index, segment)| DirectedBooleanFragment {
key: RegionContourKey::new(
RegionSide::First,
RegionContourRole::Material,
index,
),
fragment_index,
segment: segment.clone(),
})
.collect();
loops.push(BooleanBoundaryLoop::new(fragments)?);
}
Self::new(loops)
}
pub fn from_contour_classification(
contours: Classification<Vec<Contour2>>,
) -> CurveResult<Classification<Self>> {
match contours {
Classification::Decided(contours) => {
Self::from_contours(contours).map(Classification::Decided)
}
Classification::Uncertain(reason) => Ok(Classification::Uncertain(reason)),
}
}
pub fn loops(&self) -> &[BooleanBoundaryLoop] {
&self.loops
}
pub fn into_loops(self) -> Vec<BooleanBoundaryLoop> {
self.loops
}
pub fn is_empty(&self) -> bool {
self.loops.is_empty()
}
pub fn len(&self) -> usize {
self.loops.len()
}
pub fn to_contours(&self, fill_rule: FillRule) -> CurveResult<Vec<Contour2>> {
self.loops
.iter()
.map(|boundary_loop| boundary_loop.to_contour(fill_rule))
.collect()
}
pub fn into_contours(self, fill_rule: FillRule) -> CurveResult<Vec<Contour2>> {
self.loops
.into_iter()
.map(|boundary_loop| boundary_loop.into_contour(fill_rule))
.collect()
}
}
type EndpointAdjacency = (Vec<Option<usize>>, Vec<Option<usize>>);
fn directed_boolean_fragment_owner(
fragment: &DirectedBooleanFragment,
) -> (RegionContourKey, usize) {
(fragment.key, fragment.fragment_index)
}
fn validate_directed_boolean_fragments(
fragments: &[DirectedBooleanFragment],
owner: &str,
) -> CurveResult<()> {
if fragments.is_empty() {
return Err(CurveError::Topology(format!(
"{owner} must carry at least one directed fragment"
)));
}
let mut fragment_owners = fragments
.iter()
.map(directed_boolean_fragment_owner)
.collect::<Vec<_>>();
fragment_owners.sort_unstable();
if fragment_owners
.windows(2)
.any(|window| window[0] == window[1])
{
return Err(CurveError::Topology(format!(
"{owner} directed fragment ownership must be unique"
)));
}
validate_directed_boolean_fragment_geometry(fragments, owner)?;
Ok(())
}
fn validate_directed_boolean_fragment_geometry(
fragments: &[DirectedBooleanFragment],
owner: &str,
) -> CurveResult<()> {
let policy = CurvePolicy::certified();
for fragment in fragments {
match is_zero(
&fragment
.segment
.start()
.distance_squared(fragment.segment.end()),
&policy,
) {
Some(false) => {}
Some(true) => {
return Err(CurveError::Topology(format!(
"{owner} directed fragment must carry nonzero geometry"
)));
}
None => {
return Err(CurveError::Topology(format!(
"{owner} directed fragment geometry must be certified nonzero"
)));
}
}
}
Ok(())
}
fn validate_boolean_boundary_chain_geometry(
fragments: &[DirectedBooleanFragment],
closed: bool,
) -> CurveResult<()> {
validate_directed_boolean_fragment_connectivity(fragments, "boolean boundary chain")?;
let (first, last) = directed_fragment_endpoints(fragments, "boolean boundary chain")?;
let endpoints_close = certified_endpoint_match(last, first, "boolean boundary chain")?;
if endpoints_close != closed {
return Err(CurveError::Topology(
"boolean boundary chain closed flag must match endpoint evidence".to_owned(),
));
}
Ok(())
}
fn validate_boolean_boundary_loop_geometry(
fragments: &[DirectedBooleanFragment],
) -> CurveResult<()> {
validate_directed_boolean_fragment_connectivity(fragments, "boolean boundary loop")?;
let (first, last) = directed_fragment_endpoints(fragments, "boolean boundary loop")?;
if !certified_endpoint_match(last, first, "boolean boundary loop")? {
return Err(CurveError::Topology(
"boolean boundary loop must close back to its first fragment".to_owned(),
));
}
Ok(())
}
fn validate_directed_boolean_fragment_connectivity(
fragments: &[DirectedBooleanFragment],
owner: &str,
) -> CurveResult<()> {
for window in fragments.windows(2) {
if !certified_endpoint_match(&window[0], &window[1], owner)? {
return Err(CurveError::Topology(format!(
"{owner} fragments must be endpoint-connected"
)));
}
}
Ok(())
}
fn directed_fragment_endpoints<'a>(
fragments: &'a [DirectedBooleanFragment],
owner: &str,
) -> CurveResult<(&'a DirectedBooleanFragment, &'a DirectedBooleanFragment)> {
let first = fragments.first().ok_or_else(|| {
CurveError::Topology(format!("{owner} must carry at least one directed fragment"))
})?;
let last = fragments.last().ok_or_else(|| {
CurveError::Topology(format!("{owner} must carry at least one directed fragment"))
})?;
Ok((first, last))
}
fn certified_endpoint_match(
left: &DirectedBooleanFragment,
right: &DirectedBooleanFragment,
owner: &str,
) -> CurveResult<bool> {
let policy = CurvePolicy::certified();
match points_match(left.segment.end(), right.segment.start(), &policy) {
Classification::Decided(matches) => Ok(matches),
Classification::Uncertain(reason) => Err(CurveError::Topology(format!(
"{owner} endpoint equality could not be certified: {reason:?}"
))),
}
}
fn validate_boolean_boundary_chains(chains: &[BooleanBoundaryChain]) -> CurveResult<()> {
let mut fragment_owners = Vec::new();
for chain in chains {
validate_directed_boolean_fragments(chain.fragments(), "boolean boundary chain")?;
fragment_owners.extend(
chain
.fragments()
.iter()
.map(directed_boolean_fragment_owner),
);
}
validate_unique_boolean_fragment_owners(
fragment_owners,
"boolean boundary chain set must not reuse directed fragment ownership",
)
}
fn validate_boolean_boundary_loops(loops: &[BooleanBoundaryLoop]) -> CurveResult<()> {
let mut fragment_owners = Vec::new();
for boundary_loop in loops {
validate_directed_boolean_fragments(boundary_loop.fragments(), "boolean boundary loop")?;
fragment_owners.extend(
boundary_loop
.fragments()
.iter()
.map(directed_boolean_fragment_owner),
);
}
validate_unique_boolean_fragment_owners(
fragment_owners,
"boolean boundary loop set must not reuse directed fragment ownership",
)
}
fn validate_unique_boolean_fragment_owners(
mut fragment_owners: Vec<(RegionContourKey, usize)>,
message: &str,
) -> CurveResult<()> {
fragment_owners.sort_unstable();
if fragment_owners
.windows(2)
.any(|window| window[0] == window[1])
{
return Err(CurveError::Topology(message.to_owned()));
}
Ok(())
}
fn validate_boolean_boundary_fragment_set(
directed_fragments: &[DirectedBooleanFragment],
unresolved_boundaries: &[BooleanFragmentClassification],
) -> CurveResult<()> {
validate_directed_boolean_fragment_geometry(
directed_fragments,
"boolean boundary fragment set",
)?;
for unresolved in unresolved_boundaries {
validate_boolean_fragment_classification_boundary_action(unresolved)?;
}
let mut fragment_owners = directed_fragments
.iter()
.map(directed_boolean_fragment_owner)
.collect::<Vec<_>>();
fragment_owners.extend(
unresolved_boundaries
.iter()
.map(|classification| (classification.key, classification.fragment_index)),
);
validate_unique_boolean_fragment_owners(
fragment_owners,
"boolean boundary fragment set must not contain duplicate source fragment ownership",
)
}
fn decided_boolean_boundary_chain(
fragments: Vec<DirectedBooleanFragment>,
closed: bool,
) -> Classification<BooleanBoundaryChain> {
match BooleanBoundaryChain::new(fragments, closed) {
Ok(chain) => Classification::Decided(chain),
Err(_) => Classification::Uncertain(crate::UncertaintyReason::Unsupported),
}
}
fn decided_boolean_boundary_chain_set(
chains: Vec<BooleanBoundaryChain>,
) -> Classification<BooleanBoundaryChainSet> {
match BooleanBoundaryChainSet::new(chains) {
Ok(chain_set) => Classification::Decided(chain_set),
Err(_) => Classification::Uncertain(crate::UncertaintyReason::Unsupported),
}
}
fn decided_boolean_boundary_loop_set(
loops: Vec<BooleanBoundaryLoop>,
) -> Classification<BooleanBoundaryLoopSet> {
match BooleanBoundaryLoopSet::new(loops) {
Ok(loop_set) => Classification::Decided(loop_set),
Err(_) => Classification::Uncertain(crate::UncertaintyReason::Unsupported),
}
}
fn endpoint_adjacency(
fragments: &[DirectedBooleanFragment],
policy: &CurvePolicy,
) -> Classification<EndpointAdjacency> {
let mut successors = vec![None; fragments.len()];
let mut predecessors = vec![None; fragments.len()];
for (left_index, left) in fragments.iter().enumerate() {
for (right_index, right) in fragments.iter().enumerate() {
if left_index == right_index {
continue;
}
let matches = match points_match(left.segment.end(), right.segment.start(), policy) {
Classification::Decided(matches) => matches,
Classification::Uncertain(reason) => return Classification::Uncertain(reason),
};
if !matches {
continue;
}
if successors[left_index].replace(right_index).is_some()
|| predecessors[right_index].replace(left_index).is_some()
{
return Classification::Uncertain(crate::UncertaintyReason::Unsupported);
}
}
}
Classification::Decided((successors, predecessors))
}
fn follow_chain(
start: usize,
fragments: &[DirectedBooleanFragment],
successors: &[Option<usize>],
used: &mut [bool],
) -> Classification<BooleanBoundaryChain> {
let mut chain = Vec::new();
let mut current = start;
let mut closed = false;
loop {
if used[current] {
return Classification::Uncertain(crate::UncertaintyReason::Unsupported);
}
used[current] = true;
chain.push(fragments[current].clone());
let Some(next) = successors[current] else {
break;
};
if next == start {
closed = true;
break;
}
if used[next] {
return Classification::Uncertain(crate::UncertaintyReason::Unsupported);
}
current = next;
}
decided_boolean_boundary_chain(chain, closed)
}
fn points_match(
left: &crate::Point2,
right: &crate::Point2,
policy: &CurvePolicy,
) -> Classification<bool> {
let distance = left.distance_squared(right);
match is_zero(&distance, policy) {
Some(matches) => Classification::Decided(matches),
None => Classification::Uncertain(crate::UncertaintyReason::RealSign),
}
}