Revision control

++ // Copyright (C) 2022 The Qt Company Ltd.
++ // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
++
++ #include <QtTest>
++
++ #include <QtQuick3DUtils/private/qssgutils_p.h>
++
++ #include <QtGui/qquaternion.h>
++
++ class tst_RotationDataClass : public QObject
++ {
++ Q_OBJECT
++
++ public:
++ tst_RotationDataClass() = default;
++ ~tst_RotationDataClass() = default;
++
++ using Dirty = RotationData::Dirty;
++ static bool isDirty(const RotationData &rd) { return (rd.m_dirty != Dirty::None); };
++
++ private slots:
++ void test_initialState();
++ void test_construct();
++ void test_eulerAssign();
++ void test_quatAssign();
++ void test_aba();
++ void test_compare();
++ void test_compare2();
++ void test_fuzzyQuatCompare();
++ void test_compare_precision();
++ };
++
++ void tst_RotationDataClass::test_initialState()
++ {
++
++ // Initial state
++ RotationData rotation;
++ QCOMPARE(isDirty(rotation), false);
++ QVERIFY(qFuzzyCompare(rotation.toEulerAngles(), QVector3D()));
++ QVERIFY(qFuzzyCompare(rotation.toQuaternion(), QQuaternion()));
++ }
++
++ void tst_RotationDataClass::test_construct()
++ {
++ QVector3D eulerRot = QVector3D(1.0f, 0.0f, 0.0f);
++ QQuaternion quatRot = QQuaternion::fromEulerAngles(eulerRot);
++
++ RotationData a(eulerRot);
++ QCOMPARE(isDirty(a), true);
++
++ RotationData b(quatRot);
++ QCOMPARE(isDirty(b), true);
++
++ QCOMPARE(a, b);
++ QCOMPARE(a.m_quatRot, b.m_quatRot); // Compensate for the XFAIL. Verify that the two quaternions a componentwise equal.
++ // NOTE: Comparison is done based on the stored quaternion
++ QCOMPARE(isDirty(a), false); // Dirty is cleared as 'a' was set using euler angles
++ QCOMPARE(isDirty(b), true); // Still dirty, as the Euler angles where never compared and therefore not calculated
++ QCOMPARE(b.m_dirty, Dirty::Euler);
++ QVERIFY(qFuzzyCompare(b.toEulerAngles(), eulerRot));
++ QCOMPARE(isDirty(b), false); // Both internal values queried, so neither should be dirty
++
++
++ }
++
++ void tst_RotationDataClass::test_eulerAssign()
++ {
++ RotationData rotation;
++ QCOMPARE(isDirty(rotation), false);
++ QVERIFY(qFuzzyCompare(rotation.toEulerAngles(), QVector3D()));
++ QVERIFY(qFuzzyCompare(rotation.toQuaternion(), QQuaternion()));
++
++ QVector3D eulerRot = QVector3D(1.0f, 0.0f, 0.0f);
++ QQuaternion quatRot = QQuaternion::fromEulerAngles(eulerRot);
++
++ rotation = eulerRot;
++ QVERIFY(qFuzzyCompare(rotation.m_eulerRot, eulerRot));
++ QCOMPARE(isDirty(rotation), true);
++
++ {
++ QCOMPARE(rotation.m_dirty, RotationData::Dirty::Quaternion);
++ QQuaternion retQuatRot = rotation.toQuaternion();
++ QCOMPARE(isDirty(rotation), false);
++ QVERIFY(qFuzzyCompare(retQuatRot, quatRot));
++ QVERIFY(qFuzzyCompare(rotation.m_quatRot, quatRot));
++ }
++ }
++
++ void tst_RotationDataClass::test_quatAssign()
++ {
++ RotationData rotation;
++ QCOMPARE(isDirty(rotation), false);
++ QVERIFY(qFuzzyCompare(rotation.toEulerAngles(), QVector3D()));
++ QVERIFY(qFuzzyCompare(rotation.toQuaternion(), QQuaternion()));
++
++ QVector3D eulerRot = QVector3D(1.0f, 0.0f, 0.0f);
++ QQuaternion quatRot = QQuaternion::fromEulerAngles(eulerRot);
++
++ rotation = quatRot;
++ QVERIFY(qFuzzyCompare(rotation.m_quatRot, quatRot));
++ QCOMPARE(isDirty(rotation), true);
++
++ {
++ QCOMPARE(rotation.m_dirty, RotationData::Dirty::Euler);
++ QVector3D retEulerRot = rotation.toEulerAngles();
++ QCOMPARE(isDirty(rotation), false);
++ QVERIFY(qFuzzyCompare(retEulerRot, eulerRot));
++ QVERIFY(qFuzzyCompare(rotation.m_eulerRot, eulerRot));
++ }
++ }
++
++ void tst_RotationDataClass::test_aba()
++ {
++ // Dirty state should be mutually exclusive and the value
++ // updated appropriately each time
++ // (i.e., not skipping a valid write by testing agains an old value).
++ RotationData rotation; // Init { 0, 0, 0}
++ rotation = QVector3D(1.0f, 0.0f, 0.0f);
++ // Quat dirty
++ rotation = QQuaternion::fromEulerAngles(QVector3D(0.0f, 0.0f, 0.0f));
++ // Euler dirty
++ QCOMPARE(rotation, RotationData{});
++ }
++
++ void tst_RotationDataClass::test_compare()
++ {
++ {
++ RotationData a;
++ RotationData b;
++ QVERIFY(a == b);
++ QVERIFY(b == a);
++
++ a = RotationData(QVector3D(1.0f, 1.0f, 1.0f));
++ QVERIFY(a != b);
++ QVERIFY(b != a);
++
++ a = RotationData(QQuaternion());
++ QVERIFY(a == b);
++ QVERIFY(b == a);
++ }
++
++ {
++ RotationData a;
++ QVector3D b;
++ QVERIFY(a == b);
++ QVERIFY(b == a);
++
++ a = QVector3D(1.0f, 1.0f, 1.0f);
++ QVERIFY(a != b);
++ QVERIFY(b != a);
++
++ a = QQuaternion();
++ QVERIFY(a == b);
++ QVERIFY(b == a);
++ }
++
++ {
++ RotationData a;
++ QQuaternion b;
++ QVERIFY(a == b);
++ QVERIFY(b == a);
++
++ a = QVector3D(1.0f, 1.0f, 1.0f);
++ QVERIFY(a != b);
++ QVERIFY(b != a);
++
++ a = QQuaternion();
++ QVERIFY(a == b);
++ QVERIFY(b == a);
++ }
++ }
++
++ void tst_RotationDataClass::test_compare2()
++ {
++ RotationData a;
++ RotationData b;
++ QVERIFY(a == b);
++ QVERIFY(b == a);
++
++ {
++ const auto v = QVector3D();
++ const auto qa = QQuaternion::fromEulerAngles(v);
++
++ // Quat stored as normalized value ?
++ QQuaternion qaNormalized = qa.normalized();
++ a = qa;
++ QVERIFY(a == qaNormalized);
++ QVERIFY(a == v);
++
++ b = -a.toQuaternion();
++ QVERIFY(a == b);
++ QVERIFY(b == a);
++ }
++
++ {
++ const auto v = QVector3D(0.0f, 0.0f, 45.0f);
++ const auto qa = QQuaternion::fromEulerAngles(v);
++
++ // Quat stored as normalized value ?
++ QQuaternion qaNormalized = qa.normalized();
++ {
++ // Assumption 1: QQuaternion can be non-normalized but the RotationData class always
++ // contains the normalized quaternion.
++ {
++ const auto qaNonNormalized = qa * 30.0f;
++ QVERIFY(!qFuzzyCompare(qaNonNormalized, qaNormalized));
++ {
++ RotationData rd(qaNonNormalized);
++ QVERIFY(rd.m_quatRot == qaNormalized);
++ }
++
++ {
++ RotationData rd;
++ rd = qaNonNormalized;
++ QVERIFY(rd.m_quatRot == qaNormalized);
++ }
++ }
++
++ // Assumption 2: compare or fuzzy compare of QQuaternion is strictly a component by component compare
++ auto qb = -qa;
++ QVERIFY(!qFuzzyCompare(qb, qa));
++ }
++ a = qa;
++ QVERIFY(a == qaNormalized);
++ QVERIFY(a.m_quatRot == qaNormalized); // Compensate for the XFAIL
++ QVERIFY(a == v);
++
++ b = -a.toQuaternion();
++ QVERIFY(a == b);
++ QVERIFY(-a.m_quatRot == b.m_quatRot);
++ QVERIFY(b == a);
++ QVERIFY(b.m_quatRot == -a.m_quatRot);
++ }
++ }
++
++ void tst_RotationDataClass::test_fuzzyQuatCompare()
++ {
++ RotationData a;
++ RotationData b;
++ QVERIFY(a == b);
++ QVERIFY(b == a);
++
++ { // QTBUG-105918 (Verify that the divergence from 1.0f does not result in a negative number).
++ // (i.e., -0.00001 <= e)
++ const QQuaternion qa = QQuaternion(0.5f, 0.5f, 0.5f, 0.5f);
++ const QQuaternion qb = QQuaternion(0.5f + 0.0007f, 0.5f, 0.5f, 0.5f);
++ QVERIFY(qa != qb);
++ QVERIFY(!qFuzzyCompare(qa, qb));
++ a = qa;
++ b = qb;
++ QVERIFY(a != b);
++ b = -qb;
++ QVERIFY(a != b);
++ }
++ }
++
++ void tst_RotationDataClass::test_compare_precision()
++ {
++ // Compare precision between the component-by-component compare and the fuzzyQuaternion compare
++ // These are just for monitoring, it is possible that these fail if e.g., the fudge factor is changed.
++ RotationData a;
++ RotationData b;
++ QVERIFY(a == b);
++ QVERIFY(b == a);
++
++ {
++ const QQuaternion qa = QQuaternion(0.5f, 0.5f, 0.5f, 0.5f);
++ const QQuaternion qb = QQuaternion(0.5f + 0.0007f, 0.5f, 0.5f, 0.5f);
++ QVERIFY(qa != qb);
++ QVERIFY(!qFuzzyCompare(qa, qb));
++ a = qa;
++ b = qb;
++ QVERIFY(a != b);
++ b = -qb;
++ QVERIFY(a != b);
++ }
++ {
++ const QQuaternion qa = QQuaternion(0.5f, 0.5f, 0.5f, 0.5f);
++ const QQuaternion qb = QQuaternion(0.5f, 0.5f + 0.001f, 0.5f, 0.5f);
++ QVERIFY(qa != qb);
++ QVERIFY(!qFuzzyCompare(qa, qb));
++ a = qa;
++ b = qb;
++ QVERIFY(a != b);
++ b = -qb;
++ QVERIFY(a != b);
++ }
++ {
++ const QQuaternion qa = QQuaternion(0.5f, 0.5f, 0.5f, 0.5f);
++ const QQuaternion qb = QQuaternion(0.5f, 0.5f + 0.0007f, 0.5f, 0.5f);
++ QVERIFY(qa != qb);
++ QVERIFY(!qFuzzyCompare(qa, qb));
++ a = qa;
++ b = qb;
++ QVERIFY(a != b);
++ b = -qb;
++ QVERIFY(a != b);
++ }
++ {
++ const QQuaternion qa = QQuaternion(0.5f, 0.5f, 0.5f, 0.5f);
++ const QQuaternion qb = QQuaternion(0.5f, 0.5f + 0.0007f, 0.5f, 0.5f);
++ QVERIFY(qa != qb);
++ QVERIFY(!qFuzzyCompare(qa, qb));
++ a = qa;
++ b = qb;
++ QVERIFY(a != b);
++ b = -qb;
++ QVERIFY(a != b);
++ }
++ {
++ const QQuaternion qa = QQuaternion(1000.f, 0.5f, 0.5f, 0.5f);
++ const QQuaternion qb = QQuaternion(1000.f + 0.000001f, 0.5f + 0.000001f, 0.5f, 0.5f);
++ QVERIFY(qa != qb);
++ QVERIFY(qFuzzyCompare(qa, qb));
++ a = qa;
++ b = qb;
++ QEXPECT_FAIL("", "Difference is too big...", Continue);
++ QVERIFY(a == b);
++ QVERIFY(qFuzzyCompare(a.m_quatRot, b.m_quatRot)); // Compensate for XFAIL
++ b = -qb;
++ QEXPECT_FAIL("", "Negation introduces change", Continue);
++ QVERIFY(a == b);
++ QVERIFY(qFuzzyCompare(-a.m_quatRot, b.m_quatRot)); // Compensate for XFAIL
++ }
++ // RotationData is preciser than qFuzzyCompare(QQuaternion)
++ {
++ const float e = std::numeric_limits<float>::epsilon();
++ float fa = 1.0f;
++ float fb = fa + e;
++ QVERIFY(fa != fb);
++ const QQuaternion qa = QQuaternion(1.0f, fa, 0.f, 0.f);
++ const QQuaternion qb = QQuaternion(1.0f, fb, 0.f, 0.f);
++
++ QVERIFY(qa != qb); // Component wise compare
++ QVERIFY(qFuzzyCompare(qa, qb));
++ a = qa;
++ b = qb;
++ QVERIFY(a != b);
++ b = -qb;
++ QVERIFY(a != b);
++ { // Push QQuaternion past it's limits and verify that RotationData still picks up the difference
++ const auto qad = qa / 2.0f;
++ const auto qbd = qa / 2.0f;
++
++ QVERIFY(qad == qbd); // Component wise compare (now below the threshold).
++ QVERIFY(qFuzzyCompare(qad, qbd));
++ a = qad;
++ b = qbd;
++ QVERIFY(a.m_quatRot == b.m_quatRot);
++ // When float components are identical after normalization, RotationData correctly reports equal
++ QVERIFY(a == b);
++ b = -qbd;
++ QVERIFY(a == b);
++ }
++ }
++ {
++ const QQuaternion qa = QQuaternion(0.5f, 0.5f, 0.5f, 0.5f);
++ const QQuaternion qb = QQuaternion(0.5f, 0.5f + 0.000001f, 0.5f, 0.5f);
++ QVERIFY(qa != qb);
++ QVERIFY(qFuzzyCompare(qa, qb));
++ a = qa;
++ b = qb;
++ QVERIFY(a != b);
++ b = -qb;
++ QVERIFY(a != b);
++ }
++ {
++ const QQuaternion qa = QQuaternion(0.5f, 0.5f, 0.5f, 0.5f);
++ const QQuaternion qb = QQuaternion(0.5f + 0.000001f, 0.5f + 0.000001f, 0.5f, 0.5f);
++ QVERIFY(qa != qb);
++ QVERIFY(qFuzzyCompare(qa, qb));
++ a = qa;
++ b = qb;
++ QVERIFY(a != b);
++ b = -qb;
++ QVERIFY(a != b);
++ }
++
++ // As above but with a non-normalized quaternion, to verify that the precision
++ // is not lost when normalizing the quaternion in the constructor or assignment operator.
++ {
++ const QQuaternion qa = QQuaternion::fromEulerAngles(QVector3D(45.0f, 30.0f, 15.0f));
++ const QQuaternion qb = qa * (1.0f + 1e-6f);
++
++ RotationData rd;
++ rd = qb;
++ QVERIFY(rd == qb);
++ QVERIFY(qb == rd);
++ QVERIFY(rd == -qb);
++ QVERIFY(-qb == rd);
++ }
++ }
++
++ QTEST_APPLESS_MAIN(tst_RotationDataClass)
++
++ #include "tst_rotation.moc"