arris_math/periodic.rs
1//! Periodic parameters: how a parameter of a closed curve or surface is
2//! wrapped, shifted and differenced. The period always comes from the
3//! entity (`Curve::period`, `Surface::period`); nothing here assumes `2π`
4//! except [`wrap_angle`], which says so (`docs/DATA-MODEL.md` §Geometry).
5
6use crate::{Interval, Vec2};
7
8/// `t` moved by whole periods into `[lo, lo + period)`. A value already in
9/// that range comes back unchanged, bit for bit (no add-and-subtract of
10/// `lo`), and a result that rounds up to `lo + period` becomes `lo` — the
11/// same point on the circle, and inside the range. A non-finite `t` comes
12/// back unchanged.
13///
14/// ```
15/// use arris_math::wrap_into;
16///
17/// assert_eq!(wrap_into(7.0, 2.0, 3.0), 4.0);
18/// // Exactly at `lo + period` is the start of the next period.
19/// assert_eq!(wrap_into(5.0, 2.0, 3.0), 2.0);
20/// // A period that is not a turn.
21/// assert_eq!(wrap_into(-0.5, 0.0, 1.5), 1.0);
22/// // A tiny negative value whose sum with the period rounds up to it.
23/// assert_eq!(wrap_into(-1e-300, 0.0, 3.0), 0.0);
24/// ```
25pub fn wrap_into(t: f64, lo: f64, period: f64) -> f64 {
26 if !t.is_finite() {
27 return t;
28 }
29 let w = if t >= lo && t - lo < period {
30 t
31 } else {
32 lo + (t - lo).rem_euclid(period)
33 };
34 if w - lo >= period { lo } else { w }
35}
36
37/// An angle moved into `[0, 2π)`: what a periodic curve's or surface's
38/// parameter is reported in (`docs/DATA-MODEL.md` §Conventions). A
39/// negative angle whose sum with `2π` rounds up to `2π` becomes `0` —
40/// the same point on the circle, and inside the domain. A non-finite
41/// angle comes back unchanged.
42///
43/// ```
44/// use arris_math::wrap_angle;
45/// use core::f64::consts::TAU;
46///
47/// assert_eq!(wrap_angle(0.0), 0.0);
48/// assert_eq!(wrap_angle(-1.0), TAU - 1.0);
49/// assert_eq!(wrap_angle(-1e-300), 0.0);
50/// assert_eq!(wrap_angle(TAU + 1.0), 1.0);
51/// ```
52pub fn wrap_angle(t: f64) -> f64 {
53 wrap_into(t, 0.0, core::f64::consts::TAU)
54}
55
56/// A difference moved into `[−period / 2, period / 2)`, by wrapping
57/// `x + period / 2` into `[0, period)` and moving back. Unlike
58/// [`wrap_signed`] it can return a value one rounding from `x` when `x`
59/// is already in range, because of the add and subtract of the half
60/// period; callers that were written against it keep that rounding.
61///
62/// ```
63/// use arris_math::wrap_offset;
64/// use core::f64::consts::{PI, TAU};
65///
66/// assert_eq!(wrap_offset(0.0, TAU), 0.0);
67/// // Exactly half a period goes to the lower end.
68/// assert_eq!(wrap_offset(PI, TAU), -PI);
69/// assert!((wrap_offset(TAU + 0.25, TAU) - 0.25).abs() < 1e-15);
70/// // A period that is not a turn.
71/// assert_eq!(wrap_offset(1.0, 1.0), 0.0);
72/// ```
73pub fn wrap_offset(x: f64, period: f64) -> f64 {
74 let half = 0.5 * period;
75 wrap_into(x + half, 0.0, period) - half
76}
77
78/// The whole-period shift that moves `x` nearest to `target`: `k·period`
79/// with `k` the integer nearest `(target − x) / period` (halves away from
80/// zero). Adding it to `x` is the translate of `x` that lies closest to
81/// `target`; it is `0.0` when `x` already is.
82///
83/// ```
84/// use arris_math::shift_nearest;
85/// use core::f64::consts::TAU;
86///
87/// assert_eq!(shift_nearest(0.5, 0.5 + TAU, TAU), TAU);
88/// assert_eq!(shift_nearest(0.5, 0.5 - 2.0 * TAU, TAU), -2.0 * TAU);
89/// assert_eq!(shift_nearest(1.0, 1.4, TAU), 0.0);
90/// // A period that is not a turn.
91/// assert_eq!(shift_nearest(0.0, 7.0, 3.0), 6.0);
92/// ```
93pub fn shift_nearest(x: f64, target: f64, period: f64) -> f64 {
94 ((target - x) / period).round() * period
95}
96
97/// `d` rounded to whole periods in each periodic parameter of a surface
98/// (`Surface::period`), zero in the others: the translate in (u, v) that
99/// carries a point by `d` back to the nearest copy of itself.
100///
101/// ```
102/// use arris_math::{Vec2, shift_nearest_uv};
103/// use core::f64::consts::TAU;
104///
105/// let by = shift_nearest_uv(Vec2::new(TAU + 0.1, 5.0), [Some(TAU), None]);
106/// assert_eq!(by, Vec2::new(TAU, 0.0));
107/// ```
108pub fn shift_nearest_uv(d: Vec2, period: [Option<f64>; 2]) -> Vec2 {
109 let round = |x: f64, p: Option<f64>| p.map_or(0.0, |p| shift_nearest(0.0, x, p));
110 Vec2::new(round(d.x, period[0]), round(d.y, period[1]))
111}
112
113/// A difference moved into `[−period / 2, period / 2]`: `d` less the
114/// whole periods nearest it. A half period goes to whichever end the
115/// rounding of [`shift_nearest`] sends it, so the ends are not a promise.
116///
117/// ```
118/// use arris_math::wrap_signed;
119/// use core::f64::consts::TAU;
120///
121/// assert_eq!(wrap_signed(0.5, TAU), 0.5);
122/// assert!((wrap_signed(TAU - 0.5, TAU) + 0.5).abs() < 1e-15);
123/// assert_eq!(wrap_signed(-4.0, 3.0), -1.0);
124/// ```
125pub fn wrap_signed(d: f64, period: f64) -> f64 {
126 d - shift_nearest(0.0, d, period)
127}
128
129/// `t` moved by whole periods into `range` when it can be; a parameter of
130/// a non-periodic entity (`period` of `None`) only if it already lies in
131/// `range`. The shift is the smallest one that brings `t` to or above
132/// `range.lo()`, so the answer is the first translate at or after the
133/// start, and `None` when that overshoots `range.hi()`.
134///
135/// ```
136/// use arris_math::{Interval, shift_into_range};
137///
138/// let range = Interval::new(1.0, 2.0).unwrap();
139/// assert_eq!(shift_into_range(range, 1.5 + 6.0, Some(3.0)), Some(1.5));
140/// assert_eq!(shift_into_range(range, 0.5, Some(3.0)), None);
141/// assert_eq!(shift_into_range(range, 1.5, None), Some(1.5));
142/// assert_eq!(shift_into_range(range, 2.5, None), None);
143/// ```
144pub fn shift_into_range(range: Interval, t: f64, period: Option<f64>) -> Option<f64> {
145 match period {
146 Some(p) => {
147 let k = ((range.lo() - t) / p).ceil();
148 let shifted = t + k * p;
149 (shifted <= range.hi()).then_some(shifted)
150 }
151 None => range.contains(t).then_some(t),
152 }
153}
154
155/// The end of one whole period from `lo`: `lo + period`, stepped down to
156/// the representable value below when that sum rounds up, so that
157/// `end - lo <= period` holds exactly. A closed edge spans one period and
158/// no more (`docs/DATA-MODEL.md` §Invariants, E1), and for a
159/// `lo` that is not a small multiple of the period the sum can round to
160/// one unit in the last place too far; this is the range's construction,
161/// not a tolerance. A non-finite argument, or a `period` that is not
162/// positive, comes back as `lo + period`.
163///
164/// ```
165/// use arris_math::period_end;
166/// use core::f64::consts::TAU;
167///
168/// assert_eq!(period_end(0.0, TAU), TAU);
169/// // A pave one unit in the last place below a full turn: the sum
170/// // rounds up, and the end is stepped back to keep the turn one turn.
171/// let lo = f64::from_bits(TAU.to_bits() - 1);
172/// assert!(lo + TAU - lo > TAU);
173/// assert!(period_end(lo, TAU) - lo <= TAU);
174/// ```
175pub fn period_end(lo: f64, period: f64) -> f64 {
176 let mut end = lo + period;
177 if !(end.is_finite() && period > 0.0) {
178 return end;
179 }
180 while end - lo > period {
181 end = f64::from_bits(end.to_bits() - 1);
182 }
183 end
184}