From 491cbee81c83234809dd5429b80cca254f70535a Mon Sep 17 00:00:00 2001 From: "Benjamin A. Beasley" Date: Sun, 5 Jul 2026 07:31:23 +0100 Subject: [PATCH] Update earcut.hpp to version 3.2.3 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit https://github.com/mapbox/earcut.hpp/raw/refs/tags/v3.2.3/include/mapbox/earcut.hpp https://github.com/mapbox/earcut.hpp/releases/tag/v3.2.3 “Catches the C++ port up to earcut.js v3.2.3 (from the v2.2.x baseline), with faster triangulation, optional Delaunay refinement, and a fully modernized build.” --- include/mapbox/earcut.hpp | 900 +++++++++++++++++++++++++++----------- 1 file changed, 647 insertions(+), 253 deletions(-) diff --git a/include/mapbox/earcut.hpp b/include/mapbox/earcut.hpp index b98508f..97e7677 100644 --- a/include/mapbox/earcut.hpp +++ b/include/mapbox/earcut.hpp @@ -4,6 +4,7 @@ #include #include #include +#include #include #include #include @@ -13,12 +14,12 @@ namespace mapbox { namespace util { -template struct nth { - inline static typename std::tuple_element::type - get(const T& t) { return std::get(t); }; +template +struct nth { + inline static typename std::tuple_element::type get(const T& t) { return std::get(t); }; }; -} +} // namespace util namespace detail { @@ -33,13 +34,15 @@ class Earcut { private: struct Node { - Node(N index, double x_, double y_) : i(index), x(x_), y(y_) {} + // i is a (bits(N)-1)-wide field packed alongside the 1-bit steiner flag; mask index to that + // width so it fits without a narrowing warning (a no-op for any real vertex index). + Node(N index, double x_, double y_) + : x(x_), y(y_), i(index & ((N(1) << (sizeof(N) * 8 - 1)) - 1)), steiner(0) {} Node(const Node&) = delete; Node& operator=(const Node&) = delete; Node(Node&&) = delete; Node& operator=(Node&&) = delete; - const N i; const double x; const double y; @@ -50,24 +53,70 @@ class Earcut { // z-order curve value int32_t z = 0; + // original index in polygon + const N i : (sizeof(N) * 8 - 1); + + // indicates whether this is a steiner point + N steiner : 1; + // previous and next nodes in z-order Node* prevZ = nullptr; Node* nextZ = nullptr; + }; - // indicates whether this is a steiner point - bool steiner = false; + // Cache-optimized Triangle structure for repeated geometric tests + struct Triangle { + const double ax, ay; + const double bx, by; + const double cx, cy; + // triangle bounding box, used to cheaply reject most candidate points before the + // full point-in-triangle test (which is 6 multiplies) + const double minX, minY, maxX, maxY; + + Triangle(const Node* a, const Node* b, const Node* c) + : ax(a->x), + ay(a->y), + bx(b->x), + by(b->y), + cx(c->x), + cy(c->y), + minX(std::min(ax, std::min(bx, cx))), + minY(std::min(ay, std::min(by, cy))), + maxX(std::max(ax, std::max(bx, cx))), + maxY(std::max(ay, std::max(by, cy))) {} + + inline double area() const { return (by - ay) * (cx - bx) - (bx - ax) * (cy - by); } + + inline bool inBBox(double px, double py) const { return px >= minX && px <= maxX && py >= minY && py <= maxY; } + + inline bool containsPoint(double px, double py) const { + return (cx - px) * (ay - py) >= (ax - px) * (cy - py) && (ax - px) * (by - py) >= (bx - px) * (ay - py) && + (bx - px) * (cy - py) >= (cx - px) * (by - py); + } + + // as containsPoint, but false when the point coincides with the triangle's first vertex (a) + inline bool containsPointExceptFirst(double px, double py) const { + return !(ax == px && ay == py) && containsPoint(px, py); + } }; - template Node* linkedList(const Ring& points, const bool clockwise); + template + Node* linkedList(const Ring& points, const bool clockwise); Node* filterPoints(Node* start, Node* end = nullptr); - void earcutLinked(Node* ear, int pass = 0); + void earcutLinked(Node* ear); bool isEar(Node* ear); bool isEarHashed(Node* ear); Node* cureLocalIntersections(Node* start); void splitEarcut(Node* start); - template Node* eliminateHoles(const Polygon& points, Node* outerNode); + template + Node* eliminateHoles(const Polygon& points, Node* outerNode); Node* eliminateHole(Node* hole, Node* outerNode); Node* findHoleBridge(Node* hole, Node* outerNode); + void buildBlockIndex(std::size_t maxNodes, std::size_t numHoles); + void indexSegment(Node* head, Node* stop); + void growBlock(Node* head, Node* tail); + Node* liveBlockHead(std::size_t b); + Node* liveBlockStop(std::size_t b); bool sectorContainsSector(const Node* m, const Node* p); void indexCurve(Node* start); Node* sortLinked(Node* list); @@ -77,17 +126,20 @@ class Earcut { bool isValidDiagonal(Node* a, Node* b); double area(const Node* p, const Node* q, const Node* r) const; bool equals(const Node* p1, const Node* p2); - bool intersects(const Node* p1, const Node* q1, const Node* p2, const Node* q2); + bool intersects(const Node* p1, const Node* q1, const Node* p2, const Node* q2, bool includeBoundary = true); bool onSegment(const Node* p, const Node* q, const Node* r); - int sign(double val); bool intersectsPolygon(const Node* a, const Node* b); bool locallyInside(const Node* a, const Node* b); bool middleInside(const Node* a, const Node* b); Node* splitPolygon(Node* a, Node* b); - template Node* insertNode(std::size_t i, const Point& p, Node* last); + template + Node* insertNode(std::size_t i, const Point& p, Node* last); void removeNode(Node* p); bool hashing; + // set by filterPoints whenever it removes at least one node; read by earcutLinked's stall + // handler to decide whether another clip pass is worth attempting before the costlier stages + bool filteredOut = false; double minX, maxX; double minY, maxY; double inv_size = 0; @@ -95,46 +147,105 @@ class Earcut { template > class ObjectPool { public: - ObjectPool() { } - ObjectPool(std::size_t blockSize_) { - reset(blockSize_); - } - ~ObjectPool() { - clear(); + ObjectPool() { allocateNewBlock(256); } + ObjectPool(std::size_t blockSize_) : baseBlockSize(blockSize_) { + allocateNewBlock(std::max(blockSize_, 256)); } + ~ObjectPool() { clear(); } template T* construct(Args&&... args) { - if (currentIndex >= blockSize) { - currentBlock = alloc_traits::allocate(alloc, blockSize); - allocations.emplace_back(currentBlock); - currentIndex = 0; + // If current block is full, move to next block or allocate new one + if (currentIndex >= baseBlockSize) { + currentBlockIndex++; + if (currentBlockIndex < memoryBlocks.size()) { + // Reuse existing block + currentIndex = 0; + } else { + // Allocate a new one + allocateNewBlock(baseBlockSize); + } } - T* object = ¤tBlock[currentIndex++]; + + T* object = memoryBlocks[currentBlockIndex].get() + currentIndex; alloc_traits::construct(alloc, object, std::forward(args)...); + totalObjects++; + currentIndex++; return object; } - void reset(std::size_t newBlockSize) { - for (auto allocation : allocations) { - alloc_traits::deallocate(alloc, allocation, blockSize); + void reset() { clear(); } + void clear() { + // Destroy all objects, but keep blocks allocated for reuse + std::size_t objectsDestroyed = 0; + for (std::size_t blockIdx = 0; blockIdx < memoryBlocks.size() && objectsDestroyed < totalObjects; + ++blockIdx) { + // check if we are in the last block + std::size_t objectsInThisBlock = std::min(baseBlockSize, totalObjects - objectsDestroyed); + for (std::size_t i = 0; i < objectsInThisBlock; ++i) { + T* object = memoryBlocks[blockIdx].get() + i; + alloc_traits::destroy(alloc, object); + } + objectsDestroyed += objectsInThisBlock; } - allocations.clear(); - blockSize = std::max(1, newBlockSize); - currentBlock = nullptr; - currentIndex = blockSize; + // Reset to start from first block again + currentBlockIndex = 0; + currentIndex = 0; + totalObjects = 0; } - void clear() { reset(blockSize); } + private: - T* currentBlock = nullptr; - std::size_t currentIndex = 1; - std::size_t blockSize = 1; - std::vector allocations; Alloc alloc; typedef typename std::allocator_traits alloc_traits; + + // Custom deleter that uses the allocator + struct AllocDeleter { + Alloc alloc; + std::size_t capacity; + void operator()(T* ptr) { alloc_traits::deallocate(alloc, ptr, capacity); } + }; + + std::vector> memoryBlocks; + std::vector blockCapacities; + std::size_t currentBlockIndex = 0; + std::size_t currentIndex = 0; + std::size_t totalObjects = 0; + std::size_t baseBlockSize = 256; + + void allocateNewBlock(std::size_t capacity) { + T* rawMemory = alloc_traits::allocate(alloc, capacity); + auto newBlock = std::unique_ptr(rawMemory, AllocDeleter{alloc, capacity}); + memoryBlocks.push_back(std::move(newBlock)); + blockCapacities.push_back(capacity); + currentBlockIndex = memoryBlocks.size() - 1; + currentIndex = 0; + } }; - ObjectPool nodes; + + std::unique_ptr> nodes; + std::vector holeQueue; + // reused scratch buffer for sortLinked: materialize the z-linked ring, std::sort, relink + std::vector sortBuffer; + + // Block-bbox index for findHoleBridge (issue #183): one [minX,minY,maxX,maxY] bbox per K + // consecutive ring edges, so the leftward-ray scan can skip whole blocks in O(1) instead of + // walking the whole merged ring. Grown append-only — the outer ring seeds it, then each merged + // hole appends a segment (head node, stop node, K-blocks over head..stop); independent segments, + // not a ring tiling, since splices land mid-ring. Buffers reused/grown across calls. + // + // filterPoints only drops collinear/coincident points, so a stale bbox stays a conservative + // superset of its live edges (never a false skip); the scan skips dead nodes (p->prev->next != p) + // and lazily advances a dead head/stop. Blocks are scanned in append (not ring) order, so the + // chosen bridge can differ from the un-indexed code — a different but equally valid result. + static constexpr int32_t K = 16; // edges per block + std::vector blockBBox; // [minX,minY,maxX,maxY] per block + std::vector blockHead; // first node of each block's segment + std::vector blockStop; // node just past each block's segment (exclusive walk bound) + std::size_t numBlocks = 0; + // true only while eliminateHoles merges holes, so removeNode keeps the block index live (growBlock) + bool indexActive = false; }; -template template +template +template void Earcut::operator()(const Polygon& points) { // reset indices.clear(); @@ -152,8 +263,11 @@ void Earcut::operator()(const Polygon& points) { len += points[i].size(); } - //estimate size of nodes and indices - nodes.reset(len * 3 / 2); + // estimate size of nodes and indices + if (!nodes) { + std::size_t estimatedNodes = len * 3 / 2; + nodes = std::make_unique>(std::max(estimatedNodes, 256)); + } indices.reserve(len + points[0].size()); Node* outerNode = linkedList(points[0], true); @@ -184,13 +298,14 @@ void Earcut::operator()(const Polygon& points) { earcutLinked(outerNode); - nodes.clear(); + nodes->clear(); + holeQueue.clear(); } // create a circular doubly linked list from polygon points in the specified winding order -template template -typename Earcut::Node* -Earcut::linkedList(const Ring& points, const bool clockwise) { +template +template +typename Earcut::Node* Earcut::linkedList(const Ring& points, const bool clockwise) { using Point = typename Ring::value_type; double sum = 0; const std::size_t len = points.size(); @@ -225,26 +340,30 @@ Earcut::linkedList(const Ring& points, const bool clockwise) { return last; } -// eliminate colinear or duplicate points +// Remove collinear or coincident points; removability depends only on a node's immediate +// neighbors, so we sweep forward and re-check the predecessor after each removal. With no `end` +// we sweep the whole ring, lapping until nothing is removable (the fixpoint the clipper needs). +// With an explicit `end` we heal only the dirty window around a bridge/diagonal cut, stopping at +// `end` rather than lapping — O(window) instead of O(ring). template -typename Earcut::Node* -Earcut::filterPoints(Node* start, Node* end) { - if (!end) end = start; +typename Earcut::Node* Earcut::filterPoints(Node* start, Node* end) { + if (!start) return start; + const bool full = !end; + if (full) end = start; Node* p = start; bool again; do { again = false; - - if (!p->steiner && (equals(p, p->next) || area(p->prev, p, p->next) == 0)) { + if (p != p->next && !p->steiner && (equals(p, p->next) || area(p->prev, p, p->next) == 0)) { + if (full || p == end) end = p->prev; // pull the stop bound back past the removal + filteredOut = true; removeNode(p); - p = end = p->prev; - - if (p == p->next) break; + p = p->prev; // re-check the predecessor again = true; - - } else { + } else if (full || p != end) { p = p->next; + again = !full; // local heal: keep looping until the sweep reaches end } } while (again || p != end); @@ -253,22 +372,24 @@ Earcut::filterPoints(Node* start, Node* end) { // main ear slicing loop which triangulates a polygon (given as a linked list) template -void Earcut::earcutLinked(Node* ear, int pass) { +void Earcut::earcutLinked(Node* ear) { if (!ear) return; // interlink polygon nodes in z-order - if (!pass && hashing) indexCurve(ear); + if (hashing) indexCurve(ear); Node* stop = ear; Node* prev; Node* next; + bool cured = false; // iterate through ears, slicing them one by one while (ear->prev != ear->next) { prev = ear->prev; next = ear->next; - if (hashing ? isEarHashed(ear) : isEar(ear)) { + // reflex check is hoisted here to avoid constructing the Triangle for reflex corners + if (area(prev, ear, next) < 0 && (hashing ? isEarHashed(ear) : isEar(ear))) { // cut off the triangle indices.emplace_back(prev->i); indices.emplace_back(ear->i); @@ -276,9 +397,8 @@ void Earcut::earcutLinked(Node* ear, int pass) { removeNode(ear); - // skipping the next vertice leads to less sliver triangles - ear = next->next; - stop = next->next; + ear = next; + stop = next; continue; } @@ -287,17 +407,25 @@ void Earcut::earcutLinked(Node* ear, int pass) { // if we looped through the whole remaining polygon and can't find any more ears if (ear == stop) { - // try filtering points and slicing again - if (!pass) earcutLinked(filterPoints(ear), 1); + // try filtering collinear/coincident points and slicing again — repeat as long as + // filtering actually removes nodes, since each removal can expose new ears + filteredOut = false; + ear = filterPoints(ear); + if (filteredOut) { + stop = ear; + continue; + } - // if this didn't work, try curing all small self-intersections locally - else if (pass == 1) { - ear = cureLocalIntersections(filterPoints(ear)); - earcutLinked(ear, 2); + // filtering is exhausted: cure small local self-intersections once, then retry + if (!cured) { + ear = cureLocalIntersections(ear); + stop = ear; + cured = true; + continue; + } // as a last resort, try splitting the remaining polygon into two - } else if (pass == 2) splitEarcut(ear); - + splitEarcut(ear); break; } } @@ -310,14 +438,15 @@ bool Earcut::isEar(Node* ear) { const Node* b = ear; const Node* c = ear->next; - if (area(a, b, c) >= 0) return false; // reflex, can't be an ear + // reflex check is hoisted into the earcutLinked caller + const Triangle tri(a, b, c); // now make sure we don't have other points inside the potential ear Node* p = ear->next->next; while (p != ear->prev) { - if (pointInTriangle(a->x, a->y, b->x, b->y, c->x, c->y, p->x, p->y) && - area(p->prev, p, p->next) >= 0) return false; + if (tri.inBBox(p->x, p->y) && tri.containsPointExceptFirst(p->x, p->y) && area(p->prev, p, p->next) >= 0) + return false; p = p->next; } @@ -330,25 +459,20 @@ bool Earcut::isEarHashed(Node* ear) { const Node* b = ear; const Node* c = ear->next; - if (area(a, b, c) >= 0) return false; // reflex, can't be an ear - - // triangle bbox; min & max are calculated like this for speed - const double minTX = std::min(a->x, std::min(b->x, c->x)); - const double minTY = std::min(a->y, std::min(b->y, c->y)); - const double maxTX = std::max(a->x, std::max(b->x, c->x)); - const double maxTY = std::max(a->y, std::max(b->y, c->y)); + // reflex check is hoisted into the earcutLinked caller + const Triangle tri(a, b, c); // z-order range for the current triangle bbox; - const int32_t minZ = zOrder(minTX, minTY); - const int32_t maxZ = zOrder(maxTX, maxTY); + const int32_t minZ = zOrder(tri.minX, tri.minY); + const int32_t maxZ = zOrder(tri.maxX, tri.maxY); // first look for points inside the triangle in increasing z-order Node* p = ear->nextZ; while (p && p->z <= maxZ) { - if (p != ear->prev && p != ear->next && - pointInTriangle(a->x, a->y, b->x, b->y, c->x, c->y, p->x, p->y) && - area(p->prev, p, p->next) >= 0) return false; + if (p != ear->next && tri.inBBox(p->x, p->y) && tri.containsPointExceptFirst(p->x, p->y) && + area(p->prev, p, p->next) >= 0) + return false; p = p->nextZ; } @@ -356,9 +480,9 @@ bool Earcut::isEarHashed(Node* ear) { p = ear->prevZ; while (p && p->z >= minZ) { - if (p != ear->prev && p != ear->next && - pointInTriangle(a->x, a->y, b->x, b->y, c->x, c->y, p->x, p->y) && - area(p->prev, p, p->next) >= 0) return false; + if (p != ear->next && tri.inBBox(p->x, p->y) && tri.containsPointExceptFirst(p->x, p->y) && + area(p->prev, p, p->next) >= 0) + return false; p = p->prevZ; } @@ -367,15 +491,16 @@ bool Earcut::isEarHashed(Node* ear) { // go through all polygon nodes and cure small local self-intersections template -typename Earcut::Node* -Earcut::cureLocalIntersections(Node* start) { +typename Earcut::Node* Earcut::cureLocalIntersections(Node* start) { Node* p = start; + bool cured = false; do { Node* a = p->prev; Node* b = p->next->next; - // a self-intersection where edge (v[i-1],v[i]) intersects (v[i+1],v[i+2]) - if (!equals(a, b) && intersects(a, p, p->next, b) && locallyInside(a, b) && locallyInside(b, a)) { + // a self-intersection where edge (v[i-1],v[i]) intersects (v[i+1],v[i+2]); + // includeBoundary=false so a mere collinear touch isn't treated as a crossing + if (intersects(a, p, p->next, b, false) && locallyInside(a, b) && locallyInside(b, a)) { indices.emplace_back(a->i); indices.emplace_back(p->i); indices.emplace_back(b->i); @@ -385,11 +510,12 @@ Earcut::cureLocalIntersections(Node* start) { removeNode(p->next); p = start = b; + cured = true; } p = p->next; } while (p != start); - return filterPoints(p); + return cured ? filterPoints(p) : p; } // try splitting polygon into two and triangulate them independently @@ -420,35 +546,49 @@ void Earcut::splitEarcut(Node* start) { } // link every hole into the outer loop, producing a single-ring polygon without holes -template template -typename Earcut::Node* -Earcut::eliminateHoles(const Polygon& points, Node* outerNode) { +template +template +typename Earcut::Node* Earcut::eliminateHoles(const Polygon& points, Node* outerNode) { const size_t len = points.size(); - std::vector queue; + holeQueue.clear(); for (size_t i = 1; i < len; i++) { Node* list = linkedList(points[i], false); if (list) { if (list == list->next) list->steiner = true; - queue.push_back(getLeftmost(list)); + holeQueue.push_back(getLeftmost(list)); } } - std::sort(queue.begin(), queue.end(), [](const Node* a, const Node* b) { - return a->x < b->x; + // compareXYSlope: sort by x, then y, then slope. When two holes' leftmost points coincide, the + // slope tiebreak makes the bridge land on the shared vertex instead of bridging the wrong hole. + std::sort(holeQueue.begin(), holeQueue.end(), [](const Node* a, const Node* b) { + if (a->x != b->x) return a->x < b->x; + if (a->y != b->y) return a->y < b->y; + const double aSlope = (a->next->y - a->y) / (a->next->x - a->x); + const double bSlope = (b->next->y - b->y) / (b->next->x - b->x); + return aSlope < bSlope; }); - // process holes from left to right - for (size_t i = 0; i < queue.size(); i++) { - outerNode = eliminateHole(queue[i], outerNode); + // block-bbox index for findHoleBridge, grown append-only as holes merge. Seed it with the + // outer ring, then append each merged hole. + buildBlockIndex(vertices, holeQueue.size()); + indexSegment(outerNode, outerNode); + + // process holes from left to right; indexActive lets removeNode keep block bboxes live as + // filterPoints heals edges during merges (see growBlock) + indexActive = true; + for (size_t i = 0; i < holeQueue.size(); i++) { + outerNode = eliminateHole(holeQueue[i], outerNode); } + indexActive = false; - return outerNode; + // collapse collinear/coincident points across the whole merged ring once before clipping + return filterPoints(outerNode); } // find a bridge between vertices that connects hole with an outer ring and and link it template -typename Earcut::Node* -Earcut::eliminateHole(Node* hole, Node* outerNode) { +typename Earcut::Node* Earcut::eliminateHole(Node* hole, Node* outerNode) { Node* bridge = findHoleBridge(hole, outerNode); if (!bridge) { return outerNode; @@ -456,36 +596,56 @@ Earcut::eliminateHole(Node* hole, Node* outerNode) { Node* bridgeReverse = splitPolygon(bridge, hole); - // filter collinear points around the cuts - filterPoints(bridgeReverse, bridgeReverse->next); + // index the merged-in segment before filtering: in ring order the splice runs + // bridge -> hole -> bridgeReverse -> bridge2 -> (bridge's old next), covering the hole's edges + // and both new slit edges. filterPoints below only drops collinear/coincident points, so these + // bboxes stay valid (conservative) supersets. + Node* bridge2 = bridgeReverse->next; + indexSegment(bridge, bridge2->next); - // Check if input node was removed by the filtering + // heal collinear/coincident points around the two new slit edges + filterPoints(bridgeReverse, bridgeReverse->next); return filterPoints(bridge, bridge->next); } // David Eberly's algorithm for finding a bridge between hole and outer polygon template -typename Earcut::Node* -Earcut::findHoleBridge(Node* hole, Node* outerNode) { +typename Earcut::Node* Earcut::findHoleBridge(Node* hole, Node* outerNode) { Node* p = outerNode; double hx = hole->x; double hy = hole->y; - double qx = -std::numeric_limits::infinity(); + double qx = -std::numeric_limits::max(); Node* m = nullptr; // find a segment intersected by a ray from the hole's leftmost Vertex to the left; - // segment's endpoint with lesser x will be potential connection Vertex - do { - if (hy <= p->y && hy >= p->next->y && p->next->y != p->y) { - double x = p->x + (hy - p->y) * (p->next->x - p->x) / (p->next->y - p->y); - if (x <= hx && x > qx) { - qx = x; - m = p->x < p->next->x ? p : p->next; - if (x == hx) return m; // hole touches outer segment; pick leftmost endpoint - } - } - p = p->next; - } while (p != outerNode); + // segment's endpoint with lesser x will be potential connection Vertex, + // unless they intersect at a vertex, then choose the vertex + if (equals(hole, p)) return p; + + // scan blocks; skip any whose bbox can't hold a crossing that beats qx and lies left of hx + // (the prune Morton order can't express — explicit per-axis [minY,maxY]/[minX,maxX]) + for (std::size_t b = 0, g = 0; b < numBlocks; b++, g += 4) { + if (hy < blockBBox[g + 1] || hy > blockBBox[g + 3] || blockBBox[g] > hx || blockBBox[g + 2] <= qx) continue; + + // ensure the walk's exclusive bound is live so we don't overrun into other blocks + const Node* stop = liveBlockStop(b); + p = liveBlockHead(b); + do { + if (p->prev->next == p) { // skip nodes removed by filterPoints (stale in the index) + if (equals(hole, p->next)) + return p->next; + else if (hy <= p->y && hy >= p->next->y && p->next->y != p->y) { + double x = p->x + (hy - p->y) * (p->next->x - p->x) / (p->next->y - p->y); + if (x <= hx && x > qx) { + qx = x; + m = p->x < p->next->x ? p : p->next; + if (x == hx) return m; // hole touches outer segment; pick leftmost endpoint + } + } + } + p = p->next; + } while (p != stop); + } if (!m) return 0; @@ -493,31 +653,119 @@ Earcut::findHoleBridge(Node* hole, Node* outerNode) { // if there are no points found, we have a valid connection; // otherwise choose the Vertex of the minimum angle with the ray as connection Vertex - const Node* stop = m; - double tanMin = std::numeric_limits::infinity(); - double tanCur = 0; + const double mx = m->x; + const double my = m->y; + const double tminY = std::min(hy, my); // the triangle's y span; x span is [mx, hx] + const double tmaxY = std::max(hy, my); + double tanMin = std::numeric_limits::max(); - p = m; - double mx = m->x; - double my = m->y; + // scan the same blocks; skip any whose bbox can't overlap the triangle's [mx,hx]x[tminY,tmaxY] box + for (std::size_t b = 0, g = 0; b < numBlocks; b++, g += 4) { + if (blockBBox[g + 2] < mx || blockBBox[g] > hx || blockBBox[g + 3] < tminY || blockBBox[g + 1] > tmaxY) + continue; - do { - if (hx >= p->x && p->x >= mx && hx != p->x && - pointInTriangle(hy < my ? hx : qx, hy, mx, my, hy < my ? qx : hx, hy, p->x, p->y)) { + const Node* stop = liveBlockStop(b); + p = liveBlockHead(b); + do { + if (p->prev->next == p && hx >= p->x && p->x >= mx && hx != p->x && // skip dead nodes + pointInTriangle(hy < my ? hx : qx, hy, mx, my, hy < my ? qx : hx, hy, p->x, p->y)) { + const double tanCur = std::abs(hy - p->y) / (hx - p->x); // tangential + + // if hole point sits on p's horizontal edge (T-junction touch): the bridge runs + // along that edge — locallyInside rejects it as collinear, but it's valid + if ((locallyInside(p, hole) || (p->y == hy && p->next->y == hy && p->next->x > hx)) && + (tanCur < tanMin || + (tanCur == tanMin && (p->x > m->x || (p->x == m->x && sectorContainsSector(m, p)))))) { + m = p; + tanMin = tanCur; + } + } + p = p->next; + } while (p != stop); + } - tanCur = std::abs(hy - p->y) / (hx - p->x); // tangential + return m; +} - if (locallyInside(p, hole) && - (tanCur < tanMin || (tanCur == tanMin && (p->x > m->x || sectorContainsSector(m, p))))) { - m = p; - tanMin = tanCur; - } - } +// Block-bbox index buffers: size once from the input upper bound and reuse across calls. +template +void Earcut::buildBlockIndex(std::size_t maxNodes, std::size_t numHoles) { + // upper bound: every input node indexed once, +2 bridge nodes per hole, plus a partial + // trailing block per appended segment (outer ring + one per hole) + const std::size_t maxBlocks = (maxNodes + 2 * numHoles + K - 1) / K + numHoles + 2; + if (blockBBox.size() < maxBlocks * 4) blockBBox.resize(maxBlocks * 4); + if (blockHead.size() < maxBlocks) { + blockHead.resize(maxBlocks); + blockStop.resize(maxBlocks); + } + numBlocks = 0; +} - p = p->next; +// index the ring run head..stop (exclusive) as ceil(len / K) blocks; head == stop means the whole +// ring. each block's bbox covers both endpoints of every edge it owns. +template +void Earcut::indexSegment(Node* head, Node* stop) { + Node* p = head; + do { + const std::size_t b = numBlocks++; + blockHead[b] = p; + double minX = std::numeric_limits::max(); + double minY = std::numeric_limits::max(); + double maxX = std::numeric_limits::lowest(); + double maxY = std::numeric_limits::lowest(); + int32_t k = 0; + do { + Node* c = p->next; // edge p->c; bbox must bound both endpoints + p->z = static_cast(b); // reuse z as the owning block during eliminateHoles (see growBlock) + if (p->x < minX) minX = p->x; + if (p->x > maxX) maxX = p->x; + if (p->y < minY) minY = p->y; + if (p->y > maxY) maxY = p->y; + if (c->x < minX) minX = c->x; + if (c->x > maxX) maxX = c->x; + if (c->y < minY) minY = c->y; + if (c->y > maxY) maxY = c->y; + p = c; + } while (++k < K && p != stop); + blockStop[b] = p; + const std::size_t g = b * 4; + blockBBox[g] = minX; + blockBBox[g + 1] = minY; + blockBBox[g + 2] = maxX; + blockBBox[g + 3] = maxY; } while (p != stop); +} - return m; +// when filterPoints heals an edge head->tail (removing the collinear node between them), the healed +// edge can extend past head's frozen block bbox if its old far endpoint lived in another block; grow +// head's block bbox to cover tail so the leftward-ray prune can't false-skip it. +template +void Earcut::growBlock(Node* head, Node* tail) { + const std::size_t g = static_cast(head->z) * 4; + if (tail->x < blockBBox[g]) blockBBox[g] = tail->x; + if (tail->y < blockBBox[g + 1]) blockBBox[g + 1] = tail->y; + if (tail->x > blockBBox[g + 2]) blockBBox[g + 2] = tail->x; + if (tail->y > blockBBox[g + 3]) blockBBox[g + 3] = tail->y; +} + +// the block's head node can be removed by filterPoints during merges; advance it to the next live +// node so the walk doesn't start on (and immediately terminate at) a dead node. For the single +// full-ring seed block (head == stop) the same forward advance keeps them equal, so the do-while +// still laps the whole ring instead of collapsing to an empty walk. +template +typename Earcut::Node* Earcut::liveBlockHead(std::size_t b) { + Node* head = blockHead[b]; + while (head->prev->next != head) head = head->next; + blockHead[b] = head; + return head; +} + +template +typename Earcut::Node* Earcut::liveBlockStop(std::size_t b) { + Node* stop = blockStop[b]; + while (stop->prev->next != stop) stop = stop->next; + blockStop[b] = stop; + return stop; } // whether sector in vertex m contains sector in vertex p in the same coordinates @@ -533,7 +781,8 @@ void Earcut::indexCurve(Node* start) { Node* p = start; do { - p->z = p->z ? p->z : zOrder(p->x, p->y); + // always (re)compute: z may still hold a block index left over from eliminateHoles + p->z = zOrder(p->x, p->y); p->prevZ = p->prev; p->nextZ = p->next; p = p->next; @@ -545,73 +794,28 @@ void Earcut::indexCurve(Node* start) { sortLinked(p); } -// Simon Tatham's linked list merge sort algorithm -// http://www.chiark.greenend.org.uk/~sgtatham/algorithms/listsort.html +// Sort the z-linked ring by z-order. Upstream earcut replaced its linked merge sort with an +// array sort (materialize node refs → sort → relink); in C++ std::sort over a contiguous +// Node* buffer inlines the comparator fully and beats both a linked merge sort and a hand radix +// (measured on the MVT tiles fixture) — JS's rejection of native Array.sort does not transfer. template -typename Earcut::Node* -Earcut::sortLinked(Node* list) { +typename Earcut::Node* Earcut::sortLinked(Node* list) { assert(list); - Node* p; - Node* q; - Node* e; - Node* tail; - int i, numMerges, pSize, qSize; - int inSize = 1; - - for (;;) { - p = list; - list = nullptr; - tail = nullptr; - numMerges = 0; - - while (p) { - numMerges++; - q = p; - pSize = 0; - for (i = 0; i < inSize; i++) { - pSize++; - q = q->nextZ; - if (!q) break; - } - - qSize = inSize; - - while (pSize > 0 || (qSize > 0 && q)) { - - if (pSize == 0) { - e = q; - q = q->nextZ; - qSize--; - } else if (qSize == 0 || !q) { - e = p; - p = p->nextZ; - pSize--; - } else if (p->z <= q->z) { - e = p; - p = p->nextZ; - pSize--; - } else { - e = q; - q = q->nextZ; - qSize--; - } - - if (tail) tail->nextZ = e; - else list = e; - - e->prevZ = tail; - tail = e; - } - - p = q; - } - - tail->nextZ = nullptr; - - if (numMerges <= 1) return list; - - inSize *= 2; + // list is a null-terminated nextZ chain (see indexCurve); walk it into the scratch buffer + sortBuffer.clear(); + for (Node* p = list; p; p = p->nextZ) sortBuffer.push_back(p); + + std::sort(sortBuffer.begin(), sortBuffer.end(), [](const Node* a, const Node* b) { return a->z < b->z; }); + + // relink in sorted order + Node* prev = nullptr; + for (Node* p : sortBuffer) { + p->prevZ = prev; + if (prev) prev->nextZ = p; + prev = p; } + prev->nextZ = nullptr; + return sortBuffer.front(); } // z-order of a Vertex given coords and size of the data bounding box @@ -636,13 +840,11 @@ int32_t Earcut::zOrder(const double x_, const double y_) { // find the leftmost node of a polygon ring template -typename Earcut::Node* -Earcut::getLeftmost(Node* start) { +typename Earcut::Node* Earcut::getLeftmost(Node* start) { Node* p = start; Node* leftmost = start; do { - if (p->x < leftmost->x || (p->x == leftmost->x && p->y < leftmost->y)) - leftmost = p; + if (p->x < leftmost->x || (p->x == leftmost->x && p->y < leftmost->y)) leftmost = p; p = p->next; } while (p != start); @@ -651,19 +853,23 @@ Earcut::getLeftmost(Node* start) { // check if a point lies within a convex triangle template -bool Earcut::pointInTriangle(double ax, double ay, double bx, double by, double cx, double cy, double px, double py) const { - return (cx - px) * (ay - py) >= (ax - px) * (cy - py) && - (ax - px) * (by - py) >= (bx - px) * (ay - py) && +bool Earcut::pointInTriangle( + double ax, double ay, double bx, double by, double cx, double cy, double px, double py) const { + return (cx - px) * (ay - py) >= (ax - px) * (cy - py) && (ax - px) * (by - py) >= (bx - px) * (ay - py) && (bx - px) * (cy - py) >= (cx - px) * (by - py); } // check if a diagonal between two polygon nodes is valid (lies in polygon interior) template bool Earcut::isValidDiagonal(Node* a, Node* b) { - return a->next->i != b->i && a->prev->i != b->i && !intersectsPolygon(a, b) && // dones't intersect other edges - ((locallyInside(a, b) && locallyInside(b, a) && middleInside(a, b) && // locally visible - (area(a->prev, a, b->prev) != 0.0 || area(a, b->prev, b) != 0.0)) || // does not create opposite-facing sectors - (equals(a, b) && area(a->prev, a, a->next) > 0 && area(b->prev, b, b->next) > 0)); // special zero-length case + // degenerate zero-length case + const bool zeroLength = equals(a, b) && area(a->prev, a, a->next) > 0 && area(b->prev, b, b->next) > 0; + return a->next->i != b->i && + (zeroLength || + (locallyInside(a, b) && locallyInside(b, a) && // locally visible + (area(a->prev, a, b->prev) != 0.0 || area(a, b->prev, b) != 0.0))) && // no opposite-facing sectors + !intersectsPolygon(a, b) && // doesn't intersect other edges + (zeroLength || middleInside(a, b)); // diagonal inside polygon } // signed area of a triangle @@ -678,15 +884,18 @@ bool Earcut::equals(const Node* p1, const Node* p2) { return p1->x == p2->x && p1->y == p2->y; } -// check if two segments intersect +// check if two segments intersect; by default includes collinear boundary touches template -bool Earcut::intersects(const Node* p1, const Node* q1, const Node* p2, const Node* q2) { - int o1 = sign(area(p1, q1, p2)); - int o2 = sign(area(p1, q1, q2)); - int o3 = sign(area(p2, q2, p1)); - int o4 = sign(area(p2, q2, q1)); +bool Earcut::intersects(const Node* p1, const Node* q1, const Node* p2, const Node* q2, bool includeBoundary) { + const double o1 = area(p1, q1, p2); + const double o2 = area(p1, q1, q2); + const double o3 = area(p2, q2, p1); + const double o4 = area(p2, q2, q1); + + // general case: the two segments straddle each other (proper crossing) + if (((o1 > 0 && o2 < 0) || (o1 < 0 && o2 > 0)) && ((o3 > 0 && o4 < 0) || (o3 < 0 && o4 > 0))) return true; - if (o1 != o2 && o3 != o4) return true; // general case + if (!includeBoundary) return false; if (o1 == 0 && onSegment(p1, p2, q1)) return true; // p1, q1 and p2 are collinear and p2 lies on p1q1 if (o2 == 0 && onSegment(p1, q2, q1)) return true; // p1, q1 and q2 are collinear and q2 lies on p1q1 @@ -699,25 +908,30 @@ bool Earcut::intersects(const Node* p1, const Node* q1, const Node* p2, const // for collinear points p, q, r, check if point q lies on segment pr template bool Earcut::onSegment(const Node* p, const Node* q, const Node* r) { - return q->x <= std::max(p->x, r->x) && - q->x >= std::min(p->x, r->x) && - q->y <= std::max(p->y, r->y) && - q->y >= std::min(p->y, r->y); -} - -template -int Earcut::sign(double val) { - return (0.0 < val) - (val < 0.0); + return q->x <= std::max(p->x, r->x) && q->x >= std::min(p->x, r->x) && + q->y <= std::max(p->y, r->y) && q->y >= std::min(p->y, r->y); } // check if a polygon diagonal intersects any polygon segments template bool Earcut::intersectsPolygon(const Node* a, const Node* b) { + // diagonal bbox; an edge whose bbox can't overlap it can't intersect it, so + // skip the orientation test for those (the common case — the diagonal is short) + const double minX = std::min(a->x, b->x); + const double maxX = std::max(a->x, b->x); + const double minY = std::min(a->y, b->y); + const double maxY = std::max(a->y, b->y); + const Node* p = a; do { - if (p->i != a->i && p->next->i != a->i && p->i != b->i && p->next->i != b->i && - intersects(p, p->next, a, b)) return true; - p = p->next; + const Node* n = p->next; + if ((p->x > maxX && n->x > maxX) || (p->x < minX && n->x < minX) || (p->y > maxY && n->y > maxY) || + (p->y < minY && n->y < minY)) { + p = n; + continue; + } + if (p->i != a->i && n->i != a->i && p->i != b->i && n->i != b->i && intersects(p, n, a, b)) return true; + p = n; } while (p != a); return false; @@ -726,9 +940,8 @@ bool Earcut::intersectsPolygon(const Node* a, const Node* b) { // check if a polygon diagonal is locally inside the polygon template bool Earcut::locallyInside(const Node* a, const Node* b) { - return area(a->prev, a, a->next) < 0 ? - area(a, b, a->next) >= 0 && area(a, a->prev, b) >= 0 : - area(a, b, a->prev) < 0 || area(a, a->next, b) < 0; + return area(a->prev, a, a->next) < 0 ? area(a, b, a->next) >= 0 && area(a, a->prev, b) >= 0 + : area(a, b, a->prev) < 0 || area(a, a->next, b) < 0; } // check if the middle Vertex of a polygon diagonal is inside the polygon @@ -739,10 +952,9 @@ bool Earcut::middleInside(const Node* a, const Node* b) { double px = (a->x + b->x) / 2; double py = (a->y + b->y) / 2; do { - if (((p->y > py) != (p->next->y > py)) && p->next->y != p->y && - (px < (p->next->x - p->x) * (py - p->y) / (p->next->y - p->y) + p->x)) - inside = !inside; - p = p->next; + const Node* n = p->next; + if (((p->y > py) != (n->y > py)) && (px < (n->x - p->x) * (py - p->y) / (n->y - p->y) + p->x)) inside = !inside; + p = n; } while (p != a); return inside; @@ -752,10 +964,9 @@ bool Earcut::middleInside(const Node* a, const Node* b) { // polygon into two; if one belongs to the outer ring and another to a hole, it merges it into a // single ring template -typename Earcut::Node* -Earcut::splitPolygon(Node* a, Node* b) { - Node* a2 = nodes.construct(a->i, a->x, a->y); - Node* b2 = nodes.construct(b->i, b->x, b->y); +typename Earcut::Node* Earcut::splitPolygon(Node* a, Node* b) { + Node* a2 = nodes->construct(a->i, a->x, a->y); + Node* b2 = nodes->construct(b->i, b->x, b->y); Node* an = a->next; Node* bp = b->prev; @@ -775,10 +986,10 @@ Earcut::splitPolygon(Node* a, Node* b) { } // create a node and util::optionally link it with previous one (in a circular doubly linked list) -template template -typename Earcut::Node* -Earcut::insertNode(std::size_t i, const Point& pt, Node* last) { - Node* p = nodes.construct(static_cast(i), util::nth<0, Point>::get(pt), util::nth<1, Point>::get(pt)); +template +template +typename Earcut::Node* Earcut::insertNode(std::size_t i, const Point& pt, Node* last) { + Node* p = nodes->construct(static_cast(i), util::nth<0, Point>::get(pt), util::nth<1, Point>::get(pt)); if (!last) { p->prev = p; @@ -801,8 +1012,11 @@ void Earcut::removeNode(Node* p) { if (p->prevZ) p->prevZ->nextZ = p->nextZ; if (p->nextZ) p->nextZ->prevZ = p->prevZ; + + // keep the hole-bridge index's block bboxes covering the healed prev->next edge + if (indexActive) growBlock(p->prev, p->next); } -} +} // namespace detail template std::vector earcut(const Polygon& poly) { @@ -810,4 +1024,184 @@ std::vector earcut(const Polygon& poly) { earcut(poly); return std::move(earcut.indices); } + +namespace detail { + +// Refine a triangulation toward the constrained Delaunay triangulation by legalizing every interior +// edge in place with Lawson flips — maximizing the minimum angle and removing most slivers. Adapted +// from delaunator's edge legalization. Uses non-robust predicates: float input is fine, and the +// worst case is a not-quite-Delaunay edge, never an invalid mesh. Ported from earcut v3.2.3. +template +class Refiner { +public: + // triangles: triangle indices as returned by earcut, mutated in place. + // coords: random-access container of points, indexed by vertex index (coords[i] -> point i), + // read through the same util::nth<0>/<1> accessors as earcut's input. + template + void operator()(std::vector& triangles, const Coords& coords) { + using Point = typename std::decay::type; + const int n = static_cast(triangles.size()); + if (n < 6) return; + ensureScratch(static_cast(n)); + gen++; // bumping the generation logically empties the hash (no clearing) + std::fill(he.begin(), he.begin() + n, -1); + + // Raw pointers into the scratch: indexed by the int/uint half-edge and hash indices below, + // where operator[]'s size_type would trip -Wsign-conversion on every subscript. + N* t = triangles.data(); + int32_t* he = this->he.data(); + int32_t* edgeStack = this->edgeStack.data(); + int32_t* hTable = this->hTable.data(); + uint32_t* hStamp = this->hStamp.data(); + uint8_t* edgeStamp = this->edgeStamp.data(); + + auto X = [&](N p) -> double { return static_cast(util::nth<0, Point>::get(coords[p])); }; + auto Y = [&](N p) -> double { return static_cast(util::nth<1, Point>::get(coords[p])); }; + + // Build half-edge twins with an undirected-edge hash; consumed slots mark linked pairs. As + // each pair is linked we seed the stack with one representative (s, the earlier-inserted + // edge) — this fuses the initial "push every interior edge" pass into the build, saving a + // full O(n) scan. edgeStamp is all-zero here (balanced push/pop leaves it clean) and each + // pair links once, so the seed write needs no dedup guard. + int i = 0; + for (int e = 0; e < n; e++) { + const N a = t[e], b = t[nextHE(e)]; + const N lo = a < b ? a : b, hi = a < b ? b : a; + uint32_t h = (uint32_t(lo) * 0x9e3779b1u ^ uint32_t(hi) * 0x85ebca6bu) & hMask; + while (hStamp[h] == gen) { + const int32_t s = hTable[h]; + // s == -1 marks a consumed slot (a pair already linked) — skip past it + if (s != -1) { + const N sa = t[s], sb = t[nextHE(s)]; + if ((sa == lo && sb == hi) || (sa == hi && sb == lo)) { + he[e] = s; + he[s] = e; + hTable[h] = -1; // link, then consume the slot + edgeStamp[s] = 1; + edgeStack[i++] = s; // seed the interior edge for the cascade + break; + } + } + h = (h + 1) & hMask; + } + if (hStamp[h] != gen) { + hTable[h] = e; + hStamp[h] = gen; + } // first occurrence: insert + } + + while (i > 0) { + const int a = edgeStack[--i]; + edgeStamp[a] = 0; + const int b = he[a]; + if (b == -1) continue; + + const int a0 = a - a % 3; + const int b0 = b - b % 3; + const int ar = a0 + (a + 2) % 3; + const int al = a0 + (a + 1) % 3; + const int bl = b0 + (b + 2) % 3; + const int br = b0 + (b + 1) % 3; + const N p0 = t[ar], pr = t[a], pl = t[al], p1 = t[bl]; + + const double x0 = X(p0), y0 = Y(p0); + const double xr = X(pr), yr = Y(pr); + const double xl = X(pl), yl = Y(pl); + const double x1 = X(p1), y1 = Y(p1); + + // Test inCircle first: most interior edges are already Delaunay (inCircle true → no + // flip), so this short-circuits before the two convexity orients on the common path. The + // quad must also be convex (both new triangles CCW) — flipping a reflex quad would push + // a triangle outside the polygon. Boundary/hole edges self-protect via he == -1. + if (!inCircle(x0, y0, xr, yr, xl, yl, x1, y1) && orient(x0, y0, xr, yr, x1, y1) > 0 && + orient(x0, y0, x1, y1, xl, yl) > 0) { + t[a] = p1; + t[b] = p0; + const int32_t hbl = he[bl], har = he[ar]; + he[a] = hbl; + if (hbl != -1) he[hbl] = a; + he[b] = har; + if (har != -1) he[har] = b; + he[ar] = bl; + he[bl] = ar; + + // re-check the quad's four outer edges; skip boundary edges (he == -1) and any + // already queued (edgeStamp), which also keeps the stack bounded by n. + if (hbl != -1 && edgeStamp[a] == 0) { + edgeStamp[a] = 1; + edgeStack[i++] = a; + } + if (har != -1 && edgeStamp[b] == 0) { + edgeStamp[b] = 1; + edgeStack[i++] = b; + } + if (he[al] != -1 && edgeStamp[al] == 0) { + edgeStamp[al] = 1; + edgeStack[i++] = al; + } + if (he[br] != -1 && edgeStamp[br] == 0) { + edgeStamp[br] = 1; + edgeStack[i++] = br; + } + } + } + } + +private: + // Reusable scratch, grown on demand like earcut's z-order arrays and reused across calls: + // he = twin half-edge of each edge, or -1 on the polygon boundary + // hTable = open-addressing hash, slot -> half-edge index, valid iff hStamp[slot] == gen + // edgeStamp = pending-in-stack flag, cleared when the edge is popped + std::vector he, edgeStack, hTable; + std::vector hStamp; + std::vector edgeStamp; + uint32_t hMask = 0, gen = 0; + + static int nextHE(int e) { return e - e % 3 + (e + 1) % 3; } // next half-edge in same triangle + + static double orient(double ax, double ay, double bx, double by, double cx, double cy) { + return (bx - ax) * (cy - ay) - (by - ay) * (cx - ax); + } + + // Whether p is inside or exactly on the circumcircle of triangle (a, b, c). Sign is negated vs + // the usual predicate to match earcut's CCW winding — the standard sign builds the anti-Delaunay + // mesh. Cocircular quads are legal ties, so refine only flips when this returns false. + static bool inCircle(double ax, double ay, double bx, double by, double cx, double cy, double px, double py) { + const double dx = ax - px, dy = ay - py, ex = bx - px, ey = by - py, fx = cx - px, fy = cy - py; + const double ap = dx * dx + dy * dy, bp = ex * ex + ey * ey, cp = fx * fx + fy * fy; + // A near-cocircular quad is a legal Delaunay tie, but roundoff can flag both an edge and its + // flip as illegal, cascading into an endless flip loop (#205) — so treat a determinant + // within a small margin of zero as a tie. The determinant's worst-case roundoff error is + // provably below 9e-16·(ap+bp+cp)² (Shewchuk-style bound), so the margin guarantees every + // executed flip is illegal in exact arithmetic, and Lawson flipping always terminates. + const double s = ap + bp + cp; + return dx * (ey * cp - bp * fy) - dy * (ex * cp - bp * fx) + ap * (ex * fy - ey * fx) <= 1e-13 * s * s; + } + + void ensureScratch(std::size_t n) { + // edgeStack holds at most one entry per half-edge (edgeStamp dedups), so n is a safe cap. + if (edgeStack.size() < n) edgeStack.resize(n); + if (he.size() < n) he.resize(n); + if (edgeStamp.size() < n) edgeStamp.resize(n, 0); + std::size_t size = 1; + while (size < n * 4) size <<= 1; // power-of-two table, load factor <= 0.25 + if (hTable.size() < size) { + hTable.resize(size); + hStamp.resize(size, 0); + } + hMask = uint32_t(size) - 1; + } +}; + +} // namespace detail + +// Opt-in Delaunay-refinement post-pass for earcut() output (or any manifold triangle-index array). +// Legalizes every interior edge in place with Lawson flips. See detail::Refiner. `coords` is a +// random-access container of points indexed by vertex index; `triangles` is mutated in place. +template +void refine(std::vector& triangles, const Coords& coords) { + static thread_local mapbox::detail::Refiner refiner; + refiner(triangles, coords); } + +} // namespace mapbox