Skip to main content

ogeom_offset/
fill.rs

1//! Surface filling: a face fitted over the region four edges bound.
2//!
3//! The construction is the transfinite Coons blend of the four boundary
4//! curves (which interpolates them exactly), sampled and fitted through
5//! the grid machinery, error reported. What the caller gets is a *natural*
6//! face over the fitted patch: the patch's own chart rectangle is the trim,
7//! and the patch boundary stands within the stated fit tolerance of the
8//! edges it was asked to fill.
9
10use ogeom_algo::{Built, History, make_natural_face};
11use ogeom_core::{OgeomResult, Tolerances, ogeom_bail};
12use ogeom_geom::{Curve, Curve3d as _, Reversible as _, SurfaceGeometry, TrimmedCurve};
13use ogeom_topo::{EdgeRepr, Model, Shape, ShapeType};
14
15/// Fill the loop `edges` bound with a fitted patch face.
16///
17/// The four edges must chain head to tail into a closed loop, in order;
18/// the first runs along the patch's `u` direction. `samples` controls the
19/// Coons sampling per direction and `tolerance` the fit target; the fit
20/// that cannot meet it refuses with the error it reached.
21///
22/// # Errors
23///
24/// [`OgeomError::Construction`](ogeom_core::OgeomError::Construction) if the
25/// edges do not chain into a loop, an edge carries no curve, or the fit
26/// misses the tolerance.
27pub fn make_filling(
28    model: &mut Model,
29    edges: &[Shape; 4],
30    samples: usize,
31    tolerance: f64,
32    tol: Tolerances,
33) -> OgeomResult<Built> {
34    let mut curves: Vec<Curve> = Vec::with_capacity(4);
35    for edge in edges {
36        if model.kind_of(edge)? != ShapeType::Edge {
37            ogeom_bail!(Construction, "a filling is bounded by edges");
38        }
39        let Some(data) = model.node(edge).and_then(|n| n.data().as_edge()) else {
40            ogeom_bail!(Construction, "edge holds no edge data");
41        };
42        let Some(EdgeRepr::Curve3d { curve, range, .. }) = data.curve3d() else {
43            ogeom_bail!(Construction, "a filling edge needs a 3D curve");
44        };
45        let Some(geometry) = model.geometry().curve(*curve) else {
46            ogeom_bail!(Construction, "edge refers to a curve not in this model");
47        };
48        curves.push(Curve::Trimmed(Box::new(TrimmedCurve::new(
49            geometry.clone(),
50            range.0,
51            range.1,
52            tol,
53        )?)));
54    }
55
56    // Chain head to tail, reversing edges whose stored direction runs
57    // against the loop.
58    let slack = tol.confusion() * 1e3;
59    let start = curves[0].start(tol)?;
60    let mut cursor = curves[0].end(tol)?;
61    for curve in curves.iter_mut().skip(1) {
62        if curve.start(tol)?.distance(cursor) > slack {
63            if curve.end(tol)?.distance(cursor) > slack {
64                ogeom_bail!(
65                    Construction,
66                    "the edges do not chain into a loop; a gap of {} remains",
67                    curve
68                        .start(tol)?
69                        .distance(cursor)
70                        .min(curve.end(tol)?.distance(cursor))
71                );
72            }
73            *curve = curve.reversed();
74        }
75        cursor = curve.end(tol)?;
76    }
77    if cursor.distance(start) > slack {
78        ogeom_bail!(
79            Construction,
80            "the loop does not close; a gap of {} remains",
81            cursor.distance(start)
82        );
83    }
84
85    // Loop order to Coons orientation: bottom with u, right with v, top
86    // and left reversed back into the same directions.
87    let bottom = curves[0].clone();
88    let right = curves[1].clone();
89    let top = curves[2].reversed();
90    let left = curves[3].reversed();
91    let fitted =
92        ogeom_geom::fit::fill_boundary(&bottom, &top, &left, &right, samples, tolerance, tol)?;
93    if !fitted.met {
94        ogeom_bail!(
95            NotDone,
96            "the filling reached {} against a target of {tolerance}",
97            fitted.error
98        );
99    }
100
101    let built = make_natural_face(model, SurfaceGeometry::BSpline(fitted.curve))?;
102    let mut history = History::new();
103    for edge in edges {
104        history.modify(edge, built.shape.clone());
105    }
106    Ok(Built::new(built.shape, history))
107}