浏览代码

Fix segment intersection consistency in Geometry2D

Segment collision results could be different depending on the direction
when they exactly touch (order of the points in segments). This was due
to the way parallelism was checked, using different logic based on
positive or negative sign of cross products.

Now the results are the same whatever the direction, without changing
the current design, which is that parallel or colinear segments are
not considered colinear.

Fixes inconsistencies with raycasts exactly on edges of convex shapes
depending on the direction.
PouleyKetchoupp 4 年之前
父节点
当前提交
511c80b2ec
共有 2 个文件被更改,包括 41 次插入12 次删除
  1. 17 3
      core/math/geometry_2d.h
  2. 24 9
      tests/test_geometry_2d.h

+ 17 - 3
core/math/geometry_2d.h

@@ -182,7 +182,15 @@ public:
 		C = Vector2(C.x * Bn.x + C.y * Bn.y, C.y * Bn.x - C.x * Bn.y);
 		D = Vector2(D.x * Bn.x + D.y * Bn.y, D.y * Bn.x - D.x * Bn.y);
 
-		if ((C.y < 0 && D.y < 0) || (C.y >= 0 && D.y >= 0)) {
+		// Fail if C x B and D x B have the same sign (segments don't intersect).
+		// (equivalent to condition (C.y < 0 && D.y < CMP_EPSILON) || (C.y > 0 && D.y > CMP_EPSILON))
+		if (C.y * D.y > CMP_EPSILON) {
+			return false;
+		}
+
+		// Fail if segments are parallel or colinear.
+		// (when A x B == zero, i.e (C - D) x B == zero, i.e C x B == D x B)
+		if (Math::is_equal_approx(C.y, D.y)) {
 			return false;
 		}
 
@@ -193,7 +201,7 @@ public:
 			return false;
 		}
 
-		// (4) Apply the discovered position to line A-B in the original coordinate system.
+		// Apply the discovered position to line A-B in the original coordinate system.
 		if (r_result) {
 			*r_result = p_from_a + B * ABpos;
 		}
@@ -353,8 +361,14 @@ public:
 		for (int i = 0; i < c; i++) {
 			const Vector2 &v1 = p[i];
 			const Vector2 &v2 = p[(i + 1) % c];
-			if (segment_intersects_segment(v1, v2, p_point, further_away, nullptr)) {
+
+			Vector2 res;
+			if (segment_intersects_segment(v1, v2, p_point, further_away, &res)) {
 				intersections++;
+				if (res.is_equal_approx(p_point)) {
+					// Point is in one of the polygon edges.
+					return true;
+				}
 			}
 		}
 

+ 24 - 9
tests/test_geometry_2d.h

@@ -51,8 +51,6 @@ TEST_CASE("[Geometry2D] Point in circle") {
 	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.
-	// In `is_point_in_triangle` and `is_point_in_polygon` they are treated as being outside, so in order the make
-	// the behaviour consistent this may change in the future (see issue #44717 and PR #44274).
 	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));
 }
@@ -66,7 +64,7 @@ TEST_CASE("[Geometry2D] Point in triangle") {
 	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` they are treated as being inside, so in order the make
+	// 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)));
@@ -95,11 +93,16 @@ TEST_CASE("[Geometry2D] Point in polygon") {
 	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 outside the polygon.
-	// In `is_point_in_circle` 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_polygon(Vector2(68, 112), p));
-	CHECK_FALSE(Geometry2D::is_point_in_polygon(Vector2(-88, 120), 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") {
@@ -140,9 +143,21 @@ TEST_CASE("[Geometry2D] Segment intersection.") {
 	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(1, -1), &r),
+			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") {