brepkit-math 3.2.13

Vector math, transforms, NURBS, and geometric predicates for brepkit
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
//! Analytic 3D curve types: lines, circles, and ellipses.
//!
//! These provide exact evaluation (no NURBS approximation) for the
//! most common curve types in CAD.

use std::f64::consts::PI;

use crate::MathError;
use crate::frame::Frame3;
use crate::vec::{Point3, Vec3};

// ── Line3D ─────────────────────────────────────────────────────────

/// A 3D line defined by origin and direction.
///
/// Parameterized as `P(t) = origin + t * direction`.
#[derive(Debug, Clone)]
pub struct Line3D {
    origin: Point3,
    direction: Vec3,
}

impl Line3D {
    /// Create a new line.
    ///
    /// # Errors
    ///
    /// Returns an error if `direction` is zero-length.
    pub fn new(origin: Point3, direction: Vec3) -> Result<Self, MathError> {
        let len = direction.length();
        if len < 1e-15 {
            return Err(MathError::ZeroVector);
        }
        Ok(Self {
            origin,
            direction: Vec3::new(
                direction.x() / len,
                direction.y() / len,
                direction.z() / len,
            ),
        })
    }

    /// Evaluate the line at parameter `t`.
    #[must_use]
    pub fn evaluate(&self, t: f64) -> Point3 {
        self.origin + self.direction * t
    }

    /// The tangent direction (constant for a line).
    #[must_use]
    pub const fn tangent(&self) -> Vec3 {
        self.direction
    }

    /// Project a point onto the line, returning the parameter.
    #[must_use]
    pub fn project(&self, point: Point3) -> f64 {
        let v = point - self.origin;
        self.direction.dot(v)
    }

    /// Distance from a point to the line.
    #[must_use]
    pub fn distance_to_point(&self, point: Point3) -> f64 {
        let v = point - self.origin;
        let proj = self.direction * self.direction.dot(v);
        (v - proj).length()
    }

    /// The line origin.
    #[must_use]
    pub const fn origin(&self) -> Point3 {
        self.origin
    }

    /// The unit direction.
    #[must_use]
    pub const fn direction(&self) -> Vec3 {
        self.direction
    }
}

// ── Circle3D ───────────────────────────────────────────────────────

/// A 3D circle defined by center, normal (axis), and radius.
///
/// Parameterized as `P(t) = center + radius*(cos(t)*u + sin(t)*v)`
/// where `u` and `v` form an orthonormal basis in the circle plane.
/// `t` ranges from 0 to 2π for a full circle.
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Circle3D {
    center: Point3,
    normal: Vec3,
    radius: f64,
    u_axis: Vec3,
    v_axis: Vec3,
}

impl Circle3D {
    /// Create a new circle.
    ///
    /// # Errors
    ///
    /// Returns an error if `radius` is non-positive or `normal` is zero.
    pub fn new(center: Point3, normal: Vec3, radius: f64) -> Result<Self, MathError> {
        if radius <= 0.0 {
            return Err(MathError::ParameterOutOfRange {
                value: radius,
                min: 0.0,
                max: f64::INFINITY,
            });
        }
        let f = Frame3::from_normal(center, normal)?;
        Ok(Self {
            center,
            normal: f.z,
            radius,
            u_axis: f.x,
            v_axis: f.y,
        })
    }

    /// Create a new circle with a caller-supplied reference x-direction.
    ///
    /// `ref_dir` is projected onto the plane perpendicular to `normal` to
    /// produce `u_axis`. Circles are radially symmetric so the choice of
    /// `u_axis` has no geometric effect — but it does fix the seam vertex
    /// at `evaluate(0.0)`, which downstream code (closed-edge construction,
    /// PCurve computation) can depend on.
    ///
    /// # Errors
    ///
    /// Returns an error if `radius` is non-positive or `normal` is zero.
    pub fn new_with_ref(
        center: Point3,
        normal: Vec3,
        radius: f64,
        ref_dir: Vec3,
    ) -> Result<Self, MathError> {
        if radius <= 0.0 {
            return Err(MathError::ParameterOutOfRange {
                value: radius,
                min: 0.0,
                max: f64::INFINITY,
            });
        }
        let f = Frame3::from_normal_and_ref(center, normal, ref_dir)?;
        Ok(Self {
            center,
            normal: f.z,
            radius,
            u_axis: f.x,
            v_axis: f.y,
        })
    }

