geop_core_geometry/nurb_curve/
evaluate.rs1use geop_core_math::{
2 geop_error::{GeopError, GeopResult, WithContext},
3 scalars::Scalar,
4 vector::{Vector, Vector2, Vector3},
5};
6
7use super::NurbCurve;
8
9impl<S: Scalar, const D: usize> NurbCurve<S, D> {
10 pub(super) fn find_knot_span(&self, t: S) -> GeopResult<usize> {
12 let n = self.control_points.len() - 1;
13 let p = self.degree;
14 let u = &self.knot_vector;
15
16 if t.definitely_less(u[p]) || t.definitely_greater(u[n + 1]) {
17 return Err(GeopError::new(format!(
18 "parameter t={} out of domain [{}, {}]",
19 t,
20 u[p],
21 u[n + 1]
22 )));
23 }
24
25 if !t.definitely_less(u[n + 1]) {
26 for k in (p..=n).rev() {
27 if u[k].definitely_less(u[n + 1]) {
28 return Ok(k);
29 }
30 }
31 return Ok(p);
32 }
33
34 for k in p..=n {
35 if !t.definitely_less(u[k]) && t.definitely_less(u[k + 1]) {
36 return Ok(k);
37 }
38 }
39
40 Err(GeopError::new("could not find knot span"))
41 }
42
43 pub(super) fn de_boor(&self, t: S, span: usize) -> Vector<S, D> {
45 let p = self.degree;
46 let u = &self.knot_vector;
47 let mut d: Vec<Vector<S, D>> = (0..=p).map(|j| self.control_points[span - p + j]).collect();
48
49 for r in 1..=p {
50 for j in (r..=p).rev() {
51 let i = span - p + j;
52 let denom = u[i + p - r + 1].sub(u[i]);
53 let alpha = if denom.could_be_equal(S::ZERO) {
54 S::ZERO
55 } else {
56 t.sub(u[i]).div(denom).unwrap_or(S::ZERO)
57 };
58 d[j] = Vector::interpolate(&d[j - 1], &d[j], alpha);
59 }
60 }
61 d[p]
62 }
63}
64
65impl<S: Scalar> NurbCurve<S, 4> {
68 pub fn evaluate(&self, t: S) -> GeopResult<Vector3<S>> {
70 let span = self.find_knot_span(t)?;
71 let hw = self.de_boor(t, span);
72 let w = hw[3];
73 if w.could_be_equal(S::ZERO) {
74 return Err(GeopError::new(format!(
75 "weight is zero at evaluation point (t={t:?}, span={span}, homogeneous de_boor result w={w:?}, degree={}, knot_vector={:?}, control_points={:?})",
76 self.degree, self.knot_vector, self.control_points
77 )));
78 }
79 let inv_w = S::ONE.div(w).with_context(&|e: GeopError| {
80 e.with_context(format!(
81 "NurbCurve::evaluate(t={t}): degree={}, knot_vector={:?}, control_points={:?}",
82 self.degree, self.knot_vector, self.control_points
83 ))
84 })?;
85 let mut result = Vector3::zero();
86 for c in 0..3 {
87 result[c] = hw[c].mul(inv_w);
88 }
89 Ok(result)
90 }
91}
92
93impl<S: Scalar> geop_core_math::primitives::scene::RasterizableCurve<S> for NurbCurve<S, 4> {
94 fn eval_at(&self, t: S) -> GeopResult<Vector3<S>> {
95 self.evaluate(t)
96 }
97}
98
99impl<S: Scalar> NurbCurve<S, 3> {
102 pub fn evaluate(&self, t: S) -> GeopResult<Vector2<S>> {
104 let span = self.find_knot_span(t)?;
105 let hw = self.de_boor(t, span);
106 let w = hw[2];
107 if w.could_be_equal(S::ZERO) {
108 return Err(GeopError::new(format!(
109 "weight is zero at evaluation point (t={t:?}, span={span}, homogeneous de_boor result w={w:?}, degree={}, knot_vector={:?}, control_points={:?})",
110 self.degree, self.knot_vector, self.control_points
111 )));
112 }
113 let inv_w = S::ONE.div(w).with_context(&|e: GeopError| {
114 e.with_context(format!(
115 "NurbCurve::evaluate(t={t}): degree={}, knot_vector={:?}, control_points={:?}",
116 self.degree, self.knot_vector, self.control_points
117 ))
118 })?;
119 let mut result = Vector2::zero();
120 for c in 0..2 {
121 result[c] = hw[c].mul(inv_w);
122 }
123 Ok(result)
124 }
125}
126
127#[cfg(test)]
128mod tests {
129 use crate::nurb_curve::NurbCurve;
130 use geop_core_math::for_all_scalars;
131 use geop_core_math::{scalars::Scalar, vector::Vector4};
132
133 fn pt<S: Scalar>(x: f64, y: f64, z: f64, w: f64) -> Vector4<S> {
134 Vector4::from_array([
135 S::from_f64(x),
136 S::from_f64(y),
137 S::from_f64(z),
138 S::from_f64(w),
139 ])
140 }
141
142 fn line_curve<S: Scalar>() -> NurbCurve<S, 4> {
143 NurbCurve::try_new(
144 1,
145 vec![pt(0.0, 0.0, 0.0, 1.0), pt(1.0, 0.0, 0.0, 1.0)],
146 vec![
147 S::from_f64(0.0),
148 S::from_f64(0.0),
149 S::from_f64(1.0),
150 S::from_f64(1.0),
151 ],
152 )
153 .unwrap()
154 }
155
156 fn check_line_at_start<S: Scalar>() {
157 let c = line_curve::<S>();
158 let p = c.evaluate(S::ZERO).unwrap();
159 assert!(p[0].could_be_equal(S::ZERO));
160 assert!(p[1].could_be_equal(S::ZERO));
161 }
162 #[test]
163 fn line_at_start() {
164 for_all_scalars!(check_line_at_start);
165 }
166
167 fn check_line_at_end<S: Scalar>() {
168 let c = line_curve::<S>();
169 let p = c.evaluate(S::ONE).unwrap();
170 assert!(p[0].could_be_equal(S::ONE));
171 assert!(p[1].could_be_equal(S::ZERO));
172 }
173 #[test]
174 fn line_at_end() {
175 for_all_scalars!(check_line_at_end);
176 }
177
178 fn check_line_at_midpoint<S: Scalar>() {
179 let c = line_curve::<S>();
180 let p = c.evaluate(S::from_f64(0.5)).unwrap();
181 assert!(p[0].could_be_equal(S::from_f64(0.5)));
182 }
183 #[test]
184 fn line_at_midpoint() {
185 for_all_scalars!(check_line_at_midpoint);
186 }
187
188 fn check_out_of_domain_returns_err<S: Scalar>() {
189 let c = line_curve::<S>();
190 assert!(c.evaluate(S::from_f64(-0.1)).is_err());
191 assert!(c.evaluate(S::from_f64(1.1)).is_err());
192 }
193 #[test]
194 fn out_of_domain_returns_err() {
195 for_all_scalars!(check_out_of_domain_returns_err);
196 }
197
198 fn check_quadratic_midpoint<S: Scalar>() {
199 let curve = NurbCurve::try_new(
200 2,
201 vec![
202 pt(0.0, 0.0, 0.0, 1.0),
203 pt(0.5, 0.0, 0.0, 1.0),
204 pt(1.0, 0.0, 0.0, 1.0),
205 ],
206 vec![
207 S::from_f64(0.0),
208 S::from_f64(0.0),
209 S::from_f64(0.0),
210 S::from_f64(1.0),
211 S::from_f64(1.0),
212 S::from_f64(1.0),
213 ],
214 )
215 .unwrap();
216 let p = curve.evaluate(S::from_f64(0.5)).unwrap();
217 assert!(p[0].could_be_equal(S::from_f64(0.5)));
218 assert!(p[1].could_be_equal(S::ZERO));
219 }
220 #[test]
221 fn quadratic_midpoint() {
222 for_all_scalars!(check_quadratic_midpoint);
223 }
224
225 fn check_weight_one_constant_line_does_not_report_zero_weight<S: Scalar>() {
232 let curve = NurbCurve::try_new(
233 1,
234 vec![pt(0.5, 0.5, 0.5, 1.0), pt(0.5, 0.5, -0.5, 1.0)],
235 vec![
236 S::from_f64(0.0),
237 S::from_f64(0.0),
238 S::from_f64(1.0),
239 S::from_f64(1.0),
240 ],
241 )
242 .unwrap();
243 let p = curve.evaluate(S::from_f64(0.972)).unwrap();
244 assert!(
245 p[2].could_be_equal(S::from_f64(0.5 - 1.0 * 0.972)),
246 "p={p:?}"
247 );
248 }
249 #[test]
250 fn weight_one_constant_line_does_not_report_zero_weight() {
251 for_all_scalars!(check_weight_one_constant_line_does_not_report_zero_weight);
252 }
253
254 fn check_everything_matches_any_point<S: Scalar>() {
255 let c = NurbCurve::<S, 4>::everything();
256 let p = c.evaluate(S::from_f64(7.0)).unwrap();
257 assert!(p[0].could_be_equal(S::from_f64(123.456)));
258 assert!(p[1].could_be_equal(S::from_f64(-9.0)));
259 assert!(p[2].could_be_equal(S::ZERO));
260 }
261 #[test]
262 fn everything_matches_any_point() {
263 for_all_scalars!(check_everything_matches_any_point);
264 }
265}