Skip to content

Commit

Permalink
fix: Updated PolygonComponent.containsPoint to account for concave po…
Browse files Browse the repository at this point in the history
…lygons (#2979)

Previously, the `PolygonComponent.containsPoint()` and
`.containsLocalPoint()` functions consisted of duplicate code that
checked whether a given point lies within a convex polygon. They didn't
function properly with concave polygons.

I created a new `_containsPoint()` function that is called from both
functions to reduce redundancies. This new function uses a different
approach to figure out whether a point lies within a polygon, which
should also work for concave polygons, or even polygons with holes. The
algorithm is vaguely explained within code comments, and is visualized
in this post: https://stackoverflow.com/a/218081/5008997

---------

Co-authored-by: Lukas Klingsbo <me@lukas.fyi>
  • Loading branch information
davidjan3 and spydon committed Jan 21, 2024
1 parent cf09e04 commit a6fe62a
Show file tree
Hide file tree
Showing 2 changed files with 71 additions and 27 deletions.
71 changes: 44 additions & 27 deletions packages/flame/lib/src/geometry/polygon_component.dart
Expand Up @@ -201,48 +201,65 @@ class PolygonComponent extends ShapeComponent {
canvas.drawPath(_path, debugPaint);
}

/// Checks whether the polygon contains the [point].
/// Note: The polygon needs to be convex for this to work.
@override
bool containsPoint(Vector2 point) {
bool _containsPoint(Vector2 point, List<Vector2> vertices) {
// If the size is 0 then it can't contain any points
if (size.x == 0 || size.y == 0) {
return false;
}

final vertices = globalVertices();
// Count the amount of edges crossed by going left from the point
var count = 0;
for (var i = 0; i < vertices.length; i++) {
final edge = getEdge(i, vertices: vertices);
final isOutside = (edge.to.x - edge.from.x) * (point.y - edge.from.y) -
(point.x - edge.from.x) * (edge.to.y - edge.from.y) >
0;
if (isOutside) {
// Point is outside of convex polygon
return false;
final from = vertices[i];
final to = vertices[(i + 1) % vertices.length];

// Skip if the edge is entirely to the right, above or below the point
if (from.x > point.x && to.x > point.x ||
min(from.y, to.y) > point.y ||
max(from.y, to.y) < point.y) {
continue;
}

// Get x coordinate of where the edge intersects with the horizontal line
double intersectionX;
if (from.y == to.y) {
intersectionX = min(from.x, to.x);
} else {
intersectionX =
((point.y - from.y) * (to.x - from.x)) / (to.y - from.y) + from.x;
}

if (intersectionX == point.x) {
// If the point is on the edge, return true
return true;
} else if (intersectionX < point.x) {
// Only count one edge if vertex is crossed
// Only count if edges cross the line, not just touch it and go back
if ((from.y != point.y && to.y != point.y) ||
to.y == from.y ||
point.y == max(from.y, to.y)) {
count++;
}
}
}
return true;

// If the amount of edges crossed is odd, the point is inside the polygon
return (count % 2).isOdd;
}

@override
bool containsPoint(Vector2 point) {
final vertices = globalVertices();
return _containsPoint(point, vertices);
}

@override
bool containsLocalPoint(Vector2 point) {
// Take anchor into consideration.
final localPoint =
anchor.toOtherAnchorPosition(point, Anchor.topLeft, size);
if (size.x == 0 || size.y == 0) {
return false;
}
for (var i = 0; i < _vertices.length; i++) {
final edge = getEdge(i, vertices: vertices);
final isOutside =
(edge.to.x - edge.from.x) * (localPoint.y - edge.from.y) -
(localPoint.x - edge.from.x) * (edge.to.y - edge.from.y) >
0;
if (isOutside) {
return false;
}
}
return true;

return _containsPoint(localPoint, _vertices);
}

/// Return all vertices as [LineSegment]s that intersect [rect], if [rect]
Expand Down
27 changes: 27 additions & 0 deletions packages/flame/test/components/shape_component_test.dart
Expand Up @@ -180,6 +180,33 @@ void main() {
);
});

test('concave polygon contains point', () {
final component = PolygonComponent(
[
Vector2(0, 0),
Vector2(-2, -4),
Vector2(2, 0),
Vector2(-2, 4),
],
);
expect(
component.containsPoint(Vector2(-1, 0)),
isFalse,
);
expect(
component.containsPoint(Vector2(-1, 1)),
isFalse,
);
expect(
component.containsPoint(Vector2(2, 0)),
isTrue,
);
expect(
component.containsPoint(Vector2(1, 1)),
isTrue,
);
});

test('horizontally flipped rectangle contains point', () {
final component = RectangleComponent(
position: Vector2.all(1.0),
Expand Down

0 comments on commit a6fe62a

Please sign in to comment.