    /// Evaluate the circle at angle `t` (radians).
    #[must_use]
    pub fn evaluate(&self, t: f64) -> Point3 {
        let cos_t = t.cos();
        let sin_t = t.sin();
        self.center + self.u_axis * (self.radius * cos_t) + self.v_axis * (self.radius * sin_t)
    }

    /// Tangent at angle `t` (unit-length).
    #[must_use]
    pub fn tangent(&self, t: f64) -> Vec3 {
        let cos_t = t.cos();
        let sin_t = t.sin();
        self.u_axis * (-sin_t) + self.v_axis * cos_t
    }

    /// The circle circumference.
    #[must_use]
    pub fn circumference(&self) -> f64 {
        2.0 * PI * self.radius
    }

    /// The circle center.
    #[must_use]
    pub const fn center(&self) -> Point3 {
        self.center
    }

    /// The circle radius.
    #[must_use]
    pub const fn radius(&self) -> f64 {
        self.radius
    }

    /// The circle normal (axis direction).
    #[must_use]
    pub const fn normal(&self) -> Vec3 {
        self.normal
    }

    /// Project a point onto the circle, returning the angle parameter.
    #[must_use]
    pub fn project(&self, point: Point3) -> f64 {
        let v = point - self.center;
        let u_comp = self.u_axis.dot(v);
        let v_comp = self.v_axis.dot(v);
        v_comp.atan2(u_comp)
    }

    /// The u-axis direction (major axis in the circle plane).
    #[must_use]
    pub const fn u_axis(&self) -> Vec3 {
        self.u_axis
    }

    /// The v-axis direction (minor axis in the circle plane).
    #[must_use]
    pub const fn v_axis(&self) -> Vec3 {
        self.v_axis
    }

    /// Create a circle with explicit basis vectors (for transform/copy).
    ///
    /// # Errors
    ///
    /// Returns an error if `radius` is non-positive.
    pub fn with_axes(
        center: Point3,
        normal: Vec3,
        radius: f64,
        u_axis: Vec3,
        v_axis: Vec3,
    ) -> Result<Self, MathError> {
        if radius <= 0.0 {
            return Err(MathError::ParameterOutOfRange {
                value: radius,
                min: 0.0,
                max: f64::INFINITY,
            });
        }
        Ok(Self {
            center,
            normal,
            radius,
            u_axis,
            v_axis,
        })
    }

