Skip to main content

brep_kernel/construction/
loft_guides.rs

1//! A loft between two sections along guide curves through their corners.
2//!
3//! Each wall is the patch its four edges bound: a side of the first section,
4//! the two guides from its ends, and the matching side of the last section,
5//! filled by the bilinearly blended Coons patch ([`crate::coons_bilinear`]).
6//! Where the two sides are straight, that patch is the surface ruled between
7//! the two guides at equal parameters. A side of no length (the last section
8//! closing on a point) makes its wall three-sided, closed on a pole. The walls
9//! and the two flat ends are faces of their own, joined into the solid they
10//! bound ([`crate::solid_from_sheets`]).
11use crate::{coons_bilinear, face_from_surface, face_from_surface_trimmed, make_plane, solid_from_sheets, BrepSolid, KernelRefusal, KernelStage, NurbsCurve, OrRefuse, Vec3};
12
13/// The prefix of every refusal of this operation.
14pub const LOFT_ON_GUIDES_REFUSAL: &str = "loftOnGuides";
15
16fn input(what: &'static str, message: String) -> KernelRefusal {
17    KernelRefusal::input(KernelStage::Collect, what, format!("{LOFT_ON_GUIDES_REFUSAL}: {message}"))
18}
19
20fn ends(curve: &NurbsCurve) -> Result<[Vec3; 2], KernelRefusal> {
21    let [t0, t1] = curve.domain().or_refuse(KernelStage::Collect, "loftOnGuides.domain")?;
22    Ok([curve.evaluate(t0).or_refuse(KernelStage::Collect, "loftOnGuides.end")?, curve.evaluate(t1).or_refuse(KernelStage::Collect, "loftOnGuides.end")?])
23}
24
25fn poles(curves: &[NurbsCurve]) -> Vec<Vec3> {
26    curves.iter().flat_map(|curve| &curve.control_points).map(|pole| Vec3::new(pole.x / pole.w, pole.y / pole.w, pole.z / pole.w)).collect()
27}
28
29/// The flat face inside the closed loop `section`, which must lie in a plane.
30fn flat_end(section: &[NurbsCurve], size: f64, which: &str) -> Result<BrepSolid, KernelRefusal> {
31    let points = poles(section);
32    let origin = points[0];
33    let far = |off: Option<Vec3>| {
34        points.iter().map(|point| point.sub(origin)).map(|arm| off.map_or(arm, |axis| arm.sub(axis.scale(arm.dot(axis))))).fold(Vec3::new(0.0, 0.0, 0.0), |best, arm| if arm.length() > best.length() { arm } else { best })
35    };
36    let flat = || input("section_plane", format!("the {which} section does not lie in a plane"));
37    let e1 = far(None).normalized().map_err(|_| flat())?;
38    let across = far(Some(e1));
39    if across.length() <= 1e-9 * size {
40        return Err(flat());
41    }
42    let e2 = across.normalized().map_err(|_| flat())?;
43    let normal = e1.cross(e2);
44    if points.iter().any(|point| point.sub(origin).dot(normal).abs() > 1e-9 * size) {
45        return Err(flat());
46    }
47    let reach = |axis: Vec3| points.iter().map(|point| point.sub(origin).dot(axis)).fold((0.0f64, 0.0f64), |(low, high), at| (low.min(at), high.max(at)));
48    let ((low1, high1), (low2, high2)) = (reach(e1), reach(e2));
49    let corner = origin.add(e1.scale(low1 - 0.5 * size)).add(e2.scale(low2 - 0.5 * size));
50    let plane = make_plane(corner, e1, e2, high1 - low1 + size, high2 - low2 + size).or_refuse(KernelStage::Collect, "loftOnGuides.plane")?;
51    face_from_surface_trimmed(&plane, &[section.to_vec()])
52}
53
54/// The wall standing on the straight side `side` between two guides that meet
55/// at one point: the surface ruled between the guides at equal parameters,
56/// closed on a pole where they meet. (The Coons patch of a straight side, two
57/// guides and a point is that ruled surface.)
58fn pointed_wall(side: &NurbsCurve, rail: &NurbsCurve, next: &NurbsCurve, k: usize, tolerance: f64) -> Result<crate::NurbsSurface, KernelRefusal> {
59    let [from, to] = ends(side)?;
60    let straight = poles(std::slice::from_ref(side)).iter().all(|point| point.sub(from).cross(to.sub(from)).length() <= tolerance * to.sub(from).length());
61    if !straight {
62        return Err(KernelRefusal::unsupported(KernelStage::Fragment, "pointed_wall_side", format!("{LOFT_ON_GUIDES_REFUSAL}: wall {k} closes on a point from a side that is not straight")));
63    }
64    if rail.degree != next.degree || rail.knots != next.knots {
65        return Err(KernelRefusal::unsupported(KernelStage::Fragment, "pointed_wall_guides", format!("{LOFT_ON_GUIDES_REFUSAL}: wall {k} closes on a point between guides of different degree or knots")));
66    }
67    let net = vec![rail.control_points.clone(), next.control_points.clone()];
68    crate::NurbsSurface::new(1, rail.degree, vec![0.0, 0.0, 1.0, 1.0], rail.knots.clone(), net).or_refuse(KernelStage::Fragment, "loftOnGuides.pointed_wall")
69}
70
71/// The solid lofted from the closed section `first` to the closed section
72/// `last` along `guides`.
73///
74/// The two sections have the same number of curves, each a closed loop run the
75/// same way round, curve `k` of the first matched with curve `k` of the last;
76/// each lies in a plane. `guides[k]` runs from the start of `first[k]` to the
77/// start of `last[k]` (given in either direction). A curve of `last` may have
78/// no length: where every one of them is the same point the loft closes on
79/// that point and has no last end.
80///
81/// Refuses sections of different counts, guides that do not join the matched
82/// corners, sections off a plane, and walls the Coons patch refuses.
83pub fn loft_profile_brep_on_guides(first: &[NurbsCurve], last: &[NurbsCurve], guides: &[NurbsCurve]) -> Result<BrepSolid, KernelRefusal> {
84    let count = first.len();
85    if count < 2 || last.len() != count || guides.len() != count {
86        return Err(input("counts", format!("the sections and the guides must be equally many, at least 2 (got {count}, {} and {})", last.len(), guides.len())));
87    }
88    let all = poles(&[first, last, guides].concat());
89    let size = all.iter().map(|point| point.sub(all[0]).length()).fold(0.0, f64::max);
90    let tolerance = 1e-9 * size;
91    let near = |a: Vec3, b: Vec3| a.sub(b).length() <= tolerance;
92    // each guide turned to run from the first section to the last
93    let mut rails = Vec::with_capacity(count);
94    for k in 0..count {
95        let ([foot, _], [head, _]) = (ends(&first[k])?, ends(&last[k])?);
96        let [from, to] = ends(&guides[k])?;
97        let rail = if near(from, foot) && near(to, head) {
98            guides[k].clone()
99        } else if near(to, foot) && near(from, head) {
100            guides[k].reversed().or_refuse(KernelStage::Collect, "loftOnGuides.reverse")?
101        } else {
102            return Err(input("guide_ends", format!("guide {k} does not join the start of curve {k} of the first section to that of the last")));
103        };
104        rails.push(rail);
105    }
106    let mut sheets = Vec::with_capacity(count + 2);
107    for k in 0..count {
108        let next = (k + 1) % count;
109        let far_side = last[k].reversed().or_refuse(KernelStage::Collect, "loftOnGuides.reverse")?;
110        let back = rails[k].reversed().or_refuse(KernelStage::Collect, "loftOnGuides.reverse")?;
111        let [head, head_next] = ends(&last[k])?;
112        let patch = if near(head, head_next) && poles(&last[k..=k]).iter().all(|point| near(*point, head)) {
113            pointed_wall(&first[k], &rails[k], &rails[next], k, tolerance)?
114        } else {
115            coons_bilinear([first[k].clone(), rails[next].clone(), far_side, back])
116                .map_err(|error| KernelRefusal::unsupported(KernelStage::Fragment, "wall", format!("{LOFT_ON_GUIDES_REFUSAL}: wall {k} has no Coons patch: {error:?}")))?
117        };
118        sheets.push(face_from_surface(&patch)?);
119    }
120    sheets.push(flat_end(first, size, "first")?);
121    let apex = ends(&last[0])?[0];
122    let pointed = poles(last).iter().all(|point| near(*point, apex));
123    if !pointed {
124        sheets.push(flat_end(last, size, "last")?);
125    }
126    // sewing reads each edge's place by projection, whose own error on a cubic is near 1e-8
127    solid_from_sheets(&sheets, (1e-8 * size).max(1e-7))
128}
129