* new JS Mesh class * fix Mesh * switch MeshGL to vectors * new Mesh constructor * export Mesh and updated types * got scallop working again * optional normals * fixed Mesh constructor * added halfedgeTangent * cleanup
1087 lines
33 KiB
C++
1087 lines
33 KiB
C++
// 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 <random>
|
|
|
|
#include "manifold.h"
|
|
#include "polygon.h"
|
|
#include "sdf.h"
|
|
#include "test.h"
|
|
|
|
#ifdef MANIFOLD_EXPORT
|
|
#include "meshIO.h"
|
|
#endif
|
|
|
|
namespace {
|
|
|
|
using namespace manifold;
|
|
|
|
Mesh Csaszar() {
|
|
Mesh csaszar;
|
|
csaszar.vertPos = {{-20, -20, -10}, //
|
|
{-20, 20, -15}, //
|
|
{-5, -8, 8}, //
|
|
{0, 0, 30}, //
|
|
{5, 8, 8}, //
|
|
{20, -20, -15}, //
|
|
{20, 20, -10}};
|
|
csaszar.triVerts = {{1, 3, 6}, //
|
|
{1, 6, 5}, //
|
|
{2, 5, 6}, //
|
|
{0, 2, 6}, //
|
|
{0, 6, 4}, //
|
|
{3, 4, 6}, //
|
|
{1, 2, 3}, //
|
|
{1, 4, 2}, //
|
|
{1, 0, 4}, //
|
|
{1, 5, 0}, //
|
|
{3, 5, 4}, //
|
|
{0, 5, 3}, //
|
|
{0, 3, 2}, //
|
|
{2, 4, 5}};
|
|
return csaszar;
|
|
}
|
|
|
|
Mesh Tet() {
|
|
Mesh tet;
|
|
tet.vertPos = {{-1.0f, -1.0f, 1.0f},
|
|
{-1.0f, 1.0f, -1.0f},
|
|
{1.0f, -1.0f, -1.0f},
|
|
{1.0f, 1.0f, 1.0f}};
|
|
tet.triVerts = {{2, 0, 1}, {0, 3, 1}, {2, 3, 0}, {3, 2, 1}};
|
|
return tet;
|
|
}
|
|
|
|
void Identical(const Mesh& mesh1, const Mesh& mesh2) {
|
|
ASSERT_EQ(mesh1.vertPos.size(), mesh2.vertPos.size());
|
|
for (int i = 0; i < mesh1.vertPos.size(); ++i)
|
|
for (int j : {0, 1, 2})
|
|
ASSERT_NEAR(mesh1.vertPos[i][j], mesh2.vertPos[i][j], 0.0001);
|
|
|
|
ASSERT_EQ(mesh1.triVerts.size(), mesh2.triVerts.size());
|
|
for (int i = 0; i < mesh1.triVerts.size(); ++i)
|
|
ASSERT_EQ(mesh1.triVerts[i], mesh2.triVerts[i]);
|
|
}
|
|
|
|
void Related(const Manifold& out, const std::vector<Mesh>& input,
|
|
const std::map<int, int>& meshID2idx) {
|
|
Mesh output = out.GetMesh();
|
|
MeshRelation relation = out.GetMeshRelation();
|
|
for (int tri = 0; tri < out.NumTri(); ++tri) {
|
|
int meshID = relation.triBary[tri].originalID;
|
|
int meshIdx = meshID2idx.at(meshID);
|
|
ASSERT_LT(meshIdx, input.size());
|
|
const Mesh& inMesh = input[meshIdx];
|
|
int inTri = relation.triBary[tri].tri;
|
|
ASSERT_LT(inTri, inMesh.triVerts.size());
|
|
glm::mat3 inTriangle = {inMesh.vertPos[inMesh.triVerts[inTri][0]],
|
|
inMesh.vertPos[inMesh.triVerts[inTri][1]],
|
|
inMesh.vertPos[inMesh.triVerts[inTri][2]]};
|
|
for (int j : {0, 1, 2}) {
|
|
glm::vec3 vPos = output.vertPos[output.triVerts[tri][j]];
|
|
glm::vec3 uvw = relation.UVW(tri, j);
|
|
ASSERT_NEAR(uvw[0] + uvw[1] + uvw[2], 1, 0.0002);
|
|
glm::vec3 vRelation = inTriangle * uvw;
|
|
for (int k : {0, 1, 2})
|
|
ASSERT_NEAR(vPos[k], vRelation[k], 10 * out.Precision());
|
|
}
|
|
}
|
|
}
|
|
|
|
void RelatedOp(const Manifold& inP, const Manifold& inQ, const Manifold& outR) {
|
|
std::vector<Mesh> input;
|
|
std::map<int, int> meshID2idx;
|
|
|
|
int meshID = inP.OriginalID();
|
|
EXPECT_GE(meshID, 0);
|
|
meshID2idx[meshID] = input.size();
|
|
input.push_back(inP.GetMesh());
|
|
|
|
meshID = inQ.OriginalID();
|
|
EXPECT_GE(meshID, 0);
|
|
meshID2idx[meshID] = input.size();
|
|
input.push_back(inQ.GetMesh());
|
|
|
|
Related(outR, input, meshID2idx);
|
|
}
|
|
|
|
void ExpectMeshes(const Manifold& manifold,
|
|
const std::vector<std::pair<int, int>>& numVertTri) {
|
|
EXPECT_TRUE(manifold.IsManifold());
|
|
EXPECT_TRUE(manifold.MatchesTriNormals());
|
|
std::vector<Manifold> manifolds = manifold.Decompose();
|
|
ASSERT_EQ(manifolds.size(), numVertTri.size());
|
|
std::sort(manifolds.begin(), manifolds.end(),
|
|
[](const Manifold& a, const Manifold& b) {
|
|
return a.NumVert() != b.NumVert() ? a.NumVert() > b.NumVert()
|
|
: a.NumTri() > b.NumTri();
|
|
});
|
|
for (int i = 0; i < manifolds.size(); ++i) {
|
|
EXPECT_TRUE(manifolds[i].IsManifold());
|
|
EXPECT_EQ(manifolds[i].NumVert(), numVertTri[i].first);
|
|
EXPECT_EQ(manifolds[i].NumTri(), numVertTri[i].second);
|
|
const Mesh mesh = manifolds[i].GetMesh();
|
|
for (const glm::vec3& normal : mesh.vertNormal) {
|
|
ASSERT_NEAR(glm::length(normal), 1, 0.0001);
|
|
}
|
|
}
|
|
}
|
|
|
|
void CheckStrictly(const Manifold& manifold) {
|
|
EXPECT_TRUE(manifold.IsManifold());
|
|
EXPECT_TRUE(manifold.MatchesTriNormals());
|
|
EXPECT_EQ(manifold.NumDegenerateTris(), 0);
|
|
}
|
|
|
|
Polygons SquareHole(float xOffset = 0.0) {
|
|
Polygons polys;
|
|
polys.push_back({
|
|
{glm::vec2(2 + xOffset, 2), 0}, //
|
|
{glm::vec2(-2 + xOffset, 2), 0}, //
|
|
{glm::vec2(-2 + xOffset, -2), 0}, //
|
|
{glm::vec2(2 + xOffset, -2), 0}, //
|
|
});
|
|
polys.push_back({
|
|
{glm::vec2(-1 + xOffset, 1), 0}, //
|
|
{glm::vec2(1 + xOffset, 1), 0}, //
|
|
{glm::vec2(1 + xOffset, -1), 0}, //
|
|
{glm::vec2(-1 + xOffset, -1), 0}, //
|
|
});
|
|
return polys;
|
|
}
|
|
|
|
struct Gyroid {
|
|
__host__ __device__ float operator()(glm::vec3 p) const {
|
|
const glm::vec3 min = p;
|
|
const glm::vec3 max = glm::vec3(glm::two_pi<float>()) - p;
|
|
const float min3 = glm::min(min.x, glm::min(min.y, min.z));
|
|
const float max3 = glm::min(max.x, glm::min(max.y, max.z));
|
|
const float bound = glm::min(min3, max3);
|
|
const float gyroid =
|
|
cos(p.x) * sin(p.y) + cos(p.y) * sin(p.z) + cos(p.z) * sin(p.x);
|
|
return glm::min(gyroid, bound);
|
|
}
|
|
};
|
|
|
|
} // 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.vertPos[3 * i + j], mesh_out.vertPos[i][j]);
|
|
ASSERT_EQ(meshGL_out.vertNormal[3 * i + j], mesh_out.vertNormal[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::NO_ERROR);
|
|
EXPECT_TRUE(empty.IsManifold());
|
|
}
|
|
|
|
TEST(Manifold, ValidInput) {
|
|
std::vector<float> propTol = {0.1, 0.2};
|
|
std::vector<float> prop = {0, 0, 1, -1, 2, -2, 3, -3, 4, -4, 5, -5, 6, -6};
|
|
std::vector<glm::ivec3> triProp = {
|
|
{2, 0, 1}, {0, 3, 1}, {2, 3, 0}, {6, 5, 4}};
|
|
Manifold tet(Tet(), triProp, prop, propTol);
|
|
EXPECT_FALSE(tet.IsEmpty());
|
|
EXPECT_EQ(tet.Status(), Manifold::Error::NO_ERROR);
|
|
EXPECT_TRUE(tet.IsManifold());
|
|
}
|
|
|
|
TEST(Manifold, InvalidInput1) {
|
|
Mesh in = Tet();
|
|
in.vertPos[2][1] = NAN;
|
|
Manifold tet(in);
|
|
EXPECT_TRUE(tet.IsEmpty());
|
|
EXPECT_EQ(tet.Status(), Manifold::Error::NON_FINITE_VERTEX);
|
|
EXPECT_TRUE(tet.IsManifold());
|
|
}
|
|
|
|
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::NOT_MANIFOLD);
|
|
EXPECT_TRUE(tet.IsManifold());
|
|
}
|
|
|
|
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::VERTEX_INDEX_OUT_OF_BOUNDS);
|
|
EXPECT_TRUE(tet.IsManifold());
|
|
}
|
|
|
|
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::VERTEX_INDEX_OUT_OF_BOUNDS);
|
|
EXPECT_TRUE(tet.IsManifold());
|
|
}
|
|
|
|
TEST(Manifold, InvalidInput5) {
|
|
std::vector<float> propTol = {0.1, 0.2};
|
|
std::vector<float> prop = {0, 0, 1, -1, 2, -2, 3, -3, 4, -4, 5, -5, 6};
|
|
std::vector<glm::ivec3> triProp = {
|
|
{2, 0, 1}, {0, 3, 1}, {2, 3, 0}, {6, 5, 4}};
|
|
Manifold tet(Tet(), triProp, prop, propTol);
|
|
EXPECT_TRUE(tet.IsEmpty());
|
|
EXPECT_EQ(tet.Status(), Manifold::Error::PROPERTIES_WRONG_LENGTH);
|
|
EXPECT_TRUE(tet.IsManifold());
|
|
}
|
|
|
|
TEST(Manifold, InvalidInput6) {
|
|
std::vector<float> propTol = {0.1, 0.2};
|
|
std::vector<float> prop = {0, 0, 1, -1, 2, -2, 3, -3, 4, -4, 5, -5, 6, -6};
|
|
std::vector<glm::ivec3> triProp = {{2, 0, 1}, {0, 3, 1}, {2, 3, 0}};
|
|
Manifold tet(Tet(), triProp, prop, propTol);
|
|
EXPECT_TRUE(tet.IsEmpty());
|
|
EXPECT_EQ(tet.Status(), Manifold::Error::TRI_PROPERTIES_WRONG_LENGTH);
|
|
EXPECT_TRUE(tet.IsManifold());
|
|
}
|
|
|
|
TEST(Manifold, InvalidInput7) {
|
|
std::vector<float> propTol = {0.1, 0.2};
|
|
std::vector<float> prop = {0, 0, 1, -1, 2, -2, 3, -3, 4, -4, 5, -5, 6, -6};
|
|
std::vector<glm::ivec3> triProp = {
|
|
{2, 0, 1}, {0, 3, 1}, {2, 3, 0}, {6, 5, 7}};
|
|
Manifold tet(Tet(), triProp, prop, propTol);
|
|
EXPECT_TRUE(tet.IsEmpty());
|
|
EXPECT_EQ(tet.Status(), Manifold::Error::TRI_PROPERTIES_OUT_OF_BOUNDS);
|
|
EXPECT_TRUE(tet.IsManifold());
|
|
}
|
|
|
|
/**
|
|
* ExpectMeshes performs a decomposition, so this test ensures that compose and
|
|
* decompose are inverse operations.
|
|
*/
|
|
TEST(Manifold, Decompose) {
|
|
std::vector<Manifold> manifoldList;
|
|
manifoldList.push_back(Manifold::Tetrahedron());
|
|
manifoldList.push_back(Manifold::Cube());
|
|
manifoldList.push_back(Manifold::Sphere(1, 4));
|
|
Manifold manifolds = Manifold::Compose(manifoldList);
|
|
|
|
ExpectMeshes(manifolds, {{8, 12}, {6, 8}, {4, 4}});
|
|
|
|
std::vector<Mesh> input;
|
|
std::map<int, int> meshID2idx;
|
|
|
|
for (const Manifold& manifold : manifoldList) {
|
|
int meshID = manifold.OriginalID();
|
|
EXPECT_GE(meshID, 0);
|
|
meshID2idx[meshID] = input.size();
|
|
input.push_back(manifold.GetMesh());
|
|
}
|
|
|
|
Related(manifolds, input, meshID2idx);
|
|
}
|
|
|
|
/**
|
|
* These tests check the various manifold constructors.
|
|
*/
|
|
TEST(Manifold, Sphere) {
|
|
int n = 25;
|
|
Manifold sphere = Manifold::Sphere(1.0f, 4 * n);
|
|
EXPECT_TRUE(sphere.IsManifold());
|
|
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_TRUE(donut.IsManifold());
|
|
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_TRUE(donut.IsManifold());
|
|
EXPECT_EQ(donut.Genus(), 0);
|
|
EXPECT_FLOAT_EQ(donut.GetProperties().volume, 4.0f);
|
|
}
|
|
|
|
TEST(Manifold, Revolve) {
|
|
Polygons polys = SquareHole();
|
|
Manifold vug = Manifold::Revolve(polys, 48);
|
|
EXPECT_TRUE(vug.IsManifold());
|
|
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) {
|
|
Polygons polys = SquareHole(2.0f);
|
|
Manifold donutHole = Manifold::Revolve(polys, 48);
|
|
EXPECT_TRUE(donutHole.IsManifold());
|
|
EXPECT_EQ(donutHole.Genus(), 0);
|
|
auto prop = donutHole.GetProperties();
|
|
EXPECT_NEAR(prop.volume, 48.0f * glm::pi<float>(), 1.0f);
|
|
EXPECT_NEAR(prop.surfaceArea, 96.0f * glm::pi<float>(), 1.0f);
|
|
}
|
|
|
|
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());
|
|
MeshRelation rel = interp.GetMeshRelation();
|
|
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 = rel.UVW(tri, i);
|
|
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, 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());
|
|
MeshRelation rel = csaszar.GetMeshRelation();
|
|
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 = rel.barycentric[rel.triBary[tri].vertBary[i]];
|
|
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();
|
|
EXPECT_TRUE(cube.IsManifold());
|
|
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);
|
|
}
|
|
|
|
/**
|
|
* 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, GetCurvature) {
|
|
const float precision = 0.015;
|
|
for (int n = 4; n < 100; n *= 2) {
|
|
Manifold sphere = Manifold::Sphere(1, 64);
|
|
Curvature curvature = sphere.GetCurvature();
|
|
EXPECT_NEAR(curvature.minMeanCurvature, 2, 2 * precision);
|
|
EXPECT_NEAR(curvature.maxMeanCurvature, 2, 2 * precision);
|
|
EXPECT_NEAR(curvature.minGaussianCurvature, 1, precision);
|
|
EXPECT_NEAR(curvature.maxGaussianCurvature, 1, precision);
|
|
|
|
sphere = sphere.Scale(glm::vec3(2.0f));
|
|
curvature = sphere.GetCurvature();
|
|
EXPECT_NEAR(curvature.minMeanCurvature, 1, precision);
|
|
EXPECT_NEAR(curvature.maxMeanCurvature, 1, precision);
|
|
EXPECT_NEAR(curvature.minGaussianCurvature, 0.25, 0.25 * precision);
|
|
EXPECT_NEAR(curvature.maxGaussianCurvature, 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) {
|
|
const float period = glm::two_pi<float>();
|
|
|
|
Mesh gyroidMesh = LevelSet(Gyroid(), {glm::vec3(0), glm::vec3(period)}, 0.5);
|
|
|
|
std::vector<Mesh> input;
|
|
std::map<int, int> meshID2idx;
|
|
|
|
input.push_back(gyroidMesh);
|
|
Manifold gyroid(input[0]);
|
|
|
|
#ifdef MANIFOLD_EXPORT
|
|
if (options.exportModels) ExportMesh("gyroid.glb", gyroid.GetMesh(), {});
|
|
#endif
|
|
|
|
int meshID = gyroid.OriginalID();
|
|
EXPECT_GE(meshID, 0);
|
|
meshID2idx[meshID] = input.size() - 1;
|
|
|
|
Related(gyroid, input, meshID2idx);
|
|
}
|
|
|
|
TEST(Manifold, MeshRelationRefine) {
|
|
std::vector<Mesh> input;
|
|
std::map<int, int> meshID2idx;
|
|
|
|
input.push_back(Csaszar());
|
|
Manifold csaszar(input[0]);
|
|
|
|
int meshID = csaszar.OriginalID();
|
|
EXPECT_GE(meshID, 0);
|
|
meshID2idx[meshID] = input.size() - 1;
|
|
|
|
Related(csaszar, input, meshID2idx);
|
|
csaszar.Refine(4);
|
|
Related(csaszar, input, meshID2idx);
|
|
}
|
|
|
|
/**
|
|
* The very simplest Boolean operation test.
|
|
*/
|
|
TEST(Boolean, Tetra) {
|
|
Manifold tetra = Manifold::Tetrahedron();
|
|
EXPECT_TRUE(tetra.IsManifold());
|
|
|
|
Manifold tetra2 = tetra;
|
|
tetra2 = tetra2.Translate(glm::vec3(0.5f)).AsOriginal();
|
|
Manifold result = tetra2 - tetra;
|
|
|
|
ExpectMeshes(result, {{8, 12}});
|
|
|
|
RelatedOp(tetra, tetra2, result);
|
|
}
|
|
|
|
/**
|
|
* These tests check Boolean operations on coplanar faces.
|
|
*/
|
|
TEST(Boolean, SelfSubtract) {
|
|
Manifold cube = Manifold::Cube();
|
|
Manifold empty = cube - cube;
|
|
EXPECT_TRUE(empty.IsManifold());
|
|
EXPECT_TRUE(empty.IsEmpty());
|
|
|
|
auto prop = empty.GetProperties();
|
|
EXPECT_FLOAT_EQ(prop.volume, 0.0f);
|
|
EXPECT_FLOAT_EQ(prop.surfaceArea, 0.0f);
|
|
}
|
|
|
|
TEST(Boolean, Perturb) {
|
|
Mesh tmp;
|
|
tmp.vertPos = {{0.0f, 0.0f, 0.0f},
|
|
{0.0f, 1.0f, 0.0f},
|
|
{1.0f, 0.0f, 0.0f},
|
|
{0.0f, 0.0f, 1.0f}};
|
|
tmp.triVerts = {{2, 0, 1}, {0, 3, 1}, {2, 3, 0}, {3, 2, 1}};
|
|
Manifold corner(tmp);
|
|
Manifold empty = corner - corner;
|
|
EXPECT_TRUE(empty.IsManifold());
|
|
EXPECT_TRUE(empty.IsEmpty());
|
|
|
|
auto prop = empty.GetProperties();
|
|
EXPECT_FLOAT_EQ(prop.volume, 0.0f);
|
|
EXPECT_FLOAT_EQ(prop.surfaceArea, 0.0f);
|
|
}
|
|
|
|
TEST(Boolean, Coplanar) {
|
|
Manifold cylinder = Manifold::Cylinder(1.0f, 1.0f);
|
|
Manifold cylinder2 = cylinder.AsOriginal();
|
|
cylinder2 = cylinder2.Scale({0.5f, 0.5f, 1.0f})
|
|
.Rotate(0, 0, 15)
|
|
.Translate({0.25f, 0.25f, 0.0f});
|
|
Manifold out = cylinder - cylinder2;
|
|
ExpectMeshes(out, {{32, 64}});
|
|
EXPECT_EQ(out.NumDegenerateTris(), 0);
|
|
EXPECT_EQ(out.Genus(), 1);
|
|
|
|
#ifdef MANIFOLD_EXPORT
|
|
if (options.exportModels) ExportMesh("coplanar.glb", out.GetMesh(), {});
|
|
#endif
|
|
|
|
RelatedOp(cylinder, cylinder2, out);
|
|
}
|
|
|
|
TEST(Boolean, MultiCoplanar) {
|
|
Manifold cube = Manifold::Cube();
|
|
Manifold first = cube - cube.Translate({0.3f, 0.3f, 0.0f});
|
|
cube = cube.Translate({-0.3f, -0.3f, 0.0f});
|
|
Manifold out = first - cube;
|
|
CheckStrictly(out);
|
|
EXPECT_EQ(out.Genus(), -1);
|
|
auto prop = out.GetProperties();
|
|
EXPECT_NEAR(prop.volume, 0.18, 1e-5);
|
|
EXPECT_NEAR(prop.surfaceArea, 2.76, 1e-5);
|
|
}
|
|
|
|
TEST(Boolean, FaceUnion) {
|
|
Manifold cubes = Manifold::Cube();
|
|
cubes += cubes.Translate({1, 0, 0});
|
|
EXPECT_EQ(cubes.Genus(), 0);
|
|
ExpectMeshes(cubes, {{12, 20}});
|
|
auto prop = cubes.GetProperties();
|
|
EXPECT_NEAR(prop.volume, 2, 1e-5);
|
|
EXPECT_NEAR(prop.surfaceArea, 10, 1e-5);
|
|
|
|
#ifdef MANIFOLD_EXPORT
|
|
if (options.exportModels) ExportMesh("faceUnion.glb", cubes.GetMesh(), {});
|
|
#endif
|
|
}
|
|
|
|
TEST(Boolean, EdgeUnion) {
|
|
Manifold cubes = Manifold::Cube();
|
|
cubes += cubes.Translate({1, 1, 0});
|
|
ExpectMeshes(cubes, {{8, 12}, {8, 12}});
|
|
}
|
|
|
|
TEST(Boolean, EdgeUnion2) {
|
|
Manifold tets = Manifold::Tetrahedron();
|
|
Manifold cube2 = tets;
|
|
tets = tets.Translate({0, 0, -1});
|
|
tets += cube2.Translate({0, 0, 1}).Rotate(0, 0, 90);
|
|
ExpectMeshes(tets, {{4, 4}, {4, 4}});
|
|
}
|
|
|
|
TEST(Boolean, CornerUnion) {
|
|
Manifold cubes = Manifold::Cube();
|
|
cubes += cubes.Translate({1, 1, 1});
|
|
ExpectMeshes(cubes, {{8, 12}, {8, 12}});
|
|
}
|
|
|
|
/**
|
|
* These tests verify that the spliting helper functions return meshes with
|
|
* volumes that make sense.
|
|
*/
|
|
TEST(Boolean, Split) {
|
|
Manifold cube = Manifold::Cube(glm::vec3(2.0f), true);
|
|
Manifold oct = Manifold::Sphere(1, 4).Translate(glm::vec3(0.0f, 0.0f, 1.0f));
|
|
std::pair<Manifold, Manifold> splits = cube.Split(oct);
|
|
CheckStrictly(splits.first);
|
|
CheckStrictly(splits.second);
|
|
EXPECT_FLOAT_EQ(splits.first.GetProperties().volume +
|
|
splits.second.GetProperties().volume,
|
|
cube.GetProperties().volume);
|
|
}
|
|
|
|
TEST(Boolean, SplitByPlane) {
|
|
Manifold cube = Manifold::Cube(glm::vec3(2.0f), true);
|
|
cube = cube.Translate({0.0f, 1.0f, 0.0f});
|
|
cube = cube.Rotate(90.0f, 0.0f, 0.0f);
|
|
std::pair<Manifold, Manifold> splits =
|
|
cube.SplitByPlane({0.0f, 0.0f, 1.0f}, 1.0f);
|
|
CheckStrictly(splits.first);
|
|
CheckStrictly(splits.second);
|
|
EXPECT_NEAR(splits.first.GetProperties().volume,
|
|
splits.second.GetProperties().volume, 1e-5);
|
|
|
|
Manifold first = cube.TrimByPlane({0.0f, 0.0f, 1.0f}, 1.0f);
|
|
// Verify trim returns the same result as the first split by checking that
|
|
// their bounding boxes contain each other, thus they are equal.
|
|
EXPECT_TRUE(splits.first.BoundingBox().Contains(first.BoundingBox()));
|
|
EXPECT_TRUE(first.BoundingBox().Contains(splits.first.BoundingBox()));
|
|
}
|
|
|
|
TEST(Boolean, SplitByPlane60) {
|
|
Manifold cube = Manifold::Cube(glm::vec3(2.0f), true);
|
|
cube = cube.Translate({0.0f, 1.0f, 0.0f});
|
|
cube = cube.Rotate(0.0f, 0.0f, -60.0f);
|
|
cube = cube.Translate({2.0f, 0.0f, 0.0f});
|
|
float phi = 30.0f;
|
|
std::pair<Manifold, Manifold> splits =
|
|
cube.SplitByPlane({sind(phi), -cosd(phi), 0.0f}, 1.0f);
|
|
CheckStrictly(splits.first);
|
|
CheckStrictly(splits.second);
|
|
EXPECT_NEAR(splits.first.GetProperties().volume,
|
|
splits.second.GetProperties().volume, 1e-5);
|
|
}
|
|
|
|
/**
|
|
* This tests that non-intersecting geometry is properly retained.
|
|
*/
|
|
TEST(Boolean, Vug) {
|
|
Manifold cube = Manifold::Cube(glm::vec3(4.0f), true);
|
|
Manifold vug = cube - Manifold::Cube();
|
|
|
|
EXPECT_EQ(vug.Genus(), -1);
|
|
|
|
Manifold half = vug.SplitByPlane({0.0f, 0.0f, 1.0f}, -1.0f).first;
|
|
CheckStrictly(half);
|
|
EXPECT_EQ(half.Genus(), -1);
|
|
|
|
auto prop = half.GetProperties();
|
|
EXPECT_FLOAT_EQ(prop.volume, 4.0 * 4.0 * 3.0 - 1.0);
|
|
EXPECT_FLOAT_EQ(prop.surfaceArea, 16.0 * 2 + 12.0 * 4 + 6.0);
|
|
}
|
|
|
|
TEST(Boolean, Empty) {
|
|
Manifold cube = Manifold::Cube();
|
|
float cubeVol = cube.GetProperties().volume;
|
|
Manifold empty;
|
|
|
|
EXPECT_EQ((cube + empty).GetProperties().volume, cubeVol);
|
|
EXPECT_EQ((cube - empty).GetProperties().volume, cubeVol);
|
|
EXPECT_TRUE((empty - cube).IsEmpty());
|
|
EXPECT_TRUE((cube ^ empty).IsEmpty());
|
|
}
|
|
|
|
TEST(Boolean, Winding) {
|
|
std::vector<Manifold> cubes;
|
|
cubes.push_back(Manifold::Cube(glm::vec3(3.0f), true));
|
|
cubes.push_back(Manifold::Cube(glm::vec3(2.0f), true));
|
|
Manifold doubled = Manifold::Compose(cubes);
|
|
|
|
Manifold cube = Manifold::Cube(glm::vec3(1.0f), true);
|
|
EXPECT_TRUE((cube ^= doubled).IsManifold());
|
|
}
|
|
|
|
TEST(Boolean, NonIntersecting) {
|
|
Manifold cube1 = Manifold::Cube();
|
|
float vol1 = cube1.GetProperties().volume;
|
|
Manifold cube2 = cube1.Scale(glm::vec3(2)).Translate({3, 0, 0});
|
|
float vol2 = cube2.GetProperties().volume;
|
|
|
|
EXPECT_EQ((cube1 + cube2).GetProperties().volume, vol1 + vol2);
|
|
EXPECT_EQ((cube1 - cube2).GetProperties().volume, vol1);
|
|
EXPECT_TRUE((cube1 ^ cube2).IsEmpty());
|
|
}
|
|
|
|
TEST(Boolean, Precision) {
|
|
Manifold cube = Manifold::Cube();
|
|
Manifold cube2 = cube;
|
|
Manifold cube3 = cube;
|
|
float distance = 100;
|
|
float scale = distance * kTolerance;
|
|
cube2 = cube2.Scale(glm::vec3(scale)).Translate({distance, 0, 0});
|
|
|
|
cube += cube2;
|
|
ExpectMeshes(cube, {{8, 12}});
|
|
|
|
cube3 = cube3.Scale(glm::vec3(2 * scale)).Translate({distance, 0, 0});
|
|
cube += cube3;
|
|
ExpectMeshes(cube, {{8, 12}, {8, 12}});
|
|
}
|
|
|
|
TEST(Boolean, Precision2) {
|
|
float scale = 1000;
|
|
Manifold cube = Manifold::Cube(glm::vec3(scale));
|
|
Manifold cube2 = cube;
|
|
float distance = scale * (1 - kTolerance / 2);
|
|
|
|
cube2 = cube2.Translate(glm::vec3(-distance));
|
|
EXPECT_TRUE((cube ^ cube2).IsEmpty());
|
|
|
|
cube2 = cube2.Translate(glm::vec3(scale * kTolerance));
|
|
EXPECT_FALSE((cube ^ cube2).IsEmpty());
|
|
}
|
|
|
|
/**
|
|
* These tests verify correct topology and geometry for complex boolean
|
|
* operations between valid shapes with many faces.
|
|
*/
|
|
TEST(Boolean, Sphere) {
|
|
Manifold sphere = Manifold::Sphere(1.0f, 12);
|
|
Manifold sphere2 = sphere;
|
|
sphere2 = sphere2.Translate(glm::vec3(0.5));
|
|
sphere2 = sphere2.AsOriginal();
|
|
Manifold result = sphere - sphere2;
|
|
|
|
ExpectMeshes(result, {{74, 144}});
|
|
EXPECT_EQ(result.NumDegenerateTris(), 0);
|
|
|
|
RelatedOp(sphere, sphere2, result);
|
|
}
|
|
|
|
TEST(Boolean, MeshRelation) {
|
|
const float period = glm::two_pi<float>();
|
|
|
|
Mesh gyroidMesh = LevelSet(Gyroid(), {glm::vec3(0), glm::vec3(period)}, 0.5);
|
|
Manifold gyroid(gyroidMesh);
|
|
|
|
Mesh gyroidMesh2 = gyroidMesh;
|
|
std::transform(gyroidMesh.vertPos.begin(), gyroidMesh.vertPos.end(),
|
|
gyroidMesh2.vertPos.begin(),
|
|
[](const glm::vec3& v) { return v + glm::vec3(2.0f); });
|
|
Manifold gyroid2(gyroidMesh2);
|
|
|
|
EXPECT_TRUE(gyroid.IsManifold());
|
|
EXPECT_TRUE(gyroid.MatchesTriNormals());
|
|
EXPECT_LE(gyroid.NumDegenerateTris(), 0);
|
|
Manifold result = gyroid + gyroid2;
|
|
|
|
#ifdef MANIFOLD_EXPORT
|
|
if (options.exportModels) ExportMesh("gyroidUnion.glb", result.GetMesh(), {});
|
|
#endif
|
|
|
|
EXPECT_TRUE(result.IsManifold());
|
|
EXPECT_TRUE(result.MatchesTriNormals());
|
|
EXPECT_LE(result.NumDegenerateTris(), 1);
|
|
EXPECT_EQ(result.Decompose().size(), 1);
|
|
auto prop = result.GetProperties();
|
|
EXPECT_NEAR(prop.volume, 226, 1);
|
|
EXPECT_NEAR(prop.surfaceArea, 387, 1);
|
|
|
|
std::vector<Mesh> input;
|
|
std::map<int, int> meshID2idx;
|
|
|
|
int meshID = gyroid.OriginalID();
|
|
EXPECT_GE(meshID, 0);
|
|
meshID2idx[meshID] = input.size();
|
|
input.push_back(gyroidMesh);
|
|
|
|
meshID = gyroid2.OriginalID();
|
|
EXPECT_GE(meshID, 0);
|
|
meshID2idx[meshID] = input.size();
|
|
input.push_back(gyroidMesh2);
|
|
|
|
Related(result, input, meshID2idx);
|
|
}
|
|
|
|
TEST(Boolean, Cylinders) {
|
|
Manifold rod = Manifold::Cylinder(1.0, 0.4, -1.0, 12);
|
|
float arrays1[][12] = {
|
|
{0, 0, 1, 3, //
|
|
-1, 0, 0, 3, //
|
|
0, -1, 0, 6}, //
|
|
{0, 0, 1, 2, //
|
|
-1, 0, 0, 3, //
|
|
0, -1, 0, 8}, //
|
|
|
|
{0, 0, 1, 1, //
|
|
-1, 0, 0, 2, //
|
|
0, -1, 0, 7}, //
|
|
{1, 0, 0, 3, //
|
|
0, 1, 0, 2, //
|
|
0, 0, 1, 6}, //
|
|
{0, 0, 1, 3, //
|
|
-1, 0, 0, 3, //
|
|
0, -1, 0, 7}, //
|
|
{0, 0, 1, 1, //
|
|
-1, 0, 0, 3, //
|
|
0, -1, 0, 7}, //
|
|
{1, 0, 0, 3, //
|
|
0, 0, 1, 4, //
|
|
0, -1, 0, 6}, //
|
|
{1, 0, 0, 4, //
|
|
0, 0, 1, 4, //
|
|
0, -1, 0, 6}, //
|
|
};
|
|
float arrays2[][12] = {
|
|
{1, 0, 0, 3, //
|
|
0, 0, 1, 2, //
|
|
0, -1, 0, 6}, //
|
|
{1, 0, 0, 4, //
|
|
0, 1, 0, 3, //
|
|
0, 0, 1, 6}, //
|
|
|
|
{0, 0, 1, 2, //
|
|
-1, 0, 0, 2, //
|
|
0, -1, 0, 7}, //
|
|
{1, 0, 0, 3, //
|
|
0, 1, 0, 3, //
|
|
0, 0, 1, 7}, //
|
|
{1, 0, 0, 2, //
|
|
0, 1, 0, 3, //
|
|
0, 0, 1, 7}, //
|
|
{1, 0, 0, 1, //
|
|
0, 1, 0, 3, //
|
|
0, 0, 1, 7}, //
|
|
{1, 0, 0, 3, //
|
|
0, 1, 0, 4, //
|
|
0, 0, 1, 7}, //
|
|
{1, 0, 0, 3, //
|
|
0, 1, 0, 5, //
|
|
0, 0, 1, 6}, //
|
|
{0, 0, 1, 3, //
|
|
-1, 0, 0, 4, //
|
|
0, -1, 0, 6}, //
|
|
};
|
|
|
|
Manifold m1;
|
|
for (auto& array : arrays1) {
|
|
glm::mat4x3 mat;
|
|
for (const int i : {0, 1, 2, 3}) {
|
|
for (const int j : {0, 1, 2}) {
|
|
mat[i][j] = array[j * 4 + i];
|
|
}
|
|
}
|
|
m1 += rod.Transform(mat);
|
|
}
|
|
|
|
Manifold m2;
|
|
for (auto& array : arrays2) {
|
|
glm::mat4x3 mat;
|
|
for (const int i : {0, 1, 2, 3}) {
|
|
for (const int j : {0, 1, 2}) {
|
|
mat[i][j] = array[j * 4 + i];
|
|
}
|
|
}
|
|
m2 += rod.Transform(mat);
|
|
}
|
|
m1 += m2;
|
|
|
|
EXPECT_TRUE(m1.IsManifold());
|
|
EXPECT_TRUE(m1.MatchesTriNormals());
|
|
EXPECT_LE(m1.NumDegenerateTris(), 12);
|
|
}
|
|
|
|
TEST(Boolean, Cubes) {
|
|
Manifold result = Manifold::Cube({1.2, 1, 1}, true).Translate({0, -0.5, 0.5});
|
|
result += Manifold::Cube({1, 0.8, 0.5}).Translate({-0.5, 0, 0.5});
|
|
result += Manifold::Cube({1.2, 0.1, 0.5}).Translate({-0.6, -0.1, 0});
|
|
|
|
EXPECT_TRUE(result.IsManifold());
|
|
EXPECT_TRUE(result.MatchesTriNormals());
|
|
EXPECT_LE(result.NumDegenerateTris(), 0);
|
|
auto prop = result.GetProperties();
|
|
EXPECT_NEAR(prop.volume, 1.6, 0.001);
|
|
EXPECT_NEAR(prop.surfaceArea, 9.2, 0.01);
|
|
|
|
#ifdef MANIFOLD_EXPORT
|
|
if (options.exportModels) ExportMesh("cubes.glb", result.GetMesh(), {});
|
|
#endif
|
|
}
|
|
|
|
TEST(Boolean, Subtract) {
|
|
Mesh firstMesh;
|
|
firstMesh.vertPos = {{0, 0, 0}, {1540, 0, 0},
|
|
{1540, 70, 0}, {0, 70, 0},
|
|
{0, 0, -278.282}, {1540, 70, -278.282},
|
|
{1540, 0, -278.282}, {0, 70, -278.282}};
|
|
firstMesh.triVerts = {
|
|
{0, 1, 2}, {2, 3, 0}, {4, 5, 6}, {5, 4, 7}, {6, 2, 1}, {6, 5, 2},
|
|
{5, 3, 2}, {5, 7, 3}, {7, 0, 3}, {7, 4, 0}, {4, 1, 0}, {4, 6, 1},
|
|
};
|
|
|
|
Mesh secondMesh;
|
|
secondMesh.vertPos = {
|
|
{2.04636e-12, 70, 50000}, {2.04636e-12, -1.27898e-13, 50000},
|
|
{1470, -1.27898e-13, 50000}, {1540, 70, 50000},
|
|
{2.04636e-12, 70, -28.2818}, {1470, -1.27898e-13, 0},
|
|
{2.04636e-12, -1.27898e-13, 0}, {1540, 70, -28.2818}};
|
|
secondMesh.triVerts = {{0, 1, 2}, {2, 3, 0}, {4, 5, 6}, {5, 4, 7},
|
|
{6, 2, 1}, {6, 5, 2}, {5, 3, 2}, {5, 7, 3},
|
|
{7, 0, 3}, {7, 4, 0}, {4, 1, 0}, {4, 6, 1}};
|
|
|
|
Manifold first(firstMesh);
|
|
Manifold second(secondMesh);
|
|
|
|
first -= second;
|
|
first.GetMesh();
|
|
}
|
|
|
|
TEST(Boolean, Close) {
|
|
PolygonParams().processOverlaps = true;
|
|
|
|
const float r = 10;
|
|
Manifold a = Manifold::Sphere(r, 256);
|
|
Manifold result = a;
|
|
for (int i = 0; i < 10; i++) {
|
|
// std::cout << i << std::endl;
|
|
result ^= a.Translate({a.Precision() / 10 * i, 0.0, 0.0});
|
|
EXPECT_TRUE(result.IsManifold());
|
|
}
|
|
auto prop = result.GetProperties();
|
|
const float tol = 0.004;
|
|
EXPECT_NEAR(prop.volume, (4.0f / 3.0f) * glm::pi<float>() * r * r * r,
|
|
tol * r * r * r);
|
|
EXPECT_NEAR(prop.surfaceArea, 4 * glm::pi<float>() * r * r, tol * r * r);
|
|
|
|
#ifdef MANIFOLD_EXPORT
|
|
if (options.exportModels) ExportMesh("close.glb", result.GetMesh(), {});
|
|
#endif
|
|
|
|
PolygonParams().processOverlaps = false;
|
|
}
|
|
|
|
TEST(Boolean, UnionDifference) {
|
|
Manifold block = Manifold::Cube({1, 1, 1}, true) - Manifold::Cylinder(1, 0.5);
|
|
Manifold result = block + block.Translate({0, 0, 1});
|
|
float resultsize = result.GetProperties().volume;
|
|
float blocksize = block.GetProperties().volume;
|
|
EXPECT_NEAR(resultsize, blocksize * 2, 0.0001);
|
|
}
|