Skip to main content

geop_ops_model/
curves.rs

1//! [`Chain`]: curves each on `[0, 1]` and starting where the one before
2//! ends, with a stable name for every curve and every joint — what a
3//! profile, a path, a rail or a guide is, however it was drawn or picked.
4
5use geop_core_geometry::nurb_curve::{NurbCurve, NurbCurve2D, NurbCurve3D};
6use geop_core_math::{
7    geop_error::{GeopError, GeopResult},
8    primitives::CoordinateSystem,
9    scalars::Scalar,
10    vector::{Vector2, Vector3},
11};
12
13/// A chain of curves, with a stable name for every curve and every joint
14/// between them — what extrude, revolve and the sweeps build the names of
15/// what they make from (see the crate docs). For a sketch these are its
16/// element ids; for a shape built in code, positions in the chain
17/// ([`Chain::closed`]).
18///
19/// `joint_names[i]` names the joint where `curves[i]` starts. A closed loop
20/// has one joint per curve; an open chain one more, its end.
21#[derive(Clone, Debug)]
22pub struct Chain<C> {
23    /// What the chain goes by: its sketch's name, or its edge's; empty for
24    /// one built in code.
25    pub name: String,
26    pub curves: Vec<C>,
27    pub curve_names: Vec<String>,
28    pub joint_names: Vec<String>,
29}
30
31/// A chain in a plane: the profile extrude and revolve sweep.
32pub type Profile = Chain<NurbCurve2D>;
33
34/// A curve a [`Chain`] is made of: a clamped NURBS curve, in a plane or in
35/// space, which starts at its first control point and ends at its last
36/// (see [`NurbCurve::start`]).
37pub trait ChainCurve: Clone {
38    type Point;
39
40    /// The point the curve starts at.
41    fn start(&self) -> GeopResult<Self::Point>;
42
43    /// The point the curve ends at.
44    fn end(&self) -> GeopResult<Self::Point>;
45}
46
47impl ChainCurve for NurbCurve2D {
48    type Point = Vector2;
49
50    fn start(&self) -> GeopResult<Vector2> {
51        NurbCurve2D::start(self)
52    }
53
54    fn end(&self) -> GeopResult<Vector2> {
55        NurbCurve2D::end(self)
56    }
57}
58
59impl ChainCurve for NurbCurve3D {
60    type Point = Vector3;
61
62    fn start(&self) -> GeopResult<Vector3> {
63        NurbCurve3D::start(self)
64    }
65
66    fn end(&self) -> GeopResult<Vector3> {
67        NurbCurve3D::end(self)
68    }
69}
70
71impl<const D: usize> Chain<NurbCurve<D>>
72where
73    NurbCurve<D>: ChainCurve,
74{
75    /// A closed loop whose curves are called `c0, c1, ...` and whose joints
76    /// `p0, p1, ...`, `p0` where `c0` starts.
77    pub fn closed(curves: Vec<NurbCurve<D>>) -> Self {
78        let n = curves.len();
79        Self::numbered(curves, n)
80    }
81
82    /// An open chain, named like [`Chain::closed`] plus its end joint.
83    pub fn open(curves: Vec<NurbCurve<D>>) -> Self {
84        let n = curves.len();
85        Self::numbered(curves, n + 1)
86    }
87
88    fn numbered(curves: Vec<NurbCurve<D>>, joints: usize) -> Self {
89        Self {
90            name: String::new(),
91            curve_names: (0..curves.len()).map(|i| format!("c{i}")).collect(),
92            joint_names: (0..joints).map(|i| format!("p{i}")).collect(),
93            curves,
94        }
95    }
96
97    /// The same chain with `prefix` put in front of every name — to keep
98    /// several [`Chain::closed`] loops of one extrude apart.
99    pub fn with_prefix(mut self, prefix: &str) -> Self {
100        for name in self.curve_names.iter_mut().chain(&mut self.joint_names) {
101            *name = format!("{prefix}{name}");
102        }
103        self
104    }
105
106    pub fn is_closed(&self) -> bool {
107        self.joint_names.len() == self.curves.len()
108    }
109
110    /// Checks that there is one name per curve and one per joint.
111    pub fn check_names(&self) -> GeopResult<()> {
112        let n = self.curves.len();
113        if self.curve_names.len() != n
114            || !(self.joint_names.len() == n || self.joint_names.len() == n + 1)
115        {
116            return Err(GeopError::new(format!(
117                "chain of {n} curves has {} curve names and {} joint names",
118                self.curve_names.len(),
119                self.joint_names.len()
120            )));
121        }
122        Ok(())
123    }
124
125    /// The same chain traversed the other way; every name stays with its
126    /// curve or joint.
127    pub fn reversed(&self) -> Self {
128        let n = self.curves.len();
129        let joint_names = if self.is_closed() {
130            // Reversed curve `m` is old curve `n - 1 - m`, which now starts
131            // where it used to end: at old joint `n - m`.
132            (0..n)
133                .map(|m| self.joint_names[(n - m) % n].clone())
134                .collect()
135        } else {
136            self.joint_names.iter().rev().cloned().collect()
137        };
138        Self {
139            name: self.name.clone(),
140            curves: self.curves.iter().rev().map(|c| c.reverse()).collect(),
141            curve_names: self.curve_names.iter().rev().cloned().collect(),
142            joint_names,
143        }
144    }
145
146    /// The closed chain starting at its joint `k`.
147    pub fn starting_at(&self, k: usize) -> Self {
148        let mut out = self.clone();
149        out.curves.rotate_left(k);
150        out.curve_names.rotate_left(k);
151        out.joint_names.rotate_left(k);
152        out
153    }
154
155    /// The same chain with `f` applied to every curve, keeping the names.
156    pub fn map_curves(&self, f: impl Fn(&NurbCurve<D>) -> NurbCurve<D>) -> Self {
157        Self {
158            curves: self.curves.iter().map(f).collect(),
159            ..self.clone()
160        }
161    }
162
163    /// Joint `i`, where it is.
164    pub fn joint(&self, i: usize) -> GeopResult<<NurbCurve<D> as ChainCurve>::Point> {
165        if i < self.curves.len() {
166            self.curves[i].start()
167        } else {
168            self.curves[i - 1].end()
169        }
170    }
171
172    /// The joints, where they are.
173    pub fn joints(&self) -> GeopResult<Vec<<NurbCurve<D> as ChainCurve>::Point>> {
174        (0..self.joint_names.len()).map(|i| self.joint(i)).collect()
175    }
176}
177
178impl Chain<NurbCurve2D> {
179    /// The joints of the closed chain where its tangent turns — the corners
180    /// a designer sees — as indices into `joint_names`, in order. A joint is
181    /// smooth where the curve arriving and the curve leaving could have one
182    /// tangent direction: their tangents' cross product could be zero and
183    /// their dot product is definitely positive. Everything else is a
184    /// corner: a turn, a cusp, or a tangent that could vanish. The split of
185    /// an arc at a quarter turn, or a sketch point where two curves meet
186    /// tangentially, is no corner.
187    pub fn corners(&self) -> GeopResult<Vec<usize>> {
188        let n = self.curves.len();
189        let mut corners = Vec::new();
190        for i in 0..n {
191            let before = &self.curves[(i + n - 1) % n];
192            let after = &self.curves[i];
193            let arriving = before.tangent(before.domain().1)?;
194            let leaving = after.tangent(after.domain().0)?;
195            let smooth = arriving.prod_cross(&leaving).could_be_equal(Scalar::ZERO)
196                && arriving.prod_dot(&leaving).definitely_greater(Scalar::ZERO);
197            if !smooth {
198                corners.push(i);
199            }
200        }
201        Ok(corners)
202    }
203}
204
205impl Chain<NurbCurve3D> {
206    /// The open chain run from its end on `plane`: as it is if it starts
207    /// there, the other way round if only its end could lie on it. `None`
208    /// if neither end could.
209    pub fn starting_on(&self, plane: &CoordinateSystem) -> GeopResult<Option<Self>> {
210        let on_plane = |p: &Vector3| plane.to_uvw(p)[2].could_be_equal(Scalar::ZERO);
211        let joints = self.joints()?;
212        Ok(if on_plane(&joints[0]) {
213            Some(self.clone())
214        } else if on_plane(&joints[joints.len() - 1]) {
215            Some(self.reversed())
216        } else {
217            None
218        })
219    }
220}