    /// Intersect the circle with a 3D line segment.
    ///
    /// Returns up to 2 intersection points along with their angle parameter
    /// `t` on the circle. Points returned are restricted to the segment
    /// `[seg_start, seg_end]` (with `tol` slack on the endpoints).
    ///
    /// Cases:
    /// - Segment crosses the circle's plane at one point: at most 1
    ///   intersection (when that crossing is on the circle, within `tol`).
    /// - Segment lies in the circle's plane: up to 2 intersections.
    /// - Segment is parallel to the plane but offset: 0 intersections.
    ///
    /// `tol` is the absolute linear tolerance for "on the plane" and
    /// "on the circle" tests, and for clamping the segment parameter.
    #[must_use]
    pub fn intersect_segment(
        &self,
        seg_start: Point3,
        seg_end: Point3,
        tol: f64,
    ) -> Vec<(Point3, f64)> {
        let mut out = Vec::new();
        let d = seg_end - seg_start;
        let seg_len_sq = d.length_squared();
        if seg_len_sq < tol * tol {
            return out;
        }

        // Signed distance of each endpoint to the circle's plane.
        let h0 = (seg_start - self.center).dot(self.normal);
        let h1 = (seg_end - self.center).dot(self.normal);

        let on_plane = |p: Point3| -> bool {
            let v = p - self.center;
            let in_plane = v.dot(self.normal).abs() < tol;
            let r = v.length();
            in_plane && (r - self.radius).abs() < tol
        };

        // Helper: append `t_seg` (segment parameter) → intersection point with
        // `tol` slack on the endpoints; drop duplicates within `tol`.
        let mut push_if_unique = |p: Point3| {
            let v = p - self.center;
            // angle in [0, 2π)
            let mut t = v.dot(self.v_axis).atan2(v.dot(self.u_axis));
            if t < 0.0 {
                t += std::f64::consts::TAU;
            }
            if out
                .iter()
                .any(|(q, _): &(Point3, f64)| (*q - p).length() < tol)
            {
                return;
            }
            out.push((p, t));
        };

        if h0.abs() < tol && h1.abs() < tol {
            // Segment lies in the circle's plane: solve 2D line-circle.
            // Project everything into UV coordinates centered at the circle.
            let p0_u = (seg_start - self.center).dot(self.u_axis);
            let p0_v = (seg_start - self.center).dot(self.v_axis);
            let p1_u = (seg_end - self.center).dot(self.u_axis);
            let p1_v = (seg_end - self.center).dot(self.v_axis);
            let du = p1_u - p0_u;
            let dv = p1_v - p0_v;
            // |P0 + s*(P1-P0)|² = r²
            // a*s² + 2*b*s + c = 0 where
            //   a = du² + dv²
            //   b = p0_u*du + p0_v*dv
            //   c = p0_u² + p0_v² - r²
            let a = du * du + dv * dv;
            let b = p0_u * du + p0_v * dv;
            let c = p0_u * p0_u + p0_v * p0_v - self.radius * self.radius;
            let disc = b * b - a * c;
            // `disc` has units of length^4 (it's b² - a·c, both products of
            // squared coordinates). Compare against a scale-aware threshold
            // `(tol² · a)` rather than raw `tol` (which is length).
            // Negative discriminants smaller than this in magnitude are
            // floating-point noise on a tangent intersection — clamp to 0.
            if a < tol * tol || disc < -tol * tol * a {
                return out;
            }
            let disc = disc.max(0.0);
            let s_slack = tol / seg_len_sq.sqrt();
            // Near-tangent collapse. The two roots straddle the foot of the
            // circle center on the line by half_chord = sqrt(disc/a); the
            // line's penetration into the circle is δ ≈ half_chord²/(2r).
            // When δ ≤ tol the configuration is tangent AT TOLERANCE and the
            // separate roots are conditioning noise (position error grows as
            // sqrt(2rδ): a 1e-13 residual at r=4 already shifts each root a
            // full micron, minting near-duplicate vertices next to an exact
            // tangency vertex). Emit the well-conditioned double root — the
            // foot itself — instead of the noise pair.
            let sqrt_disc = disc.sqrt();
            let roots: &[f64] = if disc <= 2.0 * self.radius * tol * a {
                &[-b / a]
            } else {
                &[(-b - sqrt_disc) / a, (-b + sqrt_disc) / a]
            };
            for &s in roots {
                if s >= -s_slack && s <= 1.0 + s_slack {
                    let s = s.clamp(0.0, 1.0);
                    let p = Point3::new(
                        seg_start.x() + s * d.x(),
                        seg_start.y() + s * d.y(),
                        seg_start.z() + s * d.z(),
                    );
                    push_if_unique(p);
                }
            }
        } else if h0 * h1 <= tol * tol {
            // Segment crosses the circle's plane (or touches it). Solve
            // for the unique s where signed-distance = 0:
            //   h0 + s*(h1 - h0) = 0  →  s = h0 / (h0 - h1)
            let denom = h0 - h1;
            if denom.abs() < tol {
                return out;
            }
            let s = h0 / denom;
            let s_slack = tol / seg_len_sq.sqrt();
            if s < -s_slack || s > 1.0 + s_slack {
                return out;
            }
            let s = s.clamp(0.0, 1.0);
            let p = Point3::new(
                seg_start.x() + s * d.x(),
                seg_start.y() + s * d.y(),
                seg_start.z() + s * d.z(),
            );
            if on_plane(p) {
                push_if_unique(p);
            }
        }
        // else: segment is on one side of the plane → no crossings.

        out
    }

