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}