Skip to content

Commit ff30848

Browse files
RobWalteckzmweatherleyIQuick143alice-i-cecile
authored
add more Curve adaptors (#14794)
# Objective This implements another item on the way to complete the `Curves` implementation initiative Citing @mweatherley > Curve adaptors for making a curve repeat or ping-pong would be useful. This adds three widely applicable adaptors: - `ReverseCurve` "plays" the curve backwards - `RepeatCurve` to repeat the curve for `n` times where `n` in `[0,inf)` - `ForeverCurve` which extends the curves domain to `EVERYWHERE` - `PingPongCurve` (name wip (?)) to chain the curve with it's reverse. This would be achievable with `ReverseCurve` and `ChainCurve`, but it would require the use of `by_ref` which can be restrictive in some scenarios where you'd rather just consume the curve. Users can still create the same effect by combination of the former two, but since this will be most likely a very typical adaptor we should also provide it on the library level. (Why it's typical: you can create a single period of common waves with it pretty easily, think square wave (= pingpong + step), triangle wave ( = pingpong + linear), etc.) - `ContinuationCurve` which chains two curves but also makes sure that the samples of the second curve are translated so that `sample(first.end) == sample(second.start)` ## Solution Implement the adaptors above. (More suggestions are welcome!) ## Testing - [x] add simple tests. One per adaptor --------- Co-authored-by: eckz <[email protected]> Co-authored-by: Matty <[email protected]> Co-authored-by: IQuick 143 <[email protected]> Co-authored-by: Matty <[email protected]> Co-authored-by: Alice Cecile <[email protected]>
1 parent 78a3aae commit ff30848

File tree

2 files changed

+552
-66
lines changed

2 files changed

+552
-66
lines changed
Lines changed: 270 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,270 @@
1+
//! A module containing utility helper structs to transform a [`Curve`] into another. This is useful
2+
//! for building up complex curves from simple segments.
3+
use core::marker::PhantomData;
4+
5+
use crate::VectorSpace;
6+
7+
use super::{Curve, Interval};
8+
9+
/// The curve that results from chaining one curve with another. The second curve is
10+
/// effectively reparametrized so that its start is at the end of the first.
11+
///
12+
/// Curves of this type are produced by [`Curve::chain`].
13+
///
14+
/// # Domain
15+
///
16+
/// The first curve's domain must be right-finite and the second's must be left-finite to get a
17+
/// valid [`ChainCurve`].
18+
#[derive(Clone, Debug)]
19+
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
20+
#[cfg_attr(feature = "bevy_reflect", derive(bevy_reflect::Reflect))]
21+
pub struct ChainCurve<T, C, D> {
22+
pub(super) first: C,
23+
pub(super) second: D,
24+
pub(super) _phantom: PhantomData<T>,
25+
}
26+
27+
impl<T, C, D> Curve<T> for ChainCurve<T, C, D>
28+
where
29+
C: Curve<T>,
30+
D: Curve<T>,
31+
{
32+
#[inline]
33+
fn domain(&self) -> Interval {
34+
// This unwrap always succeeds because `first` has a valid Interval as its domain and the
35+
// length of `second` cannot be NAN. It's still fine if it's infinity.
36+
Interval::new(
37+
self.first.domain().start(),
38+
self.first.domain().end() + self.second.domain().length(),
39+
)
40+
.unwrap()
41+
}
42+
43+
#[inline]
44+
fn sample_unchecked(&self, t: f32) -> T {
45+
if t > self.first.domain().end() {
46+
self.second.sample_unchecked(
47+
// `t - first.domain.end` computes the offset into the domain of the second.
48+
t - self.first.domain().end() + self.second.domain().start(),
49+
)
50+
} else {
51+
self.first.sample_unchecked(t)
52+
}
53+
}
54+
}
55+
56+
/// The curve that results from reversing another.
57+
///
58+
/// Curves of this type are produced by [`Curve::reverse`].
59+
///
60+
/// # Domain
61+
///
62+
/// The original curve's domain must be bounded to get a valid [`ReverseCurve`].
63+
#[derive(Clone, Debug)]
64+
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
65+
#[cfg_attr(feature = "bevy_reflect", derive(bevy_reflect::Reflect))]
66+
pub struct ReverseCurve<T, C> {
67+
pub(super) curve: C,
68+
pub(super) _phantom: PhantomData<T>,
69+
}
70+
71+
impl<T, C> Curve<T> for ReverseCurve<T, C>
72+
where
73+
C: Curve<T>,
74+
{
75+
#[inline]
76+
fn domain(&self) -> Interval {
77+
self.curve.domain()
78+
}
79+
80+
#[inline]
81+
fn sample_unchecked(&self, t: f32) -> T {
82+
self.curve
83+
.sample_unchecked(self.domain().end() - (t - self.domain().start()))
84+
}
85+
}
86+
87+
/// The curve that results from repeating a curve `N` times.
88+
///
89+
/// # Notes
90+
///
91+
/// - the value at the transitioning points (`domain.end() * n` for `n >= 1`) in the results is the
92+
/// value at `domain.end()` in the original curve
93+
///
94+
/// Curves of this type are produced by [`Curve::repeat`].
95+
///
96+
/// # Domain
97+
///
98+
/// The original curve's domain must be bounded to get a valid [`RepeatCurve`].
99+
#[derive(Clone, Debug)]
100+
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
101+
#[cfg_attr(feature = "bevy_reflect", derive(bevy_reflect::Reflect))]
102+
pub struct RepeatCurve<T, C> {
103+
pub(super) domain: Interval,
104+
pub(super) curve: C,
105+
pub(super) _phantom: PhantomData<T>,
106+
}
107+
108+
impl<T, C> Curve<T> for RepeatCurve<T, C>
109+
where
110+
C: Curve<T>,
111+
{
112+
#[inline]
113+
fn domain(&self) -> Interval {
114+
self.domain
115+
}
116+
117+
#[inline]
118+
fn sample_unchecked(&self, t: f32) -> T {
119+
// the domain is bounded by construction
120+
let d = self.curve.domain();
121+
let cyclic_t = (t - d.start()).rem_euclid(d.length());
122+
let t = if t != d.start() && cyclic_t == 0.0 {
123+
d.end()
124+
} else {
125+
d.start() + cyclic_t
126+
};
127+
self.curve.sample_unchecked(t)
128+
}
129+
}
130+
131+
/// The curve that results from repeating a curve forever.
132+
///
133+
/// # Notes
134+
///
135+
/// - the value at the transitioning points (`domain.end() * n` for `n >= 1`) in the results is the
136+
/// value at `domain.end()` in the original curve
137+
///
138+
/// Curves of this type are produced by [`Curve::forever`].
139+
///
140+
/// # Domain
141+
///
142+
/// The original curve's domain must be bounded to get a valid [`ForeverCurve`].
143+
#[derive(Clone, Debug)]
144+
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
145+
#[cfg_attr(feature = "bevy_reflect", derive(bevy_reflect::Reflect))]
146+
pub struct ForeverCurve<T, C> {
147+
pub(super) curve: C,
148+
pub(super) _phantom: PhantomData<T>,
149+
}
150+
151+
impl<T, C> Curve<T> for ForeverCurve<T, C>
152+
where
153+
C: Curve<T>,
154+
{
155+
#[inline]
156+
fn domain(&self) -> Interval {
157+
Interval::EVERYWHERE
158+
}
159+
160+
#[inline]
161+
fn sample_unchecked(&self, t: f32) -> T {
162+
// the domain is bounded by construction
163+
let d = self.curve.domain();
164+
let cyclic_t = (t - d.start()).rem_euclid(d.length());
165+
let t = if t != d.start() && cyclic_t == 0.0 {
166+
d.end()
167+
} else {
168+
d.start() + cyclic_t
169+
};
170+
self.curve.sample_unchecked(t)
171+
}
172+
}
173+
174+
/// The curve that results from chaining a curve with its reversed version. The transition point
175+
/// is guaranteed to make no jump.
176+
///
177+
/// Curves of this type are produced by [`Curve::ping_pong`].
178+
///
179+
/// # Domain
180+
///
181+
/// The original curve's domain must be right-finite to get a valid [`PingPongCurve`].
182+
#[derive(Clone, Debug)]
183+
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
184+
#[cfg_attr(feature = "bevy_reflect", derive(bevy_reflect::Reflect))]
185+
pub struct PingPongCurve<T, C> {
186+
pub(super) curve: C,
187+
pub(super) _phantom: PhantomData<T>,
188+
}
189+
190+
impl<T, C> Curve<T> for PingPongCurve<T, C>
191+
where
192+
C: Curve<T>,
193+
{
194+
#[inline]
195+
fn domain(&self) -> Interval {
196+
// This unwrap always succeeds because `curve` has a valid Interval as its domain and the
197+
// length of `curve` cannot be NAN. It's still fine if it's infinity.
198+
Interval::new(
199+
self.curve.domain().start(),
200+
self.curve.domain().end() + self.curve.domain().length(),
201+
)
202+
.unwrap()
203+
}
204+
205+
#[inline]
206+
fn sample_unchecked(&self, t: f32) -> T {
207+
// the domain is bounded by construction
208+
let final_t = if t > self.curve.domain().end() {
209+
self.curve.domain().end() * 2.0 - t
210+
} else {
211+
t
212+
};
213+
self.curve.sample_unchecked(final_t)
214+
}
215+
}
216+
217+
/// The curve that results from chaining two curves.
218+
///
219+
/// Additionally the transition of the samples is guaranteed to not make sudden jumps. This is
220+
/// useful if you really just know about the shapes of your curves and don't want to deal with
221+
/// stitching them together properly when it would just introduce useless complexity. It is
222+
/// realized by translating the second curve so that its start sample point coincides with the
223+
/// first curves' end sample point.
224+
///
225+
/// Curves of this type are produced by [`Curve::chain_continue`].
226+
///
227+
/// # Domain
228+
///
229+
/// The first curve's domain must be right-finite and the second's must be left-finite to get a
230+
/// valid [`ContinuationCurve`].
231+
#[derive(Clone, Debug)]
232+
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
233+
#[cfg_attr(feature = "bevy_reflect", derive(bevy_reflect::Reflect))]
234+
pub struct ContinuationCurve<T, C, D> {
235+
pub(super) first: C,
236+
pub(super) second: D,
237+
// cache the offset in the curve directly to prevent triple sampling for every sample we make
238+
pub(super) offset: T,
239+
pub(super) _phantom: PhantomData<T>,
240+
}
241+
242+
impl<T, C, D> Curve<T> for ContinuationCurve<T, C, D>
243+
where
244+
T: VectorSpace,
245+
C: Curve<T>,
246+
D: Curve<T>,
247+
{
248+
#[inline]
249+
fn domain(&self) -> Interval {
250+
// This unwrap always succeeds because `curve` has a valid Interval as its domain and the
251+
// length of `curve` cannot be NAN. It's still fine if it's infinity.
252+
Interval::new(
253+
self.first.domain().start(),
254+
self.first.domain().end() + self.second.domain().length(),
255+
)
256+
.unwrap()
257+
}
258+
259+
#[inline]
260+
fn sample_unchecked(&self, t: f32) -> T {
261+
if t > self.first.domain().end() {
262+
self.second.sample_unchecked(
263+
// `t - first.domain.end` computes the offset into the domain of the second.
264+
t - self.first.domain().end() + self.second.domain().start(),
265+
) + self.offset
266+
} else {
267+
self.first.sample_unchecked(t)
268+
}
269+
}
270+
}

0 commit comments

Comments
 (0)