https://mooseframework.inl.gov
Loading...
Searching...
No Matches
Classes | Public Types | Public Member Functions | Static Public Member Functions | Static Public Attributes | Private Member Functions | Static Private Member Functions | Private Attributes | Static Private Attributes | List of all members
XYIncrementalDelaunay Class Reference

Constrained Delaunay triangulation of a set of points in the plane, built one point at a time. More...

#include <XYIncrementalDelaunay.h>

Classes

struct  Point2D
 A point of the triangulation, held as plain coordinates. More...
 
struct  Triangle
 A triangle of the triangulation. More...
 

Public Types

using Segment = std::pair< std::size_t, std::size_t >
 A constrained segment, held as a vertex id pair with the smaller id first.
 

Public Member Functions

void initialize (const std::vector< Point2D > &points, const std::vector< Segment > &segments)
 Triangulates points and recovers every entry of segments as an edge of the result.
 
std::size_t insertPoint (const Point2D &p)
 Inserts a point, restoring the constrained Delaunay property around it.
 
void insertSegment (std::size_t v0, std::size_t v1)
 Makes the segment between two vertices an edge of the triangulation and records it as constrained, so that later insertions neither remove nor cross it.
 
std::size_t numPoints () const
 
const Point2Dpoint (std::size_t id) const
 
std::vector< TrianglegetTriangles () const
 
bool isConstrainedSegment (std::size_t v0, std::size_t v1) const
 
const std::set< Segment > & constrainedSegments () const
 
std::vector< std::string > checkInvariants () const
 Checks everything this class promises: that every triangle is counter-clockwise, that the neighbor entries agree with each other, that every constrained segment is an edge, and that every edge that is not constrained is locally Delaunay.
 
std::vector< std::string > checkEmptyCircumcircle () const
 Tests every vertex against the circumcircle of every triangle.
 

Static Public Member Functions

static Segment makeSegment (std::size_t v0, std::size_t v1)
 

Static Public Attributes

static constexpr std::size_t invalid_index = std::numeric_limits<std::size_t>::max()
 Sentinel for a vertex, triangle or neighbor that does not exist.
 

Private Member Functions

const double * xy (std::size_t v) const
 
std::size_t toInternal (std::size_t id) const
 
std::size_t toCaller (std::size_t v) const
 
bool isConstrainedEdge (std::size_t v0, std::size_t v1) const
 
unsigned int localVertexIndex (std::size_t t, std::size_t v) const
 
unsigned int localEdgeIndex (std::size_t t, const Segment &edge) const
 
bool containsPoint (std::size_t t, const Point2D &p) const
 
std::size_t locate (const Point2D &p) const
 Finds the triangle a point falls in by walking from the triangle the last insertion produced, which keeps the search short when successive points are close together.
 
void growCavity (std::size_t seed, std::size_t v_new, std::set< std::size_t > &cavity) const
 Collects the triangles of the triangulation built so far that have to make way for a new vertex, the Delaunay cavity of its insertion, starting from the triangle it falls in and spreading to every neighbor whose circumcircle contains it.
 
std::vector< std::size_t > retriangulate (const std::vector< std::size_t > &removed, const std::vector< std::array< std::size_t, 3 > > &added)
 Swaps one triangulation of a region for another, reusing the slots of the triangles it removes and rebuilding the neighbor entries both inside the region and along its boundary.
 
void triangulatePseudopolygon (std::size_t v_start, std::size_t v_end, const std::vector< std::size_t > &chain, std::size_t first, std::size_t last, std::vector< std::array< std::size_t, 3 > > &triangles) const
 Triangulates a polygon whose vertices are all visible from one of its edges, which is the shape a recovered segment leaves on either side of itself.
 
bool isStrictlyBetween (std::size_t v_first, std::size_t v_mid, std::size_t v_last) const
 
std::string vertexName (std::size_t v) const
 

Static Private Member Functions

static bool isBounding (std::size_t v)
 

Private Attributes

std::vector< Point2D_vertices
 The bounding triangle vertices followed by the caller's points.
 
std::vector< Triangle_triangles
 The triangles, every one of them live and counter-clockwise.
 
std::vector< std::size_t > _vertex_triangle
 One triangle touching each vertex of _vertices, which is where a walk around it starts.
 
std::set< Segment_constraints
 The constrained segments, in caller vertex ids with the smaller id first.
 
std::size_t _last_triangle = invalid_index
 A triangle the last insertion produced, which is where the next point walk starts.
 

Static Private Attributes

static constexpr std::size_t _num_bounding = 3
 The number of bounding triangle vertices padding the front of the vertex list.
 
static constexpr double _bounding_reach = 1000.0
 How far the bounding triangle reaches beyond the points, as a multiple of their extent.
 

Detailed Description

Constrained Delaunay triangulation of a set of points in the plane, built one point at a time.

Points are inserted with the Bowyer-Watson construction: the triangle containing the new point is found by walking the triangulation, the Delaunay cavity (the triangles whose circumcircle contains the point) is deleted, and the point is connected to the boundary of that cavity. Constrained segments are edges that the triangulation is required to contain; the Delaunay cavity never grows across one, and a segment that is not an edge once the points are in is recovered by retriangulating the strip of triangles it crosses.

After every insertion no vertex lies strictly inside the circumcircle of any triangle, except where a constrained segment separates the two. Cocircular and collinear configurations are resolved consistently rather than arbitrarily, so the same input always gives the same triangulation. Every geometric decision is made with the exact predicates in framework/contrib/predicates, so no tolerance enters the algorithm.

The triangulation carries no mesh types: points are plain coordinate pairs and triangles are plain index triples, so it can be exercised on its own.

