Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: Updated PolygonComponent.containsPoint to account for concave polygons #2979

Merged
merged 4 commits into from
Jan 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
71 changes: 44 additions & 27 deletions packages/flame/lib/src/geometry/polygon_component.dart
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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