diff --git a/Build/xcode5/PlayRho.xcodeproj/project.pbxproj b/Build/xcode5/PlayRho.xcodeproj/project.pbxproj index ebeeed786..fc8c5b181 100644 --- a/Build/xcode5/PlayRho.xcodeproj/project.pbxproj +++ b/Build/xcode5/PlayRho.xcodeproj/project.pbxproj @@ -402,6 +402,7 @@ 9919883A2AD5FEE80076F969 /* IslandStats.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 991988392AD5FEE80076F969 /* IslandStats.cpp */; }; 9919883C2ADAFEE20076F969 /* CheckedMath.hpp in Headers */ = {isa = PBXBuildFile; fileRef = 9919883B2ADAFEE20076F969 /* CheckedMath.hpp */; }; 9921FFD729AD43C30043F23F /* TypeInfo.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 9921FFD629AD43C30043F23F /* TypeInfo.cpp */; }; + 99294BD12BDEBCFE00322777 /* UnitMagnitudeChecker.hpp in Headers */ = {isa = PBXBuildFile; fileRef = 99294BD02BDEBCFE00322777 /* UnitMagnitudeChecker.hpp */; }; 996B7B832A6DF1A500A8207F /* PoolMemoryResource.hpp in Headers */ = {isa = PBXBuildFile; fileRef = 996B7B7E2A6DF1A400A8207F /* PoolMemoryResource.hpp */; }; 996B7B842A6DF1A500A8207F /* MemoryResource.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 996B7B7F2A6DF1A400A8207F /* MemoryResource.cpp */; }; 996B7B852A6DF1A500A8207F /* MemoryResource.hpp in Headers */ = {isa = PBXBuildFile; fileRef = 996B7B802A6DF1A400A8207F /* MemoryResource.hpp */; }; @@ -908,6 +909,7 @@ 9919883B2ADAFEE20076F969 /* CheckedMath.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = CheckedMath.hpp; sourceTree = ""; }; 991988C22AE2D46F0076F969 /* Settings.hpp.in */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = Settings.hpp.in; sourceTree = ""; }; 9921FFD629AD43C30043F23F /* TypeInfo.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = TypeInfo.cpp; sourceTree = ""; }; + 99294BD02BDEBCFE00322777 /* UnitMagnitudeChecker.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = UnitMagnitudeChecker.hpp; sourceTree = ""; }; 996B7B7E2A6DF1A400A8207F /* PoolMemoryResource.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = PoolMemoryResource.hpp; sourceTree = ""; }; 996B7B7F2A6DF1A400A8207F /* MemoryResource.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = MemoryResource.cpp; sourceTree = ""; }; 996B7B802A6DF1A400A8207F /* MemoryResource.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = MemoryResource.hpp; sourceTree = ""; }; @@ -1448,6 +1450,7 @@ 9901AF0E2A9FD68A0075122D /* Templates.hpp */, 9901AF0C2A9E30450075122D /* TypeInfo.hpp */, 9901AF182AB248A80075122D /* UnitIntervalChecker.hpp */, + 99294BD02BDEBCFE00322777 /* UnitMagnitudeChecker.hpp */, 9901AF012A9D5D6B0075122D /* underlying_type.hpp */, 4768D3F21E3A7ACA00574143 /* Wider.hpp */, 9919882B2ACCC6150076F969 /* ZeroToUnderOneChecker.hpp */, @@ -1658,6 +1661,7 @@ 99D1BE2C2AEF2575001BA497 /* WorldConcept.hpp in Headers */, 80BB899A141C3E5900F1753A /* EdgeShapeConf.hpp in Headers */, 80BB899C141C3E5900F1753A /* PolygonShapeConf.hpp in Headers */, + 99294BD12BDEBCFE00322777 /* UnitMagnitudeChecker.hpp in Headers */, 4791330E26A5B90C0011707B /* ToiOutput.hpp in Headers */, 470F9B0C1EDE6340007EF7B6 /* Vector3.hpp in Headers */, 47D61F761F1F1F2500E702BD /* DroidSansTtfData.h in Headers */, diff --git a/Library/CMakeLists.txt b/Library/CMakeLists.txt index 15510c39f..3434a1a57 100644 --- a/Library/CMakeLists.txt +++ b/Library/CMakeLists.txt @@ -170,6 +170,7 @@ set(PLAYRHO_DETAIL_HDRS include/playrho/detail/Templates.hpp include/playrho/detail/TypeInfo.hpp include/playrho/detail/UnitIntervalChecker.hpp + include/playrho/detail/UnitMagnitudeChecker.hpp include/playrho/detail/Wider.hpp include/playrho/detail/ZeroToUnderOneChecker.hpp include/playrho/detail/underlying_type.hpp diff --git a/Library/include/playrho/d2/UnitVec.hpp b/Library/include/playrho/d2/UnitVec.hpp index 579e0f951..18b29b313 100644 --- a/Library/include/playrho/d2/UnitVec.hpp +++ b/Library/include/playrho/d2/UnitVec.hpp @@ -28,7 +28,6 @@ #include // for std::sqrt, etc #include #include -#include // for std::reverse_iterator #include #include @@ -39,6 +38,9 @@ #include #include // for IsValid #include +#include + +#include // IWYU pragma: end_exports @@ -55,50 +57,87 @@ namespace d2 { /// @brief 2-D unit vector. /// @details This is a 2-dimensional directional vector. +/// @invariant The magnitude of a non-oriented unit vector is zero. The magnitude of an +/// oriented unit vector is approximately one, and is always more than zero. class UnitVec { public: + /// @brief Dimensionality of this type. + static constexpr auto N = std::size_t{2}; + /// @brief Value type used for the coordinate values of this vector. using value_type = Real; + /// @brief Alias for the underlying type of this object. + using underlying_type = Vector; + /// @brief Size type. - using size_type = std::size_t; + using size_type = underlying_type::size_type; /// @brief Constant reference type. - using const_reference = const value_type&; + using const_reference = underlying_type::const_reference; /// @brief Constant pointer type. - using const_pointer = const value_type*; + using const_pointer = underlying_type::const_pointer; /// @brief Constant iterator type. - using const_iterator = const value_type*; - + using const_iterator = underlying_type::const_iterator; + /// @brief Constant reverse iterator type. - using const_reverse_iterator = std::reverse_iterator; + using const_reverse_iterator = underlying_type::const_reverse_iterator; + + /// @brief Alias for checker type this class template was instantiated for. + using checker_type = detail::UnitMagnitudeChecker; + + /// @brief Alias for the exception type possibly thrown by this class. + using exception_type = InvalidArgument; + + /// @brief Throws this class's exception type if the given value is invalid. + /// @throws exception_type Constructed with the returned error - if the checker + /// returned a non-null explanatory string. + /// @see exception_type. + static auto ThrowIfInvalid(const underlying_type& value) -> void + { + if (const auto error = checker_type{}(value)) { + throw exception_type(error); + } + } + + // @brief Validator function. + /// @details Validates the given value using the @c checker_type type and returns + /// the value given if it checks out. + /// @throws exception_type Constructed with the returned error - if the checker + /// returned a non-null explanatory string. + /// @return Value given. + static auto Validate(const underlying_type& value) -> underlying_type + { + ThrowIfInvalid(value); + return value; + } /// @brief Gets the right-ward oriented unit vector. /// @note This is the value for the 0/4 turned (0 angled) unit vector. /// @note This is the reverse perpendicular unit vector of the down oriented vector. /// @note This is the forward perpendicular unit vector of the up oriented vector. - static constexpr UnitVec GetRight() noexcept { return UnitVec{1, 0}; } + static constexpr UnitVec GetRight() noexcept { return UnitVec{1, 0, {}}; } /// @brief Gets the up-ward oriented unit vector. /// @note This is the actual value for the 1/4 turned (90 degree angled) unit vector. /// @note This is the reverse perpendicular unit vector of the right oriented vector. /// @note This is the forward perpendicular unit vector of the left oriented vector. - static constexpr UnitVec GetUp() noexcept { return UnitVec{0, 1}; } + static constexpr UnitVec GetUp() noexcept { return UnitVec{0, 1, {}}; } /// @brief Gets the left-ward oriented unit vector. /// @note This is the actual value for the 2/4 turned (180 degree angled) unit vector. /// @note This is the reverse perpendicular unit vector of the up oriented vector. /// @note This is the forward perpendicular unit vector of the down oriented vector. - static constexpr UnitVec GetLeft() noexcept { return UnitVec{-1, 0}; } + static constexpr UnitVec GetLeft() noexcept { return UnitVec{-1, 0, {}}; } /// @brief Gets the down-ward oriented unit vector. /// @note This is the actual value for the 3/4 turned (270 degree angled) unit vector. /// @note This is the reverse perpendicular unit vector of the left oriented vector. /// @note This is the forward perpendicular unit vector of the right oriented vector. - static constexpr UnitVec GetDown() noexcept { return UnitVec{0, -1}; } + static constexpr UnitVec GetDown() noexcept { return UnitVec{0, -1, {}}; } /// @brief Gets the non-oriented unit vector. static constexpr UnitVec GetZero() noexcept { return UnitVec{}; } @@ -108,7 +147,7 @@ class UnitVec static constexpr UnitVec GetUpRight() noexcept { // Note that 1/sqrt(2) == sqrt(2)/(sqrt(2)*sqrt(2)) == sqrt(2)/2 - return UnitVec{+SquareRootTwo/Real(2), +SquareRootTwo/Real(2)}; + return UnitVec{+SquareRootTwo/Real(2), +SquareRootTwo/Real(2), {}}; } /// @brief Gets the -45 degree unit vector. @@ -117,7 +156,7 @@ class UnitVec static constexpr UnitVec GetDownRight() noexcept { // Note that 1/sqrt(2) == sqrt(2)/(sqrt(2)*sqrt(2)) == sqrt(2)/2 - return UnitVec{+SquareRootTwo/Real(2), -SquareRootTwo/Real(2)}; + return UnitVec{+SquareRootTwo/Real(2), -SquareRootTwo/Real(2), {}}; } /// @brief Gets the default fallback. @@ -131,10 +170,12 @@ class UnitVec using PolarCoord = std::enable_if_t, std::pair>; /// @brief Gets the unit vector & magnitude from the given parameters. + /// @see HypotGet. template static PolarCoord Get(const T x, const T y, const UnitVec& fallback = GetDefaultFallback()) noexcept { +#if 0 // Try the fastest way first... static constexpr auto t0 = T{}; enum: unsigned { None = 0x0, Left = 0x1, Right = 0x2, Up = 0x4, Down = 0x8, NaN = 0xF }; @@ -149,20 +190,48 @@ class UnitVec case NaN: return std::make_pair(fallback, T{}); default: break; } +#endif // Try the faster way next... const auto magnitudeSquared = x * x + y * y; +#if 0 + // UnitVectorFromVector/1000 9562 ns 9472 ns 74858 + // UnitVectorFromVectorAndBack/1000 9549 ns 9438 ns 72501 + // UnitVecFromAngle/1000 3360 ns 3351 ns 193443 + + // UnitVectorFromVector/1000 9417 ns 9363 ns 73743 + // UnitVectorFromVectorAndBack/1000 9224 ns 9195 ns 74121 + // UnitVecFromAngle/1000 3327 ns 3312 ns 208817 + if (nextafter(nextafter(Real(StripUnit(magnitudeSquared)), Real(1)), Real(1)) == Real(1)) { + return {UnitVec{value_type{StripUnit(x)}, value_type{StripUnit(y)}}, Real(1)}; + } +#else + // UnitVectorFromVector/1000 2291 ns 2279 ns 321588 + // UnitVectorFromVectorAndBack/1000 2136 ns 2123 ns 335097 + // UnitVecFromAngle/1000 3575 ns 3556 ns 200317 + + // UnitVectorFromVector/1000 2316 ns 2307 ns 320174 + // UnitVectorFromVectorAndBack/1000 2309 ns 2302 ns 299427 + // UnitVecFromAngle/1000 3327 ns 3312 ns 198916 + + // UnitVectorFromVector/1000 1714 ns 1711 ns 408220 + // UnitVectorFromVectorAndBack/1000 1722 ns 1712 ns 408537 + // UnitVecFromAngle/1000 3412 ns 3384 ns 195707 +#endif if (isnormal(magnitudeSquared)) { const auto magnitude = sqrt(magnitudeSquared); assert(isnormal(magnitude)); const auto invMagnitude = Real{1} / magnitude; - return {UnitVec{value_type{x * invMagnitude}, value_type{y * invMagnitude}}, magnitude}; + return {UnitVec{value_type{x * invMagnitude}, value_type{y * invMagnitude}, {}}, magnitude}; } // Finally, try the more accurate and robust way... const auto magnitude = hypot(x, y); - return std::make_pair(UnitVec{x / magnitude, y / magnitude}, magnitude); + if (!isnormal(magnitude)) { + return std::make_pair(fallback, T{}); + } + return std::make_pair(UnitVec{x / magnitude, y / magnitude, {}}, magnitude); } /// @brief Gets the given angled unit vector. @@ -176,6 +245,20 @@ class UnitVec /// @post GetX() and GetY() return zero. constexpr UnitVec() noexcept = default; + /// @brief Initializing constructor. + /// @details Explicitly converts a vector of unit magnitude to an object or throws. + /// @note This function is intended for use with values having a magnitude believed to already + /// be within two ULPs of the real value of 1. Use a function like the polar coordinate + /// returning @c Get function instead for arbitrary values. + /// @post @c GetX and @c GetY return the X and Y components respectively of the given value, + /// exactly. + /// @throws exception_type Constructed with the returned error - if the checker + /// returned a non-null explanatory string. + explicit UnitVec(const underlying_type& v) : m_elems{Validate(v)} + { + // Intentionally empty. + } + /// @brief Gets the max size. static constexpr size_type max_size() noexcept { return N; } @@ -187,39 +270,39 @@ class UnitVec static constexpr bool empty() noexcept { return false; } /// @brief Gets a "begin" iterator. - const_iterator begin() const noexcept { return const_iterator(data()); } + const_iterator begin() const noexcept { return m_elems.begin(); } /// @brief Gets an "end" iterator. - const_iterator end() const noexcept { return const_iterator(data() + N); } + const_iterator end() const noexcept { return m_elems.end(); } /// @brief Gets a "begin" iterator. - const_iterator cbegin() const noexcept { return begin(); } + const_iterator cbegin() const noexcept { return m_elems.cbegin(); } /// @brief Gets an "end" iterator. - const_iterator cend() const noexcept { return end(); } + const_iterator cend() const noexcept { return m_elems.cend(); } /// @brief Gets a reverse "begin" iterator. const_reverse_iterator crbegin() const noexcept { - return const_reverse_iterator{data() + N}; + return m_elems.crbegin(); } /// @brief Gets a reverse "end" iterator. const_reverse_iterator crend() const noexcept { - return const_reverse_iterator{data()}; + return m_elems.crend(); } /// @brief Gets a reverse "begin" iterator. const_reverse_iterator rbegin() const noexcept { - return crbegin(); + return m_elems.rbegin(); } /// @brief Gets a reverse "end" iterator. const_reverse_iterator rend() const noexcept { - return crend(); + return m_elems.rend(); } /// @brief Gets a constant reference to the requested element. @@ -228,26 +311,21 @@ class UnitVec /// @pre The given position parameter (@p pos), is less than size(). constexpr const_reference operator[](size_type pos) const noexcept { - assert(pos < size()); - return m_elems[pos]; // NOLINT(cppcoreguidelines-pro-bounds-constant-array-index) + return m_elems[pos]; } /// @brief Gets a constant reference to the requested element. /// @throws InvalidArgument if given a position that's >= size(). constexpr const_reference at(size_type pos) const { - if (pos >= size()) - { - throw InvalidArgument("Vector::at: position >= size()"); - } - return m_elems[pos]; // NOLINT(cppcoreguidelines-pro-bounds-constant-array-index) + return m_elems.at(pos); } /// @brief Direct access to data. constexpr const_pointer data() const noexcept { // Cast to be more explicit about wanting to decay array into pointer... - return static_cast(m_elems); + return m_elems.data(); } /// @brief Gets the "X" value. @@ -257,13 +335,13 @@ class UnitVec constexpr auto GetY() const noexcept { return m_elems[1]; } /// @brief Flips the X and Y values. - constexpr UnitVec FlipXY() const noexcept { return UnitVec{-GetX(), -GetY()}; } + constexpr UnitVec FlipXY() const noexcept { return UnitVec{-GetX(), -GetY(), {}}; } /// @brief Flips the X value. - constexpr UnitVec FlipX() const noexcept { return UnitVec{-GetX(), GetY()}; } + constexpr UnitVec FlipX() const noexcept { return UnitVec{-GetX(), GetY(), {}}; } /// @brief Flips the Y value. - constexpr UnitVec FlipY() const noexcept { return UnitVec{GetX(), -GetY()}; } + constexpr UnitVec FlipY() const noexcept { return UnitVec{GetX(), -GetY(), {}}; } /// @brief Rotates the unit vector by the given amount. /// @@ -274,8 +352,9 @@ class UnitVec /// constexpr UnitVec Rotate(const UnitVec& amount) const noexcept { - return UnitVec{GetX() * amount.GetX() - GetY() * amount.GetY(), - GetY() * amount.GetX() + GetX() * amount.GetY()}; + return UnitVec{GetX() * amount.GetX() - GetY() * amount.GetY(), // newline + GetY() * amount.GetX() + GetX() * amount.GetY(), // newline + {}}; } /// @brief Gets a vector counter-clockwise (reverse-clockwise) perpendicular to this vector. @@ -285,7 +364,7 @@ class UnitVec constexpr UnitVec GetRevPerpendicular() const noexcept { // See http://mathworld.wolfram.com/PerpendicularVector.html - return UnitVec{-GetY(), GetX()}; + return UnitVec{-GetY(), GetX(), {}}; } /// @brief Gets a vector clockwise (forward-clockwise) perpendicular to this vector. @@ -295,32 +374,37 @@ class UnitVec constexpr UnitVec GetFwdPerpendicular() const noexcept { // See http://mathworld.wolfram.com/PerpendicularVector.html - return UnitVec{GetY(), -GetX()}; + return UnitVec{GetY(), -GetX(), {}}; + } + + /// @brief Implicitly gets the underlying value via a cast or implicit conversion. + constexpr operator underlying_type () const noexcept + { + return m_elems; } /// @brief Negation operator. - constexpr UnitVec operator-() const noexcept { return UnitVec{-GetX(), -GetY()}; } + constexpr UnitVec operator-() const noexcept { return UnitVec{-GetX(), -GetY(), {}}; } /// @brief Positive operator. - constexpr UnitVec operator+() const noexcept { return UnitVec{+GetX(), +GetY()}; } + constexpr UnitVec operator+() const noexcept { return UnitVec{+GetX(), +GetY(), {}}; } /// @brief Gets the absolute value. constexpr UnitVec Absolute() const noexcept { - return UnitVec{abs(GetX()), abs(GetY())}; + return UnitVec{abs(GetX()), abs(GetY()), {}}; } private: - /// @brief Dimensionality of this type. - static constexpr auto N = std::size_t{2}; + struct Hidden {}; /// @brief Initializing constructor. - constexpr UnitVec(value_type x, value_type y) noexcept : m_elems{x, y} + constexpr UnitVec(value_type x, value_type y, Hidden) noexcept : m_elems{x, y} { // Intentionally empty. } - value_type m_elems[N] = {}; ///< Element values. + underlying_type m_elems = {}; ///< Element values. }; // Free functions... diff --git a/Library/include/playrho/detail/UnitMagnitudeChecker.hpp b/Library/include/playrho/detail/UnitMagnitudeChecker.hpp new file mode 100644 index 000000000..25503c2b5 --- /dev/null +++ b/Library/include/playrho/detail/UnitMagnitudeChecker.hpp @@ -0,0 +1,60 @@ +/* + * Copyright (c) 2023 Louis Langholtz https://github.com/louis-langholtz/PlayRho + * + * This software is provided 'as-is', without any express or implied + * warranty. In no event will the authors be held liable for any damages + * arising from the use of this software. + * + * Permission is granted to anyone to use this software for any purpose, + * including commercial applications, and to alter it and redistribute it + * freely, subject to the following restrictions: + * + * 1. The origin of this software must not be misrepresented; you must not + * claim that you wrote the original software. If you use this software + * in a product, an acknowledgment in the product documentation would be + * appreciated but is not required. + * 2. Altered source versions must be plainly marked as such, and must not be + * misrepresented as being the original software. + * 3. This notice may not be removed or altered from any source distribution. + */ + +#ifndef PLAYRHO_DETAIL_UNITVECTORCHECKER_HPP +#define PLAYRHO_DETAIL_UNITVECTORCHECKER_HPP + +#include // for std::nextafter + +#include // for StripUnit + +namespace playrho::detail { + +/// @brief Unit magnitude constrained value checker. +/// @note This is meant to be used as a checker with types like Checked. +/// @tparam T Underlying type for this checker. +/// @ingroup Checkers +/// @see Checked. +template +struct UnitMagnitudeChecker { + + /// @brief Value checking functor. + /// @return Null string if given value is valid, else + /// a non-null string explanation. + auto operator()(const T& v) const noexcept + -> decltype(begin(v), end(v), StripUnit(*begin(v)), static_cast(nullptr)) + { + static constexpr auto one = Real(1); + auto sum = std::decay_t{}; + for (const auto& element: v) { + sum += element * element; + } + // Tolerance of 2 ULPs per accuracy cos/sin generally provide! + if (std::nextafter(std::nextafter(Real(StripUnit(sum)), one), one) != one) { + return "value not of unit magnitude"; + } + return {}; + } +}; + +} // namespace playrho::detail + + +#endif // PLAYRHO_DETAIL_UNITVECTORCHECKER_HPP diff --git a/Library/source/playrho/d2/UnitVec.cpp b/Library/source/playrho/d2/UnitVec.cpp index 579949a0f..7c6e60a60 100644 --- a/Library/source/playrho/d2/UnitVec.cpp +++ b/Library/source/playrho/d2/UnitVec.cpp @@ -26,7 +26,7 @@ namespace d2 { UnitVec UnitVec::Get(const Angle angle) noexcept { - return UnitVec{cos(angle), sin(angle)}; + return UnitVec{cos(angle), sin(angle), {}}; } } // namespace d2 diff --git a/UnitTests/PolygonShape.cpp b/UnitTests/PolygonShape.cpp index 27e8ed08c..cdc29e5fc 100644 --- a/UnitTests/PolygonShape.cpp +++ b/UnitTests/PolygonShape.cpp @@ -88,11 +88,18 @@ TEST(PolygonShapeConf, BoxConstruction) EXPECT_EQ(shape.GetVertex(2), Length2(-hx, hy)); // top left EXPECT_EQ(shape.GetVertex(3), Length2(-hx, -hy)); // bottom left - EXPECT_EQ(shape.GetNormal(0) * Real{1}, Vec2(+1, 0)); - EXPECT_EQ(shape.GetNormal(1) * Real{1}, Vec2(0, +1)); - EXPECT_EQ(shape.GetNormal(2) * Real{1}, Vec2(-1, 0)); - EXPECT_EQ(shape.GetNormal(3) * Real{1}, Vec2(0, -1)); - + EXPECT_DOUBLE_EQ(shape.GetNormal(0).GetX(), GetX(Vec2(+1, 0))); + EXPECT_DOUBLE_EQ(shape.GetNormal(0).GetY(), GetY(Vec2(+1, 0))); + + EXPECT_DOUBLE_EQ(shape.GetNormal(1).GetX(), GetX(Vec2(0, +1))); + EXPECT_DOUBLE_EQ(shape.GetNormal(1).GetY(), GetY(Vec2(0, +1))); + + EXPECT_DOUBLE_EQ(shape.GetNormal(2).GetX(), GetX(Vec2(-1, 0))); + EXPECT_DOUBLE_EQ(shape.GetNormal(2).GetY(), GetY(Vec2(-1, 0))); + + EXPECT_DOUBLE_EQ(shape.GetNormal(3).GetX(), GetX(Vec2(0, -1))); + EXPECT_DOUBLE_EQ(shape.GetNormal(3).GetY(), GetY(Vec2(0, -1))); + EXPECT_TRUE(Validate(shape.GetVertices())); } @@ -113,10 +120,14 @@ TEST(PolygonShapeConf, Copy) ASSERT_EQ(shape.GetVertex(2), Length2(-hx, hy)); // top left ASSERT_EQ(shape.GetVertex(3), Length2(-hx, -hy)); // bottom left - ASSERT_EQ(shape.GetNormal(0) * Real{1}, Vec2(+1, 0)); - ASSERT_EQ(shape.GetNormal(1) * Real{1}, Vec2(0, +1)); - ASSERT_EQ(shape.GetNormal(2) * Real{1}, Vec2(-1, 0)); - ASSERT_EQ(shape.GetNormal(3) * Real{1}, Vec2(0, -1)); + ASSERT_DOUBLE_EQ(shape.GetNormal(0).GetX(), Real(+1)); + ASSERT_DOUBLE_EQ(shape.GetNormal(0).GetY(), Real(0)); + ASSERT_DOUBLE_EQ(shape.GetNormal(1).GetX(), Real(0)); + ASSERT_DOUBLE_EQ(shape.GetNormal(1).GetY(), Real(+1)); + ASSERT_DOUBLE_EQ(shape.GetNormal(2).GetX(), Real(-1)); + ASSERT_DOUBLE_EQ(shape.GetNormal(2).GetY(), Real(0)); + ASSERT_DOUBLE_EQ(shape.GetNormal(3).GetX(), Real(0)); + ASSERT_DOUBLE_EQ(shape.GetNormal(3).GetY(), Real(-1)); const auto copy = shape; @@ -134,10 +145,14 @@ TEST(PolygonShapeConf, Copy) EXPECT_EQ(copy.GetVertex(2), Length2(-hx, hy)); // top left EXPECT_EQ(copy.GetVertex(3), Length2(-hx, -hy)); // bottom left - EXPECT_EQ(copy.GetNormal(0) * Real{1}, Vec2(+1, 0)); - EXPECT_EQ(copy.GetNormal(1) * Real{1}, Vec2(0, +1)); - EXPECT_EQ(copy.GetNormal(2) * Real{1}, Vec2(-1, 0)); - EXPECT_EQ(copy.GetNormal(3) * Real{1}, Vec2(0, -1)); + EXPECT_DOUBLE_EQ(copy.GetNormal(0).GetX(), Real(+1)); + EXPECT_DOUBLE_EQ(copy.GetNormal(0).GetY(), Real(0)); + EXPECT_DOUBLE_EQ(copy.GetNormal(1).GetX(), Real(0)); + EXPECT_DOUBLE_EQ(copy.GetNormal(1).GetY(), Real(+1)); + EXPECT_DOUBLE_EQ(copy.GetNormal(2).GetX(), Real(-1)); + EXPECT_DOUBLE_EQ(copy.GetNormal(2).GetY(), Real(0)); + EXPECT_DOUBLE_EQ(copy.GetNormal(3).GetX(), Real(0)); + EXPECT_DOUBLE_EQ(copy.GetNormal(3).GetY(), Real(-1)); } TEST(PolygonShapeConf, Transform) @@ -157,11 +172,15 @@ TEST(PolygonShapeConf, Transform) ASSERT_EQ(shape.GetVertex(2), Length2(-hx, hy)); // top left ASSERT_EQ(shape.GetVertex(3), Length2(-hx, -hy)); // bottom left - ASSERT_EQ(shape.GetNormal(0) * Real{1}, Vec2(+1, 0)); - ASSERT_EQ(shape.GetNormal(1) * Real{1}, Vec2(0, +1)); - ASSERT_EQ(shape.GetNormal(2) * Real{1}, Vec2(-1, 0)); - ASSERT_EQ(shape.GetNormal(3) * Real{1}, Vec2(0, -1)); - + ASSERT_DOUBLE_EQ(shape.GetNormal(0).GetX(), Real(+1)); + ASSERT_DOUBLE_EQ(shape.GetNormal(0).GetY(), Real(0)); + ASSERT_DOUBLE_EQ(shape.GetNormal(1).GetX(), Real(0)); + ASSERT_DOUBLE_EQ(shape.GetNormal(1).GetY(), Real(+1)); + ASSERT_DOUBLE_EQ(shape.GetNormal(2).GetX(), Real(-1)); + ASSERT_DOUBLE_EQ(shape.GetNormal(2).GetY(), Real(0)); + ASSERT_DOUBLE_EQ(shape.GetNormal(3).GetX(), Real(0)); + ASSERT_DOUBLE_EQ(shape.GetNormal(3).GetY(), Real(-1)); + const auto new_ctr = Length2{-3_m, 67_m}; shape = PolygonShapeConf{ PolygonShapeConf{}.SetAsBox(hx, hy).Transform(Transformation{new_ctr, UnitVec::GetRight()}) @@ -206,10 +225,14 @@ TEST(PolygonShapeConf, SetAsBox) EXPECT_EQ(shape.GetVertex(2), Length2(-hx, hy)); // top left EXPECT_EQ(shape.GetVertex(3), Length2(-hx, -hy)); // bottom left - EXPECT_EQ(shape.GetNormal(0) * Real{1}, Vec2(+1, 0)); - EXPECT_EQ(shape.GetNormal(1) * Real{1}, Vec2(0, +1)); - EXPECT_EQ(shape.GetNormal(2) * Real{1}, Vec2(-1, 0)); - EXPECT_EQ(shape.GetNormal(3) * Real{1}, Vec2(0, -1)); + EXPECT_DOUBLE_EQ(shape.GetNormal(0).GetX(), Real(+1)); + EXPECT_DOUBLE_EQ(shape.GetNormal(0).GetY(), Real(0)); + EXPECT_DOUBLE_EQ(shape.GetNormal(1).GetX(), Real(0)); + EXPECT_DOUBLE_EQ(shape.GetNormal(1).GetY(), Real(+1)); + EXPECT_DOUBLE_EQ(shape.GetNormal(2).GetX(), Real(-1)); + EXPECT_DOUBLE_EQ(shape.GetNormal(2).GetY(), Real(0)); + EXPECT_DOUBLE_EQ(shape.GetNormal(3).GetX(), Real(0)); + EXPECT_DOUBLE_EQ(shape.GetNormal(3).GetY(), Real(-1)); } TEST(PolygonShapeConf, SetAsZeroCenteredRotatedBox) @@ -230,10 +253,14 @@ TEST(PolygonShapeConf, SetAsZeroCenteredRotatedBox) EXPECT_EQ(shape.GetVertex(2), Length2(-hx, hy)); // top left EXPECT_EQ(shape.GetVertex(3), Length2(-hx, -hy)); // bottom left - EXPECT_EQ(shape.GetNormal(0) * Real{1}, Vec2(+1, 0)); - EXPECT_EQ(shape.GetNormal(1) * Real{1}, Vec2(0, +1)); - EXPECT_EQ(shape.GetNormal(2) * Real{1}, Vec2(-1, 0)); - EXPECT_EQ(shape.GetNormal(3) * Real{1}, Vec2(0, -1)); + EXPECT_DOUBLE_EQ(shape.GetNormal(0).GetX(), Real(+1)); + EXPECT_DOUBLE_EQ(shape.GetNormal(0).GetY(), Real(0)); + EXPECT_DOUBLE_EQ(shape.GetNormal(1).GetX(), Real(0)); + EXPECT_DOUBLE_EQ(shape.GetNormal(1).GetY(), Real(+1)); + EXPECT_DOUBLE_EQ(shape.GetNormal(2).GetX(), Real(-1)); + EXPECT_DOUBLE_EQ(shape.GetNormal(2).GetY(), Real(0)); + EXPECT_DOUBLE_EQ(shape.GetNormal(3).GetX(), Real(0)); + EXPECT_DOUBLE_EQ(shape.GetNormal(3).GetY(), Real(-1)); } TEST(PolygonShapeConf, SetAsCenteredBox) @@ -260,10 +287,14 @@ TEST(PolygonShapeConf, SetAsCenteredBox) EXPECT_EQ(shape.GetVertex(2), Length2(-hx + x_off, hy + y_off)); // top left EXPECT_EQ(shape.GetVertex(3), Length2(-hx + x_off, -hy + y_off)); // bottom left - EXPECT_EQ(shape.GetNormal(0) * Real{1}, Vec2(+1, 0)); - EXPECT_EQ(shape.GetNormal(1) * Real{1}, Vec2(0, +1)); - EXPECT_EQ(shape.GetNormal(2) * Real{1}, Vec2(-1, 0)); - EXPECT_EQ(shape.GetNormal(3) * Real{1}, Vec2(0, -1)); + EXPECT_DOUBLE_EQ(shape.GetNormal(0).GetX(), Real(+1)); + EXPECT_DOUBLE_EQ(shape.GetNormal(0).GetY(), Real(0)); + EXPECT_DOUBLE_EQ(shape.GetNormal(1).GetX(), Real(0)); + EXPECT_DOUBLE_EQ(shape.GetNormal(1).GetY(), Real(+1)); + EXPECT_DOUBLE_EQ(shape.GetNormal(2).GetX(), Real(-1)); + EXPECT_DOUBLE_EQ(shape.GetNormal(2).GetY(), Real(0)); + EXPECT_DOUBLE_EQ(shape.GetNormal(3).GetX(), Real(0)); + EXPECT_DOUBLE_EQ(shape.GetNormal(3).GetY(), Real(-1)); } TEST(PolygonShapeConf, SetAsBoxAngledDegrees90) diff --git a/UnitTests/UnitVec.cpp b/UnitTests/UnitVec.cpp index a0b1a4d28..ac1b81897 100644 --- a/UnitTests/UnitVec.cpp +++ b/UnitTests/UnitVec.cpp @@ -319,3 +319,71 @@ TEST(UnitVec, BeginEnd) EXPECT_EQ(uv.end(), uv.cend()); EXPECT_EQ(uv.begin() + 2, uv.end()); } + +TEST(UnitVec, MagSquaredSinCosWithinTwoUlps) +{ + auto ulps = 0; + for (auto counter = 0; counter < 360000; ++counter) { + SCOPED_TRACE(counter); + const auto angle = (counter * Degree) / Real(1000); + const auto x = cos(angle); + const auto y = sin(angle); + const auto m2 = x * x + y * y; + while (!AlmostEqual(Real(m2), Real(1), ulps)) { + ++ulps; + } + } + EXPECT_EQ(ulps, 2); +} + +TEST(UnitVec, ConstructorWithVec2) +{ + EXPECT_THROW(UnitVec(Vec2(4, 2)), InvalidArgument); + EXPECT_NO_THROW(UnitVec(Vec2(0, 1))); + { + const auto value = Vec2(1, 0); + ASSERT_NO_THROW((UnitVec{value})); + EXPECT_EQ(static_cast(UnitVec(value)), value); + } +} + +TEST(UnitVec, CosSinConstructedReversibleWithinZeroUlps) +{ + auto ulps = 0; + for (auto counter = 0; counter < 360000; ++counter) { + SCOPED_TRACE(counter); + const auto angle = (counter * Degree) / Real(1000); + const auto x = cos(angle); + const auto y = sin(angle); + const auto pc = UnitVec(Vec2{x, y}); + while (!AlmostEqual(Real(pc.GetX()), Real(x), ulps)) { + ++ulps; + } + while (!AlmostEqual(Real(pc.GetY()), Real(y), ulps)) { + ++ulps; + } + } + EXPECT_EQ(ulps, 0); +} + +TEST(UnitVec, GetCosSinIsReversibleWithTwoUlps) +{ + auto ulps = 0; + for (auto counter = 0; counter < 360000; ++counter) { + SCOPED_TRACE(counter); + const auto angle = (counter * Degree) / Real(1000); + const auto x = cos(angle); + const auto y = sin(angle); + const auto pc = UnitVec::Get(x, y); + while (!AlmostEqual(Real(pc.first.GetX()), Real(x), ulps)) { + ++ulps; + } + while (!AlmostEqual(Real(pc.first.GetY()), Real(y), ulps)) { + ++ulps; + } + while (!AlmostEqual(Real(pc.second), Real(1), ulps)) { + ++ulps; + } + } + EXPECT_EQ(ulps, 2); +} diff --git a/UnitTests/World.cpp b/UnitTests/World.cpp index 179ca9db7..918d0e040 100644 --- a/UnitTests/World.cpp +++ b/UnitTests/World.cpp @@ -2731,7 +2731,7 @@ TEST(World_Longer, TilesComesToRest) const auto ExpectedFirstBodiesSlept = []() -> unsigned long { if constexpr (std::is_same_v) { -#if defined(__k8__) || defined(__core2__) || defined(_WIN64) +#if defined(__k8__) || defined(__core2__) || defined(_WIN64) || (defined(__arm64__) && !defined(NDEBUG)) return 76u; #elif defined(__arm64__) // apple silicon return 110u; @@ -2753,8 +2753,10 @@ TEST(World_Longer, TilesComesToRest) return 1828u; #elif defined(__core2__) return 1798u; -#elif defined(__arm64__) // apple silicon +#elif defined(__arm64__) && defined(NDEBUG) // apple silicon return 1796u; +#elif defined(__arm64__) && !defined(NDBUG) + return 1766u; #elif defined(_WIN64) return 1803u; #elif defined(_WIN32) @@ -3080,6 +3082,12 @@ TEST(World_Longer, TilesComesToRest) EXPECT_EQ(sumRegVelIters, 46948ul); EXPECT_EQ(sumToiPosIters, 44022ul); EXPECT_EQ(sumToiVelIters, 113284ul); +#elif defined(__arm64__) && !defined(NDEBUG) + EXPECT_EQ(numSteps, 1767ul); + EXPECT_EQ(sumRegPosIters, 36414ul); + EXPECT_EQ(sumRegVelIters, 46675ul); + EXPECT_EQ(sumToiPosIters, 44239ul); + EXPECT_EQ(sumToiVelIters, 114126ul); #else EXPECT_EQ(numSteps, 1797ul); EXPECT_EQ(sumRegPosIters, 36508ul); @@ -3101,7 +3109,11 @@ TEST(World_Longer, TilesComesToRest) EXPECT_EQ(sumRegVelIters, 46885ul); EXPECT_EQ(sumToiPosIters, 44086ul); #if defined(__clang_major__) && (__clang_major__ >= 14) && !defined(PLAYRHO_USE_BOOST_UNITS) +#if defined(__arm64__) && !defined(NDEBUG) + EXPECT_EQ(sumToiVelIters, 112885ul); +#else EXPECT_EQ(sumToiVelIters, 112990ul); +#endif #else EXPECT_EQ(sumToiVelIters, 114196ul); #endif @@ -3929,6 +3941,9 @@ TEST(World, Recreate) constexpr auto e_count = 20; auto createdBodyCount = 0ul; constexpr auto ExpectedFirstWithBodiesSlept = []() -> unsigned { +#if defined(__arm64__) && !defined(NDEBUG) && !defined(PLAYRHO_USE_BOOST_UNITS) + return 81u; +#else if constexpr (std::is_same_v) { return 82u; } @@ -3936,8 +3951,12 @@ TEST(World, Recreate) return 81u; } return 0u; +#endif }; constexpr auto ExpectedWorldContactRange = []() -> unsigned { +#if defined(__arm64__) && !defined(NDEBUG) && !defined(PLAYRHO_USE_BOOST_UNITS) + return 441u; +#else if constexpr (std::is_same_v) { return 442u; } @@ -3945,8 +3964,12 @@ TEST(World, Recreate) return 441u; } return 0u; +#endif }; constexpr auto ExpectedTotalContactsDestroyed = []() -> unsigned { +#if defined(__arm64__) && !defined(NDEBUG) && !defined(PLAYRHO_USE_BOOST_UNITS) + return 21u; +#else if constexpr (std::is_same_v) { return 20u; } @@ -3954,6 +3977,7 @@ TEST(World, Recreate) return 21u; } return 0u; +#endif }; {