blob: fc73c9f5a7de331a14898eada42346ba7f00c34f [file] [log] [blame]
egdaniela22ea182014-06-11 06:51:51 -07001/*
2 * Copyright 2014 Google Inc.
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 "SkDashPathPriv.h"
9#include "SkPathMeasure.h"
Cary Clarkdf429f32017-11-08 11:44:31 -050010#include "SkPointPriv.h"
halcanary435657f2015-09-15 12:53:07 -070011#include "SkStrokeRec.h"
egdaniela22ea182014-06-11 06:51:51 -070012
Ben Wagnerf08d1d02018-06-18 15:11:00 -040013#include <utility>
14
egdaniela22ea182014-06-11 06:51:51 -070015static inline int is_even(int x) {
caryclark3127c992015-12-09 12:02:30 -080016 return !(x & 1);
egdaniela22ea182014-06-11 06:51:51 -070017}
18
19static SkScalar find_first_interval(const SkScalar intervals[], SkScalar phase,
20 int32_t* index, int count) {
21 for (int i = 0; i < count; ++i) {
caryclarkd3cfd942016-03-17 05:33:28 -070022 SkScalar gap = intervals[i];
23 if (phase > gap || (phase == gap && gap)) {
24 phase -= gap;
egdaniela22ea182014-06-11 06:51:51 -070025 } else {
26 *index = i;
caryclarkd3cfd942016-03-17 05:33:28 -070027 return gap - phase;
egdaniela22ea182014-06-11 06:51:51 -070028 }
29 }
30 // If we get here, phase "appears" to be larger than our length. This
31 // shouldn't happen with perfect precision, but we can accumulate errors
32 // during the initial length computation (rounding can make our sum be too
33 // big or too small. In that event, we just have to eat the error here.
34 *index = 0;
35 return intervals[0];
36}
37
38void SkDashPath::CalcDashParameters(SkScalar phase, const SkScalar intervals[], int32_t count,
39 SkScalar* initialDashLength, int32_t* initialDashIndex,
40 SkScalar* intervalLength, SkScalar* adjustedPhase) {
41 SkScalar len = 0;
42 for (int i = 0; i < count; i++) {
43 len += intervals[i];
44 }
45 *intervalLength = len;
caryclarkeb75c7d2016-03-18 06:04:26 -070046 // Adjust phase to be between 0 and len, "flipping" phase if negative.
47 // e.g., if len is 100, then phase of -20 (or -120) is equivalent to 80
48 if (adjustedPhase) {
49 if (phase < 0) {
50 phase = -phase;
51 if (phase > len) {
egdaniela22ea182014-06-11 06:51:51 -070052 phase = SkScalarMod(phase, len);
53 }
caryclarkeb75c7d2016-03-18 06:04:26 -070054 phase = len - phase;
55
56 // Due to finite precision, it's possible that phase == len,
57 // even after the subtract (if len >>> phase), so fix that here.
58 // This fixes http://crbug.com/124652 .
59 SkASSERT(phase <= len);
60 if (phase == len) {
61 phase = 0;
62 }
63 } else if (phase >= len) {
64 phase = SkScalarMod(phase, len);
egdaniela22ea182014-06-11 06:51:51 -070065 }
caryclarkeb75c7d2016-03-18 06:04:26 -070066 *adjustedPhase = phase;
egdaniela22ea182014-06-11 06:51:51 -070067 }
caryclarkeb75c7d2016-03-18 06:04:26 -070068 SkASSERT(phase >= 0 && phase < len);
69
70 *initialDashLength = find_first_interval(intervals, phase,
71 initialDashIndex, count);
72
73 SkASSERT(*initialDashLength >= 0);
74 SkASSERT(*initialDashIndex >= 0 && *initialDashIndex < count);
egdaniela22ea182014-06-11 06:51:51 -070075}
76
77static void outset_for_stroke(SkRect* rect, const SkStrokeRec& rec) {
78 SkScalar radius = SkScalarHalf(rec.getWidth());
79 if (0 == radius) {
80 radius = SK_Scalar1; // hairlines
81 }
82 if (SkPaint::kMiter_Join == rec.getJoin()) {
Mike Reeda99b6ce2017-02-04 11:04:26 -050083 radius *= rec.getMiter();
egdaniela22ea182014-06-11 06:51:51 -070084 }
85 rect->outset(radius, radius);
86}
87
Greg Daniel9838b492017-12-21 14:55:00 +000088#ifndef SK_SUPPORT_LEGACY_DASH_CULL_PATH
89// If line is zero-length, bump out the end by a tiny amount
90// to draw endcaps. The bump factor is sized so that
91// SkPoint::Distance() computes a non-zero length.
92// Offsets SK_ScalarNearlyZero or smaller create empty paths when Iter measures length.
93// Large values are scaled by SK_ScalarNearlyZero so significant bits change.
94static void adjust_zero_length_line(SkPoint pts[2]) {
95 SkASSERT(pts[0] == pts[1]);
96 pts[1].fX += SkTMax(1.001f, pts[1].fX) * SK_ScalarNearlyZero;
97}
98
99static bool clip_line(SkPoint pts[2], const SkRect& bounds, SkScalar intervalLength,
100 SkScalar priorPhase) {
101 SkVector dxy = pts[1] - pts[0];
102
103 // only horizontal or vertical lines
104 if (dxy.fX && dxy.fY) {
105 return false;
106 }
107 int xyOffset = SkToBool(dxy.fY); // 0 to adjust horizontal, 1 to adjust vertical
108
109 SkScalar minXY = (&pts[0].fX)[xyOffset];
110 SkScalar maxXY = (&pts[1].fX)[xyOffset];
111 bool swapped = maxXY < minXY;
112 if (swapped) {
Ben Wagnerf08d1d02018-06-18 15:11:00 -0400113 using std::swap;
114 swap(minXY, maxXY);
Greg Daniel9838b492017-12-21 14:55:00 +0000115 }
116
117 SkASSERT(minXY <= maxXY);
118 SkScalar leftTop = (&bounds.fLeft)[xyOffset];
119 SkScalar rightBottom = (&bounds.fRight)[xyOffset];
120 if (maxXY < leftTop || minXY > rightBottom) {
121 return false;
122 }
123
124 // Now we actually perform the chop, removing the excess to the left/top and
125 // right/bottom of the bounds (keeping our new line "in phase" with the dash,
126 // hence the (mod intervalLength).
127
128 if (minXY < leftTop) {
129 minXY = leftTop - SkScalarMod(leftTop - minXY, intervalLength);
130 if (!swapped) {
131 minXY -= priorPhase; // for rectangles, adjust by prior phase
132 }
133 }
134 if (maxXY > rightBottom) {
135 maxXY = rightBottom + SkScalarMod(maxXY - rightBottom, intervalLength);
136 if (swapped) {
137 maxXY += priorPhase; // for rectangles, adjust by prior phase
138 }
139 }
140
141 SkASSERT(maxXY >= minXY);
142 if (swapped) {
Ben Wagnerf08d1d02018-06-18 15:11:00 -0400143 using std::swap;
144 swap(minXY, maxXY);
Greg Daniel9838b492017-12-21 14:55:00 +0000145 }
146 (&pts[0].fX)[xyOffset] = minXY;
147 (&pts[1].fX)[xyOffset] = maxXY;
148
149 if (minXY == maxXY) {
150 adjust_zero_length_line(pts);
151 }
152 return true;
153}
154
155static bool contains_inclusive(const SkRect& rect, const SkPoint& pt) {
156 return rect.fLeft <= pt.fX && pt.fX <= rect.fRight &&
157 rect.fTop <= pt.fY && pt.fY <= rect.fBottom;
158}
159
160// Returns true is b is between a and c, that is: a <= b <= c, or a >= b >= c.
161// Can perform this test with one branch by observing that, relative to b,
162// the condition is true only if one side is positive and one side is negative.
163// If the numbers are very small, the optimization may return the wrong result
164// because the multiply may generate a zero where the simple compare does not.
165// For this reason the assert does not fire when all three numbers are near zero.
166static bool between(SkScalar a, SkScalar b, SkScalar c) {
167 SkASSERT(((a <= b && b <= c) || (a >= b && b >= c)) == ((a - b) * (c - b) <= 0)
168 || (SkScalarNearlyZero(a) && SkScalarNearlyZero(b) && SkScalarNearlyZero(c)));
169 return (a - b) * (c - b) <= 0;
170}
171#endif
172
egdaniela22ea182014-06-11 06:51:51 -0700173// Only handles lines for now. If returns true, dstPath is the new (smaller)
174// path. If returns false, then dstPath parameter is ignored.
175static bool cull_path(const SkPath& srcPath, const SkStrokeRec& rec,
176 const SkRect* cullRect, SkScalar intervalLength,
177 SkPath* dstPath) {
Greg Daniel9838b492017-12-21 14:55:00 +0000178#ifdef SK_SUPPORT_LEGACY_DASH_CULL_PATH
halcanary96fcdcc2015-08-27 07:41:13 -0700179 if (nullptr == cullRect) {
egdaniela22ea182014-06-11 06:51:51 -0700180 return false;
181 }
182
Cary Clark394197d2017-12-19 20:20:13 +0000183 SkPoint pts[2];
184 if (!srcPath.isLine(pts)) {
egdaniela22ea182014-06-11 06:51:51 -0700185 return false;
186 }
Cary Clark394197d2017-12-19 20:20:13 +0000187
188 SkRect bounds = *cullRect;
egdaniela22ea182014-06-11 06:51:51 -0700189 outset_for_stroke(&bounds, rec);
Cary Clark394197d2017-12-19 20:20:13 +0000190
191 SkScalar dx = pts[1].x() - pts[0].x();
192 SkScalar dy = pts[1].y() - pts[0].y();
193
194 // just do horizontal lines for now (lazy)
195 if (dy) {
egdaniela22ea182014-06-11 06:51:51 -0700196 return false;
197 }
Cary Clark394197d2017-12-19 20:20:13 +0000198
199 SkScalar minX = pts[0].fX;
200 SkScalar maxX = pts[1].fX;
201
202 if (dx < 0) {
Ben Wagnerf08d1d02018-06-18 15:11:00 -0400203 using std::swap;
204 swap(minX, maxX);
Cary Clark394197d2017-12-19 20:20:13 +0000205 }
206
207 SkASSERT(minX <= maxX);
208 if (maxX < bounds.fLeft || minX > bounds.fRight) {
209 return false;
210 }
211
212 // Now we actually perform the chop, removing the excess to the left and
213 // right of the bounds (keeping our new line "in phase" with the dash,
214 // hence the (mod intervalLength).
215
216 if (minX < bounds.fLeft) {
217 minX = bounds.fLeft - SkScalarMod(bounds.fLeft - minX,
218 intervalLength);
219 }
220 if (maxX > bounds.fRight) {
221 maxX = bounds.fRight + SkScalarMod(maxX - bounds.fRight,
222 intervalLength);
223 }
224
225 SkASSERT(maxX >= minX);
226 if (dx < 0) {
Ben Wagnerf08d1d02018-06-18 15:11:00 -0400227 using std::swap;
228 swap(minX, maxX);
Cary Clark394197d2017-12-19 20:20:13 +0000229 }
230 pts[0].fX = minX;
231 pts[1].fX = maxX;
232
233 // If line is zero-length, bump out the end by a tiny amount
234 // to draw endcaps. The bump factor is sized so that
235 // SkPoint::Distance() computes a non-zero length.
236 if (minX == maxX) {
237 pts[1].fX += maxX * FLT_EPSILON * 32; // 16 instead of 32 does not draw; length stays zero
238 }
Greg Daniel9838b492017-12-21 14:55:00 +0000239#else // !SK_SUPPORT_LEGACY_DASH_CULL_PATH
240 SkPoint pts[4];
241 if (nullptr == cullRect) {
242 if (srcPath.isLine(pts) && pts[0] == pts[1]) {
243 adjust_zero_length_line(pts);
244 } else {
245 return false;
246 }
247 } else {
248 SkRect bounds;
249 bool isLine = srcPath.isLine(pts);
250 bool isRect = !isLine && srcPath.isRect(nullptr);
251 if (!isLine && !isRect) {
252 return false;
253 }
254 bounds = *cullRect;
255 outset_for_stroke(&bounds, rec);
256 if (isRect) {
257 // break rect into four lines, and call each one separately
258 SkPath::Iter iter(srcPath, false);
259 SkAssertResult(SkPath::kMove_Verb == iter.next(pts));
260 SkScalar priorLength = 0;
261 while (SkPath::kLine_Verb == iter.next(pts)) {
262 SkVector v = pts[1] - pts[0];
263 // if line is entirely outside clip rect, skip it
264 if (v.fX ? between(bounds.fTop, pts[0].fY, bounds.fBottom) :
265 between(bounds.fLeft, pts[0].fX, bounds.fRight)) {
266 bool skipMoveTo = contains_inclusive(bounds, pts[0]);
267 if (clip_line(pts, bounds, intervalLength,
268 SkScalarMod(priorLength, intervalLength))) {
269 if (0 == priorLength || !skipMoveTo) {
270 dstPath->moveTo(pts[0]);
271 }
272 dstPath->lineTo(pts[1]);
273 }
274 }
275 // keep track of all prior lengths to set phase of next line
276 priorLength += SkScalarAbs(v.fX ? v.fX : v.fY);
277 }
278 return !dstPath->isEmpty();
279 }
280 SkASSERT(isLine);
281 if (!clip_line(pts, bounds, intervalLength, 0)) {
282 return false;
283 }
284 }
285#endif
egdaniela22ea182014-06-11 06:51:51 -0700286 dstPath->moveTo(pts[0]);
287 dstPath->lineTo(pts[1]);
288 return true;
289}
290
291class SpecialLineRec {
292public:
293 bool init(const SkPath& src, SkPath* dst, SkStrokeRec* rec,
294 int intervalCount, SkScalar intervalLength) {
295 if (rec->isHairlineStyle() || !src.isLine(fPts)) {
296 return false;
297 }
298
299 // can relax this in the future, if we handle square and round caps
300 if (SkPaint::kButt_Cap != rec->getCap()) {
301 return false;
302 }
303
304 SkScalar pathLength = SkPoint::Distance(fPts[0], fPts[1]);
305
306 fTangent = fPts[1] - fPts[0];
307 if (fTangent.isZero()) {
308 return false;
309 }
310
311 fPathLength = pathLength;
312 fTangent.scale(SkScalarInvert(pathLength));
Cary Clarkdf429f32017-11-08 11:44:31 -0500313 SkPointPriv::RotateCCW(fTangent, &fNormal);
egdaniela22ea182014-06-11 06:51:51 -0700314 fNormal.scale(SkScalarHalf(rec->getWidth()));
315
316 // now estimate how many quads will be added to the path
317 // resulting segments = pathLen * intervalCount / intervalLen
318 // resulting points = 4 * segments
319
Mike Reeda99b6ce2017-02-04 11:04:26 -0500320 SkScalar ptCount = pathLength * intervalCount / (float)intervalLength;
lsalzmanf41ae2f2016-07-21 09:37:59 -0700321 ptCount = SkTMin(ptCount, SkDashPath::kMaxDashCount);
egdaniela22ea182014-06-11 06:51:51 -0700322 int n = SkScalarCeilToInt(ptCount) << 2;
323 dst->incReserve(n);
324
325 // we will take care of the stroking
326 rec->setFillStyle();
327 return true;
328 }
329
330 void addSegment(SkScalar d0, SkScalar d1, SkPath* path) const {
lsalzman0adbd3e2016-08-08 13:40:27 -0700331 SkASSERT(d0 <= fPathLength);
egdaniela22ea182014-06-11 06:51:51 -0700332 // clamp the segment to our length
333 if (d1 > fPathLength) {
334 d1 = fPathLength;
335 }
336
Mike Reeda99b6ce2017-02-04 11:04:26 -0500337 SkScalar x0 = fPts[0].fX + fTangent.fX * d0;
338 SkScalar x1 = fPts[0].fX + fTangent.fX * d1;
339 SkScalar y0 = fPts[0].fY + fTangent.fY * d0;
340 SkScalar y1 = fPts[0].fY + fTangent.fY * d1;
egdaniela22ea182014-06-11 06:51:51 -0700341
342 SkPoint pts[4];
343 pts[0].set(x0 + fNormal.fX, y0 + fNormal.fY); // moveTo
344 pts[1].set(x1 + fNormal.fX, y1 + fNormal.fY); // lineTo
345 pts[2].set(x1 - fNormal.fX, y1 - fNormal.fY); // lineTo
346 pts[3].set(x0 - fNormal.fX, y0 - fNormal.fY); // lineTo
347
348 path->addPoly(pts, SK_ARRAY_COUNT(pts), false);
349 }
350
351private:
352 SkPoint fPts[2];
353 SkVector fTangent;
354 SkVector fNormal;
355 SkScalar fPathLength;
356};
357
358
caryclarkeb75c7d2016-03-18 06:04:26 -0700359bool SkDashPath::InternalFilter(SkPath* dst, const SkPath& src, SkStrokeRec* rec,
egdaniela22ea182014-06-11 06:51:51 -0700360 const SkRect* cullRect, const SkScalar aIntervals[],
361 int32_t count, SkScalar initialDashLength, int32_t initialDashIndex,
bsalomon398e3f42016-06-13 10:22:48 -0700362 SkScalar intervalLength,
363 StrokeRecApplication strokeRecApplication) {
egdaniela22ea182014-06-11 06:51:51 -0700364
caryclarkeb75c7d2016-03-18 06:04:26 -0700365 // we do nothing if the src wants to be filled
bsalomona0587862016-06-09 06:03:38 -0700366 SkStrokeRec::Style style = rec->getStyle();
367 if (SkStrokeRec::kFill_Style == style || SkStrokeRec::kStrokeAndFill_Style == style) {
egdaniela22ea182014-06-11 06:51:51 -0700368 return false;
369 }
370
371 const SkScalar* intervals = aIntervals;
372 SkScalar dashCount = 0;
373 int segCount = 0;
374
375 SkPath cullPathStorage;
376 const SkPath* srcPtr = &src;
377 if (cull_path(src, *rec, cullRect, intervalLength, &cullPathStorage)) {
Cary Clark4fb229d2017-12-22 08:10:09 -0500378 // if rect is closed, starts in a dash, and ends in a dash, add the initial join
379 // potentially a better fix is described here: bug.skia.org/7445
380 if (src.isRect(nullptr) && src.isLastContourClosed() && is_even(initialDashIndex)) {
381 SkScalar pathLength = SkPathMeasure(src, false, rec->getResScale()).getLength();
382 SkScalar endPhase = SkScalarMod(pathLength + initialDashLength, intervalLength);
383 int index = 0;
384 while (endPhase > intervals[index]) {
385 endPhase -= intervals[index++];
386 SkASSERT(index <= count);
387 }
388 // if dash ends inside "on", or ends at beginning of "off"
389 if (is_even(index) == (endPhase > 0)) {
390 SkPoint midPoint = src.getPoint(0);
391 // get vector at end of rect
392 int last = src.countPoints() - 1;
393 while (midPoint == src.getPoint(last)) {
394 --last;
395 SkASSERT(last >= 0);
396 }
397 // get vector at start of rect
398 int next = 1;
399 while (midPoint == src.getPoint(next)) {
400 ++next;
401 SkASSERT(next < last);
402 }
403 SkVector v = midPoint - src.getPoint(last);
404 const SkScalar kTinyOffset = SK_ScalarNearlyZero;
405 // scale vector to make start of tiny right angle
406 v *= kTinyOffset;
407 cullPathStorage.moveTo(midPoint - v);
408 cullPathStorage.lineTo(midPoint);
409 v = midPoint - src.getPoint(next);
410 // scale vector to make end of tiny right angle
411 v *= kTinyOffset;
412 cullPathStorage.lineTo(midPoint - v);
413 }
414 }
egdaniela22ea182014-06-11 06:51:51 -0700415 srcPtr = &cullPathStorage;
416 }
417
418 SpecialLineRec lineRec;
bsalomon398e3f42016-06-13 10:22:48 -0700419 bool specialLine = (StrokeRecApplication::kAllow == strokeRecApplication) &&
420 lineRec.init(*srcPtr, dst, rec, count >> 1, intervalLength);
egdaniela22ea182014-06-11 06:51:51 -0700421
caryclark1a7eb262016-01-21 07:07:02 -0800422 SkPathMeasure meas(*srcPtr, false, rec->getResScale());
egdaniela22ea182014-06-11 06:51:51 -0700423
424 do {
425 bool skipFirstSegment = meas.isClosed();
426 bool addedSegment = false;
427 SkScalar length = meas.getLength();
428 int index = initialDashIndex;
429
430 // Since the path length / dash length ratio may be arbitrarily large, we can exert
431 // significant memory pressure while attempting to build the filtered path. To avoid this,
432 // we simply give up dashing beyond a certain threshold.
433 //
434 // The original bug report (http://crbug.com/165432) is based on a path yielding more than
435 // 90 million dash segments and crashing the memory allocator. A limit of 1 million
436 // segments seems reasonable: at 2 verbs per segment * 9 bytes per verb, this caps the
437 // maximum dash memory overhead at roughly 17MB per path.
egdaniela22ea182014-06-11 06:51:51 -0700438 dashCount += length * (count >> 1) / intervalLength;
439 if (dashCount > kMaxDashCount) {
440 dst->reset();
441 return false;
442 }
443
444 // Using double precision to avoid looping indefinitely due to single precision rounding
445 // (for extreme path_length/dash_length ratios). See test_infinite_dash() unittest.
446 double distance = 0;
447 double dlen = initialDashLength;
448
449 while (distance < length) {
450 SkASSERT(dlen >= 0);
451 addedSegment = false;
caryclark5cb00a92015-08-26 09:04:55 -0700452 if (is_even(index) && !skipFirstSegment) {
egdaniela22ea182014-06-11 06:51:51 -0700453 addedSegment = true;
454 ++segCount;
455
456 if (specialLine) {
457 lineRec.addSegment(SkDoubleToScalar(distance),
458 SkDoubleToScalar(distance + dlen),
459 dst);
460 } else {
461 meas.getSegment(SkDoubleToScalar(distance),
462 SkDoubleToScalar(distance + dlen),
463 dst, true);
464 }
465 }
466 distance += dlen;
467
468 // clear this so we only respect it the first time around
469 skipFirstSegment = false;
470
471 // wrap around our intervals array if necessary
472 index += 1;
473 SkASSERT(index <= count);
474 if (index == count) {
475 index = 0;
476 }
477
478 // fetch our next dlen
479 dlen = intervals[index];
480 }
481
482 // extend if we ended on a segment and we need to join up with the (skipped) initial segment
483 if (meas.isClosed() && is_even(initialDashIndex) &&
caryclarkeb75c7d2016-03-18 06:04:26 -0700484 initialDashLength >= 0) {
egdaniela22ea182014-06-11 06:51:51 -0700485 meas.getSegment(0, initialDashLength, dst, !addedSegment);
486 ++segCount;
487 }
488 } while (meas.nextContour());
489
490 if (segCount > 1) {
491 dst->setConvexity(SkPath::kConcave_Convexity);
492 }
493
494 return true;
495}
496
497bool SkDashPath::FilterDashPath(SkPath* dst, const SkPath& src, SkStrokeRec* rec,
498 const SkRect* cullRect, const SkPathEffect::DashInfo& info) {
caryclarkeb75c7d2016-03-18 06:04:26 -0700499 if (!ValidDashPath(info.fPhase, info.fIntervals, info.fCount)) {
500 return false;
501 }
egdaniela22ea182014-06-11 06:51:51 -0700502 SkScalar initialDashLength = 0;
503 int32_t initialDashIndex = 0;
504 SkScalar intervalLength = 0;
505 CalcDashParameters(info.fPhase, info.fIntervals, info.fCount,
506 &initialDashLength, &initialDashIndex, &intervalLength);
caryclarkeb75c7d2016-03-18 06:04:26 -0700507 return InternalFilter(dst, src, rec, cullRect, info.fIntervals, info.fCount, initialDashLength,
egdaniela22ea182014-06-11 06:51:51 -0700508 initialDashIndex, intervalLength);
509}
caryclarkeb75c7d2016-03-18 06:04:26 -0700510
511bool SkDashPath::ValidDashPath(SkScalar phase, const SkScalar intervals[], int32_t count) {
512 if (count < 2 || !SkIsAlign2(count)) {
513 return false;
514 }
515 SkScalar length = 0;
516 for (int i = 0; i < count; i++) {
517 if (intervals[i] < 0) {
518 return false;
519 }
520 length += intervals[i];
521 }
522 // watch out for values that might make us go out of bounds
523 return length > 0 && SkScalarIsFinite(phase) && SkScalarIsFinite(length);
524}