Partial Revolve (#455)

* 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
This commit is contained in:
John Collins
2023-07-04 15:52:48 -07:00
committed by GitHub
parent dd3445ddb1
commit 6cec951aed
5 changed files with 161 additions and 78 deletions
+6 -4
View File
@@ -182,8 +182,9 @@ Module.setup = function() {
return (center ? man.translate([0., 0., -height / 2.]) : man);
};
Module.CrossSection.prototype.revolve = function(circularSegments = 0) {
return Module._Revolve(this, circularSegments);
Module.CrossSection.prototype.revolve = function(
circularSegments = 0, revolveDegrees = 360.0) {
return Module._Revolve(this, circularSegments, revolveDegrees);
};
Module.CrossSection.prototype.add = function(other) {
@@ -555,11 +556,12 @@ Module.setup = function() {
return cs.extrude(height, nDivisions, twistDegrees, scaleTop, center);
};
Module.Manifold.revolve = function(polygons, circularSegments = 0) {
Module.Manifold.revolve = function(
polygons, circularSegments = 0, revolveDegrees = 360.0) {
const cs = (polygons instanceof CrossSectionCtor) ?
polygons :
Module.CrossSection(polygons, 'Positive');
return cs.revolve(circularSegments);
return cs.revolve(circularSegments, revolveDegrees);
};
Module.Manifold.reserveIDs = function(n) {
+5 -3
View File
@@ -91,7 +91,7 @@ export class CrossSection {
* @param circularSegments Number of segments along its diameter. Default is
* calculated by the static Defaults.
*/
revolve(circularSegments?: number): Manifold;
revolve(circularSegments?: number, revolveDegrees?: number): Manifold;
// Transformations
@@ -421,9 +421,11 @@ export class Manifold {
* @param polygons A set of non-overlapping polygons to revolve.
* @param circularSegments Number of segments along its diameter. Default is
* calculated by the static Defaults.
* @param revolveDegrees Number of degrees to revolve. Default is 360 degrees.
*/
static revolve(polygons: CrossSection|Polygons, circularSegments?: number):
Manifold;
static revolve(
polygons: CrossSection|Polygons, circularSegments?: number,
revolveDegrees?: number): Manifold;
// Mesh Conversion
+2 -1
View File
@@ -141,7 +141,8 @@ class Manifold {
int nDivisions = 0, float twistDegrees = 0.0f,
glm::vec2 scaleTop = glm::vec2(1.0f));
static Manifold Revolve(const CrossSection& crossSection,
int circularSegments = 0);
int circularSegments = 0,
float revolveDegrees = 360.0f);
///@}
/** @name Topological
+96 -65
View File
@@ -317,92 +317,123 @@ Manifold Manifold::Extrude(const CrossSection& crossSection, float height,
* @param crossSection A set of non-overlapping polygons to revolve.
* @param circularSegments Number of segments along its diameter. Default is
* calculated by the static Defaults.
* @param revolveDegrees Number of degrees to revolve. Default is 360 degrees.
*/
Manifold Manifold::Revolve(const CrossSection& crossSection,
int circularSegments) {
auto polygons = crossSection.ToPolygons();
int circularSegments, float revolveDegrees) {
Polygons polygons = crossSection.ToPolygons();
if (polygons.size() == 0) {
return Invalid();
}
const Rect bounds = crossSection.Bounds();
if (bounds.max.x <= 0) {
return Invalid();
} else if (bounds.min.x < 0) {
// Take the x>=0 slice.
glm::vec2 min = bounds.min;
glm::vec2 max = bounds.max;
CrossSection posBoundingBox = CrossSection(
{{0.0, min.y}, {max.x, min.y}, {max.x, max.y}, {0.0, max.y}});
// Can't use RectClip because it can't distinguish holes from outlines.
polygons = (crossSection ^ posBoundingBox).ToPolygons();
}
float radius = 0.0f;
for (const auto& poly : polygons) {
for (const auto& vert : poly) {
radius = fmax(radius, vert.x);
}
}
int nDivisions = circularSegments > 2 ? circularSegments
: Quality::GetCircularSegments(radius);
if (revolveDegrees > 360.0f) {
revolveDegrees = 360.0f;
}
const bool isFullRevolution = revolveDegrees == 360.0f;
const int nDivisions = circularSegments > 2
? circularSegments
: Quality::GetCircularSegments(radius);
auto pImpl_ = std::make_shared<Impl>();
auto& vertPos = pImpl_->vertPos_;
VecDH<glm::ivec3> triVertsDH;
auto& triVerts = triVertsDH;
float dPhi = 360.0f / nDivisions;
std::vector<int> startPoses;
std::vector<int> endPoses;
const float dPhi = revolveDegrees / nDivisions;
// first and last slice are distinguished if not a full revolution.
const int nSlices = isFullRevolution ? nDivisions : nDivisions + 1;
for (const auto& poly : polygons) {
int start = -1;
for (int polyVert = 0; polyVert < poly.size(); ++polyVert) {
if (poly[polyVert].x <= 0) {
start = polyVert;
break;
std::size_t nPosVerts = 0;
std::size_t nRevolveAxisVerts = 0;
for (auto& pt : poly) {
if (pt.x > 0) {
nPosVerts++;
} else {
nRevolveAxisVerts++;
}
}
if (start == -1) { // poly all positive
for (int polyVert = 0; polyVert < poly.size(); ++polyVert) {
int startVert = vertPos.size();
int lastStart =
startVert +
(polyVert == 0 ? nDivisions * (poly.size() - 1) : -nDivisions);
for (int slice = 0; slice < nDivisions; ++slice) {
int lastSlice = (slice == 0 ? nDivisions : slice) - 1;
float phi = slice * dPhi;
glm::vec2 pos = poly[polyVert];
vertPos.push_back({pos.x * cosd(phi), pos.x * sind(phi), pos.y});
triVerts.push_back({startVert + slice, startVert + lastSlice,
lastStart + lastSlice});
triVerts.push_back(
{lastStart + lastSlice, lastStart + slice, startVert + slice});
for (int polyVert = 0; polyVert < poly.size(); ++polyVert) {
const int startPosIndex = vertPos.size();
if (!isFullRevolution) startPoses.push_back(startPosIndex);
const int prevStartPosIndex =
startPosIndex +
(polyVert == 0 ? nRevolveAxisVerts + (nSlices * nPosVerts) - nSlices
: poly[polyVert - 1].x == 0.0 ? -1
: -nSlices);
glm::vec2 currPolyVertex = poly[polyVert];
glm::vec2 prevPolyVertex =
poly[polyVert == 0 ? poly.size() - 1 : polyVert - 1];
for (int slice = 0; slice < nSlices; ++slice) {
const float phi = slice * dPhi;
if (slice == 0 || currPolyVertex.x > 0) {
vertPos.push_back({currPolyVertex.x * cosd(phi),
currPolyVertex.x * sind(phi), currPolyVertex.y});
}
if (isFullRevolution || slice > 0) {
const int lastSlice = (slice == 0 ? nDivisions : slice) - 1;
if (currPolyVertex.x > 0.0) {
triVerts.push_back(
{startPosIndex + slice, startPosIndex + lastSlice,
// "Reuse" vertex of first slice if it lies on the revolve axis
(prevPolyVertex.x == 0.0 ? prevStartPosIndex
: prevStartPosIndex + lastSlice)});
}
if (prevPolyVertex.x > 0.0) {
triVerts.push_back(
{prevStartPosIndex + lastSlice, prevStartPosIndex + slice,
(currPolyVertex.x == 0.0 ? startPosIndex
: startPosIndex + slice)});
}
}
}
} else { // poly crosses zero
int polyVert = start;
glm::vec2 pos = poly[polyVert];
do {
glm::vec2 lastPos = pos;
polyVert = (polyVert + 1) % poly.size();
pos = poly[polyVert];
if (pos.x > 0) {
if (lastPos.x <= 0) {
float a = pos.x / (pos.x - lastPos.x);
vertPos.push_back({0.0f, 0.0f, glm::mix(pos.y, lastPos.y, a)});
}
int startVert = vertPos.size();
for (int slice = 0; slice < nDivisions; ++slice) {
int lastSlice = (slice == 0 ? nDivisions : slice) - 1;
float phi = slice * dPhi;
glm::vec2 pos = poly[polyVert];
vertPos.push_back({pos.x * cosd(phi), pos.x * sind(phi), pos.y});
if (lastPos.x > 0) {
triVerts.push_back({startVert + slice, startVert + lastSlice,
startVert - nDivisions + lastSlice});
triVerts.push_back({startVert - nDivisions + lastSlice,
startVert - nDivisions + slice,
startVert + slice});
} else {
triVerts.push_back(
{startVert - 1, startVert + slice, startVert + lastSlice});
}
}
} else if (lastPos.x > 0) {
int startVert = vertPos.size();
float a = pos.x / (pos.x - lastPos.x);
vertPos.push_back({0.0f, 0.0f, glm::mix(pos.y, lastPos.y, a)});
for (int slice = 0; slice < nDivisions; ++slice) {
int lastSlice = (slice == 0 ? nDivisions : slice) - 1;
triVerts.push_back({startVert, startVert - nDivisions + lastSlice,
startVert - nDivisions + slice});
}
}
} while (polyVert != start);
if (!isFullRevolution) endPoses.push_back(vertPos.size() - 1);
}
}
// Add front and back triangles if not a full revolution.
if (!isFullRevolution) {
std::vector<glm::ivec3> frontTriangles =
Triangulate(polygons, pImpl_->precision_);
for (auto& t : frontTriangles) {
triVerts.push_back({startPoses[t.x], startPoses[t.y], startPoses[t.z]});
}
for (auto& t : frontTriangles) {
triVerts.push_back({endPoses[t.z], endPoses[t.y], endPoses[t.x]});
}
}
+52 -5
View File
@@ -197,13 +197,28 @@ TEST(Manifold, ExtrudeCone) {
EXPECT_FLOAT_EQ(donut.GetProperties().volume, 4.0f);
}
Polygons RotatePolygons(Polygons polys, const int index) {
Polygons rotatedPolys;
for (auto& polygon : polys) {
auto rotatedPolygon = polygon;
std::rotate(rotatedPolygon.begin(), rotatedPolygon.begin() + index,
rotatedPolygon.end());
rotatedPolys.push_back(rotatedPolygon);
}
return rotatedPolys;
}
TEST(Manifold, Revolve) {
Polygons polys = SquareHole();
Manifold vug = Manifold::Revolve(polys, 48);
EXPECT_EQ(vug.Genus(), -1);
auto prop = vug.GetProperties();
EXPECT_NEAR(prop.volume, 14.0f * glm::pi<float>(), 0.2f);
EXPECT_NEAR(prop.surfaceArea, 30.0f * glm::pi<float>(), 0.2f);
Manifold vug;
for (int i = 0; i < polys[0].size(); i++) {
Polygons rotatedPolys = RotatePolygons(polys, i);
vug = Manifold::Revolve(rotatedPolys, 48);
EXPECT_EQ(vug.Genus(), -1);
auto prop = vug.GetProperties();
EXPECT_NEAR(prop.volume, 14.0f * glm::pi<float>(), 0.2f);
EXPECT_NEAR(prop.surfaceArea, 30.0f * glm::pi<float>(), 0.2f);
}
}
TEST(Manifold, Revolve2) {
@@ -215,6 +230,38 @@ TEST(Manifold, Revolve2) {
EXPECT_NEAR(prop.surfaceArea, 96.0f * glm::pi<float>(), 1.0f);
}
TEST(Manifold, PartialRevolveOnYAxis) {
Polygons polys = SquareHole(2.0f);
Polygons offsetPolys = SquareHole(10.0f);
Manifold revolute;
for (int i = 0; i < polys[0].size(); i++) {
Polygons rotatedPolys = RotatePolygons(polys, i);
revolute = Manifold::Revolve(rotatedPolys, 48, 180);
EXPECT_EQ(revolute.Genus(), 1);
auto prop = revolute.GetProperties();
EXPECT_NEAR(prop.volume, 24.0f * glm::pi<float>(), 1.0f);
EXPECT_NEAR(
prop.surfaceArea,
48.0f * glm::pi<float>() + 4.0f * 4.0f * 2.0f - 2.0f * 2.0f * 2.0f,
1.0f);
}
}
TEST(Manifold, PartialRevolveOffset) {
Polygons polys = SquareHole(10.0f);
Manifold revolute;
for (int i = 0; i < polys[0].size(); i++) {
Polygons rotatedPolys = RotatePolygons(polys, i);
revolute = Manifold::Revolve(rotatedPolys, 48, 180);
auto prop = revolute.GetProperties();
EXPECT_EQ(revolute.Genus(), 1);
EXPECT_NEAR(prop.surfaceArea, 777.0f, 1.0f);
EXPECT_NEAR(prop.volume, 376.0f, 1.0f);
}
}
TEST(Manifold, Warp) {
CrossSection square = CrossSection::Square({1, 1});
Manifold shape = Manifold::Extrude(square, 2, 10).Warp([](glm::vec3& v) {