LCOV - code coverage report
Current view: top level - src/constraints - MortarSegmentHelper.C (source / functions) Hit Total Coverage
Test: idaholab/moose framework: #33390 (250e9c) with base 846a5c Lines: 581 696 83.5 %
Date: 2026-07-31 18:15:22 Functions: 39 40 97.5 %
Legend: Lines: hit not hit

          Line data    Source code
       1             : //* This file is part of the MOOSE framework
       2             : //* https://mooseframework.inl.gov
       3             : //*
       4             : //* All rights reserved, see COPYRIGHT for full restrictions
       5             : //* https://github.com/idaholab/moose/blob/master/COPYRIGHT
       6             : //*
       7             : //* Licensed under LGPL 2.1, please see LICENSE for details
       8             : //* https://www.gnu.org/licenses/lgpl-2.1.html
       9             : #include "MortarSegmentHelper.h"
      10             : #include "MooseError.h"
      11             : #include "MooseTypes.h"
      12             : 
      13             : #include "libmesh/fe_interface.h"
      14             : #include "libmesh/fe_map.h"
      15             : #include "libmesh/face_quad4.h"
      16             : #include "libmesh/face_tri3.h"
      17             : #include "libmesh/int_range.h"
      18             : #include "libmesh/node.h"
      19             : #include "libmesh/utility.h"
      20             : #if defined(LIBMESH_HAVE_TRIANGLE) || defined(LIBMESH_HAVE_POLY2TRI)
      21             : #include "libmesh/replicated_mesh.h"
      22             : #include "libmesh/mesh_triangle_interface.h"
      23             : #include "libmesh/poly2tri_triangulator.h"
      24             : #endif
      25             : 
      26             : #include <algorithm>
      27             : #include <array>
      28             : #include <cmath>
      29             : #include <limits>
      30             : #include <map>
      31             : #include <numeric>
      32             : #include <optional>
      33             : #include <sstream>
      34             : #include <set>
      35             : #include <string>
      36             : #include <unordered_map>
      37             : #include <utility>
      38             : 
      39             : using namespace libMesh;
      40             : 
      41             : namespace
      42             : {
      43             : 
      44             : constexpr Real mortar_reference_mapping_tolerance = 1e-8;
      45             : 
      46             : bool
      47      368358 : isFinitePoint(const Point & point)
      48             : {
      49     1473432 :   for (const auto component : make_range(Moose::dim))
      50     1105074 :     if (!std::isfinite(point(component)))
      51           0 :       return false;
      52             : 
      53      368358 :   return true;
      54             : }
      55             : 
      56             : // Signed-area test for the 2D triangle (a, b, c). Returns twice the signed area:
      57             : // positive if a->b->c is counter-clockwise, negative if clockwise, zero if
      58             : // collinear. Used as the building block for orientation, point-in-triangle, and
      59             : // circumcircle predicates.
      60             : Real
      61      221600 : orient2dHelper(const Point & a, const Point & b, const Point & c)
      62             : {
      63      221600 :   return (b(0) - a(0)) * (c(1) - a(1)) - (b(1) - a(1)) * (c(0) - a(0));
      64             : }
      65             : 
      66             : Real
      67      108918 : triangleAreaHelper(const Point & a, const Point & b, const Point & c)
      68             : {
      69      108918 :   return 0.5 * std::abs(orient2dHelper(a, b, c));
      70             : }
      71             : 
      72             : // Canonical key for an undirected edge: the two endpoint indices sorted so that
      73             : // (a, b) and (b, a) hash and compare equal. Used to dedupe / look up edges in
      74             : // triangle-adjacency maps.
      75             : std::array<unsigned int, 2>
      76       55905 : canonicalEdgeHelper(const unsigned int a, const unsigned int b)
      77             : {
      78       55905 :   return {{std::min(a, b), std::max(a, b)}};
      79             : }
      80             : 
      81             : // Reorder the three vertex indices (a, b, c) so the resulting triangle is wound
      82             : // counter-clockwise (CCW) in the 2D plane spanned by \p nodes. Many of the
      83             : // triangulation paths (orientation tests, area accumulation, ear-clipping
      84             : // validity checks) assume CCW input, so we normalize before emitting triangles.
      85             : std::array<unsigned int, 3>
      86        6711 : makeCCWTriangleHelper(const std::vector<Point> & nodes,
      87             :                       const unsigned int a,
      88             :                       const unsigned int b,
      89             :                       const unsigned int c)
      90             : {
      91        6711 :   if (orient2dHelper(nodes[a], nodes[b], nodes[c]) >= 0)
      92        5679 :     return {{a, b, c}};
      93        1032 :   return {{a, c, b}};
      94             : }
      95             : 
      96             : bool
      97        6069 : pointInCircumcircleHelper(const Point & a, const Point & b, const Point & c, const Point & p)
      98             : {
      99        6069 :   const auto ax = a(0) - p(0);
     100        6069 :   const auto ay = a(1) - p(1);
     101        6069 :   const auto bx = b(0) - p(0);
     102        6069 :   const auto by = b(1) - p(1);
     103        6069 :   const auto cx = c(0) - p(0);
     104        6069 :   const auto cy = c(1) - p(1);
     105        6069 :   const Real det = (ax * ax + ay * ay) * (bx * cy - by * cx) -
     106        6069 :                    (bx * bx + by * by) * (ax * cy - ay * cx) +
     107        6069 :                    (cx * cx + cy * cy) * (ax * by - ay * bx);
     108        6069 :   const Real orientation = orient2dHelper(a, b, c);
     109        6069 :   return orientation >= 0 ? det > TOLERANCE : det < -TOLERANCE;
     110             : }
     111             : 
     112             : void
     113        4422 : performLocalDelaunayFlips(const std::vector<Point> & poly_nodes,
     114             :                           const std::set<std::array<unsigned int, 2>> & constrained_edges,
     115             :                           std::vector<std::array<unsigned int, 3>> & triangles)
     116             : {
     117        4422 :   bool flipped = true;
     118        9756 :   while (flipped)
     119             :   {
     120        5334 :     flipped = false;
     121             : 
     122        5334 :     std::map<std::array<unsigned int, 2>, std::vector<unsigned int>> edge_to_triangles;
     123       17763 :     for (const auto tri_index : index_range(triangles))
     124             :     {
     125       12429 :       const auto & tri = triangles[tri_index];
     126       12429 :       edge_to_triangles[canonicalEdgeHelper(tri[0], tri[1])].push_back(tri_index);
     127       12429 :       edge_to_triangles[canonicalEdgeHelper(tri[1], tri[2])].push_back(tri_index);
     128       12429 :       edge_to_triangles[canonicalEdgeHelper(tri[2], tri[0])].push_back(tri_index);
     129             :     }
     130             : 
     131       32178 :     for (const auto & [edge, owning_triangles] : edge_to_triangles)
     132             :     {
     133       27756 :       if (owning_triangles.size() != 2 || constrained_edges.count(edge))
     134       20940 :         continue;
     135             : 
     136        6816 :       const auto first_tri_index = owning_triangles[0];
     137        6816 :       const auto second_tri_index = owning_triangles[1];
     138        6816 :       const auto & first_triangle = triangles[first_tri_index];
     139        6816 :       const auto & second_triangle = triangles[second_tri_index];
     140             : 
     141        6816 :       const auto a = edge[0];
     142        6816 :       const auto b = edge[1];
     143             :       const auto first_opposite =
     144        6816 :           *std::find_if(first_triangle.begin(),
     145             :                         first_triangle.end(),
     146       12141 :                         [a, b](const unsigned int vertex) { return vertex != a && vertex != b; });
     147             :       const auto second_opposite =
     148        6816 :           *std::find_if(second_triangle.begin(),
     149             :                         second_triangle.end(),
     150       15024 :                         [a, b](const unsigned int vertex) { return vertex != a && vertex != b; });
     151             : 
     152        6816 :       if (first_opposite == second_opposite)
     153           0 :         continue;
     154             : 
     155             :       const auto side_a =
     156        6816 :           orient2dHelper(poly_nodes[first_opposite], poly_nodes[second_opposite], poly_nodes[a]);
     157             :       const auto side_b =
     158        6816 :           orient2dHelper(poly_nodes[first_opposite], poly_nodes[second_opposite], poly_nodes[b]);
     159        6816 :       if (side_a * side_b >= -TOLERANCE)
     160         747 :         continue;
     161             : 
     162        6069 :       if (!pointInCircumcircleHelper(poly_nodes[first_triangle[0]],
     163        6069 :                                      poly_nodes[first_triangle[1]],
     164        6069 :                                      poly_nodes[first_triangle[2]],
     165        6069 :                                      poly_nodes[second_opposite]))
     166        5157 :         continue;
     167             : 
     168         912 :       triangles[first_tri_index] =
     169         912 :           makeCCWTriangleHelper(poly_nodes, first_opposite, second_opposite, b);
     170         912 :       triangles[second_tri_index] =
     171         912 :           makeCCWTriangleHelper(poly_nodes, second_opposite, first_opposite, a);
     172         912 :       flipped = true;
     173         912 :       break;
     174             :     }
     175        5334 :   }
     176        4422 : }
     177             : 
     178             : #if defined(LIBMESH_HAVE_TRIANGLE) || defined(LIBMESH_HAVE_POLY2TRI)
     179             : void
     180        2211 : triangulateConstrainedDelaunayPolygon(std::vector<Point> & poly_nodes,
     181             :                                       const Real area_tol,
     182             :                                       const Real length_tol,
     183             :                                       std::vector<std::vector<unsigned int>> & tri_map)
     184             : {
     185        2211 :   Parallel::Communicator comm_self;
     186        2211 :   ReplicatedMesh triangulation_mesh(comm_self, 2);
     187        2211 :   std::unordered_map<dof_id_type, unsigned int> node_id_to_local_index;
     188        2211 :   node_id_to_local_index.reserve(poly_nodes.size());
     189             : 
     190       11520 :   for (const auto i : index_range(poly_nodes))
     191        9309 :     triangulation_mesh.add_point(poly_nodes[i], i);
     192             : 
     193        2211 :   triangulation_mesh.set_mesh_dimension(2);
     194             : 
     195             : #ifdef LIBMESH_HAVE_TRIANGLE
     196             :   TriangleInterface triangulator(triangulation_mesh);
     197             : #else
     198        2211 :   Poly2TriTriangulator triangulator(triangulation_mesh);
     199        2211 :   triangulator.set_refine_boundary_allowed(false);
     200             : #endif
     201             : 
     202        2211 :   triangulator.triangulation_type() = TriangulatorInterface::PSLG;
     203        2211 :   triangulator.elem_type() = TRI3;
     204        2211 :   triangulator.set_interpolate_boundary_points(0);
     205        2211 :   triangulator.set_verify_hole_boundaries(false);
     206        2211 :   triangulator.desired_area() = 0;
     207        2211 :   triangulator.minimum_angle() = 0;
     208        2211 :   triangulator.smooth_after_generating() = false;
     209        2211 :   triangulator.quiet() = true;
     210        2211 :   triangulator.segments.reserve(poly_nodes.size());
     211       11520 :   for (const auto i : index_range(poly_nodes))
     212        9309 :     triangulator.segments.emplace_back(i, (i + 1) % poly_nodes.size());
     213             : 
     214        2211 :   triangulator.triangulate();
     215             : 
     216             :   // node_ptr_range() and active_element_ptr_range() iterate in id order on this
     217             :   // serial ReplicatedMesh, so no explicit sort is needed.
     218       11520 :   for (const auto * const node : triangulation_mesh.node_ptr_range())
     219        9309 :     if (!node_id_to_local_index.count(node->id()))
     220             :     {
     221             :       // Node inherits from Point and the triangulator operates on a 2D plane, so
     222             :       // the libMesh node already lives at z = 0 and we can use it directly.
     223        9309 :       unsigned int matched_index = libMesh::invalid_uint;
     224        9309 :       Real best_distance = std::numeric_limits<Real>::max();
     225             : 
     226       48870 :       for (const auto i : index_range(poly_nodes))
     227             :       {
     228       39561 :         const Real distance = (*node - poly_nodes[i]).norm();
     229       39561 :         if (distance <= length_tol && distance < best_distance)
     230             :         {
     231        9309 :           matched_index = i;
     232        9309 :           best_distance = distance;
     233             :         }
     234             :       }
     235             : 
     236        9309 :       if (matched_index == libMesh::invalid_uint)
     237             :       {
     238           0 :         matched_index = cast_int<unsigned int>(poly_nodes.size());
     239           0 :         poly_nodes.push_back(*node);
     240             :       }
     241             : 
     242        9309 :       node_id_to_local_index.emplace(node->id(), matched_index);
     243        2211 :     }
     244             : 
     245        2211 :   std::vector<std::array<unsigned int, 3>> triangles;
     246        2211 :   triangles.reserve(triangulation_mesh.n_elem());
     247             : 
     248        7098 :   for (const auto * const elem : triangulation_mesh.active_element_ptr_range())
     249             :   {
     250             :     mooseAssert(elem->type() == TRI3,
     251             :                 "The delaunay mortar triangulation backend produced a non-TRI3 element: "
     252             :                     << static_cast<int>(elem->type()));
     253             : 
     254             :     std::array<unsigned int, 3> local_triangle;
     255       19548 :     for (const auto i : index_range(local_triangle))
     256       14661 :       local_triangle[i] = libmesh_map_find(node_id_to_local_index, elem->node_id(i));
     257             : 
     258        4887 :     const Real orientation = orient2dHelper(poly_nodes[local_triangle[0]],
     259        4887 :                                             poly_nodes[local_triangle[1]],
     260        4887 :                                             poly_nodes[local_triangle[2]]);
     261        4887 :     if (std::abs(orientation) <= 2. * area_tol)
     262           0 :       continue;
     263             : 
     264        4887 :     if (orientation < 0)
     265           0 :       std::swap(local_triangle[1], local_triangle[2]);
     266             : 
     267        4887 :     triangles.push_back(local_triangle);
     268        2211 :   }
     269             : 
     270        2211 :   std::set<std::array<unsigned int, 2>> constrained_edges;
     271       11520 :   for (const auto i : index_range(poly_nodes))
     272        9309 :     constrained_edges.insert(canonicalEdgeHelper(i, (i + 1) % poly_nodes.size()));
     273             : 
     274        2211 :   performLocalDelaunayFlips(poly_nodes, constrained_edges, triangles);
     275             : 
     276        2211 :   std::set<std::array<unsigned int, 3>> seen_triangles;
     277        7098 :   for (auto local_triangle : triangles)
     278             :   {
     279        4887 :     auto canonical_triangle = local_triangle;
     280        4887 :     std::sort(canonical_triangle.begin(), canonical_triangle.end());
     281        4887 :     if (!seen_triangles.insert(canonical_triangle).second)
     282           0 :       continue;
     283             : 
     284       14661 :     tri_map.push_back({local_triangle[0], local_triangle[1], local_triangle[2]});
     285             :   }
     286        2211 : }
     287             : #endif
     288             : 
     289             : } // namespace
     290             : 
     291       11051 : MortarSegmentHelper::MortarSegmentHelper(std::vector<Point> secondary_nodes,
     292             :                                          const Point & center,
     293             :                                          const Point & normal,
     294             :                                          const MortarSegmentTriangulationMode triangulation_mode,
     295       11051 :                                          const bool triangulate_triangles)
     296             :   : MortarSegmentHelper(
     297       11051 :         std::move(secondary_nodes), {}, center, normal, triangulation_mode, triangulate_triangles)
     298             : {
     299       11051 : }
     300             : 
     301       11781 : MortarSegmentHelper::MortarSegmentHelper(std::vector<Point> secondary_nodes,
     302             :                                          std::vector<Point> secondary_reference_points,
     303             :                                          const Point & center,
     304             :                                          const Point & normal,
     305             :                                          const MortarSegmentTriangulationMode triangulation_mode,
     306       11781 :                                          const bool triangulate_triangles)
     307       11781 :   : _center(center),
     308       11781 :     _normal(normal),
     309       11781 :     _debug(false),
     310       11781 :     _triangulation_mode(triangulation_mode),
     311       11781 :     _triangulate_triangles(triangulate_triangles),
     312       11781 :     _secondary_reference_points(std::move(secondary_reference_points))
     313             : {
     314             :   mooseAssert(_secondary_reference_points.empty() ||
     315             :                   secondary_nodes.size() == _secondary_reference_points.size(),
     316             :               "Each projected secondary node needs one parent reference point.");
     317             : 
     318       11781 :   _secondary_poly.clear();
     319       11781 :   _secondary_poly.reserve(secondary_nodes.size());
     320             : 
     321             :   // Get orientation of secondary poly
     322       11781 :   const Point e1 = secondary_nodes[0] - secondary_nodes[1];
     323       11781 :   const Point e2 = secondary_nodes[2] - secondary_nodes[1];
     324       11781 :   const Real orient = e2.cross(e1) * _normal;
     325             : 
     326             :   // u and v define the tangent plane of the element (at center)
     327             :   // Note we embed orientation into our transformation to make 2D poly always
     328             :   // positively oriented
     329       11781 :   _u = _normal.cross(secondary_nodes[0] - center).unit();
     330       11781 :   _v = (orient > 0) ? _normal.cross(_u).unit() : _u.cross(_normal).unit();
     331             : 
     332             :   // Transform problem to 2D plane spanned by u and v
     333       52695 :   for (const auto & node : secondary_nodes)
     334             :   {
     335       40914 :     Point pt = node - _center;
     336       40914 :     _secondary_poly.emplace_back(pt * _u, pt * _v, 0);
     337             :   }
     338             : 
     339             :   // Initialize area of secondary polygon
     340       11781 :   _remaining_area_fraction = 1.0;
     341       11781 :   _secondary_area = area(_secondary_poly);
     342             : 
     343             :   // Tolerance for quantities with area dimensions
     344       11781 :   _area_tol = _tolerance * _secondary_area;
     345             : 
     346             :   // Tolerance for quantites with length dimensions
     347       11781 :   _length_tol = _tolerance * std::sqrt(_secondary_area);
     348       11781 : }
     349             : 
     350             : Point
     351      224006 : MortarSegmentHelper::getIntersection(
     352             :     const Point & p1, const Point & p2, const Point & q1, const Point & q2, Real & s) const
     353             : {
     354      224006 :   const Point dp = p2 - p1;
     355      224006 :   const Point dq = q2 - q1;
     356      224006 :   const Real cp1q1 = p1(0) * q1(1) - p1(1) * q1(0);
     357      224006 :   const Real cp1q2 = p1(0) * q2(1) - p1(1) * q2(0);
     358      224006 :   const Real cq1q2 = q1(0) * q2(1) - q1(1) * q2(0);
     359      224006 :   const Real alpha = 1. / (dp(0) * dq(1) - dp(1) * dq(0));
     360      224006 :   s = -alpha * (cp1q2 - cp1q1 - cq1q2);
     361             : 
     362             :   // Intersection should be between p1 and p2, if it's not (due to poor conditioning), simply
     363             :   // move it to one of the end points
     364      224006 :   s = s > 1 ? 1. : s;
     365      224006 :   s = s < 0 ? 0. : s;
     366      224006 :   return p1 + s * dp;
     367             : }
     368             : 
     369             : bool
     370           0 : MortarSegmentHelper::isInsideSecondary(const Point & pt) const
     371             : {
     372           0 :   for (auto i : index_range(_secondary_poly))
     373             :   {
     374           0 :     const Point & q1 = _secondary_poly[i];
     375           0 :     const Point & q2 = _secondary_poly[(i + 1) % _secondary_poly.size()];
     376             : 
     377           0 :     const Point e1 = q2 - q1;
     378           0 :     const Point e2 = pt - q1;
     379             : 
     380             :     // If point corresponds to one of the secondary vertices, skip
     381           0 :     if (e2.norm() < _tolerance)
     382           0 :       return true;
     383             : 
     384           0 :     const bool inside = (e1(0) * e2(1) - e1(1) * e2(0)) < _area_tol;
     385           0 :     if (!inside)
     386           0 :       return false;
     387             :   }
     388           0 :   return true;
     389             : }
     390             : 
     391             : bool
     392      393422 : MortarSegmentHelper::isDisjoint(const std::vector<Point> & poly) const
     393             : {
     394      935378 :   for (auto i : index_range(_secondary_poly))
     395             :   {
     396             :     // Get edge to check
     397      875031 :     const Point & q1 = _secondary_poly[i];
     398      875031 :     const Point & q2 = _secondary_poly[(i + 1) % _secondary_poly.size()];
     399      875031 :     const Point edg = q2 - q1;
     400      875031 :     const Real cp = q2(0) * q1(1) - q2(1) * q1(0);
     401             : 
     402             :     // If more optimization needed, could store these values for later
     403             :     // Check if point is to the left of (or on) clip_edge
     404     2956597 :     auto is_inside = [&edg, cp](Point & pt, Real tol)
     405     2956597 :     { return pt(0) * edg(1) - pt(1) * edg(0) + cp < -tol; };
     406             : 
     407      875031 :     bool all_outside = true;
     408     3831628 :     for (auto pt : poly)
     409     2956597 :       if (is_inside(pt, _area_tol))
     410     1518284 :         all_outside = false;
     411             : 
     412      875031 :     if (all_outside)
     413      333075 :       return true;
     414             :   }
     415       60347 :   return false;
     416             : }
     417             : 
     418             : std::vector<Point>
     419      393422 : MortarSegmentHelper::projectPrimaryPoly(const std::vector<Point> & primary_nodes) const
     420             : {
     421             :   // Check orientation of primary_poly
     422      393422 :   const Point e1 = primary_nodes[0] - primary_nodes[1];
     423      393422 :   const Point e2 = primary_nodes[2] - primary_nodes[1];
     424             : 
     425             :   // Note we use u x v here instead of normal because it may be flipped if secondary elem was
     426             :   // negatively oriented
     427      393422 :   const Real orient = e2.cross(e1) * _u.cross(_v);
     428             : 
     429             :   // Get primary_poly (primary is clipping poly). If negatively oriented, reverse
     430      393422 :   std::vector<Point> primary_poly;
     431      393422 :   const int n_verts = primary_nodes.size();
     432      393422 :   primary_poly.reserve(primary_nodes.size());
     433     1704539 :   for (auto n : index_range(primary_nodes))
     434             :   {
     435     1311117 :     Point pt = (orient > 0) ? primary_nodes[n] - _center : primary_nodes[n_verts - 1 - n] - _center;
     436     1311117 :     primary_poly.emplace_back(pt * _u, pt * _v, 0.);
     437             :   }
     438             : 
     439      786844 :   return primary_poly;
     440           0 : }
     441             : 
     442             : std::vector<Point>
     443      352108 : MortarSegmentHelper::clipPoly(const std::vector<Point> & primary_nodes) const
     444             : {
     445      352108 :   return clipProjectedPoly(projectPrimaryPoly(primary_nodes));
     446             : }
     447             : 
     448             : std::vector<Point>
     449      393422 : MortarSegmentHelper::clipProjectedPoly(const std::vector<Point> & primary_poly) const
     450             : {
     451      393422 :   if (isDisjoint(primary_poly))
     452      333075 :     return {};
     453             : 
     454             :   // Initialize clipped poly with secondary poly (secondary is target poly)
     455       60347 :   std::vector<Point> clipped_poly = _secondary_poly;
     456             : 
     457             :   // Loop through clipping edges
     458      263461 :   for (auto i : index_range(primary_poly))
     459             :   {
     460             :     // If clipped poly trivial, return
     461      208782 :     if (clipped_poly.size() < 3)
     462             :     {
     463        5668 :       clipped_poly.clear();
     464        5668 :       return clipped_poly;
     465             :     }
     466             : 
     467             :     // Set input poly to current clipped poly
     468      203114 :     std::vector<Point> input_poly(clipped_poly);
     469      203114 :     clipped_poly.clear();
     470             : 
     471             :     // Get clipping edge
     472      203114 :     const Point & clip_pt1 = primary_poly[i];
     473      203114 :     const Point & clip_pt2 = primary_poly[(i + 1) % primary_poly.size()];
     474      203114 :     const Point edg = clip_pt2 - clip_pt1;
     475      203114 :     const Real cp = clip_pt2(0) * clip_pt1(1) - clip_pt2(1) * clip_pt1(0);
     476             : 
     477             :     // Check if point is to the left of (or on) clip_edge
     478             :     /*
     479             :      * Note that use of tolerance here is to avoid degenerate case when lines are
     480             :      * essentially on top of each other (common when meshes match across interface)
     481             :      * since finding intersection is ill-conditioned in this case.
     482             :      */
     483     1518780 :     auto is_inside = [&edg, cp](const Point & pt, Real tol)
     484     1518780 :     { return pt(0) * edg(1) - pt(1) * edg(0) + cp < tol; };
     485             : 
     486             :     // Loop through edges of target polygon (with previous clippings already included)
     487      962504 :     for (auto j : index_range(input_poly))
     488             :     {
     489             :       // Get target edge
     490      759390 :       const Point curr_pt = input_poly[(j + 1) % input_poly.size()];
     491      759390 :       const Point prev_pt = input_poly[j];
     492             : 
     493             :       // TODO: Don't need to calculate both each loop
     494      759390 :       const bool is_current_inside = is_inside(curr_pt, _area_tol);
     495      759390 :       const bool is_previous_inside = is_inside(prev_pt, _area_tol);
     496             : 
     497      759390 :       if (is_current_inside)
     498             :       {
     499      524367 :         if (!is_previous_inside)
     500             :         {
     501             :           Real s;
     502      112003 :           Point intersect = getIntersection(prev_pt, curr_pt, clip_pt1, clip_pt2, s);
     503             : 
     504             :           /*
     505             :            * s is the fraction of distance along clip poly edge that intersection lies
     506             :            * It is used here to avoid degenerate polygon cases. For example, consider a
     507             :            * case like:
     508             :            *          o
     509             :            *          |    (inside)
     510             :            *    ------|------
     511             :            *          |    (outside)
     512             :            * when the distance is small (< 1e-7) we don't want to to add both the point
     513             :            * and intersection. Also note that when distance on the scale of 1e-7,
     514             :            * area on scale of 1e-14 so is insignificant if this results in dropping
     515             :            * a tri (for example if next edge crosses again)
     516             :            */
     517      112003 :           if (s < (1 - _tolerance))
     518      110650 :             clipped_poly.push_back(intersect);
     519             :         }
     520      524367 :         clipped_poly.push_back(curr_pt);
     521             :       }
     522      235023 :       else if (is_previous_inside)
     523             :       {
     524             :         Real s;
     525      112003 :         Point intersect = getIntersection(prev_pt, curr_pt, clip_pt1, clip_pt2, s);
     526      112003 :         if (s > _tolerance)
     527      110379 :           clipped_poly.push_back(intersect);
     528             :       }
     529             :     }
     530      203114 :   }
     531             : 
     532             :   // Make sure final clipped poly is not trivial
     533       54679 :   if (clipped_poly.size() < 3)
     534             :   {
     535        1991 :     clipped_poly.clear();
     536        1991 :     return clipped_poly;
     537             :   }
     538             : 
     539             :   // Clean up result by removing any duplicate nodes
     540       52688 :   std::vector<Point> cleaned_poly;
     541       52688 :   cleaned_poly.push_back(clipped_poly.back());
     542      200526 :   for (auto i : make_range(clipped_poly.size() - 1))
     543             :   {
     544      147838 :     const Point prev_pt = cleaned_poly.back();
     545      147838 :     const Point curr_pt = clipped_poly[i];
     546             : 
     547             :     // If points are sufficiently distanced, add to output
     548      147838 :     if ((curr_pt - prev_pt).norm() > _length_tol)
     549      147838 :       cleaned_poly.push_back(curr_pt);
     550             :   }
     551             : 
     552             :   mooseAssert(
     553             :       cleaned_poly.size() <= 8,
     554             :       "Our distributed mesh numbering scheme assumes that we have at most 8 nodes resulting from "
     555             :       "clipping the projection of the primary sub-element onto the secondary sub-element");
     556       52688 :   return cleaned_poly;
     557       60347 : }
     558             : 
     559             : void
     560       52688 : MortarSegmentHelper::triangulatePoly(std::vector<Point> & poly_nodes,
     561             :                                      std::vector<std::vector<unsigned int>> & tri_map) const
     562             : {
     563             :   // tri_map is populated with triangle indices that are local to poly_nodes (starting at 0).
     564             :   // Callers are responsible for shifting these indices into a global node numbering.
     565        6522 :   const auto polygon_centroid = [](const std::vector<Point> & polygon_nodes)
     566             :   {
     567        6522 :     Point centroid(0);
     568        6522 :     Real double_area = 0;
     569       31622 :     for (const auto i : index_range(polygon_nodes))
     570             :     {
     571       25100 :       const auto & a = polygon_nodes[i];
     572       25100 :       const auto & b = polygon_nodes[(i + 1) % polygon_nodes.size()];
     573       25100 :       const Real cross = a(0) * b(1) - b(0) * a(1);
     574       25100 :       double_area += cross;
     575       25100 :       centroid(0) += (a(0) + b(0)) * cross;
     576       25100 :       centroid(1) += (a(1) + b(1)) * cross;
     577             :     }
     578             : 
     579        6522 :     if (std::abs(double_area) <= TOLERANCE)
     580             :     {
     581         410 :       for (const auto & node : polygon_nodes)
     582         309 :         centroid += node;
     583         101 :       centroid /= polygon_nodes.size();
     584         101 :       return centroid;
     585             :     }
     586             : 
     587        6421 :     centroid /= (3. * double_area);
     588        6421 :     centroid(2) = 0;
     589        6421 :     return centroid;
     590             :   };
     591             : 
     592       10557 :   const auto append_triangle = [this, &poly_nodes, &tri_map](
     593             :                                    const unsigned int a, const unsigned int b, const unsigned int c)
     594             :   {
     595       10557 :     if (triangleAreaHelper(poly_nodes[a], poly_nodes[b], poly_nodes[c]) <= _area_tol)
     596           0 :       return false;
     597             : 
     598       10557 :     if (orient2dHelper(poly_nodes[a], poly_nodes[b], poly_nodes[c]) >= 0)
     599       31671 :       tri_map.push_back({a, b, c});
     600             :     else
     601           0 :       tri_map.push_back({a, c, b});
     602             : 
     603       10557 :     return true;
     604       52688 :   };
     605             : 
     606             :   const auto point_in_triangle =
     607       13494 :       [this](const Point & p, const Point & a, const Point & b, const Point & c)
     608             :   {
     609       13494 :     const Real ab = orient2dHelper(a, b, p);
     610       13494 :     const Real bc = orient2dHelper(b, c, p);
     611       13494 :     const Real ca = orient2dHelper(c, a, p);
     612       13494 :     return ab >= -_area_tol && bc >= -_area_tol && ca >= -_area_tol;
     613       52688 :   };
     614             : 
     615       11169 :   const auto min_triangle_angle = [](const Point & a, const Point & b, const Point & c)
     616             :   {
     617       33471 :     const auto clamp_cos = [](Real value) { return std::max(-1., std::min(1., value)); };
     618             :     const auto angle_at =
     619       33507 :         [&clamp_cos](const Point & vertex, const Point & point_one, const Point & point_two)
     620             :     {
     621       33507 :       const Point edge_one = point_one - vertex;
     622       33507 :       const Point edge_two = point_two - vertex;
     623       33507 :       const Real denom = edge_one.norm() * edge_two.norm();
     624       33507 :       if (denom <= TOLERANCE)
     625          36 :         return 0.;
     626       33471 :       return std::acos(clamp_cos((edge_one * edge_two) / denom));
     627       11169 :     };
     628             : 
     629       11169 :     return std::min({angle_at(a, b, c), angle_at(b, c, a), angle_at(c, a, b)});
     630             :   };
     631             : 
     632       13938 :   const auto canonicalize_polygon = [this, &poly_nodes]()
     633             :   {
     634       13938 :     if (poly_nodes.size() < 3)
     635           0 :       return;
     636             : 
     637       13938 :     if (area(poly_nodes) < 0)
     638           0 :       std::reverse(poly_nodes.begin(), poly_nodes.end());
     639             : 
     640       13938 :     bool changed = true;
     641       25118 :     while (changed && poly_nodes.size() > 3)
     642             :     {
     643       11180 :       changed = false;
     644       58282 :       for (const auto i : index_range(poly_nodes))
     645             :       {
     646       47102 :         const auto prev = (i + poly_nodes.size() - 1) % poly_nodes.size();
     647       47102 :         const auto next = (i + 1) % poly_nodes.size();
     648       47102 :         if ((poly_nodes[i] - poly_nodes[prev]).norm() <= _length_tol ||
     649       94204 :             (poly_nodes[next] - poly_nodes[i]).norm() <= _length_tol ||
     650       47102 :             triangleAreaHelper(poly_nodes[prev], poly_nodes[i], poly_nodes[next]) <= _area_tol)
     651             :         {
     652           0 :           poly_nodes.erase(poly_nodes.begin() + i);
     653           0 :           changed = true;
     654           0 :           break;
     655             :         }
     656             :       }
     657             :     }
     658             : 
     659       13938 :     if (poly_nodes.size() >= 3 && area(poly_nodes) < 0)
     660           0 :       std::reverse(poly_nodes.begin(), poly_nodes.end());
     661       52688 :   };
     662             : 
     663             :   const auto triangulate_with_ear_clipping =
     664        2211 :       [this, &poly_nodes, &point_in_triangle, &min_triangle_angle](
     665             :           const bool perform_delaunay_flips)
     666             :   {
     667        2211 :     std::vector<std::array<unsigned int, 3>> triangles;
     668        2211 :     if (poly_nodes.size() < 3)
     669           0 :       return triangles;
     670             : 
     671        2211 :     if (poly_nodes.size() == 3)
     672             :     {
     673           0 :       triangles.push_back(makeCCWTriangleHelper(poly_nodes, 0, 1, 2));
     674           0 :       return triangles;
     675             :     }
     676             : 
     677        2211 :     std::vector<unsigned int> remaining_vertices(poly_nodes.size());
     678        2211 :     std::iota(remaining_vertices.begin(), remaining_vertices.end(), 0);
     679             : 
     680        4887 :     while (remaining_vertices.size() > 3)
     681             :     {
     682        2676 :       std::optional<std::size_t> best_position;
     683        2676 :       Real best_score = -std::numeric_limits<Real>::max();
     684        2676 :       Real best_area = -std::numeric_limits<Real>::max();
     685             : 
     686       13845 :       for (const auto position : index_range(remaining_vertices))
     687             :       {
     688             :         const auto prev_position =
     689       11169 :             (position + remaining_vertices.size() - 1) % remaining_vertices.size();
     690       11169 :         const auto next_position = (position + 1) % remaining_vertices.size();
     691       11169 :         const auto prev = remaining_vertices[prev_position];
     692       11169 :         const auto curr = remaining_vertices[position];
     693       11169 :         const auto next = remaining_vertices[next_position];
     694             : 
     695       11169 :         if (orient2dHelper(poly_nodes[prev], poly_nodes[curr], poly_nodes[next]) <= _area_tol)
     696           0 :           continue;
     697             : 
     698       11169 :         bool contains_other_vertex = false;
     699       58170 :         for (const auto other : remaining_vertices)
     700             :         {
     701       47001 :           if (other == prev || other == curr || other == next)
     702       33507 :             continue;
     703             : 
     704       13494 :           if (point_in_triangle(
     705       13494 :                   poly_nodes[other], poly_nodes[prev], poly_nodes[curr], poly_nodes[next]))
     706             :           {
     707           0 :             contains_other_vertex = true;
     708           0 :             break;
     709             :           }
     710             :         }
     711             : 
     712       11169 :         if (contains_other_vertex)
     713           0 :           continue;
     714             : 
     715             :         const Real candidate_score =
     716       11169 :             min_triangle_angle(poly_nodes[prev], poly_nodes[curr], poly_nodes[next]);
     717             :         const Real candidate_area =
     718       11169 :             triangleAreaHelper(poly_nodes[prev], poly_nodes[curr], poly_nodes[next]);
     719       16299 :         if (!best_position || candidate_score > best_score + TOLERANCE ||
     720        5058 :             (std::abs(candidate_score - best_score) <= TOLERANCE &&
     721          72 :              candidate_area > best_area + _area_tol))
     722             :         {
     723        6147 :           best_position = position;
     724        6147 :           best_score = candidate_score;
     725        6147 :           best_area = candidate_area;
     726             :         }
     727             :       }
     728             : 
     729        2676 :       if (!best_position)
     730             :       {
     731           0 :         std::vector<std::array<unsigned int, 3>> best_fan;
     732           0 :         Real best_fan_score = -std::numeric_limits<Real>::max();
     733           0 :         Real best_fan_area = -std::numeric_limits<Real>::max();
     734             : 
     735           0 :         for (const auto root_position : index_range(remaining_vertices))
     736             :         {
     737           0 :           std::vector<std::array<unsigned int, 3>> candidate_fan;
     738           0 :           Real candidate_score = std::numeric_limits<Real>::max();
     739           0 :           Real candidate_area = std::numeric_limits<Real>::max();
     740           0 :           bool valid_fan = true;
     741           0 :           const auto root = remaining_vertices[root_position];
     742             : 
     743           0 :           for (unsigned int step = 1; step + 1 < remaining_vertices.size(); ++step)
     744             :           {
     745           0 :             const auto next_position = (root_position + step) % remaining_vertices.size();
     746           0 :             const auto following_position = (root_position + step + 1) % remaining_vertices.size();
     747           0 :             const auto vertex_one = remaining_vertices[next_position];
     748           0 :             const auto vertex_two = remaining_vertices[following_position];
     749             : 
     750           0 :             if (orient2dHelper(poly_nodes[root], poly_nodes[vertex_one], poly_nodes[vertex_two]) <=
     751           0 :                 _area_tol)
     752             :             {
     753           0 :               valid_fan = false;
     754           0 :               break;
     755             :             }
     756             : 
     757           0 :             candidate_fan.push_back(
     758           0 :                 makeCCWTriangleHelper(poly_nodes, root, vertex_one, vertex_two));
     759           0 :             candidate_score =
     760           0 :                 std::min(candidate_score,
     761           0 :                          min_triangle_angle(
     762           0 :                              poly_nodes[root], poly_nodes[vertex_one], poly_nodes[vertex_two]));
     763           0 :             candidate_area =
     764           0 :                 std::min(candidate_area,
     765           0 :                          triangleAreaHelper(
     766           0 :                              poly_nodes[root], poly_nodes[vertex_one], poly_nodes[vertex_two]));
     767             :           }
     768             : 
     769           0 :           if (!valid_fan || candidate_fan.empty())
     770           0 :             continue;
     771             : 
     772           0 :           if (candidate_score > best_fan_score + TOLERANCE ||
     773           0 :               (std::abs(candidate_score - best_fan_score) <= TOLERANCE &&
     774           0 :                candidate_area > best_fan_area + _area_tol))
     775             :           {
     776           0 :             best_fan = std::move(candidate_fan);
     777           0 :             best_fan_score = candidate_score;
     778           0 :             best_fan_area = candidate_area;
     779             :           }
     780           0 :         }
     781             : 
     782           0 :         if (best_fan.empty())
     783           0 :           for (unsigned int i = 1; i + 1 < remaining_vertices.size(); ++i)
     784           0 :             best_fan.push_back(makeCCWTriangleHelper(poly_nodes,
     785           0 :                                                      remaining_vertices[0],
     786           0 :                                                      remaining_vertices[i],
     787           0 :                                                      remaining_vertices[i + 1]));
     788             : 
     789           0 :         triangles.insert(triangles.end(), best_fan.begin(), best_fan.end());
     790           0 :         break;
     791           0 :       }
     792             : 
     793             :       const auto prev_position =
     794        2676 :           (*best_position + remaining_vertices.size() - 1) % remaining_vertices.size();
     795        2676 :       const auto next_position = (*best_position + 1) % remaining_vertices.size();
     796        2676 :       triangles.push_back(makeCCWTriangleHelper(poly_nodes,
     797        2676 :                                                 remaining_vertices[prev_position],
     798        2676 :                                                 remaining_vertices[*best_position],
     799        2676 :                                                 remaining_vertices[next_position]));
     800        2676 :       remaining_vertices.erase(remaining_vertices.begin() + *best_position);
     801             :     }
     802             : 
     803        2211 :     if (remaining_vertices.size() == 3)
     804        2211 :       triangles.push_back(makeCCWTriangleHelper(
     805        2211 :           poly_nodes, remaining_vertices[0], remaining_vertices[1], remaining_vertices[2]));
     806             : 
     807        2211 :     if (!perform_delaunay_flips)
     808           0 :       return triangles;
     809             : 
     810        2211 :     std::set<std::array<unsigned int, 2>> boundary_edges;
     811       11520 :     for (const auto i : index_range(poly_nodes))
     812        9309 :       boundary_edges.insert(canonicalEdgeHelper(i, (i + 1) % poly_nodes.size()));
     813             : 
     814        2211 :     performLocalDelaunayFlips(poly_nodes, boundary_edges, triangles);
     815        2211 :     return triangles;
     816        2211 :   };
     817             : 
     818        4547 :   const auto is_convex_polygon = [this](const std::vector<Point> & polygon_nodes)
     819             :   {
     820        4547 :     if (polygon_nodes.size() <= 3)
     821           0 :       return true;
     822             : 
     823       23722 :     for (const auto i : index_range(polygon_nodes))
     824             :     {
     825       19175 :       const auto prev = (i + polygon_nodes.size() - 1) % polygon_nodes.size();
     826       19175 :       const auto next = (i + 1) % polygon_nodes.size();
     827       19175 :       if (orient2dHelper(polygon_nodes[prev], polygon_nodes[i], polygon_nodes[next]) <= _area_tol)
     828           0 :         return false;
     829             :     }
     830             : 
     831        4547 :     return true;
     832       52688 :   };
     833             : 
     834             :   // Fewer than 3 nodes can't be triangulated
     835       52688 :   if (poly_nodes.size() < 3)
     836           0 :     mooseError("Can't triangulate poly with fewer than 3 nodes");
     837             : 
     838             :   // Legacy centroid path: when the default triangulation (centroid) is selected
     839             :   // and triangle re-tessellation is not requested, reproduce the legacy
     840             :   // algorithm byte-for-byte so existing mortar baselines remain valid.
     841             :   // Uses the arithmetic mean of the vertices (not the area-weighted centroid),
     842             :   // emits one triangle per polygon edge without degeneracy filtering, and skips
     843             :   // the canonicalization pass which would drop near-degenerate vertices and
     844             :   // perturb integration weights in downstream test baselines.
     845       52688 :   if (_triangulation_mode == MortarSegmentTriangulationMode::Centroid && !_triangulate_triangles)
     846             :   {
     847       38750 :     if (poly_nodes.size() == 3)
     848             :     {
     849       30140 :       tri_map.push_back({0, 1, 2});
     850       15070 :       return;
     851             :     }
     852             : 
     853       23680 :     const unsigned int n_verts = poly_nodes.size();
     854       23680 :     Point poly_center;
     855      123620 :     for (const auto & node : poly_nodes)
     856       99940 :       poly_center += node;
     857       23680 :     poly_center /= n_verts;
     858             : 
     859      123620 :     for (const auto i : make_range(n_verts))
     860      299820 :       tri_map.push_back({i, (i + 1) % n_verts, n_verts});
     861             : 
     862       23680 :     poly_nodes.push_back(poly_center);
     863       23680 :     return;
     864             :   }
     865             : 
     866       13938 :   canonicalize_polygon();
     867       13938 :   if (poly_nodes.size() < 3)
     868           0 :     return;
     869             : 
     870       13938 :   if (poly_nodes.size() == 3 && !_triangulate_triangles)
     871             :   {
     872         783 :     append_triangle(0, 1, 2);
     873         783 :     return;
     874             :   }
     875             : 
     876       13155 :   const bool force_triangle_centroid_split = _triangulate_triangles && poly_nodes.size() == 3;
     877             : 
     878       13155 :   if (_triangulation_mode == MortarSegmentTriangulationMode::Vertex &&
     879        2215 :       !force_triangle_centroid_split)
     880             :   {
     881        2211 :     const unsigned int n_verts = poly_nodes.size();
     882        7098 :     for (unsigned int i = 1; i + 1 < n_verts; ++i)
     883        4887 :       append_triangle(0, i, i + 1);
     884        2211 :     return;
     885             :   }
     886             : 
     887       10944 :   if (_triangulation_mode == MortarSegmentTriangulationMode::Delaunay &&
     888        2215 :       !force_triangle_centroid_split)
     889             :   {
     890             : #if defined(LIBMESH_HAVE_TRIANGLE) || defined(LIBMESH_HAVE_POLY2TRI)
     891        2211 :     triangulateConstrainedDelaunayPolygon(poly_nodes, _area_tol, _length_tol, tri_map);
     892        2211 :     return;
     893             : #else
     894             :     mooseError("The 'delaunay' mortar triangulation mode requires libMesh TriangleInterface or "
     895             :                "Poly2Tri support.");
     896             : #endif
     897             :   }
     898             : 
     899        8733 :   if (_triangulation_mode == MortarSegmentTriangulationMode::EarClipping &&
     900        2215 :       !force_triangle_centroid_split)
     901             :   {
     902        7098 :     for (const auto & triangle : triangulate_with_ear_clipping(true))
     903        7098 :       append_triangle(triangle[0], triangle[1], triangle[2]);
     904        2211 :     return;
     905             :   }
     906             : 
     907        6522 :   if (!force_triangle_centroid_split && !is_convex_polygon(poly_nodes))
     908             :   {
     909           0 :     for (const auto & triangle : triangulate_with_ear_clipping(true))
     910           0 :       append_triangle(triangle[0], triangle[1], triangle[2]);
     911           0 :     return;
     912             :   }
     913             : 
     914        6522 :   const unsigned int n_verts = poly_nodes.size();
     915        6522 :   const Point poly_center = polygon_centroid(poly_nodes);
     916             : 
     917        6522 :   bool added_triangle = false;
     918       31622 :   for (const auto i : make_range(n_verts))
     919       25100 :     if (triangleAreaHelper(poly_nodes[i], poly_nodes[(i + 1) % n_verts], poly_center) > _area_tol)
     920             :     {
     921       50200 :       tri_map.push_back({i, (i + 1) % n_verts, n_verts});
     922       25100 :       added_triangle = true;
     923             :     }
     924             : 
     925        6522 :   if (added_triangle)
     926        6522 :     poly_nodes.push_back(poly_center);
     927             : }
     928             : 
     929             : void
     930      352108 : MortarSegmentHelper::getMortarSegments(const std::vector<Point> & primary_nodes,
     931             :                                        std::vector<Point> & nodes,
     932             :                                        std::vector<std::vector<unsigned int>> & elem_to_nodes)
     933             : {
     934      352108 :   getMortarSegmentsImpl(primary_nodes, nodes, elem_to_nodes, nullptr);
     935      352108 : }
     936             : 
     937             : void
     938       41314 : MortarSegmentHelper::getMortarSegments(
     939             :     const std::vector<Point> & primary_nodes,
     940             :     const std::vector<Point> & primary_reference_points,
     941             :     std::vector<Point> & nodes,
     942             :     std::vector<std::vector<unsigned int>> & elem_to_nodes,
     943             :     std::vector<std::array<Point, 3>> & elem_to_secondary_reference_points,
     944             :     std::vector<std::array<Point, 3>> & elem_to_primary_reference_points,
     945             :     const Real minimum_segment_area)
     946             : {
     947             :   ReferenceMappingData reference_mapping{primary_reference_points,
     948             :                                          elem_to_secondary_reference_points,
     949             :                                          elem_to_primary_reference_points,
     950       41314 :                                          minimum_segment_area};
     951       41314 :   getMortarSegmentsImpl(primary_nodes, nodes, elem_to_nodes, &reference_mapping);
     952       41314 : }
     953             : 
     954             : void
     955      393422 : MortarSegmentHelper::getMortarSegmentsImpl(const std::vector<Point> & primary_nodes,
     956             :                                            std::vector<Point> & nodes,
     957             :                                            std::vector<std::vector<unsigned int>> & elem_to_nodes,
     958             :                                            ReferenceMappingData * const reference_mapping)
     959             : {
     960      393422 :   std::vector<Point> primary_poly;
     961      393422 :   std::vector<Point> primary_poly_reference_points;
     962             : 
     963      393422 :   if (reference_mapping)
     964             :   {
     965       41314 :     if (primary_nodes.size() != reference_mapping->primary_reference_points.size())
     966           0 :       mooseError("Reference-interpolation mortar segment generation requires one primary "
     967             :                  "reference point per primary sub-element node.");
     968       41314 :     if (_secondary_poly.size() != _secondary_reference_points.size())
     969           0 :       mooseError("Reference-interpolation mortar segment generation requires one secondary "
     970             :                  "reference point per secondary sub-element node.");
     971       82628 :     if (reference_mapping->elem_to_secondary_reference_points.size() != elem_to_nodes.size() ||
     972       41314 :         reference_mapping->elem_to_primary_reference_points.size() != elem_to_nodes.size())
     973           0 :       mooseError("Reference-interpolation mortar segment outputs must be aligned before appending "
     974             :                  "new segments.");
     975             : 
     976             :     // Keep reference points in the projected polygon's orientation.
     977       41314 :     const Point e1 = primary_nodes[0] - primary_nodes[1];
     978       41314 :     const Point e2 = primary_nodes[2] - primary_nodes[1];
     979       41314 :     const Real orient = e2.cross(e1) * _u.cross(_v);
     980       41314 :     const auto n_verts = primary_nodes.size();
     981             : 
     982       41314 :     primary_poly = projectPrimaryPoly(primary_nodes);
     983       41314 :     primary_poly_reference_points.reserve(reference_mapping->primary_reference_points.size());
     984      176448 :     for (const auto n : index_range(primary_nodes))
     985             :     {
     986      135134 :       const auto primary_node_index = (orient > 0) ? n : n_verts - 1 - n;
     987      135134 :       primary_poly_reference_points.push_back(
     988      135134 :           reference_mapping->primary_reference_points[primary_node_index]);
     989             :     }
     990             :   }
     991             : 
     992             :   // Clip primary elem against secondary elem. Reference mode preserves the projected primary
     993             :   // ordering so its reference points remain aligned.
     994             :   std::vector<Point> clipped_poly =
     995      393422 :       reference_mapping ? clipProjectedPoly(primary_poly) : clipPoly(primary_nodes);
     996      393422 :   if (clipped_poly.size() < 3)
     997      340734 :     return;
     998             : 
     999       52688 :   if (_debug)
    1000           0 :     for (const auto & point : clipped_poly)
    1001           0 :       if (!isInsideSecondary(point))
    1002           0 :         mooseError("Clipped polygon not inside linearized secondary element");
    1003             : 
    1004             :   // Compute area of clipped polygon, update remaining area fraction
    1005       52688 :   _remaining_area_fraction -= area(clipped_poly) / _secondary_area;
    1006             : 
    1007             :   // Triangulate clip polygon. tri_map indices are local to clipped_poly (starting at 0); we
    1008             :   // shift them into the global node numbering after appending the polygon nodes below.
    1009       52688 :   std::vector<std::vector<unsigned int>> tri_map;
    1010       52688 :   triangulatePoly(clipped_poly, tri_map);
    1011       52688 :   if (reference_mapping && reference_mapping->minimum_segment_area > 0.)
    1012        8080 :     tri_map.erase(
    1013        4040 :         std::remove_if(tri_map.begin(),
    1014             :                        tri_map.end(),
    1015       14990 :                        [&clipped_poly, reference_mapping](const std::vector<unsigned int> & tri)
    1016             :                        {
    1017             :                          mooseAssert(tri.size() == 3,
    1018             :                                      "Mortar segment triangulation should only produce TRI3 maps.");
    1019       14990 :                          return triangleAreaHelper(clipped_poly[tri[0]],
    1020       14990 :                                                    clipped_poly[tri[1]],
    1021       14990 :                                                    clipped_poly[tri[2]]) <
    1022       14990 :                                 reference_mapping->minimum_segment_area;
    1023             :                        }),
    1024        8080 :         tri_map.end());
    1025       52688 :   if (tri_map.empty())
    1026         100 :     return;
    1027             : 
    1028       52588 :   std::vector<Point> secondary_node_reference_points;
    1029       52588 :   std::vector<Point> primary_node_reference_points;
    1030       52588 :   if (reference_mapping)
    1031             :   {
    1032        3964 :     secondary_node_reference_points.reserve(clipped_poly.size());
    1033        3964 :     primary_node_reference_points.reserve(clipped_poly.size());
    1034             : 
    1035       37476 :     const auto recover_reference_point = [this](const Point & projected_point,
    1036             :                                                 const std::vector<Point> & poly,
    1037             :                                                 const std::vector<Point> & reference_points,
    1038             :                                                 const char * const parent_name,
    1039             :                                                 const std::size_t node_index)
    1040             :     {
    1041       37476 :       std::string failure_reason;
    1042             :       const auto reference_point =
    1043       37476 :           referencePoint(projected_point, poly, reference_points, &failure_reason);
    1044       37476 :       if (!reference_point)
    1045           0 :         mooseError("Unable to recover the ",
    1046             :                    parent_name,
    1047             :                    " parent reference point for retained 3D mortar overlap vertex ",
    1048             :                    node_index,
    1049             :                    " at projected point ",
    1050             :                    projected_point,
    1051             :                    ". Reason: ",
    1052             :                    failure_reason,
    1053             :                    ". Reference interpolation does not fall back to normal projection.");
    1054             : 
    1055       74952 :       return *reference_point;
    1056       37476 :     };
    1057             : 
    1058       22702 :     for (const auto node_index : index_range(clipped_poly))
    1059             :     {
    1060       18738 :       const auto & point = clipped_poly[node_index];
    1061       18738 :       secondary_node_reference_points.push_back(recover_reference_point(
    1062       18738 :           point, _secondary_poly, _secondary_reference_points, "secondary", node_index));
    1063       18738 :       primary_node_reference_points.push_back(recover_reference_point(
    1064             :           point, primary_poly, primary_poly_reference_points, "primary", node_index));
    1065             :     }
    1066             :   }
    1067             : 
    1068             :   // Transform clipped poly back to (linearized) 3d and append to list
    1069       52588 :   const auto offset = cast_int<unsigned int>(nodes.size());
    1070      282908 :   for (const auto & point : clipped_poly)
    1071      230320 :     nodes.emplace_back((point(0) * _u) + (point(1) * _v) + _center);
    1072             : 
    1073      207830 :   for (const auto & tri : tri_map)
    1074             :   {
    1075      155242 :     std::vector<unsigned int> shifted_tri;
    1076      155242 :     shifted_tri.reserve(tri.size());
    1077      620968 :     for (const auto local_index : tri)
    1078      465726 :       shifted_tri.push_back(offset + local_index);
    1079      155242 :     elem_to_nodes.push_back(std::move(shifted_tri));
    1080             : 
    1081      155242 :     if (reference_mapping)
    1082             :     {
    1083             :       mooseAssert(tri.size() == 3, "Mortar segment triangulation should only produce TRI3 maps.");
    1084       14758 :       std::array<Point, 3> elem_secondary_reference_points;
    1085       14758 :       std::array<Point, 3> elem_primary_reference_points;
    1086       59032 :       for (const auto n : index_range(tri))
    1087             :       {
    1088       44274 :         const auto local_node = tri[n];
    1089       44274 :         elem_secondary_reference_points[n] = secondary_node_reference_points[local_node];
    1090       44274 :         elem_primary_reference_points[n] = primary_node_reference_points[local_node];
    1091             :       }
    1092             : 
    1093       14758 :       reference_mapping->elem_to_secondary_reference_points.push_back(
    1094             :           elem_secondary_reference_points);
    1095       14758 :       reference_mapping->elem_to_primary_reference_points.push_back(elem_primary_reference_points);
    1096             :     }
    1097      155242 :   }
    1098     1075190 : }
    1099             : 
    1100             : std::optional<Point>
    1101       37500 : MortarSegmentHelper::referencePoint(const Point & point,
    1102             :                                     const std::vector<Point> & poly,
    1103             :                                     const std::vector<Point> & reference_points,
    1104             :                                     std::string * const failure_reason) const
    1105             : {
    1106             :   mooseAssert(poly.size() == reference_points.size(),
    1107             :               "Projected point and reference point containers should be the same size.");
    1108             : 
    1109       37500 :   if (failure_reason)
    1110       37500 :     failure_reason->clear();
    1111             : 
    1112          12 :   const auto fail = [failure_reason](const std::string & reason) -> std::optional<Point>
    1113             :   {
    1114          12 :     if (failure_reason)
    1115          12 :       *failure_reason = reason;
    1116          12 :     return std::nullopt;
    1117       37500 :   };
    1118             : 
    1119       37500 :   if (!isFinitePoint(point))
    1120           0 :     return fail("the projected target point contains a non-finite coordinate");
    1121             : 
    1122      165440 :   for (const auto i : index_range(poly))
    1123             :   {
    1124      127940 :     if (!isFinitePoint(poly[i]))
    1125           0 :       return fail("projected polygon vertex " + std::to_string(i) +
    1126           0 :                   " contains a non-finite coordinate");
    1127      127940 :     if (!isFinitePoint(reference_points[i]))
    1128           0 :       return fail("parent reference vertex " + std::to_string(i) +
    1129           0 :                   " contains a non-finite coordinate");
    1130             :   }
    1131             : 
    1132       37500 :   if (poly.size() != 3 && poly.size() != 4)
    1133           0 :     return fail("reference point recovery only supports triangular and quadrilateral mortar "
    1134           0 :                 "sub-elements, but received " +
    1135           0 :                 std::to_string(poly.size()) + " vertices");
    1136             : 
    1137       37500 :   Real minimum_edge_length = std::numeric_limits<Real>::max();
    1138       37500 :   Point local_origin;
    1139      165440 :   for (const auto & vertex : poly)
    1140      127940 :     local_origin += vertex;
    1141       37500 :   local_origin /= poly.size();
    1142             : 
    1143       37500 :   Real local_scale = 0.;
    1144      165440 :   for (const auto i : index_range(poly))
    1145             :   {
    1146      127940 :     minimum_edge_length =
    1147      127940 :         std::min(minimum_edge_length, (poly[(i + 1) % poly.size()] - poly[i]).norm());
    1148      127940 :     local_scale = std::max(local_scale, (poly[i] - local_origin).norm());
    1149             :   }
    1150             : 
    1151       37500 :   const Real singular_tolerance = 100. * std::numeric_limits<Real>::epsilon();
    1152       37500 :   if (!std::isfinite(local_scale) || local_scale <= singular_tolerance)
    1153           0 :     return fail("the projected polygon has a zero local length scale");
    1154       75000 :   if (!std::isfinite(minimum_edge_length) ||
    1155       37500 :       minimum_edge_length / local_scale <= singular_tolerance)
    1156           0 :     return fail("the projected polygon has a zero-length edge relative to its local scale");
    1157             : 
    1158       37500 :   std::vector<Point> normalized_poly;
    1159       37500 :   normalized_poly.reserve(poly.size());
    1160      165440 :   for (const auto & vertex : poly)
    1161      127940 :     normalized_poly.push_back((vertex - local_origin) / local_scale);
    1162       37500 :   const Point normalized_point = (point - local_origin) / local_scale;
    1163             :   const Real reference_tolerance =
    1164       37500 :       std::max(mortar_reference_mapping_tolerance, _area_tol / (minimum_edge_length * local_scale));
    1165       37500 :   std::array<Node, 4> element_nodes;
    1166       37500 :   const FEType fe_type(FIRST, LAGRANGE);
    1167       37500 :   const auto recover_with_libmesh = [&](auto & element) -> std::optional<Point>
    1168             :   {
    1169      165440 :     for (const auto i : index_range(normalized_poly))
    1170             :     {
    1171      127940 :       element_nodes[i] = normalized_poly[i];
    1172      127940 :       element_nodes[i].set_id(i);
    1173      127940 :       element.set_node(i, &element_nodes[i]);
    1174             :     }
    1175             : 
    1176       37500 :     if (!element.has_invertible_map(mortar_reference_mapping_tolerance))
    1177          24 :       return fail("the projected sub-element map is degenerate or non-invertible");
    1178             : 
    1179       37492 :     if (element.type() == QUAD4)
    1180       77162 :       for (const auto corner : make_range(element.n_vertices()))
    1181             :       {
    1182       61730 :         Point tangent_xi;
    1183       61730 :         Point tangent_eta;
    1184      308650 :         for (const auto node : make_range(element.n_nodes()))
    1185             :         {
    1186      493840 :           tangent_xi += FEInterface::shape_deriv(
    1187      246920 :                             fe_type, 0, &element, node, 0, element.master_point(corner)) *
    1188      246920 :                         element.point(node);
    1189      493840 :           tangent_eta += FEInterface::shape_deriv(
    1190      493840 :                              fe_type, 0, &element, node, 1, element.master_point(corner)) *
    1191      246920 :                          element.point(node);
    1192             :         }
    1193             : 
    1194       61730 :         const Real corner_jacobian = tangent_xi.cross(tangent_eta).norm();
    1195       61730 :         if (!std::isfinite(corner_jacobian) ||
    1196             :             corner_jacobian <= mortar_reference_mapping_tolerance)
    1197           6 :           return fail("the projected quadrilateral has a singular or ill-conditioned corner map");
    1198             :       }
    1199             : 
    1200       37490 :     Point local_reference = FEMap::inverse_map(
    1201             :         2, &element, normalized_point, mortar_reference_mapping_tolerance, false, false);
    1202       37490 :     if (!isFinitePoint(local_reference))
    1203           0 :       return fail("libMesh inverse_map produced a non-finite reference point");
    1204             : 
    1205             :     const Real inverse_map_error =
    1206       37490 :         (FEMap::map(2, &element, local_reference) - normalized_point).norm();
    1207       37490 :     if (!std::isfinite(inverse_map_error) || inverse_map_error > mortar_reference_mapping_tolerance)
    1208             :     {
    1209           0 :       std::ostringstream reason;
    1210           0 :       reason << "the normalized inverse-map error " << inverse_map_error << " exceeds "
    1211           0 :              << mortar_reference_mapping_tolerance;
    1212           0 :       return fail(reason.str());
    1213           0 :     }
    1214             : 
    1215       37490 :     if (element.type() == TRI3)
    1216             :     {
    1217             :       std::array<Real, 3> weights;
    1218       88232 :       for (const auto i : index_range(weights))
    1219       66174 :         weights[i] = FEInterface::shape(fe_type, &element, i, local_reference, false);
    1220             : 
    1221       88232 :       for (auto & weight : weights)
    1222       66174 :         weight = std::clamp(weight, 0., 1.);
    1223       22058 :       const Real weight_sum = std::accumulate(weights.begin(), weights.end(), 0.);
    1224       22058 :       if (!std::isfinite(weight_sum) || weight_sum <= singular_tolerance)
    1225           0 :         return fail("clamped triangle barycentric coordinates have a zero or non-finite sum");
    1226       88232 :       for (auto & weight : weights)
    1227       66174 :         weight /= weight_sum;
    1228             : 
    1229             :       // Preserve partition of unity after clamping tolerance-sized violations.
    1230             :       const auto corrected_weight =
    1231       22058 :           std::distance(weights.begin(), std::max_element(weights.begin(), weights.end()));
    1232       22058 :       weights[corrected_weight] = 1.;
    1233       88232 :       for (const auto i : index_range(weights))
    1234       66174 :         if (i != static_cast<unsigned int>(corrected_weight))
    1235       44116 :           weights[corrected_weight] -= weights[i];
    1236             : 
    1237       22058 :       local_reference = Point();
    1238       88232 :       for (const auto i : index_range(weights))
    1239       66174 :         local_reference += weights[i] * element.master_point(i);
    1240             :     }
    1241             :     else
    1242             :     {
    1243       15432 :       local_reference(0) = std::clamp(local_reference(0), -1., 1.);
    1244       15432 :       local_reference(1) = std::clamp(local_reference(1), -1., 1.);
    1245       15432 :       local_reference(2) = 0.;
    1246             :     }
    1247             : 
    1248       37490 :     if (!element.on_reference_element(local_reference, mortar_reference_mapping_tolerance))
    1249           0 :       return fail("the clamped inverse-map result is outside the reference element");
    1250             : 
    1251             :     const Real round_trip_error =
    1252       37490 :         (FEMap::map(2, &element, local_reference) - normalized_point).norm();
    1253       37490 :     if (!std::isfinite(round_trip_error) || round_trip_error > reference_tolerance)
    1254             :     {
    1255           2 :       std::ostringstream reason;
    1256           2 :       reason << "the normalized inverse-map round-trip error " << round_trip_error
    1257           2 :              << " exceeds the clipping-consistent tolerance " << reference_tolerance;
    1258           2 :       return fail(reason.str());
    1259           2 :     }
    1260             : 
    1261       37488 :     Point parent_reference;
    1262      165384 :     for (const auto i : index_range(normalized_poly))
    1263      127896 :       parent_reference +=
    1264      255792 :           FEInterface::shape(fe_type, &element, i, local_reference, false) * reference_points[i];
    1265             : 
    1266       37488 :     if (!isFinitePoint(parent_reference))
    1267           0 :       return fail("reference interpolation produced a non-finite parent reference point");
    1268             : 
    1269       37488 :     return parent_reference;
    1270       37500 :   };
    1271             : 
    1272       37500 :   if (poly.size() == 3)
    1273             :   {
    1274       22060 :     Tri3 element;
    1275       22060 :     return recover_with_libmesh(element);
    1276       22060 :   }
    1277             : 
    1278       15440 :   Quad4 element;
    1279       15440 :   return recover_with_libmesh(element);
    1280       37500 : }
    1281             : 
    1282             : Real
    1283       92345 : MortarSegmentHelper::area(const std::vector<Point> & nodes) const
    1284             : {
    1285       92345 :   Real poly_area = 0;
    1286      444537 :   for (auto i : index_range(nodes))
    1287      352192 :     poly_area += nodes[i](0) * nodes[(i + 1) % nodes.size()](1) -
    1288      352192 :                  nodes[i](1) * nodes[(i + 1) % nodes.size()](0);
    1289       92345 :   poly_area *= 0.5;
    1290       92345 :   return poly_area;
    1291             : }

Generated by: LCOV version 1.14