Skip to main content

ogeom_offset/
fill.rs

1//! Surface filling: a face fitted over the region a loop of edges bounds.
2//!
3//! [`make_filling`] takes four edges and fits the transfinite Coons blend
4//! of them (which interpolates them exactly), fitted at the blend's own
5//! parameters and measured between the samples, error reported. What the
6//! caller gets is a *natural*
7//! face over the fitted patch: the patch's own chart rectangle is the trim,
8//! and the patch boundary stands within the stated fit tolerance of the
9//! edges it was asked to fill.
10//!
11//! [`make_filling_n`] takes any number of sides, each meeting the face it
12//! came from at G0, G1 or G2, and points and curves inside the hole the
13//! surface must pass through. Its face is bounded by the given edges
14//! themselves, each carrying a pcurve on the fitted patch, so sewing it to
15//! the faces around it finds the edges already shared. The patch is a
16//! height field over the plane the boundary loop spans, or where the loop
17//! or its supports turn square to that plane a patch free in all three
18//! coordinates over a chart drawn from the loop, fitted over a rectangle
19//! wider than the hole and trimmed by the edges; every deviation the caller
20//! asked about is measured along every side and reported.
21
22use ogeom_algo::{Built, History, make_natural_face};
23use ogeom_core::{OgeomResult, Tolerances, ogeom_bail};
24use ogeom_geom::{Curve, Curve3d as _, Reversible as _, SurfaceGeometry, TrimmedCurve};
25use ogeom_topo::{Model, Shape, ShapeType};
26
27pub use ogeom_fillet::{FillBoundary, FillSide, Filled, make_filling_n};
28
29/// Fill the loop `edges` bound with a fitted patch face.
30///
31/// The four edges must chain head to tail into a closed loop, in order;
32/// the first runs along the patch's `u` direction. `samples` is the Coons
33/// sampling per direction the fit starts from and `tolerance` the fit
34/// target, measured between the samples as well as at them and the
35/// sampling refined where it misses; the fit that cannot meet it refuses
36/// with the error it reached.
37///
38/// # Errors
39///
40/// [`OgeomError::Construction`](ogeom_core::OgeomError::Construction) if the
41/// edges do not chain into a loop, an edge carries no curve, or the fit
42/// misses the tolerance.
43pub fn make_filling(
44    model: &mut Model,
45    edges: &[Shape; 4],
46    samples: usize,
47    tolerance: f64,
48    tol: Tolerances,
49) -> OgeomResult<Built> {
50    let mut curves: Vec<Curve> = Vec::with_capacity(4);
51    for edge in edges {
52        if model.kind_of(edge)? != ShapeType::Edge {
53            ogeom_bail!(Construction, "a filling is bounded by edges");
54        }
55        let Some((geometry, range)) = ogeom_algo::placed_edge_curve(model, edge, tol)? else {
56            ogeom_bail!(Construction, "a filling edge needs a 3D curve");
57        };
58        curves.push(Curve::Trimmed(Box::new(TrimmedCurve::new(
59            geometry, range.0, range.1, tol,
60        )?)));
61    }
62
63    // Chain head to tail, reversing edges whose stored direction runs
64    // against the loop.
65    let slack = tol.confusion() * 1e3;
66    let start = curves[0].start(tol)?;
67    let mut cursor = curves[0].end(tol)?;
68    for curve in curves.iter_mut().skip(1) {
69        if curve.start(tol)?.distance(cursor) > slack {
70            if curve.end(tol)?.distance(cursor) > slack {
71                ogeom_bail!(
72                    Construction,
73                    "the edges do not chain into a loop; a gap of {} remains",
74                    curve
75                        .start(tol)?
76                        .distance(cursor)
77                        .min(curve.end(tol)?.distance(cursor))
78                );
79            }
80            *curve = curve.reversed();
81        }
82        cursor = curve.end(tol)?;
83    }
84    if cursor.distance(start) > slack {
85        ogeom_bail!(
86            Construction,
87            "the loop does not close; a gap of {} remains",
88            cursor.distance(start)
89        );
90    }
91
92    // Loop order to Coons orientation: bottom with u, right with v, top
93    // and left reversed back into the same directions.
94    let bottom = curves[0].clone();
95    let right = curves[1].clone();
96    let top = curves[2].reversed();
97    let left = curves[3].reversed();
98    let fitted =
99        ogeom_geom::fit::fill_boundary(&bottom, &top, &left, &right, samples, tolerance, tol)?;
100    if !fitted.met {
101        ogeom_bail!(
102            NotDone,
103            "the filling reached {} against a target of {tolerance}",
104            fitted.error
105        );
106    }
107
108    let built = make_natural_face(model, SurfaceGeometry::BSpline(fitted.curve))?;
109    let mut history = History::new();
110    for edge in edges {
111        history.modify(edge, built.shape.clone());
112    }
113    Ok(Built::new(built.shape, history))
114}