Internally the point set is padded with the three vertices of a bounding triangle that encloses everything, which is what lets the Delaunay cavity of every insertion be a closed polygon. Those three vertices are not part of the caller's point set and never appear in the public interface: the vertex ids used here run from 0 to numPoints() - 1 in the order the points were added, and getTriangles() drops the triangles that touch the bounding triangle.

Definition at line 45 of file XYIncrementalDelaunay.h.

Member Typedef Documentation

◆ Segment

using XYIncrementalDelaunay::Segment = std::pair<std::size_t, std::size_t>

A constrained segment, held as a vertex id pair with the smaller id first.

Definition at line 72 of file XYIncrementalDelaunay.h.

Member Function Documentation

◆ checkEmptyCircumcircle()

std::vector< std::string > XYIncrementalDelaunay::checkEmptyCircumcircle ( ) const

Tests every vertex against the circumcircle of every triangle.

This ignores constrained segments, which a vertex is allowed to sit behind, so it is the right criterion only for a triangulation with no constrained segments; use checkInvariants() otherwise.

Returns
One entry describing each vertex that lies strictly inside a circumcircle, empty if there are none

Definition at line 710 of file XYIncrementalDelaunay.C.

711{
712 Moose::initPredicates();
713
714 std::vector<std::string> violations;
715 for (const auto t : index_range(_triangles))
716 {
717 const auto & triangle = _triangles[t];
718 for (const auto v : index_range(_vertices))
719 {
720 if (v == triangle.vertices[0] || v == triangle.vertices[1] || v == triangle.vertices[2])
721 continue;
722 if (moose_incircle(
723 xy(triangle.vertices[0]), xy(triangle.vertices[1]), xy(triangle.vertices[2]), xy(v)) >
724 0.0)
725 violations.push_back(vertexName(v) + " is inside the circumcircle of triangle " +
726 std::to_string(t));
727 }
728 }
729 return violations;
730}
std::vector< Triangle > _triangles
The triangles, every one of them live and counter-clockwise.
std::string vertexName(std::size_t v) const
std::vector< Point2D > _vertices
The bounding triangle vertices followed by the caller's points.
const double * xy(std::size_t v) const
auto index_range(const T &sizable)

◆ checkInvariants()

std::vector< std::string > XYIncrementalDelaunay::checkInvariants ( ) const

Checks everything this class promises: that every triangle is counter-clockwise, that the neighbor entries agree with each other, that every constrained segment is an edge, and that every edge that is not constrained is locally Delaunay.

A triangulation whose unconstrained edges are all locally Delaunay is a constrained Delaunay triangulation.

Returns
One entry describing each violation found, empty if there are none

Definition at line 643 of file XYIncrementalDelaunay.C.

