ogeom-offset 0.9.12

Offsetting, shelling, sweeping, lofting and draft
Documentation
//! Surface filling: a face fitted over the region a loop of edges bounds.
//!
//! [`make_filling`] takes four edges and fits the transfinite Coons blend
//! of them (which interpolates them exactly), fitted at the blend's own
//! parameters and measured between the samples, error reported. What the
//! caller gets is a *natural*
//! face over the fitted patch: the patch's own chart rectangle is the trim,
//! and the patch boundary stands within the stated fit tolerance of the
//! edges it was asked to fill.
//!
//! [`make_filling_n`] takes any number of sides, each meeting the face it
//! came from at G0, G1 or G2, and points and curves inside the hole the
//! surface must pass through. Its face is bounded by the given edges
//! themselves, each carrying a pcurve on the fitted patch, so sewing it to
//! the faces around it finds the edges already shared. The patch is a
//! height field over the plane the boundary loop spans, or where the loop
//! or its supports turn square to that plane a patch free in all three
//! coordinates over a chart drawn from the loop, fitted over a rectangle
//! wider than the hole and trimmed by the edges; every deviation the caller
//! asked about is measured along every side and reported.

use ogeom_algo::{Built, History, make_natural_face};
use ogeom_core::{OgeomResult, Tolerances, ogeom_bail};
use ogeom_geom::{Curve, Curve3d as _, Reversible as _, SurfaceGeometry, TrimmedCurve};
use ogeom_topo::{EdgeRepr, Model, Shape, ShapeType};

pub use crate::fill_n::{FillBoundary, FillSide, Filled, make_filling_n};

/// Fill the loop `edges` bound with a fitted patch face.
///
/// The four edges must chain head to tail into a closed loop, in order;
/// the first runs along the patch's `u` direction. `samples` is the Coons
/// sampling per direction the fit starts from and `tolerance` the fit
/// target, measured between the samples as well as at them and the
/// sampling refined where it misses; the fit that cannot meet it refuses
/// with the error it reached.
///
/// # Errors
///
/// [`OgeomError::Construction`](ogeom_core::OgeomError::Construction) if the
/// edges do not chain into a loop, an edge carries no curve, or the fit
/// misses the tolerance.
pub fn make_filling(
    model: &mut Model,
    edges: &[Shape; 4],
    samples: usize,
    tolerance: f64,
    tol: Tolerances,
) -> OgeomResult<Built> {
    let mut curves: Vec<Curve> = Vec::with_capacity(4);
    for edge in edges {
        if model.kind_of(edge)? != ShapeType::Edge {
            ogeom_bail!(Construction, "a filling is bounded by edges");
        }
        let Some(data) = model.node(edge).and_then(|n| n.data().as_edge()) else {
            ogeom_bail!(Construction, "edge holds no edge data");
        };
        let Some(EdgeRepr::Curve3d { curve, range, .. }) = data.curve3d() else {
            ogeom_bail!(Construction, "a filling edge needs a 3D curve");
        };
        let Some(geometry) = model.geometry().curve(*curve) else {
            ogeom_bail!(Construction, "edge refers to a curve not in this model");
        };
        curves.push(Curve::Trimmed(Box::new(TrimmedCurve::new(
            geometry.clone(),
            range.0,
            range.1,
            tol,
        )?)));
    }

    // Chain head to tail, reversing edges whose stored direction runs
    // against the loop.
    let slack = tol.confusion() * 1e3;
    let start = curves[0].start(tol)?;
    let mut cursor = curves[0].end(tol)?;
    for curve in curves.iter_mut().skip(1) {
        if curve.start(tol)?.distance(cursor) > slack {
            if curve.end(tol)?.distance(cursor) > slack {
                ogeom_bail!(
                    Construction,
                    "the edges do not chain into a loop; a gap of {} remains",
                    curve
                        .start(tol)?
                        .distance(cursor)
                        .min(curve.end(tol)?.distance(cursor))
                );
            }
            *curve = curve.reversed();
        }
        cursor = curve.end(tol)?;
    }
    if cursor.distance(start) > slack {
        ogeom_bail!(
            Construction,
            "the loop does not close; a gap of {} remains",
            cursor.distance(start)
        );
    }

    // Loop order to Coons orientation: bottom with u, right with v, top
    // and left reversed back into the same directions.
    let bottom = curves[0].clone();
    let right = curves[1].clone();
    let top = curves[2].reversed();
    let left = curves[3].reversed();
    let fitted =
        ogeom_geom::fit::fill_boundary(&bottom, &top, &left, &right, samples, tolerance, tol)?;
    if !fitted.met {
        ogeom_bail!(
            NotDone,
            "the filling reached {} against a target of {tolerance}",
            fitted.error
        );
    }

    let built = make_natural_face(model, SurfaceGeometry::BSpline(fitted.curve))?;
    let mut history = History::new();
    for edge in edges {
        history.modify(edge, built.shape.clone());
    }
    Ok(Built::new(built.shape, history))
}