| /* |
| * Copyright 2025 Google LLC. |
| * |
| * Use of this source code is governed by a BSD-style license that can be |
| * found in the LICENSE file. |
| */ |
| |
| #include "include/core/SkPathTypes.h" |
| #include "include/core/SkRRect.h" |
| #include "include/core/SkSpan.h" |
| #include "include/private/base/SkFloatingPoint.h" |
| #include "include/private/base/SkMalloc.h" |
| #include "src/base/SkSafeMath.h" |
| #include "src/core/SkPathData.h" |
| #include "src/core/SkPathEnums.h" |
| #include "src/core/SkPathPriv.h" |
| #include "src/core/SkPathRawShapes.h" |
| #include "src/core/SkSpanPriv.h" |
| |
| #include <new> |
| #include <optional> |
| #include <type_traits> |
| |
| SkPathData* SkPathData::PeekEmptySingleton() { |
| static SkPathData* gEmpty = SkPathData::MakeNoCheck({}, {}, {}, {}, {}).release(); |
| return gEmpty; |
| } |
| |
| static uint32_t next_pathdata_unique_id() { |
| constexpr int kHighBitsToMakeRoomForFillType = 2; |
| |
| static std::atomic<int32_t> nextID{1}; |
| |
| uint32_t id; |
| do { |
| id = nextID.fetch_add(1, std::memory_order_relaxed); |
| // clear the high bits to make room for filltype |
| id <<= kHighBitsToMakeRoomForFillType; |
| id >>= kHighBitsToMakeRoomForFillType; |
| } while (id == 0); |
| return id; |
| } |
| |
| class SkSafeAccumulator { |
| public: |
| SkSafeAccumulator(size_t n = 0) : fTotal(n) {} |
| |
| bool ok() const { return fSafe.ok(); } |
| explicit operator bool() const { return fSafe.ok(); } |
| |
| SkSafeAccumulator& add(size_t n) { |
| fTotal = fSafe.add(fTotal, n); |
| return *this; |
| } |
| |
| SkSafeAccumulator& addMul(size_t a, size_t b) { |
| fTotal = fSafe.add(fTotal, fSafe.mul(a, b)); |
| return *this; |
| } |
| |
| std::optional<size_t> total() const { |
| if (fSafe.ok()) { |
| return fTotal; |
| } |
| return {}; |
| } |
| private: |
| SkSafeMath fSafe; |
| size_t fTotal; |
| }; |
| |
| const uint8_t gPtsPerVerb[] = { |
| 1, 1, 2, 2, 3, 0, // move, line, quad, conic, cubic, close |
| }; |
| |
| static std::pair<size_t, size_t> count_pts_cns(SkSpan<const SkPathVerb> vbs) { |
| size_t pts = 0, |
| cns = 0; |
| for (auto v : vbs) { |
| SkASSERT((unsigned)v < sizeof(gPtsPerVerb)); |
| pts += gPtsPerVerb[(unsigned)v]; |
| cns += (v == SkPathVerb::kConic); |
| } |
| return {pts, cns}; |
| } |
| |
| // If it detects a single trailing Move verb, remove it and it's corresponding point. |
| // Otherwise return leave the spans unchanged. |
| // |
| static void trim_trailing_move(SkSpan<const SkPoint>* pts, SkSpan<const SkPathVerb>* vbs) { |
| if (!vbs->empty() && vbs->back() == SkPathVerb::kMove) { |
| *vbs = vbs->first(vbs->size() - 1); |
| *pts = pts->first(pts->size() - 1); |
| } |
| } |
| |
| static bool valid_verbs(SkSpan<const SkPathVerb> vbs) { |
| if (vbs.size() == 0) { |
| return true; |
| } |
| |
| // We must begin with a Move (unless we're empty) |
| if (vbs.front() != SkPathVerb::kMove) { |
| return false; |
| } |
| SkPathVerb prev = SkPathVerb::kMove; |
| |
| // Check that we have a valid sequence. |
| for (size_t i = 1; i < vbs.size(); ++i) { |
| SkPathVerb curr = vbs[i]; |
| |
| if (static_cast<unsigned>(curr) > static_cast<unsigned>(SkPathVerb::kLast_Verb)) { |
| return false; |
| } |
| |
| // Previous verb Valid next verb |
| // ----------------------------------------- |
| // Move --> not Move |
| // Line/Quad/Conic/Cubic --> * |
| // Close --> Move |
| // |
| if (prev == SkPathVerb::kMove && curr == SkPathVerb::kMove) { |
| return false; |
| } |
| if (prev == SkPathVerb::kClose && curr != SkPathVerb::kMove) { |
| return false; |
| } |
| prev = curr; |
| } |
| |
| // A trailing Move is also illegal, since it creates an empty contour, |
| // which will confuse some of our callers |
| // |
| // See trim_trailing_move() helper, which may be called before calling us. |
| // |
| return vbs.back() != SkPathVerb::kMove; |
| } |
| |
| static inline bool valid_conic_weight(float w) { |
| return w >= 0 && SkIsFinite(w); |
| } |
| |
| // Handing in debugging, to set a break-point here if you want to know why we |
| // failed to create a pathdta |
| // |
| static void report_pathdata_make_failure(const char reason[]) { |
| // ` SkDEBUGF("SkPathData::Make failed: %s\n", reason); |
| } |
| |
| // This just sets-up the spans to point inside our allocation |
| // |
| SkPathData::SkPathData(size_t npts, size_t nvbs, size_t ncns) |
| : fUniqueID(next_pathdata_unique_id()) |
| , fConvexity((uint8_t)SkPathConvexity::kUnknown) |
| , fType(SkPathIsAType::kGeneral) |
| { |
| SkASSERT((npts == 0 && nvbs == 0 && ncns == 0) || |
| (npts != 0 && nvbs != 0)); |
| |
| #ifdef SK_DEBUG |
| { |
| SkSafeAccumulator accum(sizeof(*this)); |
| accum.addMul(npts, sizeof(SkPoint)) |
| .addMul(ncns, sizeof(SkPoint)) |
| .addMul(nvbs, sizeof(SkPathVerb)); |
| SkASSERT(accum.ok()); |
| } |
| #endif |
| |
| if (nvbs == 0) { |
| SkASSERT(fPoints.empty()); |
| SkASSERT(fVerbs.empty()); |
| SkASSERT(fConics.empty()); |
| } else { |
| auto data = reinterpret_cast<std::byte*>(this + 1); |
| |
| fPoints = {reinterpret_cast<SkPoint*>(data), npts}; |
| data += fPoints.size_bytes(); |
| |
| fConics = {reinterpret_cast<float*>(data), ncns}; |
| data += fConics.size_bytes(); |
| |
| fVerbs = {reinterpret_cast<SkPathVerb*>(data), nvbs}; |
| } |
| |
| // fBounds is initialized in finishInit() |
| } |
| |
| SkPathData::~SkPathData() { |
| // We will implicitly call our IDChangeList here, notifying them that we are |
| // being dstroyed. |
| SkDEBUGCODE(fUniqueID = 0xEEEEEEEE;) |
| } |
| |
| void SkPathData::operator delete(void* p) { |
| ::operator delete(p); |
| } |
| |
| void SkPathData::addGenIDChangeListener(sk_sp<SkIDChangeListener> listener) const { |
| // our empty singleton is never deleted, so we don't want to add any listeners to it. |
| if (this != SkPathData::PeekEmptySingleton()) { |
| // this method on the list is thread-safe |
| fGenIDChangeListeners.add(std::move(listener)); |
| } |
| } |
| |
| ////////////////////////////////////////////////////////////////////////////////////////////// |
| |
| // NOTE: This only allocates and initializes the span pointers (points, verbs), |
| // it does NOT set the other fields |
| sk_sp<SkPathData> SkPathData::Alloc(size_t npts, size_t nvbs, size_t ncns) { |
| SkSafeAccumulator accum(sizeof(SkPathData)); |
| |
| accum.addMul(npts, sizeof(SkPoint)) |
| .addMul(ncns, sizeof(SkPoint)) |
| .addMul(nvbs, sizeof(SkPathVerb)); |
| |
| if (auto size = accum.total()) { |
| // This trick allows us to just make one allocation, for us and our buffer |
| // rather than allocating us and also allocating the buffer (via malloc or new[]) |
| // We have the corresponding operator delete() specified as well. |
| void* storage = ::operator new (*size); |
| sk_sp<SkPathData> path(new (storage) SkPathData(npts, nvbs, ncns)); |
| |
| return path; |
| } |
| return nullptr; |
| } |
| |
| bool SkPathData::finishInit(std::optional<SkRect> bounds, std::optional<uint8_t> segmentMask) { |
| if (fPoints.empty()) { |
| fBounds = SkRect::MakeEmpty(); |
| fSegmentMask = 0; |
| return true; |
| } |
| |
| if (segmentMask.has_value()) { |
| fSegmentMask = segmentMask.value(); |
| } else { |
| fSegmentMask = SkPathPriv::ComputeSegmentMask(fVerbs); |
| } |
| |
| if (bounds.has_value()) { |
| fBounds = bounds.value().makeSorted(); |
| SkASSERT(SkIsFinite(&fPoints.data()->fX, fPoints.size() * 2)); |
| } else { |
| if (auto r = SkRect::Bounds(fPoints)) { |
| fBounds = r.value(); |
| } else { |
| report_pathdata_make_failure("non-finite bounds"); |
| return false; |
| } |
| } |
| |
| SkASSERT(fBounds.isSorted()); |
| return true; |
| } |
| |
| sk_sp<SkPathData> SkPathData::MakeTransform(const SkPathRaw& src, const SkMatrix& mx) { |
| if (src.empty()) { |
| return SkPathData::Empty(); |
| } |
| |
| if (mx.hasPerspective()) { |
| SkPathBuilder bu; |
| bu.addRaw(src); |
| bu.transform(mx); |
| return bu.detachData(); |
| } |
| |
| // Allocate our result, so we can map the new points directly into it |
| auto result = Alloc(src.points().size(), src.verbs().size(), src.conics().size()); |
| mx.mapPoints(result->fPoints, src.points()); |
| SkSpanPriv::Copy(result->fConics, src.conics()); |
| SkSpanPriv::Copy(result->fVerbs, src.verbs()); |
| |
| std::optional<SkRect> transformedBounds; |
| if (mx.rectStaysRect()) { |
| // safe us from having to compute our transformed bounds in finishInit() |
| transformedBounds = mx.mapRect(src.bounds()); |
| if (!transformedBounds.value().isFinite()) { |
| report_pathdata_make_failure("transform created non-finite bounds"); |
| return nullptr; |
| } |
| } |
| |
| if (!result->finishInit(transformedBounds, src.fSegmentMask)) { |
| return nullptr; |
| } |
| |
| result->setConvexity(SkPathPriv::TransformConvexity(mx, src.fPoints, src.fConvexity)); |
| |
| return result; |
| } |
| |
| sk_sp<SkPathData> SkPathData::makeTransform(const SkMatrix& mx) const { |
| if (mx.isIdentity()) { |
| return sk_ref_sp(this); |
| } |
| |
| // not important for transform, just need a value |
| const SkPathFillType ft = SkPathFillType::kDefault; |
| |
| if (auto result = MakeTransform(this->raw(ft, SkResolveConvexity::kNo), mx)) { |
| // See if we can maintian our IsA status ... |
| if ((fType == SkPathIsAType::kOval || fType == SkPathIsAType::kRRect) && |
| mx.rectStaysRect() && SkPathPriv::IsAxisAligned(fPoints)) |
| { |
| auto [dir, start] = |
| SkPathPriv::TransformDirAndStart(mx, fType == SkPathIsAType::kRRect, |
| fIsA.fDirection, fIsA.fStartIndex); |
| result->setupIsA(fType, dir, start); |
| } |
| return result; |
| } |
| return nullptr; |
| } |
| |
| sk_sp<SkPathData> SkPathData::makeOffset(SkVector v) const { |
| return this->makeTransform(SkMatrix::Translate(v)); |
| } |
| |
| bool operator==(const SkPathData& a, const SkPathData& b) { |
| if (&a == &b) { |
| return true; |
| } |
| |
| return SkSpanPriv::EQ(a.fPoints, b.fPoints) && |
| SkSpanPriv::EQ(a.fConics, b.fConics) && |
| SkSpanPriv::EQ(a.fVerbs, b.fVerbs); |
| } |
| |
| ///////////////////////////////////// |
| |
| sk_sp<SkPathData> SkPathData::MakeNoCheck(SkSpan<const SkPoint> pts, |
| SkSpan<const SkPathVerb> vbs, |
| SkSpan<const float> conics, |
| std::optional<SkRect> bounds, |
| std::optional<unsigned> segmentMask) { |
| trim_trailing_move(&pts, &vbs); |
| SkASSERT(valid_verbs(vbs)); |
| |
| auto path = Alloc(pts.size(), vbs.size(), conics.size()); |
| |
| SkSpanPriv::Copy(path->fPoints, pts); |
| SkSpanPriv::Copy(path->fConics, conics); |
| SkSpanPriv::Copy(path->fVerbs, vbs); |
| |
| return path->finishInit(bounds, segmentMask) ? path : nullptr; |
| } |
| |
| sk_sp<SkPathData> SkPathData::MakeNoCheck(const SkPathRaw& raw) { |
| return MakeNoCheck(raw.points(), raw.verbs(), raw.conics(), raw.fBounds, raw.fSegmentMask); |
| } |
| |
| sk_sp<SkPathData> SkPathData::Empty() { |
| return sk_ref_sp(PeekEmptySingleton()); |
| } |
| |
| void SkPathData::setupIsA(SkPathIsAType type, SkPathDirection dir, unsigned index) { |
| this->setConvexity(SkPathDirection_ToConvexity(dir)); |
| |
| SkASSERT(type == SkPathIsAType::kOval || type == SkPathIsAType::kRRect); |
| fType = type; |
| |
| SkASSERT((type == SkPathIsAType::kOval && index < 4) || |
| (type == SkPathIsAType::kRRect && index < 8)); |
| |
| fIsA.fDirection = dir; |
| fIsA.fStartIndex = SkTo<uint8_t>(index); |
| } |
| |
| sk_sp<SkPathData> SkPathData::Rect(const SkRect& r, SkPathDirection dir, unsigned index) { |
| if (!r.isFinite()) { |
| return nullptr; |
| } |
| SkPathRawShapes::Rect raw(r, dir, index); |
| return MakeNoCheck(raw.points(), raw.verbs(), raw.conics(), raw.fBounds, raw.fSegmentMask); |
| } |
| |
| sk_sp<SkPathData> SkPathData::Oval(const SkRect& r, SkPathDirection dir, unsigned index) { |
| if (!r.isFinite()) { |
| return nullptr; |
| } |
| SkPathRawShapes::Oval raw(r, dir, index); |
| auto path = MakeNoCheck(raw.points(), raw.verbs(), raw.conics(), raw.fBounds, raw.fSegmentMask); |
| |
| path->setupIsA(SkPathIsAType::kOval, dir, index); |
| return path; |
| } |
| |
| sk_sp<SkPathData> SkPathData::RRect(const SkRRect& r, SkPathDirection dir, unsigned index) { |
| if (!r.isValid()) { |
| return nullptr; |
| } |
| SkPathRawShapes::RRect raw(r, dir, index); |
| // we use Make, not MakeNoCheck, to confirm all points an conics are finite |
| if (auto path = Make(raw.points(), raw.verbs(), raw.conics())) { |
| path->setupIsA(SkPathIsAType::kRRect, dir, index); |
| return path; |
| } |
| return nullptr; |
| } |
| |
| sk_sp<SkPathData> SkPathData::Polygon(SkSpan<const SkPoint> pts, bool isClosed) { |
| if (pts.size() == 0 || (pts.size() == 1 && !isClosed)) { |
| return Empty(); |
| } |
| |
| const size_t nverbs = pts.size() + isClosed; // +1 for the kClose verb |
| const size_t nconics = 0; |
| auto path = Alloc(pts.size(), nverbs, nconics); |
| |
| SkSpanPriv::Copy(path->fPoints, pts); |
| |
| path->fVerbs[0] = SkPathVerb::kMove; |
| for (size_t i = 1; i < pts.size(); ++i) { |
| path->fVerbs[i] = SkPathVerb::kLine; |
| } |
| if (isClosed) { |
| path->fVerbs.back() = SkPathVerb::kClose; |
| } |
| |
| return path->finishInit({}, kLine_SkPathSegmentMask) ? path : nullptr; |
| } |
| |
| ///////////////////////////////////// |
| |
| SkPathConvexity SkPathData::getConvexityOrUnknown() const { |
| return static_cast<SkPathConvexity>(fConvexity.load(std::memory_order_relaxed)); |
| } |
| |
| SkPathConvexity SkPathData::getResolvedConvexity() const { |
| auto convexity = this->getConvexityOrUnknown(); |
| if (convexity == SkPathConvexity::kUnknown) { |
| convexity = SkPathPriv::ComputeConvexity(fPoints, fVerbs, fConics); |
| this->setConvexity(convexity); |
| } |
| return convexity; |
| } |
| |
| void SkPathData::setConvexity(SkPathConvexity convexity) const { |
| fConvexity.store((uint8_t)convexity, std::memory_order_relaxed); |
| } |
| |
| bool SkPathData::isConvex() const { |
| return SkPathConvexity_IsConvex(this->getResolvedConvexity()); |
| } |
| |
| SkRect SkPathData::computeTightBounds() const { |
| return SkPathPriv::ComputeTightBounds(this->points(), this->verbs(), this->conics()); |
| } |
| |
| SkPathRaw SkPathData::raw(SkPathFillType ft, SkResolveConvexity rc) const { |
| return { |
| fPoints, |
| fVerbs, |
| fConics, |
| fBounds, |
| ft, |
| rc == SkResolveConvexity::kYes ? this->getResolvedConvexity() |
| : this->getConvexityOrUnknown(), |
| fSegmentMask, |
| }; |
| } |
| |
| std::optional<std::array<SkPoint, 2>> SkPathData::asLine() const { |
| if (fPoints.size() == 2 && fVerbs.size() == 2 && fConics.size() == 0 && |
| fVerbs[1] == SkPathVerb::kLine) |
| { |
| SkASSERT(fVerbs[0] == SkPathVerb::kMove); |
| return {{ fPoints[0], fPoints[1] }}; |
| } |
| return {}; |
| } |
| |
| std::optional<SkPathRectInfo> SkPathData::asRect() const { |
| if (auto rc = SkPathPriv::IsRectContour(fPoints, fVerbs, fSegmentMask, false)) { |
| SkASSERT(rc->fRect == fBounds); |
| return {{ |
| fBounds, |
| rc->fDirection, |
| 0, // start index??? |
| }}; |
| } |
| return {}; |
| } |
| |
| std::optional<SkPathOvalInfo> SkPathData::asOval() const { |
| if (fType == SkPathIsAType::kOval) { |
| return {{ |
| fBounds, |
| fIsA.fDirection, |
| fIsA.fStartIndex, |
| }}; |
| } |
| return {}; |
| } |
| |
| std::optional<SkPathRRectInfo> SkPathData::asRRect() const { |
| if (fType == SkPathIsAType::kRRect) { |
| return {{ |
| SkPathPriv::DeduceRRectFromContour(fBounds, fPoints, fVerbs), |
| fIsA.fDirection, |
| fIsA.fStartIndex, |
| }}; |
| } |
| return {}; |
| } |
| |
| bool SkPathData::contains(SkPoint p, SkPathFillType ft) const { |
| return SkPathPriv::Contains(this->raw(ft, SkResolveConvexity::kNo), p); |
| } |
| |
| ///////////////// |
| |
| sk_sp<SkPathData> SkPathData::Make(SkSpan<const SkPoint> pts, |
| SkSpan<const SkPathVerb> vbs, |
| SkSpan<const float> conics) { |
| trim_trailing_move(&pts, &vbs); |
| if (!valid_verbs(vbs)) { |
| report_pathdata_make_failure("invalid verb sequence"); |
| return nullptr; |
| } |
| |
| auto [npts, ncns] = count_pts_cns(vbs); |
| if (pts.size() != npts || conics.size() != ncns) { |
| report_pathdata_make_failure("unexpected # points or conics"); |
| return nullptr; |
| } |
| |
| // Now we can check for a dangling kMove verb, and just ignore it |
| if (!vbs.empty() && vbs.back() == SkPathVerb::kMove) { |
| SkASSERT(!pts.empty()); |
| vbs = vbs.first(vbs.size() - 1); |
| pts = pts.first(pts.size() - 1); |
| } |
| if (vbs.empty()) { |
| return Empty(); |
| } |
| |
| for (auto w : conics) { |
| if (!valid_conic_weight(w)) { |
| report_pathdata_make_failure("non-finite conics"); |
| return nullptr; |
| } |
| } |
| |
| // MakeNoCheck *does* compute/check bounds if we don't pass them in |
| return MakeNoCheck(pts, vbs, conics, {}, {}); |
| } |