LCOV - code coverage report
Current view: top level - src/meshgenerators - SurfaceSubdomainsDelaunayRemesher.C (source / functions) Hit Total Coverage
Test: idaholab/moose framework: #32971 (54bef8) with base c6cf66 Lines: 200 224 89.3 %
Date: 2026-05-29 20:35:17 Functions: 4 4 100.0 %
Legend: Lines: hit not hit

          Line data    Source code
       1             : //* This file is part of the MOOSE framework
       2             : //* https://www.mooseframework.org
       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             : 
      10             : #include "SurfaceSubdomainsDelaunayRemesher.h"
      11             : 
      12             : #include "MooseMeshUtils.h"
      13             : #include "CastUniquePointer.h"
      14             : 
      15             : #include "libmesh/boundary_info.h"
      16             : #include "libmesh/mesh_triangle_holes.h"
      17             : #include "libmesh/mesh_modification.h"
      18             : #include "libmesh/parallel_algebra.h"
      19             : #include "libmesh/poly2tri_triangulator.h"
      20             : #include "libmesh/unstructured_mesh.h"
      21             : 
      22             : #include "libmesh/mesh_base.h"
      23             : #include "libmesh/replicated_mesh.h"
      24             : 
      25             : registerMooseObject("MooseApp", SurfaceSubdomainsDelaunayRemesher);
      26             : 
      27             : InputParameters
      28        3125 : SurfaceSubdomainsDelaunayRemesher::validParams()
      29             : {
      30        3125 :   InputParameters params = SurfaceDelaunayGeneratorBase::validParams();
      31        3125 :   params += LevelSetMeshingHelper::validParams();
      32       12500 :   params.renameParameterGroup("Parsed expression advanced", "Level set shape parsed expression");
      33             : 
      34        6250 :   params.addClassDescription(
      35             :       "Mesh generator that re-meshes a 2D surface mesh given as one or more subdomains into "
      36             :       "a 2D surface mesh using Delaunay triangulation.");
      37             : 
      38             :   // Definition of the region to re-mesh
      39       12500 :   params.addRequiredParam<MeshGeneratorName>("input", "The mesh we want to modify");
      40       12500 :   params.addParam<std::vector<std::vector<SubdomainName>>>(
      41             :       "subdomain_names", {}, "The surface mesh subdomains to be re-meshed");
      42       12500 :   params.addParam<std::vector<SubdomainName>>(
      43             :       "exclude_subdomain_names", {}, "The surface mesh subdomains that should not be re-meshed");
      44             : 
      45             :   // Surface re-meshing parameters
      46       18750 :   params.addRangeCheckedParam<Real>(
      47             :       "max_edge_length",
      48             :       "max_edge_length>0",
      49             :       "Maximum length of an edge in the 1D meshes around each subdomain. Only a single value is "
      50             :       "currently allowed as the boundary refinement must be consistent between all subdomains.");
      51             :   // NOTE: we could make this a per-included-subdomain parameter, we would just need to identify
      52             :   // interfaces between each subdomain and use the minimum (or average or maximum, just needs to
      53             :   // be consistent) edge length on those interfaces.
      54       12500 :   params.addParam<std::vector<unsigned int>>(
      55             :       "interpolate_boundaries",
      56             :       {1},
      57             :       "Ratio of points to add to the boundaries. Can be set to a single value for all groups at "
      58             :       "once, or specified individually. A single value is recommended as the boundary refinement "
      59             :       "must be consistent between all subdomains.");
      60       12500 :   params.addParam<std::vector<Real>>(
      61             :       "desired_areas",
      62             :       {0},
      63             :       "Target element size when triangulating projection of the subdomain group. Can be set to a "
      64             :       "single value for all groups at once, or specified individually. Default of 0 means no "
      65             :       "constraint.");
      66       12500 :   params.addParamNamesToGroup("max_edge_length interpolate_boundaries desired_areas",
      67             :                               "Delaunay triangulation");
      68             : 
      69             :   // Parameters for stitching meshes at the end
      70        9375 :   params.addParam<bool>("avoid_merging_subdomains",
      71        6250 :                         false,
      72             :                         "Whether to prevent merging subdomains by offsetting ids. The first mesh "
      73             :                         "in the input will keep the same subdomains ids, the others will have "
      74             :                         "offsets. All subdomain names will remain valid");
      75        9375 :   params.addParam<bool>("clear_stitching_boundaries",
      76        6250 :                         true,
      77             :                         "Whether to clear the boundaries between the subdomains being stitched "
      78             :                         "together after they were re-meshed with triangles");
      79       12500 :   MooseEnum algorithm("BINARY EXHAUSTIVE", "BINARY");
      80       12500 :   params.addParam<MooseEnum>(
      81             :       "stitching_algorithm",
      82             :       algorithm,
      83             :       "Control the use of binary search for the nodes of the stitched surfaces.");
      84        9375 :   params.addParam<bool>(
      85        6250 :       "verbose_stitching", false, "Whether mesh stitching should have verbose output.");
      86        9375 :   params.addParamNamesToGroup(
      87             :       "avoid_merging_subdomains clear_stitching_boundaries stitching_algorithm verbose_stitching",
      88             :       "Stitching triangularized meshes");
      89             : 
      90        6250 :   return params;
      91        3125 : }
      92             : 
      93          32 : SurfaceSubdomainsDelaunayRemesher::SurfaceSubdomainsDelaunayRemesher(
      94          32 :     const InputParameters & parameters)
      95             :   : SurfaceDelaunayGeneratorBase(parameters),
      96             :     LevelSetMeshingHelper(parameters),
      97          32 :     _input(getMesh("input")),
      98          64 :     _subdomain_names(getParam<std::vector<std::vector<SubdomainName>>>("subdomain_names")),
      99          64 :     _interpolate_boundaries(getParam<std::vector<unsigned int>>("interpolate_boundaries")),
     100         128 :     _desired_areas(getParam<std::vector<Real>>("desired_areas"))
     101             : {
     102         112 :   if (isParamSetByUser("subdomain_names") && isParamSetByUser("exclude_subdomain_names"))
     103           0 :     paramError("exclude_subdomain_names",
     104             :                "Excluding subdomain names is only to be set when 'subdomain_names' is not set");
     105          32 : }
     106             : 
     107             : std::unique_ptr<MeshBase>
     108          32 : SurfaceSubdomainsDelaunayRemesher::generate()
     109             : {
     110          32 :   std::unique_ptr<MeshBase> mesh_3d = std::move(_input);
     111             : 
     112             :   // Select subdomain names if it has not been selected by the user
     113             :   // We form 1 group per subdomain by default (easiest to mesh)
     114          32 :   if (_subdomain_names.empty() || _subdomain_names[0].empty())
     115             :   {
     116          24 :     if (_subdomain_names.size())
     117           0 :       _subdomain_names.erase(_subdomain_names.begin());
     118             : 
     119             :     // Get all subdomains from the mesh
     120          24 :     std::set<subdomain_id_type> sub_ids;
     121          24 :     mesh_3d->subdomain_ids(sub_ids);
     122             : 
     123             :     // Copy only the ones that are not excluded
     124          48 :     const auto & excluded = getParam<std::vector<SubdomainName>>("exclude_subdomain_names");
     125         288 :     for (const auto & sub_id : sub_ids)
     126             :     {
     127         264 :       const auto & sn = mesh_3d->subdomain_name(sub_id) == "" ? std::to_string(sub_id)
     128         264 :                                                               : mesh_3d->subdomain_name(sub_id);
     129         264 :       if (excluded.empty() || std::find(excluded.begin(), excluded.end(), sn) == excluded.end())
     130         768 :         _subdomain_names.push_back({sn});
     131         264 :     }
     132          24 :   }
     133          32 :   _num_groups = _subdomain_names.size();
     134          32 :   if (_interpolate_boundaries.size() == 1)
     135          32 :     _interpolate_boundaries.resize(_num_groups, _interpolate_boundaries[0]);
     136          32 :   if (_desired_areas.size() == 1)
     137          32 :     _desired_areas.resize(_num_groups, _desired_areas[0]);
     138          32 :   if (_interpolate_boundaries.size() != _num_groups)
     139           0 :     paramError("interpolate_boundaries",
     140             :                "Should be the same size as 'subdomain_names' or of size 1");
     141          32 :   if (_desired_areas.size() != _num_groups)
     142           0 :     paramError("desired_areas", "Should be the same size as 'subdomain_names' or of size 1");
     143             : 
     144             :   // Re-create surface meshes from each of the subdomains
     145          32 :   std::vector<std::unique_ptr<UnstructuredMesh>> remeshed_2d;
     146             : 
     147         304 :   for (const auto i : make_range(_num_groups))
     148             :   {
     149         272 :     auto mesh_2d = buildReplicatedMesh();
     150         272 :     MooseMeshUtils::convertBlockToMesh(*mesh_3d, *mesh_2d, _subdomain_names[i]);
     151             : 
     152             :     // Mesh the subdomains by groups
     153             :     // TODO: holes for each subdomain are not currently supported
     154         272 :     std::vector<std::unique_ptr<UnstructuredMesh>> no_holes = {};
     155         272 :     auto new_mesh = General2DDelaunay(*mesh_2d, /*holes*/ no_holes, i);
     156             : 
     157             :     // Keep the remeshed subdomains 2D meshes
     158         272 :     remeshed_2d.push_back(std::move(new_mesh));
     159         272 :   }
     160             : 
     161             :   // TODO: can we gain efficiency by using different boundary ids for each stitch?
     162          32 :   const auto primary_bcid = 1;
     163             :   // Prevent deleting a legitimate ID if we start conserving boundary IDs from the input mesh
     164             :   // to the output mesh
     165          32 :   const auto starting_stitching_id = MooseMeshUtils::getNextFreeBoundaryID(*mesh_3d);
     166          32 :   auto paired_bcid = starting_stitching_id;
     167          64 :   const auto verbose_stitching = getParam<bool>("verbose_stitching");
     168          64 :   const auto use_binary_search = getParam<MooseEnum>("stitching_algorithm") == "BINARY";
     169             : 
     170             :   // Stitch all the parts to the first one
     171          32 :   std::unique_ptr<UnstructuredMesh> full_mesh;
     172          32 :   full_mesh = std::move(remeshed_2d[0]);
     173             : 
     174             :   // Build a subdomain map
     175          32 :   auto & main_subdomain_map = full_mesh->set_subdomain_name_map();
     176             : 
     177         304 :   for (auto remesh_i : index_range(remeshed_2d))
     178             :   {
     179         272 :     if (remesh_i == 0)
     180          32 :       continue;
     181             : 
     182         240 :     UnstructuredMesh & remeshed = dynamic_cast<UnstructuredMesh &>(*remeshed_2d[remesh_i]);
     183             : 
     184             :     // NOTE: we cannot use MeshedHole because the meshes are no longer in the XY plane
     185             :     // Potential optimization: instead of using the full mesh outer boundary for stitching
     186             :     // use what is done in the XYDelaunayGenerator and re-build only the boundary near the
     187             :     // mesh that we are stitching
     188             :     // Potential optimization: we could keep the external boundaries from the triangulation phase
     189             : 
     190             :     // Create the boundary outside the remeshed mesh
     191         240 :     bool rem_has_external_bdy = false;
     192         240 :     MooseMeshUtils::addExternalBoundary(remeshed, paired_bcid, rem_has_external_bdy);
     193             :     mooseAssert(rem_has_external_bdy, "Subdomain mesh should have an external boundary");
     194             : 
     195             :     // We need to re-create the boundary outside the parent mesh because we are clearing
     196             :     // it on every stitch
     197         240 :     bool base_has_external_bdy = false;
     198         240 :     MooseMeshUtils::addExternalBoundary(*full_mesh, primary_bcid, base_has_external_bdy);
     199         240 :     if (!base_has_external_bdy)
     200           0 :       mooseWarning(
     201             :           "Full mesh should have an external boundary. This could occur if the surface mesh is "
     202             :           "disconnected in several parts and a part's surface mesh has just been fully remeshed");
     203             : 
     204             :     // Retrieve subdomain name map from the mesh to be stitched and insert it into the main
     205             :     // subdomain map
     206         240 :     const auto & increment_subdomain_map = remeshed.get_subdomain_name_map();
     207         240 :     main_subdomain_map.insert(increment_subdomain_map.begin(), increment_subdomain_map.end());
     208             : 
     209         240 :     full_mesh->stitch_meshes(remeshed,
     210             :                              primary_bcid,
     211             :                              paired_bcid,
     212             :                              TOLERANCE,
     213         240 :                              /*clear_stitched_bcids*/ getParam<bool>("clear_stitching_boundaries"),
     214             :                              verbose_stitching,
     215             :                              use_binary_search,
     216             :                              /*enforce_all_nodes_match_on_boundaries*/ false,
     217             :                              /*merge_boundary_nodes_all_or_nothing*/ false,
     218         720 :                              /*remap_subdomain_ids*/ !getParam<bool>("avoid_merging_subdomains"));
     219             : 
     220             :     // If we want to keep track of the stitching boundaries
     221         240 :     paired_bcid++;
     222             :     // TODO: when implementing hole meshes, we might want to also stitch the hole meshes
     223             :   }
     224             : 
     225             :   // We do not need the 3D mesh anymore
     226          32 :   mesh_3d->clear();
     227             :   // The stitching boundary should be removed by the stitcher, but this does not hurt
     228          96 :   if (getParam<bool>("clear_stitching_boundaries"))
     229         272 :     for (const auto bcid : make_range(starting_stitching_id, paired_bcid))
     230         240 :       full_mesh->get_boundary_info().remove_id(bcid);
     231             :   // Mesh is not prepared after stitching (TODO: fix this)
     232          32 :   full_mesh->unset_is_prepared();
     233             : 
     234          64 :   return full_mesh;
     235         288 : }
     236             : 
     237             : std::unique_ptr<UnstructuredMesh>
     238         272 : SurfaceSubdomainsDelaunayRemesher::General2DDelaunay(
     239             :     UnstructuredMesh & mesh_2d,
     240             :     std::vector<std::unique_ptr<UnstructuredMesh>> & hole_meshes_2d,
     241             :     unsigned int group_i)
     242             : {
     243         272 :   if (_verbose)
     244             :   {
     245         272 :     if (!mesh_2d.preparation().has_cached_elem_data)
     246         272 :       mesh_2d.cache_elem_data();
     247         272 :     _console << "Re-meshing mesh\n " << mesh_2d << std::endl;
     248        1360 :     _console << "with subdomains " << Moose::stringify(mesh_2d.get_subdomain_name_map())
     249         272 :              << std::endl;
     250         272 :     if (hole_meshes_2d.size())
     251           0 :       _console << "With " << hole_meshes_2d.size() << " holes:" << std::endl;
     252         272 :     for (const auto & hole_m : hole_meshes_2d)
     253             :     {
     254           0 :       if (!hole_m->preparation().has_cached_elem_data)
     255           0 :         hole_m->cache_elem_data();
     256           0 :       _console << "Hole subdomains " << Moose::stringify(hole_m->get_subdomain_name_map())
     257           0 :                << std::endl;
     258           0 :       _console << *hole_m << std::endl;
     259             :     }
     260             :   }
     261             :   // If a level set is provided, we need to check if the nodes in the original 2D mesh match the
     262             :   // level set
     263         272 :   if (_func_level_set)
     264             :   {
     265        1872 :     for (const auto & node : mesh_2d.node_ptr_range())
     266             :     {
     267        1616 :       if (std::abs(levelSetEvaluator(*node)) > libMesh::TOLERANCE)
     268             :       {
     269           0 :         paramError("level_set",
     270             :                    "The level set function does not match the nodes in the given boundary of the "
     271           0 :                    "input mesh. Level set evaluates at: " +
     272           0 :                        std::to_string(levelSetEvaluator(*node)) + " at node: " + node->get_info());
     273             :       }
     274         256 :     }
     275             :   }
     276             : 
     277             :   // Create the external boundary of the 2D mesh
     278             :   // Easier to work with a TRI3 mesh
     279             :   // all_tri() also prepares the mesh for use
     280         272 :   mesh_2d.prepare_for_use();
     281         272 :   bool has_external_side = false;
     282         272 :   MeshTools::Modification::all_tri(mesh_2d);
     283         272 :   const auto mesh_2d_ext_bdry = MooseMeshUtils::getNextFreeBoundaryID(mesh_2d);
     284        1456 :   for (const auto elem : mesh_2d.active_element_ptr_range())
     285        4736 :     for (const auto i_side : elem->side_index_range())
     286        3552 :       if (elem->neighbor_ptr(i_side) == nullptr)
     287             :       {
     288        1728 :         mesh_2d.get_boundary_info().add_side(elem, i_side, mesh_2d_ext_bdry);
     289        1728 :         has_external_side = true;
     290         272 :       }
     291             : 
     292             :   // Create a clone of the 2D mesh to be used for the 1D mesh generation
     293         272 :   auto mesh_2d_dummy = mesh_2d.clone();
     294             :   // Generate a new 1D block based on the external boundary
     295         272 :   const auto new_block_id_1d = MooseMeshUtils::getNextFreeSubdomainID(*mesh_2d_dummy);
     296             : 
     297         272 :   if (has_external_side)
     298        1360 :     MooseMeshUtils::createSubdomainFromSidesets(*mesh_2d_dummy,
     299         272 :                                                 {std::to_string(mesh_2d_ext_bdry)},
     300             :                                                 new_block_id_1d,
     301         544 :                                                 SubdomainName(),
     302         272 :                                                 type());
     303             : 
     304             :   // Create a 1D mesh from the 1D block
     305         272 :   auto mesh_1d = buildMeshBaseObject();
     306         272 :   if (has_external_side)
     307         816 :     MooseMeshUtils::convertBlockToMesh(*mesh_2d_dummy, *mesh_1d, {std::to_string(new_block_id_1d)});
     308         272 :   mesh_2d_dummy->clear();
     309             : 
     310             :   // Add more nodes in the 1D mesh
     311             :   // We need to do this BEFORE the mesh is projected to avoid distortion in the node distances
     312             :   // The distortion is not the same for each subdomain, so we would end up with a non-conformal mesh
     313         816 :   if (isParamValid("max_edge_length"))
     314             :   {
     315             :     // We need to extract the points in the order of the boundary. This utility will build the
     316             :     // boundary as a loop
     317             :     // Add the nodeset from the sideset
     318             :     // NOTE: this is redoing the 1D mesh from the boundary
     319         176 :     mesh_2d.get_boundary_info().build_node_list_from_side_list({mesh_2d_ext_bdry});
     320          88 :     const auto boundary_1d = MooseMeshUtils::buildLoopBoundaryOf2DMesh(mesh_2d, mesh_2d_ext_bdry);
     321             :     // Extract the points
     322          88 :     std::vector<Point> mesh_1d_points;
     323          88 :     mesh_1d_points.reserve(boundary_1d->n_nodes());
     324         648 :     for (const auto & node : boundary_1d->node_ptr_range())
     325         648 :       mesh_1d_points.push_back(*node);
     326             : 
     327             :     // Re-create the 1D mesh with a polyline with a maximum edge length
     328          88 :     mesh_1d->clear();
     329             :     // we add a tiny offset to avoid roundoff differences
     330         176 :     MooseMeshUtils::buildPolyLineMesh(
     331         352 :         *mesh_1d, mesh_1d_points, true, "", "", getParam<Real>("max_edge_length") + 1e-8);
     332          88 :   }
     333             : 
     334             :   // Find centroid of the 2D mesh
     335         272 :   const Point centroid = MooseMeshUtils::meshCentroidCalculator(mesh_2d);
     336             :   // calculate an average normal vector of the 2D mesh
     337         272 :   const Point mesh_norm = meshNormal2D(mesh_2d);
     338             :   // Check the deviation of the mesh normal vector from the global average normal vector
     339         272 :   if (meshNormalDeviation2D(mesh_2d, mesh_norm) > _max_angle_deviation)
     340           0 :     paramError("subdomain_names",
     341             :                "The normal vector of some elements in the 2D mesh deviates too much from the "
     342           0 :                "global average normal vector. The maximum deviation found / allowed is " +
     343           0 :                    std::to_string(meshNormalDeviation2D(mesh_2d, mesh_norm)) + " / " +
     344           0 :                    std::to_string(_max_angle_deviation) +
     345             :                    ". Consider dividing the boundary into several parts to "
     346             :                    "reduce the angle deviation.");
     347             : 
     348             :   // Move both 2d and 1d meshes to the centroid of the 2D mesh
     349         272 :   MeshTools::Modification::translate(*mesh_1d, -centroid(0), -centroid(1), -centroid(2));
     350         272 :   MeshTools::Modification::translate(mesh_2d, -centroid(0), -centroid(1), -centroid(2));
     351             : 
     352             :   // Calculate the Euler angles to rotate the meshes so that the 2D mesh is close to the XY plane
     353             :   // (i.e., the normal vector of the 2D mesh is aligned with the Z axis)
     354         272 :   const Real theta = std::acos(mesh_norm(2)) / M_PI * 180.0;
     355             :   const Real phi =
     356         272 :       (MooseUtils::absoluteFuzzyLessThan(mesh_norm(2), 1.0) ? std::atan2(mesh_norm(1), mesh_norm(0))
     357         272 :                                                             : 0.0) /
     358         272 :       M_PI * 180.0;
     359         272 :   MeshTools::Modification::rotate(*mesh_1d, 90.0 - phi, theta, 0.0);
     360         272 :   MeshTools::Modification::rotate(mesh_2d, 90.0 - phi, theta, 0.0);
     361             : 
     362             :   // Clone the 2D mesh to be used for reverse projection later
     363         272 :   auto mesh_2d_xyz = dynamic_pointer_cast<MeshBase>(mesh_2d.clone());
     364             : 
     365             :   // Project the 2D mesh to the XY plane so that XYDelaunay can be used
     366        2000 :   for (const auto & node : mesh_2d.node_ptr_range())
     367        2000 :     (*node)(2) = 0;
     368             :   // Project the 1D mesh to the XY plane as well
     369        2560 :   for (const auto & node : mesh_1d->node_ptr_range())
     370        2560 :     (*node)(2) = 0;
     371             : 
     372             :   // Finally, triangulation
     373             :   std::unique_ptr<UnstructuredMesh> mesh =
     374         272 :       dynamic_pointer_cast<UnstructuredMesh>(std::move(mesh_1d));
     375             : 
     376         272 :   Poly2TriTriangulator poly2tri(*mesh);
     377         272 :   poly2tri.triangulation_type() = TriangulatorInterface::PSLG;
     378             : 
     379         272 :   poly2tri.set_interpolate_boundary_points(_interpolate_boundaries[group_i]);
     380         272 :   poly2tri.set_verify_hole_boundaries(false);
     381         272 :   poly2tri.desired_area() = _desired_areas[group_i];
     382         272 :   poly2tri.minimum_angle() = 0; // Not yet supported
     383         272 :   poly2tri.smooth_after_generating() = false;
     384             :   // Future TODO: correct the area function based on the local normal vector
     385         272 :   if (_use_auto_area_func)
     386           0 :     poly2tri.set_auto_area_function(this->comm(),
     387           0 :                                     _auto_area_function_num_points,
     388           0 :                                     _auto_area_function_power,
     389           0 :                                     _auto_area_func_default_size,
     390           0 :                                     _auto_area_func_default_size_dist);
     391         272 :   poly2tri.triangulate();
     392             : 
     393             :   // Reverse the projection based on the original 2D mesh
     394        5800 :   for (const auto & node : mesh->node_ptr_range())
     395             :   {
     396        5528 :     bool node_mod = false;
     397             :     // Try to find the element in mesh_2d that contains the new node
     398       14944 :     for (const auto & elem : mesh_2d.active_element_ptr_range())
     399             :     {
     400       14944 :       if (elem->contains_point(Point((*node)(0), (*node)(1), 0.0)))
     401             :       {
     402             :         // Element id
     403        5528 :         const auto elem_id = elem->id();
     404             :         // element in xyz_in_xyz
     405        5528 :         const Elem & elem_xyz = *mesh_2d_xyz->elem_ptr(elem_id);
     406             : 
     407        5528 :         const Point elem_normal = elemNormal(elem_xyz);
     408        5528 :         const auto & elem_p = mesh_2d_xyz->elem_ptr(elem_id)->point(0);
     409             : 
     410             :         // if the x and y values of the node is the same as the elem_p's first node, we can just
     411             :         // move it to that node's position
     412        5912 :         if (MooseUtils::absoluteFuzzyEqual((*node)(0), elem_p(0)) &&
     413         384 :             MooseUtils::absoluteFuzzyEqual((*node)(1), elem_p(1)))
     414             :         {
     415         296 :           (*node)(2) = elem_p(2);
     416         296 :           node_mod = true;
     417         296 :           break;
     418             :         }
     419             :         // Otherwise, we need to find a position inside the 2D element
     420             :         // It has the same x and y coordinates as the node in the projected mesh;
     421        5232 :         (*node)(2) = elem_p(2) - (((*node)(0) - elem_p(0)) * elem_normal(0) +
     422        5232 :                                   ((*node)(1) - elem_p(1)) * elem_normal(1)) /
     423        5232 :                                      elem_normal(2);
     424        5232 :         node_mod = true;
     425        5232 :         break;
     426             :       }
     427        5528 :     }
     428        5528 :     if (!node_mod)
     429           0 :       mooseError("Node not found in mesh_in_xy");
     430         272 :   }
     431             : 
     432             :   // Rotate the mesh back
     433         272 :   MeshTools::Modification::rotate(*mesh, 0.0, -theta, phi - 90.0);
     434             :   // Translate the mesh back
     435         272 :   MeshTools::Modification::translate(*mesh, centroid(0), centroid(1), centroid(2));
     436             : 
     437             :   // Correct the nodes based on the level set function
     438         272 :   if (_func_level_set)
     439             :   {
     440        5560 :     for (const auto & node : mesh->node_ptr_range())
     441             :     {
     442        5304 :       unsigned int iter_ct = 0;
     443       29048 :       while (iter_ct < _max_level_set_correction_iterations &&
     444       12680 :              std::abs(levelSetEvaluator(*node)) > libMesh::TOLERANCE * libMesh::TOLERANCE)
     445             :       {
     446       11064 :         levelSetCorrection(*node);
     447       11064 :         ++iter_ct;
     448             :       }
     449         256 :     }
     450             :   }
     451             :   // Give the old subdomain to all elements
     452         272 :   const auto common_id = mesh_2d.get_mesh_subdomains().begin();
     453        5648 :   for (auto & elem : mesh->active_element_ptr_range())
     454        5648 :     elem->subdomain_id() = *common_id;
     455             : 
     456             :   // Pass the subdomain names
     457         272 :   mesh->set_subdomain_name_map() = mesh_2d.get_subdomain_name_map();
     458             :   // Remove the boundaries, they get re-added (with a known ID) when stitching the pieces together
     459         272 :   mesh->get_boundary_info().remove_id(mesh_2d_ext_bdry);
     460             :   // Elements have changed, neighbors, ids etc
     461         272 :   mesh->unset_is_prepared();
     462             : 
     463         544 :   return mesh;
     464         816 : }

Generated by: LCOV version 1.14