-
Notifications
You must be signed in to change notification settings - Fork 69
Feature: Feature mesh deformation #2196
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
Lena-Richter
wants to merge
27
commits into
main
Choose a base branch
from
feature-mesh_deformation
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
27 commits
Select commit
Hold shift + click to select a range
52dbb3d
Implement mesh deformation: add new file and adjust related code
Lena-Richter afee13d
implement sc_options for command line
Lena-Richter 04959e2
fix doxygen warnings
Lena-Richter 820f1e7
fix doxygen warnings again
Lena-Richter bd3f549
add doxygen file header for mesh deformation
Lena-Richter 2d3af8f
Merge branch 'main' into feature-mesh_deformation
sandro-elsweijer fa8e214
add missing documentation for cad manager and iterators
Lena-Richter 59a421e
Merge branch 'main' into feature-mesh_deformation
Lena-Richter f0b2182
add T8_ENABLE_OCC guards to t8_cad class
Lena-Richter f13f6fb
chore: restart CI tests
Lena-Richter 6a8cafa
compile mesh deformation only if OCC is enabled
Lena-Richter 1749a48
merge main and resolve conflicts
Lena-Richter 7214e89
fix doxygen param name in t8_geometry_cad
Lena-Richter 8105450
Merge branch 'main' into feature-mesh_deformation
Lena-Richter 8b535bb
Merge branch 'main' into feature-mesh_deformation
Lena-Richter c11a6cc
move deformation logic to member functions
Lena-Richter 3f4fe39
fix gp_Pnt.hxx error
Lena-Richter b20b36a
fix gp_Pnt.hxx error for example
Lena-Richter 53a09de
fix MPI error for example
Lena-Richter c99f5b5
Merge branch 'main' into feature-mesh_deformation
Lena-Richter b82fa7e
address review comments for mesh deformation and clean up
Lena-Richter 504e60d
clean up mesh deformation example
Lena-Richter 78fd6b2
wrap t8_mesm_mesh_deformation.hxx include with OCC guards in the emsh…
Lena-Richter 14a07c6
address review and add comments
Lena-Richter 374ecd6
Merge branch 'main' into feature-mesh_deformation
Lena-Richter 83d421f
Merge branch 'main' into feature-mesh_deformation
Lena-Richter c2175b3
address comments
Lena-Richter File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,156 @@ | ||||||
| /* | ||||||
| This file is part of t8code. | ||||||
| t8code is a C library to manage a collection (a forest) of multiple | ||||||
| connected adaptive space-trees of general element classes in parallel. | ||||||
|
|
||||||
| Copyright (C) 2026 the developers | ||||||
|
|
||||||
| t8code is free software; you can redistribute it and/or modify | ||||||
| it under the terms of the GNU General Public License as published by | ||||||
| the Free Software Foundation; either version 2 of the License, or | ||||||
| (at your option) any later version. | ||||||
|
|
||||||
| t8code is distributed in the hope that it will be useful, | ||||||
| but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||||
| MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||||||
| GNU General Public License for more details. | ||||||
|
|
||||||
| You should have received a copy of the GNU General Public License | ||||||
| along with t8code; if not, write to the Free Software Foundation, Inc., | ||||||
| 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | ||||||
| */ | ||||||
|
|
||||||
| /** \file t8_cmesh_mesh_deformation.cxx | ||||||
| * This file implements an example for CAD-based mesh deformation. | ||||||
| */ | ||||||
|
|
||||||
| #include <t8_cmesh/t8_cmesh.hxx> | ||||||
| #include <t8_cmesh/t8_cmesh_vertex_connectivity/t8_cmesh_vertex_connectivity.hxx> | ||||||
| #include <t8_cmesh/t8_cmesh_io/t8_cmesh_readmshfile.h> | ||||||
| #include <t8_schemes/t8_default/t8_default.hxx> | ||||||
| #if T8CODE_ENABLE_OCC | ||||||
| #include <t8_cad/t8_cad_handle.hxx> | ||||||
| #include <t8_cmesh/t8_cmesh_mesh_deformation/t8_cmesh_mesh_deformation.hxx> | ||||||
| #endif /* T8CODE_ENABLE_OCC */ | ||||||
| #include <t8_vtk/t8_vtk_writer.h> | ||||||
| #include <sc_options.h> | ||||||
|
|
||||||
| #include <iostream> | ||||||
| #include <string> | ||||||
| #include <vector> | ||||||
| #include <array> | ||||||
|
|
||||||
| int | ||||||
| main ([[maybe_unused]] int argc, [[maybe_unused]] char **argv) | ||||||
| { | ||||||
| #if T8CODE_ENABLE_OCC | ||||||
|
|
||||||
| char usage[BUFSIZ]; | ||||||
| /* Brief help message. */ | ||||||
| int sreturnA = snprintf (usage, BUFSIZ, "Usage:\t%s <OPTIONS>\n\t%s -h\t for a brief overview of all options.", | ||||||
| basename (argv[0]), basename (argv[0])); | ||||||
|
|
||||||
| char help[BUFSIZ]; | ||||||
| /* Long help message. */ | ||||||
| int sreturnB = snprintf ( | ||||||
| help, BUFSIZ, | ||||||
| "Deform a mesh based on a msh file with the new CAD geometry.\n" | ||||||
| "Required arguments are the input mesh file, the deformation geometry file, and the mesh dimension.\n\n%s\n", | ||||||
| usage); | ||||||
|
|
||||||
| if (sreturnA > BUFSIZ || sreturnB > BUFSIZ) { | ||||||
| /* The usage string or help message was truncated */ | ||||||
| /* Note: gcc >= 7.1 prints a warning if we | ||||||
| * do not check the return value of snprintf. */ | ||||||
| t8_debugf ("WARNING: Truncated usage string and help message to '%s' and '%s'\n", usage, help); | ||||||
| } | ||||||
|
|
||||||
| /* | ||||||
| * Initialization. | ||||||
| */ | ||||||
|
|
||||||
| /* Initialize MPI. This has to happen before we initialize sc or t8code. */ | ||||||
| int mpiret = sc_MPI_Init (&argc, &argv); | ||||||
|
|
||||||
| /* Error check the MPI return value. */ | ||||||
| SC_CHECK_MPI (mpiret); | ||||||
|
|
||||||
| /* Initialize the sc library, has to happen before we initialize t8code. */ | ||||||
| sc_init (sc_MPI_COMM_WORLD, 1, 1, NULL, SC_LP_PRODUCTION); | ||||||
|
|
||||||
| /* Initialize t8code with log level SC_LP_PRODUCTION. See sc.h for more info on the log levels. */ | ||||||
| t8_init (SC_LP_PRODUCTION); | ||||||
|
|
||||||
| int helpme = 0; | ||||||
| const char *msh_file = NULL; | ||||||
| const char *brep_file = NULL; | ||||||
|
sandro-elsweijer marked this conversation as resolved.
|
||||||
| int dim, level; | ||||||
|
|
||||||
| /* Initialize command line argument parser. */ | ||||||
| sc_options_t *opt = sc_options_new (argv[0]); | ||||||
| sc_options_add_switch (opt, 'h', "help", &helpme, "Display a short help message."); | ||||||
|
sandro-elsweijer marked this conversation as resolved.
|
||||||
| sc_options_add_string (opt, 'm', "mshfile", &msh_file, NULL, "File prefix of the input mesh file (without .msh)"); | ||||||
| sc_options_add_string (opt, 'b', "brepfile", &brep_file, NULL, | ||||||
| "File prefix of the deformation geometry file (without .brep)"); | ||||||
| sc_options_add_int (opt, 'd', "dimension", &dim, 0, "Dimension of the mesh (1, 2 or 3)"); | ||||||
| sc_options_add_int (opt, 'l', "level", &level, 2, "Uniform refinement level for the input mesh. Default: 2"); | ||||||
|
|
||||||
| int parsed = sc_options_parse (t8_get_package_id (), SC_LP_ERROR, opt, argc, argv); | ||||||
|
|
||||||
| if (helpme) { | ||||||
| t8_global_productionf ("%s\n", help); | ||||||
| sc_options_print_usage (t8_get_package_id (), SC_LP_ERROR, opt, NULL); | ||||||
| } | ||||||
| else if (msh_file == NULL || brep_file == NULL || dim == 0 || parsed < 0) { | ||||||
| t8_global_errorf ("\n\t ERROR: Wrong usage\n"); | ||||||
| sc_options_print_usage (t8_get_package_id (), SC_LP_ERROR, opt, NULL); | ||||||
| } | ||||||
| else if (dim < 1 || dim > 3) { | ||||||
| t8_global_errorf ("ERROR: Invalid mesh dimension: dim=%d. Dimension must be 1, 2 or 3.\n\n", dim); | ||||||
| sc_options_print_usage (t8_get_package_id (), SC_LP_ERROR, opt, NULL); | ||||||
| } | ||||||
| else if (parsed >= 0) { | ||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What would happen if
Suggested change
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ok |
||||||
|
|
||||||
| /* We will use MPI_COMM_WORLD as a communicator. */ | ||||||
| sc_MPI_Comm comm = sc_MPI_COMM_WORLD; | ||||||
|
|
||||||
| /* Create cmesh from msh. */ | ||||||
| t8_cmesh_t cmesh = t8_cmesh_from_msh_file (msh_file, 0, comm, dim, 0, 1); | ||||||
|
sandro-elsweijer marked this conversation as resolved.
|
||||||
| t8_forest_t forest = t8_forest_new_uniform (cmesh, t8_scheme_new_default (), level, 0, comm); | ||||||
|
|
||||||
| /* Load CAD geometry from .brep file. */ | ||||||
| auto cad = std::make_shared<t8_cad_handle> (brep_file); | ||||||
|
|
||||||
| /* Initialize the deformation object for the given mesh. */ | ||||||
| t8_cmesh_mesh_deformation deformation (cmesh); | ||||||
|
|
||||||
| /* Calculate displacements. */ | ||||||
| auto displacements = deformation.calculate_displacement_surface_vertices (cad.get ()); | ||||||
|
|
||||||
| /* Write output. */ | ||||||
| t8_forest_vtk_write_file (forest, "input_forest", 1, 1, 1, 1, 0, 0, NULL); | ||||||
|
|
||||||
| /* Apply displacements. */ | ||||||
| deformation.apply_vertex_displacements (displacements, cad); | ||||||
|
|
||||||
| /* Write output. */ | ||||||
| t8_forest_vtk_write_file (forest, "deformed_forest", 1, 1, 1, 1, 0, 0, NULL); | ||||||
|
|
||||||
| /* Cleanup. */ | ||||||
| t8_forest_unref (&forest); | ||||||
|
|
||||||
| t8_global_productionf ("Mesh deformation completed."); | ||||||
| } | ||||||
|
|
||||||
| sc_options_destroy (opt); | ||||||
|
|
||||||
| sc_finalize (); | ||||||
| mpiret = sc_MPI_Finalize (); | ||||||
| SC_CHECK_MPI (mpiret); | ||||||
|
|
||||||
| #else /* T8CODE_ENABLE_OCC */ | ||||||
| t8_global_errorf ("ERROR: This example requires OpenCASCADE support to be enabled in t8code.\n"); | ||||||
| #endif /* T8CODE_ENABLE_OCC */ | ||||||
|
|
||||||
| return 0; | ||||||
| } | ||||||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
190 changes: 190 additions & 0 deletions
190
src/t8_cmesh/t8_cmesh_mesh_deformation/t8_cmesh_mesh_deformation.cxx
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,190 @@ | ||
| /* | ||
| This file is part of t8code. | ||
| t8code is a C library to manage a collection (a forest) of multiple | ||
| connected adaptive space-trees of general element classes in parallel. | ||
|
|
||
| Copyright (C) 2026 the developers | ||
|
|
||
| t8code is free software; you can redistribute it and/or modify | ||
| it under the terms of the GNU General Public License as published by | ||
| the Free Software Foundation; either version 2 of the License, or | ||
| (at your option) any later version. | ||
|
|
||
| t8code is distributed in the hope that it will be useful, | ||
| but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
| MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
| GNU General Public License for more details. | ||
|
|
||
| You should have received a copy of the GNU General Public License | ||
| along with t8code; if not, write to the Free Software Foundation, Inc., | ||
| 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | ||
| */ | ||
|
|
||
| /** \file t8_cmesh_mesh_deformation.cxx | ||
| * This file implements the routines for CAD-based mesh deformation. | ||
| */ | ||
|
|
||
| #include <unordered_map> | ||
| #include <t8_cmesh/t8_cmesh_vertex_connectivity/t8_cmesh_vertex_connectivity.hxx> | ||
| #include <t8_cad/t8_cad_handle.hxx> | ||
| #include <t8_cmesh/t8_cmesh.h> | ||
| #include <t8_cmesh/t8_cmesh.hxx> | ||
| #include <t8_cmesh/t8_cmesh_mesh_deformation/t8_cmesh_mesh_deformation.hxx> | ||
| #include <t8_geometry/t8_geometry_handler.hxx> | ||
| #include <t8_geometry/t8_geometry_implementations/t8_geometry_cad.hxx> | ||
|
|
||
| std::unordered_map<t8_gloidx_t, t8_3D_vec> | ||
| t8_cmesh_mesh_deformation::calculate_displacement_surface_vertices (const t8_cad_handle *cad) | ||
| { | ||
| T8_ASSERT (t8_cmesh_is_committed (associated_cmesh)); | ||
|
|
||
| const int mesh_dimension = t8_cmesh_get_dimension (associated_cmesh); | ||
|
|
||
| /* Map from global vertex id -> displacement vector. */ | ||
| std::unordered_map<t8_gloidx_t, t8_3D_vec> displacements; | ||
|
|
||
| for (const auto &global_vertex : *(associated_cmesh->vertex_connectivity)) { | ||
|
|
||
| /* Get the list of all trees associated with the vertex. */ | ||
| const auto &tree_list = global_vertex.second; | ||
| const t8_gloidx_t global_vertex_id = global_vertex.first; | ||
|
|
||
| /* Get the first tree and the local corner index of the vertex in the tree. */ | ||
| const auto &first_tree = tree_list.front (); | ||
| const t8_locidx_t first_tree_id = first_tree.first; | ||
| const int local_corner_index = first_tree.second; | ||
|
|
||
| /* Get the first tree as a reference. */ | ||
| const int *first_tree_geom_attribute = static_cast<const int *> (t8_cmesh_get_attribute ( | ||
| associated_cmesh, t8_get_package_id (), T8_CMESH_NODE_GEOMETRY_ATTRIBUTE_KEY, first_tree_id)); | ||
|
|
||
| /* Check if the geometry attribute is available for this tree. */ | ||
| if (first_tree_geom_attribute == nullptr) { | ||
| t8_errorf ("Error: Geometry attribute missing for tree %d\n.", first_tree_id); | ||
| SC_ABORTF ("Geometry attribute is missing.\n"); | ||
| } | ||
|
|
||
| const int first_tree_entity_dim = first_tree_geom_attribute[2 * tree_list[0].second]; | ||
| const int first_tree_entity_tag = first_tree_geom_attribute[2 * tree_list[0].second + 1]; | ||
|
|
||
| /* Check if all trees sharing this vertex have consistent geometry attributes. */ | ||
| #if T8_ENABLE_DEBUG | ||
| /* Iterate over all trees and compare to the reference tree. */ | ||
| for (const auto &[tree_id, local_corner_index] : tree_list) { | ||
|
|
||
| const int *geom_attribute = static_cast<const int *> ( | ||
| t8_cmesh_get_attribute (associated_cmesh, t8_get_package_id (), T8_CMESH_NODE_GEOMETRY_ATTRIBUTE_KEY, tree_id)); | ||
|
|
||
| const int entity_dim = geom_attribute[2 * local_corner_index]; | ||
| const int entity_tag = geom_attribute[2 * local_corner_index + 1]; | ||
|
|
||
| /* Check if the attribute of the vertex is the same in all trees. */ | ||
| if (!(entity_dim == first_tree_entity_dim && entity_tag == first_tree_entity_tag)) { | ||
| t8_errorf ( | ||
| "Error: Inconsistent entity info for global vertex %li: tree %d: dim=%d tag=%d, expected dim=%d tag=%d\n", | ||
| global_vertex_id, tree_id, entity_dim, entity_tag, first_tree_entity_dim, first_tree_entity_tag); | ||
| SC_ABORTF ("Inconsistency in vertex info.\n"); | ||
| } | ||
| } | ||
| #endif /* T8_ENABLE_DEBUG */ | ||
|
|
||
| /* Check if this vertex is a boundary node. */ | ||
| if (first_tree_entity_dim < mesh_dimension && first_tree_entity_dim >= 0) { | ||
|
|
||
| /* Get the pointer to the array of (u,v)-parameters for the CAD geometry. */ | ||
| const double *uv_attribute = (const double *) t8_cmesh_get_attribute ( | ||
| associated_cmesh, t8_get_package_id (), T8_CMESH_NODE_PARAMETERS_ATTRIBUTE_KEY, first_tree_id); | ||
|
|
||
| /* Check if the (u,v)-parameters are available. */ | ||
| if (uv_attribute == nullptr) { | ||
| t8_errorf ("Error: (u,v)-parameters are missing for tree %d\n.", first_tree_id); | ||
| SC_ABORT ("(u,v)-parameters are missing.\n"); | ||
| } | ||
| /* Get the (u,v)-parameter of the vertex. */ | ||
| const double *uv_parameter = &uv_attribute[2 * local_corner_index]; | ||
|
|
||
| /* Get the pointer to the coordinate array as it was before the deformation. */ | ||
| const double *old_coords = (const double *) t8_cmesh_get_attribute ( | ||
| associated_cmesh, t8_get_package_id (), T8_CMESH_VERTICES_ATTRIBUTE_KEY, first_tree_id); | ||
|
|
||
| /* Check if the coordinates are available. */ | ||
| if (old_coords == nullptr) { | ||
| t8_errorf ("Error: Coordinates attribute missing for tree %d\n.", first_tree_id); | ||
| SC_ABORTF ("Vertex coordinates are missing.\n"); | ||
| } | ||
|
|
||
| gp_Pnt new_coords; | ||
|
|
||
| /* Find the new coordinates of the vertex in the cad file, based on the geometry its lying on. */ | ||
| switch (first_tree_entity_dim) { | ||
| case 0: { | ||
| new_coords = cad->get_cad_point (first_tree_entity_tag); | ||
| break; | ||
| } | ||
| case 1: { | ||
| Handle_Geom_Curve curve = cad->get_cad_curve (first_tree_entity_tag); | ||
| curve->D0 (uv_parameter[0], new_coords); | ||
| break; | ||
| } | ||
| case 2: { | ||
| Handle_Geom_Surface surface = cad->get_cad_surface (first_tree_entity_tag); | ||
| surface->D0 (uv_parameter[0], uv_parameter[1], new_coords); | ||
| break; | ||
| } | ||
| default: | ||
| SC_ABORT_NOT_REACHED (); | ||
| } | ||
|
|
||
| /* Get the old coordinates before the deformation. */ | ||
| const double old_x = old_coords[3 * local_corner_index + 0]; | ||
| const double old_y = old_coords[3 * local_corner_index + 1]; | ||
| const double old_z = old_coords[3 * local_corner_index + 2]; | ||
|
|
||
| /* Calculate the displacement of the vertex which should be then done in the deformation. */ | ||
| displacements[global_vertex_id] = { new_coords.X () - old_x, new_coords.Y () - old_y, new_coords.Z () - old_z }; | ||
| } | ||
| } | ||
| return displacements; | ||
| } | ||
|
|
||
| void | ||
| t8_cmesh_mesh_deformation::apply_vertex_displacements (const std::unordered_map<t8_gloidx_t, t8_3D_vec> &displacements, | ||
| std::shared_ptr<t8_cad_handle> cad) | ||
| { | ||
| T8_ASSERT (t8_cmesh_is_committed (associated_cmesh)); | ||
|
|
||
| /* Iterate over all vertices in the displacement map. */ | ||
| for (const auto &[global_vertex, displacement] : displacements) { | ||
|
|
||
| /* Get the list of trees where this vertex exists. */ | ||
| const auto &tree_list = associated_cmesh->vertex_connectivity->get_tree_list_of_vertex (global_vertex); | ||
|
|
||
| /* Update the vertex coordinates in each tree. */ | ||
| for (const auto &[tree_id, local_vertex_index] : tree_list) { | ||
|
|
||
| /* Get the vertex coordinates of the current tree. */ | ||
| double *tree_vertex_coords = (double *) t8_cmesh_get_attribute (associated_cmesh, t8_get_package_id (), | ||
| T8_CMESH_VERTICES_ATTRIBUTE_KEY, tree_id); | ||
|
|
||
| /* Check if the coordinates are available. */ | ||
| if (tree_vertex_coords != nullptr) { | ||
| /* Update the coordinates of the vertex. */ | ||
| for (int coord_index = 0; coord_index < 3; ++coord_index) { | ||
| tree_vertex_coords[3 * local_vertex_index + coord_index] += displacement[coord_index]; | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| /* Update the cad geometry. */ | ||
| t8_geometry_handler *geometry_handler = associated_cmesh->geometry_handler; | ||
| T8_ASSERT (geometry_handler != nullptr); | ||
|
|
||
| for (auto geom = geometry_handler->begin (); geom != geometry_handler->end (); ++geom) { | ||
| if (geom->second->t8_geom_get_type () == T8_GEOMETRY_TYPE_CAD) { | ||
| t8_geometry_cad *cad_geom = static_cast<t8_geometry_cad *> (geom->second.get ()); | ||
| cad_geom->update_cad_handle (cad); | ||
| break; | ||
| } | ||
| } | ||
|
sandro-elsweijer marked this conversation as resolved.
|
||
| } | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.