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