// Copyright 2021 The Manifold Authors. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. #include "manifold.h" #include "cross_section.h" #include "test.h" #ifdef MANIFOLD_EXPORT #include "meshIO.h" #endif namespace { using namespace manifold; template int NumUnique(const std::vector& in) { std::set unique; for (const T& v : in) { unique.emplace(v); } return unique.size(); } } // namespace /** * This tests that turning a mesh into a manifold and returning it to a mesh * produces a consistent result. */ TEST(Manifold, GetMesh) { Manifold manifold = Manifold::Sphere(1); Mesh mesh_out = manifold.GetMesh(); Manifold manifold2(mesh_out); Mesh mesh_out2 = manifold2.GetMesh(); Identical(mesh_out, mesh_out2); } TEST(Manifold, GetMeshGL) { Manifold manifold = Manifold::Sphere(1); Mesh mesh_out = manifold.GetMesh(); MeshGL meshGL_out = manifold.GetMeshGL(); ASSERT_EQ(meshGL_out.NumVert(), mesh_out.vertPos.size()); ASSERT_EQ(meshGL_out.NumTri(), mesh_out.triVerts.size()); for (int i = 0; i < meshGL_out.NumVert(); ++i) { for (const int j : {0, 1, 2}) { ASSERT_EQ(meshGL_out.vertProperties[3 * i + j], mesh_out.vertPos[i][j]); } } for (int i = 0; i < meshGL_out.NumTri(); ++i) { for (const int j : {0, 1, 2}) ASSERT_EQ(meshGL_out.triVerts[3 * i + j], mesh_out.triVerts[i][j]); } } TEST(Manifold, Empty) { Mesh emptyMesh; Manifold empty(emptyMesh); EXPECT_TRUE(empty.IsEmpty()); EXPECT_EQ(empty.Status(), Manifold::Error::NoError); } TEST(Manifold, ValidInput) { std::vector propTol = {0.1, 0.2}; MeshGL tetGL = TetGL(); Manifold tet(tetGL, propTol); EXPECT_FALSE(tet.IsEmpty()); EXPECT_EQ(tet.Status(), Manifold::Error::NoError); } TEST(Manifold, InvalidInput1) { Mesh in = Tet(); in.vertPos[2][1] = NAN; Manifold tet(in); EXPECT_TRUE(tet.IsEmpty()); EXPECT_EQ(tet.Status(), Manifold::Error::NonFiniteVertex); } TEST(Manifold, InvalidInput2) { Mesh in = Tet(); std::swap(in.triVerts[2][1], in.triVerts[2][2]); Manifold tet(in); EXPECT_TRUE(tet.IsEmpty()); EXPECT_EQ(tet.Status(), Manifold::Error::NotManifold); } TEST(Manifold, InvalidInput3) { Mesh in = Tet(); for (glm::ivec3& tri : in.triVerts) { for (int i : {0, 1, 2}) { if (tri[i] == 2) tri[i] = -2; } } Manifold tet(in); EXPECT_TRUE(tet.IsEmpty()); EXPECT_EQ(tet.Status(), Manifold::Error::VertexOutOfBounds); } TEST(Manifold, InvalidInput4) { Mesh in = Tet(); for (glm::ivec3& tri : in.triVerts) { for (int i : {0, 1, 2}) { if (tri[i] == 2) tri[i] = 4; } } Manifold tet(in); EXPECT_TRUE(tet.IsEmpty()); EXPECT_EQ(tet.Status(), Manifold::Error::VertexOutOfBounds); } TEST(Manifold, InvalidInput5) { MeshGL tetGL = TetGL(); tetGL.mergeFromVert[tetGL.mergeFromVert.size() - 1] = 7; Manifold tet(tetGL); EXPECT_TRUE(tet.IsEmpty()); EXPECT_EQ(tet.Status(), Manifold::Error::MergeIndexOutOfBounds); } TEST(Manifold, InvalidInput7) { MeshGL tetGL = TetGL(); tetGL.triVerts[tetGL.triVerts.size() - 1] = 7; Manifold tet(tetGL); EXPECT_TRUE(tet.IsEmpty()); EXPECT_EQ(tet.Status(), Manifold::Error::VertexOutOfBounds); } /** * ExpectMeshes performs a decomposition, so this test ensures that compose and * decompose are inverse operations. */ TEST(Manifold, Decompose) { std::vector manifoldList; manifoldList.emplace_back(Manifold::Tetrahedron()); manifoldList.emplace_back(Manifold::Cube()); manifoldList.emplace_back(Manifold::Sphere(1, 4)); Manifold manifolds = Manifold::Compose(manifoldList); ExpectMeshes(manifolds, {{8, 12}, {6, 8}, {4, 4}}); std::vector input; for (const Manifold& manifold : manifoldList) { EXPECT_GE(manifold.OriginalID(), 0); input.emplace_back(manifold.GetMeshGL()); } RelatedGL(manifolds, input); } /** * These tests check the various manifold constructors. */ TEST(Manifold, Sphere) { int n = 25; Manifold sphere = Manifold::Sphere(1.0f, 4 * n); EXPECT_EQ(sphere.NumTri(), n * n * 8); } TEST(Manifold, Normals) { Mesh cube = Manifold::Cube(glm::vec3(1), true).GetMesh(); const int nVert = cube.vertPos.size(); for (int i = 0; i < nVert; ++i) { glm::vec3 v = glm::normalize(cube.vertPos[i]); glm::vec3& n = cube.vertNormal[i]; EXPECT_FLOAT_EQ(v.x, n.x); EXPECT_FLOAT_EQ(v.y, n.y); EXPECT_FLOAT_EQ(v.z, n.z); } } TEST(Manifold, Extrude) { Polygons polys = SquareHole(); Manifold donut = Manifold::Extrude(polys, 1.0f, 3); EXPECT_EQ(donut.Genus(), 1); auto prop = donut.GetProperties(); EXPECT_FLOAT_EQ(prop.volume, 12.0f); EXPECT_FLOAT_EQ(prop.surfaceArea, 48.0f); } TEST(Manifold, ExtrudeCone) { Polygons polys = SquareHole(); Manifold donut = Manifold::Extrude(polys, 1.0f, 0, 0, glm::vec2(0.0f)); EXPECT_EQ(donut.Genus(), 0); 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; 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(), 0.2f); EXPECT_NEAR(prop.surfaceArea, 30.0f * glm::pi(), 0.2f); } } TEST(Manifold, Revolve2) { Polygons polys = SquareHole(2.0f); Manifold donutHole = Manifold::Revolve(polys, 48); EXPECT_EQ(donutHole.Genus(), 0); auto prop = donutHole.GetProperties(); EXPECT_NEAR(prop.volume, 48.0f * glm::pi(), 1.0f); EXPECT_NEAR(prop.surfaceArea, 96.0f * glm::pi(), 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(), 1.0f); EXPECT_NEAR( prop.surfaceArea, 48.0f * glm::pi() + 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) { v.x += v.z * v.z; }); auto propBefore = shape.GetProperties(); Manifold simplified = Manifold::Compose({shape}); auto propAfter = simplified.GetProperties(); EXPECT_NEAR(propBefore.volume, propAfter.volume, 0.0001); EXPECT_NEAR(propBefore.surfaceArea, propAfter.surfaceArea, 0.0001); EXPECT_NEAR(propBefore.volume, 2, 0.0001); } TEST(Manifold, Warp2) { CrossSection circle = CrossSection::Circle(5, 20).Translate(glm::vec2(10.0, 10.0)); Manifold shape = Manifold::Extrude(circle, 2, 10).Warp([](glm::vec3& v) { int nSegments = 10; double angleStep = 2.0 / 3.0 * glm::pi() / nSegments; int zIndex = nSegments - 1 - std::round(v.z); double angle = zIndex * angleStep; v.z = v.y; v.y = v.x * sin(angle); v.x = v.x * cos(angle); }); auto propBefore = shape.GetProperties(); Manifold simplified = Manifold::Compose({shape}); auto propAfter = simplified.GetProperties(); EXPECT_NEAR(propBefore.volume, propAfter.volume, 0.0001); EXPECT_NEAR(propBefore.surfaceArea, propAfter.surfaceArea, 0.0001); EXPECT_NEAR(propBefore.volume, 321, 1); } TEST(Manifold, Smooth) { Manifold tet = Manifold::Tetrahedron(); Manifold smooth = Manifold::Smooth(tet.GetMesh()); smooth = smooth.Refine(100); ExpectMeshes(smooth, {{20002, 40000}}); auto prop = smooth.GetProperties(); EXPECT_NEAR(prop.volume, 17.38, 0.1); EXPECT_NEAR(prop.surfaceArea, 33.38, 0.1); #ifdef MANIFOLD_EXPORT if (options.exportModels) ExportMesh("smoothTet.glb", smooth.GetMesh(), {}); #endif } TEST(Manifold, SmoothSphere) { int n[5] = {4, 8, 16, 32, 64}; float precision[5] = {0.03, 0.003, 0.003, 0.0005, 0.00006}; for (int i = 0; i < 5; ++i) { Manifold sphere = Manifold::Sphere(1, n[i]); // Refine(odd) puts a center point in the triangle, which is the worst case. Manifold smoothed = Manifold::Smooth(sphere.GetMesh()).Refine(7); Mesh out = smoothed.GetMesh(); auto bounds = std::minmax_element(out.vertPos.begin(), out.vertPos.end(), [](const glm::vec3& a, const glm::vec3& b) { return glm::dot(a, a) < glm::dot(b, b); }); float min = glm::length(*bounds.first); float max = glm::length(*bounds.second); EXPECT_NEAR(min, 1, precision[i]); EXPECT_NEAR(max, 1, precision[i]); } } TEST(Manifold, ManualSmooth) { // Unit Octahedron const Mesh oct = Manifold::Sphere(1, 4).GetMesh(); Mesh smooth = Manifold::Smooth(oct).GetMesh(); // Sharpen the edge from vert 4 to 5 smooth.halfedgeTangent[6] = {0, 0, 0, 1}; smooth.halfedgeTangent[22] = {0, 0, 0, 1}; smooth.halfedgeTangent[16] = {0, 0, 0, 1}; smooth.halfedgeTangent[18] = {0, 0, 0, 1}; Manifold interp(smooth); interp = interp.Refine(100); ExpectMeshes(interp, {{40002, 80000}}); auto prop = interp.GetProperties(); EXPECT_NEAR(prop.volume, 3.53, 0.01); EXPECT_NEAR(prop.surfaceArea, 11.39, 0.01); #ifdef MANIFOLD_EXPORT if (options.exportModels) { const Mesh out = interp.GetMesh(); ExportOptions options; options.faceted = false; options.mat.roughness = 0.1; options.mat.vertColor.resize(interp.NumVert()); const glm::vec4 red(1, 0, 0, 1); const glm::vec4 purple(1, 0, 1, 1); for (int tri = 0; tri < interp.NumTri(); ++tri) { for (int i : {0, 1, 2}) { const glm::vec3& uvw = {0.5, 0.5, 0.0}; const float alpha = glm::min(uvw[0], glm::min(uvw[1], uvw[2])); options.mat.vertColor[out.triVerts[tri][i]] = glm::mix(purple, red, glm::smoothstep(0.0f, 0.2f, alpha)); } } ExportMesh("sharpenedSphere.glb", out, options); } #endif } TEST(Manifold, SmoothMirrored) { const Mesh tet = Manifold::Tetrahedron().GetMesh(); Manifold smooth = Manifold::Smooth(tet); Manifold mirror = smooth.Scale({-1, 1, 2}).Refine(10); smooth = smooth.Refine(10).Scale({1, 1, 2}); auto prop0 = smooth.GetProperties(); auto prop1 = mirror.GetProperties(); EXPECT_NEAR(prop0.volume, prop1.volume, 0.1); EXPECT_NEAR(prop0.surfaceArea, prop1.surfaceArea, 0.1); } TEST(Manifold, Csaszar) { Manifold csaszar = Manifold::Smooth(Csaszar()); csaszar = csaszar.Refine(100); ExpectMeshes(csaszar, {{70000, 140000}}); auto prop = csaszar.GetProperties(); EXPECT_NEAR(prop.volume, 84699, 10); EXPECT_NEAR(prop.surfaceArea, 14796, 10); #ifdef MANIFOLD_EXPORT if (options.exportModels) { const Mesh out = csaszar.GetMesh(); ExportOptions options; options.faceted = false; options.mat.roughness = 0.1; options.mat.vertColor.resize(csaszar.NumVert()); const glm::vec4 blue(0, 0, 1, 1); const glm::vec4 yellow(1, 1, 0, 1); for (int tri = 0; tri < csaszar.NumTri(); ++tri) { for (int i : {0, 1, 2}) { const glm::vec3& uvw = {0.5, 0.5, 0.0}; const float alpha = glm::min(uvw[0], glm::min(uvw[1], uvw[2])); options.mat.vertColor[out.triVerts[tri][i]] = glm::mix(yellow, blue, glm::smoothstep(0.0f, 0.2f, alpha)); } } ExportMesh("smoothCsaszar.glb", out, options); } #endif } /** * These tests verify the calculation of a manifold's geometric properties. */ TEST(Manifold, GetProperties) { Manifold cube = Manifold::Cube(); auto prop = cube.GetProperties(); EXPECT_FLOAT_EQ(prop.volume, 1.0f); EXPECT_FLOAT_EQ(prop.surfaceArea, 6.0f); cube = cube.Scale(glm::vec3(-1.0f)); prop = cube.GetProperties(); EXPECT_FLOAT_EQ(prop.volume, 1.0f); EXPECT_FLOAT_EQ(prop.surfaceArea, 6.0f); } TEST(Manifold, Precision) { Manifold cube = Manifold::Cube(); EXPECT_FLOAT_EQ(cube.Precision(), kTolerance); cube = cube.Scale({0.1, 1, 10}); EXPECT_FLOAT_EQ(cube.Precision(), 10 * kTolerance); cube = cube.Translate({-100, -10, -1}); EXPECT_FLOAT_EQ(cube.Precision(), 100 * kTolerance); } TEST(Manifold, Precision2) { Manifold cube = Manifold::Cube(); cube = cube.Translate({-0.5, 0, 0}).Scale({2, 1, 1}); EXPECT_FLOAT_EQ(cube.Precision(), 2 * kTolerance); } TEST(Manifold, Precision3) { Manifold cylinder = Manifold::Cylinder(1, 1, 1, 1000); const auto prop = cylinder.GetProperties(); MeshGL mesh = cylinder.GetMeshGL(); mesh.precision = 0.001; mesh.faceID.clear(); Manifold cylinder2(mesh); const auto prop2 = cylinder2.GetProperties(); EXPECT_NEAR(prop.volume, prop2.volume, 0.001); EXPECT_NEAR(prop.surfaceArea, prop2.surfaceArea, 0.001); } /** * Curvature is the inverse of the radius of curvature, and signed such that * positive is convex and negative is concave. There are two orthogonal * principal curvatures at any point on a manifold, with one maximum and the * other minimum. Gaussian curvature is their product, while mean * curvature is their sum. Here we check our discrete approximations calculated * at each vertex against the constant expected values of spheres of different * radii and at different mesh resolutions. */ TEST(Manifold, CalculateCurvature) { const float precision = 0.015; for (int n = 4; n < 100; n *= 2) { const int gaussianIdx = 3; const int meanIdx = 4; Manifold sphere = Manifold::Sphere(1, 64).CalculateCurvature( gaussianIdx - 3, meanIdx - 3); MeshGL sphereGL = sphere.GetMeshGL(); ASSERT_EQ(sphereGL.numProp, 5); EXPECT_NEAR(GetMinProperty(sphereGL, meanIdx), 2, 2 * precision); EXPECT_NEAR(GetMaxProperty(sphereGL, meanIdx), 2, 2 * precision); EXPECT_NEAR(GetMinProperty(sphereGL, gaussianIdx), 1, precision); EXPECT_NEAR(GetMaxProperty(sphereGL, gaussianIdx), 1, precision); sphere = sphere.Scale(glm::vec3(2.0f)) .CalculateCurvature(gaussianIdx - 3, meanIdx - 3); sphereGL = sphere.GetMeshGL(); ASSERT_EQ(sphereGL.numProp, 5); EXPECT_NEAR(GetMinProperty(sphereGL, meanIdx), 1, precision); EXPECT_NEAR(GetMaxProperty(sphereGL, meanIdx), 1, precision); EXPECT_NEAR(GetMinProperty(sphereGL, gaussianIdx), 0.25, 0.25 * precision); EXPECT_NEAR(GetMaxProperty(sphereGL, gaussianIdx), 0.25, 0.25 * precision); } } /** * Testing more advanced Manifold operations. */ TEST(Manifold, Transform) { Manifold cube = Manifold::Cube({1, 2, 3}); Manifold cube2 = cube; cube = cube.Rotate(30, 40, 50).Scale({6, 5, 4}).Translate({1, 2, 3}); glm::mat3 rX(1.0f, 0.0f, 0.0f, // 0.0f, cosd(30), sind(30), // 0.0f, -sind(30), cosd(30)); glm::mat3 rY(cosd(40), 0.0f, -sind(40), // 0.0f, 1.0f, 0.0f, // sind(40), 0.0f, cosd(40)); glm::mat3 rZ(cosd(50), sind(50), 0.0f, // -sind(50), cosd(50), 0.0f, // 0.0f, 0.0f, 1.0f); glm::mat3 s = glm::mat3(1.0f); s[0][0] = 6; s[1][1] = 5; s[2][2] = 4; glm::mat4x3 transform = glm::mat4x3(s * rZ * rY * rX); transform[3] = glm::vec3(1, 2, 3); cube2 = cube2.Transform(transform); Identical(cube.GetMesh(), cube2.GetMesh()); } TEST(Manifold, MeshRelation) { Mesh gyroidMesh = Gyroid(); MeshGL gyroidMeshGL = WithIndexColors(gyroidMesh); Manifold gyroid(gyroidMeshGL); #ifdef MANIFOLD_EXPORT ExportOptions opt; opt.mat.roughness = 1; opt.mat.colorChannels = glm::ivec4(3, 4, 5, -1); if (options.exportModels) ExportMesh("gyroid.glb", gyroid.GetMeshGL(), opt); #endif RelatedGL(gyroid, {gyroidMeshGL}); } TEST(Manifold, MeshRelationTransform) { const Manifold cube = Manifold::Cube(); const MeshGL cubeGL = cube.GetMeshGL(); const Manifold turned = cube.Rotate(45, 90); RelatedGL(turned, {cubeGL}); } TEST(Manifold, MeshRelationRefine) { const Mesh in = Csaszar(); MeshGL inGL = WithIndexColors(in); Manifold csaszar(inGL); RelatedGL(csaszar, {inGL}); csaszar.Refine(4); RelatedGL(csaszar, {inGL}); } TEST(Manifold, MeshGLRoundTrip) { const Manifold cylinder = Manifold::Cylinder(2, 1); ASSERT_GE(cylinder.OriginalID(), 0); MeshGL inGL = cylinder.GetMeshGL(); const Manifold cylinder2(inGL); const MeshGL outGL = cylinder2.GetMeshGL(); ASSERT_EQ(inGL.runOriginalID.size(), 1); ASSERT_EQ(outGL.runOriginalID.size(), 1); ASSERT_EQ(outGL.runOriginalID[0], inGL.runOriginalID[0]); RelatedGL(cylinder2, {inGL}); } void CheckCube(const MeshGL& cubeSTL) { Manifold cube(cubeSTL); EXPECT_EQ(cube.NumTri(), 12); EXPECT_EQ(cube.NumVert(), 8); EXPECT_EQ(cube.NumPropVert(), 24); auto prop = cube.GetProperties(); EXPECT_FLOAT_EQ(prop.volume, 1.0f); EXPECT_FLOAT_EQ(prop.surfaceArea, 6.0f); } TEST(Manifold, Merge) { MeshGL cubeSTL = CubeSTL(); EXPECT_EQ(cubeSTL.NumTri(), 12); EXPECT_EQ(cubeSTL.NumVert(), 36); Manifold cubeBad(cubeSTL); EXPECT_TRUE(cubeBad.IsEmpty()); EXPECT_EQ(cubeBad.Status(), Manifold::Error::NotManifold); EXPECT_TRUE(cubeSTL.Merge()); CheckCube(cubeSTL); EXPECT_FALSE(cubeSTL.Merge()); EXPECT_EQ(cubeSTL.mergeFromVert.size(), 28); cubeSTL.mergeFromVert.resize(14); cubeSTL.mergeToVert.resize(14); EXPECT_TRUE(cubeSTL.Merge()); EXPECT_EQ(cubeSTL.mergeFromVert.size(), 28); CheckCube(cubeSTL); } TEST(Manifold, FaceIDRoundTrip) { const Manifold cube = Manifold::Cube(); ASSERT_GE(cube.OriginalID(), 0); MeshGL inGL = cube.GetMeshGL(); ASSERT_EQ(NumUnique(inGL.faceID), 6); inGL.faceID = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11}; const Manifold cube2(inGL); const MeshGL outGL = cube2.GetMeshGL(); ASSERT_EQ(NumUnique(outGL.faceID), 12); } TEST(Manifold, MirrorUnion) { auto a = Manifold::Cube({5., 5., 5.}, true); auto b = a.Translate({2.5, 2.5, 2.5}); auto result = a + b + b.Mirror({1, 1, 0}); #ifdef MANIFOLD_EXPORT if (options.exportModels) ExportMesh("manifold_mirror_union.glb", result.GetMesh(), {}); #endif auto vol_a = a.GetProperties().volume; EXPECT_FLOAT_EQ(vol_a * 2.75, result.GetProperties().volume); EXPECT_TRUE(a.Mirror(glm::vec3(0)).IsEmpty()); } TEST(Manifold, MirrorUnion2) { auto a = Manifold::Cube(); auto result = Manifold::Compose({a.Mirror({1, 0, 0})}); EXPECT_TRUE(result.MatchesTriNormals()); } TEST(Manifold, Invalid) { auto invalid = Manifold::Error::InvalidConstruction; auto circ = CrossSection::Circle(10.); auto empty_circ = CrossSection::Circle(-2.); auto empty_sq = CrossSection::Square(glm::vec3(0.0f)); EXPECT_EQ(Manifold::Sphere(0).Status(), invalid); EXPECT_EQ(Manifold::Cylinder(0, 5).Status(), invalid); EXPECT_EQ(Manifold::Cylinder(2, -5).Status(), invalid); EXPECT_EQ(Manifold::Cube(glm::vec3(0.0f)).Status(), invalid); EXPECT_EQ(Manifold::Cube({-1, 1, 1}).Status(), invalid); EXPECT_EQ(Manifold::Extrude(circ, 0.).Status(), invalid); EXPECT_EQ(Manifold::Extrude(empty_circ, 10.).Status(), invalid); EXPECT_EQ(Manifold::Revolve(empty_sq).Status(), invalid); } TEST(Manifold, MultiCompose) { auto part = Manifold::Compose({Manifold::Cube({10, 10, 10})}); auto finalAssembly = Manifold::Compose({part, part.Translate({0, 10, 0}), part.Mirror({1, 0, 0}).Translate({10, 0, 0}), part.Mirror({1, 0, 0}).Translate({10, 10, 0})}); EXPECT_FLOAT_EQ(finalAssembly.GetProperties().volume, 4000); } TEST(Manifold, MergeDegenerates) { MeshGL cube = Manifold::Cube(glm::vec3(1), true).GetMeshGL(); MeshGL squash; squash.vertProperties = cube.vertProperties; squash.triVerts = cube.triVerts; // Move one vert to the position of its neighbor and remove one triangle // linking them to break the manifold. squash.vertProperties[squash.vertProperties.size() - 1] *= -1; squash.triVerts.resize(squash.triVerts.size() - 3); // Rotate the degenerate triangle to the middle to catch more problems. std::rotate(squash.triVerts.begin(), squash.triVerts.begin() + 3 * 5, squash.triVerts.end()); // Merge should remove the now duplicate vertex. EXPECT_TRUE(squash.Merge()); // Manifold should remove the triangle with two references to the same vert. Manifold squashed = Manifold(squash); EXPECT_FALSE(squashed.IsEmpty()); EXPECT_EQ(squashed.Status(), Manifold::Error::NoError); } TEST(Manifold, PinchedVert) { Mesh shape; shape.vertPos = {{0, 0, 0}, // {1, 1, 0}, // {1, -1, 0}, // {-0.00001, 0, 0}, // {-1, -1, -0}, // {-1, 1, 0}, // {0, 0, 2}, // {0, 0, -2}}; shape.triVerts = {{0, 2, 6}, // {2, 1, 6}, // {1, 0, 6}, // {4, 3, 6}, // {3, 5, 6}, // {5, 4, 6}, // {2, 0, 4}, // {0, 3, 4}, // {3, 0, 1}, // {3, 1, 5}, // {7, 2, 4}, // {7, 4, 5}, // {7, 5, 1}, // {7, 1, 2}}; Manifold touch(shape); EXPECT_FALSE(touch.IsEmpty()); EXPECT_EQ(touch.Status(), Manifold::Error::NoError); EXPECT_EQ(touch.Genus(), 0); } TEST(Manifold, TictacHull) { const float tictacRad = 100; const float tictacHeight = 500; const int tictacSeg = 1000; const float tictacMid = tictacHeight - 2 * tictacRad; const auto sphere = Manifold::Sphere(tictacRad, tictacSeg); const std::vector spheres{sphere, sphere.Translate({0, 0, tictacMid})}; const auto tictac = Manifold::Hull(spheres); #ifdef MANIFOLD_EXPORT if (options.exportModels) { ExportMesh("tictac_hull.glb", tictac.GetMesh(), {}); } #endif EXPECT_EQ(sphere.NumVert() + tictacSeg, tictac.NumVert()); } TEST(Manifold, HollowHull) { auto sphere = Manifold::Sphere(100, 360); auto hollow = sphere - sphere.Scale({0.8, 0.8, 0.8}); const float sphere_vol = sphere.GetProperties().volume; EXPECT_FLOAT_EQ(hollow.Hull().GetProperties().volume, sphere_vol); } TEST(Manifold, CubeHull) { std::vector cubePts = { {0, 0, 0}, {1, 0, 0}, {0, 1, 0}, {0, 0, 1}, // corners {1, 1, 0}, {0, 1, 1}, {1, 0, 1}, {1, 1, 1}, // corners {0.5, 0.5, 0.5}, {0.5, 0, 0}, {0.5, 0.7, 0.2} // internal points }; auto cube = Manifold::Hull(cubePts); EXPECT_FLOAT_EQ(cube.GetProperties().volume, 1); } TEST(Manifold, EmptyHull) { const std::vector tooFew{{0, 0, 0}, {1, 0, 0}, {0, 1, 0}}; EXPECT_TRUE(Manifold::Hull(tooFew).IsEmpty()); const std::vector coplanar{ {0, 0, 0}, {1, 0, 0}, {0, 1, 0}, {1, 1, 0}}; EXPECT_TRUE(Manifold::Hull(coplanar).IsEmpty()); }