644{
645 Moose::initPredicates();
646
647 std::vector<std::string> violations;
648 std::set<Segment> edges;
649
650 for (const auto t : index_range(_triangles))
651 {
652 const auto & triangle = _triangles[t];
653 if (moose_orient2d(
654 xy(triangle.vertices[0]), xy(triangle.vertices[1]), xy(triangle.vertices[2])) <= 0.0)
655 violations.push_back("triangle " + std::to_string(t) +
656 " is not counter-clockwise, so its area is zero or negative");
657
658 for (const auto i : make_range(3u))
659 {
660 const auto v0 = triangle.vertices[(i + 1) % 3];
661 const auto v1 = triangle.vertices[(i + 2) % 3];
662 edges.insert(makeSegment(v0, v1));
663
664 const auto n = triangle.neighbors[i];
665 if (n == invalid_index)
666 continue;
667 if (n >= _triangles.size())
668 {
669 violations.push_back("triangle " + std::to_string(t) + " has neighbor " +
670 std::to_string(n) + ", which is not a triangle");
671 continue;
672 }
673
674 unsigned int j = 3;
675 for (const auto k : make_range(3u))
676 if (makeSegment(_triangles[n].vertices[(k + 1) % 3], _triangles[n].vertices[(k + 2) % 3]) ==
677 makeSegment(v0, v1))
678 j = k;
679
680 if (j == 3)
681 {
682 violations.push_back("triangles " + std::to_string(t) + " and " + std::to_string(n) +
683 " are neighbors but share no edge");
684 continue;
685 }
686 if (_triangles[n].neighbors[j] != t)
687 violations.push_back("triangle " + std::to_string(t) + " has neighbor " +
688 std::to_string(n) + ", which does not have it back");
689
690 if (!isConstrainedEdge(v0, v1) && moose_incircle(xy(triangle.vertices[0]),
691 xy(triangle.vertices[1]),
692 xy(triangle.vertices[2]),
693 xy(_triangles[n].vertices[j])) > 0.0)
694 violations.push_back("the edge between " + vertexName(v0) + " and " + vertexName(v1) +
695 " is not constrained and is not locally Delaunay, because " +
696 vertexName(_triangles[n].vertices[j]) +
697 " is inside the circumcircle of triangle " + std::to_string(t));
698 }
699 }
700
701 for (const auto & [v0, v1] : _constraints)
702 if (edges.count(makeSegment(toInternal(v0), toInternal(v1))) == 0)
703 violations.push_back("constrained segment (" + std::to_string(v0) + ", " +
704 std::to_string(v1) + ") is not an edge of the triangulation");
705
706 return violations;
707}
unsigned int count
Definition MortarUtils.C:53
if(!dmm->_nl) SETERRQ(PETSC_COMM_WORLD
static Segment makeSegment(std::size_t v0, std::size_t v1)
std::set< Segment > _constraints
The constrained segments, in caller vertex ids with the smaller id first.
std::size_t toInternal(std::size_t id) const
bool isConstrainedEdge(std::size_t v0, std::size_t v1) const
static constexpr std::size_t invalid_index
Sentinel for a vertex, triangle or neighbor that does not exist.
IntRange< T > make_range(T beg, T end)

◆ constrainedSegments()

const std::set< Segment > & XYIncrementalDelaunay::constrainedSegments ( ) const
inline
Returns
The constrained segments, each with the smaller vertex id first

Definition at line 127 of file XYIncrementalDelaunay.h.

127{ return _constraints; }

Referenced by XYFrontalDelaunayGenerator::advanceFront(), and XYFrontalDelaunayGenerator::recordSplitBoundaryIds().

◆ containsPoint()

bool XYIncrementalDelaunay::containsPoint ( std::size_t  t,
const Point2D p 
) const
private
Returns
Whether a point lies inside or on the boundary of a triangle

Definition at line 110 of file XYIncrementalDelaunay.C.

111{
112 for (const auto i : make_range(3u))
113 if (moose_orient2d(xy(_triangles[t].vertices[(i + 1) % 3]),
114 xy(_triangles[t].vertices[(i + 2) % 3]),
115 coords(p)) < 0.0)
116 return false;
117 return true;
118}

Referenced by locate().

◆ getTriangles()

std::vector< XYIncrementalDelaunay::Triangle > XYIncrementalDelaunay::getTriangles ( ) const
Returns
The triangles covering the convex hull of the points, counter-clockwise, ordered so that the same input always gives the same list. The neighbor entries index into this same list and are invalid_index on the hull, where no triangle lies across the edge.

Definition at line 616 of file XYIncrementalDelaunay.C.

617{
618 std::vector<std::size_t> position(_triangles.size(), invalid_index);
619 std::size_t count = 0;
620 for (const auto t : index_range(_triangles))
621 if (!isBounding(_triangles[t].vertices[0]) && !isBounding(_triangles[t].vertices[1]) &&
622 !isBounding(_triangles[t].vertices[2]))
623 position[t] = count++;
624
625 std::vector<Triangle> triangles(count);
626 for (const auto t : index_range(_triangles))
627 {
628 if (position[t] == invalid_index)
629 continue;
630
631 auto & out = triangles[position[t]];
632 for (const auto i : make_range(3u))
633 {
634 out.vertices[i] = toCaller(_triangles[t].vertices[i]);
635 const auto n = _triangles[t].neighbors[i];
636 out.neighbors[i] = n == invalid_index ? invalid_index : position[n];
637 }
638 }
639 return triangles;
640}
static bool isBounding(std::size_t v)
std::size_t toCaller(std::size_t v) const
OStreamProxy out(std::cout)

Referenced by XYFrontalDelaunayGenerator::advanceFront(), and XYFrontalDelaunayGenerator::buildTriangleMesh().

◆ growCavity()

void XYIncrementalDelaunay::growCavity ( std::size_t  seed,
std::size_t  v_new,
std::set< std::size_t > &  cavity 
) const
private

Collects the triangles of the triangulation built so far that have to make way for a new vertex, the Delaunay cavity of its insertion, starting from the triangle it falls in and spreading to every neighbor whose circumcircle contains it.

Constrained segments stop the spread. A neighbor is also taken in when the shared edge would otherwise produce a triangle of zero or negative area, which is what keeps the cavity star shaped about the vertex.

Parameters
seedThe triangle the new vertex falls in
v_newThe internal vertex id of the new vertex
cavityFilled with the triangles to remove

Definition at line 185 of file XYIncrementalDelaunay.C.

188{
189 cavity.insert(seed);
190 std::vector<std::size_t> pending{seed};
191
192 while (!pending.empty())
193 {
194 const auto t = pending.back();
195 pending.pop_back();
196
197 for (const auto i : make_range(3u))
198 {
199 const auto v0 = _triangles[t].vertices[(i + 1) % 3];
200 const auto v1 = _triangles[t].vertices[(i + 2) % 3];
201 const auto side = moose_orient2d(xy(v0), xy(v1), xy(v_new));
202
203 if (isConstrainedEdge(v0, v1))
204 {
205 if (side <= 0.0)
206 mooseError("XYIncrementalDelaunay: the point (",
207 _vertices[v_new].x,
208 ", ",
209 _vertices[v_new].y,
210 ") is on or beyond constrained segment (",
211 toCaller(v0),
212 ", ",
213 toCaller(v1),
214 "), so it lies outside the region that segment bounds.");
215 continue;
216 }
217
218 const auto n = _triangles[t].neighbors[i];
219 if (n == invalid_index || cavity.count(n) > 0)
220 continue;
221
222 // Taking the neighbor in when the shared edge would give a triangle of zero or negative area
223 // is what keeps the cavity star shaped about the new vertex; with exact predicates that only
224 // happens when the new vertex falls exactly on the edge.
225 if (side <= 0.0 || moose_incircle(xy(_triangles[n].vertices[0]),
226 xy(_triangles[n].vertices[1]),
227 xy(_triangles[n].vertices[2]),
228 xy(v_new)) > 0.0)
229 {
230 cavity.insert(n);
231 pending.push_back(n);
232 }
233 }
234 }
235}
void mooseError(Args &&... args)
Emit an error message with the given stringified, concatenated args and terminate the application.
Definition MooseError.h:311

Referenced by insertPoint().

◆ initialize()

void XYIncrementalDelaunay::initialize ( const std::vector< Point2D > &  points,
const std::vector< Segment > &  segments 
)

Triangulates points and recovers every entry of segments as an edge of the result.

Points are inserted in the order given, so vertex id i refers to points[i]. Any later insertPoint() must lie within the region this call encloses, which is the bounding box of points grown by a large multiple of its own size; the interior points a frontal advance adds always do.

Parameters
pointsThe points to triangulate, which must not contain two identical points
segmentsThe segments the triangulation is required to contain, as vertex id pairs in either order

Definition at line 346 of file XYIncrementalDelaunay.C.

348{
349 Moose::initPredicates();
350
351 if (points.empty())
352 mooseError("XYIncrementalDelaunay: initialize() needs at least one point.");
353
354 _vertices.clear();
355 _triangles.clear();
356 _vertex_triangle.clear();
357 _constraints.clear();
358
359 auto x_min = points.front().x;
360 auto x_max = x_min;
361 auto y_min = points.front().y;
362 auto y_max = y_min;
363 for (const auto & p : points)
364 {
365 x_min = std::min(x_min, p.x);
366 x_max = std::max(x_max, p.x);
367 y_min = std::min(y_min, p.y);
368 y_max = std::max(y_max, p.y);
369 }
370
371 // A triangle far enough outside the points that all of them are strictly inside it. Inserting a
372 // point deletes the Delaunay cavity of that insertion, the triangles whose circumcircle contains
373 // the point (see growCavity()), and fans the hole they leave out from the point. With every
374 // point inside the bounding triangle that hole is always a closed polygon, so no insertion ever
375 // has to extend the convex hull of the triangulation.
376 const auto reach = _bounding_reach * std::max({x_max - x_min, y_max - y_min, 1.0});
377 const auto x_mid = 0.5 * (x_min + x_max);
378 const auto y_mid = 0.5 * (y_min + y_max);
379 _vertices.push_back({x_mid - reach, y_mid - reach});
380 _vertices.push_back({x_mid + reach, y_mid - reach});
381 _vertices.push_back({x_mid, y_mid + reach});
382 _vertex_triangle.assign(_num_bounding, std::size_t(0));
383
384 Triangle bounding;
385 bounding.vertices = {0, 1, 2};
386 bounding.neighbors = {invalid_index, invalid_index, invalid_index};
387 _triangles.push_back(bounding);
388 _last_triangle = 0;
389
390 for (const auto i : index_range(points))
391 {
392 const auto id = insertPoint(points[i]);
393 if (id != i)
394 mooseError("XYIncrementalDelaunay: points ",
395 id,
396 " and ",
397 i,
398 " are the same point; initialize() needs the points to be distinct, because the "
399 "constrained segments refer to them by position.");
400 }
401
402 for (const auto & [v0, v1] : segments)
403 insertSegment(v0, v1);
404}
Triangle geometry helper.
Definition Triangle.h:22
std::size_t _last_triangle
A triangle the last insertion produced, which is where the next point walk starts.
static constexpr std::size_t _num_bounding
The number of bounding triangle vertices padding the front of the vertex list.
std::size_t insertPoint(const Point2D &p)
Inserts a point, restoring the constrained Delaunay property around it.
static constexpr double _bounding_reach
How far the bounding triangle reaches beyond the points, as a multiple of their extent.
void insertSegment(std::size_t v0, std::size_t v1)
Makes the segment between two vertices an edge of the triangulation and records it as constrained,...
std::vector< std::size_t > _vertex_triangle
One triangle touching each vertex of _vertices, which is where a walk around it starts.

◆ insertPoint()

std::size_t XYIncrementalDelaunay::insertPoint ( const Point2D p)

Inserts a point, restoring the constrained Delaunay property around it.

Parameters
pThe point to insert
Returns
The vertex id of p. A point identical to an existing vertex is not inserted a second time; the id of that vertex is returned and the triangulation is left alone. A point that falls exactly on a constrained segment splits it, replacing that segment by the two halves the point divides it into.

Definition at line 407 of file XYIncrementalDelaunay.C.

408{
409 Moose::initPredicates();
410
411 if (_triangles.empty())
412 mooseError("XYIncrementalDelaunay: initialize() has to run before a point can be inserted.");
413
414 const auto seed = locate(p);
415 if (seed == invalid_index)
416 mooseError("XYIncrementalDelaunay: the point (",
417 p.x,
418 ", ",
419 p.y,
420 ") is outside the region initialize() enclosed, so it cannot be inserted.");
421
422 // Two points count as the same point only when their coordinates agree exactly, which is the
423 // criterion the exact predicates use as well.
424 for (const auto v : _triangles[seed].vertices)
425 if (_vertices[v].x == p.x && _vertices[v].y == p.y)
426 {
427 if (isBounding(v))
428 mooseError("XYIncrementalDelaunay: the point (",
429 p.x,
430 ", ",
431 p.y,
432 ") is a vertex of the bounding triangle, so it is far outside the region "
433 "initialize() enclosed.");
434 return toCaller(v);
435 }
436
437 // A point landing on a constrained segment divides it, because the Delaunay cavity may not cross
438 // a constrained segment and a segment with a vertex on it can no longer be a single edge.
440 for (const auto i : make_range(3u))
441 {
442 const auto v0 = _triangles[seed].vertices[(i + 1) % 3];
443 const auto v1 = _triangles[seed].vertices[(i + 2) % 3];
444 if (moose_orient2d(xy(v0), xy(v1), coords(p)) == 0.0 && isConstrainedEdge(v0, v1))
445 {
447 _constraints.erase(split);
448 break;
449 }
450 }
451
452 _vertices.push_back(p);
454 const auto v_new = _vertices.size() - 1;
455
456 std::set<std::size_t> cavity;
457 growCavity(seed, v_new, cavity);
458
459 std::vector<std::array<std::size_t, 3>> added;
460 for (const auto t : cavity)
461 for (const auto i : make_range(3u))
462 {
463 const auto n = _triangles[t].neighbors[i];
464 if (n == invalid_index || cavity.count(n) == 0)
465 added.push_back(
466 {_triangles[t].vertices[(i + 1) % 3], _triangles[t].vertices[(i + 2) % 3], v_new});
467 }
468
469 mooseAssert(added.size() == cavity.size() + 2,
470 "The Delaunay cavity of an insertion is a disc, whose boundary has two more edges "
471 "than the disc has triangles");
472
473 const std::vector<std::size_t> removed(cavity.begin(), cavity.end());
474 _last_triangle = retriangulate(removed, added).front();
475
476 const auto id = toCaller(v_new);
477 if (split.first != invalid_index)
478 {
479 insertSegment(split.first, id);
480 insertSegment(id, split.second);
481 }
482 return id;
483}
for(PetscInt i=0;i< nvars;++i)
std::pair< std::size_t, std::size_t > Segment
A constrained segment, held as a vertex id pair with the smaller id first.
std::size_t locate(const Point2D &p) const
Finds the triangle a point falls in by walking from the triangle the last insertion produced,...
void growCavity(std::size_t seed, std::size_t v_new, std::set< std::size_t > &cavity) const
Collects the triangles of the triangulation built so far that have to make way for a new vertex,...
std::vector< std::size_t > retriangulate(const std::vector< std::size_t > &removed, const std::vector< std::array< std::size_t, 3 > > &added)
Swaps one triangulation of a region for another, reusing the slots of the triangles it removes and re...
tbb::split split

Referenced by XYFrontalDelaunayGenerator::advanceFront(), and initialize().

◆ insertSegment()

void XYIncrementalDelaunay::insertSegment ( std::size_t  v0,
std::size_t  v1 
)

Makes the segment between two vertices an edge of the triangulation and records it as constrained, so that later insertions neither remove nor cross it.

Doing this to a segment that is already an edge only records the constraint.

A segment that runs exactly through a third vertex cannot be a single edge, and is recorded as the pieces that vertex divides it into rather than as itself.

Parameters
v0The vertex id of one end of the segment
v1The vertex id of the other end of the segment

Definition at line 486 of file XYIncrementalDelaunay.C.

487{
488 Moose::initPredicates();
489
490 if (v0 == v1)
492 "XYIncrementalDelaunay: a constrained segment needs two different vertices, but both "
493 "ends of this one are vertex ",
494 v0,
495 ".");
496
497 const auto v_from = toInternal(v0);
498 const auto v_to = toInternal(v1);
499
500 // Turn around v_from until the triangle the segment leaves through is found. The segment is
501 // already an edge if v_to is met on the way, and needs splitting if some other vertex is.
502 auto entered = invalid_index;
503 auto right = invalid_index;
504 auto left = invalid_index;
505 const auto start = _vertex_triangle[v_from];
506 mooseAssert(start < _triangles.size(), "Every vertex records a triangle it belongs to");
507
508 auto current = start;
509 do
510 {
511 const auto i = localVertexIndex(current, v_from);
512 const auto next_v = _triangles[current].vertices[(i + 1) % 3];
513 const auto far_v = _triangles[current].vertices[(i + 2) % 3];
514
515 if (next_v == v_to)
516 {
517 _constraints.insert(makeSegment(v0, v1));
518 return;
519 }
520
521 const auto next_side = moose_orient2d(xy(v_from), xy(next_v), xy(v_to));
522
523 // Nothing has been changed yet, so recovering the two halves from scratch is safe.
524 if (next_side == 0.0 && isStrictlyBetween(v_from, next_v, v_to))
525 {
526 insertSegment(v0, toCaller(next_v));
527 insertSegment(toCaller(next_v), v1);
528 return;
529 }
530
531 if (next_side > 0.0 && moose_orient2d(xy(v_from), xy(far_v), xy(v_to)) < 0.0)
532 {
533 entered = current;
534 right = next_v;
535 left = far_v;
536 break;
537 }
538
539 current = _triangles[current].neighbors[(i + 1) % 3];
540 } while (current != start && current != invalid_index);
541
542 if (entered == invalid_index)
543 mooseError("XYIncrementalDelaunay: no triangle around vertex ",
544 v0,
545 " is entered by the segment to vertex ",
546 v1,
547 ".");
548
549 // Follow the segment across the triangulation, keeping the vertices it passes on either side.
550 std::vector<std::size_t> crossed{entered};
551 std::vector<std::size_t> right_chain{right};
552 std::vector<std::size_t> left_chain{left};
553
554 while (true)
555 {
556 if (isConstrainedEdge(right, left))
557 mooseError("XYIncrementalDelaunay: the segment from vertex ",
558 v0,
559 " to vertex ",
560 v1,
561 " crosses constrained segment (",
562 toCaller(right),
563 ", ",
564 toCaller(left),
565 "); constrained segments may only meet at their ends.");
566
567 const auto edge = makeSegment(right, left);
568 const auto next_t = _triangles[crossed.back()].neighbors[localEdgeIndex(crossed.back(), edge)];
569 if (next_t == invalid_index)
570 mooseError("XYIncrementalDelaunay: the segment from vertex ",
571 v0,
572 " to vertex ",
573 v1,
574 " leaves the triangulation before it reaches its end.");
575
576 const auto apex = _triangles[next_t].vertices[localEdgeIndex(next_t, edge)];
577 if (apex == v_to)
578 {
579 crossed.push_back(next_t);
580 break;
581 }
582
583 const auto side = moose_orient2d(xy(v_from), xy(v_to), xy(apex));
584 if (side == 0.0)
585 {
586 insertSegment(v0, toCaller(apex));
587 insertSegment(toCaller(apex), v1);
588 return;
589 }
590
591 crossed.push_back(next_t);
592 if (side > 0.0)
593 {
594 left_chain.push_back(apex);
595 left = apex;
596 }
597 else
598 {
599 right_chain.push_back(apex);
600 right = apex;
601 }
602 }
603
604 // The segment leaves a polygon on either side of itself, every vertex of which the segment sees,
605 // so triangulating each on its own restores the Delaunay property everywhere.
606 std::vector<std::array<std::size_t, 3>> added;
607 const std::vector<std::size_t> left_polygon(left_chain.rbegin(), left_chain.rend());
608 triangulatePseudopolygon(v_from, v_to, left_polygon, 0, left_polygon.size(), added);
609 triangulatePseudopolygon(v_to, v_from, right_chain, 0, right_chain.size(), added);
610
611 retriangulate(crossed, added);
612 _constraints.insert(makeSegment(v0, v1));
613}
void triangulatePseudopolygon(std::size_t v_start, std::size_t v_end, const std::vector< std::size_t > &chain, std::size_t first, std::size_t last, std::vector< std::array< std::size_t, 3 > > &triangles) const
Triangulates a polygon whose vertices are all visible from one of its edges, which is the shape a rec...
unsigned int localVertexIndex(std::size_t t, std::size_t v) const
bool isStrictlyBetween(std::size_t v_first, std::size_t v_mid, std::size_t v_last) const
unsigned int localEdgeIndex(std::size_t t, const Segment &edge) const

