-
Notifications
You must be signed in to change notification settings - Fork 114
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
Partial Revolve #455
Partial Revolve #455
Conversation
Thanks for your pull request! It looks like this may be your first contribution to a Google open source project. Before we can look at your pull request, you'll need to sign a Contributor License Agreement (CLA). View this failed invocation of the CLA check for more information. For the most up to date status, view the checks section at the bottom of the pull request. |
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, this looks promising. Any trouble signing the CLA?
test/manifold_test.cpp
Outdated
TEST(Manifold, PartialRevolve) { | ||
Polygons polys = SquareHole(2.0f); | ||
Manifold donutHole = Manifold::Revolve(polys, 48, 180); | ||
EXPECT_EQ(donutHole.Genus(), 1); |
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.
Would you mind pasting an image of this object? I'm having trouble visualizing what axis it's rotating around.
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.
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'd recommend pushing it either in or out a little, since right now it's just rounding error determining a major topological change in the center.
src/manifold/src/constructors.cpp
Outdated
if (currPolyVertex.x > 0.0) { | ||
triVerts.push_back({startPosIndex + slice, | ||
startPosIndex + lastSlice, | ||
// "Reuse" vertex of first slice if it lies on the revolve axis |
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.
Is this the part you were saying messes up the genus? Can you give an example?
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.
Kinda. Genus "breaks" when you don't have any special handling of vertices lying on the Y axis. When x=0, {pos.x * manifold::cosd(phi), pos.x * manifold::sind(phi), pos.y}
yields coincident vertices and degenerate triangles. For example, when a square is offset from the Y axis Revolve will yield 8 triangles and 4 vertices per slice, whereas if one edge is on the Y axis, Revolve will yield 4 triangles and two vertices per slice. In my initial implementation I didn't have any special handling of such cases but surprisingly it still mostly worked. But Genus was off I guess because Manifold considered the revolved square to be an infinitesimal toroid.
This is the original loop that "works":
for (const auto& poly : polygons) {
for (std::size_t polyVert = 0; polyVert < poly.size(); ++polyVert) {
int startVert = vertPos.size();
if (!isFullRevolution)
startPoses.push_back(startVert);
// first and last slice are distinguished if not a full revolution.
int nSlices = isFullRevolution ? nDivisions : nDivisions + 1;
int lastStart = startVert + (polyVert == 0 ? nSlices * (poly.size() - 1) : -nSlices);
for (int slice = 0; slice < nSlices; ++slice) {
float phi = slice * dPhi;
glm::vec2 pos = poly[polyVert];
glm::vec3 p = {pos.x * manifold::cosd(phi), pos.x * manifold::sind(phi), pos.y};
vertPos.push_back(p);
int lastSlice = (slice == 0 ? nDivisions : slice) - 1;
if (isFullRevolution || slice > 0) {
triVerts.push_back({startVert + slice, startVert + lastSlice,
lastStart + lastSlice});
triVerts.push_back(
{lastStart + lastSlice, lastStart + slice, startVert + slice});
}
}
if (!isFullRevolution)
endPoses.push_back(vertPos.size() -1);
}
}
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.
Right, so the problem is without special handling of the center verts you end up with an infinitesimal hole through it like a bead. Definitely important to avoid that, since I don't think our decimator can fix it. If your test case doesn't already check that, it's probably a good idea to add a test for that specifically.
src/manifold/src/constructors.cpp
Outdated
CrossSection posBoundingBox = CrossSection({{0.0, min.y},{max.x, min.y}, | ||
{max.x,max.y},{0.0,max.y}}); | ||
|
||
// Can't use RectClip as it has many failure cases. |
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 seems odd - what failure cases have you seen?
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 wish I would have kept failing cases. I will try to recreate them.
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.
Even in the plain Revolve test case this doesn't seem to work.
if (bounds.min.x < 0) {
glm::vec2 min = bounds.min;
glm::vec2 max = bounds.max;
Rect boundingBox = Rect({0.0, min.y}, {max.x, max.y});
// Can't use RectClip as it has many failure cases.
polygons = crossSection.RectClip(boundingBox).ToPolygons();
}
Result is not manifold-2, doesn't export. Am I constructing the Rect wrong?
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'm not seeing a manifoldness failure, but I'm definitely reproducing a serious problem with RectClip
: When I clip a polygon with a hole in half, I'm only getting the outer contour; intersection gives the correct result. @geoffder would you mind taking a look?
This is fine for this PR, thanks!
src/manifold/src/constructors.cpp
Outdated
std::vector<int> startPoses; | ||
std::vector<int> endPoses; | ||
|
||
float dPhi = revolveDegrees / nDivisions; |
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've been trying to update our style to use const
as much as possible; would you mind applying that across your PR?
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.
Yep, good call.
@cartesian-theatrics Is anything blocking you on this PR? I would like to merge it. |
My apologies, got distracted with other priorities. I will work this today. |
Codecov ReportPatch coverage:
Additional details and impacted files@@ Coverage Diff @@
## master #455 +/- ##
==========================================
+ Coverage 89.94% 90.09% +0.15%
==========================================
Files 35 35
Lines 4286 4291 +5
==========================================
+ Hits 3855 3866 +11
+ Misses 431 425 -6
☔ View full report in Codecov by Sentry. |
Thanks! Do whatever's easiest with the CLA - if you want to make a different branch with a new PR, that's fine. Let us know when you're ready for review. |
327ccbc
to
e59a9d1
Compare
4c0bbc6
to
0ad0b10
Compare
@elalish other than the unresolved RectClip issue (see my comment), I think this is ready for review. |
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 looking great, just a few minor things. Thanks for cleaning up my code!
Manifold; | ||
static revolve( | ||
polygons: CrossSection|Polygons, circularSegments?: number, | ||
revolveDegrees?: number): Manifold; |
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 also need to update CrossSection.revolve
?
test/manifold_test.cpp
Outdated
rotatedPolygon.end()); | ||
rotatedPolys.push_back(rotatedPolygon); | ||
} | ||
Manifold vug = Manifold::Revolve(rotatedPolys, 48); |
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 for expanding this test case - I think you can remove the unused vug
at the top now.
test/manifold_test.cpp
Outdated
std::rotate(rotatedPolygon.begin(), rotatedPolygon.begin() + i, | ||
rotatedPolygon.end()); | ||
rotatedOffsetPolys.push_back(rotatedPolygon); | ||
} |
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.
Given the reuse, perhaps this could be a test helper function?
test/manifold_test.cpp
Outdated
@@ -14,6 +14,8 @@ | |||
|
|||
#include "manifold.h" | |||
|
|||
#include <iostream> |
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.
necessary?
@@ -215,6 +227,44 @@ TEST(Manifold, Revolve2) { | |||
EXPECT_NEAR(prop.surfaceArea, 96.0f * glm::pi<float>(), 1.0f); | |||
} | |||
|
|||
TEST(Manifold, PartialRevolve) { | |||
Polygons polys = SquareHole(2.0f); | |||
Polygons offsetPolys = SquareHole(10.0f); |
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.
Since these appear to be independent, how about splitting them into two separate test cases?
src/manifold/src/constructors.cpp
Outdated
CrossSection posBoundingBox = CrossSection({{0.0, min.y},{max.x, min.y}, | ||
{max.x,max.y},{0.0,max.y}}); | ||
|
||
// Can't use RectClip as it has many failure cases. |
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'm not seeing a manifoldness failure, but I'm definitely reproducing a serious problem with RectClip
: When I clip a polygon with a hole in half, I'm only getting the outer contour; intersection gives the correct result. @geoffder would you mind taking a look?
This is fine for this PR, thanks!
This sounded familiar so I went to the Clipper2 issue tracker and it may be related to a known limitation mentioned in January AngusJohnson/Clipper2#386. edit: Had the feeling is see more of this story before, and there was followup discussion: AngusJohnson/Clipper2#383. So the multiple polygon results was fixed I think, but in his comments here he does mention that RectClip is fill rule agnostic as it was then (and still now based on your findings) so it doesn't know the difference between holes and outlines. |
Thanks, @geoffder, we can follow up on those Clipper2 threads. The behavior I saw was worse than the examples he showed, but probably the same issue. If we can't get a proper fill-rule preserving RectClip, I think I'd prefer to remove it from our API, due to this kind of confusion. |
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!
Hmm I just tested clipping a ring (circle with opposite winding circular hole) and got the same result with intersection and rect clip in Clipper2 (via my auto rect = Rect({0, 0}, {5, 5});
auto ring = CrossSection::Circle(4) - CrossSection::Circle(2);
auto clipped_ring = ring.RectClip(rect);
auto clipped_ring_ex = Manifold::Extrude(ring.RectClip(rect), 5); // empty result
auto inter_ring_ex = Manifold::Extrude(ring ^ rect.AsCrossSection(), 5); // works after AsCrossSection fix
EXPECT_EQ(clipped_ring.NumContour(), 1); // fails, with 2 Going by Angus' comments in the linked discussions, this is because While testing this, I've found a bug (my fault) with the winding of the path made in |
Following discussion on #455, this removes RectClip from our API, as it does not provide the same guarantees that Clipper2's regular boolean clipping operations give us (that make sure that CrossSections can be extruded successfully). Also, this fixes AsCrossSection which was returning empty CrossSections due to incorrect winding followed by a Positive fill rule union operation. Now it is working as advertised, while skipping over the needless clipping by implementing it as a CrossSection constructor (building a PathsD directly).
* handle verts on revolve axis * Tests working * Const declares, test cycle start vert * clang-format * clang-format test code * fix header declaration * WASM bindings * format bindings.js * clang format * Address comments
Following discussion on elalish#455, this removes RectClip from our API, as it does not provide the same guarantees that Clipper2's regular boolean clipping operations give us (that make sure that CrossSections can be extruded successfully). Also, this fixes AsCrossSection which was returning empty CrossSections due to incorrect winding followed by a Positive fill rule union operation. Now it is working as advertised, while skipping over the needless clipping by implementing it as a CrossSection constructor (building a PathsD directly).
This adds an optional arity for Revolve that supports partial revolutions. It changes the implementation slightly by first taking the x>=0 slice if the bounding box contains negative x values. The cases of vertices lying on the revolve axis is handled in the same loop, arguably simplifying the logic slightly and making it easier to support partial evolves. I believe the tessellation of full revolutions is equivalent to the previous version.
It might be worth noting it's not strictly required to implement special handling of coincident vertices. The logic is far simpler in this case and the core implementation seems to be able handle it no problem. However, it does break Genus. I think it's better to do the correct construction, but might be something to consider. The complexity overhead is quite high unfortunately... Perhaps it can be simplified.