    /// Intersect the circle with another COPLANAR circle.
    ///
    /// Returns up to 2 intersection points along with their angle parameter
    /// `t` on `self`. Non-coplanar pairs (skew or offset planes) and
    /// coincident/concentric pairs return no points — callers own those
    /// configurations separately.
    ///
    /// Near-tangent conditioning: when the circles graze (the chord implied
    /// by the root pair penetrates by less than `tol`), the two roots are
    /// noise straddling the tangency foot — position error grows as
    /// `sqrt(2·r·δ)`, the recurring tangential-contact class. The
    /// well-conditioned double root (the foot on the center line) is emitted
    /// instead of the pair.
    #[must_use]
    pub fn intersect_circle(&self, other: &Self, tol: f64) -> Vec<(Point3, f64)> {
        let mut out = Vec::new();
        if self.normal.cross(other.normal).length() > 1e-9 {
            return out; // Skew planes — not this primitive's case.
        }
        let dvec = other.center - self.center;
        if dvec.dot(self.normal).abs() > tol {
            return out; // Parallel but offset planes.
        }
        let du = dvec.dot(self.u_axis);
        let dv = dvec.dot(self.v_axis);
        let d2 = du * du + dv * dv;
        let d = d2.sqrt();
        if d < tol {
            return out; // Concentric (incl. coincident) — no discrete crossings.
        }
        let (r1, r2) = (self.radius, other.radius);
        let a = (d2 + r1 * r1 - r2 * r2) / (2.0 * d);
        let h2 = r1 * r1 - a * a;
        let r_eff = r1.min(r2);
        if h2 < -2.0 * r_eff * tol {
            return out; // Separated (or nested) beyond the tangency well.
        }
        let ux = Vec3::new(
            (self.u_axis.x() * du + self.v_axis.x() * dv) / d,
            (self.u_axis.y() * du + self.v_axis.y() * dv) / d,
            (self.u_axis.z() * du + self.v_axis.z() * dv) / d,
        );
        let vx = self.normal.cross(ux);
        let foot = self.center + ux * a;
        let mut push = |p: Point3| {
            let v = p - self.center;
            let mut t = v.dot(self.v_axis).atan2(v.dot(self.u_axis));
            if t < 0.0 {
                t += std::f64::consts::TAU;
            }
            if !out
                .iter()
                .any(|(q, _): &(Point3, f64)| (*q - p).length() < tol)
            {
                out.push((p, t));
            }
        };
        if h2 <= 2.0 * r_eff * tol {
            push(foot);
        } else {
            let h = h2.sqrt();
            push(foot + vx * h);
            push(foot - vx * h);
        }
        out
    }
}

// ── Ellipse3D ──────────────────────────────────────────────────────

/// A 3D ellipse defined by center, normal, and two semi-axis lengths.
///
/// Parameterized as `P(t) = center + a*cos(t)*u + b*sin(t)*v`.
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Ellipse3D {
    center: Point3,
    normal: Vec3,
    semi_major: f64,
    semi_minor: f64,
    u_axis: Vec3,
    v_axis: Vec3,
}