Referenced by initialize(), and insertSegment().

◆ isBounding()

static bool XYIncrementalDelaunay::isBounding ( std::size_t  v)
inlinestaticprivate
Returns
Whether an internal vertex belongs to the bounding triangle rather than to the caller

Definition at line 158 of file XYIncrementalDelaunay.h.

158{ return v < _num_bounding; }

Referenced by getTriangles(), insertPoint(), isConstrainedEdge(), toCaller(), and vertexName().

◆ isConstrainedEdge()

bool XYIncrementalDelaunay::isConstrainedEdge ( std::size_t  v0,
std::size_t  v1 
) const
private
Returns
Whether the edge between two internal vertices is a constrained segment

Definition at line 77 of file XYIncrementalDelaunay.C.

78{
79 if (isBounding(v0) || isBounding(v1))
80 return false;
81 return _constraints.count(makeSegment(toCaller(v0), toCaller(v1))) > 0;
82}

Referenced by checkInvariants(), growCavity(), insertPoint(), and insertSegment().

◆ isConstrainedSegment()

bool XYIncrementalDelaunay::isConstrainedSegment ( std::size_t  v0,
std::size_t  v1 
) const
Returns
Whether the segment between two vertices is constrained, given its ends in either order

Definition at line 71 of file XYIncrementalDelaunay.C.

