Skip to content

Conversation

TiagoFar78
Copy link
Contributor

This PR introduces a new algorithm to calculate the area of 2D polygons using discrete points. The previous implementation was designed for concrete points and did not handle certain cases correctly.

Since the new algorithm is relatively complex, I have also prepared a detailed explanation for reference.

Closes #888

@TiagoFar78 TiagoFar78 requested a review from a team as a code owner July 30, 2025 01:31
@me4502
Copy link
Member

me4502 commented Jul 30, 2025

Thank you for this PR! :)

It looks good to me, although I'll let someone else who's better at maths review that side of things. The only thing I'd personally change are a few unnecessary allocations (& usage of Streams APIs), although none of this is really hot code so it's not too major an issue.

@me4502
Copy link
Member

me4502 commented Aug 4, 2025

I've done some testing of this PR, and it definitely seems vastly better than the existing behaviour, thank you for this! I'm approving it based on code changes & my testing- but still would prefer someone more maths-focused to take a look before I merge it :)

@octylFractal
Copy link
Member

I saw in the limitations that this doesn't handle self-intersecting polygons. We do allow that, so I'm not sure this code is better or worse than the previous one for that case. However, while thinking about it, I came up with what I think is a better idea:

  1. Triangulate (or at least simplify) the polygon using some method that can handle self-intersections. I'm not able to immediately find anything right now, you might have better luck. I think we can get away with just taking the outer boundary as a whole. Perhaps we can use https://github.com/AngusJohnson/Clipper2's union operation or implement something based off the Bentley–Ottmann algorithm?
  2. For each of the now-simple polygons, calculate the number of boundary integer points, b.
  3. Calculate the area of the simple polygon using the Shoelace formula, A.
  4. Calculate the number of integer points inside by inversing Pick's theorem: i = A - b / 2 + 1
  5. The area is then i + b, and the volume is that times the height.

I'm not sure if step 5 is entirely correct given the way we implement contains(...). I'd like to see some testing that validates that it's giving the right result, but I am pretty sure it should work out.

What do you think?

@TiagoFar78
Copy link
Contributor Author

  1. For each of the now-simple polygons, calculate the number of boundary integer points, b.
  2. Calculate the area of the simple polygon using the Shoelace formula, A.
  3. Calculate the number of integer points inside by inversing Pick's theorem: i = A - b / 2 + 1
  4. The area is then i + b, and the volume is that times the height.

These steps produce the same result as my current algorithm for non-self-crossing polygons, but with significantly less complexity and without the second limitation of my approach.

I think it’s a great idea, and with your permission, I’d love to implement it. I’d be happy to add you as a co-author to the commit as well.


Regarding the self crossing polygons...

I saw in the limitations that this doesn't handle self-intersecting polygons. We do allow that, so I'm not sure this code is better or worse than the previous one for that case.

Both approaches behave the same here: the previous algorithm also did not support self-crossing polygons. I assumed this was not required since the original version did not address it either.

  1. Triangulate (or at least simplify) the polygon using some method that can handle self-intersections.

I don’t think triangulation is the right approach. For example, in the figure below, the orange points would be counted twice, once per triangle, leading to double counting:

image
  1. I think we can get away with just taking the outer boundary as a whole. Perhaps we can use https://github.com/AngusJohnson/Clipper2's union operation or implement something based off the Bentley–Ottmann algorithm?

This seems like the ideal solution to me as well. I’ll try moving forward with that approach.

Thanks for the great idea!

@octylFractal
Copy link
Member

Please feel free to implement it. Add me as co-author if it's easy for you.

@TiagoFar78
Copy link
Contributor Author

I’ve tried several different approaches to handle self-crossing polygons. Here’s a summary of what I attempted:

  1. Generalization of Pick’s Theorem (winding number approach)
    I experimented with a modified formula A = Σ w(p) + B/2 - 1, where A is the polygon area, B is the number of boundary points, and w(p) is the winding number of point p. While I could compute Σ w(p), it didn’t allow me to recover the actual number of interior lattice points without falling back to a bounding-box method.

  2. Formula from Lattice Point Geometry: Pick’s Theorem and Minkowski’s Theorem, section 3.3.1
    The formula A = v - e/2 + m - 1 (where v is the total number of lattice points, boundary and interior, e is the number of edges arround the boundary, and m is the number of holes) claims to work even for non-simple polygons. However, in practice it only seemed valid for polygons with intersecting (but not fully crossing) edges, such as the one in the figure below. For truly self-crossing polygons, it produced incorrect results.

image
  1. Splitting polygons into simpler pieces
    I attempted to break a self-crossing polygon into the minimal number of simpler polygons. The problem is that the resulting sub-polygons were not always lattice polygons, so Pick’s theorem couldn’t be applied reliably. The polygon below would become two non-lattice polygons.
image
  1. Applying Pick’s theorem directly to non-lattice polygons:
    In an act of despair, I tried applying Pick’s theorem “as-is” to these cases. This resulted in fractional values for interior points, which I then attempted to round to the nearest integer. After some testing, the approach proved inconsistent and unusable.

More bad news...

The only actual solution I could find was this one, which in theory is about as costly as just using a bounding box.

Given that, I think the best path forward is simply to ignore self-crossing polygons and stick with steps 2–5 from @octylFractal’s algorithm for handling simple polygons. Most known theorems also break down when applied to self-crossing polygons, so I don’t think it’s a major issue. Even WorldEdit’s previous approach didn’t support them, and having a reliable solution for simple polygons is still a solid improvement.

I’ll wait for your feedback, but I’m happy to implement either @octylFractal’s algorithm or the “lattice points inside non-lattice polygon” approach. If neither option feels right, I’m afraid I don’t have a better alternative.

@octylFractal
Copy link
Member

I will look more into it in the next few weeks, when I have time.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

getArea() and Polygonal2DRegion - don't return real value.
3 participants