Skip to main content

brep_kernel/geometry/
closed_curves.rs

1//! Closed curves beyond the circle: the ellipse and the periodic B-spline, as
2//! the clamped rational curves they are exactly, a closed curve cut into the
3//! two-curve loop a profile is, and the ellipsoid.
4use crate::{make_arc, make_sphere_brep_framed, transform_brep, AffineTransform, BrepSolid, KernelRefusal, KernelStage, NurbsCurve, OrRefuse, Vec3, Vec4};
5
6/// The unit frame of an ellipse: `major` made a unit vector, and `minor_hint`
7/// made square to it in their common plane.
8fn frame(name: &str, major: Vec3, minor_hint: Vec3) -> Result<(Vec3, Vec3), String> {
9    let x = major.normalized().map_err(|_| format!("{name}: the major direction must be non-zero"))?;
10    let y = minor_hint
11        .sub(x.scale(minor_hint.dot(x)))
12        .normalized()
13        .map_err(|_| format!("{name}: the minor direction must not be parallel to the major one"))?;
14    Ok((x, y))
15}
16
17/// The arc of the ellipse about `center` with semi-axis `semi_major` along
18/// `major_direction` and `semi_minor` square to it (toward `minor_hint`), from
19/// the parameter `start_angle` to `end_angle` (radians) of
20/// `P(t) = center + semi_major·cos t·x + semi_minor·sin t·y`.
21///
22/// Exact: an ellipse is a circle's affine image, and an affine map carries a
23/// rational curve's poles and keeps its weights, so this is the unit circle's
24/// arc ([`make_arc`]) with each pole stretched onto the two semi-axes. Either
25/// semi-axis may be the longer.
26pub fn make_ellipse_arc(
27    center: Vec3,
28    major_direction: Vec3,
29    minor_hint: Vec3,
30    semi_major: f64,
31    semi_minor: f64,
32    start_angle: f64,
33    end_angle: f64,
34) -> Result<NurbsCurve, String> {
35    if !(semi_major.is_finite() && semi_minor.is_finite() && semi_major > 0.0 && semi_minor > 0.0) {
36        return Err("makeEllipse: both semi-axes must be positive".into());
37    }
38    let (x, y) = frame("makeEllipse", major_direction, minor_hint)?;
39    let origin = Vec3::new(0.0, 0.0, 0.0);
40    let unit = make_arc(origin, Vec3::new(1.0, 0.0, 0.0), Vec3::new(0.0, 1.0, 0.0), 1.0, start_angle, end_angle)?;
41    let poles = unit
42        .control_points
43        .iter()
44        .map(|pole| {
45            // the pole's own point (x/w, y/w) of the unit circle's plane, stretched and placed
46            let point = center.add(x.scale(semi_major * pole.x / pole.w)).add(y.scale(semi_minor * pole.y / pole.w));
47            Vec4::from_point(point, pole.w)
48        })
49        .collect();
50    NurbsCurve::new(unit.degree, unit.knots.clone(), poles)
51}
52
53/// The whole ellipse of [`make_ellipse_arc`], closed, from its point on the
54/// major direction round toward `minor_hint`.
55pub fn make_ellipse(center: Vec3, major_direction: Vec3, minor_hint: Vec3, semi_major: f64, semi_minor: f64) -> Result<NurbsCurve, String> {
56    make_ellipse_arc(center, major_direction, minor_hint, semi_major, semi_minor, 0.0, std::f64::consts::TAU)
57}
58
59/// The closed, periodic uniform B-spline of `degree` on the control polygon
60/// `control_points` (each given once; the polygon closes from the last to the
61/// first), as a clamped curve over `[0, n]` for `n` control points.
62///
63/// The periodic curve is the B-spline of the polygon wrapped round by
64/// `degree` points on evenly spaced knots, taken over one period. Its two ends
65/// are clamped there by knot insertion, which moves no point of the curve: the
66/// result has the same `n` polynomial spans, joined as smoothly as the
67/// periodic curve joins them (C^(degree−1)), and starts and ends at the same
68/// point with the same derivatives.
69pub fn make_periodic_bspline(degree: usize, control_points: &[Vec3]) -> Result<NurbsCurve, String> {
70    let (p, n) = (degree, control_points.len());
71    if p == 0 {
72        return Err("makePeriodicBspline: the degree must be at least 1".into());
73    }
74    if n < p + 1 {
75        return Err(format!("makePeriodicBspline: degree {p} needs at least {} control points, got {n}", p + 1));
76    }
77    // wrapped: n + p poles on the knots 0, 1, …, n + 2p; one period is [p, n + p]
78    let mut poles: Vec<[f64; 3]> = (0..n + p).map(|index| control_points[index % n]).map(|point| [point.x, point.y, point.z]).collect();
79    let mut knots: Vec<f64> = (0..=n + 2 * p).map(|index| index as f64).collect();
80    for at in [p as f64, (n + p) as f64] {
81        for _ in 1..p {
82            insert_knot(p, &mut knots, &mut poles, at);
83        }
84    }
85    // `at` now stands `p` times at each end: the period's poles run from the one
86    // before its first copy to the one before the end's first copy
87    let first = knots.iter().position(|&knot| knot == p as f64).expect("the start knot");
88    let last = knots.iter().position(|&knot| knot == (n + p) as f64).expect("the end knot");
89    let mut clamped = vec![0.0];
90    clamped.extend(knots[first..last + p].iter().map(|knot| knot - p as f64));
91    clamped.push(n as f64);
92    let poles = poles[first - 1..last].iter().map(|pole| Vec4::from_point(Vec3::new(pole[0], pole[1], pole[2]), 1.0)).collect();
93    NurbsCurve::new(p, clamped, poles)
94}
95
96/// Boehm's knot insertion, once, on a polynomial B-spline's raw knots and
97/// poles (the knots need not be clamped).
98fn insert_knot(degree: usize, knots: &mut Vec<f64>, poles: &mut Vec<[f64; 3]>, at: f64) {
99    // the span k with knots[k] <= at < knots[k + 1]
100    let span = knots.iter().rposition(|&knot| knot <= at).expect("a knot at or before the new one");
101    let mut refined = Vec::with_capacity(poles.len() + 1);
102    for index in 0..=poles.len() {
103        let pole = if index + degree <= span {
104            poles[index]
105        } else if index > span {
106            poles[index - 1]
107        } else {
108            // where the new knot stands on an old one the blend is all `before` (alpha = 0),
109            // and at the last span there is no pole after it to read
110            let alpha = (at - knots[index]) / (knots[index + degree] - knots[index]);
111            let before = poles[index - 1];
112            if alpha > 0.0 {
113                let after = poles[index];
114                [0, 1, 2].map(|axis| (1.0 - alpha) * before[axis] + alpha * after[axis])
115            } else {
116                before
117            }
118        };
119        refined.push(pole);
120    }
121    *poles = refined;
122    knots.insert(span + 1, at);
123}
124
125/// A closed curve as the loop of two curves a profile is: the curve cut at
126/// the middle of its domain. `None` when the curve does not close.
127pub(crate) fn closed_curve_halves(curve: &NurbsCurve) -> Result<Option<Vec<NurbsCurve>>, String> {
128    let [start, end] = curve.domain()?;
129    let (from, to) = (curve.evaluate(start)?, curve.evaluate(end)?);
130    let size = curve
131        .control_points
132        .iter()
133        .map(|pole| Vec3::new(pole.x / pole.w, pole.y / pole.w, pole.z / pole.w).sub(from).length())
134        .fold(0.0, f64::max);
135    if size <= 0.0 || from.sub(to).length() > 1e-9 * size {
136        return Ok(None);
137    }
138    let (first, second) = curve.split(0.5 * (start + end))?;
139    Ok(Some(vec![first, second]))
140}
141
142/// The prism on one closed plane curve, slid by `distance` along `direction`: its
143/// side ONE face closed on a seam (the curve times the slide, as the kernel's
144/// cylinder is its circle times its axis), its two ends flat faces inside the
145/// curve and its copy. `None` when the curve does not close.
146pub(crate) fn extrude_closed_curve(curve: &NurbsCurve, direction: Vec3, distance: f64) -> Result<Option<BrepSolid>, KernelRefusal> {
147    use crate::{face_from_surface, face_from_surface_trimmed, make_plane, solid_from_sheets, NurbsSurface};
148    let collect = KernelStage::Collect;
149    let [start, end] = curve.domain().or_refuse(collect, "extrudeClosed.domain")?;
150    let points: Vec<Vec3> = curve.control_points.iter().map(|pole| Vec3::new(pole.x / pole.w, pole.y / pole.w, pole.z / pole.w)).collect();
151    let origin = curve.evaluate(start).or_refuse(collect, "extrudeClosed.start")?;
152    let size = points.iter().map(|point| point.sub(origin).length()).fold(0.0, f64::max);
153    if size <= 0.0 || origin.sub(curve.evaluate(end).or_refuse(collect, "extrudeClosed.end")?).length() > 1e-9 * size {
154        return Ok(None);
155    }
156    let slide = direction
157        .normalized()
158        .map_err(|_| KernelRefusal::input(collect, "direction", "extrudeSolid: direction must be non-zero"))?
159        .scale(distance);
160    // the curve's plane: through its start, spanned by its two farthest-reaching poles
161    let far = |from: Vec3, off: Option<Vec3>| {
162        points.iter().map(|point| point.sub(from)).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 })
163    };
164    let planar = || KernelRefusal::input(collect, "profile_plane", "extrudeSolid: the closed profile curve does not lie in a plane");
165    let e1 = far(origin, None).normalized().map_err(|_| planar())?;
166    let e2 = far(origin, Some(e1)).normalized().map_err(|_| planar())?;
167    let normal = e1.cross(e2);
168    if points.iter().any(|point| point.sub(origin).dot(normal).abs() > 1e-9 * size) {
169        return Err(planar());
170    }
171    if slide.dot(normal).abs() <= 1e-12 * slide.length() {
172        return Err(KernelRefusal::input(collect, "direction", "extrudeSolid: direction lies in the profile's plane"));
173    }
174    // the side: the curve's poles and their slid copies, the curve's own weights
175    let net = curve.control_points.iter().map(|pole| vec![*pole, Vec4 { x: pole.x + slide.x * pole.w, y: pole.y + slide.y * pole.w, z: pole.z + slide.z * pole.w, w: pole.w }]).collect();
176    let side = NurbsSurface::new(curve.degree, 1, curve.knots.clone(), vec![0.0, 0.0, 1.0, 1.0], net).or_refuse(collect, "extrudeClosed.side")?;
177    // each end's plane, a rectangle about the curve's poles (which hold the curve)
178    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)));
179    let ((low1, high1), (low2, high2)) = (reach(e1), reach(e2));
180    let corner = origin.add(e1.scale(low1 - 0.5 * size)).add(e2.scale(low2 - 0.5 * size));
181    let end_face = |shift: Vec3, v: f64| -> Result<BrepSolid, KernelRefusal> {
182        let plane = make_plane(corner.add(shift), e1, e2, high1 - low1 + size, high2 - low2 + size).or_refuse(collect, "extrudeClosed.plane")?;
183        face_from_surface_trimmed(&plane, &[vec![side.iso_curve_v(v).or_refuse(collect, "extrudeClosed.rim")?]])
184    };
185    let sheets = [face_from_surface(&side)?, end_face(Vec3::new(0.0, 0.0, 0.0), 0.0)?, end_face(slide, 1.0)?];
186    // sewing reads each edge's place by projection, whose own error on a cubic is near 1e-8
187    solid_from_sheets(&sheets, (1e-8 * size.max(slide.length())).max(1e-7)).map(Some)
188}
189
190/// The solid ellipsoid about `center` with semi-axes `semi_axes[0]` along
191/// `x_direction`, `semi_axes[1]` square to it toward `y_hint`, and
192/// `semi_axes[2]` along their cross product.
193///
194/// Exact: the unit ball under the affine map that stretches it onto the three
195/// semi-axes, which carries the ball's rational poles and keeps their weights.
196/// Its one face is closed on a seam, the half ellipse from pole to pole through
197/// the first semi-axis, and its poles are the ends of the third.
198pub fn make_ellipsoid_brep(center: Vec3, x_direction: Vec3, y_hint: Vec3, semi_axes: [f64; 3]) -> Result<BrepSolid, KernelRefusal> {
199    if semi_axes.iter().any(|axis| !(axis.is_finite() && *axis > 0.0)) {
200        return Err(KernelRefusal::input(KernelStage::Collect, "ellipsoid_axes", "makeEllipsoid: the three semi-axes must be positive"));
201    }
202    let (x, y) = frame("makeEllipsoid", x_direction, y_hint).map_err(|message| KernelRefusal::input(KernelStage::Collect, "ellipsoid_frame", message))?;
203    let z = x.cross(y);
204    let origin = Vec3::new(0.0, 0.0, 0.0);
205    let ball = make_sphere_brep_framed(origin, 1.0, Vec3::new(0.0, 0.0, 1.0), Some(Vec3::new(1.0, 0.0, 0.0)))
206        .or_refuse(KernelStage::Collect, "makeEllipsoid.ball")?;
207    let [a, b, c] = semi_axes;
208    let (ex, ey, ez) = (x.scale(a), y.scale(b), z.scale(c));
209    let map = AffineTransform {
210        elements: [ex.x, ey.x, ez.x, center.x, ex.y, ey.y, ez.y, center.y, ex.z, ey.z, ez.z, center.z, 0.0, 0.0, 0.0, 1.0],
211    };
212    transform_brep(&ball, map, false).or_refuse(KernelStage::Collect, "makeEllipsoid.stretch")
213}
214