1use crate::MathError;
7use crate::vec::{Point2, Vec2};
8
9#[derive(Debug, Clone)]
13#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
14pub struct Line2D {
15 origin: Point2,
16 direction: Vec2,
17}
18
19impl Line2D {
20 pub fn new(origin: Point2, direction: Vec2) -> Result<Self, MathError> {
25 let len = direction.length();
26 if len < 1e-15 {
27 return Err(MathError::ZeroVector);
28 }
29 Ok(Self {
30 origin,
31 direction: Vec2::new(direction.x() / len, direction.y() / len),
32 })
33 }
34
35 #[must_use]
37 pub fn evaluate(&self, t: f64) -> Point2 {
38 self.origin + self.direction * t
39 }
40
41 #[must_use]
43 pub const fn tangent(&self, _t: f64) -> Vec2 {
44 self.direction
45 }
46
47 #[must_use]
49 pub fn project(&self, point: Point2) -> f64 {
50 let v = point - self.origin;
51 v.dot(self.direction) / self.direction.length_squared()
52 }
53
54 #[must_use]
56 pub fn distance_to_point(&self, point: Point2) -> f64 {
57 let t = self.project(point);
58 let closest = self.evaluate(t);
59 let diff = point - closest;
60 diff.length()
61 }
62
63 #[must_use]
65 pub const fn origin(&self) -> Point2 {
66 self.origin
67 }
68
69 #[must_use]
71 pub const fn direction(&self) -> Vec2 {
72 self.direction
73 }
74}
75
76#[derive(Debug, Clone)]
81#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
82pub struct Circle2D {
83 center: Point2,
84 radius: f64,
85}
86
87impl Circle2D {
88 pub fn new(center: Point2, radius: f64) -> Result<Self, MathError> {
93 if radius <= 0.0 {
94 return Err(MathError::ParameterOutOfRange {
95 value: radius,
96 min: f64::EPSILON,
97 max: f64::MAX,
98 });
99 }
100 Ok(Self { center, radius })
101 }
102
103 #[must_use]
105 pub fn evaluate(&self, t: f64) -> Point2 {
106 self.center + Vec2::new(self.radius * t.cos(), self.radius * t.sin())
107 }
108
109 #[must_use]
111 pub fn tangent(&self, t: f64) -> Vec2 {
112 Vec2::new(-self.radius * t.sin(), self.radius * t.cos())
113 }
114
115 #[must_use]
117 pub fn circumference(&self) -> f64 {
118 2.0 * std::f64::consts::PI * self.radius
119 }
120
121 #[must_use]
123 pub fn project(&self, point: Point2) -> f64 {
124 let v = point - self.center;
125 v.y().atan2(v.x()).rem_euclid(2.0 * std::f64::consts::PI)
126 }
127
128 #[must_use]
130 pub const fn center(&self) -> Point2 {
131 self.center
132 }
133
134 #[must_use]
136 pub const fn radius(&self) -> f64 {
137 self.radius
138 }
139}
140
141#[derive(Debug, Clone)]
146#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
147pub struct Ellipse2D {
148 center: Point2,
149 semi_major: f64,
150 semi_minor: f64,
151 rotation: f64,
152}
153
154impl Ellipse2D {
155 pub fn new(
160 center: Point2,
161 semi_major: f64,
162 semi_minor: f64,
163 rotation: f64,
164 ) -> Result<Self, MathError> {
165 if semi_major <= 0.0 {
166 return Err(MathError::ParameterOutOfRange {
167 value: semi_major,
168 min: f64::EPSILON,
169 max: f64::MAX,
170 });
171 }
172 if semi_minor <= 0.0 {
173 return Err(MathError::ParameterOutOfRange {
174 value: semi_minor,
175 min: f64::EPSILON,
176 max: f64::MAX,
177 });
178 }
179 if semi_minor > semi_major {
180 return Err(MathError::ParameterOutOfRange {
181 value: semi_minor,
182 min: 0.0,
183 max: semi_major,
184 });
185 }
186 Ok(Self {
187 center,
188 semi_major,
189 semi_minor,
190 rotation,
191 })
192 }
193
194 #[must_use]
196 pub fn evaluate(&self, t: f64) -> Point2 {
197 let (sin_r, cos_r) = self.rotation.sin_cos();
198 let x = self.semi_major * t.cos();
199 let y = self.semi_minor * t.sin();
200 self.center + Vec2::new(x.mul_add(cos_r, -(y * sin_r)), x.mul_add(sin_r, y * cos_r))
201 }
202
203 #[must_use]
205 pub fn tangent(&self, t: f64) -> Vec2 {
206 let (sin_r, cos_r) = self.rotation.sin_cos();
207 let dx = -self.semi_major * t.sin();
208 let dy = self.semi_minor * t.cos();
209 Vec2::new(
210 dx.mul_add(cos_r, -(dy * sin_r)),
211 dx.mul_add(sin_r, dy * cos_r),
212 )
213 }
214
215 #[must_use]
217 pub const fn center(&self) -> Point2 {
218 self.center
219 }
220
221 #[must_use]
223 pub const fn semi_major(&self) -> f64 {
224 self.semi_major
225 }
226
227 #[must_use]
229 pub const fn semi_minor(&self) -> f64 {
230 self.semi_minor
231 }
232
233 #[must_use]
235 pub const fn rotation(&self) -> f64 {
236 self.rotation
237 }
238
239 #[must_use]
241 pub fn approximate_circumference(&self) -> f64 {
242 let a = self.semi_major;
243 let b = self.semi_minor;
244 let h = ((a - b) * (a - b)) / ((a + b) * (a + b));
245 std::f64::consts::PI * (a + b) * (1.0 + 3.0 * h / (10.0 + 3.0f64.mul_add(-h, 4.0).sqrt()))
246 }
247}
248
249#[derive(Debug, Clone, PartialEq)]
254#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
255pub struct NurbsCurve2D {
256 degree: usize,
257 knots: Vec<f64>,
258 control_points: Vec<Point2>,
259 weights: Vec<f64>,
260}
261
262impl NurbsCurve2D {
263 pub fn new(
269 degree: usize,
270 knots: Vec<f64>,
271 control_points: Vec<Point2>,
272 weights: Vec<f64>,
273 ) -> Result<Self, MathError> {
274 let n = control_points.len();
275 let expected_knots = n + degree + 1;
276 if knots.len() != expected_knots {
277 return Err(MathError::InvalidKnotVector {
278 expected: expected_knots,
279 got: knots.len(),
280 });
281 }
282 if weights.len() != n {
283 return Err(MathError::InvalidWeights {
284 expected: n,
285 got: weights.len(),
286 });
287 }
288 if n == 0 {
289 return Err(MathError::EmptyInput);
290 }
291 Ok(Self {
292 degree,
293 knots,
294 control_points,
295 weights,
296 })
297 }
298
299 #[must_use]
301 pub fn evaluate(&self, u: f64) -> Point2 {
302 let n = self.control_points.len();
303 let p = self.degree;
304 let span = crate::nurbs::basis::find_span(n, p, u, &self.knots);
305 let mut basis = [0.0f64; crate::nurbs::basis::MAX_STACK_OUTPUT + 1];
306 crate::nurbs::basis::basis_funs_into(span, u, p, &self.knots, &mut basis[..=p]);
307
308 let mut wx = 0.0;
309 let mut wy = 0.0;
310 let mut ww = 0.0;
311
312 for (j, &basis_val) in basis.iter().enumerate().take(p + 1) {
313 let idx = span - p + j;
314 let cp = &self.control_points[idx];
315 let w = self.weights[idx];
316 let bw = basis_val * w;
317 wx += bw * cp.x();
318 wy += bw * cp.y();
319 ww += bw;
320 }
321
322 if ww.abs() < f64::EPSILON {
323 return self.control_points[0];
324 }
325 Point2::new(wx / ww, wy / ww)
326 }
327
328 #[must_use]
330 #[allow(clippy::many_single_char_names)]
331 pub fn tangent(&self, param: f64) -> Vec2 {
332 let num_pts = self.control_points.len();
333 let deg = self.degree;
334 let span = crate::nurbs::basis::find_span(num_pts, deg, param, &self.knots);
335 let stride = deg + 1;
336 let mut ders_buf = [0.0f64; 2 * (crate::nurbs::basis::MAX_STACK_OUTPUT + 1)];
337 crate::nurbs::basis::ders_basis_funs_into(
338 span,
339 param,
340 deg,
341 1,
342 &self.knots,
343 &mut ders_buf[..2 * stride],
344 );
345
346 let mut curve_pt = Vec2::new(0.0, 0.0);
347 let mut curve_deriv = Vec2::new(0.0, 0.0);
348 let mut weight_sum = 0.0;
349 let mut weight_deriv = 0.0;
350
351 for j in 0..=deg {
352 let basis_val = ders_buf[j];
353 let basis_deriv = ders_buf[stride + j];
354 let idx = span - deg + j;
355 let wi = self.weights[idx];
356 let cp = &self.control_points[idx];
357 let cp_vec = Vec2::new(cp.x(), cp.y());
358
359 curve_pt += cp_vec * (wi * basis_val);
360 curve_deriv += cp_vec * (wi * basis_deriv);
361 weight_sum += wi * basis_val;
362 weight_deriv += wi * basis_deriv;
363 }
364
365 if weight_sum.abs() < f64::EPSILON {
366 return Vec2::new(0.0, 0.0);
367 }
368 (curve_deriv * weight_sum - curve_pt * weight_deriv) * (1.0 / (weight_sum * weight_sum))
369 }
370
371 #[must_use]
373 pub fn domain(&self) -> (f64, f64) {
374 (
375 self.knots[self.degree],
376 self.knots[self.knots.len() - self.degree - 1],
377 )
378 }
379
380 #[must_use]
382 pub const fn degree(&self) -> usize {
383 self.degree
384 }
385
386 #[must_use]
388 pub fn knots(&self) -> &[f64] {
389 &self.knots
390 }
391
392 #[must_use]
394 pub fn control_points(&self) -> &[Point2] {
395 &self.control_points
396 }
397
398 #[must_use]
400 pub fn weights(&self) -> &[f64] {
401 &self.weights
402 }
403
404 #[must_use]
406 pub fn is_rational(&self) -> bool {
407 self.weights.iter().any(|w| (*w - 1.0).abs() > f64::EPSILON)
408 }
409
410 pub fn from_line(start: Point2, end: Point2) -> Result<Self, MathError> {
415 Self::new(
416 1,
417 vec![0.0, 0.0, 1.0, 1.0],
418 vec![start, end],
419 vec![1.0, 1.0],
420 )
421 }
422}
423
424#[derive(Debug, Clone)]
426#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
427pub enum Curve2D {
428 Line(Line2D),
430 Circle(Circle2D),
432 Ellipse(Ellipse2D),
434 Nurbs(NurbsCurve2D),
436}
437
438impl Curve2D {
439 #[must_use]
441 pub fn evaluate(&self, t: f64) -> Point2 {
442 match self {
443 Self::Line(c) => c.evaluate(t),
444 Self::Circle(c) => c.evaluate(t),
445 Self::Ellipse(c) => c.evaluate(t),
446 Self::Nurbs(c) => c.evaluate(t),
447 }
448 }
449
450 #[must_use]
452 pub fn tangent(&self, t: f64) -> Vec2 {
453 match self {
454 Self::Line(c) => c.tangent(t),
455 Self::Circle(c) => c.tangent(t),
456 Self::Ellipse(c) => c.tangent(t),
457 Self::Nurbs(c) => c.tangent(t),
458 }
459 }
460}
461
462#[cfg(test)]
463#[allow(clippy::unwrap_used, clippy::expect_used)]
464mod tests {
465 use super::*;
466 use std::f64::consts::PI;
467
468 const TOL: f64 = 1e-10;
469
470 fn approx_eq(a: f64, b: f64) -> bool {
471 (a - b).abs() < TOL
472 }
473
474 fn point2_approx_eq(a: Point2, b: Point2) -> bool {
475 approx_eq(a.x(), b.x()) && approx_eq(a.y(), b.y())
476 }
477
478 #[test]
481 fn line2d_evaluate() {
482 let line = Line2D::new(Point2::new(1.0, 2.0), Vec2::new(3.0, 4.0)).expect("valid line");
484
485 let p = line.evaluate(0.0);
486 assert!(point2_approx_eq(p, Point2::new(1.0, 2.0)));
487
488 let p = line.evaluate(5.0);
490 assert!(point2_approx_eq(p, Point2::new(4.0, 6.0)));
491
492 let p = line.evaluate(2.5);
493 assert!(point2_approx_eq(p, Point2::new(2.5, 4.0)));
494 }
495
496 #[test]
497 fn line2d_project() {
498 let line = Line2D::new(Point2::new(0.0, 0.0), Vec2::new(1.0, 0.0)).expect("valid line");
499
500 assert!(approx_eq(line.project(Point2::new(5.0, 0.0)), 5.0));
501 assert!(approx_eq(line.project(Point2::new(3.0, 7.0)), 3.0));
502 }
503
504 #[test]
505 fn line2d_distance() {
506 let line = Line2D::new(Point2::new(0.0, 0.0), Vec2::new(1.0, 0.0)).expect("valid line");
507 assert!(approx_eq(
508 line.distance_to_point(Point2::new(3.0, 4.0)),
509 4.0
510 ));
511 }
512
513 #[test]
514 fn line2d_zero_direction_error() {
515 let result = Line2D::new(Point2::new(0.0, 0.0), Vec2::new(0.0, 0.0));
516 assert!(result.is_err());
517 }
518
519 #[test]
522 fn circle2d_evaluate_at_zero() {
523 let circle = Circle2D::new(Point2::new(0.0, 0.0), 1.0).expect("valid circle");
524 let p = circle.evaluate(0.0);
525 assert!(point2_approx_eq(p, Point2::new(1.0, 0.0)));
526 }
527
528 #[test]
529 fn circle2d_evaluate_quarter() {
530 let circle = Circle2D::new(Point2::new(0.0, 0.0), 2.0).expect("valid circle");
531 let p = circle.evaluate(PI / 2.0);
532 assert!(point2_approx_eq(p, Point2::new(0.0, 2.0)));
533 }
534
535 #[test]
536 fn circle2d_circumference() {
537 let circle = Circle2D::new(Point2::new(0.0, 0.0), 3.0).expect("valid circle");
538 assert!(approx_eq(circle.circumference(), 6.0 * PI));
539 }
540
541 #[test]
542 fn circle2d_project_roundtrip() {
543 let circle = Circle2D::new(Point2::new(1.0, 1.0), 5.0).expect("valid circle");
544 let t = 1.234;
545 let point = circle.evaluate(t);
546 let t_proj = circle.project(point);
547 assert!(approx_eq(t, t_proj));
548 }
549
550 #[test]
551 fn circle2d_zero_radius_error() {
552 let result = Circle2D::new(Point2::new(0.0, 0.0), 0.0);
553 assert!(result.is_err());
554 }
555
556 #[test]
559 fn ellipse2d_evaluate_no_rotation() {
560 let ellipse = Ellipse2D::new(Point2::new(0.0, 0.0), 3.0, 2.0, 0.0).expect("valid ellipse");
561
562 let p = ellipse.evaluate(0.0);
563 assert!(point2_approx_eq(p, Point2::new(3.0, 0.0)));
564
565 let p = ellipse.evaluate(PI / 2.0);
566 assert!(point2_approx_eq(p, Point2::new(0.0, 2.0)));
567 }
568
569 #[test]
570 fn ellipse2d_evaluate_with_rotation() {
571 let ellipse =
572 Ellipse2D::new(Point2::new(0.0, 0.0), 3.0, 2.0, PI / 2.0).expect("valid ellipse");
573 let p = ellipse.evaluate(0.0);
574 assert!(point2_approx_eq(p, Point2::new(0.0, 3.0)));
575 }
576
577 #[test]
578 fn ellipse2d_circle_circumference() {
579 let ellipse = Ellipse2D::new(Point2::new(0.0, 0.0), 5.0, 5.0, 0.0).expect("valid ellipse");
580 assert!(approx_eq(
581 ellipse.approximate_circumference(),
582 2.0 * PI * 5.0
583 ));
584 }
585
586 #[test]
587 fn ellipse2d_zero_axis_error() {
588 assert!(Ellipse2D::new(Point2::new(0.0, 0.0), 0.0, 1.0, 0.0).is_err());
589 assert!(Ellipse2D::new(Point2::new(0.0, 0.0), 1.0, 0.0, 0.0).is_err());
590 }
591
592 #[test]
595 fn nurbs2d_line_segment() {
596 let curve =
597 NurbsCurve2D::from_line(Point2::new(0.0, 0.0), Point2::new(1.0, 1.0)).expect("valid");
598
599 assert!(point2_approx_eq(curve.evaluate(0.0), Point2::new(0.0, 0.0)));
600 assert!(point2_approx_eq(curve.evaluate(1.0), Point2::new(1.0, 1.0)));
601 assert!(point2_approx_eq(curve.evaluate(0.5), Point2::new(0.5, 0.5)));
602 }
603
604 #[test]
605 fn nurbs2d_quadratic_bezier() {
606 let curve = NurbsCurve2D::new(
607 2,
608 vec![0.0, 0.0, 0.0, 1.0, 1.0, 1.0],
609 vec![
610 Point2::new(0.0, 0.0),
611 Point2::new(0.5, 1.0),
612 Point2::new(1.0, 0.0),
613 ],
614 vec![1.0, 1.0, 1.0],
615 )
616 .expect("valid curve");
617
618 assert!(point2_approx_eq(curve.evaluate(0.0), Point2::new(0.0, 0.0)));
619 assert!(point2_approx_eq(curve.evaluate(1.0), Point2::new(1.0, 0.0)));
620 assert!(point2_approx_eq(curve.evaluate(0.5), Point2::new(0.5, 0.5)));
621 }
622
623 #[test]
624 fn nurbs2d_tangent() {
625 let curve =
626 NurbsCurve2D::from_line(Point2::new(0.0, 0.0), Point2::new(2.0, 3.0)).expect("valid");
627 let tangent = curve.tangent(0.5);
628 assert!(approx_eq(tangent.x(), 2.0));
629 assert!(approx_eq(tangent.y(), 3.0));
630 }
631
632 #[test]
633 fn nurbs2d_domain() {
634 let curve = NurbsCurve2D::new(
635 2,
636 vec![0.0, 0.0, 0.0, 0.5, 1.0, 1.0, 1.0],
637 vec![
638 Point2::new(0.0, 0.0),
639 Point2::new(0.25, 1.0),
640 Point2::new(0.75, 1.0),
641 Point2::new(1.0, 0.0),
642 ],
643 vec![1.0, 1.0, 1.0, 1.0],
644 )
645 .expect("valid curve");
646
647 let (u_min, u_max) = curve.domain();
648 assert!(approx_eq(u_min, 0.0));
649 assert!(approx_eq(u_max, 1.0));
650 }
651
652 #[test]
653 fn nurbs2d_invalid_knots() {
654 let result = NurbsCurve2D::new(
655 1,
656 vec![0.0, 1.0],
657 vec![Point2::new(0.0, 0.0), Point2::new(1.0, 1.0)],
658 vec![1.0, 1.0],
659 );
660 assert!(result.is_err());
661 }
662
663 #[test]
664 fn nurbs2d_invalid_weights() {
665 let result = NurbsCurve2D::new(
666 1,
667 vec![0.0, 0.0, 1.0, 1.0],
668 vec![Point2::new(0.0, 0.0), Point2::new(1.0, 1.0)],
669 vec![1.0],
670 );
671 assert!(result.is_err());
672 }
673
674 #[test]
675 fn nurbs2d_is_rational() {
676 let non_rational =
677 NurbsCurve2D::from_line(Point2::new(0.0, 0.0), Point2::new(1.0, 1.0)).expect("valid");
678 assert!(!non_rational.is_rational());
679
680 let rational = NurbsCurve2D::new(
681 1,
682 vec![0.0, 0.0, 1.0, 1.0],
683 vec![Point2::new(0.0, 0.0), Point2::new(1.0, 1.0)],
684 vec![1.0, 2.0],
685 )
686 .expect("valid");
687 assert!(rational.is_rational());
688 }
689
690 #[test]
693 fn curve2d_evaluate_dispatch() {
694 let line =
695 Curve2D::Line(Line2D::new(Point2::new(0.0, 0.0), Vec2::new(1.0, 0.0)).expect("valid"));
696 assert!(point2_approx_eq(line.evaluate(5.0), Point2::new(5.0, 0.0)));
697
698 let circle = Curve2D::Circle(Circle2D::new(Point2::new(0.0, 0.0), 1.0).expect("valid"));
699 assert!(point2_approx_eq(
700 circle.evaluate(0.0),
701 Point2::new(1.0, 0.0)
702 ));
703 }
704
705 #[test]
708 fn ellipse2d_tangent_no_rotation() {
709 let ellipse = Ellipse2D::new(Point2::new(0.0, 0.0), 3.0, 2.0, 0.0).expect("valid ellipse");
712 let tang = ellipse.tangent(0.0);
713 assert!(approx_eq(tang.x(), 0.0));
714 assert!(approx_eq(tang.y(), 2.0));
715 }
716
717 #[test]
718 fn ellipse2d_tangent_at_quarter() {
719 let ellipse = Ellipse2D::new(Point2::new(0.0, 0.0), 3.0, 2.0, 0.0).expect("valid ellipse");
722 let tang = ellipse.tangent(PI / 2.0);
723 assert!(approx_eq(tang.x(), -3.0));
724 assert!(approx_eq(tang.y(), 0.0));
725 }
726
727 #[test]
728 fn ellipse2d_minor_exceeds_major_error() {
729 assert!(Ellipse2D::new(Point2::new(0.0, 0.0), 2.0, 5.0, 0.0).is_err());
731 }
732
733 #[test]
736 fn circle2d_tangent_at_zero() {
737 let circle = Circle2D::new(Point2::new(0.0, 0.0), 3.0).expect("valid circle");
739 let tang = circle.tangent(0.0);
740 assert!(approx_eq(tang.x(), 0.0));
741 assert!(approx_eq(tang.y(), 3.0));
742 }
743
744 #[test]
745 fn circle2d_tangent_at_quarter() {
746 let circle = Circle2D::new(Point2::new(0.0, 0.0), 2.0).expect("valid circle");
748 let tang = circle.tangent(PI / 2.0);
749 assert!(approx_eq(tang.x(), -2.0));
750 assert!(approx_eq(tang.y(), 0.0));
751 }
752
753 #[test]
754 fn circle2d_tangent_perpendicular_to_radius() {
755 let circle = Circle2D::new(Point2::new(1.0, 2.0), 4.0).expect("valid circle");
757 for i in 0..8 {
758 let t = f64::from(i) * PI / 4.0;
759 let pt = circle.evaluate(t);
760 let tang = circle.tangent(t);
761 let radius_vec_x = pt.x() - circle.center().x();
762 let radius_vec_y = pt.y() - circle.center().y();
763 let dot = radius_vec_x * tang.x() + radius_vec_y * tang.y();
764 assert!(
765 approx_eq(dot, 0.0),
766 "tangent not perpendicular to radius at t={t}: dot={dot}"
767 );
768 }
769 }
770
771 #[test]
774 fn nurbs2d_tangent_quadratic_bezier() {
775 let curve = NurbsCurve2D::new(
778 2,
779 vec![0.0, 0.0, 0.0, 1.0, 1.0, 1.0],
780 vec![
781 Point2::new(0.0, 0.0),
782 Point2::new(0.5, 1.0),
783 Point2::new(1.0, 0.0),
784 ],
785 vec![1.0, 1.0, 1.0],
786 )
787 .expect("valid curve");
788
789 let tang_start = curve.tangent(0.0);
790 assert!(
791 (tang_start.x() - 1.0).abs() < 1e-8,
792 "start tangent x: {}",
793 tang_start.x()
794 );
795 assert!(
796 (tang_start.y() - 2.0).abs() < 1e-8,
797 "start tangent y: {}",
798 tang_start.y()
799 );
800
801 let tang_end = curve.tangent(1.0);
802 assert!(
803 (tang_end.x() - 1.0).abs() < 1e-8,
804 "end tangent x: {}",
805 tang_end.x()
806 );
807 assert!(
808 (tang_end.y() + 2.0).abs() < 1e-8,
809 "end tangent y: {}",
810 tang_end.y()
811 );
812 }
813
814 #[test]
815 fn nurbs2d_tangent_midpoint_is_horizontal() {
816 let curve = NurbsCurve2D::new(
818 2,
819 vec![0.0, 0.0, 0.0, 1.0, 1.0, 1.0],
820 vec![
821 Point2::new(0.0, 0.0),
822 Point2::new(0.5, 1.0),
823 Point2::new(1.0, 0.0),
824 ],
825 vec![1.0, 1.0, 1.0],
826 )
827 .expect("valid curve");
828
829 let tang = curve.tangent(0.5);
830 assert!(
831 tang.y().abs() < 1e-10,
832 "midpoint tangent y should be zero, got {}",
833 tang.y()
834 );
835 assert!(tang.x() > 0.0, "midpoint tangent x should be positive");
836 }
837
838 #[test]
839 fn nurbs2d_empty_input_error() {
840 let result = NurbsCurve2D::new(0, vec![0.0], vec![], vec![]);
843 assert!(
844 matches!(result, Err(MathError::EmptyInput)),
845 "expected EmptyInput, got {result:?}"
846 );
847 }
848
849 #[test]
850 fn curve2d_tangent_dispatch_all_variants() {
851 let line =
853 Curve2D::Line(Line2D::new(Point2::new(0.0, 0.0), Vec2::new(1.0, 0.0)).expect("valid"));
854 let tang = line.tangent(7.0);
855 assert!(approx_eq(tang.x(), 1.0));
856 assert!(approx_eq(tang.y(), 0.0));
857
858 let circle = Curve2D::Circle(Circle2D::new(Point2::new(0.0, 0.0), 1.0).expect("valid"));
860 let tang = circle.tangent(0.0);
861 assert!(approx_eq(tang.x(), 0.0));
862 assert!(approx_eq(tang.y(), 1.0));
863
864 let ellipse =
866 Curve2D::Ellipse(Ellipse2D::new(Point2::new(0.0, 0.0), 4.0, 2.0, 0.0).expect("valid"));
867 let tang = ellipse.tangent(0.0);
868 assert!(approx_eq(tang.x(), 0.0));
869 assert!(approx_eq(tang.y(), 2.0));
870
871 let nurbs = Curve2D::Nurbs(
873 NurbsCurve2D::from_line(Point2::new(0.0, 0.0), Point2::new(2.0, 3.0)).expect("valid"),
874 );
875 let tang = nurbs.tangent(0.5);
876 assert!(approx_eq(tang.x(), 2.0));
877 assert!(approx_eq(tang.y(), 3.0));
878 }
879}