Skip to content

Commit

Permalink
Partial Revolve (#455)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
cartesian-theatrics authored Jul 4, 2023
1 parent dd3445d commit 6cec951
Show file tree
Hide file tree
Showing 5 changed files with 159 additions and 76 deletions.
10 changes: 6 additions & 4 deletions bindings/wasm/bindings.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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) {
Expand Down
8 changes: 5 additions & 3 deletions bindings/wasm/manifold-encapsulated-types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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

Expand Down
3 changes: 2 additions & 1 deletion src/manifold/include/manifold.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
157 changes: 94 additions & 63 deletions src/manifold/src/constructors.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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});
}
}
} 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});
}

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)});
}
} 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});

if (prevPolyVertex.x > 0.0) {
triVerts.push_back(
{prevStartPosIndex + lastSlice, prevStartPosIndex + slice,
(currPolyVertex.x == 0.0 ? startPosIndex
: startPosIndex + 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]});
}
}

Expand Down
57 changes: 52 additions & 5 deletions test/manifold_test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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) {
Expand Down

0 comments on commit 6cec951

Please sign in to comment.