Skip to main content

geop_ops_extrude_revolve/
common.rs

1//! Shared geometry helpers for the constructors in this crate, and
2//! [`Profile`], the named curve chain extrude and revolve sweep. Topology
3//! construction itself goes straight through `Part`'s euler operators
4//! (`mvfs`, `mve`, `mef`, `mer`, `replace_face`, ...) — see `extrude.rs` for
5//! a worked example of how they compose.
6
7use geop_core_geometry::{
8    nurb_curve::{NurbCurve, NurbCurve2D, NurbCurve3D},
9    nurb_surface::{NurbSurface, NurbSurface3D},
10};
11use geop_core_math::{
12    geop_error::{GeopError, GeopResult},
13    scalars::Scalar,
14    vector::{Vector2, Vector3, Vector4},
15};
16
17// ── Profiles ────────────────────────────────────────────────────────────────
18
19/// A chain of profile curves, with a stable name for every curve and every
20/// joint between them — what extrude and revolve build the names of the
21/// faces, edges and vertices they sweep out of it from (see
22/// `geop_core_part`'s crate docs). For a sketch these are its element ids;
23/// for a shape built in code, positions in the chain ([`Profile::closed`]).
24///
25/// `joint_names[i]` names the joint where `curves[i]` starts. A closed loop
26/// has one joint per curve; an open chain one more, its end.
27#[derive(Clone, Debug)]
28pub struct Profile<S: Scalar> {
29    pub curves: Vec<NurbCurve2D<S>>,
30    pub curve_names: Vec<String>,
31    pub joint_names: Vec<String>,
32}
33
34impl<S: Scalar> Profile<S> {
35    /// A closed loop whose curves are called `c0, c1, ...` and whose joints
36    /// `p0, p1, ...`, `p0` where `c0` starts.
37    pub fn closed(curves: Vec<NurbCurve2D<S>>) -> Self {
38        let n = curves.len();
39        Self::numbered(curves, n)
40    }
41
42    /// An open chain, named like [`Profile::closed`] plus its end joint.
43    pub fn open(curves: Vec<NurbCurve2D<S>>) -> Self {
44        let n = curves.len();
45        Self::numbered(curves, n + 1)
46    }
47
48    fn numbered(curves: Vec<NurbCurve2D<S>>, joints: usize) -> Self {
49        Self {
50            curve_names: (0..curves.len()).map(|i| format!("c{i}")).collect(),
51            joint_names: (0..joints).map(|i| format!("p{i}")).collect(),
52            curves,
53        }
54    }
55
56    /// The same chain with `prefix` put in front of every name — to keep
57    /// several [`Profile::closed`] loops of one extrude apart.
58    pub fn with_prefix(mut self, prefix: &str) -> Self {
59        for name in self.curve_names.iter_mut().chain(&mut self.joint_names) {
60            *name = format!("{prefix}{name}");
61        }
62        self
63    }
64
65    pub fn is_closed(&self) -> bool {
66        self.joint_names.len() == self.curves.len()
67    }
68
69    /// Checks that there is one name per curve and one per joint.
70    pub fn check_names(&self) -> GeopResult<()> {
71        let n = self.curves.len();
72        if self.curve_names.len() != n
73            || !(self.joint_names.len() == n || self.joint_names.len() == n + 1)
74        {
75            return Err(GeopError::new(format!(
76                "profile of {n} curves has {} curve names and {} joint names",
77                self.curve_names.len(),
78                self.joint_names.len()
79            )));
80        }
81        Ok(())
82    }
83
84    /// The same chain traversed the other way; every name stays with its
85    /// curve or joint.
86    pub fn reversed(&self) -> Self {
87        let n = self.curves.len();
88        let joint_names = if self.is_closed() {
89            // Reversed curve `m` is old curve `n - 1 - m`, which now starts
90            // where it used to end: at old joint `n - m`.
91            (0..n)
92                .map(|m| self.joint_names[(n - m) % n].clone())
93                .collect()
94        } else {
95            self.joint_names.iter().rev().cloned().collect()
96        };
97        Self {
98            curves: self.curves.iter().rev().map(|c| c.reverse()).collect(),
99            curve_names: self.curve_names.iter().rev().cloned().collect(),
100            joint_names,
101        }
102    }
103
104    /// The same chain with `f` applied to every curve, keeping the names.
105    pub fn map_curves(&self, f: impl Fn(&NurbCurve2D<S>) -> NurbCurve2D<S>) -> Self {
106        Self {
107            curves: self.curves.iter().map(f).collect(),
108            ..self.clone()
109        }
110    }
111}
112
113// ── Geometry helpers ─────────────────────────────────────────────────────────
114
115/// Lift a 3-D point into homogeneous coordinates with weight 1.
116pub fn pt3<S: Scalar>(p: Vector3<S>) -> Vector4<S> {
117    Vector4::from_array([p[0], p[1], p[2], S::ONE])
118}
119
120/// Lift a 2-D point into homogeneous coordinates with weight 1.
121pub fn pt2<S: Scalar>(p: Vector2<S>) -> Vector3<S> {
122    Vector3::from_array([p[0], p[1], S::ONE])
123}
124
125/// A degree-1 line segment in 3-D from `p0` to `p1`, parametrized over `[0, 1]`.
126pub fn line3<S: Scalar>(p0: Vector3<S>, p1: Vector3<S>) -> GeopResult<NurbCurve3D<S>> {
127    NurbCurve::try_new(
128        1,
129        vec![pt3(p0), pt3(p1)],
130        vec![S::ZERO, S::ZERO, S::ONE, S::ONE],
131    )
132}
133
134/// A degree-1 line segment in parameter space from `p0` to `p1`, parametrized over `[0, 1]`.
135pub fn line2<S: Scalar>(p0: Vector2<S>, p1: Vector2<S>) -> GeopResult<NurbCurve2D<S>> {
136    NurbCurve::try_new(
137        1,
138        vec![pt2(p0), pt2(p1)],
139        vec![S::ZERO, S::ZERO, S::ONE, S::ONE],
140    )
141}
142
143/// The closed polygon through `points` (and back to the first), one line
144/// per side — the simplest profile loop for [`crate::extrude::extrude`].
145pub fn polygon<S: Scalar>(points: &[Vector2<S>]) -> GeopResult<Vec<NurbCurve2D<S>>> {
146    (0..points.len())
147        .map(|i| line2(points[i], points[(i + 1) % points.len()]))
148        .collect()
149}
150
151/// The open chain of lines through `points` — the simplest profile for
152/// [`crate::revolve::revolve_at_oriented`].
153pub fn polyline<S: Scalar>(points: &[Vector2<S>]) -> GeopResult<Vec<NurbCurve2D<S>>> {
154    points.windows(2).map(|w| line2(w[0], w[1])).collect()
155}
156
157/// First control point of `curve`, dehomogenized: its start, for the clamped
158/// knot vectors every profile curve has.
159pub fn start_point<S: Scalar>(curve: &NurbCurve2D<S>) -> GeopResult<Vector2<S>> {
160    let cp = curve.control_points[0];
161    Ok(Vector2::from_array([cp[0].div(cp[2])?, cp[1].div(cp[2])?]))
162}
163
164/// Last control point of `curve`, dehomogenized: its end.
165pub fn end_point<S: Scalar>(curve: &NurbCurve2D<S>) -> GeopResult<Vector2<S>> {
166    start_point(&curve.reverse())
167}
168
169/// Homogeneous `(w (origin + x e1 + y e2), w)` for the homogeneous 2-D point
170/// `cp = (w x, w y, w)`: linear in `cp`, so no division.
171pub fn embed_point<S: Scalar>(
172    cp: &Vector3<S>,
173    origin: &Vector3<S>,
174    e1: &Vector3<S>,
175    e2: &Vector3<S>,
176) -> Vector4<S> {
177    let p = origin
178        .prod_scalar(cp[2])
179        .add(&e1.prod_scalar(cp[0]))
180        .add(&e2.prod_scalar(cp[1]));
181    Vector4::from_array([p[0], p[1], p[2], cp[2]])
182}
183
184/// The planar 3-D curve `origin + x e1 + y e2` for `(x, y)` along `curve`.
185/// An affine map of the control points, so it is exact for any NURBS.
186pub fn embed_curve<S: Scalar>(
187    curve: &NurbCurve2D<S>,
188    origin: &Vector3<S>,
189    e1: &Vector3<S>,
190    e2: &Vector3<S>,
191) -> GeopResult<NurbCurve3D<S>> {
192    NurbCurve::try_new(
193        curve.degree,
194        curve
195            .control_points
196            .iter()
197            .map(|cp| embed_point(cp, origin, e1, e2))
198            .collect(),
199        curve.knot_vector.clone(),
200    )
201}
202
203/// A bilinear (degree 1x1) surface patch with corners `P00, P01, P10, P11`
204/// (control points laid out `[P00, P10, P11, P01]`, `num_v = 2`).
205pub fn bilinear<S: Scalar>(
206    p00: Vector3<S>,
207    p10: Vector3<S>,
208    p11: Vector3<S>,
209    p01: Vector3<S>,
210) -> GeopResult<NurbSurface3D<S>> {
211    NurbSurface::try_new(
212        1,
213        1,
214        vec![pt3(p00), pt3(p01), pt3(p10), pt3(p11)],
215        vec![S::ZERO, S::ZERO, S::ONE, S::ONE],
216        vec![S::ZERO, S::ZERO, S::ONE, S::ONE],
217    )
218}
219
220/// `sqrt(2) / 2`, the rational weight that makes a 3-point quadratic Bezier
221/// trace an exact 90-degree circular arc.
222pub fn sqrt2_over_2<S: Scalar>() -> S {
223    S::from_f64(std::f64::consts::SQRT_2 / 2.0)
224}
225
226/// A degree-2 rational Bezier curve in 3-D from `p0` to `p2` through the
227/// (weighted) control point `mid`, with middle weight `w`. For an arc
228/// centered at `c` with `w = sqrt2_over_2()`, pass `mid = p0 + p2 - c` (i.e.
229/// `c + (p0 - c) + (p2 - c)`, the sum of the two radius vectors relative to
230/// the arc's actual center, offset back into absolute coordinates) to trace
231/// an exact 90-degree circular arc of radius `|p0 - c| = |p2 - c|`.
232pub fn arc3<S: Scalar>(
233    p0: Vector3<S>,
234    mid: Vector3<S>,
235    p2: Vector3<S>,
236    w: S,
237) -> GeopResult<NurbCurve3D<S>> {
238    let cp0 = pt3(p0);
239    let cp2 = pt3(p2);
240    let cp1 = Vector4::from_array([mid[0].mul(w), mid[1].mul(w), mid[2].mul(w), w]);
241    NurbCurve::try_new(
242        2,
243        vec![cp0, cp1, cp2],
244        vec![S::ZERO, S::ZERO, S::ZERO, S::ONE, S::ONE, S::ONE],
245    )
246}
247
248/// A degree-2 rational Bezier curve in parameter space from `p0` to `p2`
249/// through the (weighted) control point `mid`, with middle weight `w`.
250pub fn arc2<S: Scalar>(
251    p0: Vector2<S>,
252    mid: Vector2<S>,
253    p2: Vector2<S>,
254    w: S,
255) -> GeopResult<NurbCurve2D<S>> {
256    let cp0 = pt2(p0);
257    let cp2 = pt2(p2);
258    let cp1 = Vector3::from_array([mid[0].mul(w), mid[1].mul(w), w]);
259    NurbCurve::try_new(
260        2,
261        vec![cp0, cp1, cp2],
262        vec![S::ZERO, S::ZERO, S::ZERO, S::ONE, S::ONE, S::ONE],
263    )
264}
265
266#[cfg(test)]
267mod tests {
268    use super::bilinear;
269    use geop_core_math::{for_all_scalars, scalars::Scalar, vector::Vector3};
270
271    fn p<S: Scalar>(x: f64, y: f64, z: f64) -> Vector3<S> {
272        Vector3::from_array([S::from_f64(x), S::from_f64(y), S::from_f64(z)])
273    }
274
275    /// 4 arbitrary (non-degenerate, non-planar-aligned) corners — evaluating
276    /// the resulting bilinear patch at its corners must return them exactly,
277    /// and at an interior `(u, v)` must match the bilinear interpolation
278    /// formula directly.
279    fn check_bilinear_maps_uv_correctly<S: Scalar>() {
280        let p00 = p::<S>(0.3, -1.2, 2.5);
281        let p10 = p::<S>(4.1, 0.7, -0.3);
282        let p11 = p::<S>(2.2, 3.3, 1.1);
283        let p01 = p::<S>(-1.5, 2.0, 0.6);
284
285        let surface = bilinear(p00, p10, p11, p01).unwrap();
286
287        assert!(
288            surface
289                .evaluate(S::ZERO, S::ZERO)
290                .unwrap()
291                .could_be_equal(&p00)
292        );
293        assert!(
294            surface
295                .evaluate(S::ONE, S::ZERO)
296                .unwrap()
297                .could_be_equal(&p10)
298        );
299        assert!(
300            surface
301                .evaluate(S::ONE, S::ONE)
302                .unwrap()
303                .could_be_equal(&p11)
304        );
305        assert!(
306            surface
307                .evaluate(S::ZERO, S::ONE)
308                .unwrap()
309                .could_be_equal(&p01)
310        );
311    }
312    #[test]
313    fn bilinear_maps_uv_correctly() {
314        for_all_scalars!(check_bilinear_maps_uv_correctly);
315    }
316}