72{
73 return _constraints.count(makeSegment(v0, v1)) > 0;
74}

◆ isStrictlyBetween()

bool XYIncrementalDelaunay::isStrictlyBetween ( std::size_t  v_first,
std::size_t  v_mid,
std::size_t  v_last 
) const
private
Returns
Whether v_mid lies strictly between v_first and v_last, which must all be collinear

Definition at line 121 of file XYIncrementalDelaunay.C.

124{
125 mooseAssert(moose_orient2d(xy(v_first), xy(v_mid), xy(v_last)) == 0.0,
126 "The three vertices have to be collinear for a coordinate comparison to order them");
127
128 // The three are collinear, so whichever coordinate separates the two ends orders all three.
129 const auto & first = _vertices[v_first];
130 const auto & mid = _vertices[v_mid];
131 const auto & last = _vertices[v_last];
132 if (first.x != last.x)
133 return (first.x < mid.x && mid.x < last.x) || (last.x < mid.x && mid.x < first.x);
134 return (first.y < mid.y && mid.y < last.y) || (last.y < mid.y && mid.y < first.y);
135}

Referenced by insertSegment().

◆ localEdgeIndex()

unsigned int XYIncrementalDelaunay::localEdgeIndex ( std::size_t  t,
const Segment edge 
) const
private
Returns
The position within a triangle of the edge opposite the given edge's vertex

