diff --git a/extras/CMakeLists.txt b/extras/CMakeLists.txt index d61da8b0d..dd8d5a132 100644 --- a/extras/CMakeLists.txt +++ b/extras/CMakeLists.txt @@ -53,4 +53,10 @@ if(BUILD_TEST_CGAL) target_link_libraries(perfTestCGAL manifold CGAL::CGAL CGAL::CGAL_Core Boost::thread) target_compile_options(perfTestCGAL PRIVATE ${MANIFOLD_FLAGS}) target_compile_features(perfTestCGAL PUBLIC cxx_std_17) -endif() + + add_executable(testHullPerformance test_hull_performance.cpp) + target_compile_definitions(testHullPerformance PRIVATE CGAL_USE_GMPXX) + target_link_libraries(testHullPerformance manifold meshIO samples CGAL::CGAL CGAL::CGAL_Core Boost::thread) + target_compile_options(testHullPerformance PRIVATE ${MANIFOLD_FLAGS}) + target_compile_features(testHullPerformance PUBLIC cxx_std_17) +endif() \ No newline at end of file diff --git a/extras/Thingi10K/raw_meshes/233198.stl b/extras/Thingi10K/raw_meshes/233198.stl new file mode 100644 index 000000000..7a55ad257 Binary files /dev/null and b/extras/Thingi10K/raw_meshes/233198.stl differ diff --git a/extras/merge_and_stats.py b/extras/merge_and_stats.py new file mode 100644 index 000000000..68a467a43 --- /dev/null +++ b/extras/merge_and_stats.py @@ -0,0 +1,242 @@ +import pandas as pd + + +# MERGING THE DATA + + +filenames=[] +# merged_data = {} +def parse_csv_and_merge(csv_files, output_file='merged_data.csv'): + """ + Merges CSV files, handling multiline entries and various error conditions. + + Args: + csv_files (list): List of tuples containing (filename, implementation_name). + output_file (str, optional): Name of the output CSV file. Defaults to 'merged_data.csv'. + """ + + # merged_data = pd.DataFrame(columns=['Filename']) + merged_data={} + is_multiline = False + multiline_data = [] + curr_file="" + for file, implementation in csv_files: + print(f"Starting File : {file}") + try: + df = pd.read_csv(file) + except FileNotFoundError: + print(f"Error: File '{file}' not found. Skipping...") + continue + + for i, row in df.iterrows(): + if is_multiline: + # Handling multiline entries (Before standard algorithm call) + if 'After standard algorithm call' in row.values[0]: + is_multiline = True + continue + elif row.values[1] == "Error": + row.fillna(0, inplace=True) + row['Status'] = 'Error' + row.values[0]= curr_file + row.values[1] = 0 + is_multiline=False + filename = row['Filename'] + if filename not in merged_data: + merged_data[filename] = row.to_dict() + else: + for col in df.columns: + if col != 'Filename' and not pd.isna(row[col]): + merged_data[filename][col+"_"+implementation] = row[col] + elif row.values[0] == "Invalid Output by algorithm": + is_multiline = True + continue + else: + is_multiline = False + prev_item=curr_file + filenames.append(curr_file) + temp_item=row.values[0] + temp_len=row.values.size + for i in range(1,temp_len): + # print(temp_item) + temp_item=row.values[i-1] + row.values[i-1]=prev_item + prev_item=temp_item + # print(row) + filename = row['Filename'] + if filename not in merged_data: + merged_data[filename] = row.to_dict() + else: + for col in df.columns: + if col != 'Filename' and not pd.isna(row[col]): + merged_data[filename][col+"_"+implementation] = row[col] + else: + # Handling single-line entries or first line of multiline entries + # Checking for timeout or error + if pd.isna(row['VolManifold']): + if (row['VolHull']=="Timeout"): + # if 'Timeout' in row['Status']: + row['VolHull']=0 + row['VolManifold'] = 0 + row.fillna(0, inplace=True) + row['Status'] = 'Timeout' + elif 'Error' in row['Status']: + row.fillna(0, inplace=True) + row['Status'] = 'Error' + elif (row['VolHull'] == "Error"): + row.fillna(0, inplace=True) + row['Status'] = 'Error' + pass + filename = row['Filename'] + if filename not in merged_data: + merged_data[filename] = row.to_dict() + else: + for col in df.columns: + if col != 'Filename' and not pd.isna(row[col]): + merged_data[filename][col+"_"+implementation] = row[col] + continue + # Converting Series to df for renaming columns + if 'Before standard algorithm call' in row.values[1]: + if row.values[2] == "Timeout": + row.fillna(0, inplace=True) + row['Status'] = 'Timeout' + row['VolHull']=0 + row['VolManifold'] = 0 + filename = row['Filename'] + if filename not in merged_data: + merged_data[filename] = row.to_dict() + else: + for col in df.columns: + if col != 'Filename' and not pd.isna(row[col]): + merged_data[filename][col+"_"+implementation] = row[col] + continue + is_multiline = True + curr_file=row.values[0] + else: + if (row['VolManifold']=="timeout: the monitored command dumped core"): + row.fillna(0, inplace=True) + row['VolManifold']=0 + row['VolHull'] = 0 + row['Status'] = 'Error' + filename = row['Filename'] + if filename not in merged_data: + merged_data[filename] = row.to_dict() + else: + # print(merged_data[filename]) + for col in df.columns: + if col != 'Filename' and not pd.isna(row[col]): + merged_data[filename][col+"_"+implementation] = row[col] + + # multiline_data.append(row.tolist()) + # print(merged_data) + + if not merged_data: + print("Warning: No valid data found in any CSV files.") + return + + # Creating df from the dictionary to store the merged data + merged_data = pd.DataFrame.from_dict(merged_data, orient='index') + + merged_data.to_csv(output_file, index=False) + +csv_files = [('Hull1.csv','hull1'),('CGAL.csv', 'CGAL')] +parse_csv_and_merge(csv_files) + + +# NORMALIZE THE DATA + + +file_path = 'merged_data.csv' +df = pd.read_csv(file_path) + +time_columns = [col for col in df.columns if 'Time' in col] +for col in time_columns: + df[col] = df[col].str.replace(' sec', '').astype(float) + +# List of base columns to normalize against +base_columns = ['VolManifold', 'VolHull', 'AreaManifold', 'AreaHull', 'ManifoldTri', 'HullTri', 'Time'] +# List of suffixes to normalize +suffixes = ['_CGAL'] +# for suffix in suffixes : +# For time metric avoiding cases with time less than 0.001 seconds +# df = df[(df['Time'] > 0.001)] +# Normalize the columns and check for zero base values +stl_files_with_diff = [] + +for base in base_columns: + base_col = base + if base_col in df.columns: + for suffix in suffixes: + col_name = f"{base}{suffix}" + if col_name in df.columns: + # Checking if base column is zero and suffix column is not zero + zero_base_nonzero_suffix = (df[base_col] == 0) & (df[col_name] != 0) + if zero_base_nonzero_suffix.any(): + raise ValueError(f"Error: {base_col} is zero while {col_name} is not zero in row(s): {df[zero_base_nonzero_suffix].index.tolist()}") + + # Setting col_name column in df to 1 if both are zero + both_zero = (df[base_col] == 0) & (df[col_name] == 0) + df.loc[both_zero, col_name] = 1 + + # Normalizing the column while handling division by zero + df[col_name] = df[col_name] / df[base_col].replace({0: 1}) + + df[base_col] = 1.0 + + +df.to_csv('normalized_output.csv', index=False) + + +# CALCULATE STATISTICS ON NORMALZIED OUTPUT + + +import pandas as pd + +file_path = 'normalized_output.csv' +df = pd.read_csv(file_path) + +# Columns for statistics calculation +columns = ['VolHull', 'AreaHull', 'HullTri', 'Time'] +# Columns suffixes to use +suffixes = ['', '_CGAL'] + +# Function to calculate statistics for each base and implementation +def calculate_stats(column, status,suffix): + filtered_df = df[(df['Status'+suffix] == status) & ~df[column].isnull()] + # filtered_df = df[(df['Status'+suffix] == status) & ~df[column].isnull() & (df['Time'+suffix] > 0.001) & (df['Time'] > 0.001)] + success_count = filtered_df.shape[0] + + if success_count > 0: + mean_val = filtered_df[column].mean() + median_val = filtered_df[column].median() + mode_val = filtered_df[column].mode().iloc[0] if not filtered_df[column].mode().empty else None + max_val = filtered_df[column].max() + min_val = filtered_df[column].min() + else: + mean_val = median_val = mode_val = max_val = min_val = None + + return mean_val, median_val, mode_val, max_val, min_val, success_count + +stats_dict = {} + +# Calculating stats for each column and their suffixes +for base in columns: + for suffix in suffixes: + col_name = f"{base}{suffix}" + if col_name in df.columns: + mean_val, median_val, mode_val, max_val, min_val, success_count = calculate_stats(col_name, 'Success',suffix) + stats_dict[col_name] = { + 'mean': mean_val, + 'median': median_val, + 'mode': mode_val, + 'max': max_val, + 'min': min_val, + 'Success_Count': success_count + } + +# Converting the stats dictionary to a df for better visualization +stats_df = pd.DataFrame(stats_dict).T + +stats_df.to_csv('statistics_output.csv') + +print("Statistics calculation complete. Output saved to 'statistics_output.csv'.") +print(stats_df) diff --git a/extras/perf_test_cgal.cpp b/extras/perf_test_cgal.cpp index 562511425..6b48b2849 100644 --- a/extras/perf_test_cgal.cpp +++ b/extras/perf_test_cgal.cpp @@ -40,7 +40,7 @@ typedef CGAL::SM_Vertex_index Vertex; void manifoldToCGALSurfaceMesh(Manifold &manifold, TriangleMesh &cgalMesh) { auto maniMesh = manifold.GetMesh(); - const int n = maniMesh.vertPos.size(); + const size_t n = maniMesh.vertPos.size(); std::vector vertices(n); for (size_t i = 0; i < n; i++) { auto &vert = maniMesh.vertPos[i]; diff --git a/extras/run.sh b/extras/run.sh new file mode 100755 index 000000000..2b5eb8b6f --- /dev/null +++ b/extras/run.sh @@ -0,0 +1,41 @@ +#!/bin/bash + +# Go to manifold/extras directory and run the command as `./run.sh {path_to_dataset_folder} {name_of_csv} {implementation(Hull,Hull_CGAL)}` +# example ./run.sh ./Thingi10K/raw_meshes/ Hull4.csv Hull + +# Checking if the correct number of arguments is provided +if [ "$#" -ne 3 ]; then + echo "Usage: $0 " + exit 1 +fi + +EXECUTABLE="../build/extras/testHullPerformance" +INPUT_FOLDER=$1 +OUTPUT_CSV=$2 +IMPLEMENTATION=$3 +TIME_LIMIT=10m # time limit in minutes +RAM_LIMIT=6000 # Memory limit in MB + +# Initializing the headers +echo "Filename,VolManifold,VolHull,AreaManifold,AreaHull,ManifoldTri,HullTri,Time,Status," > $OUTPUT_CSV + +# Iterate over all files in the input folder +for INPUT_FILE in "$INPUT_FOLDER"/*; do + FILE_NAME=$(basename "$INPUT_FILE") + + # Run the EXECUTABLE with the specified argument, time limit, and used to capture the output + OUTPUT=$(ulimit -v $((RAM_LIMIT * 1024)); timeout $TIME_LIMIT $EXECUTABLE "Input" "$IMPLEMENTATION" "0" "$INPUT_FILE" 2>&1) + STATUS=$? + + # Checking if the EXECUTABLE timed out + if [ $STATUS -eq 124 ]; then + STATUS="Timeout" + elif [ $STATUS -ne 0 ]; then + STATUS="Error" + else + STATUS="Success" + fi + + # Adding the result to the output file + echo "\"$FILE_NAME\",$OUTPUT,\"$STATUS\"" >> $OUTPUT_CSV +done diff --git a/extras/test_hull_performance.cpp b/extras/test_hull_performance.cpp new file mode 100644 index 000000000..6b43a989e --- /dev/null +++ b/extras/test_hull_performance.cpp @@ -0,0 +1,269 @@ +// Copyright 2022 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 +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include // For string manipulation + +#include "manifold.h" +#include "meshIO.h" +#include "samples.h" +using namespace std; +using namespace glm; +using namespace manifold; + +// Epick = Exact predicates Inexact constructions. Seems fair to use to compare +// to Manifold, which seems to store double coordinates. +typedef CGAL::Epick Kernel; + +// Epeck = Exact predicates Exact constructions. What OpenSCAD uses to guarantee +// geometry ends up where it should even after many operations. typedef +// CGAL::Epeck Kernel; + +typedef CGAL::Point_3 Point; +typedef CGAL::Surface_mesh TriangleMesh; +typedef CGAL::SM_Vertex_index Vertex; + +class HullImpl { + public: + // actual hull operation, we will measure the time needed to evaluate this + // function. + virtual void hull(const manifold::Manifold &input, + const std::vector &pts) = 0; + virtual ~HullImpl() = default; + +#ifdef MANIFOLD_DEBUG + // Check if the mesh remains convex after adding new faces + bool isMeshConvex() { + // Get the mesh from the manifold + manifold::Mesh mesh = hullManifold.GetMesh(); + + const auto &vertPos = mesh.vertPos; + + // Iterate over each triangle + for (const auto &tri : mesh.triVerts) { + // Get the vertices of the triangle + glm::vec3 v0 = vertPos[tri[0]]; + glm::vec3 v1 = vertPos[tri[1]]; + glm::vec3 v2 = vertPos[tri[2]]; + + // Compute the normal of the triangle + glm::vec3 normal = glm::normalize(glm::cross(v1 - v0, v2 - v0)); + + // Check all other vertices + for (int i = 0; i < (int)vertPos.size(); ++i) { + if (i == tri[0] || i == tri[2] || i == tri[3]) + continue; // Skip vertices of the current triangle + + // Get the vertex + glm::vec3 v = vertPos[i]; + + // Compute the signed distance from the plane + double distance = glm::dot(normal, v - v0); + + // If any vertex lies on the opposite side of the normal direction + if (distance > 0) { + // The manifold is not convex + return false; + } + } + } + // If we didn't find any vertex on the opposite side for any triangle, it's + // convex + return true; + } +#endif + + protected: + manifold::Manifold hullManifold; + manifold::Manifold inputManifold; +}; + +class HullImplOriginal : public HullImpl { + public: + void hull(const manifold::Manifold &input, + const std::vector &pts) override { + inputManifold = input; + auto start = std::chrono::high_resolution_clock::now(); + hullManifold = Manifold::Hull(pts); + auto end = std::chrono::high_resolution_clock::now(); + PrintVolArea(); + std::chrono::duration elapsed = end - start; + std::cout << elapsed.count() << " sec"; + } + + private: + // Prints the volume and area of the manifold and the convex hull of our + // implementation, + void PrintVolArea() { + std::cout << inputManifold.GetProperties().volume << ","; + std::cout << hullManifold.GetProperties().volume << ","; + std::cout << inputManifold.GetProperties().surfaceArea << ","; + std::cout << hullManifold.GetProperties().surfaceArea << ","; + std::cout << inputManifold.NumTri() << ","; + std::cout << hullManifold.NumTri() << ","; + return; + } +}; + +class HullImplCGAL : public HullImpl { + private: + // Converts Manfiold to CGAL Surface Mesh + void manifoldToCGALSurfaceMesh(Manifold &manifold, TriangleMesh &cgalMesh) { + auto maniMesh = manifold.GetMesh(); + + const size_t n = maniMesh.vertPos.size(); + std::vector vertices(n); + for (size_t i = 0; i < n; i++) { + auto &vert = maniMesh.vertPos[i]; + vertices[i] = cgalMesh.add_vertex(Point(vert.x, vert.y, vert.z)); + } + + for (auto &triVert : maniMesh.triVerts) { + std::vector polygon{vertices[triVert[0]], vertices[triVert[1]], + vertices[triVert[2]]}; + cgalMesh.add_face(polygon); + } + } + // Converts CGAL Surface Mesh to Manifold + void CGALToManifoldSurfaceMesh(TriangleMesh &cgalMesh, Manifold &manifold) { + Mesh mesh; + mesh.triVerts.reserve(faces(cgalMesh).size() * 3); + for (auto v : vertices(cgalMesh)) { + auto pt = cgalMesh.point(v); + mesh.vertPos.push_back({pt.x(), pt.y(), pt.z()}); + } + for (auto f : faces(cgalMesh)) { + vector::size_type> verts; + for (auto v : vertices_around_face(cgalMesh.halfedge(f), cgalMesh)) { + verts.push_back(v.idx()); + } + mesh.triVerts.push_back({verts[0], verts[1], verts[2]}); + } + manifold = Manifold(mesh); + } + // Prints the volume and area of the manifold and Convex Hull of CGAL + // Implementation + void PrintVolAreaCGAL() { + std::cout << CGAL::Polygon_mesh_processing::volume(cgalInput) << ","; + std::cout << CGAL::Polygon_mesh_processing::volume(cgalHull) << ","; + std::cout << CGAL::Polygon_mesh_processing::area(cgalInput) << ","; + std::cout << CGAL::Polygon_mesh_processing::area(cgalHull) << ","; + std::cout << std::distance(cgalInput.faces_begin(), cgalInput.faces_end()) + << ","; + std::cout << std::distance(cgalHull.faces_begin(), cgalHull.faces_end()) + << ","; + return; + } + TriangleMesh cgalHull, cgalInput; + + public: + // This was the code where I converted the CGAL Hull back to manifold, but I + // thought that added overhead, so if needed we can also use the functions of + // the library to print the output as well void hull(const + // std::vector& pts) { + // std::vector points; + // for (const auto &vert : pts) { + // points.push_back(Point(vert.x, vert.y, vert.z)); + // } + // // Convex Hull + // CGAL::convex_hull_3(points.begin(), points.end(), cgalHull); + // CGALToManifoldSurfaceMesh(cgalHull, hullManifold); + // } + void hull(const manifold::Manifold &input, + const std::vector &pts) override { + inputManifold = input; + manifoldToCGALSurfaceMesh(inputManifold, cgalInput); + std::vector points; + for (const auto &vert : cgalInput.vertices()) { + points.push_back(cgalInput.point(vert)); + } + // Convex Hull + auto start = std::chrono::high_resolution_clock::now(); + CGAL::convex_hull_3(points.begin(), points.end(), cgalHull); + auto end = std::chrono::high_resolution_clock::now(); + PrintVolAreaCGAL(); + std::chrono::duration elapsed = end - start; + std::cout << elapsed.count() << " sec"; + } +}; + +// Constructs a Menger Sponge, and tests the convex hull implementation on it +// (you can pass the specific hull implementation to be tested). Comparing the +// volume and surface area with CGAL implementation, for various values of +// rotation +void MengerTestHull(HullImpl *impl, float rx, float ry, float rz, + char *implementation) { + if (impl == NULL) return; + Manifold sponge = MengerSponge(4); + sponge = sponge.Rotate(rx, ry, rz); + impl->hull(sponge, sponge.GetMesh().vertPos); +} + +// Constructs a high quality sphere, and tests the convex hull implementation on +// it (you can pass the specific hull implementation to be tested). Comparing +// the volume and surface area with CGAL implementation +void SphereTestHull(HullImpl *impl, char *implementation) { + if (impl == NULL) return; + Manifold sphere = Manifold::Sphere(1, 6000); + sphere = sphere.Translate(glm::vec3(0.5)); + impl->hull(sphere, sphere.GetMesh().vertPos); +} + +int main(int argc, char **argv) { + if (argc < 4) { + std::cout << "Usage: ./test_hull_performance " + " " + " " + << std::endl; + return 0; + } + if (!strcmp(argv[3], "1")) + std::cout + << "VolManifold,VolHull,AreaManifold,AreaHull,ManifoldTri,HullTri,Time" + << std::endl; + + HullImpl *hullImpl = NULL; + if (!strcmp(argv[2], "Hull")) + hullImpl = new HullImplOriginal(); + else if (!strcmp(argv[2], "Hull_CGAL")) + hullImpl = new HullImplCGAL(); + else { + std::cout << "Invalid Implementation"; + return 0; + } + if (!strcmp(argv[1], "Sphere")) + SphereTestHull(hullImpl, argv[2]); + else if (!strcmp(argv[1], "Menger")) + MengerTestHull(hullImpl, 1, 2, 3, argv[2]); + else if (!strcmp(argv[1], "Input")) { + auto inputMesh = ImportMesh(argv[4], 1); + Manifold inputManifold = Manifold(inputMesh); + hullImpl->hull(inputManifold, inputManifold.GetMesh().vertPos); +#ifdef MANIFOLD_DEBUG + if (!hullImpl->isMeshConvex()) cout << "INVALID HULL" << endl; +#endif + } +} diff --git a/test/hull_test.cpp b/test/hull_test.cpp index ef1601f69..25bac36b6 100644 --- a/test/hull_test.cpp +++ b/test/hull_test.cpp @@ -22,6 +22,46 @@ using namespace manifold; +// Check if the mesh remains convex after adding new faces +bool isMeshConvex(manifold::Manifold hullManifold) { + // Get the mesh from the manifold + manifold::Mesh mesh = hullManifold.GetMesh(); + + const auto &vertPos = mesh.vertPos; + + // Iterate over each triangle + for (const auto &tri : mesh.triVerts) { + // Get the vertices of the triangle + glm::vec3 v0 = vertPos[tri[0]]; + glm::vec3 v1 = vertPos[tri[1]]; + glm::vec3 v2 = vertPos[tri[2]]; + + // Compute the normal of the triangle + glm::vec3 normal = glm::normalize(glm::cross(v1 - v0, v2 - v0)); + + // Check all other vertices + for (int i = 0; i < (int)vertPos.size(); ++i) { + if (i == tri[0] || i == tri[2] || i == tri[3]) + continue; // Skip vertices of the current triangle + + // Get the vertex + glm::vec3 v = vertPos[i]; + + // Compute the signed distance from the plane + double distance = glm::dot(normal, v - v0); + + // If any vertex lies on the opposite side of the normal direction + if (distance > 0) { + // The manifold is not convex + return false; + } + } + } + // If we didn't find any vertex on the opposite side for any triangle, it's + // convex + return true; +} + TEST(Hull, Tictac) { const float tictacRad = 100; const float tictacHeight = 500; @@ -74,4 +114,77 @@ TEST(Hull, Empty) { const std::vector coplanar{ {0, 0, 0}, {1, 0, 0}, {0, 1, 0}, {1, 1, 0}}; EXPECT_TRUE(Manifold::Hull(coplanar).IsEmpty()); +} + +TEST(Hull, MengerSponge) { + Manifold sponge = MengerSponge(4); + sponge = sponge.Rotate(10, 20, 30); + Manifold spongeHull = sponge.Hull(); + EXPECT_EQ(spongeHull.NumTri(), 12); + EXPECT_FLOAT_EQ(spongeHull.GetProperties().surfaceArea, 6); + EXPECT_FLOAT_EQ(spongeHull.GetProperties().volume, 1); +} + +TEST(Hull, Sphere) { + Manifold sphere = Manifold::Sphere(1, 1500); + sphere = sphere.Translate(glm::vec3(0.5)); + Manifold sphereHull = sphere.Hull(); + EXPECT_EQ(sphereHull.NumTri(), sphere.NumTri()); + EXPECT_FLOAT_EQ(sphereHull.GetProperties().volume, + sphere.GetProperties().volume); +} + +TEST(Hull, DISABLED_FailingTest1) { + // 39202.stl + const std::vector hullPts = { + {-24.983196259f, -43.272167206f, 52.710712433f}, + {-25.0f, -12.7726717f, 49.907142639f}, + {-23.016393661f, 39.865562439f, 79.083930969f}, + {-24.983196259f, -40.272167206f, 52.710712433f}, + {-4.5177311897f, -28.633184433f, 50.405872345f}, + {11.176083565f, -22.357545853f, 45.275596619f}, + {-25.0f, 21.885698318f, 49.907142639f}, + {-17.633232117f, -17.341972351f, 89.96282196f}, + {26.922552109f, 10.344738007f, 57.146999359f}, + {-24.949174881f, 1.5f, 54.598075867f}, + {9.2058267593f, -23.47851944f, 55.334011078f}, + {13.26748085f, -19.979951859f, 28.117856979f}, + {-18.286884308f, 31.673814774f, 2.1749999523f}, + {18.419618607f, -18.215343475f, 52.450099945f}, + {-24.983196259f, 43.272167206f, 52.710712433f}, + {-1.6232370138f, -29.794223785f, 48.394889832f}, + {49.865573883f, -0.0f, 55.507141113f}, + {-18.627283096f, -39.544368744f, 55.507141113f}, + {-20.442623138f, -35.407661438f, 8.2749996185f}, + {10.229375839f, -14.717799187f, 10.508025169f}}; + auto hull = Manifold::Hull(hullPts); + EXPECT_TRUE(isMeshConvex(hull)); +} + +TEST(Hull, DISABLED_FailingTest2) { + // 1750623.stl + const std::vector hullPts = { + {174.17001343f, -12.022000313f, 29.562002182f}, + {174.51400757f, -10.858000755f, -3.3340001106f}, + {187.50801086f, 22.826000214f, 23.486001968f}, + {172.42800903f, 12.018000603f, 28.120000839f}, + {180.98001099f, -26.866001129f, 6.9100003242f}, + {172.42800903f, -12.022000313f, 28.120000839f}, + {174.17001343f, 19.498001099f, 29.562002182f}, + {213.96600342f, 2.9400000572f, -11.100000381f}, + {182.53001404f, -22.49200058f, 23.644001007f}, + {175.89401245f, 19.900001526f, 16.118000031f}, + {211.38601685f, 3.0200002193f, -14.250000954f}, + {183.7440033f, 12.018000603f, 18.090000153f}, + {210.51000977f, 2.5040001869f, -11.100000381f}, + {204.13601685f, 34.724002838f, -11.250000954f}, + {193.23400879f, -24.704000473f, 17.768001556f}, + {171.62800598f, -19.502000809f, 27.320001602f}, + {189.67401123f, 8.486000061f, -5.4080004692f}, + {193.23800659f, 24.704000473f, 17.758001328f}, + {165.36801147f, -6.5600004196f, -14.250000954f}, + {174.17001343f, -19.502000809f, 29.562002182f}, + {190.06401062f, -0.81000006199f, -14.250000954f}}; + auto hull = Manifold::Hull(hullPts); + EXPECT_TRUE(isMeshConvex(hull)); } \ No newline at end of file