LCOV - code coverage report
Current view: top level - test - quaternion.cpp (source / functions) Coverage Total Hit
Test: coverage.info Lines: 100.0 % 268 268
Test Date: 2026-04-03 02:26:39 Functions: 100.0 % 2 2

            Line data    Source code
       1              : #include "catch.hpp"
       2              : #include "Quaternion.h"
       3              : #include "Vector.h"
       4              : #include <cmath>
       5              : 
       6           23 : SCENARIO( "Quaternion basics", "[Quaternion]" ) {
       7           23 :   GIVEN( "Default quaternion" ) {
       8            1 :     Quaternion q;
       9            2 :     THEN( "all components are zero" ) {
      10            1 :       REQUIRE(q.getX() == 0.0f);
      11            1 :       REQUIRE(q.getY() == 0.0f);
      12            1 :       REQUIRE(q.getZ() == 0.0f);
      13            1 :       REQUIRE(q.getW() == 0.0f);
      14            1 :     }
      15            1 :   }
      16              : 
      17           23 :   GIVEN( "loadMultIdentity" ) {
      18            1 :     Quaternion q(1.0f, 2.0f, 3.0f, 4.0f);
      19            1 :     q.loadMultIdentity();
      20            2 :     THEN( "is multiplicative identity" ) {
      21            1 :       REQUIRE(q.getX() == 0.0f);
      22            1 :       REQUIRE(q.getY() == 0.0f);
      23            1 :       REQUIRE(q.getZ() == 0.0f);
      24            1 :       REQUIRE(q.getW() == 1.0f);
      25            1 :     }
      26            1 :   }
      27              : 
      28           23 :   GIVEN( "loadAddIdentity" ) {
      29            1 :     Quaternion q(1.0f, 2.0f, 3.0f, 4.0f);
      30            1 :     q.loadAddIdentity();
      31            2 :     THEN( "is additive zero quaternion" ) {
      32            1 :       REQUIRE(q.getX() == 0.0f);
      33            1 :       REQUIRE(q.getY() == 0.0f);
      34            1 :       REQUIRE(q.getZ() == 0.0f);
      35            1 :       REQUIRE(q.getW() == 0.0f);
      36            1 :     }
      37            1 :   }
      38              : 
      39           23 :   GIVEN( "Quaternion from explicit components" ) {
      40            1 :     Quaternion q(0.0f, 0.0f, 0.0f, 2.0f);
      41            1 :     q.normalize();
      42            2 :     THEN( "normalize yields unit w" ) {
      43            1 :       REQUIRE(q.getW() == Approx(1.0f));
      44            1 :       REQUIRE(q.getX() == Approx(0.0f));
      45            1 :       REQUIRE(q.getY() == Approx(0.0f));
      46            1 :       REQUIRE(q.getZ() == Approx(0.0f));
      47            1 :     }
      48            1 :   }
      49              : 
      50           23 :   GIVEN( "normalize of a zero-length quaternion" ) {
      51            1 :     Quaternion q;  // default: all components zero, len == 0
      52            1 :     q.normalize();
      53            2 :     THEN( "all components remain zero — no-op guard prevents divide by zero" ) {
      54            1 :       REQUIRE(q.getX() == 0.0f);
      55            1 :       REQUIRE(q.getY() == 0.0f);
      56            1 :       REQUIRE(q.getZ() == 0.0f);
      57            1 :       REQUIRE(q.getW() == 0.0f);
      58            1 :     }
      59            1 :   }
      60              : 
      61           23 :   GIVEN( "float constructor delegates to buildFromEuler" ) {
      62            1 :     Quaternion fromCtor(0.0f, 1.0f, 0.0f);  // Quaternion(float, float, float)
      63            1 :     Quaternion fromMethod;
      64            1 :     fromMethod.buildFromEuler(0.0f, 1.0f, 0.0f);
      65            2 :     THEN( "constructor and explicit buildFromEuler produce identical results" ) {
      66            1 :       REQUIRE(fromCtor.getX() == Approx(fromMethod.getX()));
      67            1 :       REQUIRE(fromCtor.getY() == Approx(fromMethod.getY()));
      68            1 :       REQUIRE(fromCtor.getZ() == Approx(fromMethod.getZ()));
      69            1 :       REQUIRE(fromCtor.getW() == Approx(fromMethod.getW()));
      70            1 :     }
      71            1 :   }
      72              : 
      73           23 :   GIVEN( "Vector constructor delegates to buildFromEuler" ) {
      74            1 :     Vector v(0.0f, 1.0f, 0.0f);
      75            1 :     Quaternion fromCtor(v);  // Quaternion(const Vector&)
      76            1 :     Quaternion fromMethod;
      77            1 :     fromMethod.buildFromEuler(0.0f, 1.0f, 0.0f);
      78            2 :     THEN( "constructor and explicit buildFromEuler produce identical results" ) {
      79            1 :       REQUIRE(fromCtor.getX() == Approx(fromMethod.getX()));
      80            1 :       REQUIRE(fromCtor.getY() == Approx(fromMethod.getY()));
      81            1 :       REQUIRE(fromCtor.getZ() == Approx(fromMethod.getZ()));
      82            1 :       REQUIRE(fromCtor.getW() == Approx(fromMethod.getW()));
      83            1 :     }
      84            1 :   }
      85              : 
      86           23 :   GIVEN( "buildFromEuler with zero angles" ) {
      87            1 :     Quaternion q;
      88            1 :     q.buildFromEuler(0.0f, 0.0f, 0.0f);
      89            2 :     THEN( "matches identity rotation" ) {
      90            1 :       REQUIRE(q.getW() == Approx(1.0f));
      91            1 :       REQUIRE(q.getX() == Approx(0.0f));
      92            1 :       REQUIRE(q.getY() == Approx(0.0f));
      93            1 :       REQUIRE(q.getZ() == Approx(0.0f));
      94            1 :     }
      95            1 :   }
      96              : 
      97           23 :   GIVEN( "buildRotationMatrix from multiplicative identity" ) {
      98            1 :     Quaternion q;
      99            1 :     q.loadMultIdentity();
     100            1 :     Matrix m;
     101            1 :     q.buildRotationMatrix(m);
     102              : 
     103            2 :     THEN( "result is the identity matrix" ) {
     104            1 :       REQUIRE(m.data[0]  == Approx(1.0f));
     105            1 :       REQUIRE(m.data[5]  == Approx(1.0f));
     106            1 :       REQUIRE(m.data[10] == Approx(1.0f));
     107            1 :       REQUIRE(m.data[15] == Approx(1.0f));
     108            1 :       REQUIRE(m.data[1]  == Approx(0.0f));
     109            1 :       REQUIRE(m.data[2]  == Approx(0.0f));
     110            1 :       REQUIRE(m.data[4]  == Approx(0.0f));
     111            1 :       REQUIRE(m.data[6]  == Approx(0.0f));
     112            1 :       REQUIRE(m.data[9]  == Approx(0.0f));
     113            1 :     }
     114            1 :   }
     115              : 
     116           24 :   GIVEN( "buildFromAxis on X by 90 degrees" ) {
     117            2 :     Vector axis(1.0f, 0.0f, 0.0f);
     118            2 :     const float halfPi = 1.57079632679f;
     119            2 :     Quaternion q(axis, halfPi);
     120            2 :     q.normalize();
     121            2 :     Matrix m;
     122            2 :     q.buildRotationMatrix(m);
     123            2 :     Vector euler;
     124            2 :     q.getEulerAngles(euler);
     125              : 
     126            3 :     THEN( "rotation matrix for 90 deg about X maps Y to Z" ) {
     127            1 :       REQUIRE(m.data[0] == Approx(1.0f));
     128            1 :       REQUIRE(m.data[5] == Approx(0.0f));
     129            1 :       REQUIRE(m.data[9] == Approx(-1.0f));
     130            1 :       REQUIRE(m.data[6] == Approx(1.0f));
     131            1 :     }
     132              : 
     133            3 :     THEN( "euler extraction returns finite angles" ) {
     134            1 :       REQUIRE(std::isfinite(euler.x));
     135            1 :       REQUIRE(std::isfinite(euler.y));
     136            1 :       REQUIRE(std::isfinite(euler.z));
     137            1 :     }
     138            2 :   }
     139              : 
     140           23 :   GIVEN( "getEulerAngles at 90 degrees about Y (gimbal lock)" ) {
     141              :     // 90 deg about Y produces m.data[2] = -1, which is outside the epsilon
     142              :     // band and enters the gimbal-lock else branch.  v.z is forced to 0;
     143              :     // v.y and v.x are computed from atan2 with exact inputs.
     144            1 :     const float halfPi = 1.57079632679f;
     145            1 :     Quaternion q(Vector(0.0f, 1.0f, 0.0f), halfPi);
     146            1 :     q.normalize();
     147            1 :     Vector euler;
     148            1 :     q.getEulerAngles(euler);
     149              : 
     150            2 :     THEN( "euler matches analytic gimbal-lock result (0, pi/2, 0)" ) {
     151            1 :       REQUIRE(euler.x == Approx(0.0f));
     152            1 :       REQUIRE(euler.y == Approx(halfPi));
     153            1 :       REQUIRE(euler.z == Approx(0.0f));
     154            1 :     }
     155            1 :   }
     156              : 
     157           23 :   GIVEN( "Copy constructor and assignment" ) {
     158            1 :     Quaternion a(1.0f, 2.0f, 3.0f, 4.0f);
     159            1 :     Quaternion b(a);
     160            1 :     Quaternion c;
     161            1 :     c = a;
     162              : 
     163            2 :     THEN( "copies match source" ) {
     164            1 :       REQUIRE(b.getX() == a.getX());
     165            1 :       REQUIRE(c.getW() == a.getW());
     166            1 :     }
     167            1 :   }
     168              : 
     169           23 :   GIVEN( "Quaternion multiplication composes rotations" ) {
     170            1 :     Quaternion q1;
     171            1 :     q1.buildFromEuler(0.0f, 0.0f, 0.0f);
     172            1 :     Quaternion q2;
     173            1 :     q2.loadMultIdentity();
     174            1 :     Quaternion r = q1 * q2;
     175            2 :     THEN( "identity times identity" ) {
     176            1 :       REQUIRE(r.getW() == Approx(1.0f));
     177            1 :       REQUIRE(r.getX() == Approx(0.0f));
     178            1 :     }
     179            1 :   }
     180              : 
     181           23 :   GIVEN( "90 degrees about X composed with 90 degrees about Y" ) {
     182            1 :     const float halfPi = 1.57079632679f;
     183            1 :     Quaternion q1(Vector(1.0f, 0.0f, 0.0f), halfPi);
     184            1 :     Quaternion q2(Vector(0.0f, 1.0f, 0.0f), halfPi);
     185            1 :     Quaternion r = q1 * q2;
     186              : 
     187            2 :     THEN( "result matches analytic composition (0.5, 0.5, -0.5, 0.5)" ) {
     188            1 :       REQUIRE(r.getX() == Approx(0.5f));
     189            1 :       REQUIRE(r.getY() == Approx(0.5f));
     190            1 :       REQUIRE(r.getZ() == Approx(-0.5f));
     191            1 :       REQUIRE(r.getW() == Approx(0.5f));
     192            1 :     }
     193            1 :   }
     194              : 
     195           23 :   GIVEN( "operator* is non-commutative" ) {
     196              :     // q1*q2 = (0.5, 0.5, -0.5, 0.5); q2*q1 = (0.5, 0.5, 0.5, 0.5).
     197              :     // The results differ in z, confirming multiplication order matters.
     198            1 :     const float halfPi = 1.57079632679f;
     199            1 :     Quaternion q1(Vector(1.0f, 0.0f, 0.0f), halfPi);  // 90 deg about X
     200            1 :     Quaternion q2(Vector(0.0f, 1.0f, 0.0f), halfPi);  // 90 deg about Y
     201              : 
     202            1 :     Quaternion r12 = q1 * q2;
     203            1 :     Quaternion r21 = q2 * q1;
     204              : 
     205            2 :     THEN( "q1*q2 and q2*q1 differ" ) {
     206            1 :       REQUIRE(r12.getZ() == Approx(-0.5f));
     207            1 :       REQUIRE(r21.getZ() == Approx( 0.5f));
     208            1 :     }
     209            1 :   }
     210              : 
     211           23 :   GIVEN( "operator+ adds components" ) {
     212            1 :     Quaternion a(1.0f, 2.0f, 3.0f, 4.0f);
     213            1 :     Quaternion b(10.0f, 20.0f, 30.0f, 40.0f);
     214            1 :     Quaternion c = a + b;
     215            2 :     THEN( "sum is per-component" ) {
     216            1 :       REQUIRE(c.getX() == Approx(11.0f));
     217            1 :       REQUIRE(c.getY() == Approx(22.0f));
     218            1 :       REQUIRE(c.getZ() == Approx(33.0f));
     219            1 :       REQUIRE(c.getW() == Approx(44.0f));
     220            1 :     }
     221            1 :   }
     222              : 
     223           23 :   GIVEN( "slerp with t < 0" ) {
     224            1 :     Quaternion qStart;
     225            1 :     qStart.loadMultIdentity();
     226            1 :     Quaternion qEnd;
     227            1 :     qEnd.buildFromEuler(0.0f, 1.0f, 0.0f);
     228            1 :     qEnd.normalize();
     229              : 
     230            1 :     Quaternion result;  // all-zero default
     231            1 :     result.slerp(qStart, -0.5f, qEnd);
     232              : 
     233            2 :     THEN( "result is unchanged — slerp is a no-op for out-of-range t" ) {
     234            1 :       REQUIRE(result.getX() == 0.0f);
     235            1 :       REQUIRE(result.getY() == 0.0f);
     236            1 :       REQUIRE(result.getZ() == 0.0f);
     237            1 :       REQUIRE(result.getW() == 0.0f);
     238            1 :     }
     239            1 :   }
     240              : 
     241           23 :   GIVEN( "slerp with t > 1" ) {
     242            1 :     Quaternion qStart;
     243            1 :     qStart.loadMultIdentity();
     244            1 :     Quaternion qEnd;
     245            1 :     qEnd.buildFromEuler(0.0f, 1.0f, 0.0f);
     246            1 :     qEnd.normalize();
     247              : 
     248            1 :     Quaternion result;  // all-zero default
     249            1 :     result.slerp(qStart, 2.0f, qEnd);
     250              : 
     251            2 :     THEN( "result is unchanged — slerp is a no-op for out-of-range t" ) {
     252            1 :       REQUIRE(result.getX() == 0.0f);
     253            1 :       REQUIRE(result.getY() == 0.0f);
     254            1 :       REQUIRE(result.getZ() == 0.0f);
     255            1 :       REQUIRE(result.getW() == 0.0f);
     256            1 :     }
     257            1 :   }
     258              : 
     259           23 :   GIVEN( "slerp with negated end quaternion (negative dot product branch)" ) {
     260              :     // Negating all components of a quaternion yields the same rotation.
     261              :     // When dotQ < 0, slerp flips eQ to take the short arc.  At t=1 the
     262              :     // result should still land on the positive form of the end rotation.
     263            1 :     const float halfPi = 1.57079632679f;
     264            1 :     const float s = std::sin(halfPi / 2.0f);  // sin(pi/4) = sqrt(2)/2
     265            1 :     const float c = std::cos(halfPi / 2.0f);  // cos(pi/4) = sqrt(2)/2
     266              : 
     267            1 :     Quaternion qStart;
     268            1 :     qStart.loadMultIdentity();
     269            1 :     Quaternion qEndNeg(0.0f, -s, 0.0f, -c);  // negated 90 deg about Y
     270              : 
     271            1 :     Quaternion result;
     272            1 :     result.slerp(qStart, 1.0f, qEndNeg);
     273              : 
     274            2 :     THEN( "result lands on positive 90 deg about Y — short arc was taken" ) {
     275            1 :       REQUIRE(result.getX() == Approx(0.0f));
     276            1 :       REQUIRE(result.getY() == Approx(s));
     277            1 :       REQUIRE(result.getZ() == Approx(0.0f));
     278            1 :       REQUIRE(result.getW() == Approx(c));
     279            1 :     }
     280            1 :   }
     281              : 
     282           23 :   GIVEN( "slerp between identical quaternions (linear fallback branch)" ) {
     283              :     // When start == end, dotQ == 1 and (1 - dotQ) == 0 <= 0.05 tolerance,
     284              :     // so the linear path (scale1 = 1-t, scale2 = t) is used.
     285              :     // The result must equal the input quaternion for any t.
     286            1 :     const float halfPi = 1.57079632679f;
     287            1 :     Quaternion q(Vector(0.0f, 1.0f, 0.0f), halfPi);  // 90 deg about Y
     288              : 
     289            1 :     Quaternion result;
     290            1 :     result.slerp(q, 0.5f, q);
     291              : 
     292            2 :     THEN( "result equals the shared start/end quaternion" ) {
     293            1 :       REQUIRE(result.getX() == Approx(q.getX()));
     294            1 :       REQUIRE(result.getY() == Approx(q.getY()));
     295            1 :       REQUIRE(result.getZ() == Approx(q.getZ()));
     296            1 :       REQUIRE(result.getW() == Approx(q.getW()));
     297            1 :     }
     298            1 :   }
     299              : 
     300           23 :   GIVEN( "slerp endpoints" ) {
     301            1 :     Quaternion qStart;
     302            1 :     qStart.loadMultIdentity();
     303            1 :     Quaternion qEnd;
     304            1 :     qEnd.buildFromEuler(0.0f, 1.0f, 0.0f);
     305            1 :     qEnd.normalize();
     306              : 
     307            1 :     Quaternion atZero;
     308            1 :     atZero.slerp(qStart, 0.0f, qEnd);
     309            1 :     Quaternion atOne;
     310            1 :     atOne.slerp(qStart, 1.0f, qEnd);
     311              : 
     312            2 :     THEN( "t=0 matches start and t=1 matches end" ) {
     313            1 :       REQUIRE(atZero.getX() == Approx(qStart.getX()));
     314            1 :       REQUIRE(atZero.getY() == Approx(qStart.getY()));
     315            1 :       REQUIRE(atZero.getZ() == Approx(qStart.getZ()));
     316            1 :       REQUIRE(atZero.getW() == Approx(qStart.getW()));
     317            1 :       REQUIRE(atOne.getX() == Approx(qEnd.getX()));
     318            1 :       REQUIRE(atOne.getY() == Approx(qEnd.getY()));
     319            1 :       REQUIRE(atOne.getZ() == Approx(qEnd.getZ()));
     320            1 :       REQUIRE(atOne.getW() == Approx(qEnd.getW()));
     321            1 :     }
     322            1 :   }
     323           22 : }
        

Generated by: LCOV version 2.4-0