axiolid_curve/sinusoid.rs
1//! The height wave a plane cuts across a cylinder, in the cylinder's own
2//! parameters.
3//!
4//! A cylinder is parameterised by angle `u` and height `v`. A plane
5//! `z = c0 + c1 x + c2 y` meets it where `x` and `y` are themselves `r cos u`
6//! and `r sin u` (or `a cos u` and `b sin u` on an elliptical cylinder), so
7//! the cut, read in `(u, v)`, is
8//!
9//! ```text
10//! v(u) = mean + cosine * cos(u) + sine * sin(u)
11//! ```
12//!
13//! That curve is what a trimmed cylinder face needs as the pcurve of its
14//! sloped rim. No other [`Curve2`](crate::Curve2) variant holds it exactly:
15//! it is not a conic in `(u, v)`, and a B-spline only approximates it,
16//! because `u` is an angle and the wave is transcendental in it.
17
18use axiolid_core::Scalar;
19
20/// The graph `v = mean + cosine cos(t) + sine sin(t)`, parameterised by `t`.
21///
22/// The point at parameter `t` is `(t, v(t))`: the parameter IS the first
23/// coordinate. On a cylinder that coordinate is the angle, so a pcurve use
24/// states the angle span it covers directly as its interval, and a span
25/// longer than a full turn is representable (it simply wraps the surface).
26///
27/// The curve is defined for every finite `t` and repeats with period `2 pi`
28/// in `v`; its conventional domain is one full turn.
29///
30/// Dirty imported data stays representable, as everywhere else in this crate:
31/// validation rejects non-finite coefficients, construction does not.
32#[derive(Debug, Clone, Copy, PartialEq)]
33pub struct Sinusoid2 {
34 /// Mean height: the constant term.
35 pub mean: Scalar,
36 /// Amplitude of the `cos(t)` term.
37 pub cosine: Scalar,
38 /// Amplitude of the `sin(t)` term.
39 pub sine: Scalar,
40}
41
42impl Sinusoid2 {
43 /// Height at parameter `t`.
44 #[must_use]
45 pub fn height(&self, t: Scalar) -> Scalar {
46 let (s, c) = t.sin_cos();
47 self.mean + self.cosine * c + self.sine * s
48 }
49
50 /// Peak deviation from the mean, `sqrt(cosine^2 + sine^2)`.
51 ///
52 /// The wave spans `mean - amplitude ..= mean + amplitude` over a full
53 /// turn, which is what a caller needs to check a cut stays clear of
54 /// another boundary.
55 #[must_use]
56 pub fn amplitude(&self) -> Scalar {
57 self.cosine.hypot(self.sine)
58 }
59
60 /// Whether every coefficient is finite.
61 #[must_use]
62 pub fn is_finite(&self) -> bool {
63 self.mean.is_finite() && self.cosine.is_finite() && self.sine.is_finite()
64 }
65}