https://mooseframework.inl.gov
Loading...
Searching...
No Matches
TriangleManifold.C
Go to the documentation of this file.
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
10#include "TriangleManifold.h"
11
12#include "GeometryUtils.h"
13#include "MooseError.h"
14
15#include "libmesh/mesh_tools.h"
16#include "libmesh/unstructured_mesh.h"
17
18#include <algorithm>
19#include <cmath>
20#include <cstdint>
21#include <cstring>
22
24{
25std::uint64_t
26packCell(const std::size_t y_index, const std::size_t z_index)
27{
28 // The acceleration structure is indexed in yz because all rays travel in the +x direction.
29 return (static_cast<std::uint64_t>(y_index) << 32) | static_cast<std::uint64_t>(z_index);
30}
31
32std::size_t
33cellIndex(const Real value, const Real min_value, const Real cell_size, const std::size_t num_cells)
34{
35 // Clamp to the valid range so near-boundary roundoff does not produce an invalid cell id.
36 if (num_cells <= 1)
37 return 0;
38
39 const auto index = static_cast<long long>(std::floor((value - min_value) / cell_size));
40 if (index < 0)
41 return 0;
42 if (index >= static_cast<long long>(num_cells))
43 return num_cells - 1;
44 return static_cast<std::size_t>(index);
45}
46
48{
49public:
50 explicit SurfaceChecker(UnstructuredMesh & mesh) : libMesh::MeshTetInterface(mesh) {}
51 void triangulate() override { mooseError("SurfaceChecker is not meant for triangulation."); }
52 std::string improveAndValidate();
53};
54}
55
56TriangleManifold::TriangleManifold(MeshBase & mesh, const Real surface_tolerance)
57 : _mesh(mesh),
58 _surface_tolerance(surface_tolerance),
59 _bounding_box(MeshTools::create_bounding_box(_mesh)),
60 _point_locator(_mesh.sub_point_locator())
61{
62 mooseAssert(_surface_tolerance > 0.0, "surface_tolerance must be strictly positive.");
63 mooseAssert(mesh.is_serial(), "Input manifold mesh must be serialized.");
64 mooseAssert(mesh.mesh_dimension() == 2, "Manifold mesh must be a surface.");
65
66 // Finish topology validation and acceleration-structure setup before queries are allowed.
67 finalize();
68
69 _point_locator->set_close_to_point_tol(_surface_tolerance);
70}
71
72bool
73TriangleManifold::contains(const Point & point) const
74{
75 // Most points are rejected here before we perform any per-triangle work.
76 if (!pointInsideBoundingBox(point))
77 return false;
78
79 // By design, near-surface points count as inside for downstream subdomain tagging.
80 if (pointOnSurface(point))
81 return true;
82
83 // Candidate filtering keeps the parity test from touching every triangle in large surfaces.
84 const auto candidates = rayCandidates(point);
85 unsigned int num_hits = 0;
86
87 for (const auto triangle_index : candidates)
88 {
89 const auto tri = _mesh.elem_ptr(triangle_index);
90 // Triangles wholly behind the ray origin cannot contribute to the parity count.
91 if (tri->loose_bounding_box().max()(0) < point(0) - _surface_tolerance)
92 continue;
93
94 switch (rayIntersectsTriangle(point, *tri))
95 {
97 break;
99 // Standard odd/even counting on a closed surface.
100 ++num_hits;
101 break;
103 // Edge and vertex grazing hits are exactly where parity counting becomes brittle.
104 return containsBySolidAngle(point);
105 }
106 }
107
108 return num_hits % 2;
109}
110
111void
113{
114 // Validate that the mesh can be used as a manifold
115 // We use the same logic as determining if the mesh can the tetrahedralized.
116 auto umesh = dynamic_cast<UnstructuredMesh *>(&_mesh);
118 auto msg = checker.improveAndValidate();
119 if (!msg.empty())
121 "The inputted surface mesh cannot be treated as manifold for the following reasons:\n",
122 msg);
123
125}
126
127void
129{
130 // Since every containment ray travels in +x, only y and z are needed to choose candidate
131 // triangles for the parity test.
132 const auto extent_y =
133 std::max(boundingBox().max()(1) - boundingBox().min()(1), _surface_tolerance);
134 const auto extent_z =
135 std::max(boundingBox().max()(2) - boundingBox().min()(2), _surface_tolerance);
136
137 // Choose a roughly square yz grid scaled by aspect ratio so candidate lists stay short.
138 const auto target_cells =
139 std::max<dof_id_type>(1, static_cast<dof_id_type>(std::sqrt(numTriangles())));
140 const auto aspect = std::sqrt(extent_y / extent_z);
141
142 _num_y_cells = std::max<dof_id_type>(
143 1, static_cast<dof_id_type>(std::lround(std::sqrt(target_cells) * aspect)));
144 _num_z_cells = std::max<std::size_t>(
145 1, static_cast<std::size_t>(std::ceil(static_cast<Real>(target_cells) / _num_y_cells)));
146
147 _y_min = boundingBox().min()(1);
148 _z_min = boundingBox().min()(2);
149 _y_cell_size = extent_y / _num_y_cells;
150 _z_cell_size = extent_z / _num_z_cells;
151
152 for (const auto elem : _mesh.active_element_ptr_range())
153 {
154 const auto triangle_index = elem->id();
155 const auto bbox = elem->loose_bounding_box();
156
157 const auto y_start =
159 const auto y_stop =
161 const auto z_start =
163 const auto z_stop =
165
166 // Insert the triangle into every grid cell touched by its yz projection.
167 for (const auto iy : make_range(y_start, y_stop + 1))
168 for (const auto iz : make_range(z_start, z_stop + 1))
169 _ray_grid[TriangleManifoldUtils::packCell(iy, iz)].push_back(triangle_index);
170 }
171}
172
173bool
175{
176 // Inflate the global box by the tolerance so near-surface points are not rejected too early.
177 return point(0) >= boundingBox().min()(0) - _surface_tolerance &&
178 point(0) <= boundingBox().max()(0) + _surface_tolerance &&
179 point(1) >= boundingBox().min()(1) - _surface_tolerance &&
180 point(1) <= boundingBox().max()(1) + _surface_tolerance &&
181 point(2) >= boundingBox().min()(2) - _surface_tolerance &&
182 point(2) <= boundingBox().max()(2) + _surface_tolerance;
183}
184
185bool
186TriangleManifold::pointOnSurface(const Point & point) const
187{
188 return (*_point_locator)(point) != nullptr;
189}
190
192TriangleManifold::rayIntersectsTriangle(const Point & point, const libMesh::Elem & tri) const
193{
194 // This is a fixed-direction Moller-Trumbore style ray/triangle intersection test.
195 static const Point direction(1.0, 0.0, 0.0);
196
197 const Point edge1 = tri.node_ref(1) - tri.node_ref(0);
198 const Point edge2 = tri.node_ref(2) - tri.node_ref(0);
199 const Point h = direction.cross(edge2);
200 const Real determinant = edge1 * h;
201 const Real characteristic_length = std::max(edge1.norm(), edge2.norm());
202
203 if (std::abs(determinant) <= _surface_tolerance * characteristic_length)
204 // Nearly parallel triangles are ignored because they do not provide a stable parity event.
206
207 const Real inv_determinant = 1.0 / determinant;
208 const Point s = point - tri.node_ref(0);
209 const Real u = inv_determinant * (s * h);
210 if (u < 0.0 || u > 1.0)
212
213 const Point q = s.cross(edge1);
214 const Real v = inv_determinant * (direction * q);
215 if (v < 0.0 || u + v > 1.0)
217
218 const Real t = inv_determinant * (edge2 * q);
219 if (t < 0.0)
220 // Intersections behind the ray origin do not contribute to +x parity counting.
222 if (t <= _surface_tolerance)
223 // Hits too close to the ray origin are treated as ambiguous boundary situations.
225
226 const Point intersection = point + t * direction;
227 const auto tolerance_sq = _surface_tolerance * _surface_tolerance;
228 if (geom_utils::pointSegmentDistanceSq(intersection, tri.node_ref(0), tri.node_ref(1)) <=
229 tolerance_sq ||
230 geom_utils::pointSegmentDistanceSq(intersection, tri.node_ref(1), tri.node_ref(2)) <=
231 tolerance_sq ||
232 geom_utils::pointSegmentDistanceSq(intersection, tri.node_ref(2), tri.node_ref(0)) <=
233 tolerance_sq)
234 // Edge and vertex hits are where odd/even parity counting is the least reliable.
236
238}
239
240bool
242{
243 // For a closed oriented mesh, the total solid angle is approximately +-4\pi inside and 0
244 // outside. Taking parity over components handles nested shells cleanly.
245 Real total_angle = 0.0;
246 for (const auto elem : _mesh.active_element_ptr_range())
247 total_angle +=
248 geom_utils::solidAngle(point, elem->node_ref(0), elem->node_ref(1), elem->node_ref(2));
249
250 return std::abs(total_angle) > 2.0 * libMesh::pi;
251}
252
253std::vector<dof_id_type>
254TriangleManifold::rayCandidates(const Point & point) const
255{
256 // If the query is outside the manifold's yz extent, the fixed +x ray cannot hit anything.
257 if (point(1) < boundingBox().min()(1) - _surface_tolerance ||
258 point(1) > boundingBox().max()(1) + _surface_tolerance ||
259 point(2) < boundingBox().min()(2) - _surface_tolerance ||
260 point(2) > boundingBox().max()(2) + _surface_tolerance)
261 return {};
262
263 const auto y_index =
265 const auto z_index =
267 const auto it = _ray_grid.find(TriangleManifoldUtils::packCell(y_index, z_index));
268 if (it == _ray_grid.end())
269 return {};
270
271 // Return by value to keep the helper simple and avoid exposing internal storage.
272 return it->second;
273}
274
275std::string
277{
279 auto result = improve_hull_integrity();
280
281 if (result.empty())
282 return "";
283
284 std::ostringstream err_msg;
285 if (result.count(NON_TRI3))
286 err_msg << "- At least one non-Tri3 element was found.\n" << std::endl;
287 if (result.count(MISSING_NEIGHBOR))
288 err_msg << "- At least one triangle without three neighbors was found.\n" << std::endl;
289 if (result.count(EMPTY_MESH))
290 err_msg << "- The surface mesh was empty\n" << std::endl;
291 if (result.count(MISSING_BACKLINK))
292 err_msg << "- At least one triangle neighbor without a return neighbor link was found.\n";
293 if (result.count(BAD_NEIGHBOR_NODES))
294 err_msg << "- At least one triangle neighbor without expected node links was found.\n";
295 if (result.count(NON_ORIENTED))
296 err_msg << "- At least one triangle neighbor with an inconsistent orientation was found.\n";
297 if (result.count(BAD_NEIGHBOR_LINKS))
298 err_msg << "- At least one triangle neighbor with inconsistent node and neighbor links was "
299 "found.\n";
300 if (result.count(DEGENERATE_ELEMENT))
301 err_msg << "- At least one input triangle is degenerate, with near-zero area relative to the "
302 "manifold.\n";
303 if (result.count(DEGENERATE_MESH))
304 err_msg << "- Mesh is degenerate, with zero thickness in at least one direction.\n";
305 return err_msg.str();
306}
void mooseError(Args &&... args)
Emit an error message with the given stringified, concatenated args and terminate the application.
Definition MooseError.h:311
std::size_t _num_y_cells
Number of yz-grid bins in the y direction.
bool contains(const Point &point) const
const Real _surface_tolerance
Absolute tolerance used throughout validation and geometric classification.
std::size_t _num_z_cells
Number of yz-grid bins in the z direction.
std::size_t numTriangles() const
bool pointInsideBoundingBox(const Point &point) const
Cheap global bounding-box rejection for containment queries.
std::unordered_map< dof_id_type, std::vector< dof_id_type > > _ray_grid
Lookup from packed yz-grid cell index to triangles that could intersect the +x query ray.
Real _y_cell_size
Width of one yz-grid cell in the y direction.
Real _z_min
Minimum global z coordinate used to map query points into yz-grid bins.
RayIntersection rayIntersectsTriangle(const Point &point, const libMesh::Elem &tri) const
Intersect a positive x-direction ray with a single triangle.
TriangleManifold(MeshBase &mesh, const Real surface_tolerance)
Build a manifold classifier from a prepared surface mesh.
void buildCandidateGrid()
Build the yz-plane lookup grid used to accelerate +x ray queries.
Real _z_cell_size
Width of one yz-grid cell in the z direction.
bool containsBySolidAngle(const Point &point) const
Robust fallback containment query based on accumulated solid angle.
Real _y_min
Minimum global y coordinate used to map query points into yz-grid bins.
bool pointOnSurface(const Point &point) const
Detect whether a query point lies on or extremely near the manifold surface.
std::vector< dof_id_type > rayCandidates(const Point &point) const
Get the subset of triangles whose yz extents may intersect the query ray.
const libMesh::BoundingBox & boundingBox() const
void finalize()
Complete post-parse validation and acceleration-structure setup.
const std::unique_ptr< libMesh::PointLocatorBase > _point_locator
Pre-built point locator for fast proximity-to-surface detection.
RayIntersection
Result of intersecting the positive x-direction ray with a triangle.
const Point & max() const
const Point & min() const
const Node & node_ref(const unsigned int i) const
MeshTetInterface(UnstructuredMesh &mesh)
std::set< SurfaceIntegrity > improve_hull_integrity()
TypeVector< typename CompareTypes< Real, T2 >::supertype > cross(const TypeVector< T2 > &v) const
MeshBase & mesh
std::uint64_t packCell(const std::size_t y_index, const std::size_t z_index)
std::size_t cellIndex(const Real value, const Real min_value, const Real cell_size, const std::size_t num_cells)
Real pointSegmentDistanceSq(const Point &point, const Point &a, const Point &b)
Compute the squared distance from a point to a 3-D line segment.
Real solidAngle(const Point &point, const Point &v0, const Point &v1, const Point &v2)
Compute the signed solid angle subtended by one oriented triangle at the query point.
The following methods are specializations for using the libMesh::Parallel::packed_range_* routines fo...
const Real pi