-
Notifications
You must be signed in to change notification settings - Fork 223
feat: Implement is_valid algorithm for polyhedral surfaces #1409
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
base: develop
Are you sure you want to change the base?
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull Request Overview
This PR enhances the validity checking algorithm by introducing support for polyhedral surfaces in line with the OGC Simple Features Specification. Key changes include:
- Adding several new failure types for polyhedral surfaces (e.g. collinear, non-coplanar points, inconsistent orientation, etc.).
- Extending the test suite in is_valid.cpp with comprehensive cases for various valid and invalid polyhedral surfaces.
- Updating include directives to incorporate polyhedral surface support in the core algorithm implementation.
Reviewed Changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 2 comments.
File | Description |
---|---|
test/algorithms/is_valid.cpp | Adds polyhedral surface test cases and minor documentation updates. |
include/boost/geometry/algorithms/validity_failure_type.hpp | Introduces new failure codes specific to polyhedral surfaces. |
include/boost/geometry/algorithms/detail/is_valid/implementation.hpp | Adds header inclusion for polyhedral surface implementation. |
include/boost/geometry/algorithms/detail/is_valid/polyhedral_surface.hpp
Outdated
Show resolved
Hide resolved
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I noted some points inline, mostly minor but importantly (imho) the question of separating some computations into strategy.
A minor point, this PR should probably include the removal of
//TODO: add is_valid() check |
static bool are_linearly_dependent(vec3 const& a, vec3 const& b) | ||
{ | ||
return qvm::mag_sqr(qvm::cross(a, b)) < tolerance * tolerance; | ||
} | ||
|
||
// Try to find 3 non-collinear points | ||
static bool find_non_collinear_points(polygon_3d const& polygon, vec3& base, vec3& v1, vec3& v2) | ||
{ | ||
for (auto pit1 = points_begin(polygon); pit1 != points_end(polygon); ++pit1) | ||
{ | ||
for (auto pit2 = pit1; pit2 != points_end(polygon); ++pit2) | ||
{ | ||
for (auto pit3 = pit2; pit3 != points_end(polygon); ++pit3) | ||
{ | ||
set_vec_from_point::apply(*pit1, base); | ||
set_vec_from_point::apply(*pit2, v1); | ||
set_vec_from_point::apply(*pit3, v2); | ||
if (!are_linearly_dependent(v1 - base, v2 - base)) | ||
{ | ||
return true; | ||
} | ||
} | ||
} | ||
} | ||
return false; // All points are collinear | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think by the library design philosophy and also practically, if one wants to be able to plug in a robust computation later, this is a cartesian strategy.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Indeed, a lot of computations here are coordinate system spesific and should be moved in strategies. On one hand, I think that polyhedral surface can only be cartesian. But on the other hand, predicates should be moved to stategies anyways for robustness reasons (as you said).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I thought about it. I think, these are equivalent to three calls to a side_strategy for the the XY, XZ, YZ projections respectively. If any of them returns non-zero, they are non-collinear. We already have the side strategy with the correct tolerances for this,
struct side_rounded_input |
This can short-circuit (if the call for the first projection returns non-zero which it will almost surely in general position, then the other calls are not necessary) and there is probably a lot of common subexpression elimination that the compiler will do.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That reduces the computation to orientation, great. I was thinking using the project_point_to_plane_2d
to only do one orientation test but I think projecting to coordinate planes as you suggested is better since it does not involve any computations (substitutions, dot products etc).
I change this part to use the side strategy provided by the umbrella strategy used in is_valid. This uses the side_by_triangle strategy as the default but the user can change it (as in many other cases in the library). At this point I think this is better than creating a new umbrella strategy that has side_rounded_input as a side strategy. We can think of substituting the side_by_triangle by side_rounded_input in general or in as many algorithms as possible as a future step.
include/boost/geometry/algorithms/detail/is_valid/polyhedral_surface.hpp
Outdated
Show resolved
Hide resolved
include/boost/geometry/algorithms/detail/is_valid/polyhedral_surface.hpp
Outdated
Show resolved
Hide resolved
include/boost/geometry/algorithms/detail/is_valid/polyhedral_surface.hpp
Outdated
Show resolved
Hide resolved
include/boost/geometry/algorithms/detail/is_valid/polyhedral_surface.hpp
Outdated
Show resolved
Hide resolved
include/boost/geometry/algorithms/detail/is_valid/polyhedral_surface.hpp
Outdated
Show resolved
Hide resolved
include/boost/geometry/algorithms/detail/is_valid/polyhedral_surface.hpp
Outdated
Show resolved
Hide resolved
include/boost/geometry/algorithms/detail/is_valid/polyhedral_surface.hpp
Outdated
Show resolved
Hide resolved
include/boost/geometry/algorithms/detail/is_valid/polyhedral_surface.hpp
Outdated
Show resolved
Hide resolved
vec3 vi; | ||
set_vec_from_point::apply(*pit, vi); | ||
auto vol = qvm::dot(normal, vi)- dot(normal, base); | ||
if (std::abs(vol) > tolerance) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This may be to narrow for e.g. float coordinates in the range of thousand, I think, the bound should probably be scaled to the input magnitude, I will revisit this later when I have more time.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is orient3d (see e.g. https://www.[cs.cmu.edu/afs/cs/project/quake/public/code/predicates.c](https://www.cs.cmu.edu/afs/cs/project/quake/public/code/predicates.c)), i.e. the 3D generalisation of side, a determinant involving *pit
, base
, v1
, v2
. Bound for a tolerant version can be derived the same way as for side_rounded_input, I'll try to do it by the end of the week. Calling orient3d for every point may look more expensive, but I think the compiler will know to pull most unchanging subexpressions out of the loop (same as dot(normal, base) could be pulled out of the loop).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Exactly, orient3d is equivalent. I think it would be useful to add some explanations and/or references on how the tolerance bounds are derived as comments in side_rounded_input
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I thought about it. I think, we want
Since only one point changes (let's say
Then, I think, the tolerance (based on https://zenodo.org/records/7539355 with rounded input rules, rounding the second order part up because it doesn't matter for this use case):
where
Looks like a lot of ops but since only a changes, most of the subexpressions should be eliminated in the loop and it should just be around something like ~22 math ops per point.
And this would be the equivalent of side_rounded for planes.
include/boost/geometry/algorithms/detail/is_valid/polyhedral_surface.hpp
Outdated
Show resolved
Hide resolved
Looks great! I will review it next Wednesday or Thursday. |
fe6743a
to
36cb224
Compare
include/boost/geometry/algorithms/detail/is_valid/polyhedral_surface.hpp
Outdated
Show resolved
Hide resolved
@tinko92 thanks for the comments. I fixed all but the ones about tolerance and the strategy. I need to think of a unified and consistent solution for those. |
include/boost/geometry/algorithms/detail/is_valid/polyhedral_surface.hpp
Outdated
Show resolved
Hide resolved
typename boost::geometry::model::polygon<point_2d, false> | ||
>::type; | ||
using ring_2d = typename boost::geometry::ring_type<polygon_2d>::type; | ||
using segment_2d = typename boost::geometry::model::segment<point_2d>; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we need this many helper types?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
segment_2d is not used any more so I removed. Ring types we can get them from std::dectype
if you prefer it, I am ok with those as is.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's fine. But did you push already? I don't see the updates...
include/boost/geometry/algorithms/detail/is_valid/polyhedral_surface.hpp
Outdated
Show resolved
Hide resolved
include/boost/geometry/algorithms/detail/is_valid/polyhedral_surface.hpp
Outdated
Show resolved
Hide resolved
include/boost/geometry/algorithms/detail/is_valid/polyhedral_surface.hpp
Outdated
Show resolved
Hide resolved
include/boost/geometry/algorithms/detail/is_valid/polyhedral_surface.hpp
Show resolved
Hide resolved
include/boost/geometry/algorithms/detail/is_valid/polyhedral_surface.hpp
Outdated
Show resolved
Hide resolved
include/boost/geometry/algorithms/detail/is_valid/polyhedral_surface.hpp
Outdated
Show resolved
Hide resolved
include/boost/geometry/algorithms/detail/is_valid/polyhedral_surface.hpp
Outdated
Show resolved
Hide resolved
} | ||
else | ||
{ | ||
auto const& ring = boost::geometry::exterior_ring(polygon); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So this block does really the projection. Nice.
I think it's good to extract it from this class - and even to another source.
Such that it is useful on its own, and can be unit tested on its own.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree, would you mind leaving it as future work to see also what other algorithms could use it? As it is now there are a lot of shared variables with the rest of the code as well as errors related to is_valid.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
But exactly that (shared variables) will makes it harder to extract it later as well...
A generic polygon projection from 3d to 2d plane would be great. We could then immediately implement the surface-area algorithm on a 3d surface...
It would be a generic algorithm, every patch (surface) would be a separate 2D polygon. Or is it different than what you do here (I couldn't look into all the details yet).
Anyway - I'm fine having it as a next PR - I'm only afraid that it would not really safe time.
include/boost/geometry/algorithms/detail/is_valid/polyhedral_surface.hpp
Outdated
Show resolved
Hide resolved
include/boost/geometry/algorithms/detail/is_valid/polyhedral_surface.hpp
Outdated
Show resolved
Hide resolved
return intersection_type::invalid; | ||
} | ||
} | ||
// Project the polygon onto the 2D plane defined by v1 and v2 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks interesting. I'm not experienced in this so cannot really comment.
include/boost/geometry/algorithms/detail/is_valid/polyhedral_surface.hpp
Outdated
Show resolved
Hide resolved
include/boost/geometry/algorithms/detail/is_valid/polyhedral_surface.hpp
Outdated
Show resolved
Hide resolved
include/boost/geometry/algorithms/detail/is_valid/polyhedral_surface.hpp
Outdated
Show resolved
Hide resolved
include/boost/geometry/algorithms/detail/is_valid/polyhedral_surface.hpp
Outdated
Show resolved
Hide resolved
I will continue tomorrow. It looks good but I had many suggestions already... |
} | ||
else if (intersection == intersection_type::valid) | ||
{ | ||
disconnected = false; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does this check work? My understanding from the specification is that the entire polyhedral surface must be connected, is this not different from any pair of polygons being connected? If my understanding of the standard is correct, this needs a BFS.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You are right. Currently, the check only checks that there is no disconnected polygon. More work is needed to check for connectedness of the surface.
So I didn't continue tomorrow - but great that you processed our comments! Thanks! I plan now to continue this weekend. |
36cb224
to
2e6b2ed
Compare
I refactor the code addressing almost all of the comments. Some things are left as todo for next PR(s). What is missing from this PR is the predicates (places in the code that use tolerance) that should be written as orientation checks implemented in strategies. I will work on it in the next days. |
2e6b2ed
to
571d387
Compare
include/boost/geometry/algorithms/detail/is_valid/polyhedral_surface.hpp
Outdated
Show resolved
Hide resolved
return strategy.side().apply(p1_2, p2_2, p3_2) != 0; | ||
} | ||
|
||
// Try to find 3 non-collinear points |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
in any plane?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
3 non-colinear points will define a plane. Later in the code we check if the rest of the points of the polygon lay on this plane.
include/boost/geometry/algorithms/detail/is_valid/polyhedral_surface.hpp
Outdated
Show resolved
Hide resolved
include/boost/geometry/algorithms/detail/is_valid/polyhedral_surface.hpp
Outdated
Show resolved
Hide resolved
return false; // All points are collinear | ||
} | ||
|
||
static point_2d project_point_to_plane_2d(vec3 const& v1, vec3 const& v2, vec3 const& p) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Without comments, and drawings, and much experience, it's hard to follow this...
auto ring = boost::geometry::exterior_ring(polygon); | ||
if (boost::geometry::closure<ring_2d>::value == boost::geometry::closed) | ||
{ | ||
ring.pop_back(); // Remove the closing point to make it open |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We have also an iterator doing this automatically. It closes it automatically (which is similar - but you can then always skip the last point).
That avoids the need for a copy of the ring.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks, I will use closing_iterator
.
include/boost/geometry/algorithms/detail/is_valid/polyhedral_surface.hpp
Outdated
Show resolved
Hide resolved
return point_2d{qvm::dot(v1, p), qvm::dot(v2, p)}; | ||
} | ||
|
||
template <typename VisitPolicy> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So what does it do exactly? It finds if the segment point1-point2 overlaps with any segment of the outer ring of the polygon?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
To check intersection of a segment point1-point2 and a polygon (all in 3D) it computes the intersection of the segment with the plane of the the polygon and then checks whether this projected intersection point overlaps with the projected polygon.
// Check if the polygon has been projected before | ||
if (!boost::geometry::is_empty(m_polygon_projections[polygon_index].projected_polygon)) | ||
{ | ||
auto const& projection = m_polygon_projections[polygon_index]; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Shouldn't we project all polygons? Are there cases where projecting a polygon is unnecessary work?
If we project all of them, we could do that at the beginning.
But probably there are conditions that we can stop already, so then this approach is better indeed. That could use a comment.
else | ||
{ | ||
// TODO: Support interior rings | ||
ring_3d const ring = boost::geometry::exterior_ring(polygon); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's a copy...
using ring_3d = typename boost::geometry::ring_type<polygon_3d>::type; | ||
using point_3d = typename boost::geometry::point_type<ring_3d>::type; | ||
using segment_3d = typename boost::geometry::model::segment<point_3d>; | ||
using set_vec_from_point = typename strategy::transform::detail::matrix_transformer:: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The name is not 100% clear to me.
But (I see now) we have that already for a long time.
It sets a vector from a point?
Anyway, the using
clause is fine then...
return false; | ||
} | ||
|
||
// Check if a segment intersects a polygon in 3D space |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So also here point1
and point2
form a segment, it would be good to make that clear.
intersection_type check_polygon_segment_intersection(point_3d const& point1, | ||
point_3d const& point2, | ||
polygon_3d const& polygon, | ||
size_t const& polygon_index, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
-> std::size_t
and for this one the &
is not necessary (neither harmful but we never do it). const
we often do neither but it would actually be good so that can be kept.
vec3 p1; | ||
vec3 p2; | ||
set_vec_from_point::apply(point1, p1); | ||
set_vec_from_point::apply(point2, p2); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why not v1
and v2
here, if these are vectors?
And then below you have another set of v1
and v2
, it's not clear to me...
// Check if the segment intersects the plane of the polygon | ||
coordinate_type const dot_product = dot(normal, base); | ||
coordinate_type const sign1 = dot(normal, p1) - dot_product; | ||
coordinate_type const sign2 = dot(normal, p2) - dot_product; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The p1
and p2
are calculated long before...
Would it be possible to move that to here? (And rename them to something vector-like)?
571d387
to
0295306
Compare
@barendgehrels thanks for you comments! I addressed most of them. I need to work more on this PR. Apart from the orient3d some other simplifications can be implemented (the unaddressed comments of Barend are related to those simplifications). I will work on it next week. |
This PR introduces support for the
is_valid
algorithm for polyhedral surface geometries. A polyhedral_surface is a 3D composite geometry formed by a collection of polygonal faces that meet at shared edges. The implementation follows the OGC Simple Features Specification and introduces structural and topological checks for polyhedral surfaces.The following properties should hold (following OGC standard):
Six new validity failure types has been introduced that reflect the description of the standard.
Below there is a matrix with valid and invalid cases with visualizations. Those cases (and a few more) are included in the unit tests of this PR.
Two unit tests are failing and are commented out. Those are dependent on #1406