Elide inner loop geometry sometimes (toy stroker)

Many times a pair of stroked line segments doesn't actually require the
inner "loop" geometry. I think I've classified where it's required into
two cases.

This CL checks for the two cases where it is necessary, and skips adding
the extra geometry if not necessary. Requires a few extra
multiplications.

Change-Id: I421e2365523c8f5b5c190b5493cbe0e0d3d3a6c0
Reviewed-on: https://skia-review.googlesource.com/c/skia/+/305017
Reviewed-by: Tyler Denniston <tdenniston@google.com>
Commit-Queue: Tyler Denniston <tdenniston@google.com>
diff --git a/samplecode/SampleSimpleStroker.cpp b/samplecode/SampleSimpleStroker.cpp
index 9a0377c..f778752 100644
--- a/samplecode/SampleSimpleStroker.cpp
+++ b/samplecode/SampleSimpleStroker.cpp
@@ -65,6 +65,9 @@
 
     // Returns the segment unit normal
     static SkPoint unitNormal(const PathSegment& seg, float t);
+
+    // Returns squared magnitude of line segments.
+    static float squaredLineLength(const PathSegment& lineSeg);
 };
 
 void SkPathStroker2::initForPath(const SkPath& path, const SkPaint& paint) {
@@ -195,17 +198,45 @@
         const float halfMiterLength = fRadius / sinHalfTheta;
         miterVec.setLength(halfMiterLength);  // TODO: miter length limit
 
-        // Outer: connect to the miter point, and then to t=0 (on outside) of next segment.
+        // Outer: connect to the miter point, and then to t=0 (on outside stroke) of next segment.
         const SkPoint dest = setLength(after, fRadius);
         outer->lineTo(miterMidpt + miterVec);
         outer->lineTo(miterMidpt + dest);
 
-        // Inner: we're already at t=1 (on inside) of 'prev'.
-        // Connect to the miter midpoint (common path endpoint of the two segments),
-        // and then to t=0 (on inside) of the next segment. This adds an interior "loop" of
-        // geometry that in many cases is unnecessary, but handles several edge cases.
-        inner->lineTo(miterMidpt);
-        inner->lineTo(miterMidpt - dest);
+        // Inner miter is more involved. We're already at t=1 (on inside stroke) of 'prev'.
+        // Check 2 cases to see we can directly connect to the inner miter point
+        // (midpoint - miterVec), or if we need to add extra "loop" geometry.
+        const SkPoint prevUnitTangent = rotate90(before);
+        const float radiusSquared = fRadius * fRadius;
+        // 'alpha' is angle between prev tangent and the curr inwards normal
+        const float cosAlpha = prevUnitTangent.dot(-after);
+        // Solve triangle for len^2:  radius^2 = len^2 + (radius * sin(alpha))^2
+        // This is the point at which the inside "corner" of curr at t=0 will lie on a
+        // line connecting the inner and outer corners of prev at t=0. If len is below
+        // this threshold, the inside corner of curr will "poke through" the start of prev,
+        // and we'll need the inner loop geometry.
+        const float threshold1 = radiusSquared * cosAlpha * cosAlpha;
+        // Solve triangle for len^2:  halfMiterLen^2 = radius^2 + len^2
+        // This is the point at which the inner miter point will lie on the inner stroke
+        // boundary of the curr segment. If len is below this threshold, the miter point
+        // moves 'inside' of the stroked outline, and we'll need the inner loop geometry.
+        const float threshold2 = halfMiterLength * halfMiterLength - radiusSquared;
+        // If a segment length is smaller than the larger of the two thresholds,
+        // we'll have to add the inner loop geometry.
+        const float maxLenSqd = std::max(threshold1, threshold2);
+        const bool needsInnerLoop =
+                squaredLineLength(prev) < maxLenSqd || squaredLineLength(curr) < maxLenSqd;
+        if (needsInnerLoop) {
+            // Connect to the miter midpoint (common path endpoint of the two segments),
+            // and then to t=0 (on inside) of the next segment. This adds an interior "loop" of
+            // geometry that handles edge cases where segment lengths are shorter than the
+            // stroke width.
+            inner->lineTo(miterMidpt);
+            inner->lineTo(miterMidpt - dest);
+        } else {
+            // Directly connect to inner miter point.
+            inner->setLastPt(miterMidpt - miterVec);
+        }
     };
 
     switch (fJoin) {
@@ -258,6 +289,12 @@
     return setLength(normal, 1);
 }
 
+float SkPathStroker2::squaredLineLength(const PathSegment& lineSeg) {
+    SkASSERT(lineSeg.fVerb == SkPath::kLine_Verb);
+    const SkPoint diff = lineSeg.fPoints[1] - lineSeg.fPoints[0];
+    return diff.dot(diff);
+}
+
 }  // namespace
 
 //////////////////////////////////////////////////////////////////////////////