| 1 | /**************************************************************************** |
| 2 | ** |
| 3 | ** Copyright (C) 2016 The Qt Company Ltd. |
| 4 | ** Contact: https://www.qt.io/licensing/ |
| 5 | ** |
| 6 | ** This file is part of the test suite of the Qt Toolkit. |
| 7 | ** |
| 8 | ** $QT_BEGIN_LICENSE:GPL-EXCEPT$ |
| 9 | ** Commercial License Usage |
| 10 | ** Licensees holding valid commercial Qt licenses may use this file in |
| 11 | ** accordance with the commercial license agreement provided with the |
| 12 | ** Software or, alternatively, in accordance with the terms contained in |
| 13 | ** a written agreement between you and The Qt Company. For licensing terms |
| 14 | ** and conditions see https://www.qt.io/terms-conditions. For further |
| 15 | ** information use the contact form at https://www.qt.io/contact-us. |
| 16 | ** |
| 17 | ** GNU General Public License Usage |
| 18 | ** Alternatively, this file may be used under the terms of the GNU |
| 19 | ** General Public License version 3 as published by the Free Software |
| 20 | ** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT |
| 21 | ** included in the packaging of this file. Please review the following |
| 22 | ** information to ensure the GNU General Public License requirements will |
| 23 | ** be met: https://www.gnu.org/licenses/gpl-3.0.html. |
| 24 | ** |
| 25 | ** $QT_END_LICENSE$ |
| 26 | ** |
| 27 | ****************************************************************************/ |
| 28 | |
| 29 | |
| 30 | #include <QtTest/QtTest> |
| 31 | #include <qgraphicsitem.h> |
| 32 | #include <qgraphicstransform.h> |
| 33 | |
| 34 | class tst_QGraphicsTransform : public QObject { |
| 35 | Q_OBJECT |
| 36 | |
| 37 | private slots: |
| 38 | void scale(); |
| 39 | void rotation(); |
| 40 | void rotation3d_data(); |
| 41 | void rotation3d(); |
| 42 | void rotation3dArbitraryAxis_data(); |
| 43 | void rotation3dArbitraryAxis(); |
| 44 | |
| 45 | private: |
| 46 | QString toString(QTransform const&); |
| 47 | }; |
| 48 | |
| 49 | static QTransform transform2D(const QGraphicsTransform& t) |
| 50 | { |
| 51 | QMatrix4x4 m; |
| 52 | t.applyTo(matrix: &m); |
| 53 | return m.toTransform(); |
| 54 | } |
| 55 | |
| 56 | void tst_QGraphicsTransform::scale() |
| 57 | { |
| 58 | QGraphicsScale scale; |
| 59 | |
| 60 | // check initial conditions |
| 61 | QCOMPARE(scale.xScale(), qreal(1)); |
| 62 | QCOMPARE(scale.yScale(), qreal(1)); |
| 63 | QCOMPARE(scale.zScale(), qreal(1)); |
| 64 | QCOMPARE(scale.origin(), QVector3D(0, 0, 0)); |
| 65 | |
| 66 | scale.setOrigin(QVector3D(10, 10, 0)); |
| 67 | |
| 68 | QCOMPARE(scale.xScale(), qreal(1)); |
| 69 | QCOMPARE(scale.yScale(), qreal(1)); |
| 70 | QCOMPARE(scale.zScale(), qreal(1)); |
| 71 | QCOMPARE(scale.origin(), QVector3D(10, 10, 0)); |
| 72 | |
| 73 | QMatrix4x4 t; |
| 74 | scale.applyTo(matrix: &t); |
| 75 | |
| 76 | QCOMPARE(t, QMatrix4x4()); |
| 77 | QCOMPARE(transform2D(scale), QTransform()); |
| 78 | |
| 79 | scale.setXScale(10); |
| 80 | scale.setOrigin(QVector3D(0, 0, 0)); |
| 81 | |
| 82 | QCOMPARE(scale.xScale(), qreal(10)); |
| 83 | QCOMPARE(scale.yScale(), qreal(1)); |
| 84 | QCOMPARE(scale.zScale(), qreal(1)); |
| 85 | QCOMPARE(scale.origin(), QVector3D(0, 0, 0)); |
| 86 | |
| 87 | QTransform res; |
| 88 | res.scale(sx: 10, sy: 1); |
| 89 | |
| 90 | QCOMPARE(transform2D(scale), res); |
| 91 | QCOMPARE(transform2D(scale).map(QPointF(10, 10)), QPointF(100, 10)); |
| 92 | |
| 93 | scale.setOrigin(QVector3D(10, 10, 0)); |
| 94 | QCOMPARE(transform2D(scale).map(QPointF(10, 10)), QPointF(10, 10)); |
| 95 | QCOMPARE(transform2D(scale).map(QPointF(11, 10)), QPointF(20, 10)); |
| 96 | |
| 97 | scale.setYScale(2); |
| 98 | scale.setZScale(4.5); |
| 99 | scale.setOrigin(QVector3D(1, 2, 3)); |
| 100 | |
| 101 | QCOMPARE(scale.xScale(), qreal(10)); |
| 102 | QCOMPARE(scale.yScale(), qreal(2)); |
| 103 | QCOMPARE(scale.zScale(), qreal(4.5)); |
| 104 | QCOMPARE(scale.origin(), QVector3D(1, 2, 3)); |
| 105 | |
| 106 | QMatrix4x4 t2; |
| 107 | scale.applyTo(matrix: &t2); |
| 108 | |
| 109 | QCOMPARE(t2.map(QVector3D(4, 5, 6)), QVector3D(31, 8, 16.5)); |
| 110 | |
| 111 | // Because the origin has a non-zero z, mapping (4, 5) in 2D |
| 112 | // will introduce a projective component into the result. |
| 113 | QTransform t3 = t2.toTransform(); |
| 114 | QCOMPARE(t3.map(QPointF(4, 5)), QPointF(31 / t3.m33(), 8 / t3.m33())); |
| 115 | } |
| 116 | |
| 117 | // fuzzyCompareNonZero is a very slightly looser version of qFuzzyCompare |
| 118 | // for use with values that are not very close to zero |
| 119 | Q_DECL_CONSTEXPR static inline bool fuzzyCompareNonZero(float p1, float p2) |
| 120 | { |
| 121 | return (qAbs(t: p1 - p2) <= 0.00003f * qMin(a: qAbs(t: p1), b: qAbs(t: p2))); |
| 122 | } |
| 123 | |
| 124 | // This is a more tolerant version of qFuzzyCompare that also handles the case |
| 125 | // where one or more of the values being compare are close to zero |
| 126 | static inline bool fuzzyCompare(float p1, float p2) |
| 127 | { |
| 128 | if (qFuzzyIsNull(f: p1)) |
| 129 | return qFuzzyIsNull(f: p2); |
| 130 | else if (qFuzzyIsNull(f: p2)) |
| 131 | return false; |
| 132 | else |
| 133 | return fuzzyCompareNonZero(p1, p2); |
| 134 | } |
| 135 | |
| 136 | // This compares two QTransforms by casting the elements to float. This is |
| 137 | // necessary here because in this test one of the transforms is created from |
| 138 | // a QMatrix4x4 which uses float storage. |
| 139 | static bool fuzzyCompareAsFloat(const QTransform& t1, const QTransform& t2) |
| 140 | { |
| 141 | return fuzzyCompare(p1: float(t1.m11()), p2: float(t2.m11())) && |
| 142 | fuzzyCompare(p1: float(t1.m12()), p2: float(t2.m12())) && |
| 143 | fuzzyCompare(p1: float(t1.m13()), p2: float(t2.m13())) && |
| 144 | fuzzyCompare(p1: float(t1.m21()), p2: float(t2.m21())) && |
| 145 | fuzzyCompare(p1: float(t1.m22()), p2: float(t2.m22())) && |
| 146 | fuzzyCompare(p1: float(t1.m23()), p2: float(t2.m23())) && |
| 147 | fuzzyCompare(p1: float(t1.m31()), p2: float(t2.m31())) && |
| 148 | fuzzyCompare(p1: float(t1.m32()), p2: float(t2.m32())) && |
| 149 | fuzzyCompare(p1: float(t1.m33()), p2: float(t2.m33())); |
| 150 | } |
| 151 | |
| 152 | static inline bool fuzzyCompare(const QMatrix4x4& m1, const QMatrix4x4& m2) |
| 153 | { |
| 154 | bool ok = true; |
| 155 | for (int y = 0; y < 4; ++y) |
| 156 | for (int x = 0; x < 4; ++x) |
| 157 | ok &= fuzzyCompare(p1: m1(y, x), p2: m2(y, x)); |
| 158 | return ok; |
| 159 | } |
| 160 | |
| 161 | void tst_QGraphicsTransform::rotation() |
| 162 | { |
| 163 | QGraphicsRotation rotation; |
| 164 | QCOMPARE(rotation.axis(), QVector3D(0, 0, 1)); |
| 165 | QCOMPARE(rotation.origin(), QVector3D(0, 0, 0)); |
| 166 | QCOMPARE(rotation.angle(), (qreal)0); |
| 167 | |
| 168 | rotation.setOrigin(QVector3D(10, 10, 0)); |
| 169 | |
| 170 | QCOMPARE(rotation.axis(), QVector3D(0, 0, 1)); |
| 171 | QCOMPARE(rotation.origin(), QVector3D(10, 10, 0)); |
| 172 | QCOMPARE(rotation.angle(), (qreal)0); |
| 173 | |
| 174 | QMatrix4x4 t; |
| 175 | rotation.applyTo(matrix: &t); |
| 176 | |
| 177 | QCOMPARE(t, QMatrix4x4()); |
| 178 | QCOMPARE(transform2D(rotation), QTransform()); |
| 179 | |
| 180 | rotation.setAngle(40); |
| 181 | rotation.setOrigin(QVector3D(0, 0, 0)); |
| 182 | |
| 183 | QCOMPARE(rotation.axis(), QVector3D(0, 0, 1)); |
| 184 | QCOMPARE(rotation.origin(), QVector3D(0, 0, 0)); |
| 185 | QCOMPARE(rotation.angle(), (qreal)40); |
| 186 | |
| 187 | QTransform res; |
| 188 | res.rotate(a: 40); |
| 189 | |
| 190 | QVERIFY(fuzzyCompareAsFloat(transform2D(rotation), res)); |
| 191 | |
| 192 | rotation.setOrigin(QVector3D(10, 10, 0)); |
| 193 | rotation.setAngle(90); |
| 194 | QCOMPARE(transform2D(rotation).map(QPointF(10, 10)), QPointF(10, 10)); |
| 195 | QCOMPARE(transform2D(rotation).map(QPointF(20, 10)), QPointF(10, 20)); |
| 196 | |
| 197 | rotation.setOrigin(QVector3D(0, 0, 0)); |
| 198 | rotation.setAngle(qQNaN()); |
| 199 | QCOMPARE(transform2D(rotation).map(QPointF(20, 10)), QPointF(20, 10)); |
| 200 | } |
| 201 | |
| 202 | Q_DECLARE_METATYPE(Qt::Axis); |
| 203 | void tst_QGraphicsTransform::rotation3d_data() |
| 204 | { |
| 205 | QTest::addColumn<Qt::Axis>(name: "axis" ); |
| 206 | QTest::addColumn<qreal>(name: "angle" ); |
| 207 | |
| 208 | for (int angle = 0; angle <= 360; angle++) { |
| 209 | QTest::newRow(dataTag: "test rotation on X" ) << Qt::XAxis << qreal(angle); |
| 210 | QTest::newRow(dataTag: "test rotation on Y" ) << Qt::YAxis << qreal(angle); |
| 211 | QTest::newRow(dataTag: "test rotation on Z" ) << Qt::ZAxis << qreal(angle); |
| 212 | } |
| 213 | } |
| 214 | |
| 215 | void tst_QGraphicsTransform::rotation3d() |
| 216 | { |
| 217 | QFETCH(Qt::Axis, axis); |
| 218 | QFETCH(qreal, angle); |
| 219 | |
| 220 | QGraphicsRotation rotation; |
| 221 | rotation.setAxis(axis); |
| 222 | |
| 223 | QMatrix4x4 t; |
| 224 | rotation.applyTo(matrix: &t); |
| 225 | |
| 226 | QVERIFY(t.isIdentity()); |
| 227 | QVERIFY(transform2D(rotation).isIdentity()); |
| 228 | |
| 229 | rotation.setAngle(angle); |
| 230 | |
| 231 | // QGraphicsRotation uses a correct mathematical rotation in 3D. |
| 232 | // QTransform's Qt::YAxis rotation is inverted from the mathematical |
| 233 | // version of rotation. We correct for that here. |
| 234 | QTransform expected; |
| 235 | if (axis == Qt::YAxis && angle != 180.) |
| 236 | expected.rotate(a: -angle, axis); |
| 237 | else |
| 238 | expected.rotate(a: angle, axis); |
| 239 | |
| 240 | QVERIFY(fuzzyCompareAsFloat(transform2D(rotation), expected)); |
| 241 | |
| 242 | // Check that "rotation" produces the 4x4 form of the 3x3 matrix. |
| 243 | // i.e. third row and column are 0 0 1 0. |
| 244 | t.setToIdentity(); |
| 245 | rotation.applyTo(matrix: &t); |
| 246 | QMatrix4x4 r(expected); |
| 247 | QVERIFY(fuzzyCompare(t, r)); |
| 248 | |
| 249 | //now let's check that a null vector will not change the transform |
| 250 | rotation.setAxis(QVector3D(0, 0, 0)); |
| 251 | rotation.setOrigin(QVector3D(10, 10, 0)); |
| 252 | |
| 253 | t.setToIdentity(); |
| 254 | rotation.applyTo(matrix: &t); |
| 255 | |
| 256 | QVERIFY(t.isIdentity()); |
| 257 | QVERIFY(transform2D(rotation).isIdentity()); |
| 258 | |
| 259 | rotation.setAngle(angle); |
| 260 | |
| 261 | QVERIFY(t.isIdentity()); |
| 262 | QVERIFY(transform2D(rotation).isIdentity()); |
| 263 | |
| 264 | rotation.setOrigin(QVector3D(0, 0, 0)); |
| 265 | |
| 266 | QVERIFY(t.isIdentity()); |
| 267 | QVERIFY(transform2D(rotation).isIdentity()); |
| 268 | } |
| 269 | |
| 270 | QByteArray labelForTest(QVector3D const& axis, int angle) { |
| 271 | return QString("rotation of %1 on (%2, %3, %4)" ) |
| 272 | .arg(a: angle) |
| 273 | .arg(a: axis.x()) |
| 274 | .arg(a: axis.y()) |
| 275 | .arg(a: axis.z()) |
| 276 | .toLatin1(); |
| 277 | } |
| 278 | |
| 279 | void tst_QGraphicsTransform::rotation3dArbitraryAxis_data() |
| 280 | { |
| 281 | QTest::addColumn<QVector3D>(name: "axis" ); |
| 282 | QTest::addColumn<qreal>(name: "angle" ); |
| 283 | |
| 284 | QVector3D axis1 = QVector3D(1.0f, 1.0f, 1.0f); |
| 285 | QVector3D axis2 = QVector3D(2.0f, -3.0f, 0.5f); |
| 286 | QVector3D axis3 = QVector3D(-2.0f, 0.0f, -0.5f); |
| 287 | QVector3D axis4 = QVector3D(0.0001f, 0.0001f, 0.0001f); |
| 288 | QVector3D axis5 = QVector3D(0.01f, 0.01f, 0.01f); |
| 289 | |
| 290 | for (int angle = 0; angle <= 360; angle++) { |
| 291 | QTest::newRow(dataTag: labelForTest(axis: axis1, angle).constData()) << axis1 << qreal(angle); |
| 292 | QTest::newRow(dataTag: labelForTest(axis: axis2, angle).constData()) << axis2 << qreal(angle); |
| 293 | QTest::newRow(dataTag: labelForTest(axis: axis3, angle).constData()) << axis3 << qreal(angle); |
| 294 | QTest::newRow(dataTag: labelForTest(axis: axis4, angle).constData()) << axis4 << qreal(angle); |
| 295 | QTest::newRow(dataTag: labelForTest(axis: axis5, angle).constData()) << axis5 << qreal(angle); |
| 296 | } |
| 297 | } |
| 298 | |
| 299 | void tst_QGraphicsTransform::rotation3dArbitraryAxis() |
| 300 | { |
| 301 | QFETCH(QVector3D, axis); |
| 302 | QFETCH(qreal, angle); |
| 303 | |
| 304 | QGraphicsRotation rotation; |
| 305 | rotation.setAxis(axis); |
| 306 | |
| 307 | QMatrix4x4 t; |
| 308 | rotation.applyTo(matrix: &t); |
| 309 | |
| 310 | QVERIFY(t.isIdentity()); |
| 311 | QVERIFY(transform2D(rotation).isIdentity()); |
| 312 | |
| 313 | rotation.setAngle(angle); |
| 314 | |
| 315 | // Compute the expected answer using QMatrix4x4 and a projection. |
| 316 | // These two steps are performed in one hit by QGraphicsRotation. |
| 317 | QMatrix4x4 exp; |
| 318 | exp.rotate(angle, vector: axis); |
| 319 | QTransform expected = exp.toTransform(distanceToPlane: 1024.0f); |
| 320 | |
| 321 | QTransform actual = transform2D(t: rotation); |
| 322 | QVERIFY2(fuzzyCompareAsFloat(actual, expected), qPrintable( |
| 323 | QString("\nactual: %1\n" |
| 324 | "expected: %2" ) |
| 325 | .arg(toString(actual)) |
| 326 | .arg(toString(expected)) |
| 327 | )); |
| 328 | |
| 329 | // Check that "rotation" produces the 4x4 form of the 3x3 matrix. |
| 330 | // i.e. third row and column are 0 0 1 0. |
| 331 | t.setToIdentity(); |
| 332 | rotation.applyTo(matrix: &t); |
| 333 | QMatrix4x4 r(expected); |
| 334 | for (int row = 0; row < 4; ++row) { |
| 335 | for (int col = 0; col < 4; ++col) { |
| 336 | float a = t(row, col); |
| 337 | float b = r(row, col); |
| 338 | QVERIFY2(fuzzyCompare(a, b), QString("%1 is not equal to %2" ).arg(a).arg(b).toLatin1()); |
| 339 | } |
| 340 | } |
| 341 | } |
| 342 | |
| 343 | QString tst_QGraphicsTransform::toString(QTransform const& t) |
| 344 | { |
| 345 | return QString("[ [ %1 %2 %3 ]; [ %4 %5 %6 ]; [ %7 %8 %9 ] ]" ) |
| 346 | .arg(a: t.m11()) |
| 347 | .arg(a: t.m12()) |
| 348 | .arg(a: t.m13()) |
| 349 | .arg(a: t.m21()) |
| 350 | .arg(a: t.m22()) |
| 351 | .arg(a: t.m23()) |
| 352 | .arg(a: t.m31()) |
| 353 | .arg(a: t.m32()) |
| 354 | .arg(a: t.m33()) |
| 355 | ; |
| 356 | } |
| 357 | |
| 358 | |
| 359 | QTEST_MAIN(tst_QGraphicsTransform) |
| 360 | #include "tst_qgraphicstransform.moc" |
| 361 | |
| 362 | |