Definition at line 94 of file XYIncrementalDelaunay.C.

95{
96 for (const auto i : make_range(3u))
97 if (makeSegment(_triangles[t].vertices[(i + 1) % 3], _triangles[t].vertices[(i + 2) % 3]) ==
98 edge)
99 return i;
100 mooseError("XYIncrementalDelaunay: triangle ",
101 t,
102 " does not have an edge between vertices ",
103 edge.first,
104 " and ",
105 edge.second,
106 ".");
107}

Referenced by insertSegment(), and retriangulate().

◆ localVertexIndex()

unsigned int XYIncrementalDelaunay::localVertexIndex ( std::size_t  t,
std::size_t  v 
) const
private
Returns
The position of an internal vertex within a triangle

Definition at line 85 of file XYIncrementalDelaunay.C.

86{
87 for (const auto i : make_range(3u))
88 if (_triangles[t].vertices[i] == v)
89 return i;
90 mooseError("XYIncrementalDelaunay: triangle ", t, " does not have vertex ", v, ".");
91}

Referenced by insertSegment().

◆ locate()

std::size_t XYIncrementalDelaunay::locate ( const Point2D p) const
private

Finds the triangle a point falls in by walking from the triangle the last insertion produced, which keeps the search short when successive points are close together.

Falls back to a scan of every triangle if the walk leaves the triangulation or fails to settle.

Returns
The triangle containing p, or invalid_index if p is outside the triangulation

Definition at line 146 of file XYIncrementalDelaunay.C.

147{
148 // start from the last insertion if existing
149 auto current = _last_triangle < _triangles.size() ? _last_triangle : std::size_t(0);
150
151 // The walk always arrives in a Delaunay triangulation, but a constrained one can in principle
152 // send it round in a circle, so it gives up and scans rather than spin.
153 auto steps_left = 2 * _triangles.size() + 8;
154 while (steps_left > 0)
155 {
156 --steps_left;
157
158 auto next = invalid_index;
159 bool inside = true;
160 for (const auto i : make_range(3u))
161 if (moose_orient2d(xy(_triangles[current].vertices[(i + 1) % 3]),
162 xy(_triangles[current].vertices[(i + 2) % 3]),
163 coords(p)) < 0.0)
164 {
165 inside = false;
166 next = _triangles[current].neighbors[i];
167 break;
168 }
169
170 if (inside)
171 return current;
172 if (next == invalid_index)
173 break;
174 current = next;
175 }
176
177 // search all triangles if starting from the last inserted one did not succeed
178 for (const auto t : index_range(_triangles))
179 if (containsPoint(t, p))
180 return t;
181 return invalid_index;
182}
bool containsPoint(std::size_t t, const Point2D &p) const