impl Ellipse3D {
    /// Create a new ellipse.
    ///
    /// `semi_major` is the larger radius, `semi_minor` the smaller.
    /// The major axis lies along the `u_axis` direction (computed from normal).
    ///
    /// # Errors
    ///
    /// Returns an error if either semi-axis is non-positive.
    pub fn new(
        center: Point3,
        normal: Vec3,
        semi_major: f64,
        semi_minor: f64,
    ) -> Result<Self, MathError> {
        if semi_major <= 0.0 || semi_minor <= 0.0 {
            return Err(MathError::ParameterOutOfRange {
                value: semi_major.min(semi_minor),
                min: 0.0,
                max: f64::INFINITY,
            });
        }
        if semi_minor > semi_major {
            return Err(MathError::ParameterOutOfRange {
                value: semi_minor,
                min: 0.0,
                max: semi_major,
            });
        }
        let f = Frame3::from_normal(center, normal)?;
        Ok(Self {
            center,
            normal: f.z,
            semi_major,
            semi_minor,
            u_axis: f.x,
            v_axis: f.y,
        })
    }

    /// Create a new ellipse with a caller-supplied reference major-axis direction.
    ///
    /// `ref_dir` is projected onto the plane perpendicular to `normal` to
    /// produce `u_axis` (which carries the `semi_major` extent). If
    /// `ref_dir` is parallel to `normal`, falls back to an arbitrary
    /// perpendicular choice per [`Frame3::from_normal_and_ref`].
    ///
    /// # Errors
    ///
    /// Returns an error if either semi-axis is non-positive, `semi_minor`
    /// exceeds `semi_major`, or `normal` is zero.
    pub fn new_with_ref(
        center: Point3,
        normal: Vec3,
        semi_major: f64,
        semi_minor: f64,
        ref_dir: Vec3,
    ) -> Result<Self, MathError> {
        if semi_major <= 0.0 || semi_minor <= 0.0 {
            return Err(MathError::ParameterOutOfRange {
                value: semi_major.min(semi_minor),
                min: 0.0,
                max: f64::INFINITY,
            });
        }
        if semi_minor > semi_major {
            return Err(MathError::ParameterOutOfRange {
                value: semi_minor,
                min: 0.0,
                max: semi_major,
            });
        }
        let f = Frame3::from_normal_and_ref(center, normal, ref_dir)?;
        Ok(Self {
            center,
            normal: f.z,
            semi_major,
            semi_minor,
            u_axis: f.x,
            v_axis: f.y,
        })
    }

    /// Evaluate the ellipse at angle `t`.
    #[must_use]
    pub fn evaluate(&self, t: f64) -> Point3 {
        let cos_t = t.cos();
        let sin_t = t.sin();
        self.center
            + self.u_axis * (self.semi_major * cos_t)
            + self.v_axis * (self.semi_minor * sin_t)
    }

    /// Tangent at angle `t` (not unit-length).
    #[must_use]
    pub fn tangent(&self, t: f64) -> Vec3 {
        let cos_t = t.cos();
        let sin_t = t.sin();
        self.u_axis * (-self.semi_major * sin_t) + self.v_axis * (self.semi_minor * cos_t)
    }

    /// The ellipse center.
    #[must_use]
    pub const fn center(&self) -> Point3 {
        self.center
    }

    /// Semi-major axis length.
    #[must_use]
    pub const fn semi_major(&self) -> f64 {
        self.semi_major
    }

    /// Semi-minor axis length.
    #[must_use]
    pub const fn semi_minor(&self) -> f64 {
        self.semi_minor
    }

    /// The ellipse normal (axis direction).
    #[must_use]
    pub const fn normal(&self) -> Vec3 {
        self.normal
    }

    /// Approximate circumference using Ramanujan's formula.
    #[must_use]
    pub fn approximate_circumference(&self) -> f64 {
        let a = self.semi_major;
        let b = self.semi_minor;
        let h = (a - b) * (a - b) / ((a + b) * (a + b));
        PI * (a + b) * (1.0 + 3.0 * h / (10.0 + (3.0f64.mul_add(-h, 4.0)).sqrt()))
    }

    /// Project a point onto the ellipse, returning the angle parameter.
    #[must_use]
    pub fn project(&self, point: Point3) -> f64 {
        let v = point - self.center;
        let u_comp = self.u_axis.dot(v) / self.semi_major;
        let v_comp = self.v_axis.dot(v) / self.semi_minor;
        v_comp.atan2(u_comp)
    }

