@@ -197,6 +197,34 @@ impl Animatable for Quat {
197197 }
198198}
199199
200+ impl Animatable for Rot2 {
201+ /// Performs a slerp to smoothly interpolate between 2D rotations.
202+ #[ inline]
203+ fn interpolate ( a : & Self , b : & Self , t : f32 ) -> Self {
204+ // We want to smoothly interpolate between the two rotations by default,
205+ // mirroring the behavior of `Quat` to ensure shortest-path consistency.
206+ a. slerp ( * b, t)
207+ }
208+
209+ #[ inline]
210+ fn blend ( inputs : impl Iterator < Item = BlendInput < Self > > ) -> Self {
211+ let mut value = Self :: IDENTITY ;
212+ for BlendInput {
213+ weight,
214+ value : incoming_value,
215+ additive,
216+ } in inputs
217+ {
218+ if additive {
219+ value = Self :: slerp ( Self :: IDENTITY , incoming_value, weight) * value;
220+ } else {
221+ value = Self :: interpolate ( & value, & incoming_value, weight) ;
222+ }
223+ }
224+ value
225+ }
226+ }
227+
200228/// Evaluates a cubic Bézier curve at a value `t`, given two endpoints and the
201229/// derivatives at those endpoints.
202230///
@@ -270,3 +298,116 @@ where
270298 let p1p2p3 = T :: interpolate ( & p1p2, & p2p3, t) ;
271299 T :: interpolate ( & p0p1p2, & p1p2p3, t)
272300}
301+
302+ #[ cfg( test) ]
303+ mod tests {
304+ use super :: * ;
305+ use core:: f32:: consts:: { FRAC_PI_2 , PI } ;
306+
307+ const EPSILON : f32 = 1e-5 ;
308+
309+ #[ test]
310+ fn test_rot2_shortest_path ( ) {
311+ // Interpolate from 89.99° to -89.99°.
312+ // The shortest path must pass through 0° rather than 180°.
313+ let a = Rot2 :: radians ( FRAC_PI_2 - EPSILON ) ;
314+ let b = Rot2 :: radians ( EPSILON - FRAC_PI_2 ) ;
315+
316+ let mid = Animatable :: interpolate ( & a, & b, 0.5 ) ;
317+
318+ assert ! (
319+ mid. as_radians( ) . abs( ) < EPSILON ,
320+ "Expected shortest path through 0°, but got {}°" ,
321+ mid. as_radians( ) . to_degrees( )
322+ ) ;
323+ }
324+
325+ #[ test]
326+ fn test_rot2_blend_two_equal ( ) {
327+ // Two equal weights (0.5 each) for 0° and 90°.
328+ // Result should be exactly 45°.
329+ let inputs = [
330+ BlendInput {
331+ weight : 0.5 ,
332+ value : Rot2 :: IDENTITY ,
333+ additive : false ,
334+ } ,
335+ BlendInput {
336+ weight : 0.5 ,
337+ value : Rot2 :: radians ( FRAC_PI_2 ) ,
338+ additive : false ,
339+ } ,
340+ ] ;
341+
342+ let blended = Animatable :: blend ( inputs. into_iter ( ) ) ;
343+
344+ assert ! (
345+ ( blended. as_radians( ) - FRAC_PI_2 / 2.0 ) . abs( ) < EPSILON ,
346+ "Expected 45°, got {}°" ,
347+ blended. as_radians( ) . to_degrees( )
348+ ) ;
349+ }
350+
351+ #[ test]
352+ fn test_rot2_blend_three_equal ( ) {
353+ // Three equal weights (1/3 each) for 0°, 90°, and 180°.
354+ // Bevy's cumulative blending:
355+ // 1. interpolate(IDENTITY, 0°, 0.33) = 0°
356+ // 2. interpolate(0°, 90°, 0.33) = 30°
357+ // 3. interpolate(30°, 180°, 0.33) = 80°
358+ // This confirms the implementation matches Bevy's standard blending behavior.
359+
360+ let inputs = [
361+ BlendInput {
362+ weight : 1.0 / 3.0 ,
363+ value : Rot2 :: IDENTITY ,
364+ additive : false ,
365+ } ,
366+ BlendInput {
367+ weight : 1.0 / 3.0 ,
368+ value : Rot2 :: radians ( FRAC_PI_2 ) ,
369+ additive : false ,
370+ } ,
371+ BlendInput {
372+ weight : 1.0 / 3.0 ,
373+ value : Rot2 :: radians ( PI ) ,
374+ additive : false ,
375+ } ,
376+ ] ;
377+
378+ let blended = Animatable :: blend ( inputs. into_iter ( ) ) ;
379+ let result_deg = blended. as_radians ( ) . to_degrees ( ) ;
380+
381+ // We expect approximately 80° due to the cumulative nature of the blend logic.
382+ assert ! (
383+ ( result_deg - 80.0 ) . abs( ) < 5.0 ,
384+ "Three-way blend result should be approximately 80° (got {}°), matching Bevy's cumulative logic" ,
385+ result_deg
386+ ) ;
387+ }
388+
389+ #[ test]
390+ fn test_rot2_blend_additive ( ) {
391+ // Base 45° + Additive 90° (weight 1.0) = 135°.
392+ let inputs = [
393+ BlendInput {
394+ weight : 1.0 ,
395+ value : Rot2 :: radians ( PI / 4.0 ) ,
396+ additive : false ,
397+ } ,
398+ BlendInput {
399+ weight : 1.0 ,
400+ value : Rot2 :: radians ( FRAC_PI_2 ) ,
401+ additive : true ,
402+ } ,
403+ ] ;
404+
405+ let blended = Animatable :: blend ( inputs. into_iter ( ) ) ;
406+
407+ assert ! (
408+ ( blended. as_radians( ) - 3.0 * PI / 4.0 ) . abs( ) < EPSILON ,
409+ "Expected 135° (3PI/4), but got {}°" ,
410+ blended. as_radians( ) . to_degrees( )
411+ ) ;
412+ }
413+ }
0 commit comments