Referenced by insertPoint().

◆ makeSegment()

XYIncrementalDelaunay::Segment XYIncrementalDelaunay::makeSegment ( std::size_t  v0,
std::size_t  v1 
)
static
Returns
A vertex id pair with the smaller id first, the form constrained segments are held in

Definition at line 65 of file XYIncrementalDelaunay.C.

66{
67 return {std::min(v0, v1), std::max(v0, v1)};
68}

Referenced by XYFrontalDelaunayGenerator::appendLoop(), XYFrontalDelaunayGenerator::buildTriangleMesh(), checkInvariants(), insertPoint(), insertSegment(), isConstrainedEdge(), isConstrainedSegment(), localEdgeIndex(), XYFrontalDelaunayGenerator::recordSplitBoundaryIds(), and retriangulate().

◆ numPoints()

std::size_t XYIncrementalDelaunay::numPoints ( ) const
Returns
The number of points in the triangulation, whose vertex ids run from 0 to this minus 1

Definition at line 34 of file XYIncrementalDelaunay.C.

35{
36 return _vertices.empty() ? 0 : _vertices.size() - _num_bounding;
37}

Referenced by XYFrontalDelaunayGenerator::advanceFront(), and toInternal().

◆ point()

const XYIncrementalDelaunay::Point2D & XYIncrementalDelaunay::point ( std::size_t  id) const

◆ retriangulate()

std::vector< std::size_t > XYIncrementalDelaunay::retriangulate ( const std::vector< std::size_t > &  removed,
const std::vector< std::array< std::size_t, 3 > > &  added 
)
private

Swaps one triangulation of a region for another, reusing the slots of the triangles it removes and rebuilding the neighbor entries both inside the region and along its boundary.

Parameters
removedThe triangles covering the region, in any order
addedThe vertices of the triangles to cover it with, counter-clockwise
Returns
The slots the added triangles were placed in

Definition at line 238 of file XYIncrementalDelaunay.C.

240{
241 mooseAssert(added.size() >= removed.size(),
242 "A region takes at least as many triangles to cover as it is emptied of");
243
244 const std::set<std::size_t> emptied(removed.begin(), removed.end());
245
246 // What lies outside the region being replaced, keyed on the edge it is joined along.
247 std::map<Segment, std::size_t> outside;
248 for (const auto t : emptied)
249 for (const auto i : make_range(3u))
250 {
251 const auto n = _triangles[t].neighbors[i];
252 if (n != invalid_index && emptied.count(n) == 0)
253 outside.emplace(
254 makeSegment(_triangles[t].vertices[(i + 1) % 3], _triangles[t].vertices[(i + 2) % 3]),
255 n);
256 }
257
258 // Filling the emptied slots in id order, rather than in the order the region happened to be
259 // walked, is what makes the triangle numbering repeatable from one run to the next.
260 std::vector<std::size_t> slots(emptied.begin(), emptied.end());
261 while (slots.size() < added.size())
262 {
263 slots.push_back(_triangles.size());
264 _triangles.emplace_back();
265 }
266
267 for (const auto i : index_range(added))
268 {
269 auto & triangle = _triangles[slots[i]];
270 triangle.vertices = added[i];
271 triangle.neighbors = {invalid_index, invalid_index, invalid_index};
272 for (const auto v : triangle.vertices)
273 _vertex_triangle[v] = slots[i];
274 }
275
276 // An edge two new triangles share joins them to each other; one only a single new triangle has is
277 // on the boundary of the region and joins it to what was already outside.
278 std::map<Segment, std::pair<std::size_t, unsigned int>> open_edges;
279 for (const auto i : index_range(added))
280 {
281 const auto t = slots[i];
282 for (const auto e : make_range(3u))
283 {
284 const auto edge =
285 makeSegment(_triangles[t].vertices[(e + 1) % 3], _triangles[t].vertices[(e + 2) % 3]);
286 const auto it = open_edges.find(edge);
287 if (it == open_edges.end())
288 open_edges.emplace(edge, std::make_pair(t, e));
289 else
290 {
291 const auto [other_t, other_e] = it->second;
292 _triangles[t].neighbors[e] = other_t;
293 _triangles[other_t].neighbors[other_e] = t;
294 open_edges.erase(it);
295 }
296 }
297 }
298
299 for (const auto & [edge, entry] : open_edges)
300 {
301 const auto [t, e] = entry;
302 const auto it = outside.find(edge);
303 // true outer boundary (for now)
304 if (it == outside.end())
305 continue;
306 // boundary with 'outside' the retriangulated area
307 _triangles[t].neighbors[e] = it->second;
308 _triangles[it->second].neighbors[localEdgeIndex(it->second, edge)] = t;
309 }
310
311 return slots;
312}

Referenced by insertSegment().

◆ toCaller()

std::size_t XYIncrementalDelaunay::toCaller ( std::size_t  v) const
private
Returns
The caller vertex id of an internal vertex id, which must not be a bounding vertex

Definition at line 52 of file XYIncrementalDelaunay.C.

53{
54 mooseAssert(!isBounding(v), "A bounding triangle vertex has no caller vertex id");
55 return v - _num_bounding;
56}

Referenced by getTriangles(), growCavity(), insertPoint(), insertSegment(), isConstrainedEdge(), and vertexName().

◆ toInternal()

std::size_t XYIncrementalDelaunay::toInternal ( std::size_t  id) const
private
Returns
The internal vertex id of a caller vertex id, which must be in range

Definition at line 40 of file XYIncrementalDelaunay.C.