    /// The u-axis direction (major axis direction).
    #[must_use]
    pub const fn u_axis(&self) -> Vec3 {
        self.u_axis
    }

    /// The v-axis direction (minor axis direction).
    #[must_use]
    pub const fn v_axis(&self) -> Vec3 {
        self.v_axis
    }

    /// Create an ellipse with explicit basis vectors (for transform/copy).
    ///
    /// # Errors
    ///
    /// Returns an error if either semi-axis is non-positive.
    pub fn with_axes(
        center: Point3,
        normal: Vec3,
        semi_major: f64,
        semi_minor: f64,
        u_axis: Vec3,
        v_axis: Vec3,
    ) -> Result<Self, MathError> {
        if semi_major <= 0.0 || semi_minor <= 0.0 {
            return Err(MathError::ParameterOutOfRange {
                value: semi_major.min(semi_minor),
                min: 0.0,
                max: f64::INFINITY,
            });
        }
        Ok(Self {
            center,
            normal,
            semi_major,
            semi_minor,
            u_axis,
            v_axis,
        })
    }
}

/// A 3D parabola defined by vertex, axis direction, and focal length.
///
/// Parameterized as `P(t) = vertex + (t²/(4f)) * axis_dir + t * u_axis`
/// where `f` is the focal length and `u_axis` is perpendicular to the axis
/// in the parabola plane.
///
/// The parameter `t` ranges over all reals; `t = 0` is the vertex.
#[derive(Debug, Clone)]
pub struct Parabola3D {
    vertex: Point3,
    axis_dir: Vec3,
    focal_length: f64,
    u_axis: Vec3,
}

impl Parabola3D {
    /// Creates a new parabola.
    ///
    /// `axis_dir` is the direction from vertex toward the interior of the
    /// parabola (the axis of symmetry). `focal_length` is the distance
    /// from vertex to focus.
    ///
    /// # Errors
    /// Returns an error if `focal_length` is not positive or `axis_dir` is zero.
    pub fn new(vertex: Point3, axis_dir: Vec3, focal_length: f64) -> Result<Self, MathError> {
        if focal_length <= 0.0 {
            return Err(MathError::ParameterOutOfRange {
                value: focal_length,
                min: f64::EPSILON,
                max: f64::MAX,
            });
        }
        let f = Frame3::from_normal(vertex, axis_dir)?;
        Ok(Self {
            vertex,
            axis_dir: f.z,
            focal_length,
            u_axis: f.x,
        })
    }

    /// Evaluates the parabola at parameter `t`.
    ///
    /// At `t = 0` this returns the vertex.
    #[must_use]
    pub fn evaluate(&self, t: f64) -> Point3 {
        let along_axis = (t * t) / (4.0 * self.focal_length);
        self.vertex + self.axis_dir * along_axis + self.u_axis * t
    }

    /// Returns the tangent vector at parameter `t`.
    #[must_use]
    pub fn tangent(&self, t: f64) -> Vec3 {
        let d_axis = t / (2.0 * self.focal_length);
        self.axis_dir * d_axis + self.u_axis
    }

    /// Returns the curvature at parameter `t`.
    #[must_use]
    pub fn curvature(&self, t: f64) -> f64 {
        let two_f = 2.0 * self.focal_length;
        let ratio = t / two_f;
        let denom = ratio.mul_add(ratio, 1.0);
        1.0 / (two_f * denom.powf(1.5))
    }

    /// Returns the vertex.
    #[must_use]
    pub const fn vertex(&self) -> Point3 {
        self.vertex
    }

    /// Returns the focal length.
    #[must_use]
    pub const fn focal_length(&self) -> f64 {
        self.focal_length
    }

    /// Returns the axis direction (normalized).
    #[must_use]
    pub const fn axis_dir(&self) -> Vec3 {
        self.axis_dir
    }

    /// Returns the in-plane u-axis (perpendicular to `axis_dir`).
    /// At parameter `t`, the parabola is offset by `t * u_axis` from
    /// the symmetry axis.
    #[must_use]
    pub const fn u_axis(&self) -> Vec3 {
        self.u_axis
    }

