diff --git a/src/manifold/include/manifold.h b/src/manifold/include/manifold.h index 283b9346e..2ad2e7213 100644 --- a/src/manifold/include/manifold.h +++ b/src/manifold/include/manifold.h @@ -206,6 +206,7 @@ class Manifold { Manifold Transform(const glm::mat4x3&) const; Manifold Mirror(glm::vec3) const; Manifold Warp(std::function) const; + Manifold Offset(float delta, int circularSegments = 0) const; Manifold SetProperties( int, std::function) const; Manifold CalculateCurvature(int gaussianIdx, int meanIdx) const; diff --git a/src/manifold/src/manifold.cpp b/src/manifold/src/manifold.cpp index 124a67f86..2ad34c63d 100644 --- a/src/manifold/src/manifold.cpp +++ b/src/manifold/src/manifold.cpp @@ -41,6 +41,32 @@ struct MakeTri { } }; +struct ConvexEdge { + VecView vertConvex; + VecView halfedge; + VecView vertPos; + VecView faceNormal; + const bool inset; + + bool operator()(int idx) { + const Halfedge edge = halfedge[idx]; + if (!edge.IsForward()) return false; + + const glm::vec3 normal0 = faceNormal[edge.face]; + const glm::vec3 normal1 = faceNormal[halfedge[edge.pairedHalfedge].face]; + if (glm::all(glm::equal(normal0, normal1))) return false; + + const glm::vec3 edgeVec = vertPos[edge.endVert] - vertPos[edge.startVert]; + const bool convex = ((inset ? -1 : 1) * + glm::dot(edgeVec, glm::cross(normal0, normal1))) > 0; + if (convex) { + vertConvex[edge.startVert] = true; + vertConvex[edge.endVert] = true; + } + return convex; + } +}; + struct UpdateProperties { float* properties; const int numProp; @@ -569,6 +595,92 @@ Manifold Manifold::Warp(std::function warpFunc) const { return Manifold(std::make_shared(pImpl)); } +/** + * Inflate the Manifold by the specified delta, rounding convex vertices. + * + * @param delta Positive deltas will add volume to all surfaces of the Manifold, + * dilating it. Negative deltas will have the opposite effect, eroding it. + * @param circularSegments Denotes the resolution of the sphere used at convex + * vertices. Default is calculated by the static Quality defaults according to + * the radius, which is delta. + */ +Manifold Manifold::Offset(float delta, int circularSegments) const { + auto pImpl = std::make_shared(*GetCsgLeafNode().GetImpl()); + + if (delta == 0) { + return Manifold(std::make_shared(pImpl)); + } + + const bool inset = delta < 0; + const float radius = glm::abs(delta); + const int n = circularSegments > 0 ? (circularSegments + 3) / 4 + : Quality::GetCircularSegments(delta) / 4; + const Manifold sphere = Manifold::Sphere(radius, circularSegments); + const Manifold cylinder = Manifold::Cylinder(1, radius, radius, 4 * n); + const SimplePolygon triangle = {{-1, -1}, {1, 0}, {0, 1}}; + const Manifold block = Manifold::Extrude(triangle, 1); + + Vec convexEdges(NumEdge()); + Vec vertConvex(NumVert(), false); + convexEdges.resize( + copy_if(countAt(0), countAt(pImpl->halfedge_.size()), convexEdges.begin(), + ConvexEdge({vertConvex, pImpl->halfedge_, pImpl->vertPos_, + pImpl->faceNormal_, inset})) - + convexEdges.begin()); + + Vec convexVerts(NumVert()); + convexVerts.resize(copy_if(countAt(0), countAt(NumVert()), vertConvex.begin(), + convexVerts.begin(), thrust::identity()) - + convexVerts.begin()); + + const int edgeOffset = 1 + NumTri(); + const int vertOffset = edgeOffset + convexEdges.size(); + std::vector batch(vertOffset + convexVerts.size()); + batch[0] = *this; + + for_each_n(countAt(0), NumTri(), [&batch, &block, &pImpl, radius](int tri) { + glm::mat3 triPos; + for (const int i : {0, 1, 2}) { + triPos[i] = pImpl->vertPos_[pImpl->halfedge_[3 * tri + i].startVert]; + } + const glm::vec3 normal = radius * pImpl->faceNormal_[tri]; + batch[1 + tri] = block.Warp([triPos, normal](glm::vec3& pos) { + const float dir = pos.z > 0 ? 1.0f : -1.0f; + if (pos.x < 0) { + pos = triPos[0]; + } else if (pos.x > 0) { + pos = triPos[1]; + } else { + pos = triPos[2]; + } + pos += dir * normal; + }); + }); + + for_each_n(countAt(0), convexEdges.size(), + [&batch, &cylinder, &pImpl, &convexEdges, edgeOffset](int idx) { + const Halfedge halfedge = pImpl->halfedge_[convexEdges[idx]]; + glm::vec3 edge = pImpl->vertPos_[halfedge.endVert] - + pImpl->vertPos_[halfedge.startVert]; + const float length = glm::length(edge); + // Reverse RotateUp + edge.x *= -1; + edge.y *= -1; + batch[edgeOffset + idx] = + cylinder.Scale({1, 1, length}) + .Transform(RotateUp(edge)) + .Translate(pImpl->vertPos_[halfedge.startVert]); + }); + + for_each_n(countAt(0), convexVerts.size(), + [&batch, &sphere, &pImpl, &convexVerts, vertOffset](int idx) { + batch[vertOffset + idx] = + sphere.Translate(pImpl->vertPos_[convexVerts[idx]]); + }); + + return BatchBoolean(batch, inset ? OpType::Subtract : OpType::Add); +} + /** * Create a new copy of this manifold with updated vertex properties by * supplying a function that takes the existing position and properties as diff --git a/src/utilities/include/public.h b/src/utilities/include/public.h index 48945ade3..2fa222992 100644 --- a/src/utilities/include/public.h +++ b/src/utilities/include/public.h @@ -78,7 +78,10 @@ inline float cosd(float x) { return sind(x + 90.0f); } inline glm::mat4x3 RotateUp(glm::vec3 up) { up = glm::normalize(up); glm::vec3 axis = glm::cross(up, {0, 0, 1}); - float angle = glm::asin(glm::length(axis)); + float length = glm::length(axis); + if (!isfinite(length)) length = 0; + float angle = glm::asin(glm::min(length, 1.0f)); + if (length == 0) axis = {1, 0, 0}; if (glm::dot(up, {0, 0, 1}) < 0) angle = glm::pi() - angle; return glm::mat4x3(glm::rotate(glm::mat4(1), angle, axis)); } diff --git a/test/boolean_test.cpp b/test/boolean_test.cpp index 1ed7a2bc2..7ed46a9ad 100644 --- a/test/boolean_test.cpp +++ b/test/boolean_test.cpp @@ -592,6 +592,25 @@ TEST(Boolean, Subtract) { first.GetMesh(); } +TEST(Boolean, Offset) { + PolygonParams().processOverlaps = true; + + Manifold cutout = Manifold::Cube(glm::vec3(100), true) - Manifold::Sphere(60); + Manifold fat = cutout.Offset(5); + Manifold thin = cutout.Offset(-5); + EXPECT_EQ(fat.Genus(), cutout.Genus()); + EXPECT_EQ(thin.Genus(), -7); + +#ifdef MANIFOLD_EXPORT + if (options.exportModels) { + ExportMesh("fat.glb", fat.GetMesh(), {}); + ExportMesh("thin.glb", thin.GetMesh(), {}); + } +#endif + + PolygonParams().processOverlaps = false; +} + TEST(Boolean, Close) { PolygonParams().processOverlaps = true;