41{
42 if (id >= numPoints())
43 mooseError("XYIncrementalDelaunay: vertex id ",
44 id,
45 " does not exist; the triangulation has ",
46 numPoints(),
47 " points.");
48 return id + _num_bounding;
49}
std::size_t numPoints() const

Referenced by checkInvariants(), insertSegment(), and point().

◆ triangulatePseudopolygon()

void XYIncrementalDelaunay::triangulatePseudopolygon ( std::size_t  v_start,
std::size_t  v_end,
const std::vector< std::size_t > &  chain,
std::size_t  first,
std::size_t  last,
std::vector< std::array< std::size_t, 3 > > &  triangles 
) const
private

Triangulates a polygon whose vertices are all visible from one of its edges, which is the shape a recovered segment leaves on either side of itself.

The polygon runs counter-clockwise from v_start to v_end and back along chain[first] to chain[last - 1]. Splitting it at the vertex whose circumcircle holds no other and recursing on the two halves makes it Delaunay.

Parameters
v_startThe internal vertex at the start of the edge every vertex sees
v_endThe internal vertex at the end of that edge
chainThe internal vertices of the rest of the polygon, in counter-clockwise order
firstThe position in chain where this polygon begins
lastOne past the position in chain where this polygon ends
trianglesAppended with the vertices of the triangles covering the polygon

Definition at line 315 of file XYIncrementalDelaunay.C.

322{
323 // An empty chain leaves nothing but the edge from v_start to v_end itself, so there is no
324 // polygon to cover
325 if (first == last)
326 return;
327
328 // The triangle the edge (v_start, v_end) belongs to in a Delaunay triangulation of the polygon
329 // is the one whose apex leaves no other chain vertex inside its circumcircle. Every chain vertex
330 // sees the edge, so whenever a vertex lies inside the circumcircle of the current apex, that
331 // vertex takes over as the apex; the one left standing has an empty circumcircle
332 auto best = first;
333 for (const auto i : make_range(first + 1, last))
334 if (moose_incircle(xy(v_start), xy(v_end), xy(chain[best]), xy(chain[i])) > 0.0)
335 best = i;
336
337 // That triangle splits the polygon in two: the chain vertices before the apex form a polygon
338 // seen from the new edge (apex, v_end), and those after it a polygon seen from (v_start, apex),
339 // each of which is covered the same way
340 triangles.push_back({v_start, v_end, chain[best]});
341 triangulatePseudopolygon(chain[best], v_end, chain, first, best, triangles);
342 triangulatePseudopolygon(v_start, chain[best], chain, best + 1, last, triangles);
343}

Referenced by insertSegment(), and triangulatePseudopolygon().

◆ vertexName()

std::string XYIncrementalDelaunay::vertexName ( std::size_t  v) const
private
Returns
A name for an internal vertex, for the messages the check methods return

Definition at line 138 of file XYIncrementalDelaunay.C.

139{
140 if (isBounding(v))
141 return "bounding vertex " + std::to_string(v);
142 return "point " + std::to_string(toCaller(v));
143}

Referenced by checkEmptyCircumcircle(), and checkInvariants().

◆ xy()

const double * XYIncrementalDelaunay::xy ( std::size_t  v) const
inlineprivate
Returns
The two coordinates of an internal vertex, in the layout the exact predicates take

Definition at line 155 of file XYIncrementalDelaunay.h.

155{ return &_vertices[v].x; }

Referenced by checkEmptyCircumcircle(), checkInvariants(), containsPoint(), growCavity(), insertPoint(), insertSegment(), isStrictlyBetween(), locate(), and triangulatePseudopolygon().

Member Data Documentation

◆ _bounding_reach

constexpr double XYIncrementalDelaunay::_bounding_reach = 1000.0
staticconstexprprivate

How far the bounding triangle reaches beyond the points, as a multiple of their extent.

Definition at line 152 of file XYIncrementalDelaunay.h.

Referenced by initialize().

◆ _constraints

std::set<Segment> XYIncrementalDelaunay::_constraints
private

The constrained segments, in caller vertex ids with the smaller id first.

Definition at line 243 of file XYIncrementalDelaunay.h.

Referenced by checkInvariants(), constrainedSegments(), initialize(), insertPoint(), insertSegment(), isConstrainedEdge(), and isConstrainedSegment().

◆ _last_triangle

std::size_t XYIncrementalDelaunay::_last_triangle = invalid_index
private

A triangle the last insertion produced, which is where the next point walk starts.

Definition at line 246 of file XYIncrementalDelaunay.h.

Referenced by initialize(), and locate().

◆ _num_bounding

constexpr std::size_t XYIncrementalDelaunay::_num_bounding = 3
staticconstexprprivate

The number of bounding triangle vertices padding the front of the vertex list.

Definition at line 149 of file XYIncrementalDelaunay.h.

Referenced by initialize(), isBounding(), numPoints(), toCaller(), and toInternal().

◆ _triangles

std::vector<Triangle> XYIncrementalDelaunay::_triangles
private

◆ _vertex_triangle

std::vector<std::size_t> XYIncrementalDelaunay::_vertex_triangle
private

One triangle touching each vertex of _vertices, which is where a walk around it starts.

Definition at line 240 of file XYIncrementalDelaunay.h.

Referenced by initialize(), insertPoint(), insertSegment(), and retriangulate().

◆ _vertices

std::vector<Point2D> XYIncrementalDelaunay::_vertices
private

The bounding triangle vertices followed by the caller's points.

Definition at line 234 of file XYIncrementalDelaunay.h.

Referenced by checkEmptyCircumcircle(), growCavity(), initialize(), insertPoint(), isStrictlyBetween(), numPoints(), point(), and xy().

◆ invalid_index

constexpr std::size_t XYIncrementalDelaunay::invalid_index = std::numeric_limits<std::size_t>::max()
staticconstexpr

The documentation for this class was generated from the following files: