SkDashPathEffect.cpp (13911B)
1 /* 2 * Copyright 2006 The Android Open Source Project 3 * 4 * Use of this source code is governed by a BSD-style license that can be 5 * found in the LICENSE file. 6 */ 7 8 #include "include/effects/SkDashPathEffect.h" 9 10 #include "include/core/SkFlattenable.h" 11 #include "include/core/SkMatrix.h" 12 #include "include/core/SkPaint.h" 13 #include "include/core/SkPath.h" 14 #include "include/core/SkPathEffect.h" 15 #include "include/core/SkPoint.h" 16 #include "include/core/SkRect.h" 17 #include "include/core/SkStrokeRec.h" 18 #include "include/private/base/SkAlign.h" 19 #include "include/private/base/SkFloatingPoint.h" 20 #include "include/private/base/SkTemplates.h" 21 #include "src/core/SkPathEffectBase.h" 22 #include "src/core/SkReadBuffer.h" 23 #include "src/core/SkWriteBuffer.h" 24 #include "src/effects/SkDashImpl.h" 25 #include "src/utils/SkDashPathPriv.h" 26 27 #include <algorithm> 28 #include <cstdint> 29 #include <cstring> 30 #include <optional> 31 32 class SkPathBuilder; 33 34 using namespace skia_private; 35 36 SkDashImpl::SkDashImpl(SkSpan<const SkScalar> intervals, SkScalar phase) 37 : fIntervals(intervals.size()) 38 , fPhase(0) 39 , fInitialDashLength(-1) 40 , fIntervalLength(0) 41 , fInitialDashIndex(0) 42 { 43 SkASSERT(intervals.size() > 1 && SkIsAlign2(intervals.size())); 44 45 memcpy(fIntervals.data(), intervals.data(), intervals.size_bytes()); 46 47 // set the internal data members 48 SkDashPath::CalcDashParameters(phase, fIntervals, 49 &fInitialDashLength, &fInitialDashIndex, &fIntervalLength, &fPhase); 50 } 51 52 bool SkDashImpl::onFilterPath(SkPathBuilder* builder, const SkPath& src, SkStrokeRec* rec, 53 const SkRect* cullRect, const SkMatrix&) const { 54 return SkDashPath::InternalFilter(builder, src, rec, cullRect, fIntervals, 55 fInitialDashLength, fInitialDashIndex, fIntervalLength, 56 fPhase); 57 } 58 59 static void outset_for_stroke(SkRect* rect, const SkStrokeRec& rec) { 60 SkScalar radius = SkScalarHalf(rec.getWidth()); 61 if (0 == radius) { 62 radius = SK_Scalar1; // hairlines 63 } 64 if (SkPaint::kMiter_Join == rec.getJoin()) { 65 radius *= rec.getMiter(); 66 } 67 rect->outset(radius, radius); 68 } 69 70 // Attempt to trim the line to minimally cover the cull rect (currently 71 // only works for horizontal and vertical lines). 72 // Return true if processing should continue; false otherwise. 73 static bool cull_line(SkPoint* pts, const SkStrokeRec& rec, 74 const SkMatrix& ctm, const SkRect* cullRect, 75 const SkScalar intervalLength) { 76 if (nullptr == cullRect) { 77 SkASSERT(false); // Shouldn't ever occur in practice 78 return false; 79 } 80 81 SkScalar dx = pts[1].x() - pts[0].x(); 82 SkScalar dy = pts[1].y() - pts[0].y(); 83 84 if ((dx && dy) || (!dx && !dy)) { 85 return false; 86 } 87 88 SkRect bounds = *cullRect; 89 outset_for_stroke(&bounds, rec); 90 91 // cullRect is in device space while pts are in the local coordinate system 92 // defined by the ctm. We want our answer in the local coordinate system. 93 94 SkASSERT(ctm.rectStaysRect()); 95 SkMatrix inv; 96 if (!ctm.invert(&inv)) { 97 return false; 98 } 99 100 inv.mapRect(&bounds); 101 102 if (dx) { 103 SkASSERT(dx && !dy); 104 SkScalar minX = pts[0].fX; 105 SkScalar maxX = pts[1].fX; 106 107 if (dx < 0) { 108 using std::swap; 109 swap(minX, maxX); 110 } 111 112 SkASSERT(minX < maxX); 113 if (maxX <= bounds.fLeft || minX >= bounds.fRight) { 114 return false; 115 } 116 117 // Now we actually perform the chop, removing the excess to the left and 118 // right of the bounds (keeping our new line "in phase" with the dash, 119 // hence the (mod intervalLength). 120 121 if (minX < bounds.fLeft) { 122 minX = bounds.fLeft - SkScalarMod(bounds.fLeft - minX, intervalLength); 123 } 124 if (maxX > bounds.fRight) { 125 maxX = bounds.fRight + SkScalarMod(maxX - bounds.fRight, intervalLength); 126 } 127 128 SkASSERT(maxX > minX); 129 if (dx < 0) { 130 using std::swap; 131 swap(minX, maxX); 132 } 133 pts[0].fX = minX; 134 pts[1].fX = maxX; 135 } else { 136 SkASSERT(dy && !dx); 137 SkScalar minY = pts[0].fY; 138 SkScalar maxY = pts[1].fY; 139 140 if (dy < 0) { 141 using std::swap; 142 swap(minY, maxY); 143 } 144 145 SkASSERT(minY < maxY); 146 if (maxY <= bounds.fTop || minY >= bounds.fBottom) { 147 return false; 148 } 149 150 // Now we actually perform the chop, removing the excess to the top and 151 // bottom of the bounds (keeping our new line "in phase" with the dash, 152 // hence the (mod intervalLength). 153 154 if (minY < bounds.fTop) { 155 minY = bounds.fTop - SkScalarMod(bounds.fTop - minY, intervalLength); 156 } 157 if (maxY > bounds.fBottom) { 158 maxY = bounds.fBottom + SkScalarMod(maxY - bounds.fBottom, intervalLength); 159 } 160 161 SkASSERT(maxY > minY); 162 if (dy < 0) { 163 using std::swap; 164 swap(minY, maxY); 165 } 166 pts[0].fY = minY; 167 pts[1].fY = maxY; 168 } 169 170 return true; 171 } 172 173 // Currently asPoints is more restrictive then it needs to be. In the future 174 // we need to: 175 // allow kRound_Cap capping (could allow rotations in the matrix with this) 176 // allow paths to be returned 177 bool SkDashImpl::onAsPoints(PointData* results, const SkPath& src, const SkStrokeRec& rec, 178 const SkMatrix& matrix, const SkRect* cullRect) const { 179 // width < 0 -> fill && width == 0 -> hairline so requiring width > 0 rules both out 180 if (0 >= rec.getWidth()) { 181 return false; 182 } 183 184 // TODO: this next test could be eased up. We could allow any number of 185 // intervals as long as all the ons match and all the offs match. 186 // Additionally, they do not necessarily need to be integers. 187 // We cannot allow arbitrary intervals since we want the returned points 188 // to be uniformly sized. 189 if (fIntervals.size() != 2 || 190 !SkScalarNearlyEqual(fIntervals[0], fIntervals[1]) || 191 !SkScalarIsInt(fIntervals[0]) || 192 !SkScalarIsInt(fIntervals[1])) { 193 return false; 194 } 195 196 SkPoint pts[2]; 197 198 if (!src.isLine(pts)) { 199 return false; 200 } 201 202 // TODO: this test could be eased up to allow circles 203 if (SkPaint::kButt_Cap != rec.getCap()) { 204 return false; 205 } 206 207 // TODO: this test could be eased up for circles. Rotations could be allowed. 208 if (!matrix.rectStaysRect()) { 209 return false; 210 } 211 212 // See if the line can be limited to something plausible. 213 if (!cull_line(pts, rec, matrix, cullRect, fIntervalLength)) { 214 return false; 215 } 216 217 SkScalar length = SkPoint::Distance(pts[1], pts[0]); 218 219 SkVector tangent = pts[1] - pts[0]; 220 if (tangent.isZero()) { 221 return false; 222 } 223 224 tangent.scale(SkScalarInvert(length)); 225 226 // TODO: make this test for horizontal & vertical lines more robust 227 bool isXAxis = true; 228 if (SkScalarNearlyEqual(SK_Scalar1, tangent.fX) || 229 SkScalarNearlyEqual(-SK_Scalar1, tangent.fX)) { 230 results->fSize.set(SkScalarHalf(fIntervals[0]), SkScalarHalf(rec.getWidth())); 231 } else if (SkScalarNearlyEqual(SK_Scalar1, tangent.fY) || 232 SkScalarNearlyEqual(-SK_Scalar1, tangent.fY)) { 233 results->fSize.set(SkScalarHalf(rec.getWidth()), SkScalarHalf(fIntervals[0])); 234 isXAxis = false; 235 } else if (SkPaint::kRound_Cap != rec.getCap()) { 236 // Angled lines don't have axis-aligned boxes. 237 return false; 238 } 239 240 if (results) { 241 results->fFlags = 0; 242 SkScalar clampedInitialDashLength = std::min(length, fInitialDashLength); 243 244 if (SkPaint::kRound_Cap == rec.getCap()) { 245 results->fFlags |= PointData::kCircles_PointFlag; 246 } 247 248 results->fNumPoints = 0; 249 SkScalar len2 = length; 250 if (clampedInitialDashLength > 0 || 0 == fInitialDashIndex) { 251 SkASSERT(len2 >= clampedInitialDashLength); 252 if (0 == fInitialDashIndex) { 253 if (clampedInitialDashLength > 0) { 254 if (clampedInitialDashLength >= fIntervals[0]) { 255 ++results->fNumPoints; // partial first dash 256 } 257 len2 -= clampedInitialDashLength; 258 } 259 len2 -= fIntervals[1]; // also skip first space 260 if (len2 < 0) { 261 len2 = 0; 262 } 263 } else { 264 len2 -= clampedInitialDashLength; // skip initial partial empty 265 } 266 } 267 // Too many midpoints can cause results->fNumPoints to overflow or 268 // otherwise cause the results->fPoints allocation below to OOM. 269 // Cap it to a sane value. 270 SkScalar numIntervals = len2 / fIntervalLength; 271 if (!SkIsFinite(numIntervals) || numIntervals > SkDashPath::kMaxDashCount) { 272 return false; 273 } 274 int numMidPoints = SkScalarFloorToInt(numIntervals); 275 results->fNumPoints += numMidPoints; 276 len2 -= numMidPoints * fIntervalLength; 277 bool partialLast = false; 278 if (len2 > 0) { 279 if (len2 < fIntervals[0]) { 280 partialLast = true; 281 } else { 282 ++numMidPoints; 283 ++results->fNumPoints; 284 } 285 } 286 287 results->fPoints = new SkPoint[results->fNumPoints]; 288 289 SkScalar distance = 0; 290 int curPt = 0; 291 292 if (clampedInitialDashLength > 0 || 0 == fInitialDashIndex) { 293 SkASSERT(clampedInitialDashLength <= length); 294 295 if (0 == fInitialDashIndex) { 296 if (clampedInitialDashLength > 0) { 297 // partial first block 298 SkASSERT(SkPaint::kRound_Cap != rec.getCap()); // can't handle partial circles 299 SkScalar x = pts[0].fX + tangent.fX * SkScalarHalf(clampedInitialDashLength); 300 SkScalar y = pts[0].fY + tangent.fY * SkScalarHalf(clampedInitialDashLength); 301 SkScalar halfWidth, halfHeight; 302 if (isXAxis) { 303 halfWidth = SkScalarHalf(clampedInitialDashLength); 304 halfHeight = SkScalarHalf(rec.getWidth()); 305 } else { 306 halfWidth = SkScalarHalf(rec.getWidth()); 307 halfHeight = SkScalarHalf(clampedInitialDashLength); 308 } 309 if (clampedInitialDashLength < fIntervals[0]) { 310 // This one will not be like the others 311 results->fFirst = SkPath::Rect({x - halfWidth, y - halfHeight, 312 x + halfWidth, y + halfHeight}); 313 } else { 314 SkASSERT(curPt < results->fNumPoints); 315 results->fPoints[curPt].set(x, y); 316 ++curPt; 317 } 318 319 distance += clampedInitialDashLength; 320 } 321 322 distance += fIntervals[1]; // skip over the next blank block too 323 } else { 324 distance += clampedInitialDashLength; 325 } 326 } 327 328 if (0 != numMidPoints) { 329 distance += SkScalarHalf(fIntervals[0]); 330 331 for (int i = 0; i < numMidPoints; ++i) { 332 SkScalar x = pts[0].fX + tangent.fX * distance; 333 SkScalar y = pts[0].fY + tangent.fY * distance; 334 335 SkASSERT(curPt < results->fNumPoints); 336 results->fPoints[curPt].set(x, y); 337 ++curPt; 338 339 distance += fIntervalLength; 340 } 341 342 distance -= SkScalarHalf(fIntervals[0]); 343 } 344 345 if (partialLast) { 346 // partial final block 347 SkASSERT(SkPaint::kRound_Cap != rec.getCap()); // can't handle partial circles 348 SkScalar temp = length - distance; 349 SkASSERT(temp < fIntervals[0]); 350 SkScalar x = pts[0].fX + tangent.fX * (distance + SkScalarHalf(temp)); 351 SkScalar y = pts[0].fY + tangent.fY * (distance + SkScalarHalf(temp)); 352 SkScalar halfWidth, halfHeight; 353 if (isXAxis) { 354 halfWidth = SkScalarHalf(temp); 355 halfHeight = SkScalarHalf(rec.getWidth()); 356 } else { 357 halfWidth = SkScalarHalf(rec.getWidth()); 358 halfHeight = SkScalarHalf(temp); 359 } 360 results->fLast = SkPath::Rect({x - halfWidth, y - halfHeight, 361 x + halfWidth, y + halfHeight}); 362 } 363 364 SkASSERT(curPt == results->fNumPoints); 365 } 366 367 return true; 368 } 369 370 std::optional<SkPathEffectBase::DashInfo> SkDashImpl::asADash() const { 371 return {{fIntervals, fPhase}}; 372 } 373 374 void SkDashImpl::flatten(SkWriteBuffer& buffer) const { 375 buffer.writeScalar(fPhase); 376 buffer.writeScalarArray(fIntervals); 377 } 378 379 sk_sp<SkFlattenable> SkDashImpl::CreateProc(SkReadBuffer& buffer) { 380 const SkScalar phase = buffer.readScalar(); 381 uint32_t count = buffer.getArrayCount(); 382 383 // Don't allocate gigantic buffers if there's not data for them. 384 if (!buffer.validateCanReadN<SkScalar>(count)) { 385 return nullptr; 386 } 387 388 AutoSTArray<32, SkScalar> intervals(count); 389 if (buffer.readScalarArray(intervals)) { 390 return SkDashPathEffect::Make(intervals, phase); 391 } 392 return nullptr; 393 } 394 395 ////////////////////////////////////////////////////////////////////////////////////////////////// 396 397 sk_sp<SkPathEffect> SkDashPathEffect::Make(SkSpan<const SkScalar> intervals, SkScalar phase) { 398 if (!SkDashPath::ValidDashPath(phase, intervals)) { 399 return nullptr; 400 } 401 return sk_sp<SkPathEffect>(new SkDashImpl(intervals, phase)); 402 }