| #include "rive/math/rectangles_to_contour.hpp" | |
| #include <algorithm> | |
| using namespace rive; | |
| using RectEvent = RectanglesToContour::RectEvent; | |
| class SpanOffset | |
| { | |
| public: | |
| size_t start; | |
| size_t size; | |
| Span<RectEvent> toSpan(Span<RectEvent> base) | |
| { | |
| return Span<RectEvent>(base.data() + start, size); | |
| } | |
| }; | |
| // Returns SpanOffset relative to result so it can be called in succession | |
| // (re-allocating) prior to accessing data. | |
| static SpanOffset sortRectEvents(const std::vector<AABB>& rects, | |
| std::vector<RectEvent>& result, | |
| uint8_t axisA, | |
| uint8_t axisB) | |
| { | |
| size_t index = 0; | |
| size_t resultStart = result.size(); | |
| for (const AABB& rect : rects) | |
| { | |
| for (size_t pointIndex = 0; pointIndex < 2; pointIndex++) | |
| { | |
| Vec2D e = rect[pointIndex]; | |
| RectEvent event; | |
| event.type = (uint8_t)pointIndex; | |
| event.index = index; | |
| event.y = axisB == 0 ? e[axisA] : e[axisB]; | |
| event.x = axisB == 0 ? e[axisB] : e[axisA]; | |
| event.size = pointIndex == 0 ? rect[1][axisB] - e[axisB] | |
| : e[axisB] - rect[0][axisB]; | |
| result.push_back(event); | |
| } | |
| ++index; | |
| } | |
| std::sort(result.begin() + resultStart, | |
| result.end(), | |
| [&](const RectEvent& a, const RectEvent& b) { | |
| return a.getValue(axisB) < b.getValue(axisB); | |
| }); | |
| std::sort(result.begin() + resultStart, | |
| result.end(), | |
| [&](const RectEvent& a, const RectEvent& b) { | |
| return a.getValue(axisA) < b.getValue(axisA); | |
| }); | |
| return {resultStart, result.size() - resultStart}; | |
| } | |
| bool RectanglesToContour::isRectIncluded(size_t rectIndex) | |
| { | |
| return (m_rectInclusionBitset[rectIndex / 8] & (1 << (rectIndex % 8))) != 0; | |
| } | |
| void RectanglesToContour::markRectIncluded(size_t rectIndex, bool isIt) | |
| { | |
| if (isIt) | |
| { | |
| m_rectInclusionBitset[rectIndex / 8] |= 1 << (rectIndex % 8); | |
| } | |
| else | |
| { | |
| m_rectInclusionBitset[rectIndex / 8] &= ~(1 << (rectIndex % 8)); | |
| } | |
| } | |
| void RectanglesToContour::subdivideRectangles() | |
| { | |
| m_subdividedRects.clear(); | |
| m_uniquePoints.clear(); | |
| m_rectEvents.clear(); | |
| if (m_rects.empty()) | |
| { | |
| return; | |
| } | |
| auto evOffset = sortRectEvents(m_rects, m_rectEvents, 0, 1); | |
| auto ehOffset = sortRectEvents(m_rects, m_rectEvents, 1, 0); | |
| auto ev = evOffset.toSpan(m_rectEvents); | |
| auto eh = ehOffset.toSpan(m_rectEvents); | |
| size_t inclusionByteSize = m_rects.size() / 8 + 1; | |
| m_rectInclusionBitset.resize(inclusionByteSize); | |
| memset(m_rectInclusionBitset.data(), 0, inclusionByteSize); | |
| markRectIncluded(ev[0].index, true); | |
| int opened = 0; | |
| float beginY = 0; | |
| std::vector<AABB> result; | |
| for (size_t i = 0; i < ev.size() - 1; ++i) | |
| { | |
| const auto& eventV = ev[i]; | |
| markRectIncluded(eventV.index, eventV.type == 0); | |
| const auto& next = ev[i + 1]; | |
| float beginX = eventV.x; | |
| float endX = next.x; | |
| float deltaX = next.x - eventV.x; | |
| if (deltaX == 0) | |
| { | |
| continue; | |
| } | |
| for (size_t j = 0; j < eh.size(); ++j) | |
| { | |
| const auto& eventH = eh[j]; | |
| if (isRectIncluded(eventH.index)) | |
| { | |
| if (eventH.type == 0) | |
| { | |
| ++opened; | |
| if (opened == 1) | |
| { | |
| beginY = eventH.y; | |
| } | |
| } | |
| else | |
| { | |
| --opened; | |
| size_t n = 1; | |
| while (j + n < eh.size() && | |
| !isRectIncluded(eh[j + n].index)) | |
| { | |
| ++n; | |
| } | |
| const auto* next = | |
| (j + n < eh.size()) ? &eh[j + n] : nullptr; | |
| if (!next || (opened == 0 && next->y != eventH.y)) | |
| { | |
| float endY = eventH.y; | |
| m_subdividedRects.push_back( | |
| AABB(beginX, beginY, endX, endY)); | |
| } | |
| } | |
| } | |
| } | |
| } | |
| } | |
| void RectanglesToContour::reset() { m_rects.clear(); } | |
| void RectanglesToContour::addRect(const AABB& rect) | |
| { | |
| if (!m_rects.empty()) | |
| { | |
| AABB& last = m_rects.back(); | |
| if (last.minY == rect.minY && last.maxY == rect.maxY && | |
| last.maxX == rect.minX) | |
| { | |
| float minX = last.minX; | |
| float minY = last.minY; | |
| float maxY = last.maxY; | |
| m_rects.pop_back(); | |
| m_rects.emplace_back(AABB(minX, minY, rect.maxX, maxY)); | |
| return; | |
| } | |
| } | |
| m_rects.push_back(rect); | |
| } | |
| // Build contours and append them into contourPoints delineated by | |
| // contourOffsets. | |
| void extractPolygons(std::vector<ContourPoint>& contourPoints, | |
| std::vector<size_t>& contourOffsets, | |
| EdgeMap& edgesH, | |
| EdgeMap& edgesV) | |
| { | |
| while (!edgesH.empty()) | |
| { | |
| size_t contourStartOffset = contourPoints.size(); | |
| Vec2D start = edgesH.begin()->first; | |
| edgesH.erase(start); | |
| auto first = ContourPoint(start, 0); | |
| contourPoints.push_back(first); | |
| while (true) | |
| { | |
| auto& curr = contourPoints.back(); | |
| if (curr.dir == 0) | |
| { | |
| if (edgesV.find(curr.vec) != edgesV.end()) | |
| { | |
| auto next = edgesV[curr.vec]; | |
| edgesV.erase(curr.vec); | |
| contourPoints.emplace_back(next, 1); | |
| } | |
| else | |
| { | |
| break; | |
| } | |
| } | |
| else | |
| { | |
| if (edgesH.find(curr.vec) != edgesH.end()) | |
| { | |
| auto next = edgesH[curr.vec]; | |
| edgesH.erase(curr.vec); | |
| contourPoints.emplace_back(next, 0); | |
| } | |
| else | |
| { | |
| break; | |
| } | |
| } | |
| // Remove the last point if the first and last in the contour are | |
| // the same. | |
| if (first == contourPoints.back()) | |
| { | |
| contourPoints.pop_back(); | |
| break; | |
| } | |
| } | |
| contourOffsets.push_back(contourPoints.size()); | |
| for (size_t index = contourStartOffset; index < contourPoints.size(); | |
| index++) | |
| { | |
| Vec2D vertex = contourPoints[index].vec; | |
| edgesH.erase(vertex); | |
| edgesV.erase(vertex); | |
| } | |
| } | |
| } | |
| void RectanglesToContour::computeContours() | |
| { | |
| subdivideRectangles(); | |
| for (const auto& rect : m_subdividedRects) | |
| { | |
| addUniquePoint(Vec2D(rect.minX, rect.minY)); | |
| addUniquePoint(Vec2D(rect.maxX, rect.minY)); | |
| addUniquePoint(Vec2D(rect.maxX, rect.maxY)); | |
| addUniquePoint(Vec2D(rect.minX, rect.maxY)); | |
| } | |
| m_sortedPointsX.clear(); | |
| m_sortedPointsY.clear(); | |
| for (auto pt : m_uniquePoints) | |
| { | |
| m_sortedPointsX.push_back(pt); | |
| m_sortedPointsY.push_back(pt); | |
| } | |
| std::sort(m_sortedPointsX.begin(), | |
| m_sortedPointsX.end(), | |
| [](const Vec2D& a, const Vec2D& b) { | |
| return a.x < b.x || (a.x == b.x && a.y < b.y); | |
| }); | |
| std::sort(m_sortedPointsY.begin(), | |
| m_sortedPointsY.end(), | |
| [](const Vec2D& a, const Vec2D& b) { | |
| return a.y < b.y || (a.y == b.y && a.x < b.x); | |
| }); | |
| // std::unordered_map isn't guaranteed to reserve memory, so this clear | |
| // is more in prep for when we allow using allocators. We could also | |
| // experiment with a simple linear search in a vector as usually there | |
| // are not a lot of edges to traverse. | |
| m_edgesH.clear(); | |
| m_edgesV.clear(); | |
| size_t i = 0; | |
| while (i < m_sortedPointsY.size()) | |
| { | |
| float currY = m_sortedPointsY[i].y; | |
| while (i < m_sortedPointsY.size() && m_sortedPointsY[i].y == currY) | |
| { | |
| m_edgesH[m_sortedPointsY[i]] = m_sortedPointsY[i + 1]; | |
| m_edgesH[m_sortedPointsY[i + 1]] = m_sortedPointsY[i]; | |
| i += 2; | |
| } | |
| } | |
| i = 0; | |
| while (i < m_sortedPointsX.size()) | |
| { | |
| float currX = m_sortedPointsX[i].x; | |
| while (i < m_sortedPointsX.size() && m_sortedPointsX[i].x == currX) | |
| { | |
| m_edgesV[m_sortedPointsX[i]] = m_sortedPointsX[i + 1]; | |
| m_edgesV[m_sortedPointsX[i + 1]] = m_sortedPointsX[i]; | |
| i += 2; | |
| } | |
| } | |
| m_contourPoints.clear(); | |
| m_contourOffsets.clear(); | |
| extractPolygons(m_contourPoints, m_contourOffsets, m_edgesH, m_edgesV); | |
| } | |
| void RectanglesToContour::addUniquePoint(const Vec2D& point) | |
| { | |
| if (!m_uniquePoints.insert(point).second) | |
| { | |
| m_uniquePoints.erase(point); | |
| } | |
| } | |
| ContourItr& ContourItr::operator++() | |
| { | |
| m_contourIndex++; | |
| return *this; | |
| } | |
| Contour ContourItr::operator*() const | |
| { | |
| return m_rectanglesToContour->contour(m_contourIndex); | |
| } | |
| Contour RectanglesToContour::contour(size_t index) const | |
| { | |
| assert(index < m_contourOffsets.size()); | |
| size_t end = m_contourOffsets[index]; | |
| size_t start = index == 0 ? 0 : m_contourOffsets[index - 1]; | |
| const ContourPoint* data = m_contourPoints.data() + start; | |
| size_t size = end - start; | |
| return Contour(Span<const ContourPoint>(data, size)); | |
| } | |
| ContourPointItr& ContourPointItr::operator++() | |
| { | |
| Vec2D currentPoint = m_pointIndex < m_contour.size() ? *(*this) : Vec2D(); | |
| m_pointIndex++; | |
| while (m_pointIndex < m_contour.size()) | |
| { | |
| auto nextPoint = m_contour[m_pointIndex].vec; | |
| if (nextPoint != currentPoint) | |
| { | |
| break; | |
| } | |
| m_pointIndex++; | |
| } | |
| return *this; | |
| } | |
| Vec2D ContourPointItr::operator*() const { return m_contour[m_pointIndex].vec; } | |
| bool Contour::isClockwise() const | |
| { | |
| float a = 0.0f; | |
| size_t size = m_points.size(); | |
| if (size < 1) | |
| { | |
| return true; | |
| } | |
| for (size_t i = 1; i < size; i++) | |
| { | |
| a += Vec2D::cross(m_points[i - 1].vec, m_points[i].vec); | |
| } | |
| a += Vec2D::cross(m_points[size - 1].vec, m_points[0].vec); | |
| return a * 0.5f >= 0; | |
| } |