PaintBrushPaintLocationTests.cpp 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339
  1. /*
  2. * Copyright (c) Contributors to the Open 3D Engine Project.
  3. * For complete copyright and license terms please see the LICENSE at the root of this distribution.
  4. *
  5. * SPDX-License-Identifier: Apache-2.0 OR MIT
  6. *
  7. */
  8. #include <AzCore/Serialization/SerializeContext.h>
  9. #include <AzCore/UnitTest/TestTypes.h>
  10. #include <AzTest/AzTest.h>
  11. #include <AzFramework/PaintBrush/PaintBrush.h>
  12. #include <AZTestShared/Math/MathTestHelpers.h>
  13. #include <Tests/PaintBrush/MockPaintBrushNotificationHandler.h>
  14. // This set of unit tests validates that the PaintToLocation API trivially works and calculates location changes correctly.
  15. namespace UnitTest
  16. {
  17. class PaintBrushPaintLocationTestFixture : public LeakDetectionFixture
  18. {
  19. public:
  20. // Some common default values that we'll use in our tests.
  21. const AZ::EntityComponentIdPair EntityComponentIdPair{ AZ::EntityId(123), AZ::ComponentId(456) };
  22. const AZ::Vector3 TestBrushCenter{ 10.0f, 20.0f, 30.0f };
  23. const AZ::Vector3 TestBrushCenter2d{ 10.0f, 20.0f, 0.0f }; // This should be the same as TestBrushCenter, but with Z=0
  24. const AZ::Color TestColor{ 0.25f, 0.50f, 0.75f, 1.0f };
  25. // Useful helpers for our tests
  26. AzFramework::PaintBrushSettings m_settings;
  27. };
  28. TEST_F(PaintBrushPaintLocationTestFixture, PaintToLocationAtSingleLocationFunctionsCorrectly)
  29. {
  30. // This tests all of the basic PaintToLocation() functionality:
  31. // - It will call OnPaint with the correct dirty area for the brush settings and initial location
  32. // - The valueLookupFn will only return valid points that occur within the brush.
  33. // - The blendFn will blend two values together.
  34. AzFramework::PaintBrush paintBrush(EntityComponentIdPair);
  35. ::testing::NiceMock<MockPaintBrushNotificationBusHandler> mockHandler(EntityComponentIdPair);
  36. const float TestBrushRadius = 1.0f;
  37. m_settings.SetSize(TestBrushRadius * 2.0f);
  38. EXPECT_CALL(mockHandler, OnPaint(::testing::_, ::testing::_, ::testing::_, ::testing::_)).Times(1);
  39. ON_CALL(mockHandler, OnPaint)
  40. .WillByDefault(
  41. [this, TestBrushRadius]([[maybe_unused]] const AZ::Color& color,
  42. const AZ::Aabb& dirtyArea,
  43. AzFramework::PaintBrushNotifications::ValueLookupFn& valueLookupFn,
  44. AzFramework::PaintBrushNotifications::BlendFn& blendFn)
  45. {
  46. // The OnPaint method for a listener to the PaintBrushNotificationBus should work as follows:
  47. // - It should receive a dirtyArea AABB that contains the region that's been painted.
  48. // - For each point that the listener cares about in that region, it should call valueLookupFn() to find
  49. // out which points actually fall within the paintbrush, and what the opacities of those points are.
  50. // - For each valid point and opacity, the listener should call blendFn() to blend the values together.
  51. // Validate the dirtyArea AABB:
  52. // We expect the AABB to be centered around the TestBrushCenter but with a Z value of 0
  53. // because we only support painting in 2D for now. The radius of the AABB should match the radius of our paintbrush.
  54. EXPECT_THAT(dirtyArea, IsClose(AZ::Aabb::CreateCenterRadius(TestBrushCenter2d, TestBrushRadius)));
  55. // Validate the valueLookupFn:
  56. // Create a 3x3 square grid of points. Because our brush is a circle, we expect only the points in along a + to be
  57. // returned as valid points. The corners of the square should fall outside the circle and not get returned.
  58. const AZStd::vector<AZ::Vector3> points = {
  59. AZ::Vector3(dirtyArea.GetMin().GetX(), dirtyArea.GetMin().GetY(), 0.0f),
  60. AZ::Vector3(dirtyArea.GetCenter().GetX(), dirtyArea.GetMin().GetY(), 0.0f),
  61. AZ::Vector3(dirtyArea.GetMax().GetX(), dirtyArea.GetMin().GetY(), 0.0f),
  62. AZ::Vector3(dirtyArea.GetMin().GetX(), dirtyArea.GetCenter().GetY(), 0.0f),
  63. AZ::Vector3(dirtyArea.GetCenter().GetX(), dirtyArea.GetCenter().GetY(), 0.0f),
  64. AZ::Vector3(dirtyArea.GetMax().GetX(), dirtyArea.GetCenter().GetY(), 0.0f),
  65. AZ::Vector3(dirtyArea.GetMin().GetX(), dirtyArea.GetMax().GetY(), 0.0f),
  66. AZ::Vector3(dirtyArea.GetCenter().GetX(), dirtyArea.GetMax().GetY(), 0.0f),
  67. AZ::Vector3(dirtyArea.GetMax().GetX(), dirtyArea.GetMax().GetY(), 0.0f),
  68. };
  69. AZStd::vector<AZ::Vector3> validPoints;
  70. AZStd::vector<float> opacities;
  71. valueLookupFn(points, validPoints, opacities);
  72. // We should only have the 5 points along the + in validPoints.
  73. const AZStd::vector<AZ::Vector3> expectedValidPoints = {
  74. AZ::Vector3(dirtyArea.GetCenter().GetX(), dirtyArea.GetMin().GetY(), 0.0f),
  75. AZ::Vector3(dirtyArea.GetMin().GetX(), dirtyArea.GetCenter().GetY(), 0.0f),
  76. AZ::Vector3(dirtyArea.GetCenter().GetX(), dirtyArea.GetCenter().GetY(), 0.0f),
  77. AZ::Vector3(dirtyArea.GetMax().GetX(), dirtyArea.GetCenter().GetY(), 0.0f),
  78. AZ::Vector3(dirtyArea.GetCenter().GetX(), dirtyArea.GetMax().GetY(), 0.0f),
  79. };
  80. EXPECT_THAT(validPoints, ::testing::Pointwise(ContainerIsClose(), expectedValidPoints));
  81. // We should only have 5 opacities, and they should all be 1.0 because we haven't adjusted any brush settings.
  82. EXPECT_EQ(opacities.size(), 5);
  83. for (auto& opacity : opacities)
  84. {
  85. EXPECT_NEAR(opacity, 1.0f, 0.001f);
  86. }
  87. // Validate the blendFn:
  88. // By default, it should be a normal blend, so verify that a base value of 0, a new value of 1, and an opacity of 0.5
  89. // gives us back a blended value of 0.5.
  90. const float blend = blendFn(0.0f, 1.0f, 0.5f);
  91. EXPECT_NEAR(blend, 0.5f, 0.001f);
  92. });
  93. paintBrush.BeginPaintMode();
  94. paintBrush.BeginBrushStroke(m_settings);
  95. paintBrush.PaintToLocation(TestBrushCenter, m_settings);
  96. paintBrush.EndBrushStroke();
  97. paintBrush.EndPaintMode();
  98. }
  99. TEST_F(PaintBrushPaintLocationTestFixture, PaintToLocationWithSmallMovementDoesNotTriggerPainting)
  100. {
  101. // This verifies that if the distance between two PaintToLocation calls is small enough, it won't trigger
  102. // an OnPaint. "small" is defined as less than (brush size * distance %).
  103. AzFramework::PaintBrush paintBrush(EntityComponentIdPair);
  104. ::testing::NiceMock<MockPaintBrushNotificationBusHandler> mockHandler(EntityComponentIdPair);
  105. // Set the distance between brush stamps to 50%
  106. const float TestDistancePercent = 50.0f;
  107. m_settings.SetDistancePercent(TestDistancePercent);
  108. // Set the brush radius to 1 meter (diameter is 2 meters)
  109. const float TestBrushRadius = 1.0f;
  110. m_settings.SetSize(TestBrushRadius * 2.0f);
  111. // The distance we expect to need to move to trigger another paint call is 50% of our brush size.
  112. const float DistanceToTriggerSecondCall = TestBrushRadius * 2.0f * (TestDistancePercent / 100.0f);
  113. // Choose a second brush center location that's just slightly under the threshold that should be needed to trigger a second
  114. // OnPaint call.
  115. const AZ::Vector3 tooSmallSecondLocation = TestBrushCenter + AZ::Vector3(DistanceToTriggerSecondCall - 0.01f, 0.0f, 0.0f);
  116. // We expect to get called only once for our initial PaintToLocation(); the second PaintToLocation() won't
  117. // have moved far enough to trigger a second OnPaint call.
  118. EXPECT_CALL(mockHandler, OnPaint(::testing::_, ::testing::_, ::testing::_, ::testing::_)).Times(1);
  119. paintBrush.BeginPaintMode();
  120. paintBrush.BeginBrushStroke(m_settings);
  121. paintBrush.PaintToLocation(TestBrushCenter, m_settings);
  122. paintBrush.PaintToLocation(tooSmallSecondLocation, m_settings);
  123. paintBrush.EndBrushStroke();
  124. paintBrush.EndPaintMode();
  125. // Try the test again, this time moving exactly the amount we need to so that we trigger a second call.
  126. // (We do this to verify that we've correctly identified the threshold under which we should not trigger another OnPaint)
  127. const AZ::Vector3 largeEnoughSecondLocation = TestBrushCenter + AZ::Vector3(DistanceToTriggerSecondCall, 0.0f, 0.0f);
  128. EXPECT_CALL(mockHandler, OnPaint(::testing::_, ::testing::_, ::testing::_, ::testing::_)).Times(2);
  129. paintBrush.BeginPaintMode();
  130. paintBrush.BeginBrushStroke(m_settings);
  131. paintBrush.PaintToLocation(TestBrushCenter, m_settings);
  132. paintBrush.PaintToLocation(largeEnoughSecondLocation, m_settings);
  133. paintBrush.EndBrushStroke();
  134. paintBrush.EndPaintMode();
  135. }
  136. TEST_F(PaintBrushPaintLocationTestFixture, PaintToLocationSecondMovementDoesNotIncludeFirstCircle)
  137. {
  138. // When painting, the first PaintToLocation call should just contain a single paint stamp at the passed-in location.
  139. // The second PaintToLocation call should contain paint stamps from the first location to the second, but should NOT
  140. // have a second paint stamp at the first location. Ex:
  141. // O <- first PaintToLocation
  142. // -OOO <- second PaintToLocation
  143. // If the Distance % is anything less than 100% in the paint brush settings, the Os will overlap.
  144. // We'll set it to 100% just to make it obvious that we've gotten the correct result.
  145. AzFramework::PaintBrush paintBrush(EntityComponentIdPair);
  146. ::testing::NiceMock<MockPaintBrushNotificationBusHandler> mockHandler(EntityComponentIdPair);
  147. // Set the distance between brush stamps to 100%
  148. const float TestDistancePercent = 100.0f;
  149. m_settings.SetDistancePercent(TestDistancePercent);
  150. // Set the brush radius to 1 meter (diameter is 2 meters)
  151. const float TestBrushRadius = 1.0f;
  152. const float TestBrushSize = TestBrushRadius * 2.0f;
  153. m_settings.SetSize(TestBrushSize);
  154. // Choose a second brush center location that's 3 full brush stamps away. This should give us a total
  155. // of 4 brush stamps that get painted between the two calls.
  156. const AZ::Vector3 secondLocation = TestBrushCenter + AZ::Vector3(TestBrushSize * 3.0f, 0.0f, 0.0f);
  157. // We expect to get two OnPaint calls.
  158. EXPECT_CALL(mockHandler, OnPaint(::testing::_, ::testing::_, ::testing::_, ::testing::_)).Times(2);
  159. paintBrush.BeginPaintMode();
  160. paintBrush.BeginBrushStroke(m_settings);
  161. ON_CALL(mockHandler, OnPaint)
  162. .WillByDefault(
  163. [this, TestBrushRadius]([[maybe_unused]] const AZ::Color& color, const AZ::Aabb& dirtyArea,
  164. [[maybe_unused]] AzFramework::PaintBrushNotifications::ValueLookupFn& valueLookupFn,
  165. [[maybe_unused]] AzFramework::PaintBrushNotifications::BlendFn& blendFn)
  166. {
  167. // On the first PaintToLocation, we expect to get a dirtyArea that exactly fits our first paint brush stamp.
  168. const AZ::Aabb expectedFirstDirtyArea = AZ::Aabb::CreateCenterRadius(TestBrushCenter2d, TestBrushRadius);
  169. EXPECT_THAT(dirtyArea, IsClose(expectedFirstDirtyArea));
  170. });
  171. paintBrush.PaintToLocation(TestBrushCenter, m_settings);
  172. ON_CALL(mockHandler, OnPaint)
  173. .WillByDefault(
  174. [this, TestBrushSize, TestBrushRadius]([[maybe_unused]] const AZ::Color& color, const AZ::Aabb& dirtyArea,
  175. [[maybe_unused]] AzFramework::PaintBrushNotifications::ValueLookupFn& valueLookupFn,
  176. [[maybe_unused]] AzFramework::PaintBrushNotifications::BlendFn& blendFn)
  177. {
  178. // On the second PaintToLocation, we expect the dirtyArea to only contain the next 3 paint brush stamps,
  179. // but not the first one.
  180. const AZ::Vector3 stampIncrement = AZ::Vector3(TestBrushSize, 0.0f, 0.0f);
  181. AZ::Aabb expectedSecondDirtyArea = AZ::Aabb::CreateCenterRadius(TestBrushCenter2d + stampIncrement, TestBrushRadius);
  182. expectedSecondDirtyArea.AddAabb(
  183. AZ::Aabb::CreateCenterRadius(TestBrushCenter2d + (3.0f * stampIncrement), TestBrushRadius));
  184. EXPECT_THAT(dirtyArea, IsClose(expectedSecondDirtyArea));
  185. });
  186. paintBrush.PaintToLocation(secondLocation, m_settings);
  187. paintBrush.EndBrushStroke();
  188. paintBrush.EndPaintMode();
  189. }
  190. TEST_F(PaintBrushPaintLocationTestFixture, EyedropperDoesNotAffectPaintToLocation)
  191. {
  192. // When painting, we should be able to call UseEyedropper at any arbitrary location without affecting the current
  193. // state of PaintToLocation.
  194. AzFramework::PaintBrush paintBrush(EntityComponentIdPair);
  195. ::testing::NiceMock<MockPaintBrushNotificationBusHandler> mockHandler(EntityComponentIdPair);
  196. // Set the brush radius to 1 meter (diameter is 2 meters)
  197. const float TestBrushRadius = 1.0f;
  198. const float TestBrushSize = TestBrushRadius * 2.0f;
  199. m_settings.SetSize(TestBrushSize);
  200. // Choose a second brush center location that's 2 full brush stamps away in the X direction only.
  201. AZ::Vector3 secondLocation = TestBrushCenter + AZ::Vector3(TestBrushSize * 2.0f, 0.0f, 0.0f);
  202. // We expect to get two OnPaint calls.
  203. EXPECT_CALL(mockHandler, OnPaint(::testing::_, ::testing::_, ::testing::_, ::testing::_)).Times(2);
  204. ON_CALL(mockHandler, OnPaint)
  205. .WillByDefault(
  206. [this, TestBrushRadius]([[maybe_unused]] const AZ::Color& color, const AZ::Aabb& dirtyArea,
  207. [[maybe_unused]] AzFramework::PaintBrushNotifications::ValueLookupFn& valueLookupFn,
  208. [[maybe_unused]] AzFramework::PaintBrushNotifications::BlendFn& blendFn)
  209. {
  210. // We expect that the Y value for our dirtyArea won't be changed even though we'll call UseEyedropper
  211. // with a large Y value in-between the two paint calls.
  212. EXPECT_NEAR(dirtyArea.GetMin().GetY(), TestBrushCenter.GetY() - TestBrushRadius, 0.01f);
  213. EXPECT_NEAR(dirtyArea.GetMax().GetY(), TestBrushCenter.GetY() + TestBrushRadius, 0.01f);
  214. });
  215. paintBrush.BeginPaintMode();
  216. paintBrush.BeginBrushStroke(m_settings);
  217. paintBrush.PaintToLocation(TestBrushCenter, m_settings);
  218. // Call UseEyeDropper with a large Y value so that we can easily detect if it affected our PaintToLocation calls.
  219. [[maybe_unused]] AZ::Color color = paintBrush.UseEyedropper(TestBrushCenter + AZ::Vector3(0.0f, 1000.0f, 0.0f));
  220. paintBrush.PaintToLocation(secondLocation, m_settings);
  221. paintBrush.EndBrushStroke();
  222. paintBrush.EndPaintMode();
  223. }
  224. TEST_F(PaintBrushPaintLocationTestFixture, ResetBrushStrokeTrackingWorksCorrectly)
  225. {
  226. // If ResetBrushStrokeTracking is called in-between two calls to PaintToLocation within a brush stroke,
  227. // there should be a discontinuity between the two locations as if the brush has been picked up and put back down.
  228. // i.e. Instead of 'OOOOOO' between two locations it should create 'O O'.
  229. // This is typically used for handling things like leaving the edge of the image at one location and coming back onto
  230. // the image at a different location.
  231. AzFramework::PaintBrush paintBrush(EntityComponentIdPair);
  232. ::testing::NiceMock<MockPaintBrushNotificationBusHandler> mockHandler(EntityComponentIdPair);
  233. // Set the distance between brush stamps to 100%
  234. const float TestDistancePercent = 100.0f;
  235. m_settings.SetDistancePercent(TestDistancePercent);
  236. // Set the brush radius to 1 meter (diameter is 2 meters)
  237. const float TestBrushRadius = 1.0f;
  238. const float TestBrushSize = TestBrushRadius * 2.0f;
  239. m_settings.SetSize(TestBrushSize);
  240. // Choose a second brush center location that's 10 full brush stamps away to make it obvious whether or not
  241. // there are any points tracked between the two locations.
  242. const AZ::Vector3 secondLocation = TestBrushCenter + AZ::Vector3(TestBrushSize * 10.0f, 0.0f, 0.0f);
  243. // We expect to get two OnPaint calls.
  244. EXPECT_CALL(mockHandler, OnPaint(::testing::_, ::testing::_, ::testing::_, ::testing::_)).Times(2);
  245. paintBrush.BeginPaintMode();
  246. paintBrush.BeginBrushStroke(m_settings);
  247. ON_CALL(mockHandler, OnPaint)
  248. .WillByDefault(
  249. [this, TestBrushRadius]([[maybe_unused]] const AZ::Color& color, const AZ::Aabb& dirtyArea,
  250. [[maybe_unused]] AzFramework::PaintBrushNotifications::ValueLookupFn& valueLookupFn,
  251. [[maybe_unused]] AzFramework::PaintBrushNotifications::BlendFn& blendFn)
  252. {
  253. // On the first PaintToLocation, we expect to get a dirtyArea that exactly fits our first paint brush stamp.
  254. const AZ::Aabb expectedFirstDirtyArea = AZ::Aabb::CreateCenterRadius(TestBrushCenter2d, TestBrushRadius);
  255. EXPECT_THAT(dirtyArea, IsClose(expectedFirstDirtyArea));
  256. });
  257. paintBrush.PaintToLocation(TestBrushCenter, m_settings);
  258. // Reset the brush stroke tracking, so that the next location will look like the start of a stroke again.
  259. paintBrush.ResetBrushStrokeTracking();
  260. ON_CALL(mockHandler, OnPaint)
  261. .WillByDefault(
  262. [=]([[maybe_unused]] const AZ::Color& color, const AZ::Aabb& dirtyArea,
  263. [[maybe_unused]] AzFramework::PaintBrushNotifications::ValueLookupFn& valueLookupFn,
  264. [[maybe_unused]] AzFramework::PaintBrushNotifications::BlendFn& blendFn)
  265. {
  266. // On the second PaintToLocation, we expect to get a dirtyArea that exactly fits our second paint brush stamp.
  267. // It should *not* include any of the space bewteen the first and the second brush stamp.
  268. const AZ::Vector3 secondLocation2d(AZ::Vector2(secondLocation), 0.0f);
  269. const AZ::Aabb expectedSecondDirtyArea = AZ::Aabb::CreateCenterRadius(secondLocation2d, TestBrushRadius);
  270. EXPECT_THAT(dirtyArea, IsClose(expectedSecondDirtyArea));
  271. });
  272. paintBrush.PaintToLocation(secondLocation, m_settings);
  273. paintBrush.EndBrushStroke();
  274. paintBrush.EndPaintMode();
  275. }
  276. } // namespace UnitTest