diff --git a/.gitignore b/.gitignore index 867d4b2..5e2a3dc 100644 --- a/.gitignore +++ b/.gitignore @@ -24,6 +24,7 @@ *.la *.a *.lib +*.ilk # Executables *.exe @@ -34,4 +35,3 @@ # ---> Platform Files *.sh -*.*.dblite diff --git a/src/.sconsign.dblite b/src/.sconsign.dblite deleted file mode 100644 index dc9a5b0..0000000 Binary files a/src/.sconsign.dblite and /dev/null differ diff --git a/src/liborng/nodes/earcut.hpp b/src/liborng/nodes/earcut.hpp new file mode 100644 index 0000000..97e7677 --- /dev/null +++ b/src/liborng/nodes/earcut.hpp @@ -0,0 +1,1207 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace mapbox { + +namespace util { + +template +struct nth { + inline static typename std::tuple_element::type get(const T& t) { return std::get(t); }; +}; + +} // namespace util + +namespace detail { + +template +class Earcut { +public: + std::vector indices; + std::size_t vertices = 0; + + template + void operator()(const Polygon& points); + +private: + struct Node { + // 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 double x; + const double y; + + // previous and next vertice nodes in a polygon ring + Node* prev = nullptr; + Node* next = nullptr; + + // 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; + }; + + // 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); + Node* filterPoints(Node* start, Node* end = nullptr); + 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); + 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); + int32_t zOrder(const double x_, const double y_); + Node* getLeftmost(Node* start); + bool pointInTriangle(double ax, double ay, double bx, double by, double cx, double cy, double px, double py) const; + 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 includeBoundary = true); + bool onSegment(const Node* p, const Node* q, const Node* r); + 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); + 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; + + template > + class ObjectPool { + public: + ObjectPool() { allocateNewBlock(256); } + ObjectPool(std::size_t blockSize_) : baseBlockSize(blockSize_) { + allocateNewBlock(std::max(blockSize_, 256)); + } + ~ObjectPool() { clear(); } + template + T* construct(Args&&... args) { + // 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 = memoryBlocks[currentBlockIndex].get() + currentIndex; + alloc_traits::construct(alloc, object, std::forward(args)...); + totalObjects++; + currentIndex++; + return object; + } + 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; + } + // Reset to start from first block again + currentBlockIndex = 0; + currentIndex = 0; + totalObjects = 0; + } + + private: + 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; + } + }; + + 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 +void Earcut::operator()(const Polygon& points) { + // reset + indices.clear(); + vertices = 0; + + if (points.empty()) return; + + double x; + double y; + int threshold = 80; + std::size_t len = 0; + + for (size_t i = 0; threshold >= 0 && i < points.size(); i++) { + threshold -= static_cast(points[i].size()); + len += points[i].size(); + } + + // 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); + if (!outerNode || outerNode->prev == outerNode->next) return; + + if (points.size() > 1) outerNode = eliminateHoles(points, outerNode); + + // if the shape is not too simple, we'll use z-order curve hash later; calculate polygon bbox + hashing = threshold < 0; + if (hashing) { + Node* p = outerNode->next; + minX = maxX = outerNode->x; + minY = maxY = outerNode->y; + do { + x = p->x; + y = p->y; + minX = std::min(minX, x); + minY = std::min(minY, y); + maxX = std::max(maxX, x); + maxY = std::max(maxY, y); + p = p->next; + } while (p != outerNode); + + // minX, minY and inv_size are later used to transform coords into integers for z-order calculation + inv_size = std::max(maxX - minX, maxY - minY); + inv_size = inv_size != .0 ? (32767. / inv_size) : .0; + } + + earcutLinked(outerNode); + + 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) { + using Point = typename Ring::value_type; + double sum = 0; + const std::size_t len = points.size(); + std::size_t i, j; + Node* last = nullptr; + + // calculate original winding order of a polygon ring + for (i = 0, j = len > 0 ? len - 1 : 0; i < len; j = i++) { + const auto& p1 = points[i]; + const auto& p2 = points[j]; + const double p20 = util::nth<0, Point>::get(p2); + const double p10 = util::nth<0, Point>::get(p1); + const double p11 = util::nth<1, Point>::get(p1); + const double p21 = util::nth<1, Point>::get(p2); + sum += (p20 - p10) * (p11 + p21); + } + + // link points into circular doubly-linked list in the specified winding order + if (clockwise == (sum > 0)) { + for (i = 0; i < len; i++) last = insertNode(vertices + i, points[i], last); + } else { + for (i = len; i-- > 0;) last = insertNode(vertices + i, points[i], last); + } + + if (last && equals(last, last->next)) { + removeNode(last); + last = last->next; + } + + vertices += len; + + return last; +} + +// 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 (!start) return start; + const bool full = !end; + if (full) end = start; + + Node* p = start; + bool again; + do { + again = false; + 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 = p->prev; // re-check the predecessor + again = true; + } else if (full || p != end) { + p = p->next; + again = !full; // local heal: keep looping until the sweep reaches end + } + } while (again || p != end); + + return end; +} + +// main ear slicing loop which triangulates a polygon (given as a linked list) +template +void Earcut::earcutLinked(Node* ear) { + if (!ear) return; + + // interlink polygon nodes in z-order + 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; + + // 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); + indices.emplace_back(next->i); + + removeNode(ear); + + ear = next; + stop = next; + + continue; + } + + ear = next; + + // if we looped through the whole remaining polygon and can't find any more ears + if (ear == stop) { + // 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; + } + + // 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 + splitEarcut(ear); + break; + } + } +} + +// check whether a polygon node forms a valid ear with adjacent nodes +template +bool Earcut::isEar(Node* ear) { + const Node* a = ear->prev; + const Node* b = ear; + const Node* c = ear->next; + + // 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 (tri.inBBox(p->x, p->y) && tri.containsPointExceptFirst(p->x, p->y) && area(p->prev, p, p->next) >= 0) + return false; + p = p->next; + } + + return true; +} + +template +bool Earcut::isEarHashed(Node* ear) { + const Node* a = ear->prev; + const Node* b = ear; + const Node* c = ear->next; + + // 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(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->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; + } + + // then look for points in decreasing z-order + p = ear->prevZ; + + while (p && p->z >= minZ) { + 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; + } + + return true; +} + +// go through all polygon nodes and cure small local self-intersections +template +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]); + // 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); + + // remove two nodes involved + removeNode(p); + removeNode(p->next); + + p = start = b; + cured = true; + } + p = p->next; + } while (p != start); + + return cured ? filterPoints(p) : p; +} + +// try splitting polygon into two and triangulate them independently +template +void Earcut::splitEarcut(Node* start) { + // look for a valid diagonal that divides the polygon into two + Node* a = start; + do { + Node* b = a->next->next; + while (b != a->prev) { + if (a->i != b->i && isValidDiagonal(a, b)) { + // split the polygon in two by the diagonal + Node* c = splitPolygon(a, b); + + // filter colinear points around the cuts + a = filterPoints(a, a->next); + c = filterPoints(c, c->next); + + // run earcut on each half + earcutLinked(a); + earcutLinked(c); + return; + } + b = b->next; + } + a = a->next; + } while (a != 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) { + const size_t len = points.size(); + + 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; + holeQueue.push_back(getLeftmost(list)); + } + } + // 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; + }); + + // 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; + + // 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) { + Node* bridge = findHoleBridge(hole, outerNode); + if (!bridge) { + return outerNode; + } + + Node* bridgeReverse = splitPolygon(bridge, hole); + + // 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); + + // 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) { + Node* p = outerNode; + double hx = hole->x; + double hy = hole->y; + 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, + // 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; + + // look for points inside the triangle of hole Vertex, segment intersection and endpoint; + // 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 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(); + + // 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; + + 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); + } + + return m; +} + +// 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; +} + +// 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); +} + +// 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 +template +bool Earcut::sectorContainsSector(const Node* m, const Node* p) { + return area(m->prev, m, p->prev) < 0 && area(p->next, m, m->next) < 0; +} + +// interlink polygon nodes in z-order +template +void Earcut::indexCurve(Node* start) { + assert(start); + Node* p = start; + + do { + // 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; + } while (p != start); + + p->prevZ->nextZ = nullptr; + p->prevZ = nullptr; + + sortLinked(p); +} + +// 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) { + assert(list); + // 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 +template +int32_t Earcut::zOrder(const double x_, const double y_) { + // coords are transformed into non-negative 15-bit integer range + int32_t x = static_cast((x_ - minX) * inv_size); + int32_t y = static_cast((y_ - minY) * inv_size); + + x = (x | (x << 8)) & 0x00FF00FF; + x = (x | (x << 4)) & 0x0F0F0F0F; + x = (x | (x << 2)) & 0x33333333; + x = (x | (x << 1)) & 0x55555555; + + y = (y | (y << 8)) & 0x00FF00FF; + y = (y | (y << 4)) & 0x0F0F0F0F; + y = (y | (y << 2)) & 0x33333333; + y = (y | (y << 1)) & 0x55555555; + + return x | (y << 1); +} + +// find the leftmost node of a polygon ring +template +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; + p = p->next; + } while (p != start); + + return leftmost; +} + +// 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) && + (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) { + // 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 +template +double Earcut::area(const Node* p, const Node* q, const Node* r) const { + return (q->y - p->y) * (r->x - q->x) - (q->x - p->x) * (r->y - q->y); +} + +// check if two points are equal +template +bool Earcut::equals(const Node* p1, const Node* p2) { + return p1->x == p2->x && p1->y == p2->y; +} + +// 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, 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 (!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 + if (o3 == 0 && onSegment(p2, p1, q2)) return true; // p2, q2 and p1 are collinear and p1 lies on p2q2 + if (o4 == 0 && onSegment(p2, q1, q2)) return true; // p2, q2 and q1 are collinear and q1 lies on p2q2 + + return false; +} + +// 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); +} + +// 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 { + 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; +} + +// 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; +} + +// check if the middle Vertex of a polygon diagonal is inside the polygon +template +bool Earcut::middleInside(const Node* a, const Node* b) { + const Node* p = a; + bool inside = false; + double px = (a->x + b->x) / 2; + double py = (a->y + b->y) / 2; + do { + 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; +} + +// link two polygon vertices with a bridge; if the vertices belong to the same ring, it splits +// 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); + Node* an = a->next; + Node* bp = b->prev; + + a->next = b; + b->prev = a; + + a2->next = an; + an->prev = a2; + + b2->next = a2; + a2->prev = b2; + + bp->next = b2; + b2->prev = bp; + + return b2; +} + +// 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)); + + if (!last) { + p->prev = p; + p->next = p; + + } else { + assert(last); + p->next = last->next; + p->prev = last; + last->next->prev = p; + last->next = p; + } + return p; +} + +template +void Earcut::removeNode(Node* p) { + p->next->prev = p->prev; + p->prev->next = p->next; + + 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) { + mapbox::detail::Earcut earcut; + 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 diff --git a/src/liborng/nodes/mesh_editing_library.cpp b/src/liborng/nodes/mesh_editing_library.cpp new file mode 100644 index 0000000..68a82b8 --- /dev/null +++ b/src/liborng/nodes/mesh_editing_library.cpp @@ -0,0 +1,738 @@ +/* + * ©2026 Batty Bovine Productions, LLC. All Rights Reserved. + * + * If this source code makes it to the public internet, this software can be + * considered to be protected by the MIT licence. Have fun with it. + */ + +#include "mesh_editing_library.h" +#include "orng_macros.h" +#include "earcut.hpp" + +#include + +#include "godot_cpp/variant/utility_functions.hpp" +using namespace godot; + + +MeshEditingLibrary::MeshEditingLibrary() +{ +} + +MeshEditingLibrary::~MeshEditingLibrary() +{ +} + + +TypedArray MeshEditingLibrary::get_skinned_vertex_positions(const Skeleton3D *skeleton, const Mesh *mesh, const Transform3D &component_to_world) +{ + TypedArray skinned_positions; + + std::vector rest_bone_transforms; + std::vector pose_bone_transforms; + + const uint32_t num_bones = skeleton->get_bone_count(); + rest_bone_transforms.resize(num_bones); + pose_bone_transforms.resize(num_bones); + + for (uint32_t i = 0; i < num_bones; i++) + { + rest_bone_transforms[i] = skeleton->get_bone_rest(i).inverse(); + pose_bone_transforms[i] = skeleton->get_bone_pose(i); + } + + for (uint32_t surface = 0; surface < mesh->get_surface_count(); surface++) + { + Array mesh_arrays = mesh->surface_get_arrays(surface); + const PackedVector3Array &mesh_vertex_array = mesh_arrays[ArrayMesh::ARRAY_VERTEX]; + const PackedInt32Array &mesh_bones_array = mesh_arrays[ArrayMesh::ARRAY_BONES]; + const PackedFloat32Array &mesh_weights_array = mesh_arrays[ArrayMesh::ARRAY_WEIGHTS]; + const uint32_t num_vertices = mesh_vertex_array.size(); + const uint8_t num_bones_per_vertex = mesh_bones_array.size() / num_vertices; + + PackedVector3Array surface_skinned_positions; + surface_skinned_positions.resize(num_vertices); + + for (uint32_t vertex = 0; vertex < num_vertices; vertex++) + { + const Vector3 &rest_vertex_position = mesh_vertex_array[vertex]; + Vector3 skinned_vertex_position; + + for (uint8_t bone_index = 0; bone_index < num_bones_per_vertex; bone_index++) + { + const uint16_t bone = mesh_bones_array[(vertex * num_bones_per_vertex) + bone_index]; + const float weight = mesh_weights_array[(vertex * num_bones_per_vertex) + bone_index]; + const Transform3D &bone_transform = (pose_bone_transforms[bone] * rest_bone_transforms[bone]) * weight; + skinned_vertex_position += bone_transform.xform(rest_vertex_position); + } + surface_skinned_positions[vertex] = skinned_vertex_position; + } + + skinned_positions.append(surface_skinned_positions); + } + + return skinned_positions; +} + +PackedVector3Array MeshEditingLibrary::slice_mesh(const MeshInstance3D *original_mesh_instance, const Plane &local_plane, ArrayMesh *out_first_half, ArrayMesh *out_other_half, const MeshEditCapUV cap_option, const Ref cap_material) +{ + const Mesh *original_mesh = original_mesh_instance->get_mesh().ptr(); + const uint8_t num_surfaces = original_mesh->get_surface_count(); + + PackedVector3Array impact_points; + + PackedVector3Array cap_section_vertices; + PackedVector3Array cap_section_normals; + PackedVector3Array cap_section_flipped_normals; + PackedFloat32Array cap_section_tangents; + PackedVector2Array cap_section_uvs; + PackedVector2Array cap_section_uv2s; + PackedColorArray cap_section_colours; + PackedInt32Array cap_section_bones; + PackedFloat32Array cap_section_weights; + PackedInt32Array cap_section_indices; + + for (uint8_t surface = 0; surface < num_surfaces; surface++) + { + const Array &surface_arrays = original_mesh->surface_get_arrays(surface); + const PackedVector3Array &surface_vertex_array = surface_arrays[ArrayMesh::ARRAY_VERTEX]; + const PackedVector3Array &surface_normal_array = surface_arrays[ArrayMesh::ARRAY_NORMAL]; + const PackedFloat32Array &surface_tangent_array = surface_arrays[ArrayMesh::ARRAY_TANGENT]; + const PackedVector2Array &surface_uv_array = surface_arrays[ArrayMesh::ARRAY_TEX_UV]; + const PackedVector2Array &surface_uv2_array = surface_arrays[ArrayMesh::ARRAY_TEX_UV2]; + const PackedColorArray &surface_colour_array = surface_arrays[ArrayMesh::ARRAY_COLOR]; + const PackedInt32Array &surface_bone_array = surface_arrays[ArrayMesh::ARRAY_BONES]; + const PackedFloat32Array &surface_weight_array = surface_arrays[ArrayMesh::ARRAY_WEIGHTS]; + + const Vector3 *surface_vertex_array_ptr = surface_vertex_array.ptr(); + const uint32_t num_vertices = surface_vertex_array.size(); + + std::vector vertex_distance; + vertex_distance.resize(num_vertices); + + std::map base_to_sliced_vert_index; + std::map base_to_other_sliced_vert_index; + + const bool has_normal = surface_normal_array.size() >= num_vertices; + const bool has_tangent = surface_tangent_array.size() >= num_vertices * 4; + const bool has_uv = surface_uv_array.size() >= num_vertices; + const bool has_uv2 = surface_uv2_array.size() >= num_vertices; + const bool has_colour = surface_colour_array.size() >= num_vertices; + const bool has_bone = surface_bone_array.size() >= num_vertices * 4; + const bool has_weight = surface_weight_array.size() >= num_vertices * 4; + + const uint32_t bone_array_size = surface_bone_array.size(); + const uint8_t num_bones_per_vertex = bone_array_size / num_vertices; + + PackedVector3Array first_half_section_vertices; + PackedVector3Array first_half_section_normals; + PackedFloat32Array first_half_section_tangents; + PackedVector2Array first_half_section_uvs; + PackedVector2Array first_half_section_uv2s; + PackedColorArray first_half_section_colours; + PackedInt32Array first_half_section_bones; + PackedFloat32Array first_half_section_weights; + + PackedVector3Array other_half_section_vertices; + PackedVector3Array other_half_section_normals; + PackedFloat32Array other_half_section_tangents; + PackedVector2Array other_half_section_uvs; + PackedVector2Array other_half_section_uv2s; + PackedColorArray other_half_section_colours; + PackedInt32Array other_half_section_bones; + PackedFloat32Array other_half_section_weights; + + std::vector clip_edges; + + for (uint32_t vertex = 0; vertex < num_vertices; vertex++) + { + vertex_distance[vertex] = local_plane.distance_to(surface_vertex_array_ptr[vertex]); + + if (vertex_distance[vertex] >= 0.0f) + { + base_to_sliced_vert_index[vertex] = first_half_section_vertices.size(); + + first_half_section_vertices.append(surface_vertex_array_ptr[vertex]); + if (has_normal) { first_half_section_normals.append(surface_normal_array.ptr()[vertex]); } + if (has_uv) { first_half_section_uvs.append(surface_uv_array.ptr()[vertex]); } + if (has_uv2) { first_half_section_uv2s.append(surface_uv2_array.ptr()[vertex]); } + if (has_colour) { first_half_section_colours.append(surface_colour_array.ptr()[vertex]); } + if (has_tangent) + { + first_half_section_tangents.append(surface_tangent_array.ptr()[vertex * 4]); + first_half_section_tangents.append(surface_tangent_array.ptr()[(vertex * 4) + 1]); + first_half_section_tangents.append(surface_tangent_array.ptr()[(vertex * 4) + 2]); + first_half_section_tangents.append(surface_tangent_array.ptr()[(vertex * 4) + 3]); + } + if (has_bone) + { + for (uint8_t i = 0; i < num_bones_per_vertex; i++) + { + first_half_section_bones.append(surface_bone_array.ptr()[(vertex * num_bones_per_vertex) + i]); + first_half_section_weights.append(surface_weight_array.ptr()[(vertex * num_bones_per_vertex) + i]); + } + } + } + else + { + base_to_other_sliced_vert_index[vertex] = other_half_section_vertices.size(); + + other_half_section_vertices.append(surface_vertex_array_ptr[vertex]); + if (has_normal) { other_half_section_normals.append(surface_normal_array.ptr()[vertex]); } + if (has_uv) { other_half_section_uvs.append(surface_uv_array.ptr()[vertex]); } + if (has_uv2) { other_half_section_uv2s.append(surface_uv2_array.ptr()[vertex]); } + if (has_colour) { other_half_section_colours.append(surface_colour_array.ptr()[vertex]); } + if (has_tangent) + { + other_half_section_tangents.append(surface_tangent_array.ptr()[vertex * 4]); + other_half_section_tangents.append(surface_tangent_array.ptr()[(vertex * 4) + 1]); + other_half_section_tangents.append(surface_tangent_array.ptr()[(vertex * 4) + 2]); + other_half_section_tangents.append(surface_tangent_array.ptr()[(vertex * 4) + 3]); + } + if (has_bone) + { + for (uint8_t i = 0; i < num_bones_per_vertex; i++) + { + other_half_section_bones.append(surface_bone_array.ptr()[(vertex * num_bones_per_vertex) + i]); + other_half_section_weights.append(surface_weight_array.ptr()[(vertex * num_bones_per_vertex) + i]); + } + } + } + } + + PackedInt32Array first_half_section_indices; + PackedInt32Array other_half_section_indices; + const PackedInt32Array &surface_index_array = surface_arrays[ArrayMesh::ARRAY_INDEX]; + + if (!(first_half_section_vertices.size() > 0 && other_half_section_vertices.size() > 0)) + { + if (first_half_section_vertices.size() > 0) + { + first_half_section_indices = surface_index_array; + } + else if (other_half_section_vertices.size() > 0) + { + other_half_section_indices = surface_index_array; + } + } + else + { + const uint32_t num_triangles = surface_index_array.size(); + for (uint32_t triangle_index = 0; triangle_index < num_triangles; triangle_index += 3) + { + uint32_t base_v[3]; + std::map::iterator sliced_v[3]; + std::map::iterator sliced_other_v[3]; + + const std::map::iterator base_to_sliced_end = base_to_sliced_vert_index.end(); + const std::map::iterator base_to_other_sliced_end = base_to_other_sliced_vert_index.end(); + for (uint32_t i = 0; i < 3; i++) + { + base_v[i] = surface_index_array.ptr()[triangle_index + i]; + sliced_v[i] = base_to_sliced_vert_index.find(base_v[i]); + sliced_other_v[i] = base_to_other_sliced_vert_index.find(base_v[i]); + + // All vertex indices must be represented by one of the two slice index maps. + assert((sliced_v[i] != base_to_sliced_vert_index.end()) != (sliced_other_v[i] != base_to_other_sliced_vert_index.end())); + } + + if (sliced_v[0] != base_to_sliced_end && sliced_v[1] != base_to_sliced_end && sliced_v[2] != base_to_sliced_end) + { // If the triangle is entirely in the first slice, send all the vertices to the first slice. + first_half_section_indices.append(sliced_v[0]->second); + first_half_section_indices.append(sliced_v[1]->second); + first_half_section_indices.append(sliced_v[2]->second); + } + else if (sliced_other_v[0] != base_to_other_sliced_end && sliced_other_v[1] != base_to_other_sliced_end && sliced_other_v[2] != base_to_other_sliced_end) + { // If the triangle is entirely in the second slice, send all the vertices to the second slice. + other_half_section_indices.append(sliced_other_v[0]->second); + other_half_section_indices.append(sliced_other_v[1]->second); + other_half_section_indices.append(sliced_other_v[2]->second); + } + else + { // If the triangle is split by the slice plane, then slice the overlapping edges. + uint32_t final_verts[4] = { 0, 0, 0, 0 }; + uint8_t num_final_verts = 0; + + uint32_t other_final_verts[4] = { 0, 0, 0, 0 }; + uint8_t num_other_final_verts = 0; + + MeshEditEdge3D new_clip_edge; + uint8_t clipped_edges = 0; + + float plane_distance[3] = { + vertex_distance[base_v[0]], + vertex_distance[base_v[1]], + vertex_distance[base_v[2]] + }; + + for (uint32_t this_vert = 0; this_vert < 3; this_vert++) + { + if (sliced_v[this_vert] != base_to_sliced_end) + { + final_verts[num_final_verts] = sliced_v[this_vert]->second; + num_final_verts++; + } + else + { + other_final_verts[num_other_final_verts] = sliced_other_v[this_vert]->second; + num_other_final_verts++; + } + + uint32_t next_vert = (this_vert + 1) % 3; + if ((sliced_v[this_vert] == base_to_sliced_end) != (sliced_v[next_vert] == base_to_sliced_end)) + { + float alpha = UtilityFunctions::clampf(-plane_distance[this_vert] / (plane_distance[next_vert] - plane_distance[this_vert]), 0.0f, 1.0f); + + const Vector3 interp_vert = surface_vertex_array_ptr[base_v[this_vert]].lerp( + surface_vertex_array_ptr[base_v[next_vert]], alpha); + + final_verts[num_final_verts++] = first_half_section_vertices.size(); + other_final_verts[num_other_final_verts++] = other_half_section_vertices.size(); + + // Lerp the skinned vertex position here; it's necessary for projecting vertices to a 2D plane correctly. + // This part needs to be finished once we can get the skinned vertex positions. + // DON'T FORGET TO DO THAT + const Vector3 skinned_lerp = interp_vert; + + MeshEditVert3D edge_vertex; + edge_vertex.index = first_half_section_vertices.size(); + edge_vertex.position = skinned_lerp; + if (clipped_edges == 0) + { + new_clip_edge.v0 = edge_vertex; + } + else + { + new_clip_edge.v1 = edge_vertex; + } + clipped_edges++; + assert(clipped_edges <= 2); + + first_half_section_vertices.append(interp_vert); + other_half_section_vertices.append(interp_vert); + + if (has_normal) + { + const Vector3 interp_normal = surface_normal_array.ptr()[base_v[this_vert]].slerp(surface_normal_array.ptr()[base_v[next_vert]], alpha); + first_half_section_normals.append(interp_normal); + other_half_section_normals.append(interp_normal); + } + + if (has_tangent) + { + const float *this_tangent_start = (surface_tangent_array.ptr() + (base_v[this_vert] * 4)); + const float *next_tangent_start = (surface_tangent_array.ptr() + (base_v[next_vert] * 4)); + const Vector3 &this_tangent = Vector3(*this_tangent_start, *(this_tangent_start + 1), *(this_tangent_start + 2)); + const Vector3 &next_tangent = Vector3(*next_tangent_start, *(next_tangent_start + 1), *(next_tangent_start + 2)); + const float this_binormal = *(this_tangent_start + 3); + const float next_binormal = *(next_tangent_start + 3); + + const Vector3 &interp_tangent = this_tangent.slerp(next_tangent, alpha); + const uint8_t &interp_binormal = UtilityFunctions::roundi(UtilityFunctions::lerpf(this_binormal, next_binormal, alpha)); + + first_half_section_tangents.append(interp_tangent.x); + first_half_section_tangents.append(interp_tangent.y); + first_half_section_tangents.append(interp_tangent.z); + first_half_section_tangents.append(interp_binormal); + + other_half_section_tangents.append(interp_tangent.x); + other_half_section_tangents.append(interp_tangent.y); + other_half_section_tangents.append(interp_tangent.z); + other_half_section_tangents.append(interp_binormal); + } + + if (has_uv) + { + const Vector2 &interp_uv = surface_uv_array.ptr()[base_v[this_vert]].lerp(surface_uv_array.ptr()[base_v[next_vert]], alpha); + first_half_section_uvs.append(interp_uv); + other_half_section_uvs.append(interp_uv); + } + + if (has_uv2) + { + const Vector2 &interp_uv2 = surface_uv2_array.ptr()[base_v[this_vert]].lerp(surface_uv2_array.ptr()[base_v[next_vert]], alpha); + first_half_section_uv2s.append(interp_uv2); + other_half_section_uv2s.append(interp_uv2); + } + + if (has_colour) + { + const Color &interp_colour = surface_colour_array.ptr()[base_v[this_vert]].lerp(surface_colour_array.ptr()[base_v[next_vert]], alpha); + first_half_section_colours.append(interp_colour); + other_half_section_colours.append(interp_colour); + } + + if (has_bone && has_weight) + { + const int32_t *bone_pointer = surface_bone_array.ptr(); + const float *weight_pointer = surface_weight_array.ptr(); + std::vector> interp_bones_and_weights; + + // First pack all bones and weights into a list of pairs + uint8_t i = 0; + for (i = 0; i < num_bones_per_vertex; i++) + { + interp_bones_and_weights.emplace_back(std::pair( + *(bone_pointer + (base_v[this_vert] * num_bones_per_vertex) + i), + *(weight_pointer + (base_v[this_vert] * num_bones_per_vertex) + i) + )); + } + + // Next, find each first half bone that matches one in the second half, and interpolate between them. + // If the second half has a unique bone, interpolate a new value from 0 for it. + for (uint32_t other_bone_index = 0; other_bone_index < num_bones_per_vertex; other_bone_index++) + { + const uint32_t other_bone = *(bone_pointer + (base_v[next_vert] * num_bones_per_vertex) + other_bone_index); + const float other_weight = *(weight_pointer + (base_v[next_vert] * num_bones_per_vertex) + other_bone_index); + int8_t matching_bone_index = -1; + for (uint8_t i = 0; i < interp_bones_and_weights.size(); i++) + { + if (interp_bones_and_weights[i].first == other_bone) + { + matching_bone_index = i; + break; + } + } + if (matching_bone_index != -1) + { + interp_bones_and_weights[matching_bone_index].second = UtilityFunctions::lerpf( + interp_bones_and_weights[matching_bone_index].second, other_weight, alpha); + } + else + { + interp_bones_and_weights.emplace_back(std::pair( + other_bone, (float)UtilityFunctions::lerpf(0.0f, other_weight, alpha))); + } + } + + // Sort the list of bones and weights from highest weight to lowest weight + std::sort(interp_bones_and_weights.begin(), interp_bones_and_weights.end(), + [=](std::pair&a, std::pair&b) + { return a.second > b.second; }); + + // Finally, add all our new interpolated bones and weights to the arrays + for (i = 0; i < num_bones_per_vertex; i++) + { + first_half_section_bones.append(interp_bones_and_weights[i].first); + first_half_section_weights.append(interp_bones_and_weights[i].second); + + other_half_section_bones.append(interp_bones_and_weights[i].first); + other_half_section_weights.append(interp_bones_and_weights[i].second); + } + } + } + } + + // There should always be exactly two sliced edges per triangle + assert(clipped_edges == 2); + clip_edges.emplace_back(new_clip_edge); + + for (uint32_t vertex_index = 2; vertex_index < num_final_verts; vertex_index++) + { + first_half_section_indices.append(final_verts[0]); + first_half_section_indices.append(final_verts[vertex_index - 1]); + first_half_section_indices.append(final_verts[vertex_index]); + } + + for (uint32_t vertex_index = 2; vertex_index < num_other_final_verts; vertex_index++) + { + other_half_section_indices.append(other_final_verts[0]); + other_half_section_indices.append(other_final_verts[vertex_index - 1]); + other_half_section_indices.append(other_final_verts[vertex_index]); + } + } + } + } + + if (first_half_section_vertices.size() > 0 && first_half_section_indices.size() > 0) + { + Array first_half_section; + first_half_section.resize(ArrayMesh::ARRAY_MAX); + first_half_section[ArrayMesh::ARRAY_VERTEX] = first_half_section_vertices; + if (has_normal) first_half_section[ArrayMesh::ARRAY_NORMAL] = first_half_section_normals; + if (has_tangent) first_half_section[ArrayMesh::ARRAY_TANGENT] = first_half_section_tangents; + if (has_uv) first_half_section[ArrayMesh::ARRAY_TEX_UV] = first_half_section_uvs; + if (has_uv2) first_half_section[ArrayMesh::ARRAY_TEX_UV2] = first_half_section_uv2s; + if (has_colour) first_half_section[ArrayMesh::ARRAY_COLOR] = first_half_section_colours; + if (has_bone) first_half_section[ArrayMesh::ARRAY_BONES] = first_half_section_bones; + if (has_weight) first_half_section[ArrayMesh::ARRAY_WEIGHTS] = first_half_section_weights; + first_half_section[ArrayMesh::ARRAY_INDEX] = first_half_section_indices; + + out_first_half->add_surface_from_arrays(ArrayMesh::PRIMITIVE_TRIANGLES, first_half_section); + out_first_half->surface_set_material(surface, original_mesh->surface_get_material(surface)); + } + + if (other_half_section_vertices.size() > 0 && other_half_section_indices.size() > 0) + { + Array other_half_section; + other_half_section.resize(ArrayMesh::ARRAY_MAX); + other_half_section[ArrayMesh::ARRAY_VERTEX] = other_half_section_vertices; + if (has_normal) other_half_section[ArrayMesh::ARRAY_NORMAL] = other_half_section_normals; + if (has_tangent) other_half_section[ArrayMesh::ARRAY_TANGENT] = other_half_section_tangents; + if (has_uv) other_half_section[ArrayMesh::ARRAY_TEX_UV] = other_half_section_uvs; + if (has_uv2) other_half_section[ArrayMesh::ARRAY_TEX_UV2] = other_half_section_uv2s; + if (has_colour) other_half_section[ArrayMesh::ARRAY_COLOR] = other_half_section_colours; + if (has_bone) other_half_section[ArrayMesh::ARRAY_BONES] = other_half_section_bones; + if (has_weight) other_half_section[ArrayMesh::ARRAY_WEIGHTS] = other_half_section_weights; + other_half_section[ArrayMesh::ARRAY_INDEX] = other_half_section_indices; + + out_other_half->add_surface_from_arrays(ArrayMesh::PRIMITIVE_TRIANGLES, other_half_section); + out_other_half->surface_set_material(surface, original_mesh->surface_get_material(surface)); + } + + if (clip_edges.size() > 0) + { + std::vector edges_2d; + std::vector polygon_set; + std::vector error_polygons; + MeshEditingLibrary::project_edges(edges_2d, original_mesh_instance->get_transform(), clip_edges, local_plane); + MeshEditingLibrary::build_2d_polygons_from_edges(polygon_set, edges_2d, error_polygons); + + MeshSlicePlaneOrientation uv_plane = MeshSlicePlaneOrientation::Z; + const Basis &mesh_instance_basis = original_mesh_instance->get_basis(); + const Vector3 &local_plane_normal = local_plane.get_normal(); + if (UtilityFunctions::absf(local_plane_normal.dot(mesh_instance_basis.xform(Vector3(0.0f, 1.0f, 0.0f)))) > 0.5f) + { + uv_plane = MeshSlicePlaneOrientation::Y; + } + else if (UtilityFunctions::absf(local_plane_normal.dot(mesh_instance_basis.xform(Vector3(1.0f, 0.0f, 0.0f)))) > 0.5f) + { + uv_plane = MeshSlicePlaneOrientation::X; + } + + const Vector3 &mesh_bounds = original_mesh->get_aabb().get_size(); + const uint32_t num_polygons = polygon_set.size(); + for (uint32_t polygon_index = 0; polygon_index < num_polygons; polygon_index++) + { + using Point = std::array; + using Polygon = std::vector>; + + Polygon earcut_polygon; + std::vector sub_polygon; + + Vector3 polygon_centroid; + + const uint32_t polygon_vertex_base = cap_section_vertices.size(); + for (const MeshEditVert2D &vertex : polygon_set[polygon_index].vertices) + { + const Vector3 &position = first_half_section_vertices[vertex.index]; + + cap_section_vertices.append(position); + if (has_normal) { cap_section_normals.append(local_plane_normal * -1.0f); cap_section_flipped_normals.append(local_plane_normal); } + // if (has_tangent) { for (uint8_t i = 0; i < 4; i++) { cap_section_tangents.append(first_half_section_tangents[(vertex.index * 4) + i]); } } + if (has_colour) { cap_section_colours.append(first_half_section_colours[vertex.index]); } + if (has_uv) { cap_section_uvs.append(MeshEditingLibrary::calculate_planar_uv(position, mesh_bounds, uv_plane)); } + if (has_uv2) { cap_section_uv2s.append(first_half_section_uv2s[vertex.index]); } + if (has_bone) { for (uint8_t i = 0; i < num_bones_per_vertex; i++) { cap_section_bones.append(first_half_section_bones[(vertex.index * num_bones_per_vertex) + i]); } } + if (has_weight) { for (uint8_t i = 0; i < num_bones_per_vertex; i++) { cap_section_bones.append(first_half_section_weights[(vertex.index * num_bones_per_vertex) + i]); } } + + sub_polygon.push_back({vertex.position.x, vertex.position.y}); + polygon_centroid += position; + } + + earcut_polygon.push_back(sub_polygon); + std::vector indices = mapbox::earcut(earcut_polygon); + for (uint32_t i = 0; (i+2) < indices.size(); i += 3) + { + cap_section_indices.append(indices[i+1] + polygon_vertex_base); + cap_section_indices.append(indices[i] + polygon_vertex_base); + cap_section_indices.append(indices[i+2] + polygon_vertex_base); + } + + polygon_centroid /= polygon_set[polygon_index].vertices.size(); + impact_points.append(polygon_centroid); + } + } + } + + if (cap_section_vertices.size() > 0 && cap_section_indices.size() > 0) + { + Array cap_mesh_section; + cap_mesh_section.resize(ArrayMesh::ARRAY_MAX); + cap_mesh_section[ArrayMesh::ARRAY_VERTEX] = cap_section_vertices; + cap_mesh_section[ArrayMesh::ARRAY_INDEX] = cap_section_indices; + if (cap_section_normals.size()) cap_mesh_section[ArrayMesh::ARRAY_NORMAL] = cap_section_normals; + // if (cap_section_tangents.size()) cap_mesh_section[ArrayMesh::ARRAY_TANGENT] = cap_section_tangents; + if (cap_section_colours.size()) cap_mesh_section[ArrayMesh::ARRAY_COLOR] = cap_section_colours; + if (cap_section_uvs.size()) cap_mesh_section[ArrayMesh::ARRAY_TEX_UV] = cap_section_uvs; + if (cap_section_uv2s.size()) cap_mesh_section[ArrayMesh::ARRAY_TEX_UV2] = cap_section_uv2s; + if (cap_section_bones.size()) cap_mesh_section[ArrayMesh::ARRAY_BONES] = cap_section_bones; + if (cap_section_weights.size()) cap_mesh_section[ArrayMesh::ARRAY_WEIGHTS] = cap_section_weights; + out_first_half->add_surface_from_arrays(ArrayMesh::PRIMITIVE_TRIANGLES, cap_mesh_section); + out_first_half->surface_set_material(out_first_half->get_surface_count()-1, cap_material); + + const uint32_t num_cap_triangles = cap_section_indices.size(); + for (uint32_t i = 0; i < num_cap_triangles; i += 3) + { + const int32_t old_index = cap_section_indices[i]; + cap_section_indices[i] = cap_section_indices[i+1]; + cap_section_indices[i+1] = old_index; + } + + Array cap_other_mesh_section; + cap_other_mesh_section.resize(ArrayMesh::ARRAY_MAX); + cap_other_mesh_section[ArrayMesh::ARRAY_VERTEX] = cap_section_vertices; + cap_other_mesh_section[ArrayMesh::ARRAY_INDEX] = cap_section_indices; + if (cap_section_normals.size()) cap_other_mesh_section[ArrayMesh::ARRAY_NORMAL] = cap_section_flipped_normals; + // if (cap_section_tangents.size()) cap_other_mesh_section[ArrayMesh::ARRAY_TANGENT] = cap_section_flipped_tangents; + if (cap_section_colours.size()) cap_other_mesh_section[ArrayMesh::ARRAY_COLOR] = cap_section_colours; + if (cap_section_uvs.size()) cap_other_mesh_section[ArrayMesh::ARRAY_TEX_UV] = cap_section_uvs; + if (cap_section_uv2s.size()) cap_other_mesh_section[ArrayMesh::ARRAY_TEX_UV2] = cap_section_uv2s; + if (cap_section_bones.size()) cap_other_mesh_section[ArrayMesh::ARRAY_BONES] = cap_section_bones; + if (cap_section_weights.size()) cap_other_mesh_section[ArrayMesh::ARRAY_WEIGHTS] = cap_section_weights; + out_other_half->add_surface_from_arrays(ArrayMesh::PRIMITIVE_TRIANGLES, cap_other_mesh_section); + out_other_half->surface_set_material(out_other_half->get_surface_count()-1, cap_material); + } + + return impact_points; +} + +void MeshEditingLibrary::project_edges(std::vector &out_2d_edges, const Transform3D &to_node_space, const std::vector &in_3d_edges, const Plane &plane) +{ + out_2d_edges.resize(in_3d_edges.size()); + + const Transform3D &plane_inverse_transform = Transform3D(Basis::looking_at(plane.get_normal()), plane.center()).inverse(); + + for (uint32_t i = 0; i < in_3d_edges.size(); i++) + { + MeshEditVert2D v0; + Vector3 p = plane_inverse_transform.xform(in_3d_edges[i].v0.position); + v0.index = in_3d_edges[i].v0.index; + v0.position.x = p.x; + v0.position.y = p.y; + + MeshEditVert2D v1; + p = plane_inverse_transform.xform(in_3d_edges[i].v1.position); + v1.index = in_3d_edges[i].v1.index; + v1.position.x = p.x; + v1.position.y = p.y; + + out_2d_edges[i].v0 = v0; + out_2d_edges[i].v1 = v1; + } +} + +void MeshEditingLibrary::build_2d_polygons_from_edges(std::vector &out_polygons, const std::vector &in_edges, std::vector &error_polygons) +{ + std::vector edge_set = in_edges; + + while (edge_set.size() > 0) + { + MeshEditPolygon2D new_polygon; + const MeshEditEdge2D &first_edge = edge_set.back(); + edge_set.pop_back(); + + new_polygon.vertices.emplace_back(first_edge.v0); + new_polygon.vertices.emplace_back(first_edge.v1); + + MeshEditVert2D &polygon_end = new_polygon.vertices.back(); + MeshEditEdge2D next_edge; + while (MeshEditingLibrary::find_next_edge(next_edge, edge_set, polygon_end)) + { + new_polygon.vertices.emplace_back(next_edge.v1); + polygon_end = new_polygon.vertices.back(); + } + + if (new_polygon.vertices.size() >= 4 && (new_polygon.vertices.front().position - new_polygon.vertices.back().position).length_squared() < PRETTY_SMALL_NUMBER) + { + new_polygon.vertices.pop_back(); + MeshEditingLibrary::fix_polygon_winding(new_polygon); + out_polygons.emplace_back(new_polygon); + } + else + { + error_polygons.emplace_back(new_polygon); + } + } +} + +bool MeshEditingLibrary::find_next_edge(MeshEditEdge2D &out_next_edge, std::vector &in_edge_set, const MeshEditVert2D &start) +{ + float closest_squared_distance = FLT_MAX; + int32_t out_edge_index = -1; + + // Search the edges for one that starts closest to the starting point + uint32_t num_in_edges = in_edge_set.size(); + for (uint32_t i = 0; i < num_in_edges; i++) + { + float distance_squared = (in_edge_set[i].v0.position - start.position).length_squared(); + if (distance_squared < closest_squared_distance) + { + closest_squared_distance = distance_squared; + out_next_edge = in_edge_set[i]; + out_edge_index = i; + } + + distance_squared = (in_edge_set[i].v1.position - start.position).length_squared(); + if (distance_squared < closest_squared_distance) + { + closest_squared_distance = distance_squared; + out_next_edge = in_edge_set[i]; + std::swap(out_next_edge.v0, out_next_edge.v1); + out_edge_index = i; + } + } + + // If the next edge starts close enough, return it + if (closest_squared_distance < TINY_NUMBER) + { + assert(out_edge_index >= 0); + in_edge_set.erase(in_edge_set.begin() + out_edge_index); + return true; + } + + return false; +} + +void MeshEditingLibrary::fix_polygon_winding(MeshEditPolygon2D &polygon) +{ + float total_angle = 0.0f; + for (int32_t i = polygon.vertices.size() - 1; i >= 0; i--) + { + const int32_t a_index = (i - 1) % polygon.vertices.size(); + const int32_t b_index = i; + const int32_t c_index = (i + 1) % polygon.vertices.size(); + + const float ab_dist_squared = (polygon.vertices[b_index].position - polygon.vertices[a_index].position).length_squared(); + const Vector2 ab_edge = (polygon.vertices[b_index].position - polygon.vertices[a_index].position).normalized(); + + const float bc_dist_squared = (polygon.vertices[c_index].position - polygon.vertices[b_index].position).length_squared(); + const Vector2 bc_edge = (polygon.vertices[c_index].position - polygon.vertices[b_index].position).normalized(); + + if (ab_dist_squared < TINY_NUMBER || bc_dist_squared < TINY_NUMBER || (ab_edge - bc_edge).length_squared() < TEENY_TINY_NUMBER) + { + polygon.vertices.erase(polygon.vertices.begin() + i); + } + else + { + total_angle += UtilityFunctions::asin(ab_edge.x * bc_edge.y - ab_edge.y * bc_edge.x); + } + } + + if (total_angle < 0.0f) + { + const uint32_t num_vertices = polygon.vertices.size(); + + std::vector new_vertices; + new_vertices.resize(num_vertices); + for (uint32_t i = 0; i < num_vertices; i++) + { + new_vertices[i] = polygon.vertices[num_vertices - (i + 1)]; + } + polygon.vertices = new_vertices; + } +} + + +const Vector2 MeshEditingLibrary::calculate_planar_uv(const Vector3 &vertex, const Vector3 &mesh_bounds, const MeshSlicePlaneOrientation &axis) +{ + switch(axis) + { + case MeshSlicePlaneOrientation::X: + return Vector2(vertex.y / mesh_bounds.y, vertex.z / mesh_bounds.z); + case MeshSlicePlaneOrientation::Y: + return Vector2(vertex.x / mesh_bounds.x, vertex.z / mesh_bounds.z); + case MeshSlicePlaneOrientation::Z: default: + return Vector2(vertex.x / mesh_bounds.x, vertex.y / mesh_bounds.y); + } +} diff --git a/src/liborng/nodes/mesh_editing_library.h b/src/liborng/nodes/mesh_editing_library.h new file mode 100644 index 0000000..92ff554 --- /dev/null +++ b/src/liborng/nodes/mesh_editing_library.h @@ -0,0 +1,98 @@ +/* + * ©2026 Batty Bovine Productions, LLC. All Rights Reserved. + * + * If this source code makes it to the public internet, this software can be + * considered to be protected by the MIT licence. Have fun with it. + */ + +#pragma once + +#include +#include +#include +#include +#include +#include +using namespace godot; + +#include + + +struct MeshEditVert3D +{ + uint32_t index; // Index into the original vertex array + Vector3 position; // Position used for generating geometry +}; + +struct MeshEditVert2D +{ + uint32_t index; // Index into the original vertex array + Vector2 position; // Position used for generating geometry +}; + +struct MeshEditEdge2D +{ + MeshEditVert2D v0; // Start vertex + MeshEditVert2D v1; // End vertex +}; + +struct MeshEditEdge3D +{ + MeshEditVert3D v0; // Start vertex + MeshEditVert3D v1; // End vertex +}; + +struct MeshEditPolygon2D +{ + std::vector vertices; // List of vertices representing a closed 2D polygon +}; + + +class MeshEditingLibrary : public Node +{ + GDCLASS(MeshEditingLibrary, Node); + +public: + MeshEditingLibrary(); + ~MeshEditingLibrary(); + + enum MeshEditCapUV + { + CAP_UV_FILL_MESH_BOUNDS, + CAP_UV_FILL_CAP_BOUNDS, + CAP_UV_TILED + }; + + static TypedArray get_skinned_vertex_positions(const Skeleton3D *skeleton, const Mesh *mesh, const Transform3D &component_to_world); + static PackedVector3Array slice_mesh(const MeshInstance3D *original_mesh_instance, const Plane &local_plane, ArrayMesh *out_first_half, ArrayMesh *out_other_half, const MeshEditCapUV cap_option, const Ref cap_material); + +protected: + enum MeshSlicePlaneOrientation + { + X, + Y, + Z + }; + +private: + static const Vector2 calculate_planar_uv(const Vector3 &vertex, const Vector3 &mesh_bounds, const MeshSlicePlaneOrientation &axis); + + static void project_edges(std::vector &out_2d_edges, const Transform3D &to_node_space, const std::vector &in_3d_edges, const Plane &plane); + static void build_2d_polygons_from_edges(std::vector &out_polygons, const std::vector &in_edges, std::vector &error_polygons); + static bool find_next_edge(MeshEditEdge2D &out_next_edge, std::vector &in_edge_set, const MeshEditVert2D &start); + static void fix_polygon_winding(MeshEditPolygon2D &polygon); + + // Godot boilerplate below +protected: + static void _bind_methods() + { + ClassDB::bind_static_method("MeshEditingLibrary", D_METHOD("get_skinned_vertex_positions", "skeleton", "mesh"), &MeshEditingLibrary::get_skinned_vertex_positions); + ClassDB::bind_static_method("MeshEditingLibrary", D_METHOD("slice_mesh", "original_mesh_instance", "local_plane", "out_first_half", "out_other_half", "cap_option", "cap_material"), &MeshEditingLibrary::slice_mesh); + + BIND_ENUM_CONSTANT(CAP_UV_FILL_MESH_BOUNDS); + BIND_ENUM_CONSTANT(CAP_UV_FILL_CAP_BOUNDS); + BIND_ENUM_CONSTANT(CAP_UV_TILED); + } +}; + +VARIANT_ENUM_CAST(MeshEditingLibrary::MeshEditCapUV); diff --git a/src/liborng/orng_macros.h b/src/liborng/orng_macros.h index 3796b60..f4ef3ea 100644 --- a/src/liborng/orng_macros.h +++ b/src/liborng/orng_macros.h @@ -1,5 +1,13 @@ #pragma once +/** + * Helper constants + */ +#define TEENY_TINY_NUMBER 0.000000000001f +#define TINY_NUMBER 0.0000000001f +#define PRETTY_SMALL_NUMBER 0.00000001f +#define SMALL_NUMBER 0.000001f + /** * Method and property binding helpers */ @@ -27,4 +35,4 @@ * Deferred function helpers */ #define CALL_NEXT_FRAME(C) \ - this->get_tree()->create_timer(0.001)->connect("timeout", C) + this->get_tree()->create_timer(SMALL_NUMBER)->connect("timeout", C) diff --git a/src/liborng/register_types.cpp b/src/liborng/register_types.cpp index fc1091e..ada301e 100644 --- a/src/liborng/register_types.cpp +++ b/src/liborng/register_types.cpp @@ -25,6 +25,8 @@ #include "resources/level_metadata_map.h" #include "resources/level_metadata_resource.h" +#include "nodes/mesh_editing_library.h" + #include "singletons/input_handler.h" #include "singletons/save_manager.h" #include "singletons/scene_loader.h" @@ -66,6 +68,8 @@ void initialize_orng_module(ModuleInitializationLevel p_level) { ClassDB::register_class(); ClassDB::register_class(); + + ClassDB::register_class(); GDSINGLETON_REGISTER_CLASS(InputHandler, _input_handler_singleton); GDSINGLETON_REGISTER_CLASS(SaveManager, _save_manager_singleton);