axiolid-construct 0.1.0

Solid generation: profiles, lofts, sweeps, revolutions and half-space clipping
Documentation
use axiolid_core::{Point2, Scalar};
use axiolid_nurbs::{
    ParameterInterval, SurfaceSurfaceTraceEndpoint3, TransverseSurfaceSurfaceTrace3,
};
use axiolid_surface::BSplineSurface;

use crate::trimmed_intersection_types::SurfacePairMember;

#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub(super) enum BoundarySide {
    VStart,
    UEnd,
    VEnd,
    UStart,
}

#[derive(Debug, Clone, Copy)]
pub(super) struct Domain2 {
    pub u_start: Scalar,
    pub u_end: Scalar,
    pub v_start: Scalar,
    pub v_end: Scalar,
}

#[derive(Debug, Clone, Copy)]
pub(super) struct Endpoint2 {
    pub uv: Point2,
    pub side: Option<BoundarySide>,
}

#[derive(Debug, Clone, Copy)]
pub(super) struct SplitClassification {
    pub member: SurfacePairMember,
    pub owner_domain: Domain2,
    pub embedded_domain: Domain2,
    pub owner_start: Endpoint2,
    pub owner_end: Endpoint2,
    pub embedded_start: Endpoint2,
    pub embedded_end: Endpoint2,
}

pub(super) fn classify(
    first: &BSplineSurface,
    second: &BSplineSurface,
    trace: &TransverseSurfaceSurfaceTrace3,
) -> Option<SplitClassification> {
    let first_domain = domain(first)?;
    let second_domain = domain(second)?;
    let first_start = endpoint(&trace.start, SurfacePairMember::First, first_domain)?;
    let first_end = endpoint(&trace.end, SurfacePairMember::First, first_domain)?;
    let second_start = endpoint(&trace.start, SurfacePairMember::Second, second_domain)?;
    let second_end = endpoint(&trace.end, SurfacePairMember::Second, second_domain)?;

    let first_owns = owns_chord(first_start, first_end, first_domain);
    let second_owns = owns_chord(second_start, second_end, second_domain);
    let first_embeds = interior(first_start, first_domain) && interior(first_end, first_domain);
    let second_embeds =
        interior(second_start, second_domain) && interior(second_end, second_domain);

    match (first_owns, second_owns, first_embeds, second_embeds) {
        (true, false, false, true) => Some(SplitClassification {
            member: SurfacePairMember::First,
            owner_domain: first_domain,
            embedded_domain: second_domain,
            owner_start: first_start,
            owner_end: first_end,
            embedded_start: second_start,
            embedded_end: second_end,
        }),
        (false, true, true, false) => Some(SplitClassification {
            member: SurfacePairMember::Second,
            owner_domain: second_domain,
            embedded_domain: first_domain,
            owner_start: second_start,
            owner_end: second_end,
            embedded_start: first_start,
            embedded_end: first_end,
        }),
        _ => None,
    }
}

fn owns_chord(start: Endpoint2, end: Endpoint2, domain: Domain2) -> bool {
    matches!((start.side, end.side), (Some(left), Some(right)) if left != right)
        && side_interior(start, domain)
        && side_interior(end, domain)
        && start.uv != end.uv
}

fn side_interior(endpoint: Endpoint2, domain: Domain2) -> bool {
    match endpoint.side {
        Some(BoundarySide::VStart | BoundarySide::VEnd) => {
            endpoint.uv.x > domain.u_start && endpoint.uv.x < domain.u_end
        }
        Some(BoundarySide::UStart | BoundarySide::UEnd) => {
            endpoint.uv.y > domain.v_start && endpoint.uv.y < domain.v_end
        }
        None => false,
    }
}

fn interior(endpoint: Endpoint2, domain: Domain2) -> bool {
    endpoint.side.is_none()
        && endpoint.uv.x > domain.u_start
        && endpoint.uv.x < domain.u_end
        && endpoint.uv.y > domain.v_start
        && endpoint.uv.y < domain.v_end
}

fn domain(surface: &BSplineSurface) -> Option<Domain2> {
    let u_start = *surface.u_knots.first()?;
    let u_end = *surface.u_knots.last()?;
    let v_start = *surface.v_knots.first()?;
    let v_end = *surface.v_knots.last()?;
    let values = [u_start, u_end, v_start, v_end];
    if values.iter().all(|value| value.is_finite()) && u_start < u_end && v_start < v_end {
        Some(Domain2 {
            u_start,
            u_end,
            v_start,
            v_end,
        })
    } else {
        None
    }
}

fn endpoint(
    endpoint: &SurfaceSurfaceTraceEndpoint3,
    member: SurfacePairMember,
    domain: Domain2,
) -> Option<Endpoint2> {
    let parameters = endpoint.parameters;
    let (u, v) = match member {
        SurfacePairMember::First => (parameters.first_u, parameters.first_v),
        SurfacePairMember::Second => (parameters.second_u, parameters.second_v),
    };
    let uv = Point2::new(midpoint(u)?, midpoint(v)?);
    if uv.x < domain.u_start || uv.x > domain.u_end || uv.y < domain.v_start || uv.y > domain.v_end
    {
        return None;
    }
    let mut side = None;
    for candidate in [
        fixed_side(v, domain.v_start, BoundarySide::VStart),
        fixed_side(u, domain.u_end, BoundarySide::UEnd),
        fixed_side(v, domain.v_end, BoundarySide::VEnd),
        fixed_side(u, domain.u_start, BoundarySide::UStart),
    ]
    .into_iter()
    .flatten()
    {
        if side.replace(candidate).is_some() {
            return None;
        }
    }
    Some(Endpoint2 { uv, side })
}

fn fixed_side(
    interval: ParameterInterval,
    boundary: Scalar,
    side: BoundarySide,
) -> Option<BoundarySide> {
    (interval.start == boundary && interval.end == boundary).then_some(side)
}

fn midpoint(interval: ParameterInterval) -> Option<Scalar> {
    if !interval.start.is_finite() || !interval.end.is_finite() || interval.start > interval.end {
        return None;
    }
    let value = interval.start * 0.5 + interval.end * 0.5;
    value.is_finite().then_some(value)
}

pub(super) fn boundary_rank(side: BoundarySide, uv: Point2, domain: Domain2) -> Scalar {
    match side {
        BoundarySide::VStart => (uv.x - domain.u_start) / (domain.u_end - domain.u_start),
        BoundarySide::UEnd => 1.0 + (uv.y - domain.v_start) / (domain.v_end - domain.v_start),
        BoundarySide::VEnd => 2.0 + (domain.u_end - uv.x) / (domain.u_end - domain.u_start),
        BoundarySide::UStart => 3.0 + (domain.v_end - uv.y) / (domain.v_end - domain.v_start),
    }
}