@@ -266,20 +266,31 @@ int MeshUtils::findReferenceMesh(std::vector<Mesh>& meshes, int random_subset_si
266266 */
267267
268268// ---------------------------------------------------------------------------
269+ // Robust orientation test: compute the loop's signed-area vector by summing edge
270+ // cross products. The dominant axis of this vector gives the loop's normal; its sign
271+ // determines orientation. Uses ALL vertices, so it is insensitive to where loop[0]
272+ // happens to start and does not suffer from atan2 branch-cut flips.
269273static bool is_clockwise (const Eigen::MatrixXd& V, const Eigen::MatrixXi& F, const std::vector<int >& loop) {
270274 Eigen::RowVector3d centroid{0.0 , 0.0 , 0.0 };
271275 for (const auto & i : loop) {
272276 centroid += V.row (i);
273277 }
274278 centroid /= loop.size ();
275279
276- // todo this is arbitrary and works for the peanut data and initial tests on LA+Septum data
277- // it enforces a consistent ordering in the boundary loop
278- const auto v0 = V.row (loop[0 ]) - centroid;
279- const auto v1 = V.row (loop[1 ]) - centroid;
280- const double angle0 = atan2 (v0.z (), v0.y ());
281- const double angle1 = atan2 (v1.z (), v1.y ());
282- return angle0 > angle1;
280+ Eigen::RowVector3d area_vec{0.0 , 0.0 , 0.0 };
281+ for (size_t i = 0 ; i < loop.size (); i++) {
282+ const Eigen::RowVector3d v0 = V.row (loop[i]) - centroid;
283+ const Eigen::RowVector3d v1 = V.row (loop[(i + 1 ) % loop.size ()]) - centroid;
284+ area_vec += v0.cross (v1);
285+ }
286+
287+ // Use the dominant axis of the area vector as the reference normal. Calling "clockwise"
288+ // the case where the area vector points in the negative dominant-axis direction yields
289+ // consistent results across samples that share the same boundary plane.
290+ int max_axis = 0 ;
291+ if (std::abs (area_vec[1 ]) > std::abs (area_vec[max_axis])) max_axis = 1 ;
292+ if (std::abs (area_vec[2 ]) > std::abs (area_vec[max_axis])) max_axis = 2 ;
293+ return area_vec[max_axis] < 0 ;
283294}
284295
285296// ---------------------------------------------------------------------------
@@ -293,7 +304,32 @@ Mesh MeshUtils::extract_boundary_loop(Mesh mesh) {
293304 throw std::runtime_error (" Expected at least one boundary loop in the mesh" );
294305 }
295306
296- const auto & loop = loops[0 ];
307+ auto loop = loops[0 ]; // copy so we can rotate it to a canonical start vertex
308+
309+ // Rotate the loop so it always starts at a canonical vertex (the one with the
310+ // largest Y coordinate; lexicographic tiebreakers on Z then X). igl::boundary_loop
311+ // returns loops starting at an arbitrary vertex, which produces per-subject
312+ // rotational offsets that destroy inter-subject correspondence on contour domains.
313+ {
314+ size_t canonical = 0 ;
315+ for (size_t i = 1 ; i < loop.size (); i++) {
316+ const double cur_y = V (loop[i], 1 );
317+ const double cur_z = V (loop[i], 2 );
318+ const double cur_x = V (loop[i], 0 );
319+ const double best_y = V (loop[canonical], 1 );
320+ const double best_z = V (loop[canonical], 2 );
321+ const double best_x = V (loop[canonical], 0 );
322+ if (cur_y > best_y ||
323+ (cur_y == best_y && cur_z > best_z) ||
324+ (cur_y == best_y && cur_z == best_z && cur_x > best_x)) {
325+ canonical = i;
326+ }
327+ }
328+ if (canonical != 0 ) {
329+ std::rotate (loop.begin (), loop.begin () + canonical, loop.end ());
330+ }
331+ }
332+
297333 const auto is_cw = is_clockwise (V, F, loop);
298334
299335 auto pts = vtkSmartPointer<vtkPoints>::New ();
@@ -389,6 +425,12 @@ static std::tuple<Eigen::MatrixXd, Eigen::MatrixXi, Eigen::MatrixXd, Eigen::Matr
389425 Eigen::MatrixXi out_F;
390426 Eigen::MatrixXd rem_V;
391427 Eigen::MatrixXi rem_F;
428+
429+ // If either mesh is empty, there can be no shared surface
430+ if (is_empty (src_V, src_F) || is_empty (other_V, other_F)) {
431+ return std::make_tuple (out_V, out_F, src_V, src_F);
432+ }
433+
392434 igl::AABB <Eigen::MatrixXd, 3 > tree;
393435 tree.init (other_V, other_F);
394436
@@ -482,6 +524,10 @@ std::array<Mesh, 3> MeshUtils::shared_boundary_extractor(const Mesh& mesh_l, con
482524 V_r = mesh_r.points ();
483525 F_r = mesh_r.faces ();
484526
527+ if (is_empty (V_l, F_l) || is_empty (V_r, F_r)) {
528+ throw std::runtime_error (" Input mesh is empty. Cannot extract shared boundary from empty meshes" );
529+ }
530+
485531 Eigen::MatrixXd shared_V_l, shared_V_r, rem_V_l, rem_V_r;
486532 Eigen::MatrixXi shared_F_l, shared_F_r, rem_F_l, rem_F_r;
487533 std::tie (shared_V_l, shared_F_l, rem_V_l, rem_F_l) = find_shared_surface (V_l, F_l, V_r, F_r, tol);
@@ -1073,18 +1119,23 @@ vtkSmartPointer<vtkPolyData> MeshUtils::recreate_mesh(vtkSmartPointer<vtkPolyDat
10731119}
10741120
10751121// ---------------------------------------------------------------------------
1076- vtkSmartPointer<vtkPolyData> MeshUtils::repair_mesh (vtkSmartPointer<vtkPolyData> mesh) {
1122+ vtkSmartPointer<vtkPolyData> MeshUtils::repair_mesh (vtkSmartPointer<vtkPolyData> mesh, bool extract_largest ) {
10771123 auto triangle_filter = vtkSmartPointer<vtkTriangleFilter>::New ();
10781124 triangle_filter->SetInputData (mesh);
10791125 triangle_filter->PassLinesOff ();
10801126 triangle_filter->Update ();
10811127
1082- auto connectivity = vtkSmartPointer<vtkPolyDataConnectivityFilter>::New ();
1083- connectivity->SetInputConnection (triangle_filter->GetOutputPort ());
1084- connectivity->SetExtractionModeToLargestRegion ();
1085- connectivity->Update ();
1128+ vtkSmartPointer<vtkPolyData> triangulated = triangle_filter->GetOutput ();
1129+
1130+ if (extract_largest) {
1131+ auto connectivity = vtkSmartPointer<vtkPolyDataConnectivityFilter>::New ();
1132+ connectivity->SetInputData (triangulated);
1133+ connectivity->SetExtractionModeToLargestRegion ();
1134+ connectivity->Update ();
1135+ triangulated = connectivity->GetOutput ();
1136+ }
10861137
1087- auto cleaned = MeshUtils::clean_mesh (connectivity-> GetOutput () );
1138+ auto cleaned = MeshUtils::clean_mesh (triangulated );
10881139
10891140 auto fixed = Mesh (cleaned).fixNonManifold ();
10901141
0 commit comments