From 93ea68a23be2a64cddf3b06354ce980c23ffd11c Mon Sep 17 00:00:00 2001 From: Benjamin Nauck Date: Thu, 10 Apr 2025 22:14:32 +0200 Subject: [PATCH] Gui: Add tests to automatically verify the axonometric views --- src/Gui/Camera.cpp | 98 +++------------- tests/CMakeLists.txt | 1 + tests/src/Gui/CMakeLists.txt | 1 + tests/src/Gui/Camera.cpp | 215 +++++++++++++++++++++++++++++++++++ 4 files changed, 232 insertions(+), 83 deletions(-) create mode 100644 tests/src/Gui/Camera.cpp diff --git a/src/Gui/Camera.cpp b/src/Gui/Camera.cpp index e075154a3b..f6481a076f 100644 --- a/src/Gui/Camera.cpp +++ b/src/Gui/Camera.cpp @@ -29,70 +29,6 @@ using namespace Gui; -/** - Formulas to get quaternion for axonometric views: - - \code -from math import sqrt, degrees, asin, atan -p1=App.Rotation(App.Vector(1,0,0),90) -p2=App.Rotation(App.Vector(0,0,1),alpha) -p3=App.Rotation(p2.multVec(App.Vector(1,0,0)),beta) -p4=p3.multiply(p2).multiply(p1) - -from pivy import coin -c=Gui.ActiveDocument.ActiveView.getCameraNode() -c.orientation.setValue(*p4.Q) - \endcode - - The angles alpha and beta depend on the type of axonometry - Isometric: - \code -alpha=45 -beta=degrees(asin(-sqrt(1.0/3.0))) - \endcode - - Dimetric: - \code -alpha=degrees(asin(sqrt(1.0/8.0))) -beta=degrees(-asin(1.0/3.0)) - \endcode - - Trimetric: - \code -alpha=30.0 -beta=-35.0 - \endcode - - Verification code that the axonomtries are correct: - - \code -from pivy import coin -c=Gui.ActiveDocument.ActiveView.getCameraNode() -vo=App.Vector(c.getViewVolume().getMatrix().multVecMatrix(coin.SbVec3f(0,0,0)).getValue()) -vx=App.Vector(c.getViewVolume().getMatrix().multVecMatrix(coin.SbVec3f(10,0,0)).getValue()) -vy=App.Vector(c.getViewVolume().getMatrix().multVecMatrix(coin.SbVec3f(0,10,0)).getValue()) -vz=App.Vector(c.getViewVolume().getMatrix().multVecMatrix(coin.SbVec3f(0,0,10)).getValue()) -(vx-vo).Length -(vy-vo).Length -(vz-vo).Length - -# Projection -vo.z=0 -vx.z=0 -vy.z=0 -vz.z=0 - -(vx-vo).Length -(vy-vo).Length -(vz-vo).Length - \endcode - - See also: - http://www.mathematik.uni-marburg.de/~thormae/lectures/graphics1/graphics_6_2_ger_web.html#1 - http://www.mathematik.uni-marburg.de/~thormae/lectures/graphics1/code_v2/Axonometric/qt/Axonometric.cpp - https://de.wikipedia.org/wiki/Arkussinus_und_Arkuskosinus -*/ - SbRotation Camera::top() { return {0, 0, 0, 1}; @@ -127,35 +63,31 @@ SbRotation Camera::left() SbRotation Camera::isometric() { - //from math import sqrt, degrees, asin - //p1=App.Rotation(App.Vector(1,0,0),45) - //p2=App.Rotation(App.Vector(0,0,1),-45) - //p3=p2.multiply(p1) - //return SbRotation(0.353553f, -0.146447f, -0.353553f, 0.853553f); - - //from math import sqrt, degrees, asin - //p1=App.Rotation(App.Vector(1,0,0),90) - //p2=App.Rotation(App.Vector(0,0,1),135) - //p3=App.Rotation(App.Vector(-1,1,0),degrees(asin(-sqrt(1.0/3.0)))) - //p4=p3.multiply(p2).multiply(p1) - //return SbRotation(0.17592, 0.424708, 0.820473, 0.339851); - - //from math import sqrt, degrees, asin - //p1=App.Rotation(App.Vector(1,0,0),90) - //p2=App.Rotation(App.Vector(0,0,1),45) - //#p3=App.Rotation(App.Vector(1,1,0),45) - //p3=App.Rotation(App.Vector(1,1,0),degrees(asin(-sqrt(1.0/3.0)))) - //p4=p3.multiply(p2).multiply(p1) + // The values here are precalculated as our quaternion implementation + // does not support calculating the values in compile time. + // The values are verified with unit tests. return {0.424708F, 0.17592F, 0.339851F, 0.820473F}; } SbRotation Camera::dimetric() { + // The values here are precalculated as our quaternion implementation + // does not support calculating the values in compile time. + // The values are verified with unit tests. + + // While there are multiple ways to calculate the dimetric rotation, + // we use one which is similar to other CAD applications. return {0.567952F, 0.103751F, 0.146726F, 0.803205F}; } SbRotation Camera::trimetric() { + // The values here are precalculated as our quaternion implementation + // does not support calculating the values in compile time. + // The values are verified with unit tests. + + // While there are multiple ways to calculate the trimetric rotation, + // we use one which is similar to other CAD applications. return {0.446015F, 0.119509F, 0.229575F, 0.856787F}; } diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 5d45e4d8d9..6feae931e7 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -134,6 +134,7 @@ target_link_libraries(Tests_run gmock_main ${Google_Tests_LIBS} FreeCADApp + FreeCADGui ) include(GoogleTest) diff --git a/tests/src/Gui/CMakeLists.txt b/tests/src/Gui/CMakeLists.txt index d507dde3b6..774cf35f31 100644 --- a/tests/src/Gui/CMakeLists.txt +++ b/tests/src/Gui/CMakeLists.txt @@ -1,6 +1,7 @@ # Standard C++ GTest tests target_sources(Tests_run PRIVATE Assistant.cpp + Camera.cpp ) # Qt tests diff --git a/tests/src/Gui/Camera.cpp b/tests/src/Gui/Camera.cpp new file mode 100644 index 0000000000..5e1230c2ff --- /dev/null +++ b/tests/src/Gui/Camera.cpp @@ -0,0 +1,215 @@ +#include + +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +/* + This comment was previously used to get the hard coded axonometric view quaternions + in the Camera class. + This has since been replaced with unit tests that verify the correctness of the + quaternion calculations. + + The old code is kept for reference and to show how the quaternions were calculated. + + --- + + Formulas to get quaternion for axonometric views: + + \code +from math import sqrt, degrees, asin, atan +p1=App.Rotation(App.Vector(1,0,0),90) +p2=App.Rotation(App.Vector(0,0,1),alpha) +p3=App.Rotation(p2.multVec(App.Vector(1,0,0)),beta) +p4=p3.multiply(p2).multiply(p1) + +from pivy import coin +c=Gui.ActiveDocument.ActiveView.getCameraNode() +c.orientation.setValue(*p4.Q) + \endcode + + The angles alpha and beta depend on the type of axonometry + Isometric: + \code +alpha=45 +beta=degrees(asin(-sqrt(1.0/3.0))) + \endcode + + Dimetric: + \code +alpha=degrees(asin(sqrt(1.0/8.0))) +beta=degrees(-asin(1.0/3.0)) + \endcode + + Trimetric: + \code +alpha=30.0 +beta=-35.0 + \endcode + + Verification code that the axonomtries are correct: + + \code +from pivy import coin +c=Gui.ActiveDocument.ActiveView.getCameraNode() +vo=App.Vector(c.getViewVolume().getMatrix().multVecMatrix(coin.SbVec3f(0,0,0)).getValue()) +vx=App.Vector(c.getViewVolume().getMatrix().multVecMatrix(coin.SbVec3f(10,0,0)).getValue()) +vy=App.Vector(c.getViewVolume().getMatrix().multVecMatrix(coin.SbVec3f(0,10,0)).getValue()) +vz=App.Vector(c.getViewVolume().getMatrix().multVecMatrix(coin.SbVec3f(0,0,10)).getValue()) +(vx-vo).Length +(vy-vo).Length +(vz-vo).Length + +# Projection +vo.z=0 +vx.z=0 +vy.z=0 +vz.z=0 + +(vx-vo).Length +(vy-vo).Length +(vz-vo).Length + \endcode + + See also: + http://www.mathematik.uni-marburg.de/~thormae/lectures/graphics1/graphics_6_2_ger_web.html#1 + http://www.mathematik.uni-marburg.de/~thormae/lectures/graphics1/code_v2/Axonometric/qt/Axonometric.cpp + https://de.wikipedia.org/wiki/Arkussinus_und_Arkuskosinus +*/ + +using Base::convertTo; +using Base::Rotation; +using Base::toRadians; +using Base::Vector3d; + +namespace +{ + +Rotation buildAxonometricRotation(double alphaRad, double betaRad) +{ + const auto p1 = Rotation(Vector3d::UnitX, toRadians(90.0)); + const auto p2 = Rotation(Vector3d::UnitZ, alphaRad); + const auto p3 = Rotation(p2.multVec(Vector3d::UnitX), betaRad); + const auto p4 = p3 * p2 * p1; + + return p4; +} + +// Returns a tuple of 2D lengths of X, Y, Z unit vectors after applying rotation +std::array getProjectedLengths(const SbRotation& rot) +{ + // Set up a simple view volume to test the projection of the unit vectors. + // The actual values don't matter much, as we are only interested in the + // relative lengths of the projected vectors. + SbViewVolume volume; + // left, right, bottom, top, near, far + volume.ortho(-10, 10, -10, 10, -10, 10); + + volume.rotateCamera(rot); + const auto matrix = volume.getMatrix(); + + // Get the transformed unit vectors + SbVec3f vo, vx, vy, vz; + matrix.multVecMatrix(SbVec3f(0, 0, 0), vo); + matrix.multVecMatrix(SbVec3f(10, 0, 0), vx); + matrix.multVecMatrix(SbVec3f(0, 10, 0), vy); + matrix.multVecMatrix(SbVec3f(0, 0, 10), vz); + + // Project to XY plane by setting Z to 0 + vo[2] = 0; + vx[2] = 0; + vy[2] = 0; + vz[2] = 0; + + // Return the lengths of the projected vectors + return {(vx - vo).length(), (vy - vo).length(), (vz - vo).length()}; +} + +} // namespace + +TEST(CameraPrecalculatedQuaternions, testIsometric) +{ + // Use the formula to get the isometric rotation + double alpha = toRadians(45.0f); + double beta = std::asin(-std::sqrt(1.0 / 3.0)); + + const Rotation actual = buildAxonometricRotation(alpha, beta); + const Rotation expected = convertTo(Gui::Camera::isometric()); + + EXPECT_TRUE(actual.isSame(expected, 1e-6)); +} + +TEST(CameraPrecalculatedQuaternions, testDimetric) +{ + // Use the formula to get the dimetric rotation + double alpha = std::asin(std::sqrt(1.0 / 8.0)); + double beta = -std::asin(1.0 / 3.0); + + const Rotation actual = buildAxonometricRotation(alpha, beta); + const Rotation expected = convertTo(Gui::Camera::dimetric()); + + EXPECT_TRUE(actual.isSame(expected, 1e-6)); +} + +TEST(CameraPrecalculatedQuaternions, testTrimetric) +{ + // Use the formula to get the trimetric rotation + double alpha = toRadians(30.0); + double beta = toRadians(-35.0); + + const Rotation actual = buildAxonometricRotation(alpha, beta); + const Rotation expected = convertTo(Gui::Camera::trimetric()); + + EXPECT_TRUE(actual.isSame(expected, 1e-6)); +} + +TEST(CameraRotation, testIsometricProjection) +{ + auto rot = Gui::Camera::isometric(); + auto lengths = getProjectedLengths(rot); + + // In isometric, expect all lengths to be roughly equal + EXPECT_NEAR(lengths[0], lengths[1], 1e-6); // X == Y + EXPECT_NEAR(lengths[0], lengths[2], 1e-6); // X == Z + EXPECT_NEAR(lengths[1], lengths[2], 1e-6); // Y == Z +} + +TEST(CameraRotation, testDimetricProjection) +{ + const auto rot = Gui::Camera::dimetric(); + const auto lengths = getProjectedLengths(rot); + + // In dimetric, expect two lengths to be roughly equal, one different + const std::initializer_list> pairs = { + {lengths[0], lengths[1]}, + {lengths[1], lengths[2]}, + {lengths[0], lengths[2]}, + }; + + constexpr double tolerance = 1e-6; + const auto isSimilar = [&](std::pair lengths) -> bool { + return std::abs(lengths.first - lengths.second) < tolerance; + }; + + unsigned similarCount = std::ranges::count_if(pairs, isSimilar); + + EXPECT_EQ(similarCount, 1); // Exactly two are equal +} + +TEST(CameraRotation, testTrimetricProjection) +{ + auto rot = Gui::Camera::trimetric(); + auto lengths = getProjectedLengths(rot); + + // In trimetric, all should differ significantly + EXPECT_GT(std::abs(lengths[0] - lengths[1]), 1e-3); + EXPECT_GT(std::abs(lengths[1] - lengths[2]), 1e-3); + EXPECT_GT(std::abs(lengths[0] - lengths[2]), 1e-3); +}