diff options
Diffstat (limited to 'tests/core/math')
-rw-r--r-- | tests/core/math/test_aabb.h | 395 | ||||
-rw-r--r-- | tests/core/math/test_astar.h | 362 | ||||
-rw-r--r-- | tests/core/math/test_basis.h | 286 | ||||
-rw-r--r-- | tests/core/math/test_color.h | 207 | ||||
-rw-r--r-- | tests/core/math/test_expression.h | 444 | ||||
-rw-r--r-- | tests/core/math/test_geometry_2d.h | 567 | ||||
-rw-r--r-- | tests/core/math/test_geometry_3d.h | 437 | ||||
-rw-r--r-- | tests/core/math/test_math.cpp | 690 | ||||
-rw-r--r-- | tests/core/math/test_math.h | 41 | ||||
-rw-r--r-- | tests/core/math/test_random_number_generator.h | 275 | ||||
-rw-r--r-- | tests/core/math/test_rect2.h | 575 |
11 files changed, 4279 insertions, 0 deletions
diff --git a/tests/core/math/test_aabb.h b/tests/core/math/test_aabb.h new file mode 100644 index 0000000000..f5076ce1ed --- /dev/null +++ b/tests/core/math/test_aabb.h @@ -0,0 +1,395 @@ +/*************************************************************************/ +/* test_aabb.h */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/*************************************************************************/ + +#ifndef TEST_AABB_H +#define TEST_AABB_H + +#include "core/math/aabb.h" + +#include "tests/test_macros.h" + +namespace TestAABB { + +TEST_CASE("[AABB] Constructor methods") { + const AABB aabb = AABB(Vector3(-1.5, 2, -2.5), Vector3(4, 5, 6)); + const AABB aabb_copy = AABB(aabb); + + CHECK_MESSAGE( + aabb == aabb_copy, + "AABBs created with the same dimensions but by different methods should be equal."); +} + +TEST_CASE("[AABB] String conversion") { + CHECK_MESSAGE( + String(AABB(Vector3(-1.5, 2, -2.5), Vector3(4, 5, 6))) == "[P: (-1.5, 2, -2.5), S: (4, 5, 6)]", + "The string representation should match the expected value."); +} + +TEST_CASE("[AABB] Basic getters") { + const AABB aabb = AABB(Vector3(-1.5, 2, -2.5), Vector3(4, 5, 6)); + CHECK_MESSAGE( + aabb.get_position().is_equal_approx(Vector3(-1.5, 2, -2.5)), + "get_position() should return the expected value."); + CHECK_MESSAGE( + aabb.get_size().is_equal_approx(Vector3(4, 5, 6)), + "get_size() should return the expected value."); + CHECK_MESSAGE( + aabb.get_end().is_equal_approx(Vector3(2.5, 7, 3.5)), + "get_end() should return the expected value."); + CHECK_MESSAGE( + aabb.get_center().is_equal_approx(Vector3(0.5, 4.5, 0.5)), + "get_center() should return the expected value."); +} + +TEST_CASE("[AABB] Basic setters") { + AABB aabb = AABB(Vector3(-1.5, 2, -2.5), Vector3(4, 5, 6)); + aabb.set_end(Vector3(100, 0, 100)); + CHECK_MESSAGE( + aabb.is_equal_approx(AABB(Vector3(-1.5, 2, -2.5), Vector3(101.5, -2, 102.5))), + "set_end() should result in the expected AABB."); + + aabb = AABB(Vector3(-1.5, 2, -2.5), Vector3(4, 5, 6)); + aabb.set_position(Vector3(-1000, -2000, -3000)); + CHECK_MESSAGE( + aabb.is_equal_approx(AABB(Vector3(-1000, -2000, -3000), Vector3(4, 5, 6))), + "set_position() should result in the expected AABB."); + + aabb = AABB(Vector3(-1.5, 2, -2.5), Vector3(4, 5, 6)); + aabb.set_size(Vector3(0, 0, -50)); + CHECK_MESSAGE( + aabb.is_equal_approx(AABB(Vector3(-1.5, 2, -2.5), Vector3(0, 0, -50))), + "set_size() should result in the expected AABB."); +} + +TEST_CASE("[AABB] Volume getters") { + AABB aabb = AABB(Vector3(-1.5, 2, -2.5), Vector3(4, 5, 6)); + CHECK_MESSAGE( + Math::is_equal_approx(aabb.get_volume(), 120), + "get_volume() should return the expected value with positive size."); + CHECK_MESSAGE( + !aabb.has_no_volume(), + "Non-empty volumetric AABB should have a volume."); + + aabb = AABB(Vector3(-1.5, 2, -2.5), Vector3(-4, 5, 6)); + CHECK_MESSAGE( + Math::is_equal_approx(aabb.get_volume(), -120), + "get_volume() should return the expected value with negative size (1 component)."); + + aabb = AABB(Vector3(-1.5, 2, -2.5), Vector3(-4, -5, 6)); + CHECK_MESSAGE( + Math::is_equal_approx(aabb.get_volume(), 120), + "get_volume() should return the expected value with negative size (2 components)."); + + aabb = AABB(Vector3(-1.5, 2, -2.5), Vector3(-4, -5, -6)); + CHECK_MESSAGE( + Math::is_equal_approx(aabb.get_volume(), -120), + "get_volume() should return the expected value with negative size (3 components)."); + + aabb = AABB(Vector3(-1.5, 2, -2.5), Vector3(4, 0, 6)); + CHECK_MESSAGE( + aabb.has_no_volume(), + "Non-empty flat AABB should not have a volume."); + + CHECK_MESSAGE( + AABB().has_no_volume(), + "Empty AABB should not have a volume."); +} + +TEST_CASE("[AABB] Surface getters") { + AABB aabb = AABB(Vector3(-1.5, 2, -2.5), Vector3(4, 5, 6)); + CHECK_MESSAGE( + !aabb.has_no_surface(), + "Non-empty volumetric AABB should have an surface."); + + aabb = AABB(Vector3(-1.5, 2, -2.5), Vector3(4, 0, 6)); + CHECK_MESSAGE( + !aabb.has_no_surface(), + "Non-empty flat AABB should have a surface."); + + CHECK_MESSAGE( + AABB().has_no_surface(), + "Empty AABB should not have an surface."); +} + +TEST_CASE("[AABB] Intersection") { + const AABB aabb_big = AABB(Vector3(-1.5, 2, -2.5), Vector3(4, 5, 6)); + + AABB aabb_small = AABB(Vector3(-1.5, 2, -2.5), Vector3(1, 1, 1)); + CHECK_MESSAGE( + aabb_big.intersects(aabb_small), + "intersects() with fully contained AABB (touching the edge) should return the expected result."); + + aabb_small = AABB(Vector3(0.5, 1.5, -2), Vector3(1, 1, 1)); + CHECK_MESSAGE( + aabb_big.intersects(aabb_small), + "intersects() with partially contained AABB (overflowing on Y axis) should return the expected result."); + + aabb_small = AABB(Vector3(10, -10, -10), Vector3(1, 1, 1)); + CHECK_MESSAGE( + !aabb_big.intersects(aabb_small), + "intersects() with non-contained AABB should return the expected result."); + + aabb_small = AABB(Vector3(-1.5, 2, -2.5), Vector3(1, 1, 1)); + CHECK_MESSAGE( + aabb_big.intersection(aabb_small).is_equal_approx(aabb_small), + "intersection() with fully contained AABB (touching the edge) should return the expected result."); + + aabb_small = AABB(Vector3(0.5, 1.5, -2), Vector3(1, 1, 1)); + CHECK_MESSAGE( + aabb_big.intersection(aabb_small).is_equal_approx(AABB(Vector3(0.5, 2, -2), Vector3(1, 0.5, 1))), + "intersection() with partially contained AABB (overflowing on Y axis) should return the expected result."); + + aabb_small = AABB(Vector3(10, -10, -10), Vector3(1, 1, 1)); + CHECK_MESSAGE( + aabb_big.intersection(aabb_small).is_equal_approx(AABB()), + "intersection() with non-contained AABB should return the expected result."); + + CHECK_MESSAGE( + aabb_big.intersects_plane(Plane(Vector3(0, 1, 0), 4)), + "intersects_plane() should return the expected result."); + CHECK_MESSAGE( + aabb_big.intersects_plane(Plane(Vector3(0, -1, 0), -4)), + "intersects_plane() should return the expected result."); + CHECK_MESSAGE( + !aabb_big.intersects_plane(Plane(Vector3(0, 1, 0), 200)), + "intersects_plane() should return the expected result."); + + CHECK_MESSAGE( + aabb_big.intersects_segment(Vector3(1, 3, 0), Vector3(0, 3, 0)), + "intersects_segment() should return the expected result."); + CHECK_MESSAGE( + aabb_big.intersects_segment(Vector3(0, 3, 0), Vector3(0, -300, 0)), + "intersects_segment() should return the expected result."); + CHECK_MESSAGE( + aabb_big.intersects_segment(Vector3(-50, 3, -50), Vector3(50, 3, 50)), + "intersects_segment() should return the expected result."); + CHECK_MESSAGE( + !aabb_big.intersects_segment(Vector3(-50, 25, -50), Vector3(50, 25, 50)), + "intersects_segment() should return the expected result."); + CHECK_MESSAGE( + aabb_big.intersects_segment(Vector3(0, 3, 0), Vector3(0, 3, 0)), + "intersects_segment() should return the expected result with segment of length 0."); + CHECK_MESSAGE( + !aabb_big.intersects_segment(Vector3(0, 300, 0), Vector3(0, 300, 0)), + "intersects_segment() should return the expected result with segment of length 0."); +} + +TEST_CASE("[AABB] Merging") { + const AABB aabb_big = AABB(Vector3(-1.5, 2, -2.5), Vector3(4, 5, 6)); + + AABB aabb_small = AABB(Vector3(-1.5, 2, -2.5), Vector3(1, 1, 1)); + CHECK_MESSAGE( + aabb_big.merge(aabb_small).is_equal_approx(aabb_big), + "merge() with fully contained AABB (touching the edge) should return the expected result."); + + aabb_small = AABB(Vector3(0.5, 1.5, -2), Vector3(1, 1, 1)); + CHECK_MESSAGE( + aabb_big.merge(aabb_small).is_equal_approx(AABB(Vector3(-1.5, 1.5, -2.5), Vector3(4, 5.5, 6))), + "merge() with partially contained AABB (overflowing on Y axis) should return the expected result."); + + aabb_small = AABB(Vector3(10, -10, -10), Vector3(1, 1, 1)); + CHECK_MESSAGE( + aabb_big.merge(aabb_small).is_equal_approx(AABB(Vector3(-1.5, -10, -10), Vector3(12.5, 17, 13.5))), + "merge() with non-contained AABB should return the expected result."); +} + +TEST_CASE("[AABB] Encloses") { + const AABB aabb_big = AABB(Vector3(-1.5, 2, -2.5), Vector3(4, 5, 6)); + + AABB aabb_small = AABB(Vector3(-1.5, 2, -2.5), Vector3(1, 1, 1)); + CHECK_MESSAGE( + aabb_big.encloses(aabb_small), + "encloses() with fully contained AABB (touching the edge) should return the expected result."); + + aabb_small = AABB(Vector3(0.5, 1.5, -2), Vector3(1, 1, 1)); + CHECK_MESSAGE( + !aabb_big.encloses(aabb_small), + "encloses() with partially contained AABB (overflowing on Y axis) should return the expected result."); + + aabb_small = AABB(Vector3(10, -10, -10), Vector3(1, 1, 1)); + CHECK_MESSAGE( + !aabb_big.encloses(aabb_small), + "encloses() with non-contained AABB should return the expected result."); +} + +TEST_CASE("[AABB] Get endpoints") { + const AABB aabb = AABB(Vector3(-1.5, 2, -2.5), Vector3(4, 5, 6)); + CHECK_MESSAGE( + aabb.get_endpoint(0).is_equal_approx(Vector3(-1.5, 2, -2.5)), + "The endpoint at index 0 should match the expected value."); + CHECK_MESSAGE( + aabb.get_endpoint(1).is_equal_approx(Vector3(-1.5, 2, 3.5)), + "The endpoint at index 1 should match the expected value."); + CHECK_MESSAGE( + aabb.get_endpoint(2).is_equal_approx(Vector3(-1.5, 7, -2.5)), + "The endpoint at index 2 should match the expected value."); + CHECK_MESSAGE( + aabb.get_endpoint(3).is_equal_approx(Vector3(-1.5, 7, 3.5)), + "The endpoint at index 3 should match the expected value."); + CHECK_MESSAGE( + aabb.get_endpoint(4).is_equal_approx(Vector3(2.5, 2, -2.5)), + "The endpoint at index 4 should match the expected value."); + CHECK_MESSAGE( + aabb.get_endpoint(5).is_equal_approx(Vector3(2.5, 2, 3.5)), + "The endpoint at index 5 should match the expected value."); + CHECK_MESSAGE( + aabb.get_endpoint(6).is_equal_approx(Vector3(2.5, 7, -2.5)), + "The endpoint at index 6 should match the expected value."); + CHECK_MESSAGE( + aabb.get_endpoint(7).is_equal_approx(Vector3(2.5, 7, 3.5)), + "The endpoint at index 7 should match the expected value."); + + ERR_PRINT_OFF; + CHECK_MESSAGE( + aabb.get_endpoint(8).is_equal_approx(Vector3()), + "The endpoint at invalid index 8 should match the expected value."); + CHECK_MESSAGE( + aabb.get_endpoint(-1).is_equal_approx(Vector3()), + "The endpoint at invalid index -1 should match the expected value."); + ERR_PRINT_ON; +} + +TEST_CASE("[AABB] Get longest/shortest axis") { + const AABB aabb = AABB(Vector3(-1.5, 2, -2.5), Vector3(4, 5, 6)); + CHECK_MESSAGE( + aabb.get_longest_axis() == Vector3(0, 0, 1), + "get_longest_axis() should return the expected value."); + CHECK_MESSAGE( + aabb.get_longest_axis_index() == Vector3::AXIS_Z, + "get_longest_axis_index() should return the expected value."); + CHECK_MESSAGE( + aabb.get_longest_axis_size() == 6, + "get_longest_axis_size() should return the expected value."); + + CHECK_MESSAGE( + aabb.get_shortest_axis() == Vector3(1, 0, 0), + "get_shortest_axis() should return the expected value."); + CHECK_MESSAGE( + aabb.get_shortest_axis_index() == Vector3::AXIS_X, + "get_shortest_axis_index() should return the expected value."); + CHECK_MESSAGE( + aabb.get_shortest_axis_size() == 4, + "get_shortest_axis_size() should return the expected value."); +} + +#ifndef _MSC_VER +#warning Support tests need to be re-done +#endif + +/* Support function was actually broken. As it was fixed, the tests now fail. Tests need to be re-done. + +TEST_CASE("[AABB] Get support") { + const AABB aabb = AABB(Vector3(-1.5, 2, -2.5), Vector3(4, 5, 6)); + CHECK_MESSAGE( + aabb.get_support(Vector3(1, 0, 0)).is_equal_approx(Vector3(-1.5, 7, 3.5)), + "get_support() should return the expected value."); + CHECK_MESSAGE( + aabb.get_support(Vector3(0.5, 1, 0)).is_equal_approx(Vector3(-1.5, 2, 3.5)), + "get_support() should return the expected value."); + CHECK_MESSAGE( + aabb.get_support(Vector3(0.5, 1, -400)).is_equal_approx(Vector3(-1.5, 2, 3.5)), + "get_support() should return the expected value."); + CHECK_MESSAGE( + aabb.get_support(Vector3(0, -1, 0)).is_equal_approx(Vector3(2.5, 7, 3.5)), + "get_support() should return the expected value."); + CHECK_MESSAGE( + aabb.get_support(Vector3(0, -0.1, 0)).is_equal_approx(Vector3(2.5, 7, 3.5)), + "get_support() should return the expected value."); + CHECK_MESSAGE( + aabb.get_support(Vector3()).is_equal_approx(Vector3(2.5, 7, 3.5)), + "get_support() should return the expected value with a null vector."); +} +*/ +TEST_CASE("[AABB] Grow") { + const AABB aabb = AABB(Vector3(-1.5, 2, -2.5), Vector3(4, 5, 6)); + CHECK_MESSAGE( + aabb.grow(0.25).is_equal_approx(AABB(Vector3(-1.75, 1.75, -2.75), Vector3(4.5, 5.5, 6.5))), + "grow() with positive value should return the expected AABB."); + CHECK_MESSAGE( + aabb.grow(-0.25).is_equal_approx(AABB(Vector3(-1.25, 2.25, -2.25), Vector3(3.5, 4.5, 5.5))), + "grow() with negative value should return the expected AABB."); + CHECK_MESSAGE( + aabb.grow(-10).is_equal_approx(AABB(Vector3(8.5, 12, 7.5), Vector3(-16, -15, -14))), + "grow() with large negative value should return the expected AABB."); +} + +TEST_CASE("[AABB] Has point") { + const AABB aabb = AABB(Vector3(-1.5, 2, -2.5), Vector3(4, 5, 6)); + CHECK_MESSAGE( + aabb.has_point(Vector3(-1, 3, 0)), + "has_point() with contained point should return the expected value."); + CHECK_MESSAGE( + aabb.has_point(Vector3(2, 3, 0)), + "has_point() with contained point should return the expected value."); + CHECK_MESSAGE( + !aabb.has_point(Vector3(-20, 0, 0)), + "has_point() with non-contained point should return the expected value."); + + CHECK_MESSAGE( + aabb.has_point(Vector3(-1.5, 3, 0)), + "has_point() with positive size should include point on near face (X axis)."); + CHECK_MESSAGE( + aabb.has_point(Vector3(2.5, 3, 0)), + "has_point() with positive size should include point on far face (X axis)."); + CHECK_MESSAGE( + aabb.has_point(Vector3(0, 2, 0)), + "has_point() with positive size should include point on near face (Y axis)."); + CHECK_MESSAGE( + aabb.has_point(Vector3(0, 7, 0)), + "has_point() with positive size should include point on far face (Y axis)."); + CHECK_MESSAGE( + aabb.has_point(Vector3(0, 3, -2.5)), + "has_point() with positive size should include point on near face (Z axis)."); + CHECK_MESSAGE( + aabb.has_point(Vector3(0, 3, 3.5)), + "has_point() with positive size should include point on far face (Z axis)."); +} + +TEST_CASE("[AABB] Expanding") { + const AABB aabb = AABB(Vector3(-1.5, 2, -2.5), Vector3(4, 5, 6)); + CHECK_MESSAGE( + aabb.expand(Vector3(-1, 3, 0)).is_equal_approx(aabb), + "expand() with contained point should return the expected AABB."); + CHECK_MESSAGE( + aabb.expand(Vector3(2, 3, 0)).is_equal_approx(aabb), + "expand() with contained point should return the expected AABB."); + CHECK_MESSAGE( + aabb.expand(Vector3(-1.5, 3, 0)).is_equal_approx(aabb), + "expand() with contained point on negative edge should return the expected AABB."); + CHECK_MESSAGE( + aabb.expand(Vector3(2.5, 3, 0)).is_equal_approx(aabb), + "expand() with contained point on positive edge should return the expected AABB."); + CHECK_MESSAGE( + aabb.expand(Vector3(-20, 0, 0)).is_equal_approx(AABB(Vector3(-20, 0, -2.5), Vector3(22.5, 7, 6))), + "expand() with non-contained point should return the expected AABB."); +} +} // namespace TestAABB + +#endif // TEST_AABB_H diff --git a/tests/core/math/test_astar.h b/tests/core/math/test_astar.h new file mode 100644 index 0000000000..2c183374ac --- /dev/null +++ b/tests/core/math/test_astar.h @@ -0,0 +1,362 @@ +/*************************************************************************/ +/* test_astar.h */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/*************************************************************************/ + +#ifndef TEST_ASTAR_H +#define TEST_ASTAR_H + +#include "core/math/a_star.h" + +#include "tests/test_macros.h" + +namespace TestAStar { + +class ABCX : public AStar { +public: + enum { + A, + B, + C, + X, + }; + + ABCX() { + add_point(A, Vector3(0, 0, 0)); + add_point(B, Vector3(1, 0, 0)); + add_point(C, Vector3(0, 1, 0)); + add_point(X, Vector3(0, 0, 1)); + connect_points(A, B); + connect_points(A, C); + connect_points(B, C); + connect_points(X, A); + } + + // Disable heuristic completely. + real_t _compute_cost(int p_from, int p_to) { + if (p_from == A && p_to == C) { + return 1000; + } + return 100; + } +}; + +TEST_CASE("[AStar] ABC path") { + ABCX abcx; + Vector<int> path = abcx.get_id_path(ABCX::A, ABCX::C); + REQUIRE(path.size() == 3); + CHECK(path[0] == ABCX::A); + CHECK(path[1] == ABCX::B); + CHECK(path[2] == ABCX::C); +} + +TEST_CASE("[AStar] ABCX path") { + ABCX abcx; + Vector<int> path = abcx.get_id_path(ABCX::X, ABCX::C); + REQUIRE(path.size() == 4); + CHECK(path[0] == ABCX::X); + CHECK(path[1] == ABCX::A); + CHECK(path[2] == ABCX::B); + CHECK(path[3] == ABCX::C); +} + +TEST_CASE("[AStar] Add/Remove") { + AStar a; + + // Manual tests. + a.add_point(1, Vector3(0, 0, 0)); + a.add_point(2, Vector3(0, 1, 0)); + a.add_point(3, Vector3(1, 1, 0)); + a.add_point(4, Vector3(2, 0, 0)); + a.connect_points(1, 2, true); + a.connect_points(1, 3, true); + a.connect_points(1, 4, false); + + CHECK(a.are_points_connected(2, 1)); + CHECK(a.are_points_connected(4, 1)); + CHECK(a.are_points_connected(2, 1, false)); + CHECK_FALSE(a.are_points_connected(4, 1, false)); + + a.disconnect_points(1, 2, true); + CHECK(a.get_point_connections(1).size() == 2); // 3, 4 + CHECK(a.get_point_connections(2).size() == 0); + + a.disconnect_points(4, 1, false); + CHECK(a.get_point_connections(1).size() == 2); // 3, 4 + CHECK(a.get_point_connections(4).size() == 0); + + a.disconnect_points(4, 1, true); + CHECK(a.get_point_connections(1).size() == 1); // 3 + CHECK(a.get_point_connections(4).size() == 0); + + a.connect_points(2, 3, false); + CHECK(a.get_point_connections(2).size() == 1); // 3 + CHECK(a.get_point_connections(3).size() == 1); // 1 + + a.connect_points(2, 3, true); + CHECK(a.get_point_connections(2).size() == 1); // 3 + CHECK(a.get_point_connections(3).size() == 2); // 1, 2 + + a.disconnect_points(2, 3, false); + CHECK(a.get_point_connections(2).size() == 0); + CHECK(a.get_point_connections(3).size() == 2); // 1, 2 + + a.connect_points(4, 3, true); + CHECK(a.get_point_connections(3).size() == 3); // 1, 2, 4 + CHECK(a.get_point_connections(4).size() == 1); // 3 + + a.disconnect_points(3, 4, false); + CHECK(a.get_point_connections(3).size() == 2); // 1, 2 + CHECK(a.get_point_connections(4).size() == 1); // 3 + + a.remove_point(3); + CHECK(a.get_point_connections(1).size() == 0); + CHECK(a.get_point_connections(2).size() == 0); + CHECK(a.get_point_connections(4).size() == 0); + + a.add_point(0, Vector3(0, -1, 0)); + a.add_point(3, Vector3(2, 1, 0)); + // 0: (0, -1) + // 1: (0, 0) + // 2: (0, 1) + // 3: (2, 1) + // 4: (2, 0) + + // Tests for get_closest_position_in_segment. + a.connect_points(2, 3); + CHECK(a.get_closest_position_in_segment(Vector3(0.5, 0.5, 0)) == Vector3(0.5, 1, 0)); + + a.connect_points(3, 4); + a.connect_points(0, 3); + a.connect_points(1, 4); + a.disconnect_points(1, 4, false); + a.disconnect_points(4, 3, false); + a.disconnect_points(3, 4, false); + // Remaining edges: <2, 3>, <0, 3>, <1, 4> (directed). + CHECK(a.get_closest_position_in_segment(Vector3(2, 0.5, 0)) == Vector3(1.75, 0.75, 0)); + CHECK(a.get_closest_position_in_segment(Vector3(-1, 0.2, 0)) == Vector3(0, 0, 0)); + CHECK(a.get_closest_position_in_segment(Vector3(3, 2, 0)) == Vector3(2, 1, 0)); + + Math::seed(0); + + // Random tests for connectivity checks + for (int i = 0; i < 20000; i++) { + int u = Math::rand() % 5; + int v = Math::rand() % 4; + if (u == v) { + v = 4; + } + if (Math::rand() % 2 == 1) { + // Add a (possibly existing) directed edge and confirm connectivity. + a.connect_points(u, v, false); + CHECK(a.are_points_connected(u, v, false)); + } else { + // Remove a (possibly nonexistent) directed edge and confirm disconnectivity. + a.disconnect_points(u, v, false); + CHECK_FALSE(a.are_points_connected(u, v, false)); + } + } + + // Random tests for point removal. + for (int i = 0; i < 20000; i++) { + a.clear(); + for (int j = 0; j < 5; j++) { + a.add_point(j, Vector3(0, 0, 0)); + } + + // Add or remove random edges. + for (int j = 0; j < 10; j++) { + int u = Math::rand() % 5; + int v = Math::rand() % 4; + if (u == v) { + v = 4; + } + if (Math::rand() % 2 == 1) { + a.connect_points(u, v, false); + } else { + a.disconnect_points(u, v, false); + } + } + + // Remove point 0. + a.remove_point(0); + // White box: this will check all edges remaining in the segments set. + for (int j = 1; j < 5; j++) { + CHECK_FALSE(a.are_points_connected(0, j, true)); + } + } + // It's been great work, cheers. \(^ ^)/ +} + +TEST_CASE("[Stress][AStar] Find paths") { + // Random stress tests with Floyd-Warshall. + const int N = 30; + Math::seed(0); + + for (int test = 0; test < 1000; test++) { + AStar a; + Vector3 p[N]; + bool adj[N][N] = { { false } }; + + // Assign initial coordinates. + for (int u = 0; u < N; u++) { + p[u].x = Math::rand() % 100; + p[u].y = Math::rand() % 100; + p[u].z = Math::rand() % 100; + a.add_point(u, p[u]); + } + // Generate a random sequence of operations. + for (int i = 0; i < 1000; i++) { + // Pick two different vertices. + int u, v; + u = Math::rand() % N; + v = Math::rand() % (N - 1); + if (u == v) { + v = N - 1; + } + // Pick a random operation. + int op = Math::rand(); + switch (op % 9) { + case 0: + case 1: + case 2: + case 3: + case 4: + case 5: + // Add edge (u, v); possibly bidirectional. + a.connect_points(u, v, op % 2); + adj[u][v] = true; + if (op % 2) { + adj[v][u] = true; + } + break; + case 6: + case 7: + // Remove edge (u, v); possibly bidirectional. + a.disconnect_points(u, v, op % 2); + adj[u][v] = false; + if (op % 2) { + adj[v][u] = false; + } + break; + case 8: + // Remove point u and add it back; clears adjacent edges and changes coordinates. + a.remove_point(u); + p[u].x = Math::rand() % 100; + p[u].y = Math::rand() % 100; + p[u].z = Math::rand() % 100; + a.add_point(u, p[u]); + for (v = 0; v < N; v++) { + adj[u][v] = adj[v][u] = false; + } + break; + } + } + // Floyd-Warshall. + float d[N][N]; + for (int u = 0; u < N; u++) { + for (int v = 0; v < N; v++) { + d[u][v] = (u == v || adj[u][v]) ? p[u].distance_to(p[v]) : INFINITY; + } + } + for (int w = 0; w < N; w++) { + for (int u = 0; u < N; u++) { + for (int v = 0; v < N; v++) { + if (d[u][v] > d[u][w] + d[w][v]) { + d[u][v] = d[u][w] + d[w][v]; + } + } + } + } + // Display statistics. + int count = 0; + for (int u = 0; u < N; u++) { + for (int v = 0; v < N; v++) { + if (adj[u][v]) { + count++; + } + } + } + print_verbose(vformat("Test #%4d: %3d edges, ", test + 1, count)); + count = 0; + for (int u = 0; u < N; u++) { + for (int v = 0; v < N; v++) { + if (!Math::is_inf(d[u][v])) { + count++; + } + } + } + print_verbose(vformat("%3d/%d pairs of reachable points\n", count - N, N * (N - 1))); + + // Check A*'s output. + bool match = true; + for (int u = 0; u < N; u++) { + for (int v = 0; v < N; v++) { + if (u != v) { + Vector<int> route = a.get_id_path(u, v); + if (!Math::is_inf(d[u][v])) { + // Reachable. + if (route.size() == 0) { + print_verbose(vformat("From %d to %d: A* did not find a path\n", u, v)); + match = false; + goto exit; + } + float astar_dist = 0; + for (int i = 1; i < route.size(); i++) { + if (!adj[route[i - 1]][route[i]]) { + print_verbose(vformat("From %d to %d: edge (%d, %d) does not exist\n", + u, v, route[i - 1], route[i])); + match = false; + goto exit; + } + astar_dist += p[route[i - 1]].distance_to(p[route[i]]); + } + if (!Math::is_equal_approx(astar_dist, d[u][v])) { + print_verbose(vformat("From %d to %d: Floyd-Warshall gives %.6f, A* gives %.6f\n", + u, v, d[u][v], astar_dist)); + match = false; + goto exit; + } + } else { + // Unreachable. + if (route.size() > 0) { + print_verbose(vformat("From %d to %d: A* somehow found a nonexistent path\n", u, v)); + match = false; + goto exit; + } + } + } + } + } + exit: + CHECK_MESSAGE(match, "Found all paths."); + } +} +} // namespace TestAStar + +#endif // TEST_ASTAR_H diff --git a/tests/core/math/test_basis.h b/tests/core/math/test_basis.h new file mode 100644 index 0000000000..500c069a33 --- /dev/null +++ b/tests/core/math/test_basis.h @@ -0,0 +1,286 @@ +/*************************************************************************/ +/* test_basis.h */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/*************************************************************************/ + +#ifndef TEST_BASIS_H +#define TEST_BASIS_H + +#include "core/math/basis.h" +#include "core/math/random_number_generator.h" + +#include "tests/test_macros.h" + +namespace TestBasis { + +enum RotOrder { + EulerXYZ, + EulerXZY, + EulerYZX, + EulerYXZ, + EulerZXY, + EulerZYX +}; + +Vector3 deg2rad(const Vector3 &p_rotation) { + return p_rotation / 180.0 * Math_PI; +} + +Vector3 rad2deg(const Vector3 &p_rotation) { + return p_rotation / Math_PI * 180.0; +} + +Basis EulerToBasis(RotOrder mode, const Vector3 &p_rotation) { + Basis ret; + switch (mode) { + case EulerXYZ: + ret.set_euler(p_rotation, Basis::EULER_ORDER_XYZ); + break; + + case EulerXZY: + ret.set_euler(p_rotation, Basis::EULER_ORDER_XZY); + break; + + case EulerYZX: + ret.set_euler(p_rotation, Basis::EULER_ORDER_YZX); + break; + + case EulerYXZ: + ret.set_euler(p_rotation, Basis::EULER_ORDER_YXZ); + break; + + case EulerZXY: + ret.set_euler(p_rotation, Basis::EULER_ORDER_ZXY); + break; + + case EulerZYX: + ret.set_euler(p_rotation, Basis::EULER_ORDER_ZYX); + break; + + default: + // If you land here, Please integrate all rotation orders. + FAIL("This is not unreachable."); + } + + return ret; +} + +Vector3 BasisToEuler(RotOrder mode, const Basis &p_rotation) { + switch (mode) { + case EulerXYZ: + return p_rotation.get_euler(Basis::EULER_ORDER_XYZ); + + case EulerXZY: + return p_rotation.get_euler(Basis::EULER_ORDER_XZY); + + case EulerYZX: + return p_rotation.get_euler(Basis::EULER_ORDER_YZX); + + case EulerYXZ: + return p_rotation.get_euler(Basis::EULER_ORDER_YXZ); + + case EulerZXY: + return p_rotation.get_euler(Basis::EULER_ORDER_ZXY); + + case EulerZYX: + return p_rotation.get_euler(Basis::EULER_ORDER_ZYX); + + default: + // If you land here, Please integrate all rotation orders. + FAIL("This is not unreachable."); + return Vector3(); + } +} + +String get_rot_order_name(RotOrder ro) { + switch (ro) { + case EulerXYZ: + return "XYZ"; + case EulerXZY: + return "XZY"; + case EulerYZX: + return "YZX"; + case EulerYXZ: + return "YXZ"; + case EulerZXY: + return "ZXY"; + case EulerZYX: + return "ZYX"; + default: + return "[Not supported]"; + } +} + +void test_rotation(Vector3 deg_original_euler, RotOrder rot_order) { + // This test: + // 1. Converts the rotation vector from deg to rad. + // 2. Converts euler to basis. + // 3. Converts the above basis back into euler. + // 4. Converts the above euler into basis again. + // 5. Compares the basis obtained in step 2 with the basis of step 4 + // + // The conversion "basis to euler", done in the step 3, may be different from + // the original euler, even if the final rotation are the same. + // This happens because there are more ways to represents the same rotation, + // both valid, using eulers. + // For this reason is necessary to convert that euler back to basis and finally + // compares it. + // + // In this way we can assert that both functions: basis to euler / euler to basis + // are correct. + + // Euler to rotation + const Vector3 original_euler = deg2rad(deg_original_euler); + const Basis to_rotation = EulerToBasis(rot_order, original_euler); + + // Euler from rotation + const Vector3 euler_from_rotation = BasisToEuler(rot_order, to_rotation); + const Basis rotation_from_computed_euler = EulerToBasis(rot_order, euler_from_rotation); + + Basis res = to_rotation.inverse() * rotation_from_computed_euler; + + CHECK_MESSAGE((res.get_axis(0) - Vector3(1.0, 0.0, 0.0)).length() <= 0.1, vformat("Fail due to X %s\n", String(res.get_axis(0))).utf8().ptr()); + CHECK_MESSAGE((res.get_axis(1) - Vector3(0.0, 1.0, 0.0)).length() <= 0.1, vformat("Fail due to Y %s\n", String(res.get_axis(1))).utf8().ptr()); + CHECK_MESSAGE((res.get_axis(2) - Vector3(0.0, 0.0, 1.0)).length() <= 0.1, vformat("Fail due to Z %s\n", String(res.get_axis(2))).utf8().ptr()); + + // Double check `to_rotation` decomposing with XYZ rotation order. + const Vector3 euler_xyz_from_rotation = to_rotation.get_euler(Basis::EULER_ORDER_XYZ); + Basis rotation_from_xyz_computed_euler; + rotation_from_xyz_computed_euler.set_euler(euler_xyz_from_rotation, Basis::EULER_ORDER_XYZ); + + res = to_rotation.inverse() * rotation_from_xyz_computed_euler; + + CHECK_MESSAGE((res.get_axis(0) - Vector3(1.0, 0.0, 0.0)).length() <= 0.1, vformat("Double check with XYZ rot order failed, due to X %s\n", String(res.get_axis(0))).utf8().ptr()); + CHECK_MESSAGE((res.get_axis(1) - Vector3(0.0, 1.0, 0.0)).length() <= 0.1, vformat("Double check with XYZ rot order failed, due to Y %s\n", String(res.get_axis(1))).utf8().ptr()); + CHECK_MESSAGE((res.get_axis(2) - Vector3(0.0, 0.0, 1.0)).length() <= 0.1, vformat("Double check with XYZ rot order failed, due to Z %s\n", String(res.get_axis(2))).utf8().ptr()); + + INFO(vformat("Rotation order: %s\n.", get_rot_order_name(rot_order)).utf8().ptr()); + INFO(vformat("Original Rotation: %s\n", String(deg_original_euler)).utf8().ptr()); + INFO(vformat("Quaternion to rotation order: %s\n", String(rad2deg(euler_from_rotation))).utf8().ptr()); +} + +TEST_CASE("[Basis] Euler conversions") { + Vector<RotOrder> rotorder_to_test; + rotorder_to_test.push_back(EulerXYZ); + rotorder_to_test.push_back(EulerXZY); + rotorder_to_test.push_back(EulerYZX); + rotorder_to_test.push_back(EulerYXZ); + rotorder_to_test.push_back(EulerZXY); + rotorder_to_test.push_back(EulerZYX); + + Vector<Vector3> vectors_to_test; + + // Test the special cases. + vectors_to_test.push_back(Vector3(0.0, 0.0, 0.0)); + vectors_to_test.push_back(Vector3(0.5, 0.5, 0.5)); + vectors_to_test.push_back(Vector3(-0.5, -0.5, -0.5)); + vectors_to_test.push_back(Vector3(40.0, 40.0, 40.0)); + vectors_to_test.push_back(Vector3(-40.0, -40.0, -40.0)); + vectors_to_test.push_back(Vector3(0.0, 0.0, -90.0)); + vectors_to_test.push_back(Vector3(0.0, -90.0, 0.0)); + vectors_to_test.push_back(Vector3(-90.0, 0.0, 0.0)); + vectors_to_test.push_back(Vector3(0.0, 0.0, 90.0)); + vectors_to_test.push_back(Vector3(0.0, 90.0, 0.0)); + vectors_to_test.push_back(Vector3(90.0, 0.0, 0.0)); + vectors_to_test.push_back(Vector3(0.0, 0.0, -30.0)); + vectors_to_test.push_back(Vector3(0.0, -30.0, 0.0)); + vectors_to_test.push_back(Vector3(-30.0, 0.0, 0.0)); + vectors_to_test.push_back(Vector3(0.0, 0.0, 30.0)); + vectors_to_test.push_back(Vector3(0.0, 30.0, 0.0)); + vectors_to_test.push_back(Vector3(30.0, 0.0, 0.0)); + vectors_to_test.push_back(Vector3(0.5, 50.0, 20.0)); + vectors_to_test.push_back(Vector3(-0.5, -50.0, -20.0)); + vectors_to_test.push_back(Vector3(0.5, 0.0, 90.0)); + vectors_to_test.push_back(Vector3(0.5, 0.0, -90.0)); + vectors_to_test.push_back(Vector3(360.0, 360.0, 360.0)); + vectors_to_test.push_back(Vector3(-360.0, -360.0, -360.0)); + vectors_to_test.push_back(Vector3(-90.0, 60.0, -90.0)); + vectors_to_test.push_back(Vector3(90.0, 60.0, -90.0)); + vectors_to_test.push_back(Vector3(90.0, -60.0, -90.0)); + vectors_to_test.push_back(Vector3(-90.0, -60.0, -90.0)); + vectors_to_test.push_back(Vector3(-90.0, 60.0, 90.0)); + vectors_to_test.push_back(Vector3(90.0, 60.0, 90.0)); + vectors_to_test.push_back(Vector3(90.0, -60.0, 90.0)); + vectors_to_test.push_back(Vector3(-90.0, -60.0, 90.0)); + vectors_to_test.push_back(Vector3(60.0, 90.0, -40.0)); + vectors_to_test.push_back(Vector3(60.0, -90.0, -40.0)); + vectors_to_test.push_back(Vector3(-60.0, -90.0, -40.0)); + vectors_to_test.push_back(Vector3(-60.0, 90.0, 40.0)); + vectors_to_test.push_back(Vector3(60.0, 90.0, 40.0)); + vectors_to_test.push_back(Vector3(60.0, -90.0, 40.0)); + vectors_to_test.push_back(Vector3(-60.0, -90.0, 40.0)); + vectors_to_test.push_back(Vector3(-90.0, 90.0, -90.0)); + vectors_to_test.push_back(Vector3(90.0, 90.0, -90.0)); + vectors_to_test.push_back(Vector3(90.0, -90.0, -90.0)); + vectors_to_test.push_back(Vector3(-90.0, -90.0, -90.0)); + vectors_to_test.push_back(Vector3(-90.0, 90.0, 90.0)); + vectors_to_test.push_back(Vector3(90.0, 90.0, 90.0)); + vectors_to_test.push_back(Vector3(90.0, -90.0, 90.0)); + vectors_to_test.push_back(Vector3(20.0, 150.0, 30.0)); + vectors_to_test.push_back(Vector3(20.0, -150.0, 30.0)); + vectors_to_test.push_back(Vector3(-120.0, -150.0, 30.0)); + vectors_to_test.push_back(Vector3(-120.0, -150.0, -130.0)); + vectors_to_test.push_back(Vector3(120.0, -150.0, -130.0)); + vectors_to_test.push_back(Vector3(120.0, 150.0, -130.0)); + vectors_to_test.push_back(Vector3(120.0, 150.0, 130.0)); + + for (int h = 0; h < rotorder_to_test.size(); h += 1) { + for (int i = 0; i < vectors_to_test.size(); i += 1) { + test_rotation(vectors_to_test[i], rotorder_to_test[h]); + } + } +} + +TEST_CASE("[Stress][Basis] Euler conversions") { + Vector<RotOrder> rotorder_to_test; + rotorder_to_test.push_back(EulerXYZ); + rotorder_to_test.push_back(EulerXZY); + rotorder_to_test.push_back(EulerYZX); + rotorder_to_test.push_back(EulerYXZ); + rotorder_to_test.push_back(EulerZXY); + rotorder_to_test.push_back(EulerZYX); + + Vector<Vector3> vectors_to_test; + // Add 1000 random vectors with weirds numbers. + RandomNumberGenerator rng; + for (int _ = 0; _ < 1000; _ += 1) { + vectors_to_test.push_back(Vector3( + rng.randf_range(-1800, 1800), + rng.randf_range(-1800, 1800), + rng.randf_range(-1800, 1800))); + } + + for (int h = 0; h < rotorder_to_test.size(); h += 1) { + for (int i = 0; i < vectors_to_test.size(); i += 1) { + test_rotation(vectors_to_test[i], rotorder_to_test[h]); + } + } +} +} // namespace TestBasis + +#endif diff --git a/tests/core/math/test_color.h b/tests/core/math/test_color.h new file mode 100644 index 0000000000..82cf786f7a --- /dev/null +++ b/tests/core/math/test_color.h @@ -0,0 +1,207 @@ +/*************************************************************************/ +/* test_color.h */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/*************************************************************************/ + +#ifndef TEST_COLOR_H +#define TEST_COLOR_H + +#include "core/math/color.h" + +#include "tests/test_macros.h" + +namespace TestColor { + +TEST_CASE("[Color] Constructor methods") { + const Color blue_rgba = Color(0.25098, 0.376471, 1, 0.501961); + const Color blue_html = Color::html("#4060ff80"); + const Color blue_hex = Color::hex(0x4060ff80); + const Color blue_hex64 = Color::hex64(0x4040'6060'ffff'8080); + + CHECK_MESSAGE( + blue_rgba.is_equal_approx(blue_html), + "Creation with HTML notation should result in components approximately equal to the default constructor."); + CHECK_MESSAGE( + blue_rgba.is_equal_approx(blue_hex), + "Creation with a 32-bit hexadecimal number should result in components approximately equal to the default constructor."); + CHECK_MESSAGE( + blue_rgba.is_equal_approx(blue_hex64), + "Creation with a 64-bit hexadecimal number should result in components approximately equal to the default constructor."); + + ERR_PRINT_OFF; + const Color html_invalid = Color::html("invalid"); + ERR_PRINT_ON; + + CHECK_MESSAGE( + html_invalid.is_equal_approx(Color()), + "Creation with invalid HTML notation should result in a Color with the default values."); + + const Color green_rgba = Color(0, 1, 0, 0.25); + const Color green_hsva = Color(0, 0, 0).from_hsv(120 / 360.0, 1, 1, 0.25); + + CHECK_MESSAGE( + green_rgba.is_equal_approx(green_hsva), + "Creation with HSV notation should result in components approximately equal to the default constructor."); +} + +TEST_CASE("[Color] Operators") { + const Color blue = Color(0.2, 0.2, 1); + const Color dark_red = Color(0.3, 0.1, 0.1); + + // Color components may be negative. Also, the alpha component may be greater than 1.0. + CHECK_MESSAGE( + (blue + dark_red).is_equal_approx(Color(0.5, 0.3, 1.1, 2)), + "Color addition should behave as expected."); + CHECK_MESSAGE( + (blue - dark_red).is_equal_approx(Color(-0.1, 0.1, 0.9, 0)), + "Color subtraction should behave as expected."); + CHECK_MESSAGE( + (blue * 2).is_equal_approx(Color(0.4, 0.4, 2, 2)), + "Color multiplication with a scalar should behave as expected."); + CHECK_MESSAGE( + (blue / 2).is_equal_approx(Color(0.1, 0.1, 0.5, 0.5)), + "Color division with a scalar should behave as expected."); + CHECK_MESSAGE( + (blue * dark_red).is_equal_approx(Color(0.06, 0.02, 0.1)), + "Color multiplication with another Color should behave as expected."); + CHECK_MESSAGE( + (blue / dark_red).is_equal_approx(Color(0.666667, 2, 10)), + "Color division with another Color should behave as expected."); + CHECK_MESSAGE( + (-blue).is_equal_approx(Color(0.8, 0.8, 0, 0)), + "Color negation should behave as expected (affecting the alpha channel, unlike `invert()`)."); +} + +TEST_CASE("[Color] Reading methods") { + const Color dark_blue = Color(0, 0, 0.5, 0.4); + + CHECK_MESSAGE( + Math::is_equal_approx(dark_blue.get_h(), 240.0f / 360.0f), + "The returned HSV hue should match the expected value."); + CHECK_MESSAGE( + Math::is_equal_approx(dark_blue.get_s(), 1.0f), + "The returned HSV saturation should match the expected value."); + CHECK_MESSAGE( + Math::is_equal_approx(dark_blue.get_v(), 0.5f), + "The returned HSV value should match the expected value."); +} + +TEST_CASE("[Color] Conversion methods") { + const Color cyan = Color(0, 1, 1); + const Color cyan_transparent = Color(0, 1, 1, 0); + + CHECK_MESSAGE( + cyan.to_html() == "00ffffff", + "The returned RGB HTML color code should match the expected value."); + CHECK_MESSAGE( + cyan_transparent.to_html() == "00ffff00", + "The returned RGBA HTML color code should match the expected value."); + CHECK_MESSAGE( + cyan.to_argb32() == 0xff00ffff, + "The returned 32-bit RGB number should match the expected value."); + CHECK_MESSAGE( + cyan.to_abgr32() == 0xffffff00, + "The returned 32-bit BGR number should match the expected value."); + CHECK_MESSAGE( + cyan.to_rgba32() == 0x00ffffff, + "The returned 32-bit BGR number should match the expected value."); + CHECK_MESSAGE( + cyan.to_argb64() == 0xffff'0000'ffff'ffff, + "The returned 64-bit RGB number should match the expected value."); + CHECK_MESSAGE( + cyan.to_abgr64() == 0xffff'ffff'ffff'0000, + "The returned 64-bit BGR number should match the expected value."); + CHECK_MESSAGE( + cyan.to_rgba64() == 0x0000'ffff'ffff'ffff, + "The returned 64-bit BGR number should match the expected value."); + CHECK_MESSAGE( + String(cyan) == "(0, 1, 1, 1)", + "The string representation should match the expected value."); +} + +TEST_CASE("[Color] Named colors") { + CHECK_MESSAGE( + Color::named("red").is_equal_approx(Color(1, 0, 0)), + "The named color \"red\" should match the expected value."); + + // Named colors have their names automatically normalized. + CHECK_MESSAGE( + Color::named("white_smoke").is_equal_approx(Color(0.96, 0.96, 0.96)), + "The named color \"white_smoke\" should match the expected value."); + CHECK_MESSAGE( + Color::named("Slate Blue").is_equal_approx(Color(0.42, 0.35, 0.80)), + "The named color \"Slate Blue\" should match the expected value."); + + ERR_PRINT_OFF; + CHECK_MESSAGE( + Color::named("doesn't exist").is_equal_approx(Color()), + "The invalid named color \"doesn't exist\" should result in a Color with the default values."); + ERR_PRINT_ON; +} + +TEST_CASE("[Color] Validation methods") { + CHECK_MESSAGE( + Color::html_is_valid("#4080ff"), + "Valid HTML color (with leading #) should be considered valid."); + CHECK_MESSAGE( + Color::html_is_valid("4080ff"), + "Valid HTML color (without leading #) should be considered valid."); + CHECK_MESSAGE( + !Color::html_is_valid("12345"), + "Invalid HTML color should be considered invalid."); + CHECK_MESSAGE( + !Color::html_is_valid("#fuf"), + "Invalid HTML color should be considered invalid."); +} + +TEST_CASE("[Color] Manipulation methods") { + const Color blue = Color(0, 0, 1, 0.4); + + CHECK_MESSAGE( + blue.inverted().is_equal_approx(Color(1, 1, 0, 0.4)), + "Inverted color should have its red, green and blue components inverted."); + + const Color purple = Color(0.5, 0.2, 0.5, 0.25); + + CHECK_MESSAGE( + purple.lightened(0.2).is_equal_approx(Color(0.6, 0.36, 0.6, 0.25)), + "Color should be lightened by the expected amount."); + CHECK_MESSAGE( + purple.darkened(0.2).is_equal_approx(Color(0.4, 0.16, 0.4, 0.25)), + "Color should be darkened by the expected amount."); + + const Color red = Color(1, 0, 0, 0.2); + const Color yellow = Color(1, 1, 0, 0.8); + + CHECK_MESSAGE( + red.lerp(yellow, 0.5).is_equal_approx(Color(1, 0.5, 0, 0.5)), + "Red interpolated with yellow should be orange (with interpolated alpha)."); +} +} // namespace TestColor + +#endif // TEST_COLOR_H diff --git a/tests/core/math/test_expression.h b/tests/core/math/test_expression.h new file mode 100644 index 0000000000..cb1d29389f --- /dev/null +++ b/tests/core/math/test_expression.h @@ -0,0 +1,444 @@ +/*************************************************************************/ +/* test_expression.h */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/*************************************************************************/ + +#ifndef TEST_EXPRESSION_H +#define TEST_EXPRESSION_H + +#include "core/math/expression.h" + +#include "tests/test_macros.h" + +namespace TestExpression { + +TEST_CASE("[Expression] Integer arithmetic") { + Expression expression; + + CHECK_MESSAGE( + expression.parse("-123456") == OK, + "Integer identity should parse successfully."); + CHECK_MESSAGE( + int(expression.execute()) == -123456, + "Integer identity should return the expected result."); + + CHECK_MESSAGE( + expression.parse("2 + 3") == OK, + "Integer addition should parse successfully."); + CHECK_MESSAGE( + int(expression.execute()) == 5, + "Integer addition should return the expected result."); + + CHECK_MESSAGE( + expression.parse("999999999999 + 999999999999") == OK, + "Large integer addition should parse successfully."); + CHECK_MESSAGE( + int64_t(expression.execute()) == 1'999'999'999'998, + "Large integer addition should return the expected result."); + + CHECK_MESSAGE( + expression.parse("25 / 10") == OK, + "Integer / integer division should parse successfully."); + CHECK_MESSAGE( + int(expression.execute()) == 2, + "Integer / integer division should return the expected result."); + + CHECK_MESSAGE( + expression.parse("2 * (6 + 14) / 2 - 5") == OK, + "Integer multiplication-addition-subtraction-division should parse successfully."); + CHECK_MESSAGE( + int(expression.execute()) == 15, + "Integer multiplication-addition-subtraction-division should return the expected result."); +} + +TEST_CASE("[Expression] Floating-point arithmetic") { + Expression expression; + + CHECK_MESSAGE( + expression.parse("-123.456") == OK, + "Float identity should parse successfully."); + CHECK_MESSAGE( + Math::is_equal_approx(double(expression.execute()), -123.456), + "Float identity should return the expected result."); + + CHECK_MESSAGE( + expression.parse("2.0 + 3.0") == OK, + "Float addition should parse successfully."); + CHECK_MESSAGE( + Math::is_equal_approx(double(expression.execute()), 5), + "Float addition should return the expected result."); + + CHECK_MESSAGE( + expression.parse("3.0 / 10") == OK, + "Float / integer division should parse successfully."); + CHECK_MESSAGE( + Math::is_equal_approx(double(expression.execute()), 0.3), + "Float / integer division should return the expected result."); + + CHECK_MESSAGE( + expression.parse("3 / 10.0") == OK, + "Basic integer / float division should parse successfully."); + CHECK_MESSAGE( + Math::is_equal_approx(double(expression.execute()), 0.3), + "Basic integer / float division should return the expected result."); + + CHECK_MESSAGE( + expression.parse("3.0 / 10.0") == OK, + "Float / float division should parse successfully."); + CHECK_MESSAGE( + Math::is_equal_approx(double(expression.execute()), 0.3), + "Float / float division should return the expected result."); + + CHECK_MESSAGE( + expression.parse("2.5 * (6.0 + 14.25) / 2.0 - 5.12345") == OK, + "Float multiplication-addition-subtraction-division should parse successfully."); + CHECK_MESSAGE( + Math::is_equal_approx(double(expression.execute()), 20.18905), + "Float multiplication-addition-subtraction-division should return the expected result."); +} + +TEST_CASE("[Expression] Scientific notation") { + Expression expression; + + CHECK_MESSAGE( + expression.parse("2.e5") == OK, + "The expression should parse successfully."); + CHECK_MESSAGE( + Math::is_equal_approx(double(expression.execute()), 200'000), + "The expression should return the expected result."); + + // The middle "e" is ignored here. + CHECK_MESSAGE( + expression.parse("2e5") == OK, + "The expression should parse successfully."); + CHECK_MESSAGE( + Math::is_equal_approx(double(expression.execute()), 25), + "The expression should return the expected result."); + + CHECK_MESSAGE( + expression.parse("2e.5") == OK, + "The expression should parse successfully."); + CHECK_MESSAGE( + Math::is_equal_approx(double(expression.execute()), 2), + "The expression should return the expected result."); +} + +TEST_CASE("[Expression] Underscored numeric literals") { + Expression expression; + + CHECK_MESSAGE( + expression.parse("1_000_000") == OK, + "The expression should parse successfully."); + CHECK_MESSAGE( + expression.parse("1_000.000") == OK, + "The expression should parse successfully."); + CHECK_MESSAGE( + expression.parse("0xff_99_00") == OK, + "The expression should parse successfully."); +} + +TEST_CASE("[Expression] Built-in functions") { + Expression expression; + + CHECK_MESSAGE( + expression.parse("sqrt(pow(3, 2) + pow(4, 2))") == OK, + "The expression should parse successfully."); + CHECK_MESSAGE( + int(expression.execute()) == 5, + "`sqrt(pow(3, 2) + pow(4, 2))` should return the expected result."); + + CHECK_MESSAGE( + expression.parse("snapped(sin(0.5), 0.01)") == OK, + "The expression should parse successfully."); + CHECK_MESSAGE( + Math::is_equal_approx(double(expression.execute()), 0.48), + "`snapped(sin(0.5), 0.01)` should return the expected result."); + + CHECK_MESSAGE( + expression.parse("pow(2.0, -2500)") == OK, + "The expression should parse successfully."); + CHECK_MESSAGE( + Math::is_zero_approx(double(expression.execute())), + "`pow(2.0, -2500)` should return the expected result (asymptotically zero)."); +} + +TEST_CASE("[Expression] Boolean expressions") { + Expression expression; + + CHECK_MESSAGE( + expression.parse("24 >= 12") == OK, + "The boolean expression should parse successfully."); + CHECK_MESSAGE( + bool(expression.execute()), + "The boolean expression should evaluate to `true`."); + + CHECK_MESSAGE( + expression.parse("1.0 < 1.25 && 1.25 < 2.0") == OK, + "The boolean expression should parse successfully."); + CHECK_MESSAGE( + bool(expression.execute()), + "The boolean expression should evaluate to `true`."); + + CHECK_MESSAGE( + expression.parse("!2") == OK, + "The boolean expression should parse successfully."); + CHECK_MESSAGE( + !bool(expression.execute()), + "The boolean expression should evaluate to `false`."); + + CHECK_MESSAGE( + expression.parse("!!2") == OK, + "The boolean expression should parse successfully."); + CHECK_MESSAGE( + bool(expression.execute()), + "The boolean expression should evaluate to `true`."); + + CHECK_MESSAGE( + expression.parse("!0") == OK, + "The boolean expression should parse successfully."); + CHECK_MESSAGE( + bool(expression.execute()), + "The boolean expression should evaluate to `true`."); + + CHECK_MESSAGE( + expression.parse("!!0") == OK, + "The boolean expression should parse successfully."); + CHECK_MESSAGE( + !bool(expression.execute()), + "The boolean expression should evaluate to `false`."); + + CHECK_MESSAGE( + expression.parse("2 && 5") == OK, + "The boolean expression should parse successfully."); + CHECK_MESSAGE( + bool(expression.execute()), + "The boolean expression should evaluate to `true`."); + + CHECK_MESSAGE( + expression.parse("0 || 0") == OK, + "The boolean expression should parse successfully."); + CHECK_MESSAGE( + !bool(expression.execute()), + "The boolean expression should evaluate to `false`."); + + CHECK_MESSAGE( + expression.parse("(2 <= 4) && (2 > 5)") == OK, + "The boolean expression should parse successfully."); + CHECK_MESSAGE( + !bool(expression.execute()), + "The boolean expression should evaluate to `false`."); +} + +TEST_CASE("[Expression] Expressions with variables") { + Expression expression; + + PackedStringArray parameter_names; + parameter_names.push_back("foo"); + parameter_names.push_back("bar"); + CHECK_MESSAGE( + expression.parse("foo + bar + 50", parameter_names) == OK, + "The expression should parse successfully."); + Array values; + values.push_back(60); + values.push_back(20); + CHECK_MESSAGE( + int(expression.execute(values)) == 130, + "The expression should return the expected value."); + + PackedStringArray parameter_names_invalid; + parameter_names_invalid.push_back("foo"); + parameter_names_invalid.push_back("baz"); // Invalid parameter name. + CHECK_MESSAGE( + expression.parse("foo + bar + 50", parameter_names_invalid) == OK, + "The expression should parse successfully."); + Array values_invalid; + values_invalid.push_back(60); + values_invalid.push_back(20); + // Invalid parameters will parse successfully but print an error message when executing. + ERR_PRINT_OFF; + CHECK_MESSAGE( + int(expression.execute(values_invalid)) == 0, + "The expression should return the expected value."); + ERR_PRINT_ON; + + // Mismatched argument count (more values than parameters). + PackedStringArray parameter_names_mismatch; + parameter_names_mismatch.push_back("foo"); + parameter_names_mismatch.push_back("bar"); + CHECK_MESSAGE( + expression.parse("foo + bar + 50", parameter_names_mismatch) == OK, + "The expression should parse successfully."); + Array values_mismatch; + values_mismatch.push_back(60); + values_mismatch.push_back(20); + values_mismatch.push_back(110); + CHECK_MESSAGE( + int(expression.execute(values_mismatch)) == 130, + "The expression should return the expected value."); + + // Mismatched argument count (more parameters than values). + PackedStringArray parameter_names_mismatch2; + parameter_names_mismatch2.push_back("foo"); + parameter_names_mismatch2.push_back("bar"); + parameter_names_mismatch2.push_back("baz"); + CHECK_MESSAGE( + expression.parse("foo + bar + baz + 50", parameter_names_mismatch2) == OK, + "The expression should parse successfully."); + Array values_mismatch2; + values_mismatch2.push_back(60); + values_mismatch2.push_back(20); + // Having more parameters than values will parse successfully but print an + // error message when executing. + ERR_PRINT_OFF; + CHECK_MESSAGE( + int(expression.execute(values_mismatch2)) == 0, + "The expression should return the expected value."); + ERR_PRINT_ON; +} + +TEST_CASE("[Expression] Invalid expressions") { + Expression expression; + + CHECK_MESSAGE( + expression.parse("\\") == ERR_INVALID_PARAMETER, + "The expression shouldn't parse successfully."); + + CHECK_MESSAGE( + expression.parse("0++") == ERR_INVALID_PARAMETER, + "The expression shouldn't parse successfully."); + + CHECK_MESSAGE( + expression.parse("()") == ERR_INVALID_PARAMETER, + "The expression shouldn't parse successfully."); + + CHECK_MESSAGE( + expression.parse("()()") == ERR_INVALID_PARAMETER, + "The expression shouldn't parse successfully."); + + CHECK_MESSAGE( + expression.parse("() - ()") == ERR_INVALID_PARAMETER, + "The expression shouldn't parse successfully."); + + CHECK_MESSAGE( + expression.parse("() * 12345") == ERR_INVALID_PARAMETER, + "The expression shouldn't parse successfully."); + + CHECK_MESSAGE( + expression.parse("() * 12345") == ERR_INVALID_PARAMETER, + "The expression shouldn't parse successfully."); + + CHECK_MESSAGE( + expression.parse("123'456") == ERR_INVALID_PARAMETER, + "The expression shouldn't parse successfully."); + + CHECK_MESSAGE( + expression.parse("123\"456") == ERR_INVALID_PARAMETER, + "The expression shouldn't parse successfully."); +} + +TEST_CASE("[Expression] Unusual expressions") { + Expression expression; + + // Redundant parentheses don't cause a parse error as long as they're matched. + CHECK_MESSAGE( + expression.parse("(((((((((((((((666)))))))))))))))") == OK, + "The expression should parse successfully."); + + // Using invalid identifiers doesn't cause a parse error. + ERR_PRINT_OFF; + CHECK_MESSAGE( + expression.parse("hello + hello") == OK, + "The expression should parse successfully."); + CHECK_MESSAGE( + int(expression.execute()) == 0, + "The expression should return the expected result."); + ERR_PRINT_ON; + + ERR_PRINT_OFF; + CHECK_MESSAGE( + expression.parse("$1.00 + ???5") == OK, + "The expression should parse successfully."); + CHECK_MESSAGE( + int(expression.execute()) == 0, + "The expression should return the expected result."); + ERR_PRINT_ON; + + // Commas can't be used as a decimal parameter. + CHECK_MESSAGE( + expression.parse("123,456") == OK, + "The expression should parse successfully."); + CHECK_MESSAGE( + int(expression.execute()) == 123, + "The expression should return the expected result."); + + // Spaces can't be used as a separator for large numbers. + CHECK_MESSAGE( + expression.parse("123 456") == OK, + "The expression should parse successfully."); + CHECK_MESSAGE( + int(expression.execute()) == 123, + "The expression should return the expected result."); + + // Division by zero is accepted, even though it prints an error message normally. + CHECK_MESSAGE( + expression.parse("-25.4 / 0") == OK, + "The expression should parse successfully."); + ERR_PRINT_OFF; + CHECK_MESSAGE( + Math::is_inf(double(expression.execute())), + "`-25.4 / 0` should return inf."); + ERR_PRINT_ON; + + CHECK_MESSAGE( + expression.parse("0 / 0") == OK, + "The expression should parse successfully."); + ERR_PRINT_OFF; + CHECK_MESSAGE( + int(expression.execute()) == 0, + "`0 / 0` should return 0."); + ERR_PRINT_ON; + + // The tests below currently crash the engine. + // + //CHECK_MESSAGE( + // expression.parse("(-9223372036854775807 - 1) % -1") == OK, + // "The expression should parse successfully."); + //CHECK_MESSAGE( + // int64_t(expression.execute()) == 0, + // "`(-9223372036854775807 - 1) % -1` should return the expected result."); + // + //CHECK_MESSAGE( + // expression.parse("(-9223372036854775807 - 1) / -1") == OK, + // "The expression should parse successfully."); + //CHECK_MESSAGE( + // int64_t(expression.execute()) == 0, + // "`(-9223372036854775807 - 1) / -1` should return the expected result."); +} +} // namespace TestExpression + +#endif // TEST_EXPRESSION_H diff --git a/tests/core/math/test_geometry_2d.h b/tests/core/math/test_geometry_2d.h new file mode 100644 index 0000000000..8f6669b572 --- /dev/null +++ b/tests/core/math/test_geometry_2d.h @@ -0,0 +1,567 @@ +/*************************************************************************/ +/* test_geometry_2d.h */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/*************************************************************************/ + +#ifndef TEST_GEOMETRY_2D_H +#define TEST_GEOMETRY_2D_H + +#include "core/math/geometry_2d.h" + +#include "thirdparty/doctest/doctest.h" + +namespace TestGeometry2D { + +TEST_CASE("[Geometry2D] Point in circle") { + CHECK(Geometry2D::is_point_in_circle(Vector2(0, 0), Vector2(0, 0), 1.0)); + + CHECK(Geometry2D::is_point_in_circle(Vector2(0, 0), Vector2(11.99, 0), 12)); + CHECK(Geometry2D::is_point_in_circle(Vector2(-11.99, 0), Vector2(0, 0), 12)); + + CHECK_FALSE(Geometry2D::is_point_in_circle(Vector2(0, 0), Vector2(12.01, 0), 12)); + CHECK_FALSE(Geometry2D::is_point_in_circle(Vector2(-12.01, 0), Vector2(0, 0), 12)); + + CHECK(Geometry2D::is_point_in_circle(Vector2(7, -42), Vector2(4, -40), 3.7)); + CHECK_FALSE(Geometry2D::is_point_in_circle(Vector2(7, -42), Vector2(4, -40), 3.5)); + + // This tests points on the edge of the circle. They are treated as being inside the circle. + CHECK(Geometry2D::is_point_in_circle(Vector2(1.0, 0.0), Vector2(0, 0), 1.0)); + CHECK(Geometry2D::is_point_in_circle(Vector2(0.0, -1.0), Vector2(0, 0), 1.0)); +} + +TEST_CASE("[Geometry2D] Point in triangle") { + CHECK(Geometry2D::is_point_in_triangle(Vector2(0, 0), Vector2(-1, 1), Vector2(0, -1), Vector2(1, 1))); + CHECK_FALSE(Geometry2D::is_point_in_triangle(Vector2(-1.01, 1.0), Vector2(-1, 1), Vector2(0, -1), Vector2(1, 1))); + + CHECK(Geometry2D::is_point_in_triangle(Vector2(3, 2.5), Vector2(1, 4), Vector2(3, 2), Vector2(5, 4))); + CHECK(Geometry2D::is_point_in_triangle(Vector2(-3, -2.5), Vector2(-1, -4), Vector2(-3, -2), Vector2(-5, -4))); + CHECK_FALSE(Geometry2D::is_point_in_triangle(Vector2(0, 0), Vector2(1, 4), Vector2(3, 2), Vector2(5, 4))); + + // This tests points on the edge of the triangle. They are treated as being outside the triangle. + // In `is_point_in_circle` and `is_point_in_polygon` they are treated as being inside, so in order the make + // the behaviour consistent this may change in the future (see issue #44717 and PR #44274). + CHECK_FALSE(Geometry2D::is_point_in_triangle(Vector2(1, 1), Vector2(-1, 1), Vector2(0, -1), Vector2(1, 1))); + CHECK_FALSE(Geometry2D::is_point_in_triangle(Vector2(0, 1), Vector2(-1, 1), Vector2(0, -1), Vector2(1, 1))); +} + +TEST_CASE("[Geometry2D] Point in polygon") { + Vector<Vector2> p; + CHECK_FALSE(Geometry2D::is_point_in_polygon(Vector2(0, 0), p)); + + p.push_back(Vector2(-88, 120)); + p.push_back(Vector2(-74, -38)); + p.push_back(Vector2(135, -145)); + p.push_back(Vector2(425, 70)); + p.push_back(Vector2(68, 112)); + p.push_back(Vector2(-120, 370)); + p.push_back(Vector2(-323, -145)); + CHECK_FALSE(Geometry2D::is_point_in_polygon(Vector2(-350, 0), p)); + CHECK_FALSE(Geometry2D::is_point_in_polygon(Vector2(-110, 60), p)); + CHECK_FALSE(Geometry2D::is_point_in_polygon(Vector2(412, 96), p)); + CHECK_FALSE(Geometry2D::is_point_in_polygon(Vector2(83, 130), p)); + CHECK_FALSE(Geometry2D::is_point_in_polygon(Vector2(-320, -153), p)); + + CHECK(Geometry2D::is_point_in_polygon(Vector2(0, 0), p)); + CHECK(Geometry2D::is_point_in_polygon(Vector2(-230, 0), p)); + CHECK(Geometry2D::is_point_in_polygon(Vector2(130, -110), p)); + CHECK(Geometry2D::is_point_in_polygon(Vector2(370, 55), p)); + CHECK(Geometry2D::is_point_in_polygon(Vector2(-160, 190), p)); + + // This tests points on the edge of the polygon. They are treated as being inside the polygon. + int c = p.size(); + for (int i = 0; i < c; i++) { + const Vector2 &p1 = p[i]; + CHECK(Geometry2D::is_point_in_polygon(p1, p)); + + const Vector2 &p2 = p[(i + 1) % c]; + Vector2 midpoint((p1 + p2) * 0.5); + CHECK(Geometry2D::is_point_in_polygon(midpoint, p)); + } +} + +TEST_CASE("[Geometry2D] Polygon clockwise") { + Vector<Vector2> p; + CHECK_FALSE(Geometry2D::is_polygon_clockwise(p)); + + p.push_back(Vector2(5, -5)); + p.push_back(Vector2(-1, -5)); + p.push_back(Vector2(-5, -1)); + p.push_back(Vector2(-1, 3)); + p.push_back(Vector2(1, 5)); + CHECK(Geometry2D::is_polygon_clockwise(p)); + + p.reverse(); + CHECK_FALSE(Geometry2D::is_polygon_clockwise(p)); +} + +TEST_CASE("[Geometry2D] Line intersection") { + Vector2 r; + CHECK(Geometry2D::line_intersects_line(Vector2(2, 0), Vector2(0, 1), Vector2(0, 2), Vector2(1, 0), r)); + CHECK(r.is_equal_approx(Vector2(2, 2))); + + CHECK(Geometry2D::line_intersects_line(Vector2(-1, 1), Vector2(1, -1), Vector2(4, 1), Vector2(-1, -1), r)); + CHECK(r.is_equal_approx(Vector2(1.5, -1.5))); + + CHECK(Geometry2D::line_intersects_line(Vector2(-1, 0), Vector2(-1, -1), Vector2(1, 0), Vector2(1, -1), r)); + CHECK(r.is_equal_approx(Vector2(0, 1))); + + CHECK_FALSE_MESSAGE( + Geometry2D::line_intersects_line(Vector2(-1, 1), Vector2(1, -1), Vector2(0, 1), Vector2(1, -1), r), + "Parallel lines should not intersect."); +} + +TEST_CASE("[Geometry2D] Segment intersection.") { + Vector2 r; + + CHECK(Geometry2D::segment_intersects_segment(Vector2(-1, 1), Vector2(1, -1), Vector2(1, 1), Vector2(-1, -1), &r)); + CHECK(r.is_equal_approx(Vector2(0, 0))); + + CHECK_FALSE(Geometry2D::segment_intersects_segment(Vector2(-1, 1), Vector2(1, -1), Vector2(1, 1), Vector2(0.1, 0.1), &r)); + CHECK_FALSE(Geometry2D::segment_intersects_segment(Vector2(-1, 1), Vector2(1, -1), Vector2(0.1, 0.1), Vector2(1, 1), &r)); + + CHECK_FALSE_MESSAGE( + Geometry2D::segment_intersects_segment(Vector2(-1, 1), Vector2(1, -1), Vector2(0, 1), Vector2(2, -1), &r), + "Parallel segments should not intersect."); + + CHECK_MESSAGE( + Geometry2D::segment_intersects_segment(Vector2(0, 0), Vector2(0, 1), Vector2(0, 0), Vector2(1, 0), &r), + "Touching segments should intersect."); + CHECK(r.is_equal_approx(Vector2(0, 0))); + + CHECK_MESSAGE( + Geometry2D::segment_intersects_segment(Vector2(0, 1), Vector2(0, 0), Vector2(0, 0), Vector2(1, 0), &r), + "Touching segments should intersect."); + CHECK(r.is_equal_approx(Vector2(0, 0))); +} + +TEST_CASE("[Geometry2D] Closest point to segment") { + Vector2 s[] = { Vector2(-4, -4), Vector2(4, 4) }; + CHECK(Geometry2D::get_closest_point_to_segment(Vector2(4.1, 4.1), s).is_equal_approx(Vector2(4, 4))); + CHECK(Geometry2D::get_closest_point_to_segment(Vector2(-4.1, -4.1), s).is_equal_approx(Vector2(-4, -4))); + CHECK(Geometry2D::get_closest_point_to_segment(Vector2(-1, 1), s).is_equal_approx(Vector2(0, 0))); +} + +TEST_CASE("[Geometry2D] Closest point to uncapped segment") { + Vector2 s[] = { Vector2(-4, -4), Vector2(4, 4) }; + CHECK(Geometry2D::get_closest_point_to_segment_uncapped(Vector2(-1, 1), s).is_equal_approx(Vector2(0, 0))); + CHECK(Geometry2D::get_closest_point_to_segment_uncapped(Vector2(-4, -6), s).is_equal_approx(Vector2(-5, -5))); + CHECK(Geometry2D::get_closest_point_to_segment_uncapped(Vector2(4, 6), s).is_equal_approx(Vector2(5, 5))); +} + +TEST_CASE("[Geometry2D] Closest points between segments") { + Vector2 c1, c2; + Geometry2D::get_closest_points_between_segments(Vector2(2, 2), Vector2(3, 3), Vector2(4, 4), Vector2(4, 5), c1, c2); + CHECK(c1.is_equal_approx(Vector2(3, 3))); + CHECK(c2.is_equal_approx(Vector2(4, 4))); + + Geometry2D::get_closest_points_between_segments(Vector2(0, 1), Vector2(-2, -1), Vector2(0, 0), Vector2(2, -2), c1, c2); + CHECK(c1.is_equal_approx(Vector2(-0.5, 0.5))); + CHECK(c2.is_equal_approx(Vector2(0, 0))); + + Geometry2D::get_closest_points_between_segments(Vector2(-1, 1), Vector2(1, -1), Vector2(1, 1), Vector2(-1, -1), c1, c2); + CHECK(c1.is_equal_approx(Vector2(0, 0))); + CHECK(c2.is_equal_approx(Vector2(0, 0))); +} + +TEST_CASE("[Geometry2D] Make atlas") { + Vector<Point2i> result; + Size2i size; + + Vector<Size2i> r; + r.push_back(Size2i(2, 2)); + Geometry2D::make_atlas(r, result, size); + CHECK(size == Size2i(2, 2)); + CHECK(result.size() == r.size()); + + r.clear(); + result.clear(); + r.push_back(Size2i(1, 2)); + r.push_back(Size2i(3, 4)); + r.push_back(Size2i(5, 6)); + r.push_back(Size2i(7, 8)); + Geometry2D::make_atlas(r, result, size); + CHECK(result.size() == r.size()); +} + +TEST_CASE("[Geometry2D] Polygon intersection") { + Vector<Point2> a; + Vector<Point2> b; + Vector<Vector<Point2>> r; + + a.push_back(Point2(30, 60)); + a.push_back(Point2(70, 5)); + a.push_back(Point2(200, 40)); + a.push_back(Point2(80, 200)); + + SUBCASE("[Geometry2D] Both polygons are empty") { + r = Geometry2D::intersect_polygons(Vector<Point2>(), Vector<Point2>()); + CHECK_MESSAGE(r.is_empty(), "Both polygons are empty. The intersection should also be empty."); + } + + SUBCASE("[Geometry2D] One polygon is empty") { + r = Geometry2D::intersect_polygons(a, b); + REQUIRE_MESSAGE(r.is_empty(), "One polygon is empty. The intersection should also be empty."); + } + + SUBCASE("[Geometry2D] Basic intersection") { + b.push_back(Point2(200, 300)); + b.push_back(Point2(90, 200)); + b.push_back(Point2(50, 100)); + b.push_back(Point2(200, 90)); + r = Geometry2D::intersect_polygons(a, b); + REQUIRE_MESSAGE(r.size() == 1, "The polygons should intersect each other with 1 resulting intersection polygon."); + REQUIRE_MESSAGE(r[0].size() == 3, "The resulting intersection polygon should have 3 vertices."); + CHECK(r[0][0].is_equal_approx(Point2(86.52174, 191.30436))); + CHECK(r[0][1].is_equal_approx(Point2(50, 100))); + CHECK(r[0][2].is_equal_approx(Point2(160.52632, 92.63157))); + } + + SUBCASE("[Geometry2D] Intersection with one polygon being completely inside the other polygon") { + b.push_back(Point2(80, 100)); + b.push_back(Point2(50, 50)); + b.push_back(Point2(150, 50)); + r = Geometry2D::intersect_polygons(a, b); + REQUIRE_MESSAGE(r.size() == 1, "The polygons should intersect each other with 1 resulting intersection polygon."); + REQUIRE_MESSAGE(r[0].size() == 3, "The resulting intersection polygon should have 3 vertices."); + CHECK(r[0][0].is_equal_approx(b[0])); + CHECK(r[0][1].is_equal_approx(b[1])); + CHECK(r[0][2].is_equal_approx(b[2])); + } + + SUBCASE("[Geometry2D] No intersection with 2 non-empty polygons") { + b.push_back(Point2(150, 150)); + b.push_back(Point2(250, 100)); + b.push_back(Point2(300, 200)); + r = Geometry2D::intersect_polygons(a, b); + REQUIRE_MESSAGE(r.is_empty(), "The polygons should not intersect each other."); + } + + SUBCASE("[Geometry2D] Intersection with 2 resulting polygons") { + a.clear(); + a.push_back(Point2(70, 5)); + a.push_back(Point2(140, 7)); + a.push_back(Point2(100, 52)); + a.push_back(Point2(170, 50)); + a.push_back(Point2(60, 125)); + b.push_back(Point2(70, 105)); + b.push_back(Point2(115, 55)); + b.push_back(Point2(90, 15)); + b.push_back(Point2(160, 50)); + r = Geometry2D::intersect_polygons(a, b); + REQUIRE_MESSAGE(r.size() == 2, "The polygons should intersect each other with 2 resulting intersection polygons."); + REQUIRE_MESSAGE(r[0].size() == 4, "The resulting intersection polygon should have 4 vertices."); + CHECK(r[0][0].is_equal_approx(Point2(70, 105))); + CHECK(r[0][1].is_equal_approx(Point2(115, 55))); + CHECK(r[0][2].is_equal_approx(Point2(112.894737, 51.63158))); + CHECK(r[0][3].is_equal_approx(Point2(159.509537, 50.299728))); + + REQUIRE_MESSAGE(r[1].size() == 3, "The intersection polygon should have 3 vertices."); + CHECK(r[1][0].is_equal_approx(Point2(119.692307, 29.846149))); + CHECK(r[1][1].is_equal_approx(Point2(107.706421, 43.33028))); + CHECK(r[1][2].is_equal_approx(Point2(90, 15))); + } +} + +TEST_CASE("[Geometry2D] Merge polygons") { + Vector<Point2> a; + Vector<Point2> b; + Vector<Vector<Point2>> r; + + a.push_back(Point2(225, 180)); + a.push_back(Point2(160, 230)); + a.push_back(Point2(20, 212)); + a.push_back(Point2(50, 115)); + + SUBCASE("[Geometry2D] Both polygons are empty") { + r = Geometry2D::merge_polygons(Vector<Point2>(), Vector<Point2>()); + REQUIRE_MESSAGE(r.is_empty(), "Both polygons are empty. The union should also be empty."); + } + + SUBCASE("[Geometry2D] One polygon is empty") { + r = Geometry2D::merge_polygons(a, b); + REQUIRE_MESSAGE(r.size() == 1, "One polygon is non-empty. There should be 1 resulting merged polygon."); + REQUIRE_MESSAGE(r[0].size() == 4, "The resulting merged polygon should have 4 vertices."); + CHECK(r[0][0].is_equal_approx(a[0])); + CHECK(r[0][1].is_equal_approx(a[1])); + CHECK(r[0][2].is_equal_approx(a[2])); + CHECK(r[0][3].is_equal_approx(a[3])); + } + + SUBCASE("[Geometry2D] Basic merge with 2 polygons") { + b.push_back(Point2(180, 190)); + b.push_back(Point2(60, 140)); + b.push_back(Point2(160, 80)); + r = Geometry2D::merge_polygons(a, b); + REQUIRE_MESSAGE(r.size() == 1, "The merged polygons should result in 1 polygon."); + REQUIRE_MESSAGE(r[0].size() == 7, "The resulting merged polygon should have 7 vertices."); + CHECK(r[0][0].is_equal_approx(Point2(174.791077, 161.350967))); + CHECK(r[0][1].is_equal_approx(Point2(225, 180))); + CHECK(r[0][2].is_equal_approx(Point2(160, 230))); + CHECK(r[0][3].is_equal_approx(Point2(20, 212))); + CHECK(r[0][4].is_equal_approx(Point2(50, 115))); + CHECK(r[0][5].is_equal_approx(Point2(81.911758, 126.852943))); + CHECK(r[0][6].is_equal_approx(Point2(160, 80))); + } + + SUBCASE("[Geometry2D] Merge with 2 resulting merged polygons (outline and hole)") { + b.push_back(Point2(180, 190)); + b.push_back(Point2(140, 125)); + b.push_back(Point2(60, 140)); + b.push_back(Point2(160, 80)); + r = Geometry2D::merge_polygons(a, b); + REQUIRE_MESSAGE(r.size() == 2, "The merged polygons should result in 2 polygons."); + + REQUIRE_MESSAGE(!Geometry2D::is_polygon_clockwise(r[0]), "The merged polygon (outline) should be counter-clockwise."); + REQUIRE_MESSAGE(r[0].size() == 7, "The resulting merged polygon (outline) should have 7 vertices."); + CHECK(r[0][0].is_equal_approx(Point2(174.791077, 161.350967))); + CHECK(r[0][1].is_equal_approx(Point2(225, 180))); + CHECK(r[0][2].is_equal_approx(Point2(160, 230))); + CHECK(r[0][3].is_equal_approx(Point2(20, 212))); + CHECK(r[0][4].is_equal_approx(Point2(50, 115))); + CHECK(r[0][5].is_equal_approx(Point2(81.911758, 126.852943))); + CHECK(r[0][6].is_equal_approx(Point2(160, 80))); + + REQUIRE_MESSAGE(Geometry2D::is_polygon_clockwise(r[1]), "The resulting merged polygon (hole) should be clockwise."); + REQUIRE_MESSAGE(r[1].size() == 3, "The resulting merged polygon (hole) should have 3 vertices."); + CHECK(r[1][0].is_equal_approx(Point2(98.083069, 132.859421))); + CHECK(r[1][1].is_equal_approx(Point2(158.689453, 155.370377))); + CHECK(r[1][2].is_equal_approx(Point2(140, 125))); + } +} + +TEST_CASE("[Geometry2D] Clip polygons") { + Vector<Point2> a; + Vector<Point2> b; + Vector<Vector<Point2>> r; + + a.push_back(Point2(225, 180)); + a.push_back(Point2(160, 230)); + a.push_back(Point2(20, 212)); + a.push_back(Point2(50, 115)); + + SUBCASE("[Geometry2D] Both polygons are empty") { + r = Geometry2D::clip_polygons(Vector<Point2>(), Vector<Point2>()); + CHECK_MESSAGE(r.is_empty(), "Both polygons are empty. The clip should also be empty."); + } + + SUBCASE("[Geometry2D] Basic clip with one result polygon") { + b.push_back(Point2(250, 170)); + b.push_back(Point2(175, 270)); + b.push_back(Point2(120, 260)); + b.push_back(Point2(25, 80)); + r = Geometry2D::clip_polygons(a, b); + REQUIRE_MESSAGE(r.size() == 1, "The clipped polygons should result in 1 polygon."); + REQUIRE_MESSAGE(r[0].size() == 3, "The resulting clipped polygon should have 3 vertices."); + CHECK(r[0][0].is_equal_approx(Point2(100.102173, 222.298843))); + CHECK(r[0][1].is_equal_approx(Point2(20, 212))); + CHECK(r[0][2].is_equal_approx(Point2(47.588089, 122.798492))); + } + + SUBCASE("[Geometry2D] Polygon b completely overlaps polygon a") { + b.push_back(Point2(250, 170)); + b.push_back(Point2(175, 270)); + b.push_back(Point2(10, 210)); + b.push_back(Point2(55, 80)); + r = Geometry2D::clip_polygons(a, b); + CHECK_MESSAGE(r.is_empty(), "Polygon 'b' completely overlaps polygon 'a'. This should result in no clipped polygons."); + } + + SUBCASE("[Geometry2D] Polygon a completely overlaps polygon b") { + b.push_back(Point2(150, 200)); + b.push_back(Point2(65, 190)); + b.push_back(Point2(80, 140)); + r = Geometry2D::clip_polygons(a, b); + REQUIRE_MESSAGE(r.size() == 2, "Polygon 'a' completely overlaps polygon 'b'. This should result in 2 clipped polygons."); + REQUIRE_MESSAGE(r[0].size() == 4, "The resulting clipped polygon should have 4 vertices."); + REQUIRE_MESSAGE(!Geometry2D::is_polygon_clockwise(r[0]), "The resulting clipped polygon (outline) should be counter-clockwise."); + CHECK(r[0][0].is_equal_approx(a[0])); + CHECK(r[0][1].is_equal_approx(a[1])); + CHECK(r[0][2].is_equal_approx(a[2])); + CHECK(r[0][3].is_equal_approx(a[3])); + REQUIRE_MESSAGE(r[1].size() == 3, "The resulting clipped polygon should have 3 vertices."); + REQUIRE_MESSAGE(Geometry2D::is_polygon_clockwise(r[1]), "The resulting clipped polygon (hole) should be clockwise."); + CHECK(r[1][0].is_equal_approx(b[1])); + CHECK(r[1][1].is_equal_approx(b[0])); + CHECK(r[1][2].is_equal_approx(b[2])); + } +} + +TEST_CASE("[Geometry2D] Exclude polygons") { + Vector<Point2> a; + Vector<Point2> b; + Vector<Vector<Point2>> r; + + a.push_back(Point2(225, 180)); + a.push_back(Point2(160, 230)); + a.push_back(Point2(20, 212)); + a.push_back(Point2(50, 115)); + + SUBCASE("[Geometry2D] Both polygons are empty") { + r = Geometry2D::exclude_polygons(Vector<Point2>(), Vector<Point2>()); + CHECK_MESSAGE(r.is_empty(), "Both polygons are empty. The excluded polygon should also be empty."); + } + + SUBCASE("[Geometry2D] One polygon is empty") { + r = Geometry2D::exclude_polygons(a, b); + REQUIRE_MESSAGE(r.size() == 1, "One polygon is non-empty. There should be 1 resulting excluded polygon."); + REQUIRE_MESSAGE(r[0].size() == 4, "The resulting excluded polygon should have 4 vertices."); + CHECK(r[0][0].is_equal_approx(a[0])); + CHECK(r[0][1].is_equal_approx(a[1])); + CHECK(r[0][2].is_equal_approx(a[2])); + CHECK(r[0][3].is_equal_approx(a[3])); + } + + SUBCASE("[Geometry2D] Exclude with 2 resulting polygons (outline and hole)") { + b.push_back(Point2(140, 160)); + b.push_back(Point2(150, 220)); + b.push_back(Point2(40, 200)); + b.push_back(Point2(60, 140)); + r = Geometry2D::exclude_polygons(a, b); + REQUIRE_MESSAGE(r.size() == 2, "There should be 2 resulting excluded polygons (outline and hole)."); + REQUIRE_MESSAGE(r[0].size() == 4, "The resulting excluded polygon should have 4 vertices."); + REQUIRE_MESSAGE(!Geometry2D::is_polygon_clockwise(r[0]), "The resulting excluded polygon (outline) should be counter-clockwise."); + CHECK(r[0][0].is_equal_approx(a[0])); + CHECK(r[0][1].is_equal_approx(a[1])); + CHECK(r[0][2].is_equal_approx(a[2])); + CHECK(r[0][3].is_equal_approx(a[3])); + REQUIRE_MESSAGE(r[1].size() == 4, "The resulting excluded polygon should have 4 vertices."); + REQUIRE_MESSAGE(Geometry2D::is_polygon_clockwise(r[1]), "The resulting excluded polygon (hole) should be clockwise."); + CHECK(r[1][0].is_equal_approx(Point2(40, 200))); + CHECK(r[1][1].is_equal_approx(Point2(150, 220))); + CHECK(r[1][2].is_equal_approx(Point2(140, 160))); + CHECK(r[1][3].is_equal_approx(Point2(60, 140))); + } +} + +TEST_CASE("[Geometry2D] Intersect polyline with polygon") { + Vector<Vector2> l; + Vector<Vector2> p; + Vector<Vector<Point2>> r; + + l.push_back(Vector2(100, 90)); + l.push_back(Vector2(120, 250)); + + p.push_back(Vector2(225, 180)); + p.push_back(Vector2(160, 230)); + p.push_back(Vector2(20, 212)); + p.push_back(Vector2(50, 115)); + + SUBCASE("[Geometry2D] Both line and polygon are empty") { + r = Geometry2D::intersect_polyline_with_polygon(Vector<Vector2>(), Vector<Vector2>()); + CHECK_MESSAGE(r.is_empty(), "Both line and polygon are empty. The intersection line should also be empty."); + } + + SUBCASE("[Geometry2D] Line is non-empty and polygon is empty") { + r = Geometry2D::intersect_polyline_with_polygon(l, Vector<Vector2>()); + CHECK_MESSAGE(r.is_empty(), "The polygon is empty while the line is non-empty. The intersection line should be empty."); + } + + SUBCASE("[Geometry2D] Basic intersection with 1 resulting intersection line") { + r = Geometry2D::intersect_polyline_with_polygon(l, p); + REQUIRE_MESSAGE(r.size() == 1, "There should be 1 resulting intersection line."); + REQUIRE_MESSAGE(r[0].size() == 2, "The resulting intersection line should have 2 vertices."); + CHECK(r[0][0].is_equal_approx(Vector2(105.711609, 135.692886))); + CHECK(r[0][1].is_equal_approx(Vector2(116.805809, 224.446457))); + } + + SUBCASE("[Geometry2D] Complex intersection with 2 resulting intersection lines") { + l.clear(); + l.push_back(Vector2(100, 90)); + l.push_back(Vector2(190, 255)); + l.push_back(Vector2(135, 260)); + l.push_back(Vector2(57, 200)); + l.push_back(Vector2(50, 170)); + l.push_back(Vector2(15, 155)); + r = Geometry2D::intersect_polyline_with_polygon(l, p); + REQUIRE_MESSAGE(r.size() == 2, "There should be 2 resulting intersection lines."); + REQUIRE_MESSAGE(r[0].size() == 2, "The resulting intersection line should have 2 vertices."); + CHECK(r[0][0].is_equal_approx(Vector2(129.804565, 144.641693))); + CHECK(r[0][1].is_equal_approx(Vector2(171.527084, 221.132996))); + REQUIRE_MESSAGE(r[1].size() == 4, "The resulting intersection line should have 4 vertices."); + CHECK(r[1][0].is_equal_approx(Vector2(83.15609, 220.120087))); + CHECK(r[1][1].is_equal_approx(Vector2(57, 200))); + CHECK(r[1][2].is_equal_approx(Vector2(50, 170))); + CHECK(r[1][3].is_equal_approx(Vector2(34.980492, 163.563065))); + } +} + +TEST_CASE("[Geometry2D] Clip polyline with polygon") { + Vector<Vector2> l; + Vector<Vector2> p; + Vector<Vector<Point2>> r; + + l.push_back(Vector2(70, 140)); + l.push_back(Vector2(160, 320)); + + p.push_back(Vector2(225, 180)); + p.push_back(Vector2(160, 230)); + p.push_back(Vector2(20, 212)); + p.push_back(Vector2(50, 115)); + + SUBCASE("[Geometry2D] Both line and polygon are empty") { + r = Geometry2D::clip_polyline_with_polygon(Vector<Vector2>(), Vector<Vector2>()); + CHECK_MESSAGE(r.is_empty(), "Both line and polygon are empty. The clipped line should also be empty."); + } + + SUBCASE("[Geometry2D] Polygon is empty and line is non-empty") { + r = Geometry2D::clip_polyline_with_polygon(l, Vector<Vector2>()); + REQUIRE_MESSAGE(r.size() == 1, "There should be 1 resulting clipped line."); + REQUIRE_MESSAGE(r[0].size() == 2, "The resulting clipped line should have 2 vertices."); + CHECK(r[0][0].is_equal_approx(l[0])); + CHECK(r[0][1].is_equal_approx(l[1])); + } + + SUBCASE("[Geometry2D] Basic clip with 1 resulting clipped line") { + r = Geometry2D::clip_polyline_with_polygon(l, p); + REQUIRE_MESSAGE(r.size() == 1, "There should be 1 resulting clipped line."); + REQUIRE_MESSAGE(r[0].size() == 2, "The resulting clipped line should have 2 vertices."); + CHECK(r[0][0].is_equal_approx(Vector2(111.908401, 223.816803))); + CHECK(r[0][1].is_equal_approx(Vector2(160, 320))); + } + + SUBCASE("[Geometry2D] Complex clip with 2 resulting clipped lines") { + l.clear(); + l.push_back(Vector2(55, 70)); + l.push_back(Vector2(50, 190)); + l.push_back(Vector2(120, 165)); + l.push_back(Vector2(122, 250)); + l.push_back(Vector2(160, 320)); + r = Geometry2D::clip_polyline_with_polygon(l, p); + REQUIRE_MESSAGE(r.size() == 2, "There should be 2 resulting clipped lines."); + REQUIRE_MESSAGE(r[0].size() == 3, "The resulting clipped line should have 3 vertices."); + CHECK(r[0][0].is_equal_approx(Vector2(160, 320))); + CHECK(r[0][1].is_equal_approx(Vector2(122, 250))); + CHECK(r[0][2].is_equal_approx(Vector2(121.412682, 225.038757))); + REQUIRE_MESSAGE(r[1].size() == 2, "The resulting clipped line should have 2 vertices."); + CHECK(r[1][0].is_equal_approx(Vector2(53.07737, 116.143021))); + CHECK(r[1][1].is_equal_approx(Vector2(55, 70))); + } +} +} // namespace TestGeometry2D + +#endif // TEST_GEOMETRY_2D_H diff --git a/tests/core/math/test_geometry_3d.h b/tests/core/math/test_geometry_3d.h new file mode 100644 index 0000000000..f42003ffcf --- /dev/null +++ b/tests/core/math/test_geometry_3d.h @@ -0,0 +1,437 @@ +/*************************************************************************/ +/* test_geometry_3d.h */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/*************************************************************************/ + +#ifndef TEST_GEOMETRY_3D_H +#define TEST_GEOMETRY_3D_H + +#include "core/math/geometry_3d.h" +#include "tests/test_macros.h" + +namespace TestGeometry3D { +TEST_CASE("[Geometry3D] Closest Points Between Segments") { + struct Case { + Vector3 p_1, p_2, p_3, p_4; + Vector3 got_1, got_2; + Vector3 want_1, want_2; + Case(){}; + Case(Vector3 p_p_1, Vector3 p_p_2, Vector3 p_p_3, Vector3 p_p_4, Vector3 p_want_1, Vector3 p_want_2) : + p_1(p_p_1), p_2(p_p_2), p_3(p_p_3), p_4(p_p_4), want_1(p_want_1), want_2(p_want_2){}; + }; + Vector<Case> tt; + tt.push_back(Case(Vector3(1, -1, 1), Vector3(1, 1, -1), Vector3(-1, -2, -1), Vector3(-1, 1, 1), Vector3(1, -0.2, 0.2), Vector3(-1, -0.2, 0.2))); + for (int i = 0; i < tt.size(); ++i) { + Case current_case = tt[i]; + Geometry3D::get_closest_points_between_segments(current_case.p_1, current_case.p_2, current_case.p_3, current_case.p_4, current_case.got_1, current_case.got_2); + CHECK(current_case.got_1.is_equal_approx(current_case.want_1)); + CHECK(current_case.got_2.is_equal_approx(current_case.want_2)); + } +} + +TEST_CASE("[Geometry3D] Closest Distance Between Segments") { + struct Case { + Vector3 p_1, p_2, p_3, p_4; + float want; + Case(){}; + Case(Vector3 p_p_1, Vector3 p_p_2, Vector3 p_p_3, Vector3 p_p_4, float p_want) : + p_1(p_p_1), p_2(p_p_2), p_3(p_p_3), p_4(p_p_4), want(p_want){}; + }; + Vector<Case> tt; + tt.push_back(Case(Vector3(1, -2, 0), Vector3(1, 2, 0), Vector3(-1, 2, 0), Vector3(-1, -2, 0), 0.0f)); + for (int i = 0; i < tt.size(); ++i) { + Case current_case = tt[i]; + float out = Geometry3D::get_closest_distance_between_segments(current_case.p_1, current_case.p_2, current_case.p_3, current_case.p_4); + CHECK(out == current_case.want); + } +} + +TEST_CASE("[Geometry3D] Build Box Planes") { + const Vector3 extents = Vector3(5, 5, 20); + Vector<Plane> box = Geometry3D::build_box_planes(extents); + CHECK(box.size() == 6); + CHECK(extents.x == box[0].d); + CHECK(box[0].normal == Vector3(1, 0, 0)); + CHECK(extents.x == box[1].d); + CHECK(box[1].normal == Vector3(-1, 0, 0)); + CHECK(extents.y == box[2].d); + CHECK(box[2].normal == Vector3(0, 1, 0)); + CHECK(extents.y == box[3].d); + CHECK(box[3].normal == Vector3(0, -1, 0)); + CHECK(extents.z == box[4].d); + CHECK(box[4].normal == Vector3(0, 0, 1)); + CHECK(extents.z == box[5].d); + CHECK(box[5].normal == Vector3(0, 0, -1)); +} + +TEST_CASE("[Geometry3D] Build Capsule Planes") { + struct Case { + real_t radius, height; + int sides, lats; + Vector3::Axis axis; + int want_size; + Case(){}; + Case(real_t p_radius, real_t p_height, int p_sides, int p_lats, Vector3::Axis p_axis, int p_want) : + radius(p_radius), height(p_height), sides(p_sides), lats(p_lats), axis(p_axis), want_size(p_want){}; + }; + Vector<Case> tt; + tt.push_back(Case(10, 20, 6, 10, Vector3::Axis(), 126)); + for (int i = 0; i < tt.size(); ++i) { + Case current_case = tt[i]; + Vector<Plane> capsule = Geometry3D::build_capsule_planes(current_case.radius, current_case.height, current_case.sides, current_case.lats, current_case.axis); + // Should equal (p_sides * p_lats) * 2 + p_sides + CHECK(capsule.size() == current_case.want_size); + } +} + +TEST_CASE("[Geometry3D] Build Cylinder Planes") { + struct Case { + real_t radius, height; + int sides; + Vector3::Axis axis; + int want_size; + Case(){}; + Case(real_t p_radius, real_t p_height, int p_sides, Vector3::Axis p_axis, int p_want) : + radius(p_radius), height(p_height), sides(p_sides), axis(p_axis), want_size(p_want){}; + }; + Vector<Case> tt; + tt.push_back(Case(3.0f, 10.0f, 10, Vector3::Axis(), 12)); + for (int i = 0; i < tt.size(); ++i) { + Case current_case = tt[i]; + Vector<Plane> planes = Geometry3D::build_cylinder_planes(current_case.radius, current_case.height, current_case.sides, current_case.axis); + CHECK(planes.size() == current_case.want_size); + } +} + +TEST_CASE("[Geometry3D] Build Sphere Planes") { + struct Case { + real_t radius; + int lats, lons; + Vector3::Axis axis; + int want_size; + Case(){}; + Case(real_t p_radius, int p_lat, int p_lons, Vector3::Axis p_axis, int p_want) : + radius(p_radius), lats(p_lat), lons(p_lons), axis(p_axis), want_size(p_want){}; + }; + Vector<Case> tt; + tt.push_back(Case(10.0f, 10, 3, Vector3::Axis(), 63)); + for (int i = 0; i < tt.size(); ++i) { + Case current_case = tt[i]; + Vector<Plane> planes = Geometry3D::build_sphere_planes(current_case.radius, current_case.lats, current_case.lons, current_case.axis); + CHECK(planes.size() == 63); + } +} + +#if false +// This test has been temporarily disabled because it's really fragile and +// breaks if calculations change very slightly. For example, it breaks when +// using doubles, and it breaks when making Plane calculations more accurate. +TEST_CASE("[Geometry3D] Build Convex Mesh") { + struct Case { + Vector<Plane> object; + int want_faces, want_edges, want_vertices; + Case(){}; + Case(Vector<Plane> p_object, int p_want_faces, int p_want_edges, int p_want_vertices) : + object(p_object), want_faces(p_want_faces), want_edges(p_want_edges), want_vertices(p_want_vertices){}; + }; + Vector<Case> tt; + tt.push_back(Case(Geometry3D::build_box_planes(Vector3(5, 10, 5)), 6, 12, 8)); + tt.push_back(Case(Geometry3D::build_capsule_planes(5, 5, 20, 20, Vector3::Axis()), 820, 7603, 6243)); + tt.push_back(Case(Geometry3D::build_cylinder_planes(5, 5, 20, Vector3::Axis()), 22, 100, 80)); + tt.push_back(Case(Geometry3D::build_sphere_planes(5, 5, 20), 220, 1011, 522)); + for (int i = 0; i < tt.size(); ++i) { + Case current_case = tt[i]; + Geometry3D::MeshData mesh = Geometry3D::build_convex_mesh(current_case.object); + CHECK(mesh.faces.size() == current_case.want_faces); + CHECK(mesh.edges.size() == current_case.want_edges); + CHECK(mesh.vertices.size() == current_case.want_vertices); + } +} +#endif + +TEST_CASE("[Geometry3D] Clip Polygon") { + struct Case { + Plane clipping_plane; + Vector<Vector3> polygon; + bool want; + Case(){}; + Case(Plane p_clipping_plane, Vector<Vector3> p_polygon, bool p_want) : + clipping_plane(p_clipping_plane), polygon(p_polygon), want(p_want){}; + }; + Vector<Case> tt; + Vector<Plane> box_planes = Geometry3D::build_box_planes(Vector3(5, 10, 5)); + Vector<Vector3> box = Geometry3D::compute_convex_mesh_points(&box_planes[0], box_planes.size()); + tt.push_back(Case(Plane(), box, true)); + tt.push_back(Case(Plane(Vector3(0, 1, 0), Vector3(0, 3, 0)), box, false)); + for (int i = 0; i < tt.size(); ++i) { + Case current_case = tt[i]; + Vector<Vector3> output = Geometry3D::clip_polygon(current_case.polygon, current_case.clipping_plane); + if (current_case.want) { + CHECK(output == current_case.polygon); + } else { + CHECK(output != current_case.polygon); + } + } +} + +TEST_CASE("[Geometry3D] Compute Convex Mesh Points") { + struct Case { + Vector<Plane> mesh; + Vector<Vector3> want; + Case(){}; + Case(Vector<Plane> p_mesh, Vector<Vector3> p_want) : + mesh(p_mesh), want(p_want){}; + }; + Vector<Case> tt; + Vector<Vector3> cube; + cube.push_back(Vector3(-5, -5, -5)); + cube.push_back(Vector3(5, -5, -5)); + cube.push_back(Vector3(-5, 5, -5)); + cube.push_back(Vector3(5, 5, -5)); + cube.push_back(Vector3(-5, -5, 5)); + cube.push_back(Vector3(5, -5, 5)); + cube.push_back(Vector3(-5, 5, 5)); + cube.push_back(Vector3(5, 5, 5)); + tt.push_back(Case(Geometry3D::build_box_planes(Vector3(5, 5, 5)), cube)); + for (int i = 0; i < tt.size(); ++i) { + Case current_case = tt[i]; + Vector<Vector3> vectors = Geometry3D::compute_convex_mesh_points(¤t_case.mesh[0], current_case.mesh.size()); + CHECK(vectors == current_case.want); + } +} + +TEST_CASE("[Geometry3D] Get Closest Point To Segment") { + struct Case { + Vector3 point; + Vector<Vector3> segment; + Vector3 want; + Case(){}; + Case(Vector3 p_point, Vector<Vector3> p_segment, Vector3 p_want) : + point(p_point), segment(p_segment), want(p_want){}; + }; + Vector<Case> tt; + Vector<Vector3> test_segment; + test_segment.push_back(Vector3(1, 1, 1)); + test_segment.push_back(Vector3(5, 5, 5)); + tt.push_back(Case(Vector3(2, 1, 4), test_segment, Vector3(2.33333, 2.33333, 2.33333))); + for (int i = 0; i < tt.size(); ++i) { + Case current_case = tt[i]; + Vector3 output = Geometry3D::get_closest_point_to_segment(current_case.point, ¤t_case.segment[0]); + CHECK(output.is_equal_approx(current_case.want)); + } +} + +TEST_CASE("[Geometry3D] Plane and Box Overlap") { + struct Case { + Vector3 normal, max_box; + float d; + bool want; + Case(){}; + Case(Vector3 p_normal, float p_d, Vector3 p_max_box, bool p_want) : + normal(p_normal), max_box(p_max_box), d(p_d), want(p_want){}; + }; + Vector<Case> tt; + tt.push_back(Case(Vector3(3, 4, 2), 5, Vector3(5, 5, 5), true)); + tt.push_back(Case(Vector3(0, 1, 0), -10, Vector3(5, 5, 5), false)); + tt.push_back(Case(Vector3(1, 0, 0), -6, Vector3(5, 5, 5), false)); + for (int i = 0; i < tt.size(); ++i) { + Case current_case = tt[i]; + bool overlap = Geometry3D::planeBoxOverlap(current_case.normal, current_case.d, current_case.max_box); + CHECK(overlap == current_case.want); + } +} + +TEST_CASE("[Geometry3D] Is Point in Projected Triangle") { + struct Case { + Vector3 point, v_1, v_2, v_3; + bool want; + Case(){}; + Case(Vector3 p_point, Vector3 p_v_1, Vector3 p_v_2, Vector3 p_v_3, bool p_want) : + point(p_point), v_1(p_v_1), v_2(p_v_2), v_3(p_v_3), want(p_want){}; + }; + Vector<Case> tt; + tt.push_back(Case(Vector3(1, 1, 0), Vector3(3, 0, 0), Vector3(0, 3, 0), Vector3(-3, 0, 0), true)); + tt.push_back(Case(Vector3(5, 1, 0), Vector3(3, 0, 0), Vector3(0, 3, 0), Vector3(-3, 0, 0), false)); + tt.push_back(Case(Vector3(3, 0, 0), Vector3(3, 0, 0), Vector3(0, 3, 0), Vector3(-3, 0, 0), true)); + for (int i = 0; i < tt.size(); ++i) { + Case current_case = tt[i]; + bool output = Geometry3D::point_in_projected_triangle(current_case.point, current_case.v_1, current_case.v_2, current_case.v_3); + CHECK(output == current_case.want); + } +} + +TEST_CASE("[Geometry3D] Does Ray Intersect Triangle") { + struct Case { + Vector3 from, direction, v_1, v_2, v_3; + Vector3 *result; + bool want; + Case(){}; + Case(Vector3 p_from, Vector3 p_direction, Vector3 p_v_1, Vector3 p_v_2, Vector3 p_v_3, bool p_want) : + from(p_from), direction(p_direction), v_1(p_v_1), v_2(p_v_2), v_3(p_v_3), result(nullptr), want(p_want){}; + }; + Vector<Case> tt; + tt.push_back(Case(Vector3(0, 1, 1), Vector3(0, 0, -10), Vector3(0, 3, 0), Vector3(-3, 0, 0), Vector3(3, 0, 0), true)); + tt.push_back(Case(Vector3(5, 10, 1), Vector3(0, 0, -10), Vector3(0, 3, 0), Vector3(-3, 0, 0), Vector3(3, 0, 0), false)); + tt.push_back(Case(Vector3(0, 1, 1), Vector3(0, 0, 10), Vector3(0, 3, 0), Vector3(-3, 0, 0), Vector3(3, 0, 0), false)); + for (int i = 0; i < tt.size(); ++i) { + Case current_case = tt[i]; + bool output = Geometry3D::ray_intersects_triangle(current_case.from, current_case.direction, current_case.v_1, current_case.v_2, current_case.v_3, current_case.result); + CHECK(output == current_case.want); + } +} + +TEST_CASE("[Geometry3D] Does Segment Intersect Convex") { + struct Case { + Vector3 from, to; + Vector<Plane> planes; + Vector3 *result, *normal; + bool want; + Case(){}; + Case(Vector3 p_from, Vector3 p_to, Vector<Plane> p_planes, bool p_want) : + from(p_from), to(p_to), planes(p_planes), result(nullptr), normal(nullptr), want(p_want){}; + }; + Vector<Case> tt; + tt.push_back(Case(Vector3(10, 10, 10), Vector3(0, 0, 0), Geometry3D::build_box_planes(Vector3(5, 5, 5)), true)); + tt.push_back(Case(Vector3(10, 10, 10), Vector3(5, 5, 5), Geometry3D::build_box_planes(Vector3(5, 5, 5)), true)); + tt.push_back(Case(Vector3(10, 10, 10), Vector3(6, 5, 5), Geometry3D::build_box_planes(Vector3(5, 5, 5)), false)); + for (int i = 0; i < tt.size(); ++i) { + Case current_case = tt[i]; + bool output = Geometry3D::segment_intersects_convex(current_case.from, current_case.to, ¤t_case.planes[0], current_case.planes.size(), current_case.result, current_case.normal); + CHECK(output == current_case.want); + } +} + +TEST_CASE("[Geometry3D] Segment Intersects Cylinder") { + struct Case { + Vector3 from, to; + real_t height, radius; + Vector3 *result, *normal; + bool want; + Case(){}; + Case(Vector3 p_from, Vector3 p_to, real_t p_height, real_t p_radius, bool p_want) : + from(p_from), to(p_to), height(p_height), radius(p_radius), result(nullptr), normal(nullptr), want(p_want){}; + }; + Vector<Case> tt; + tt.push_back(Case(Vector3(10, 10, 10), Vector3(0, 0, 0), 5, 5, true)); + tt.push_back(Case(Vector3(10, 10, 10), Vector3(6, 6, 6), 5, 5, false)); + for (int i = 0; i < tt.size(); ++i) { + Case current_case = tt[i]; + bool output = Geometry3D::segment_intersects_cylinder(current_case.from, current_case.to, current_case.height, current_case.radius, current_case.result, current_case.normal); + CHECK(output == current_case.want); + } +} + +TEST_CASE("[Geometry3D] Segment Intersects Cylinder") { + struct Case { + Vector3 from, to, sphere_pos; + real_t radius; + Vector3 *result, *normal; + bool want; + Case(){}; + Case(Vector3 p_from, Vector3 p_to, Vector3 p_sphere_pos, real_t p_radius, bool p_want) : + from(p_from), to(p_to), sphere_pos(p_sphere_pos), radius(p_radius), result(nullptr), normal(nullptr), want(p_want){}; + }; + Vector<Case> tt; + tt.push_back(Case(Vector3(10, 10, 10), Vector3(0, 0, 0), Vector3(0, 0, 0), 5, true)); + tt.push_back(Case(Vector3(10, 10, 10), Vector3(0, 0, 2.5), Vector3(0, 0, 0), 5, true)); + tt.push_back(Case(Vector3(10, 10, 10), Vector3(5, 5, 5), Vector3(0, 0, 0), 5, false)); + for (int i = 0; i < tt.size(); ++i) { + Case current_case = tt[i]; + bool output = Geometry3D::segment_intersects_sphere(current_case.from, current_case.to, current_case.sphere_pos, current_case.radius, current_case.result, current_case.normal); + CHECK(output == current_case.want); + } +} + +TEST_CASE("[Geometry3D] Segment Intersects Triangle") { + struct Case { + Vector3 from, to, v_1, v_2, v_3, *result; + bool want; + Case(){}; + Case(Vector3 p_from, Vector3 p_to, Vector3 p_v_1, Vector3 p_v_2, Vector3 p_v_3, bool p_want) : + from(p_from), to(p_to), v_1(p_v_1), v_2(p_v_2), v_3(p_v_3), result(nullptr), want(p_want){}; + }; + Vector<Case> tt; + tt.push_back(Case(Vector3(1, 1, 1), Vector3(-1, -1, -1), Vector3(-3, 0, 0), Vector3(0, 3, 0), Vector3(3, 0, 0), true)); + tt.push_back(Case(Vector3(1, 1, 1), Vector3(3, 0, 0), Vector3(-3, 0, 0), Vector3(0, 3, 0), Vector3(3, 0, 0), true)); + tt.push_back(Case(Vector3(1, 1, 1), Vector3(10, -1, -1), Vector3(-3, 0, 0), Vector3(0, 3, 0), Vector3(3, 0, 0), false)); + for (int i = 0; i < tt.size(); ++i) { + Case current_case = tt[i]; + bool output = Geometry3D::segment_intersects_triangle(current_case.from, current_case.to, current_case.v_1, current_case.v_2, current_case.v_3, current_case.result); + CHECK(output == current_case.want); + } +} + +TEST_CASE("[Geometry3D] Triangle and Box Overlap") { + struct Case { + Vector3 box_centre; + Vector3 box_half_size; + Vector3 *tri_verts; + bool want; + Case(){}; + Case(Vector3 p_centre, Vector3 p_half_size, Vector3 *p_verts, bool p_want) : + box_centre(p_centre), box_half_size(p_half_size), tri_verts(p_verts), want(p_want){}; + }; + Vector<Case> tt; + Vector3 GoodTriangle[3] = { Vector3(3, 2, 3), Vector3(2, 2, 1), Vector3(2, 1, 1) }; + tt.push_back(Case(Vector3(0, 0, 0), Vector3(5, 5, 5), GoodTriangle, true)); + Vector3 BadTriangle[3] = { Vector3(100, 100, 100), Vector3(-100, -100, -100), Vector3(10, 10, 10) }; + tt.push_back(Case(Vector3(1000, 1000, 1000), Vector3(1, 1, 1), BadTriangle, false)); + for (int i = 0; i < tt.size(); ++i) { + Case current_case = tt[i]; + bool output = Geometry3D::triangle_box_overlap(current_case.box_centre, current_case.box_half_size, current_case.tri_verts); + CHECK(output == current_case.want); + } +} + +TEST_CASE("[Geometry3D] Triangle and Sphere Intersect") { + struct Case { + Vector<Vector3> triangle; + Vector3 normal, sphere_pos, triangle_contact, sphere_contact; + real_t sphere_radius; + bool want; + Case(){}; + Case(Vector<Vector3> p_triangle, Vector3 p_normal, Vector3 p_sphere_pos, real_t p_sphere_radius, bool p_want) : + triangle(p_triangle), normal(p_normal), sphere_pos(p_sphere_pos), triangle_contact(Vector3()), sphere_contact(Vector3()), sphere_radius(p_sphere_radius), want(p_want){}; + }; + Vector<Case> tt; + Vector<Vector3> triangle; + triangle.push_back(Vector3(3, 0, 0)); + triangle.push_back(Vector3(-3, 0, 0)); + triangle.push_back(Vector3(0, 3, 0)); + tt.push_back(Case(triangle, Vector3(0, -1, 0), Vector3(0, 0, 0), 5, true)); + tt.push_back(Case(triangle, Vector3(0, 1, 0), Vector3(0, 0, 0), 5, true)); + tt.push_back(Case(triangle, Vector3(0, 1, 0), Vector3(20, 0, 0), 5, false)); + for (int i = 0; i < tt.size(); ++i) { + Case current_case = tt[i]; + bool output = Geometry3D::triangle_sphere_intersection_test(¤t_case.triangle[0], current_case.normal, current_case.sphere_pos, current_case.sphere_radius, current_case.triangle_contact, current_case.sphere_contact); + CHECK(output == current_case.want); + } +} +} // namespace TestGeometry3D + +#endif // TEST_GEOMETRY_3D_H diff --git a/tests/core/math/test_math.cpp b/tests/core/math/test_math.cpp new file mode 100644 index 0000000000..6ec9bc2473 --- /dev/null +++ b/tests/core/math/test_math.cpp @@ -0,0 +1,690 @@ +/*************************************************************************/ +/* test_math.cpp */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/*************************************************************************/ + +#include "test_math.h" + +#include "core/math/camera_matrix.h" +#include "core/math/delaunay_3d.h" +#include "core/math/geometry_2d.h" +#include "core/os/main_loop.h" +#include "core/os/os.h" + +namespace TestMath { + +class GetClassAndNamespace { + String code; + int idx; + int line; + String error_str; + bool error; + Variant value; + + String class_name; + + enum Token { + TK_BRACKET_OPEN, + TK_BRACKET_CLOSE, + TK_CURLY_BRACKET_OPEN, + TK_CURLY_BRACKET_CLOSE, + TK_PERIOD, + TK_COLON, + TK_COMMA, + TK_SYMBOL, + TK_IDENTIFIER, + TK_STRING, + TK_NUMBER, + TK_EOF, + TK_ERROR + }; + + Token get_token() { + while (true) { + switch (code[idx]) { + case '\n': { + line++; + idx++; + break; + }; + case 0: { + return TK_EOF; + + } break; + case '{': { + idx++; + return TK_CURLY_BRACKET_OPEN; + }; + case '}': { + idx++; + return TK_CURLY_BRACKET_CLOSE; + }; + case '[': { + idx++; + return TK_BRACKET_OPEN; + }; + case ']': { + idx++; + return TK_BRACKET_CLOSE; + }; + case ':': { + idx++; + return TK_COLON; + }; + case ',': { + idx++; + return TK_COMMA; + }; + case '.': { + idx++; + return TK_PERIOD; + }; + case '#': { + //compiler directive + while (code[idx] != '\n' && code[idx] != 0) { + idx++; + } + continue; + } break; + case '/': { + switch (code[idx + 1]) { + case '*': { // block comment + + idx += 2; + while (true) { + if (code[idx] == 0) { + error_str = "Unterminated comment"; + error = true; + return TK_ERROR; + } else if (code[idx] == '*' && code[idx + 1] == '/') { + idx += 2; + break; + } else if (code[idx] == '\n') { + line++; + } + + idx++; + } + + } break; + case '/': { // line comment skip + + while (code[idx] != '\n' && code[idx] != 0) { + idx++; + } + + } break; + default: { + value = "/"; + idx++; + return TK_SYMBOL; + } + } + + continue; // a comment + } break; + case '\'': + case '"': { + char32_t begin_str = code[idx]; + idx++; + String tk_string = String(); + while (true) { + if (code[idx] == 0) { + error_str = "Unterminated String"; + error = true; + return TK_ERROR; + } else if (code[idx] == begin_str) { + idx++; + break; + } else if (code[idx] == '\\') { + //escaped characters... + idx++; + char32_t next = code[idx]; + if (next == 0) { + error_str = "Unterminated String"; + error = true; + return TK_ERROR; + } + char32_t res = 0; + + switch (next) { + case 'b': + res = 8; + break; + case 't': + res = 9; + break; + case 'n': + res = 10; + break; + case 'f': + res = 12; + break; + case 'r': + res = 13; + break; + case '\"': + res = '\"'; + break; + case '\\': + res = '\\'; + break; + default: { + res = next; + } break; + } + + tk_string += res; + + } else { + if (code[idx] == '\n') { + line++; + } + tk_string += code[idx]; + } + idx++; + } + + value = tk_string; + + return TK_STRING; + + } break; + default: { + if (code[idx] <= 32) { + idx++; + break; + } + + if ((code[idx] >= 33 && code[idx] <= 47) || (code[idx] >= 58 && code[idx] <= 64) || (code[idx] >= 91 && code[idx] <= 96) || (code[idx] >= 123 && code[idx] <= 127)) { + value = String::chr(code[idx]); + idx++; + return TK_SYMBOL; + } + + if (code[idx] == '-' || (code[idx] >= '0' && code[idx] <= '9')) { + //a number + const char32_t *rptr; + double number = String::to_float(&code[idx], &rptr); + idx += (rptr - &code[idx]); + value = number; + return TK_NUMBER; + + } else if ((code[idx] >= 'A' && code[idx] <= 'Z') || (code[idx] >= 'a' && code[idx] <= 'z') || code[idx] > 127) { + String id; + + while ((code[idx] >= 'A' && code[idx] <= 'Z') || (code[idx] >= 'a' && code[idx] <= 'z') || code[idx] > 127) { + id += code[idx]; + idx++; + } + + value = id; + return TK_IDENTIFIER; + } else { + error_str = "Unexpected character."; + error = true; + return TK_ERROR; + } + } + } + } + } + +public: + Error parse(const String &p_code, const String &p_known_class_name = String()) { + code = p_code; + idx = 0; + line = 0; + error_str = String(); + error = false; + value = Variant(); + class_name = String(); + + bool use_next_class = false; + Token tk = get_token(); + + Map<int, String> namespace_stack; + int curly_stack = 0; + + while (!error || tk != TK_EOF) { + if (tk == TK_BRACKET_OPEN) { + tk = get_token(); + if (tk == TK_IDENTIFIER && String(value) == "ScriptClass") { + if (get_token() == TK_BRACKET_CLOSE) { + use_next_class = true; + } + } + } else if (tk == TK_IDENTIFIER && String(value) == "class") { + tk = get_token(); + if (tk == TK_IDENTIFIER) { + String name = value; + if (use_next_class || p_known_class_name == name) { + for (const KeyValue<int, String> &E : namespace_stack) { + class_name += E.value + "."; + } + class_name += String(value); + break; + } + } + + } else if (tk == TK_IDENTIFIER && String(value) == "namespace") { + String name; + int at_level = curly_stack; + while (true) { + tk = get_token(); + if (tk == TK_IDENTIFIER) { + name += String(value); + } + + tk = get_token(); + if (tk == TK_PERIOD) { + name += "."; + } else if (tk == TK_CURLY_BRACKET_OPEN) { + curly_stack++; + break; + } else { + break; //whathever else + } + } + + if (name != String()) { + namespace_stack[at_level] = name; + } + + } else if (tk == TK_CURLY_BRACKET_OPEN) { + curly_stack++; + } else if (tk == TK_CURLY_BRACKET_CLOSE) { + curly_stack--; + if (namespace_stack.has(curly_stack)) { + namespace_stack.erase(curly_stack); + } + } + + tk = get_token(); + } + + if (error) { + return ERR_PARSE_ERROR; + } + + return OK; + } + + String get_error() { + return error_str; + } + + String get_class() { + return class_name; + } +}; + +void test_vec(Plane p_vec) { + CameraMatrix cm; + cm.set_perspective(45, 1, 0, 100); + Plane v0 = cm.xform4(p_vec); + + print_line("out: " + v0); + v0.normal.z = (v0.d / 100.0 * 2.0 - 1.0) * v0.d; + print_line("out_F: " + v0); +} + +uint32_t ihash(uint32_t a) { + a = (a + 0x7ed55d16) + (a << 12); + a = (a ^ 0xc761c23c) ^ (a >> 19); + a = (a + 0x165667b1) + (a << 5); + a = (a + 0xd3a2646c) ^ (a << 9); + a = (a + 0xfd7046c5) + (a << 3); + a = (a ^ 0xb55a4f09) ^ (a >> 16); + return a; +} + +uint32_t ihash2(uint32_t a) { + a = (a ^ 61) ^ (a >> 16); + a = a + (a << 3); + a = a ^ (a >> 4); + a = a * 0x27d4eb2d; + a = a ^ (a >> 15); + return a; +} + +uint32_t ihash3(uint32_t a) { + a = (a + 0x479ab41d) + (a << 8); + a = (a ^ 0xe4aa10ce) ^ (a >> 5); + a = (a + 0x9942f0a6) - (a << 14); + a = (a ^ 0x5aedd67d) ^ (a >> 3); + a = (a + 0x17bea992) + (a << 7); + return a; +} + +MainLoop *test() { + { + Vector<Vector3> points; + points.push_back(Vector3(0, 0, 0)); + points.push_back(Vector3(0, 0, 1)); + points.push_back(Vector3(0, 1, 0)); + points.push_back(Vector3(0, 1, 1)); + points.push_back(Vector3(1, 1, 0)); + points.push_back(Vector3(1, 0, 0)); + points.push_back(Vector3(1, 0, 1)); + points.push_back(Vector3(1, 1, 1)); + + for (int i = 0; i < 800; i++) { + points.push_back(Vector3(Math::randf() * 2.0 - 1.0, Math::randf() * 2.0 - 1.0, Math::randf() * 2.0 - 1.0) * Vector3(25, 30, 33)); + } + + Vector<Delaunay3D::OutputSimplex> os = Delaunay3D::tetrahedralize(points); + print_line("simplices in the end: " + itos(os.size())); + for (int i = 0; i < os.size(); i++) { + print_line("Simplex " + itos(i) + ": "); + print_line(points[os[i].points[0]]); + print_line(points[os[i].points[1]]); + print_line(points[os[i].points[2]]); + print_line(points[os[i].points[3]]); + } + + { + FileAccessRef f = FileAccess::open("res://bsp.obj", FileAccess::WRITE); + for (int i = 0; i < os.size(); i++) { + f->store_line("o Simplex" + itos(i)); + for (int j = 0; j < 4; j++) { + f->store_line(vformat("v %f %f %f", points[os[i].points[j]].x, points[os[i].points[j]].y, points[os[i].points[j]].z)); + } + static const int face_order[4][3] = { + { 1, 2, 3 }, + { 1, 3, 4 }, + { 1, 2, 4 }, + { 2, 3, 4 } + }; + + for (int j = 0; j < 4; j++) { + f->store_line(vformat("f %d %d %d", 4 * i + face_order[j][0], 4 * i + face_order[j][1], 4 * i + face_order[j][2])); + } + } + f->close(); + } + + return nullptr; + } + + { + float r = 1; + float g = 0.5; + float b = 0.1; + + const float pow2to9 = 512.0f; + const float B = 15.0f; + const float N = 9.0f; + + float sharedexp = 65408.000f; + + float cRed = MAX(0.0f, MIN(sharedexp, r)); + float cGreen = MAX(0.0f, MIN(sharedexp, g)); + float cBlue = MAX(0.0f, MIN(sharedexp, b)); + + float cMax = MAX(cRed, MAX(cGreen, cBlue)); + + float expp = MAX(-B - 1.0f, floor(Math::log(cMax) / Math_LN2)) + 1.0f + B; + + float sMax = (float)floor((cMax / Math::pow(2.0f, expp - B - N)) + 0.5f); + + float exps = expp + 1.0f; + + if (0.0 <= sMax && sMax < pow2to9) { + exps = expp; + } + + float sRed = Math::floor((cRed / pow(2.0f, exps - B - N)) + 0.5f); + float sGreen = Math::floor((cGreen / pow(2.0f, exps - B - N)) + 0.5f); + float sBlue = Math::floor((cBlue / pow(2.0f, exps - B - N)) + 0.5f); + + print_line("R: " + rtos(sRed) + " G: " + rtos(sGreen) + " B: " + rtos(sBlue) + " EXP: " + rtos(exps)); + + uint32_t rgbe = (Math::fast_ftoi(sRed) & 0x1FF) | ((Math::fast_ftoi(sGreen) & 0x1FF) << 9) | ((Math::fast_ftoi(sBlue) & 0x1FF) << 18) | ((Math::fast_ftoi(exps) & 0x1F) << 27); + + float rb = rgbe & 0x1ff; + float gb = (rgbe >> 9) & 0x1ff; + float bb = (rgbe >> 18) & 0x1ff; + float eb = (rgbe >> 27); + float mb = Math::pow(2.0, eb - 15.0 - 9.0); + float rd = rb * mb; + float gd = gb * mb; + float bd = bb * mb; + + print_line("RGBE: " + Color(rd, gd, bd)); + } + + Vector<int> ints; + ints.resize(20); + + { + int *w; + w = ints.ptrw(); + for (int i = 0; i < ints.size(); i++) { + w[i] = i; + } + } + + Vector<int> posho = ints; + + { + const int *r = posho.ptr(); + for (int i = 0; i < posho.size(); i++) { + print_line(itos(i) + " : " + itos(r[i])); + } + } + + List<String> cmdlargs = OS::get_singleton()->get_cmdline_args(); + + if (cmdlargs.is_empty()) { + //try editor! + return nullptr; + } + + String test = cmdlargs.back()->get(); + if (test == "math") { + // Not a file name but the test name, abort. + // FIXME: This test is ugly as heck, needs fixing :) + return nullptr; + } + + FileAccess *fa = FileAccess::open(test, FileAccess::READ); + ERR_FAIL_COND_V_MSG(!fa, nullptr, "Could not open file: " + test); + + Vector<uint8_t> buf; + uint64_t flen = fa->get_length(); + buf.resize(fa->get_length() + 1); + fa->get_buffer(buf.ptrw(), flen); + buf.write[flen] = 0; + + String code; + code.parse_utf8((const char *)&buf[0]); + + GetClassAndNamespace getclass; + if (getclass.parse(code)) { + print_line("Parse error: " + getclass.get_error()); + } else { + print_line("Found class: " + getclass.get_class()); + } + + { + Vector<int> hashes; + List<StringName> tl; + ClassDB::get_class_list(&tl); + + for (const StringName &E : tl) { + Vector<uint8_t> m5b = E.operator String().md5_buffer(); + hashes.push_back(hashes.size()); + } + + for (int i = nearest_shift(hashes.size()); i < 20; i++) { + bool success = true; + for (int s = 0; s < 10000; s++) { + Set<uint32_t> existing; + success = true; + + for (int j = 0; j < hashes.size(); j++) { + uint32_t eh = ihash2(ihash3(hashes[j] + ihash(s) + s)) & ((1 << i) - 1); + if (existing.has(eh)) { + success = false; + break; + } + existing.insert(eh); + } + + if (success) { + print_line("success at " + itos(i) + "/" + itos(nearest_shift(hashes.size())) + " shift " + itos(s)); + break; + } + } + if (success) { + break; + } + } + + print_line("DONE"); + } + + { + print_line("NUM: " + itos(-128)); + } + + { + Vector3 v(1, 2, 3); + v.normalize(); + real_t a = 0.3; + + Basis m(v, a); + + Vector3 v2(7, 3, 1); + v2.normalize(); + real_t a2 = 0.8; + + Basis m2(v2, a2); + + Quaternion q = m; + Quaternion q2 = m2; + + Basis m3 = m.inverse() * m2; + Quaternion q3 = (q.inverse() * q2); //.normalized(); + + print_line(Quaternion(m3)); + print_line(q3); + + print_line("before v: " + v + " a: " + rtos(a)); + q.get_axis_angle(v, a); + print_line("after v: " + v + " a: " + rtos(a)); + } + + String ret; + + List<String> args; + args.push_back("-l"); + Error err = OS::get_singleton()->execute("/bin/ls", args, &ret); + print_line("error: " + itos(err)); + print_line(ret); + + Basis m3; + m3.rotate(Vector3(1, 0, 0), 0.2); + m3.rotate(Vector3(0, 1, 0), 1.77); + m3.rotate(Vector3(0, 0, 1), 212); + Basis m32; + m32.set_euler(m3.get_euler()); + print_line("ELEULEEEEEEEEEEEEEEEEEER: " + m3.get_euler() + " vs " + m32.get_euler()); + + { + Dictionary d; + d["momo"] = 1; + Dictionary b = d; + b["44"] = 4; + } + + print_line("inters: " + rtos(Geometry2D::segment_intersects_circle(Vector2(-5, 0), Vector2(-2, 0), Vector2(), 1.0))); + + print_line("cross: " + Vector3(1, 2, 3).cross(Vector3(4, 5, 7))); + print_line("dot: " + rtos(Vector3(1, 2, 3).dot(Vector3(4, 5, 7)))); + print_line("abs: " + Vector3(-1, 2, -3).abs()); + print_line("distance_to: " + rtos(Vector3(1, 2, 3).distance_to(Vector3(4, 5, 7)))); + print_line("distance_squared_to: " + rtos(Vector3(1, 2, 3).distance_squared_to(Vector3(4, 5, 7)))); + print_line("plus: " + (Vector3(1, 2, 3) + Vector3(Vector3(4, 5, 7)))); + print_line("minus: " + (Vector3(1, 2, 3) - Vector3(Vector3(4, 5, 7)))); + print_line("mul: " + (Vector3(1, 2, 3) * Vector3(Vector3(4, 5, 7)))); + print_line("div: " + (Vector3(1, 2, 3) / Vector3(Vector3(4, 5, 7)))); + print_line("mul scalar: " + (Vector3(1, 2, 3) * 2.0)); + print_line("premul scalar: " + (2.0 * Vector3(1, 2, 3))); + print_line("div scalar: " + (Vector3(1, 2, 3) / 3.0)); + print_line("length: " + rtos(Vector3(1, 2, 3).length())); + print_line("length squared: " + rtos(Vector3(1, 2, 3).length_squared())); + print_line("normalized: " + Vector3(1, 2, 3).normalized()); + print_line("inverse: " + Vector3(1, 2, 3).inverse()); + + { + Vector3 v(4, 5, 7); + v.normalize(); + print_line("normalize: " + v); + } + + { + Vector3 v(4, 5, 7); + v += Vector3(1, 2, 3); + print_line("+=: " + v); + } + + { + Vector3 v(4, 5, 7); + v -= Vector3(1, 2, 3); + print_line("-=: " + v); + } + + { + Vector3 v(4, 5, 7); + v *= Vector3(1, 2, 3); + print_line("*=: " + v); + } + + { + Vector3 v(4, 5, 7); + v /= Vector3(1, 2, 3); + print_line("/=: " + v); + } + + { + Vector3 v(4, 5, 7); + v *= 2.0; + print_line("scalar *=: " + v); + } + + { + Vector3 v(4, 5, 7); + v /= 2.0; + print_line("scalar /=: " + v); + } + + return nullptr; +} +} // namespace TestMath diff --git a/tests/core/math/test_math.h b/tests/core/math/test_math.h new file mode 100644 index 0000000000..ab5fb6a050 --- /dev/null +++ b/tests/core/math/test_math.h @@ -0,0 +1,41 @@ +/*************************************************************************/ +/* test_math.h */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/*************************************************************************/ + +#ifndef TEST_MATH_H +#define TEST_MATH_H + +class MainLoop; + +namespace TestMath { + +MainLoop *test(); +} + +#endif diff --git a/tests/core/math/test_random_number_generator.h b/tests/core/math/test_random_number_generator.h new file mode 100644 index 0000000000..39c4771c19 --- /dev/null +++ b/tests/core/math/test_random_number_generator.h @@ -0,0 +1,275 @@ +/*************************************************************************/ +/* test_random_number_generator.h */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/*************************************************************************/ + +#ifndef TEST_RANDOM_NUMBER_GENERATOR_H +#define TEST_RANDOM_NUMBER_GENERATOR_H + +#include "core/math/random_number_generator.h" +#include "tests/test_macros.h" + +namespace TestRandomNumberGenerator { + +TEST_CASE("[RandomNumberGenerator] Float") { + Ref<RandomNumberGenerator> rng = memnew(RandomNumberGenerator); + rng->set_seed(0); + + INFO("Should give float between 0.0 and 1.0."); + for (int i = 0; i < 1000; i++) { + real_t n = rng->randf(); + CHECK(n >= 0.0); + CHECK(n <= 1.0); + } +} + +TEST_CASE("[RandomNumberGenerator] Integer range via modulo") { + Ref<RandomNumberGenerator> rng = memnew(RandomNumberGenerator); + rng->set_seed(0); + + INFO("Should give integer between 0 and 100."); + for (int i = 0; i < 1000; i++) { + uint32_t n = rng->randi() % 100; + CHECK(n >= 0); + CHECK(n <= 100); + } +} + +TEST_CASE_MAY_FAIL("[RandomNumberGenerator] Integer 32 bit") { + Ref<RandomNumberGenerator> rng = memnew(RandomNumberGenerator); + rng->set_seed(0); // Change the seed if this fails. + + bool higher = false; + int i; + for (i = 0; i < 1000; i++) { + uint32_t n = rng->randi(); + if (n > 0x0fff'ffff) { + higher = true; + break; + } + } + INFO("Current seed: ", rng->get_seed()); + INFO("Current iteration: ", i); + CHECK_MESSAGE(higher, "Given current seed, this should give an integer higher than 0x0fff'ffff at least once."); +} + +TEST_CASE("[RandomNumberGenerator] Float and integer range") { + Ref<RandomNumberGenerator> rng = memnew(RandomNumberGenerator); + rng->set_seed(0); + uint64_t initial_state = rng->get_state(); + uint32_t initial_seed = rng->get_seed(); + + INFO("Should give float between -100.0 and 100.0, base test."); + for (int i = 0; i < 1000; i++) { + real_t n0 = rng->randf_range(-100.0, 100.0); + CHECK(n0 >= -100); + CHECK(n0 <= 100); + } + + rng->randomize(); + INFO("Should give float between -75.0 and 75.0."); + INFO("Shouldn't be affected by randomize."); + for (int i = 0; i < 1000; i++) { + real_t n1 = rng->randf_range(-75.0, 75.0); + CHECK(n1 >= -75); + CHECK(n1 <= 75); + } + + rng->set_state(initial_state); + INFO("Should give integer between -50 and 50."); + INFO("Shouldn't be affected by set_state."); + for (int i = 0; i < 1000; i++) { + real_t n2 = rng->randi_range(-50, 50); + CHECK(n2 >= -50); + CHECK(n2 <= 50); + } + + rng->set_seed(initial_seed); + INFO("Should give integer between -25 and 25."); + INFO("Shouldn't be affected by set_seed."); + for (int i = 0; i < 1000; i++) { + int32_t n3 = rng->randi_range(-25, 25); + CHECK(n3 >= -25); + CHECK(n3 <= 25); + } + + rng->randf(); + rng->randf(); + + INFO("Should give float between -10.0 and 10.0."); + INFO("Shouldn't be affected after generating new numbers."); + for (int i = 0; i < 1000; i++) { + real_t n4 = rng->randf_range(-10.0, 10.0); + CHECK(n4 >= -10); + CHECK(n4 <= 10); + } + + rng->randi(); + rng->randi(); + + INFO("Should give integer between -5 and 5."); + INFO("Shouldn't be affected after generating new numbers."); + for (int i = 0; i < 1000; i++) { + real_t n5 = rng->randf_range(-5, 5); + CHECK(n5 >= -5); + CHECK(n5 <= 5); + } +} + +TEST_CASE_MAY_FAIL("[RandomNumberGenerator] Normal distribution") { + Ref<RandomNumberGenerator> rng = memnew(RandomNumberGenerator); + rng->set_seed(1); // Change the seed if this fails. + INFO("Should give a number between -5 to 5 (5 std deviations away; above 99.7% chance it will be in this range)."); + INFO("Standard randfn function call."); + for (int i = 0; i < 100; i++) { + real_t n = rng->randfn(); + CHECK(n >= -5); + CHECK(n <= 5); + } + + INFO("Should give number between -5 to 5 after multiple randi/randf calls."); + INFO("5 std deviations away; above 99.7% chance it will be in this range."); + rng->randf(); + rng->randi(); + for (int i = 0; i < 100; i++) { + real_t n = rng->randfn(); + CHECK(n >= -5); + CHECK(n <= 5); + } + + INFO("Checks if user defined mean and deviation work properly."); + INFO("5 std deviations away; above 99.7% chance it will be in this range."); + for (int i = 0; i < 100; i++) { + real_t n = rng->randfn(5, 10); + CHECK(n >= -45); + CHECK(n <= 55); + } + + INFO("Checks if randfn works with changed seeds."); + INFO("5 std deviations away; above 99.7% chance it will be in this range."); + rng->randomize(); + for (int i = 0; i < 100; i++) { + real_t n = rng->randfn(3, 3); + CHECK(n >= -12); + CHECK(n <= 18); + } +} + +TEST_CASE("[RandomNumberGenerator] Zero for first number immediately after seeding") { + Ref<RandomNumberGenerator> rng = memnew(RandomNumberGenerator); + rng->set_seed(0); + uint32_t n1 = rng->randi(); + uint32_t n2 = rng->randi(); + INFO("Initial random values: ", n1, " ", n2); + CHECK(n1 != 0); + + rng->set_seed(1); + uint32_t n3 = rng->randi(); + uint32_t n4 = rng->randi(); + INFO("Values after changing the seed: ", n3, " ", n4); + CHECK(n3 != 0); +} + +TEST_CASE("[RandomNumberGenerator] Restore state") { + Ref<RandomNumberGenerator> rng = memnew(RandomNumberGenerator); + rng->randomize(); + uint64_t last_seed = rng->get_seed(); + INFO("Current seed: ", last_seed); + + rng->randi(); + rng->randi(); + + CHECK_MESSAGE(rng->get_seed() == last_seed, + "The seed should remain the same after generating some numbers"); + + uint64_t saved_state = rng->get_state(); + INFO("Current state: ", saved_state); + + real_t f1_before = rng->randf(); + real_t f2_before = rng->randf(); + INFO("This seed produces: ", f1_before, " ", f2_before); + + // Restore now. + rng->set_state(saved_state); + + real_t f1_after = rng->randf(); + real_t f2_after = rng->randf(); + INFO("Resetting the state produces: ", f1_after, " ", f2_after); + + String msg = "Should restore the sequence of numbers after resetting the state"; + CHECK_MESSAGE(f1_before == f1_after, msg); + CHECK_MESSAGE(f2_before == f2_after, msg); +} + +TEST_CASE("[RandomNumberGenerator] Restore from seed") { + Ref<RandomNumberGenerator> rng = memnew(RandomNumberGenerator); + rng->set_seed(0); + INFO("Current seed: ", rng->get_seed()); + uint32_t s0_1_before = rng->randi(); + uint32_t s0_2_before = rng->randi(); + INFO("This seed produces: ", s0_1_before, " ", s0_2_before); + + rng->set_seed(9000); + INFO("Current seed: ", rng->get_seed()); + uint32_t s9000_1 = rng->randi(); + uint32_t s9000_2 = rng->randi(); + INFO("This seed produces: ", s9000_1, " ", s9000_2); + + rng->set_seed(0); + INFO("Current seed: ", rng->get_seed()); + uint32_t s0_1_after = rng->randi(); + uint32_t s0_2_after = rng->randi(); + INFO("This seed produces: ", s0_1_after, " ", s0_2_after); + + String msg = "Should restore the sequence of numbers after resetting the seed"; + CHECK_MESSAGE(s0_1_before == s0_1_after, msg); + CHECK_MESSAGE(s0_2_before == s0_2_after, msg); +} + +TEST_CASE_MAY_FAIL("[RandomNumberGenerator] randi_range bias check") { + int zeros = 0; + int ones = 0; + Ref<RandomNumberGenerator> rng = memnew(RandomNumberGenerator); + for (int i = 0; i < 10000; i++) { + int val = rng->randi_range(0, 1); + val == 0 ? zeros++ : ones++; + } + CHECK_MESSAGE(abs(zeros * 1.0 / ones - 1.0) < 0.1, "The ratio of zeros to ones should be nearly 1"); + + int vals[10] = { 0 }; + for (int i = 0; i < 1000000; i++) { + vals[rng->randi_range(0, 9)]++; + } + + for (int i = 0; i < 10; i++) { + CHECK_MESSAGE(abs(vals[i] / 1000000.0 - 0.1) < 0.01, "Each element should appear roughly 10% of the time"); + } +} +} // namespace TestRandomNumberGenerator + +#endif // TEST_RANDOM_NUMBER_GENERATOR_H diff --git a/tests/core/math/test_rect2.h b/tests/core/math/test_rect2.h new file mode 100644 index 0000000000..aabb950461 --- /dev/null +++ b/tests/core/math/test_rect2.h @@ -0,0 +1,575 @@ +/*************************************************************************/ +/* test_rect2.h */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/*************************************************************************/ + +#ifndef TEST_RECT2_H +#define TEST_RECT2_H + +#include "core/math/rect2.h" + +#include "thirdparty/doctest/doctest.h" + +namespace TestRect2 { +// We also test Rect2i here, for consistency with the source code where Rect2 +// and Rect2i are defined in the same file. + +// Rect2 + +TEST_CASE("[Rect2] Constructor methods") { + const Rect2 rect = Rect2(0, 100, 1280, 720); + const Rect2 rect_vector = Rect2(Vector2(0, 100), Vector2(1280, 720)); + const Rect2 rect_copy_rect = Rect2(rect); + const Rect2 rect_copy_recti = Rect2(Rect2i(0, 100, 1280, 720)); + + CHECK_MESSAGE( + rect == rect_vector, + "Rect2s created with the same dimensions but by different methods should be equal."); + CHECK_MESSAGE( + rect == rect_copy_rect, + "Rect2s created with the same dimensions but by different methods should be equal."); + CHECK_MESSAGE( + rect == rect_copy_recti, + "Rect2s created with the same dimensions but by different methods should be equal."); +} + +TEST_CASE("[Rect2] String conversion") { + // Note: This also depends on the Vector2 string representation. + CHECK_MESSAGE( + String(Rect2(0, 100, 1280, 720)) == "[P: (0, 100), S: (1280, 720)]", + "The string representation should match the expected value."); +} + +TEST_CASE("[Rect2] Basic getters") { + const Rect2 rect = Rect2(0, 100, 1280, 720); + CHECK_MESSAGE( + rect.get_position().is_equal_approx(Vector2(0, 100)), + "get_position() should return the expected value."); + CHECK_MESSAGE( + rect.get_size().is_equal_approx(Vector2(1280, 720)), + "get_size() should return the expected value."); + CHECK_MESSAGE( + rect.get_end().is_equal_approx(Vector2(1280, 820)), + "get_end() should return the expected value."); + CHECK_MESSAGE( + rect.get_center().is_equal_approx(Vector2(640, 460)), + "get_center() should return the expected value."); + CHECK_MESSAGE( + Rect2(0, 100, 1281, 721).get_center().is_equal_approx(Vector2(640.5, 460.5)), + "get_center() should return the expected value."); +} + +TEST_CASE("[Rect2] Basic setters") { + Rect2 rect = Rect2(0, 100, 1280, 720); + rect.set_end(Vector2(4000, 4000)); + CHECK_MESSAGE( + rect.is_equal_approx(Rect2(0, 100, 4000, 3900)), + "set_end() should result in the expected Rect2."); + + rect = Rect2(0, 100, 1280, 720); + rect.set_position(Vector2(4000, 4000)); + CHECK_MESSAGE( + rect.is_equal_approx(Rect2(4000, 4000, 1280, 720)), + "set_position() should result in the expected Rect2."); + + rect = Rect2(0, 100, 1280, 720); + rect.set_size(Vector2(4000, 4000)); + CHECK_MESSAGE( + rect.is_equal_approx(Rect2(0, 100, 4000, 4000)), + "set_size() should result in the expected Rect2."); +} + +TEST_CASE("[Rect2] Area getters") { + CHECK_MESSAGE( + Math::is_equal_approx(Rect2(0, 100, 1280, 720).get_area(), 921'600), + "get_area() should return the expected value."); + CHECK_MESSAGE( + Math::is_equal_approx(Rect2(0, 100, -1280, -720).get_area(), 921'600), + "get_area() should return the expected value."); + CHECK_MESSAGE( + Math::is_equal_approx(Rect2(0, 100, 1280, -720).get_area(), -921'600), + "get_area() should return the expected value."); + CHECK_MESSAGE( + Math::is_equal_approx(Rect2(0, 100, -1280, 720).get_area(), -921'600), + "get_area() should return the expected value."); + CHECK_MESSAGE( + Math::is_zero_approx(Rect2(0, 100, 0, 720).get_area()), + "get_area() should return the expected value."); + + CHECK_MESSAGE( + !Rect2(0, 100, 1280, 720).has_no_area(), + "has_no_area() should return the expected value on Rect2 with an area."); + CHECK_MESSAGE( + Rect2(0, 100, 0, 500).has_no_area(), + "has_no_area() should return the expected value on Rect2 with no area."); + CHECK_MESSAGE( + Rect2(0, 100, 500, 0).has_no_area(), + "has_no_area() should return the expected value on Rect2 with no area."); + CHECK_MESSAGE( + Rect2(0, 100, 0, 0).has_no_area(), + "has_no_area() should return the expected value on Rect2 with no area."); +} + +TEST_CASE("[Rect2] Absolute coordinates") { + CHECK_MESSAGE( + Rect2(0, 100, 1280, 720).abs().is_equal_approx(Rect2(0, 100, 1280, 720)), + "abs() should return the expected Rect2."); + CHECK_MESSAGE( + Rect2(0, -100, 1280, 720).abs().is_equal_approx(Rect2(0, -100, 1280, 720)), + "abs() should return the expected Rect2."); + CHECK_MESSAGE( + Rect2(0, -100, -1280, -720).abs().is_equal_approx(Rect2(-1280, -820, 1280, 720)), + "abs() should return the expected Rect2."); + CHECK_MESSAGE( + Rect2(0, 100, -1280, 720).abs().is_equal_approx(Rect2(-1280, 100, 1280, 720)), + "abs() should return the expected Rect2."); +} + +TEST_CASE("[Rect2] Intersection") { + CHECK_MESSAGE( + Rect2(0, 100, 1280, 720).intersection(Rect2(0, 300, 100, 100)).is_equal_approx(Rect2(0, 300, 100, 100)), + "intersection() with fully enclosed Rect2 should return the expected result."); + // The resulting Rect2 is 100 pixels high because the first Rect2 is vertically offset by 100 pixels. + CHECK_MESSAGE( + Rect2(0, 100, 1280, 720).intersection(Rect2(1200, 700, 100, 100)).is_equal_approx(Rect2(1200, 700, 80, 100)), + "intersection() with partially enclosed Rect2 should return the expected result."); + CHECK_MESSAGE( + Rect2(0, 100, 1280, 720).intersection(Rect2(-4000, -4000, 100, 100)).is_equal_approx(Rect2()), + "intersection() with non-enclosed Rect2 should return the expected result."); +} + +TEST_CASE("[Rect2] Enclosing") { + CHECK_MESSAGE( + Rect2(0, 100, 1280, 720).encloses(Rect2(0, 300, 100, 100)), + "encloses() with fully contained Rect2 should return the expected result."); + CHECK_MESSAGE( + !Rect2(0, 100, 1280, 720).encloses(Rect2(1200, 700, 100, 100)), + "encloses() with partially contained Rect2 should return the expected result."); + CHECK_MESSAGE( + !Rect2(0, 100, 1280, 720).encloses(Rect2(-4000, -4000, 100, 100)), + "encloses() with non-contained Rect2 should return the expected result."); +} + +TEST_CASE("[Rect2] Expanding") { + CHECK_MESSAGE( + Rect2(0, 100, 1280, 720).expand(Vector2(500, 600)).is_equal_approx(Rect2(0, 100, 1280, 720)), + "expand() with contained Vector2 should return the expected result."); + CHECK_MESSAGE( + Rect2(0, 100, 1280, 720).expand(Vector2(0, 0)).is_equal_approx(Rect2(0, 0, 1280, 820)), + "expand() with non-contained Vector2 should return the expected result."); +} + +TEST_CASE("[Rect2] Growing") { + CHECK_MESSAGE( + Rect2(0, 100, 1280, 720).grow(100).is_equal_approx(Rect2(-100, 0, 1480, 920)), + "grow() with positive value should return the expected Rect2."); + CHECK_MESSAGE( + Rect2(0, 100, 1280, 720).grow(-100).is_equal_approx(Rect2(100, 200, 1080, 520)), + "grow() with negative value should return the expected Rect2."); + CHECK_MESSAGE( + Rect2(0, 100, 1280, 720).grow(-4000).is_equal_approx(Rect2(4000, 4100, -6720, -7280)), + "grow() with large negative value should return the expected Rect2."); + + CHECK_MESSAGE( + Rect2(0, 100, 1280, 720).grow_individual(100, 200, 300, 400).is_equal_approx(Rect2(-100, -100, 1680, 1320)), + "grow_individual() with positive values should return the expected Rect2."); + CHECK_MESSAGE( + Rect2(0, 100, 1280, 720).grow_individual(-100, 200, 300, -400).is_equal_approx(Rect2(100, -100, 1480, 520)), + "grow_individual() with positive and negative values should return the expected Rect2."); + + CHECK_MESSAGE( + Rect2(0, 100, 1280, 720).grow_side(SIDE_TOP, 500).is_equal_approx(Rect2(0, -400, 1280, 1220)), + "grow_side() with positive value should return the expected Rect2."); + CHECK_MESSAGE( + Rect2(0, 100, 1280, 720).grow_side(SIDE_TOP, -500).is_equal_approx(Rect2(0, 600, 1280, 220)), + "grow_side() with negative value should return the expected Rect2."); +} + +TEST_CASE("[Rect2] Has point") { + Rect2 rect = Rect2(0, 100, 1280, 720); + CHECK_MESSAGE( + rect.has_point(Vector2(500, 600)), + "has_point() with contained Vector2 should return the expected result."); + CHECK_MESSAGE( + !rect.has_point(Vector2(0, 0)), + "has_point() with non-contained Vector2 should return the expected result."); + + CHECK_MESSAGE( + rect.has_point(rect.position), + "has_point() with positive size should include `position`."); + CHECK_MESSAGE( + rect.has_point(rect.position + Vector2(1, 1)), + "has_point() with positive size should include `position + (1, 1)`."); + CHECK_MESSAGE( + !rect.has_point(rect.position + Vector2(1, -1)), + "has_point() with positive size should not include `position + (1, -1)`."); + CHECK_MESSAGE( + !rect.has_point(rect.position + rect.size), + "has_point() with positive size should not include `position + size`."); + CHECK_MESSAGE( + !rect.has_point(rect.position + rect.size + Vector2(1, 1)), + "has_point() with positive size should not include `position + size + (1, 1)`."); + CHECK_MESSAGE( + rect.has_point(rect.position + rect.size + Vector2(-1, -1)), + "has_point() with positive size should include `position + size + (-1, -1)`."); + CHECK_MESSAGE( + !rect.has_point(rect.position + rect.size + Vector2(-1, 1)), + "has_point() with positive size should not include `position + size + (-1, 1)`."); + + CHECK_MESSAGE( + rect.has_point(rect.position + Vector2(0, 10)), + "has_point() with point located on left edge should return true."); + CHECK_MESSAGE( + !rect.has_point(rect.position + Vector2(rect.size.x, 10)), + "has_point() with point located on right edge should return false."); + CHECK_MESSAGE( + rect.has_point(rect.position + Vector2(10, 0)), + "has_point() with point located on top edge should return true."); + CHECK_MESSAGE( + !rect.has_point(rect.position + Vector2(10, rect.size.y)), + "has_point() with point located on bottom edge should return false."); + + /* + // FIXME: Disabled for now until GH-37617 is fixed one way or another. + // More tests should then be written like for the positive size case. + rect = Rect2(0, 100, -1280, -720); + CHECK_MESSAGE( + rect.has_point(rect.position), + "has_point() with negative size should include `position`."); + CHECK_MESSAGE( + !rect.has_point(rect.position + rect.size), + "has_point() with negative size should not include `position + size`."); + */ + + rect = Rect2(-4000, -200, 1280, 720); + CHECK_MESSAGE( + rect.has_point(rect.position + Vector2(0, 10)), + "has_point() with negative position and point located on left edge should return true."); + CHECK_MESSAGE( + !rect.has_point(rect.position + Vector2(rect.size.x, 10)), + "has_point() with negative position and point located on right edge should return false."); + CHECK_MESSAGE( + rect.has_point(rect.position + Vector2(10, 0)), + "has_point() with negative position and point located on top edge should return true."); + CHECK_MESSAGE( + !rect.has_point(rect.position + Vector2(10, rect.size.y)), + "has_point() with negative position and point located on bottom edge should return false."); +} + +TEST_CASE("[Rect2] Intersection") { + CHECK_MESSAGE( + Rect2(0, 100, 1280, 720).intersects(Rect2(0, 300, 100, 100)), + "intersects() with fully enclosed Rect2 should return the expected result."); + CHECK_MESSAGE( + Rect2(0, 100, 1280, 720).intersects(Rect2(1200, 700, 100, 100)), + "intersects() with partially enclosed Rect2 should return the expected result."); + CHECK_MESSAGE( + !Rect2(0, 100, 1280, 720).intersects(Rect2(-4000, -4000, 100, 100)), + "intersects() with non-enclosed Rect2 should return the expected result."); +} + +TEST_CASE("[Rect2] Merging") { + CHECK_MESSAGE( + Rect2(0, 100, 1280, 720).merge(Rect2(0, 300, 100, 100)).is_equal_approx(Rect2(0, 100, 1280, 720)), + "merge() with fully enclosed Rect2 should return the expected result."); + CHECK_MESSAGE( + Rect2(0, 100, 1280, 720).merge(Rect2(1200, 700, 100, 100)).is_equal_approx(Rect2(0, 100, 1300, 720)), + "merge() with partially enclosed Rect2 should return the expected result."); + CHECK_MESSAGE( + Rect2(0, 100, 1280, 720).merge(Rect2(-4000, -4000, 100, 100)).is_equal_approx(Rect2(-4000, -4000, 5280, 4820)), + "merge() with non-enclosed Rect2 should return the expected result."); +} + +// Rect2i + +TEST_CASE("[Rect2i] Constructor methods") { + Rect2i recti = Rect2i(0, 100, 1280, 720); + Rect2i recti_vector = Rect2i(Vector2i(0, 100), Vector2i(1280, 720)); + Rect2i recti_copy_recti = Rect2i(recti); + Rect2i recti_copy_rect = Rect2i(Rect2(0, 100, 1280, 720)); + + CHECK_MESSAGE( + recti == recti_vector, + "Rect2is created with the same dimensions but by different methods should be equal."); + CHECK_MESSAGE( + recti == recti_copy_recti, + "Rect2is created with the same dimensions but by different methods should be equal."); + CHECK_MESSAGE( + recti == recti_copy_rect, + "Rect2is created with the same dimensions but by different methods should be equal."); +} + +TEST_CASE("[Rect2i] String conversion") { + // Note: This also depends on the Vector2 string representation. + CHECK_MESSAGE( + String(Rect2i(0, 100, 1280, 720)) == "[P: (0, 100), S: (1280, 720)]", + "The string representation should match the expected value."); +} + +TEST_CASE("[Rect2i] Basic getters") { + const Rect2i rect = Rect2i(0, 100, 1280, 720); + CHECK_MESSAGE( + rect.get_position() == Vector2i(0, 100), + "get_position() should return the expected value."); + CHECK_MESSAGE( + rect.get_size() == Vector2i(1280, 720), + "get_size() should return the expected value."); + CHECK_MESSAGE( + rect.get_end() == Vector2i(1280, 820), + "get_end() should return the expected value."); + CHECK_MESSAGE( + rect.get_center() == Vector2i(640, 460), + "get_center() should return the expected value."); + CHECK_MESSAGE( + Rect2i(0, 100, 1281, 721).get_center() == Vector2i(640, 460), + "get_center() should return the expected value."); +} + +TEST_CASE("[Rect2i] Basic setters") { + Rect2i rect = Rect2i(0, 100, 1280, 720); + rect.set_end(Vector2i(4000, 4000)); + CHECK_MESSAGE( + rect == Rect2i(0, 100, 4000, 3900), + "set_end() should result in the expected Rect2i."); + + rect = Rect2i(0, 100, 1280, 720); + rect.set_position(Vector2i(4000, 4000)); + CHECK_MESSAGE( + rect == Rect2i(4000, 4000, 1280, 720), + "set_position() should result in the expected Rect2i."); + + rect = Rect2i(0, 100, 1280, 720); + rect.set_size(Vector2i(4000, 4000)); + CHECK_MESSAGE( + rect == Rect2i(0, 100, 4000, 4000), + "set_size() should result in the expected Rect2i."); +} + +TEST_CASE("[Rect2i] Area getters") { + CHECK_MESSAGE( + Rect2i(0, 100, 1280, 720).get_area() == 921'600, + "get_area() should return the expected value."); + CHECK_MESSAGE( + Rect2i(0, 100, -1280, -720).get_area() == 921'600, + "get_area() should return the expected value."); + CHECK_MESSAGE( + Rect2i(0, 100, 1280, -720).get_area() == -921'600, + "get_area() should return the expected value."); + CHECK_MESSAGE( + Rect2i(0, 100, -1280, 720).get_area() == -921'600, + "get_area() should return the expected value."); + CHECK_MESSAGE( + Rect2i(0, 100, 0, 720).get_area() == 0, + "get_area() should return the expected value."); + + CHECK_MESSAGE( + !Rect2i(0, 100, 1280, 720).has_no_area(), + "has_no_area() should return the expected value on Rect2i with an area."); + CHECK_MESSAGE( + Rect2i(0, 100, 0, 500).has_no_area(), + "has_no_area() should return the expected value on Rect2i with no area."); + CHECK_MESSAGE( + Rect2i(0, 100, 500, 0).has_no_area(), + "has_no_area() should return the expected value on Rect2i with no area."); + CHECK_MESSAGE( + Rect2i(0, 100, 0, 0).has_no_area(), + "has_no_area() should return the expected value on Rect2i with no area."); +} + +TEST_CASE("[Rect2i] Absolute coordinates") { + CHECK_MESSAGE( + Rect2i(0, 100, 1280, 720).abs() == Rect2i(0, 100, 1280, 720), + "abs() should return the expected Rect2i."); + CHECK_MESSAGE( + Rect2i(0, -100, 1280, 720).abs() == Rect2i(0, -100, 1280, 720), + "abs() should return the expected Rect2i."); + CHECK_MESSAGE( + Rect2i(0, -100, -1280, -720).abs() == Rect2i(-1280, -820, 1280, 720), + "abs() should return the expected Rect2i."); + CHECK_MESSAGE( + Rect2i(0, 100, -1280, 720).abs() == Rect2i(-1280, 100, 1280, 720), + "abs() should return the expected Rect2i."); +} + +TEST_CASE("[Rect2i] Intersection") { + CHECK_MESSAGE( + Rect2i(0, 100, 1280, 720).intersection(Rect2i(0, 300, 100, 100)) == Rect2i(0, 300, 100, 100), + "intersection() with fully enclosed Rect2i should return the expected result."); + // The resulting Rect2i is 100 pixels high because the first Rect2i is vertically offset by 100 pixels. + CHECK_MESSAGE( + Rect2i(0, 100, 1280, 720).intersection(Rect2i(1200, 700, 100, 100)) == Rect2i(1200, 700, 80, 100), + "intersection() with partially enclosed Rect2i should return the expected result."); + CHECK_MESSAGE( + Rect2i(0, 100, 1280, 720).intersection(Rect2i(-4000, -4000, 100, 100)) == Rect2i(), + "intersection() with non-enclosed Rect2i should return the expected result."); +} + +TEST_CASE("[Rect2i] Enclosing") { + CHECK_MESSAGE( + Rect2i(0, 100, 1280, 720).encloses(Rect2i(0, 300, 100, 100)), + "encloses() with fully contained Rect2i should return the expected result."); + CHECK_MESSAGE( + !Rect2i(0, 100, 1280, 720).encloses(Rect2i(1200, 700, 100, 100)), + "encloses() with partially contained Rect2i should return the expected result."); + CHECK_MESSAGE( + !Rect2i(0, 100, 1280, 720).encloses(Rect2i(-4000, -4000, 100, 100)), + "encloses() with non-contained Rect2i should return the expected result."); +} + +TEST_CASE("[Rect2i] Expanding") { + CHECK_MESSAGE( + Rect2i(0, 100, 1280, 720).expand(Vector2i(500, 600)) == Rect2i(0, 100, 1280, 720), + "expand() with contained Vector2i should return the expected result."); + CHECK_MESSAGE( + Rect2i(0, 100, 1280, 720).expand(Vector2i(0, 0)) == Rect2i(0, 0, 1280, 820), + "expand() with non-contained Vector2i should return the expected result."); +} + +TEST_CASE("[Rect2i] Growing") { + CHECK_MESSAGE( + Rect2i(0, 100, 1280, 720).grow(100) == Rect2i(-100, 0, 1480, 920), + "grow() with positive value should return the expected Rect2i."); + CHECK_MESSAGE( + Rect2i(0, 100, 1280, 720).grow(-100) == Rect2i(100, 200, 1080, 520), + "grow() with negative value should return the expected Rect2i."); + CHECK_MESSAGE( + Rect2i(0, 100, 1280, 720).grow(-4000) == Rect2i(4000, 4100, -6720, -7280), + "grow() with large negative value should return the expected Rect2i."); + + CHECK_MESSAGE( + Rect2i(0, 100, 1280, 720).grow_individual(100, 200, 300, 400) == Rect2i(-100, -100, 1680, 1320), + "grow_individual() with positive values should return the expected Rect2i."); + CHECK_MESSAGE( + Rect2i(0, 100, 1280, 720).grow_individual(-100, 200, 300, -400) == Rect2i(100, -100, 1480, 520), + "grow_individual() with positive and negative values should return the expected Rect2i."); + + CHECK_MESSAGE( + Rect2i(0, 100, 1280, 720).grow_side(SIDE_TOP, 500) == Rect2i(0, -400, 1280, 1220), + "grow_side() with positive value should return the expected Rect2i."); + CHECK_MESSAGE( + Rect2i(0, 100, 1280, 720).grow_side(SIDE_TOP, -500) == Rect2i(0, 600, 1280, 220), + "grow_side() with negative value should return the expected Rect2i."); +} + +TEST_CASE("[Rect2i] Has point") { + Rect2i rect = Rect2i(0, 100, 1280, 720); + CHECK_MESSAGE( + rect.has_point(Vector2i(500, 600)), + "has_point() with contained Vector2i should return the expected result."); + CHECK_MESSAGE( + !rect.has_point(Vector2i(0, 0)), + "has_point() with non-contained Vector2i should return the expected result."); + + CHECK_MESSAGE( + rect.has_point(rect.position), + "has_point() with positive size should include `position`."); + CHECK_MESSAGE( + rect.has_point(rect.position + Vector2i(1, 1)), + "has_point() with positive size should include `position + (1, 1)`."); + CHECK_MESSAGE( + !rect.has_point(rect.position + Vector2i(1, -1)), + "has_point() with positive size should not include `position + (1, -1)`."); + CHECK_MESSAGE( + !rect.has_point(rect.position + rect.size), + "has_point() with positive size should not include `position + size`."); + CHECK_MESSAGE( + !rect.has_point(rect.position + rect.size + Vector2i(1, 1)), + "has_point() with positive size should not include `position + size + (1, 1)`."); + CHECK_MESSAGE( + rect.has_point(rect.position + rect.size + Vector2i(-1, -1)), + "has_point() with positive size should include `position + size + (-1, -1)`."); + CHECK_MESSAGE( + !rect.has_point(rect.position + rect.size + Vector2i(-1, 1)), + "has_point() with positive size should not include `position + size + (-1, 1)`."); + + CHECK_MESSAGE( + rect.has_point(rect.position + Vector2i(0, 10)), + "has_point() with point located on left edge should return true."); + CHECK_MESSAGE( + !rect.has_point(rect.position + Vector2i(rect.size.x, 10)), + "has_point() with point located on right edge should return false."); + CHECK_MESSAGE( + rect.has_point(rect.position + Vector2i(10, 0)), + "has_point() with point located on top edge should return true."); + CHECK_MESSAGE( + !rect.has_point(rect.position + Vector2i(10, rect.size.y)), + "has_point() with point located on bottom edge should return false."); + + /* + // FIXME: Disabled for now until GH-37617 is fixed one way or another. + // More tests should then be written like for the positive size case. + rect = Rect2i(0, 100, -1280, -720); + CHECK_MESSAGE( + rect.has_point(rect.position), + "has_point() with negative size should include `position`."); + CHECK_MESSAGE( + !rect.has_point(rect.position + rect.size), + "has_point() with negative size should not include `position + size`."); + */ + + rect = Rect2i(-4000, -200, 1280, 720); + CHECK_MESSAGE( + rect.has_point(rect.position + Vector2i(0, 10)), + "has_point() with negative position and point located on left edge should return true."); + CHECK_MESSAGE( + !rect.has_point(rect.position + Vector2i(rect.size.x, 10)), + "has_point() with negative position and point located on right edge should return false."); + CHECK_MESSAGE( + rect.has_point(rect.position + Vector2i(10, 0)), + "has_point() with negative position and point located on top edge should return true."); + CHECK_MESSAGE( + !rect.has_point(rect.position + Vector2i(10, rect.size.y)), + "has_point() with negative position and point located on bottom edge should return false."); +} + +TEST_CASE("[Rect2i] Intersection") { + CHECK_MESSAGE( + Rect2i(0, 100, 1280, 720).intersects(Rect2i(0, 300, 100, 100)), + "intersects() with fully enclosed Rect2i should return the expected result."); + CHECK_MESSAGE( + Rect2i(0, 100, 1280, 720).intersects(Rect2i(1200, 700, 100, 100)), + "intersects() with partially enclosed Rect2i should return the expected result."); + CHECK_MESSAGE( + !Rect2i(0, 100, 1280, 720).intersects(Rect2i(-4000, -4000, 100, 100)), + "intersects() with non-enclosed Rect2i should return the expected result."); +} + +TEST_CASE("[Rect2i] Merging") { + CHECK_MESSAGE( + Rect2i(0, 100, 1280, 720).merge(Rect2i(0, 300, 100, 100)) == Rect2i(0, 100, 1280, 720), + "merge() with fully enclosed Rect2i should return the expected result."); + CHECK_MESSAGE( + Rect2i(0, 100, 1280, 720).merge(Rect2i(1200, 700, 100, 100)) == Rect2i(0, 100, 1300, 720), + "merge() with partially enclosed Rect2i should return the expected result."); + CHECK_MESSAGE( + Rect2i(0, 100, 1280, 720).merge(Rect2i(-4000, -4000, 100, 100)) == Rect2i(-4000, -4000, 5280, 4820), + "merge() with non-enclosed Rect2i should return the expected result."); +} +} // namespace TestRect2 + +#endif // TEST_RECT2_H |