    /// Returns the focus point.
    #[must_use]
    pub fn focus(&self) -> Point3 {
        self.vertex + self.axis_dir * self.focal_length
    }
}

/// A 3D hyperbola defined by center, axis, and two semi-axis lengths.
///
/// Parameterized as `P(t) = center + a * cosh(t) * u_axis + b * sinh(t) * v_axis`.
///
/// The parameter `t` ranges over all reals; `t = 0` gives the vertex
/// closest to center on the positive branch.
#[derive(Debug, Clone)]
pub struct Hyperbola3D {
    center: Point3,
    normal: Vec3,
    semi_major: f64,
    semi_minor: f64,
    u_axis: Vec3,
    v_axis: Vec3,
}

impl Hyperbola3D {
    /// Creates a new hyperbola.
    ///
    /// `semi_major` is the real semi-axis (distance from center to vertex),
    /// `semi_minor` is the imaginary semi-axis.
    ///
    /// # Errors
    /// Returns an error if either semi-axis is non-positive.
    pub fn new(
        center: Point3,
        normal: Vec3,
        semi_major: f64,
        semi_minor: f64,
    ) -> Result<Self, MathError> {
        if semi_major <= 0.0 || semi_minor <= 0.0 {
            return Err(MathError::ParameterOutOfRange {
                value: semi_major.min(semi_minor),
                min: f64::EPSILON,
                max: f64::MAX,
            });
        }
        let f = Frame3::from_normal(center, normal)?;
        Ok(Self {
            center,
            normal: f.z,
            semi_major,
            semi_minor,
            u_axis: f.x,
            v_axis: f.y,
        })
    }

    /// Evaluates the hyperbola at parameter `t`.
    #[must_use]
    pub fn evaluate(&self, t: f64) -> Point3 {
        self.center
            + self.u_axis * (self.semi_major * t.cosh())
            + self.v_axis * (self.semi_minor * t.sinh())
    }

    /// Returns the tangent vector at parameter `t`.
    #[must_use]
    pub fn tangent(&self, t: f64) -> Vec3 {
        self.u_axis * (self.semi_major * t.sinh()) + self.v_axis * (self.semi_minor * t.cosh())
    }

    /// Returns the center.
    #[must_use]
    pub const fn center(&self) -> Point3 {
        self.center
    }

    /// Returns the semi-major axis (real axis).
    #[must_use]
    pub const fn semi_major(&self) -> f64 {
        self.semi_major
    }

    /// Returns the semi-minor axis (imaginary axis).
    #[must_use]
    pub const fn semi_minor(&self) -> f64 {
        self.semi_minor
    }

    /// Returns the normal (axis perpendicular to the hyperbola plane).
    #[must_use]
    pub const fn normal(&self) -> Vec3 {
        self.normal
    }

    /// Returns the in-plane u-axis (real semi-axis direction).
    /// At parameter `t`, the hyperbola is at offset
    /// `semi_major * cosh(t) * u_axis + semi_minor * sinh(t) * v_axis`
    /// from the center.
    #[must_use]
    pub const fn u_axis(&self) -> Vec3 {
        self.u_axis
    }

    /// Returns the in-plane v-axis (imaginary semi-axis direction).
    #[must_use]
    pub const fn v_axis(&self) -> Vec3 {
        self.v_axis
    }

    /// Returns the eccentricity: `e = sqrt(1 + (b/a)²)`.
    #[must_use]
    pub fn eccentricity(&self) -> f64 {
        let ratio = self.semi_minor / self.semi_major;
        ratio.mul_add(ratio, 1.0).sqrt()
    }

    /// Returns the two foci.
    #[must_use]
    pub fn foci(&self) -> (Point3, Point3) {
        let c = self.semi_major.hypot(self.semi_minor);
        (
            self.center + self.u_axis * c,
            self.center + self.u_axis * (-c),
        )
    }
}

#[cfg(test)]
mod tests;