axiolid-construct 0.1.0

Solid generation: profiles, lofts, sweeps, revolutions and half-space clipping
Documentation
//! Topology-aware integration of certified affine surface intersections.
//!
//! The supported arrangement splits one rectangular patch whose certified trace
//! is a boundary-to-boundary chord. The same edge is retained as an explicit
//! embedded pcurve on the other patch when both endpoints are strictly interior
//! there. This avoids pretending that an interior segment partitions a disk.

use axiolid_contracts::GeomResult;
use axiolid_nurbs::{intersect_surface_surface_certified, CertifiedSurfaceSurfaceIntersection3};
use axiolid_surface::BSplineSurface;

pub use crate::trimmed_intersection_types::{
    CertifiedSurfacePairSplit3, CertifiedSurfacePairSplitOptions, CertifiedTrimmedSurfacePair3,
    EmbeddedFaceCurve, SurfacePairMember, SurfacePairSplitUnresolvedReason,
};

use crate::trimmed_intersection_assembly::assemble;
use crate::trimmed_intersection_classify::classify;

/// Intersect two supported affine patches and construct a topology-aware trimmed arrangement.
///
/// `Split` is returned only for a single certified trace whose endpoints form a
/// boundary-to-boundary chord on exactly one patch and are both interior to the
/// other patch. The returned analytic B-rep has two closed trimmed faces for the
/// split patch and one closed rectangular face for the other patch. The wrapper's
/// embedded-curve relation attaches the same intersection edge to the unsplit
/// face without inventing a dangling trim loop.
///
/// Empty intersections remain `Empty`. Incomplete tracing, dual ownership,
/// corners, mixed ownership, coincidences, and residual-policy misses remain
/// `Unresolved` with the original intersection evidence.
pub fn split_surface_pair_certified(
    first: &BSplineSurface,
    second: &BSplineSurface,
    options: CertifiedSurfacePairSplitOptions,
) -> GeomResult<CertifiedSurfacePairSplit3> {
    let intersection =
        intersect_surface_surface_certified(first, second, options.intersection_options())?;
    let (mut traces, visited_patch_pairs, boundary_queries) = match intersection {
        CertifiedSurfaceSurfaceIntersection3::Unresolved { .. } => {
            return Ok(CertifiedSurfacePairSplit3::Unresolved {
                intersection,
                reason: SurfacePairSplitUnresolvedReason::IntersectionUnresolved,
            });
        }
        CertifiedSurfaceSurfaceIntersection3::Complete {
            traces,
            visited_patch_pairs,
            boundary_queries,
        } => (traces, visited_patch_pairs, boundary_queries),
        other => {
            return Ok(CertifiedSurfacePairSplit3::Unresolved {
                intersection: other,
                reason: SurfacePairSplitUnresolvedReason::IntersectionUnresolved,
            });
        }
    };
    if traces.is_empty() {
        return Ok(CertifiedSurfacePairSplit3::Empty {
            visited_patch_pairs,
            boundary_queries: u32::from(boundary_queries),
        });
    }
    if traces.len() != 1 {
        return Ok(unresolved_complete(
            traces,
            visited_patch_pairs,
            boundary_queries,
            SurfacePairSplitUnresolvedReason::UnsupportedTraceCount,
        ));
    }
    let Some(trace_ref) = traces.first() else {
        return Ok(CertifiedSurfacePairSplit3::Empty {
            visited_patch_pairs,
            boundary_queries: u32::from(boundary_queries),
        });
    };
    let residual_upper_bound = trace_ref
        .start
        .residual_upper_bound
        .max(trace_ref.end.residual_upper_bound);
    if !residual_upper_bound.is_finite() || residual_upper_bound > options.max_surface_residual() {
        return Ok(unresolved_complete(
            traces,
            visited_patch_pairs,
            boundary_queries,
            SurfacePairSplitUnresolvedReason::ResidualExceedsPolicy,
        ));
    }
    let Some(classification) = classify(first, second, trace_ref) else {
        return Ok(unresolved_complete(
            traces,
            visited_patch_pairs,
            boundary_queries,
            SurfacePairSplitUnresolvedReason::UnsupportedEndpointOwnership,
        ));
    };
    let Some(trace) = traces.pop() else {
        return Ok(CertifiedSurfacePairSplit3::Empty {
            visited_patch_pairs,
            boundary_queries: u32::from(boundary_queries),
        });
    };
    let split = assemble(
        first,
        second,
        trace,
        classification,
        residual_upper_bound,
        visited_patch_pairs,
        boundary_queries,
    )?;
    Ok(CertifiedSurfacePairSplit3::Split(split))
}

fn unresolved_complete(
    traces: Vec<axiolid_nurbs::TransverseSurfaceSurfaceTrace3>,
    visited_patch_pairs: u32,
    boundary_queries: u8,
    reason: SurfacePairSplitUnresolvedReason,
) -> CertifiedSurfacePairSplit3 {
    CertifiedSurfacePairSplit3::Unresolved {
        intersection: CertifiedSurfaceSurfaceIntersection3::Complete {
            traces,
            visited_patch_pairs,
            boundary_queries,
        },
        reason,
    }
}