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 | |