Path: blob/master/tests/core/math/test_geometry_2d.h
10278 views
/**************************************************************************/1/* test_geometry_2d.h */2/**************************************************************************/3/* This file is part of: */4/* GODOT ENGINE */5/* https://godotengine.org */6/**************************************************************************/7/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */8/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */9/* */10/* Permission is hereby granted, free of charge, to any person obtaining */11/* a copy of this software and associated documentation files (the */12/* "Software"), to deal in the Software without restriction, including */13/* without limitation the rights to use, copy, modify, merge, publish, */14/* distribute, sublicense, and/or sell copies of the Software, and to */15/* permit persons to whom the Software is furnished to do so, subject to */16/* the following conditions: */17/* */18/* The above copyright notice and this permission notice shall be */19/* included in all copies or substantial portions of the Software. */20/* */21/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */22/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */23/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */24/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */25/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */26/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */27/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */28/**************************************************************************/2930#pragma once3132#include "core/math/geometry_2d.h"3334#include "thirdparty/doctest/doctest.h"3536namespace TestGeometry2D {3738TEST_CASE("[Geometry2D] Point in circle") {39CHECK(Geometry2D::is_point_in_circle(Vector2(0, 0), Vector2(0, 0), 1.0));4041CHECK(Geometry2D::is_point_in_circle(Vector2(0, 0), Vector2(11.99, 0), 12));42CHECK(Geometry2D::is_point_in_circle(Vector2(-11.99, 0), Vector2(0, 0), 12));4344CHECK_FALSE(Geometry2D::is_point_in_circle(Vector2(0, 0), Vector2(12.01, 0), 12));45CHECK_FALSE(Geometry2D::is_point_in_circle(Vector2(-12.01, 0), Vector2(0, 0), 12));4647CHECK(Geometry2D::is_point_in_circle(Vector2(7, -42), Vector2(4, -40), 3.7));48CHECK_FALSE(Geometry2D::is_point_in_circle(Vector2(7, -42), Vector2(4, -40), 3.5));4950// This tests points on the edge of the circle. They are treated as being inside the circle.51CHECK(Geometry2D::is_point_in_circle(Vector2(1.0, 0.0), Vector2(0, 0), 1.0));52CHECK(Geometry2D::is_point_in_circle(Vector2(0.0, -1.0), Vector2(0, 0), 1.0));53}5455TEST_CASE("[Geometry2D] Point in triangle") {56CHECK(Geometry2D::is_point_in_triangle(Vector2(0, 0), Vector2(-1, 1), Vector2(0, -1), Vector2(1, 1)));57CHECK_FALSE(Geometry2D::is_point_in_triangle(Vector2(-1.01, 1.0), Vector2(-1, 1), Vector2(0, -1), Vector2(1, 1)));5859CHECK(Geometry2D::is_point_in_triangle(Vector2(3, 2.5), Vector2(1, 4), Vector2(3, 2), Vector2(5, 4)));60CHECK(Geometry2D::is_point_in_triangle(Vector2(-3, -2.5), Vector2(-1, -4), Vector2(-3, -2), Vector2(-5, -4)));61CHECK_FALSE(Geometry2D::is_point_in_triangle(Vector2(0, 0), Vector2(1, 4), Vector2(3, 2), Vector2(5, 4)));6263// This tests points on the edge of the triangle. They are treated as being outside the triangle.64// In `is_point_in_circle` and `is_point_in_polygon` they are treated as being inside, so in order the make65// the behavior consistent this may change in the future (see issue #44717 and PR #44274).66CHECK_FALSE(Geometry2D::is_point_in_triangle(Vector2(1, 1), Vector2(-1, 1), Vector2(0, -1), Vector2(1, 1)));67CHECK_FALSE(Geometry2D::is_point_in_triangle(Vector2(0, 1), Vector2(-1, 1), Vector2(0, -1), Vector2(1, 1)));68}6970TEST_CASE("[Geometry2D] Point in polygon") {71Vector<Vector2> p;72CHECK_FALSE(Geometry2D::is_point_in_polygon(Vector2(0, 0), p));7374p.push_back(Vector2(-88, 120));75p.push_back(Vector2(-74, -38));76p.push_back(Vector2(135, -145));77p.push_back(Vector2(425, 70));78p.push_back(Vector2(68, 112));79p.push_back(Vector2(-120, 370));80p.push_back(Vector2(-323, -145));81CHECK_FALSE(Geometry2D::is_point_in_polygon(Vector2(-350, 0), p));82CHECK_FALSE(Geometry2D::is_point_in_polygon(Vector2(-110, 60), p));83CHECK_FALSE(Geometry2D::is_point_in_polygon(Vector2(412, 96), p));84CHECK_FALSE(Geometry2D::is_point_in_polygon(Vector2(83, 130), p));85CHECK_FALSE(Geometry2D::is_point_in_polygon(Vector2(-320, -153), p));8687CHECK(Geometry2D::is_point_in_polygon(Vector2(0, 0), p));88CHECK(Geometry2D::is_point_in_polygon(Vector2(-230, 0), p));89CHECK(Geometry2D::is_point_in_polygon(Vector2(130, -110), p));90CHECK(Geometry2D::is_point_in_polygon(Vector2(370, 55), p));91CHECK(Geometry2D::is_point_in_polygon(Vector2(-160, 190), p));9293// This tests points on the edge of the polygon. They are treated as being inside the polygon.94int c = p.size();95for (int i = 0; i < c; i++) {96const Vector2 &p1 = p[i];97CHECK(Geometry2D::is_point_in_polygon(p1, p));9899const Vector2 &p2 = p[(i + 1) % c];100Vector2 midpoint((p1 + p2) * 0.5);101CHECK(Geometry2D::is_point_in_polygon(midpoint, p));102}103}104105TEST_CASE("[Geometry2D] Polygon clockwise") {106Vector<Vector2> p;107CHECK_FALSE(Geometry2D::is_polygon_clockwise(p));108109p.push_back(Vector2(5, -5));110p.push_back(Vector2(-1, -5));111p.push_back(Vector2(-5, -1));112p.push_back(Vector2(-1, 3));113p.push_back(Vector2(1, 5));114CHECK(Geometry2D::is_polygon_clockwise(p));115116p.reverse();117CHECK_FALSE(Geometry2D::is_polygon_clockwise(p));118}119120TEST_CASE("[Geometry2D] Line intersection") {121Vector2 r;122CHECK(Geometry2D::line_intersects_line(Vector2(2, 0), Vector2(0, 1), Vector2(0, 2), Vector2(1, 0), r));123CHECK(r.is_equal_approx(Vector2(2, 2)));124125CHECK(Geometry2D::line_intersects_line(Vector2(-1, 1), Vector2(1, -1), Vector2(4, 1), Vector2(-1, -1), r));126CHECK(r.is_equal_approx(Vector2(1.5, -1.5)));127128CHECK(Geometry2D::line_intersects_line(Vector2(-1, 0), Vector2(-1, -1), Vector2(1, 0), Vector2(1, -1), r));129CHECK(r.is_equal_approx(Vector2(0, 1)));130131CHECK_FALSE_MESSAGE(132Geometry2D::line_intersects_line(Vector2(-1, 1), Vector2(1, -1), Vector2(0, 1), Vector2(1, -1), r),133"Parallel lines should not intersect.");134}135136TEST_CASE("[Geometry2D] Segment intersection") {137Vector2 r;138139CHECK(Geometry2D::segment_intersects_segment(Vector2(-1, 1), Vector2(1, -1), Vector2(1, 1), Vector2(-1, -1), &r));140CHECK(r.is_equal_approx(Vector2(0, 0)));141142CHECK_FALSE(Geometry2D::segment_intersects_segment(Vector2(-1, 1), Vector2(1, -1), Vector2(1, 1), Vector2(0.1, 0.1), &r));143CHECK_FALSE(Geometry2D::segment_intersects_segment(Vector2(-1, 1), Vector2(1, -1), Vector2(0.1, 0.1), Vector2(1, 1), &r));144145CHECK_FALSE_MESSAGE(146Geometry2D::segment_intersects_segment(Vector2(-1, 1), Vector2(1, -1), Vector2(0, 1), Vector2(2, -1), &r),147"Parallel segments should not intersect.");148149CHECK_FALSE_MESSAGE(150Geometry2D::segment_intersects_segment(Vector2(1, 2), Vector2(3, 2), Vector2(0, 2), Vector2(-2, 2), &r),151"Non-overlapping collinear segments should not intersect.");152153CHECK_MESSAGE(154Geometry2D::segment_intersects_segment(Vector2(0, 0), Vector2(0, 1), Vector2(0, 0), Vector2(1, 0), &r),155"Touching segments should intersect.");156CHECK(r.is_equal_approx(Vector2(0, 0)));157158CHECK_MESSAGE(159Geometry2D::segment_intersects_segment(Vector2(0, 1), Vector2(0, 0), Vector2(0, 0), Vector2(1, 0), &r),160"Touching segments should intersect.");161CHECK(r.is_equal_approx(Vector2(0, 0)));162}163164TEST_CASE("[Geometry2D] Segment intersection with circle") {165constexpr real_t minus_one = -1.0;166constexpr real_t zero = 0.0;167constexpr real_t one_quarter = 0.25;168constexpr real_t three_quarters = 0.75;169constexpr real_t one = 1.0;170171CHECK_MESSAGE(172Geometry2D::segment_intersects_circle(Vector2(0, 0), Vector2(4, 0), Vector2(0, 0), 1.0) == doctest::Approx(one_quarter),173"Segment from inside to outside of circle should intersect it.");174CHECK_MESSAGE(175Geometry2D::segment_intersects_circle(Vector2(4, 0), Vector2(0, 0), Vector2(0, 0), 1.0) == doctest::Approx(three_quarters),176"Segment from outside to inside of circle should intersect it.");177178CHECK_MESSAGE(179Geometry2D::segment_intersects_circle(Vector2(-2, 0), Vector2(2, 0), Vector2(0, 0), 1.0) == doctest::Approx(one_quarter),180"Segment running through circle should intersect it.");181CHECK_MESSAGE(182Geometry2D::segment_intersects_circle(Vector2(2, 0), Vector2(-2, 0), Vector2(0, 0), 1.0) == doctest::Approx(one_quarter),183"Segment running through circle should intersect it.");184185CHECK_MESSAGE(186Geometry2D::segment_intersects_circle(Vector2(0, 0), Vector2(1, 0), Vector2(0, 0), 1.0) == doctest::Approx(one),187"Segment starting inside the circle and ending on the circle should intersect it");188CHECK_MESSAGE(189Geometry2D::segment_intersects_circle(Vector2(1, 0), Vector2(0, 0), Vector2(0, 0), 1.0) == doctest::Approx(zero),190"Segment starting on the circle and going inwards should intersect it");191CHECK_MESSAGE(192Geometry2D::segment_intersects_circle(Vector2(1, 0), Vector2(2, 0), Vector2(0, 0), 1.0) == doctest::Approx(zero),193"Segment starting on the circle and going outwards should intersect it");194CHECK_MESSAGE(195Geometry2D::segment_intersects_circle(Vector2(2, 0), Vector2(1, 0), Vector2(0, 0), 1.0) == doctest::Approx(one),196"Segment starting outside the circle and ending on the circle intersect it");197198CHECK_MESSAGE(199Geometry2D::segment_intersects_circle(Vector2(-1, 0), Vector2(1, 0), Vector2(0, 0), 2.0) == doctest::Approx(minus_one),200"Segment completely within the circle should not intersect it");201CHECK_MESSAGE(202Geometry2D::segment_intersects_circle(Vector2(1, 0), Vector2(-1, 0), Vector2(0, 0), 2.0) == doctest::Approx(minus_one),203"Segment completely within the circle should not intersect it");204CHECK_MESSAGE(205Geometry2D::segment_intersects_circle(Vector2(2, 0), Vector2(3, 0), Vector2(0, 0), 1.0) == doctest::Approx(minus_one),206"Segment completely outside the circle should not intersect it");207CHECK_MESSAGE(208Geometry2D::segment_intersects_circle(Vector2(3, 0), Vector2(2, 0), Vector2(0, 0), 1.0) == doctest::Approx(minus_one),209"Segment completely outside the circle should not intersect it");210}211212TEST_CASE("[Geometry2D] Segment intersection with polygon") {213Vector<Point2> a;214215a.push_back(Point2(-2, 2));216a.push_back(Point2(3, 4));217a.push_back(Point2(1, 1));218a.push_back(Point2(2, -2));219a.push_back(Point2(-1, -1));220221CHECK_MESSAGE(222Geometry2D::is_segment_intersecting_polygon(Vector2(0, 2), Vector2(2, 2), a),223"Segment from inside to outside of polygon should intersect it.");224CHECK_MESSAGE(225Geometry2D::is_segment_intersecting_polygon(Vector2(2, 2), Vector2(0, 2), a),226"Segment from outside to inside of polygon should intersect it.");227228CHECK_MESSAGE(229Geometry2D::is_segment_intersecting_polygon(Vector2(2, 4), Vector2(3, 3), a),230"Segment running through polygon should intersect it.");231CHECK_MESSAGE(232Geometry2D::is_segment_intersecting_polygon(Vector2(3, 3), Vector2(2, 4), a),233"Segment running through polygon should intersect it.");234235CHECK_MESSAGE(236Geometry2D::is_segment_intersecting_polygon(Vector2(0, 0), Vector2(1, 1), a),237"Segment starting inside the polygon and ending on the polygon should intersect it");238CHECK_MESSAGE(239Geometry2D::is_segment_intersecting_polygon(Vector2(1, 1), Vector2(0, 0), a),240"Segment starting on the polygon and going inwards should intersect it");241CHECK_MESSAGE(242Geometry2D::is_segment_intersecting_polygon(Vector2(-2, 2), Vector2(-2, -1), a),243"Segment starting on the polygon and going outwards should intersect it");244CHECK_MESSAGE(245Geometry2D::is_segment_intersecting_polygon(Vector2(-2, 1), Vector2(-2, 2), a),246"Segment starting outside the polygon and ending on the polygon intersect it");247248CHECK_FALSE_MESSAGE(249Geometry2D::is_segment_intersecting_polygon(Vector2(-1, 2), Vector2(1, -1), a),250"Segment completely within the polygon should not intersect it");251CHECK_FALSE_MESSAGE(252Geometry2D::is_segment_intersecting_polygon(Vector2(1, -1), Vector2(-1, 2), a),253"Segment completely within the polygon should not intersect it");254CHECK_FALSE_MESSAGE(255Geometry2D::is_segment_intersecting_polygon(Vector2(2, 2), Vector2(2, -1), a),256"Segment completely outside the polygon should not intersect it");257CHECK_FALSE_MESSAGE(258Geometry2D::is_segment_intersecting_polygon(Vector2(2, -1), Vector2(2, 2), a),259"Segment completely outside the polygon should not intersect it");260}261262TEST_CASE("[Geometry2D] Closest point to segment") {263Vector2 a = Vector2(-4, -4);264Vector2 b = Vector2(4, 4);265CHECK(Geometry2D::get_closest_point_to_segment(Vector2(4.1, 4.1), a, b).is_equal_approx(Vector2(4, 4)));266CHECK(Geometry2D::get_closest_point_to_segment(Vector2(-4.1, -4.1), a, b).is_equal_approx(Vector2(-4, -4)));267CHECK(Geometry2D::get_closest_point_to_segment(Vector2(-1, 1), a, b).is_equal_approx(Vector2(0, 0)));268269a = Vector2(1, -2);270b = Vector2(1, -2);271CHECK_MESSAGE(272Geometry2D::get_closest_point_to_segment(Vector2(-3, 4), a, b).is_equal_approx(Vector2(1, -2)),273"Line segment is only a single point. This point should be the closest.");274}275276TEST_CASE("[Geometry2D] Closest point to uncapped segment") {277constexpr Vector2 a = Vector2(-4, -4);278constexpr Vector2 b = Vector2(4, 4);279CHECK(Geometry2D::get_closest_point_to_segment_uncapped(Vector2(-1, 1), a, b).is_equal_approx(Vector2(0, 0)));280CHECK(Geometry2D::get_closest_point_to_segment_uncapped(Vector2(-4, -6), a, b).is_equal_approx(Vector2(-5, -5)));281CHECK(Geometry2D::get_closest_point_to_segment_uncapped(Vector2(4, 6), a, b).is_equal_approx(Vector2(5, 5)));282}283284TEST_CASE("[Geometry2D] Closest points between segments") {285Vector2 c1, c2;286// Basis Path Testing suite287SUBCASE("[Geometry2D] Both segments degenerate to a point") {288Geometry2D::get_closest_points_between_segments(Vector2(0, 0), Vector2(0, 0), Vector2(0, 0), Vector2(0, 0), c1, c2);289CHECK(c1.is_equal_approx(Vector2(0, 0)));290CHECK(c2.is_equal_approx(Vector2(0, 0)));291}292293SUBCASE("[Geometry2D] Closest point on second segment trajectory is above [0,1]") {294Geometry2D::get_closest_points_between_segments(Vector2(50, -25), Vector2(50, -10), Vector2(-50, 10), Vector2(-40, 10), c1, c2);295CHECK(c1.is_equal_approx(Vector2(50, -10)));296CHECK(c2.is_equal_approx(Vector2(-40, 10)));297}298299SUBCASE("[Geometry2D] Parallel segments") {300Geometry2D::get_closest_points_between_segments(Vector2(2, 1), Vector2(4, 3), Vector2(2, 3), Vector2(4, 5), c1, c2);301CHECK(c1.is_equal_approx(Vector2(3, 2)));302CHECK(c2.is_equal_approx(Vector2(2, 3)));303}304305SUBCASE("[Geometry2D] Closest point on second segment trajectory is within [0,1]") {306Geometry2D::get_closest_points_between_segments(Vector2(2, 4), Vector2(2, 3), Vector2(1, 1), Vector2(4, 4), c1, c2);307CHECK(c1.is_equal_approx(Vector2(2, 3)));308CHECK(c2.is_equal_approx(Vector2(2.5, 2.5)));309}310311SUBCASE("[Geometry2D] Closest point on second segment trajectory is below [0,1]") {312Geometry2D::get_closest_points_between_segments(Vector2(-20, -20), Vector2(-10, -40), Vector2(10, 25), Vector2(25, 40), c1, c2);313CHECK(c1.is_equal_approx(Vector2(-20, -20)));314CHECK(c2.is_equal_approx(Vector2(10, 25)));315}316317SUBCASE("[Geometry2D] Second segment degenerates to a point") {318Geometry2D::get_closest_points_between_segments(Vector2(1, 2), Vector2(2, 1), Vector2(3, 3), Vector2(3, 3), c1, c2);319CHECK(c1.is_equal_approx(Vector2(1.5, 1.5)));320CHECK(c2.is_equal_approx(Vector2(3, 3)));321}322323SUBCASE("[Geometry2D] First segment degenerates to a point") {324Geometry2D::get_closest_points_between_segments(Vector2(1, 1), Vector2(1, 1), Vector2(2, 2), Vector2(4, 4), c1, c2);325CHECK(c1.is_equal_approx(Vector2(1, 1)));326CHECK(c2.is_equal_approx(Vector2(2, 2)));327}328// End Basis Path Testing suite329330SUBCASE("[Geometry2D] Segments are equal vectors") {331Geometry2D::get_closest_points_between_segments(Vector2(2, 2), Vector2(3, 3), Vector2(4, 4), Vector2(4, 5), c1, c2);332CHECK(c1.is_equal_approx(Vector2(3, 3)));333CHECK(c2.is_equal_approx(Vector2(4, 4)));334}335336SUBCASE("[Geometry2D] Standard case") {337Geometry2D::get_closest_points_between_segments(Vector2(0, 1), Vector2(-2, -1), Vector2(0, 0), Vector2(2, -2), c1, c2);338CHECK(c1.is_equal_approx(Vector2(-0.5, 0.5)));339CHECK(c2.is_equal_approx(Vector2(0, 0)));340}341342SUBCASE("[Geometry2D] Segments intersect") {343Geometry2D::get_closest_points_between_segments(Vector2(-1, 1), Vector2(1, -1), Vector2(1, 1), Vector2(-1, -1), c1, c2);344CHECK(c1.is_equal_approx(Vector2(0, 0)));345CHECK(c2.is_equal_approx(Vector2(0, 0)));346}347}348349TEST_CASE("[Geometry2D] Make atlas") {350Vector<Point2i> result;351Size2i size;352353Vector<Size2i> r;354r.push_back(Size2i(2, 2));355Geometry2D::make_atlas(r, result, size);356CHECK(size == Size2i(2, 2));357CHECK(result.size() == r.size());358359r.clear();360result.clear();361r.push_back(Size2i(1, 2));362r.push_back(Size2i(3, 4));363r.push_back(Size2i(5, 6));364r.push_back(Size2i(7, 8));365Geometry2D::make_atlas(r, result, size);366CHECK(result.size() == r.size());367}368369TEST_CASE("[Geometry2D] Polygon intersection") {370Vector<Point2> a;371Vector<Point2> b;372Vector<Vector<Point2>> r;373374a.push_back(Point2(30, 60));375a.push_back(Point2(70, 5));376a.push_back(Point2(200, 40));377a.push_back(Point2(80, 200));378379SUBCASE("[Geometry2D] Both polygons are empty") {380r = Geometry2D::intersect_polygons(Vector<Point2>(), Vector<Point2>());381CHECK_MESSAGE(r.is_empty(), "Both polygons are empty. The intersection should also be empty.");382}383384SUBCASE("[Geometry2D] One polygon is empty") {385r = Geometry2D::intersect_polygons(a, b);386REQUIRE_MESSAGE(r.is_empty(), "One polygon is empty. The intersection should also be empty.");387}388389SUBCASE("[Geometry2D] Basic intersection") {390b.push_back(Point2(200, 300));391b.push_back(Point2(90, 200));392b.push_back(Point2(50, 100));393b.push_back(Point2(200, 90));394r = Geometry2D::intersect_polygons(a, b);395REQUIRE_MESSAGE(r.size() == 1, "The polygons should intersect each other with 1 resulting intersection polygon.");396REQUIRE_MESSAGE(r[0].size() == 3, "The resulting intersection polygon should have 3 vertices.");397CHECK(r[0][0].is_equal_approx(Point2(86.52174, 191.30436)));398CHECK(r[0][1].is_equal_approx(Point2(50, 100)));399CHECK(r[0][2].is_equal_approx(Point2(160.52632, 92.63157)));400}401402SUBCASE("[Geometry2D] Intersection with one polygon being completely inside the other polygon") {403b.push_back(Point2(80, 100));404b.push_back(Point2(50, 50));405b.push_back(Point2(150, 50));406r = Geometry2D::intersect_polygons(a, b);407REQUIRE_MESSAGE(r.size() == 1, "The polygons should intersect each other with 1 resulting intersection polygon.");408REQUIRE_MESSAGE(r[0].size() == 3, "The resulting intersection polygon should have 3 vertices.");409CHECK(r[0][0].is_equal_approx(b[0]));410CHECK(r[0][1].is_equal_approx(b[1]));411CHECK(r[0][2].is_equal_approx(b[2]));412}413414SUBCASE("[Geometry2D] No intersection with 2 non-empty polygons") {415b.push_back(Point2(150, 150));416b.push_back(Point2(250, 100));417b.push_back(Point2(300, 200));418r = Geometry2D::intersect_polygons(a, b);419REQUIRE_MESSAGE(r.is_empty(), "The polygons should not intersect each other.");420}421422SUBCASE("[Geometry2D] Intersection with 2 resulting polygons") {423a.clear();424a.push_back(Point2(70, 5));425a.push_back(Point2(140, 7));426a.push_back(Point2(100, 52));427a.push_back(Point2(170, 50));428a.push_back(Point2(60, 125));429b.push_back(Point2(70, 105));430b.push_back(Point2(115, 55));431b.push_back(Point2(90, 15));432b.push_back(Point2(160, 50));433r = Geometry2D::intersect_polygons(a, b);434REQUIRE_MESSAGE(r.size() == 2, "The polygons should intersect each other with 2 resulting intersection polygons.");435REQUIRE_MESSAGE(r[0].size() == 4, "The resulting intersection polygon should have 4 vertices.");436CHECK(r[0][0].is_equal_approx(Point2(70, 105)));437CHECK(r[0][1].is_equal_approx(Point2(115, 55)));438CHECK(r[0][2].is_equal_approx(Point2(112.894737, 51.63158)));439CHECK(r[0][3].is_equal_approx(Point2(159.509537, 50.299728)));440441REQUIRE_MESSAGE(r[1].size() == 3, "The intersection polygon should have 3 vertices.");442CHECK(r[1][0].is_equal_approx(Point2(119.692307, 29.846149)));443CHECK(r[1][1].is_equal_approx(Point2(107.706421, 43.33028)));444CHECK(r[1][2].is_equal_approx(Point2(90, 15)));445}446}447448TEST_CASE("[Geometry2D] Merge polygons") {449Vector<Point2> a;450Vector<Point2> b;451Vector<Vector<Point2>> r;452453a.push_back(Point2(225, 180));454a.push_back(Point2(160, 230));455a.push_back(Point2(20, 212));456a.push_back(Point2(50, 115));457458SUBCASE("[Geometry2D] Both polygons are empty") {459r = Geometry2D::merge_polygons(Vector<Point2>(), Vector<Point2>());460REQUIRE_MESSAGE(r.is_empty(), "Both polygons are empty. The union should also be empty.");461}462463SUBCASE("[Geometry2D] One polygon is empty") {464r = Geometry2D::merge_polygons(a, b);465REQUIRE_MESSAGE(r.size() == 1, "One polygon is non-empty. There should be 1 resulting merged polygon.");466REQUIRE_MESSAGE(r[0].size() == 4, "The resulting merged polygon should have 4 vertices.");467CHECK(r[0][0].is_equal_approx(a[0]));468CHECK(r[0][1].is_equal_approx(a[1]));469CHECK(r[0][2].is_equal_approx(a[2]));470CHECK(r[0][3].is_equal_approx(a[3]));471}472473SUBCASE("[Geometry2D] Basic merge with 2 polygons") {474b.push_back(Point2(180, 190));475b.push_back(Point2(60, 140));476b.push_back(Point2(160, 80));477r = Geometry2D::merge_polygons(a, b);478REQUIRE_MESSAGE(r.size() == 1, "The merged polygons should result in 1 polygon.");479REQUIRE_MESSAGE(r[0].size() == 7, "The resulting merged polygon should have 7 vertices.");480CHECK(r[0][0].is_equal_approx(Point2(174.791077, 161.350967)));481CHECK(r[0][1].is_equal_approx(Point2(225, 180)));482CHECK(r[0][2].is_equal_approx(Point2(160, 230)));483CHECK(r[0][3].is_equal_approx(Point2(20, 212)));484CHECK(r[0][4].is_equal_approx(Point2(50, 115)));485CHECK(r[0][5].is_equal_approx(Point2(81.911758, 126.852943)));486CHECK(r[0][6].is_equal_approx(Point2(160, 80)));487}488489SUBCASE("[Geometry2D] Merge with 2 resulting merged polygons (outline and hole)") {490b.push_back(Point2(180, 190));491b.push_back(Point2(140, 125));492b.push_back(Point2(60, 140));493b.push_back(Point2(160, 80));494r = Geometry2D::merge_polygons(a, b);495REQUIRE_MESSAGE(r.size() == 2, "The merged polygons should result in 2 polygons.");496497REQUIRE_MESSAGE(!Geometry2D::is_polygon_clockwise(r[0]), "The merged polygon (outline) should be counter-clockwise.");498REQUIRE_MESSAGE(r[0].size() == 7, "The resulting merged polygon (outline) should have 7 vertices.");499CHECK(r[0][0].is_equal_approx(Point2(174.791077, 161.350967)));500CHECK(r[0][1].is_equal_approx(Point2(225, 180)));501CHECK(r[0][2].is_equal_approx(Point2(160, 230)));502CHECK(r[0][3].is_equal_approx(Point2(20, 212)));503CHECK(r[0][4].is_equal_approx(Point2(50, 115)));504CHECK(r[0][5].is_equal_approx(Point2(81.911758, 126.852943)));505CHECK(r[0][6].is_equal_approx(Point2(160, 80)));506507REQUIRE_MESSAGE(Geometry2D::is_polygon_clockwise(r[1]), "The resulting merged polygon (hole) should be clockwise.");508REQUIRE_MESSAGE(r[1].size() == 3, "The resulting merged polygon (hole) should have 3 vertices.");509CHECK(r[1][0].is_equal_approx(Point2(98.083069, 132.859421)));510CHECK(r[1][1].is_equal_approx(Point2(158.689453, 155.370377)));511CHECK(r[1][2].is_equal_approx(Point2(140, 125)));512}513}514515TEST_CASE("[Geometry2D] Clip polygons") {516Vector<Point2> a;517Vector<Point2> b;518Vector<Vector<Point2>> r;519520a.push_back(Point2(225, 180));521a.push_back(Point2(160, 230));522a.push_back(Point2(20, 212));523a.push_back(Point2(50, 115));524525SUBCASE("[Geometry2D] Both polygons are empty") {526r = Geometry2D::clip_polygons(Vector<Point2>(), Vector<Point2>());527CHECK_MESSAGE(r.is_empty(), "Both polygons are empty. The clip should also be empty.");528}529530SUBCASE("[Geometry2D] Basic clip with one result polygon") {531b.push_back(Point2(250, 170));532b.push_back(Point2(175, 270));533b.push_back(Point2(120, 260));534b.push_back(Point2(25, 80));535r = Geometry2D::clip_polygons(a, b);536REQUIRE_MESSAGE(r.size() == 1, "The clipped polygons should result in 1 polygon.");537REQUIRE_MESSAGE(r[0].size() == 3, "The resulting clipped polygon should have 3 vertices.");538CHECK(r[0][0].is_equal_approx(Point2(100.102173, 222.298843)));539CHECK(r[0][1].is_equal_approx(Point2(20, 212)));540CHECK(r[0][2].is_equal_approx(Point2(47.588089, 122.798492)));541}542543SUBCASE("[Geometry2D] Polygon b completely overlaps polygon a") {544b.push_back(Point2(250, 170));545b.push_back(Point2(175, 270));546b.push_back(Point2(10, 210));547b.push_back(Point2(55, 80));548r = Geometry2D::clip_polygons(a, b);549CHECK_MESSAGE(r.is_empty(), "Polygon 'b' completely overlaps polygon 'a'. This should result in no clipped polygons.");550}551552SUBCASE("[Geometry2D] Polygon a completely overlaps polygon b") {553b.push_back(Point2(150, 200));554b.push_back(Point2(65, 190));555b.push_back(Point2(80, 140));556r = Geometry2D::clip_polygons(a, b);557REQUIRE_MESSAGE(r.size() == 2, "Polygon 'a' completely overlaps polygon 'b'. This should result in 2 clipped polygons.");558REQUIRE_MESSAGE(r[0].size() == 4, "The resulting clipped polygon should have 4 vertices.");559REQUIRE_MESSAGE(!Geometry2D::is_polygon_clockwise(r[0]), "The resulting clipped polygon (outline) should be counter-clockwise.");560CHECK(r[0][0].is_equal_approx(a[0]));561CHECK(r[0][1].is_equal_approx(a[1]));562CHECK(r[0][2].is_equal_approx(a[2]));563CHECK(r[0][3].is_equal_approx(a[3]));564REQUIRE_MESSAGE(r[1].size() == 3, "The resulting clipped polygon should have 3 vertices.");565REQUIRE_MESSAGE(Geometry2D::is_polygon_clockwise(r[1]), "The resulting clipped polygon (hole) should be clockwise.");566CHECK(r[1][0].is_equal_approx(b[1]));567CHECK(r[1][1].is_equal_approx(b[0]));568CHECK(r[1][2].is_equal_approx(b[2]));569}570}571572TEST_CASE("[Geometry2D] Exclude polygons") {573Vector<Point2> a;574Vector<Point2> b;575Vector<Vector<Point2>> r;576577a.push_back(Point2(225, 180));578a.push_back(Point2(160, 230));579a.push_back(Point2(20, 212));580a.push_back(Point2(50, 115));581582SUBCASE("[Geometry2D] Both polygons are empty") {583r = Geometry2D::exclude_polygons(Vector<Point2>(), Vector<Point2>());584CHECK_MESSAGE(r.is_empty(), "Both polygons are empty. The excluded polygon should also be empty.");585}586587SUBCASE("[Geometry2D] One polygon is empty") {588r = Geometry2D::exclude_polygons(a, b);589REQUIRE_MESSAGE(r.size() == 1, "One polygon is non-empty. There should be 1 resulting excluded polygon.");590REQUIRE_MESSAGE(r[0].size() == 4, "The resulting excluded polygon should have 4 vertices.");591CHECK(r[0][0].is_equal_approx(a[0]));592CHECK(r[0][1].is_equal_approx(a[1]));593CHECK(r[0][2].is_equal_approx(a[2]));594CHECK(r[0][3].is_equal_approx(a[3]));595}596597SUBCASE("[Geometry2D] Exclude with 2 resulting polygons (outline and hole)") {598b.push_back(Point2(140, 160));599b.push_back(Point2(150, 220));600b.push_back(Point2(40, 200));601b.push_back(Point2(60, 140));602r = Geometry2D::exclude_polygons(a, b);603REQUIRE_MESSAGE(r.size() == 2, "There should be 2 resulting excluded polygons (outline and hole).");604REQUIRE_MESSAGE(r[0].size() == 4, "The resulting excluded polygon should have 4 vertices.");605REQUIRE_MESSAGE(!Geometry2D::is_polygon_clockwise(r[0]), "The resulting excluded polygon (outline) should be counter-clockwise.");606CHECK(r[0][0].is_equal_approx(a[0]));607CHECK(r[0][1].is_equal_approx(a[1]));608CHECK(r[0][2].is_equal_approx(a[2]));609CHECK(r[0][3].is_equal_approx(a[3]));610REQUIRE_MESSAGE(r[1].size() == 4, "The resulting excluded polygon should have 4 vertices.");611REQUIRE_MESSAGE(Geometry2D::is_polygon_clockwise(r[1]), "The resulting excluded polygon (hole) should be clockwise.");612CHECK(r[1][0].is_equal_approx(Point2(40, 200)));613CHECK(r[1][1].is_equal_approx(Point2(150, 220)));614CHECK(r[1][2].is_equal_approx(Point2(140, 160)));615CHECK(r[1][3].is_equal_approx(Point2(60, 140)));616}617}618619TEST_CASE("[Geometry2D] Intersect polyline with polygon") {620Vector<Vector2> l;621Vector<Vector2> p;622Vector<Vector<Point2>> r;623624l.push_back(Vector2(100, 90));625l.push_back(Vector2(120, 250));626627p.push_back(Vector2(225, 180));628p.push_back(Vector2(160, 230));629p.push_back(Vector2(20, 212));630p.push_back(Vector2(50, 115));631632SUBCASE("[Geometry2D] Both line and polygon are empty") {633r = Geometry2D::intersect_polyline_with_polygon(Vector<Vector2>(), Vector<Vector2>());634CHECK_MESSAGE(r.is_empty(), "Both line and polygon are empty. The intersection line should also be empty.");635}636637SUBCASE("[Geometry2D] Line is non-empty and polygon is empty") {638r = Geometry2D::intersect_polyline_with_polygon(l, Vector<Vector2>());639CHECK_MESSAGE(r.is_empty(), "The polygon is empty while the line is non-empty. The intersection line should be empty.");640}641642SUBCASE("[Geometry2D] Basic intersection with 1 resulting intersection line") {643r = Geometry2D::intersect_polyline_with_polygon(l, p);644REQUIRE_MESSAGE(r.size() == 1, "There should be 1 resulting intersection line.");645REQUIRE_MESSAGE(r[0].size() == 2, "The resulting intersection line should have 2 vertices.");646CHECK(r[0][0].is_equal_approx(Vector2(105.711609, 135.692886)));647CHECK(r[0][1].is_equal_approx(Vector2(116.805809, 224.446457)));648}649650SUBCASE("[Geometry2D] Complex intersection with 2 resulting intersection lines") {651l.clear();652l.push_back(Vector2(100, 90));653l.push_back(Vector2(190, 255));654l.push_back(Vector2(135, 260));655l.push_back(Vector2(57, 200));656l.push_back(Vector2(50, 170));657l.push_back(Vector2(15, 155));658r = Geometry2D::intersect_polyline_with_polygon(l, p);659REQUIRE_MESSAGE(r.size() == 2, "There should be 2 resulting intersection lines.");660REQUIRE_MESSAGE(r[0].size() == 2, "The resulting intersection line should have 2 vertices.");661CHECK(r[0][0].is_equal_approx(Vector2(129.804565, 144.641693)));662CHECK(r[0][1].is_equal_approx(Vector2(171.527084, 221.132996)));663REQUIRE_MESSAGE(r[1].size() == 4, "The resulting intersection line should have 4 vertices.");664CHECK(r[1][0].is_equal_approx(Vector2(83.15609, 220.120087)));665CHECK(r[1][1].is_equal_approx(Vector2(57, 200)));666CHECK(r[1][2].is_equal_approx(Vector2(50, 170)));667CHECK(r[1][3].is_equal_approx(Vector2(34.980492, 163.563065)));668}669}670671TEST_CASE("[Geometry2D] Clip polyline with polygon") {672Vector<Vector2> l;673Vector<Vector2> p;674Vector<Vector<Point2>> r;675676l.push_back(Vector2(70, 140));677l.push_back(Vector2(160, 320));678679p.push_back(Vector2(225, 180));680p.push_back(Vector2(160, 230));681p.push_back(Vector2(20, 212));682p.push_back(Vector2(50, 115));683684SUBCASE("[Geometry2D] Both line and polygon are empty") {685r = Geometry2D::clip_polyline_with_polygon(Vector<Vector2>(), Vector<Vector2>());686CHECK_MESSAGE(r.is_empty(), "Both line and polygon are empty. The clipped line should also be empty.");687}688689SUBCASE("[Geometry2D] Polygon is empty and line is non-empty") {690r = Geometry2D::clip_polyline_with_polygon(l, Vector<Vector2>());691REQUIRE_MESSAGE(r.size() == 1, "There should be 1 resulting clipped line.");692REQUIRE_MESSAGE(r[0].size() == 2, "The resulting clipped line should have 2 vertices.");693CHECK(r[0][0].is_equal_approx(l[0]));694CHECK(r[0][1].is_equal_approx(l[1]));695}696697SUBCASE("[Geometry2D] Basic clip with 1 resulting clipped line") {698r = Geometry2D::clip_polyline_with_polygon(l, p);699REQUIRE_MESSAGE(r.size() == 1, "There should be 1 resulting clipped line.");700REQUIRE_MESSAGE(r[0].size() == 2, "The resulting clipped line should have 2 vertices.");701CHECK(r[0][0].is_equal_approx(Vector2(111.908401, 223.816803)));702CHECK(r[0][1].is_equal_approx(Vector2(160, 320)));703}704705SUBCASE("[Geometry2D] Complex clip with 2 resulting clipped lines") {706l.clear();707l.push_back(Vector2(55, 70));708l.push_back(Vector2(50, 190));709l.push_back(Vector2(120, 165));710l.push_back(Vector2(122, 250));711l.push_back(Vector2(160, 320));712r = Geometry2D::clip_polyline_with_polygon(l, p);713REQUIRE_MESSAGE(r.size() == 2, "There should be 2 resulting clipped lines.");714REQUIRE_MESSAGE(r[0].size() == 3, "The resulting clipped line should have 3 vertices.");715CHECK(r[0][0].is_equal_approx(Vector2(121.412682, 225.038757)));716CHECK(r[0][1].is_equal_approx(Vector2(122, 250)));717CHECK(r[0][2].is_equal_approx(Vector2(160, 320)));718REQUIRE_MESSAGE(r[1].size() == 2, "The resulting clipped line should have 2 vertices.");719CHECK(r[1][0].is_equal_approx(Vector2(55, 70)));720CHECK(r[1][1].is_equal_approx(Vector2(53.07737, 116.143021)));721}722}723724TEST_CASE("[Geometry2D] Convex hull") {725Vector<Point2> a;726Vector<Point2> r;727728a.push_back(Point2(-4, -8));729a.push_back(Point2(-10, -4));730a.push_back(Point2(8, 2));731a.push_back(Point2(-6, 10));732a.push_back(Point2(-12, 4));733a.push_back(Point2(10, -8));734a.push_back(Point2(4, 8));735736SUBCASE("[Geometry2D] No points") {737r = Geometry2D::convex_hull(Vector<Vector2>());738739CHECK_MESSAGE(r.is_empty(), "The convex hull should be empty if there are no input points.");740}741742SUBCASE("[Geometry2D] Single point") {743Vector<Point2> b;744b.push_back(Point2(4, -3));745746r = Geometry2D::convex_hull(b);747REQUIRE_MESSAGE(r.size() == 1, "Convex hull should contain 1 point.");748CHECK(r[0].is_equal_approx(b[0]));749}750751SUBCASE("[Geometry2D] All points form the convex hull") {752r = Geometry2D::convex_hull(a);753REQUIRE_MESSAGE(r.size() == 8, "Convex hull should contain 8 points.");754CHECK(r[0].is_equal_approx(Point2(-12, 4)));755CHECK(r[1].is_equal_approx(Point2(-10, -4)));756CHECK(r[2].is_equal_approx(Point2(-4, -8)));757CHECK(r[3].is_equal_approx(Point2(10, -8)));758CHECK(r[4].is_equal_approx(Point2(8, 2)));759CHECK(r[5].is_equal_approx(Point2(4, 8)));760CHECK(r[6].is_equal_approx(Point2(-6, 10)));761CHECK(r[7].is_equal_approx(Point2(-12, 4)));762}763764SUBCASE("[Geometry2D] Add extra points inside original convex hull") {765a.push_back(Point2(-4, -8));766a.push_back(Point2(0, 0));767a.push_back(Point2(0, 8));768a.push_back(Point2(-10, -3));769a.push_back(Point2(9, -4));770a.push_back(Point2(6, 4));771772r = Geometry2D::convex_hull(a);773REQUIRE_MESSAGE(r.size() == 8, "Convex hull should contain 8 points.");774CHECK(r[0].is_equal_approx(Point2(-12, 4)));775CHECK(r[1].is_equal_approx(Point2(-10, -4)));776CHECK(r[2].is_equal_approx(Point2(-4, -8)));777CHECK(r[3].is_equal_approx(Point2(10, -8)));778CHECK(r[4].is_equal_approx(Point2(8, 2)));779CHECK(r[5].is_equal_approx(Point2(4, 8)));780CHECK(r[6].is_equal_approx(Point2(-6, 10)));781CHECK(r[7].is_equal_approx(Point2(-12, 4)));782}783784SUBCASE("[Geometry2D] Add extra points on border of original convex hull") {785a.push_back(Point2(9, -3));786a.push_back(Point2(-2, -8));787788r = Geometry2D::convex_hull(a);789REQUIRE_MESSAGE(r.size() == 8, "Convex hull should contain 8 points.");790CHECK(r[0].is_equal_approx(Point2(-12, 4)));791CHECK(r[1].is_equal_approx(Point2(-10, -4)));792CHECK(r[2].is_equal_approx(Point2(-4, -8)));793CHECK(r[3].is_equal_approx(Point2(10, -8)));794CHECK(r[4].is_equal_approx(Point2(8, 2)));795CHECK(r[5].is_equal_approx(Point2(4, 8)));796CHECK(r[6].is_equal_approx(Point2(-6, 10)));797CHECK(r[7].is_equal_approx(Point2(-12, 4)));798}799800SUBCASE("[Geometry2D] Add extra points outside border of original convex hull") {801a.push_back(Point2(-11, -1));802a.push_back(Point2(7, 6));803804r = Geometry2D::convex_hull(a);805REQUIRE_MESSAGE(r.size() == 10, "Convex hull should contain 10 points.");806CHECK(r[0].is_equal_approx(Point2(-12, 4)));807CHECK(r[1].is_equal_approx(Point2(-11, -1)));808CHECK(r[2].is_equal_approx(Point2(-10, -4)));809CHECK(r[3].is_equal_approx(Point2(-4, -8)));810CHECK(r[4].is_equal_approx(Point2(10, -8)));811CHECK(r[5].is_equal_approx(Point2(8, 2)));812CHECK(r[6].is_equal_approx(Point2(7, 6)));813CHECK(r[7].is_equal_approx(Point2(4, 8)));814CHECK(r[8].is_equal_approx(Point2(-6, 10)));815CHECK(r[9].is_equal_approx(Point2(-12, 4)));816}817}818819TEST_CASE("[Geometry2D] Bresenham line") {820Vector<Vector2i> r;821822SUBCASE("[Geometry2D] Single point") {823r = Geometry2D::bresenham_line(Point2i(0, 0), Point2i(0, 0));824825REQUIRE_MESSAGE(r.size() == 1, "The Bresenham line should contain exactly one point.");826CHECK(r[0] == Vector2i(0, 0));827}828829SUBCASE("[Geometry2D] Line parallel to x-axis") {830r = Geometry2D::bresenham_line(Point2i(1, 2), Point2i(5, 2));831832REQUIRE_MESSAGE(r.size() == 5, "The Bresenham line should contain exactly five points.");833CHECK(r[0] == Vector2i(1, 2));834CHECK(r[1] == Vector2i(2, 2));835CHECK(r[2] == Vector2i(3, 2));836CHECK(r[3] == Vector2i(4, 2));837CHECK(r[4] == Vector2i(5, 2));838}839840SUBCASE("[Geometry2D] 45 degree line from the origin") {841r = Geometry2D::bresenham_line(Point2i(0, 0), Point2i(4, 4));842843REQUIRE_MESSAGE(r.size() == 5, "The Bresenham line should contain exactly five points.");844CHECK(r[0] == Vector2i(0, 0));845CHECK(r[1] == Vector2i(1, 1));846CHECK(r[2] == Vector2i(2, 2));847CHECK(r[3] == Vector2i(3, 3));848CHECK(r[4] == Vector2i(4, 4));849}850851SUBCASE("[Geometry2D] Sloped line going up one unit") {852r = Geometry2D::bresenham_line(Point2i(0, 0), Point2i(4, 1));853854REQUIRE_MESSAGE(r.size() == 5, "The Bresenham line should contain exactly five points.");855CHECK(r[0] == Vector2i(0, 0));856CHECK(r[1] == Vector2i(1, 0));857CHECK(r[2] == Vector2i(2, 0));858CHECK(r[3] == Vector2i(3, 1));859CHECK(r[4] == Vector2i(4, 1));860}861862SUBCASE("[Geometry2D] Sloped line going up two units") {863r = Geometry2D::bresenham_line(Point2i(0, 0), Point2i(4, 2));864865REQUIRE_MESSAGE(r.size() == 5, "The Bresenham line should contain exactly five points.");866CHECK(r[0] == Vector2i(0, 0));867CHECK(r[1] == Vector2i(1, 0));868CHECK(r[2] == Vector2i(2, 1));869CHECK(r[3] == Vector2i(3, 1));870CHECK(r[4] == Vector2i(4, 2));871}872873SUBCASE("[Geometry2D] Long sloped line") {874r = Geometry2D::bresenham_line(Point2i(0, 0), Point2i(11, 5));875876REQUIRE_MESSAGE(r.size() == 12, "The Bresenham line should contain exactly twelve points.");877CHECK(r[0] == Vector2i(0, 0));878CHECK(r[1] == Vector2i(1, 0));879CHECK(r[2] == Vector2i(2, 1));880CHECK(r[3] == Vector2i(3, 1));881CHECK(r[4] == Vector2i(4, 2));882CHECK(r[5] == Vector2i(5, 2));883CHECK(r[6] == Vector2i(6, 3));884CHECK(r[7] == Vector2i(7, 3));885CHECK(r[8] == Vector2i(8, 4));886CHECK(r[9] == Vector2i(9, 4));887CHECK(r[10] == Vector2i(10, 5));888CHECK(r[11] == Vector2i(11, 5));889}890}891} // namespace TestGeometry2D892893894