| // Copyright 2013 The Flutter Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| import 'dart:math' as math; |
| import 'dart:typed_data'; |
| |
| import 'conic.dart'; |
| import 'cubic.dart'; |
| import 'path_iterator.dart'; |
| import 'path_ref.dart'; |
| import 'path_utils.dart'; |
| |
| /// Computes winding number and onCurveCount for a path and point. |
| class PathWinding { |
| PathWinding(this.pathRef, this.x, this.y) { |
| _walkPath(); |
| } |
| |
| final PathRef pathRef; |
| final double x; |
| final double y; |
| int _w = 0; |
| int _onCurveCount = 0; |
| |
| int get w => _w; |
| |
| int get onCurveCount => _onCurveCount; |
| |
| /// Buffer used for max(iterator result, chopped 3 cubics). |
| final Float32List _buffer = Float32List(8 + 10); |
| |
| /// Iterates through path and computes winding. |
| void _walkPath() { |
| final PathIterator iter = PathIterator(pathRef, true); |
| int verb; |
| while ((verb = iter.next(_buffer)) != SPath.kDoneVerb) { |
| switch (verb) { |
| case SPath.kMoveVerb: |
| case SPath.kCloseVerb: |
| break; |
| case SPath.kLineVerb: |
| _computeLineWinding(); |
| break; |
| case SPath.kQuadVerb: |
| _computeQuadWinding(); |
| break; |
| case SPath.kConicVerb: |
| _computeConicWinding(pathRef.conicWeights![iter.conicWeightIndex]); |
| break; |
| case SPath.kCubicVerb: |
| _computeCubicWinding(); |
| break; |
| } |
| } |
| } |
| |
| void _computeLineWinding() { |
| final double x0 = _buffer[0]; |
| final double startY = _buffer[1]; |
| double y0 = startY; |
| final double x1 = _buffer[2]; |
| final double endY = _buffer[3]; |
| double y1 = endY; |
| final double dy = y1 - y0; |
| int dir = 1; |
| // Swap so that y0 <= y1 holds. |
| if (y0 > y1) { |
| final double temp = y0; |
| y0 = y1; |
| y1 = temp; |
| dir = -1; |
| } |
| // If point is outside top/bottom bounds, winding is 0. |
| if (y < y0 || y > y1) { |
| return; |
| } |
| if (_checkOnCurve(x, y, x0, startY, x1, endY)) { |
| _onCurveCount++; |
| return; |
| } |
| if (y == y1) { |
| return; |
| } |
| // c = ax*by − ay*bx where a is the line and b is line formed from start |
| // to the given point(x,y). |
| final double crossProduct = (x1 - x0) * (y - startY) - dy * (x - x0); |
| if (crossProduct == 0) { |
| // zero cross means the point is on the line, and since the case where |
| // y of the query point is at the end point is handled above, we can be |
| // sure that we're on the line (excluding the end point) here. |
| if (x != x1 || y != endY) { |
| _onCurveCount++; |
| } |
| dir = 0; |
| } else if (SPath.scalarSignedAsInt(crossProduct) == dir) { |
| // Direction of cross product and line the same. |
| dir = 0; |
| } |
| _w += dir; |
| } |
| |
| // Check if point starts the line, handle special case for horizontal lines |
| // where and point except the end point is considered on curve. |
| static bool _checkOnCurve(double x, double y, double startX, double startY, |
| double endX, double endY) { |
| if (startY == endY) { |
| // Horizontal line. |
| return SPath.between(startX, x, endX) && x != endX; |
| } else { |
| return x == startX && y == startY; |
| } |
| } |
| |
| void _computeQuadWinding() { |
| // Check if we need to chop quadratic at extrema to compute 2 separate |
| // windings. |
| int n = 0; |
| if (!_isQuadMonotonic(_buffer)) { |
| n = _chopQuadAtExtrema(_buffer); |
| } |
| int winding = _computeMonoQuadWinding( |
| _buffer[0], _buffer[1], _buffer[2], _buffer[3], _buffer[4], _buffer[5]); |
| if (n > 0) { |
| winding += _computeMonoQuadWinding(_buffer[4], _buffer[5], _buffer[6], |
| _buffer[7], _buffer[8], _buffer[9]); |
| } |
| _w += winding; |
| } |
| |
| int _computeMonoQuadWinding( |
| double x0, double y0, double x1, double y1, double x2, double y2) { |
| int dir = 1; |
| final double startY = y0; |
| final double endY = y2; |
| if (y0 > y2) { |
| final double temp = y0; |
| y0 = y2; |
| y2 = temp; |
| dir = -1; |
| } |
| if (y < y0 || y > y2) { |
| return 0; |
| } |
| if (_checkOnCurve(x, y, x0, startY, x2, endY)) { |
| _onCurveCount++; |
| return 0; |
| } |
| if (y == y2) { |
| return 0; |
| } |
| |
| final QuadRoots quadRoots = QuadRoots(); |
| final int n = quadRoots.findRoots( |
| startY - 2 * y1 + endY, 2 * (y1 - startY), startY - y); |
| assert(n <= 1); |
| double xt; |
| if (0 == n) { |
| // zero roots are returned only when y0 == y |
| xt = dir == 1 ? x0 : x2; |
| } else { |
| final double t = quadRoots.root0!; |
| final double C = x0; |
| final double A = x2 - 2 * x1 + C; |
| final double B = 2 * (x1 - C); |
| xt = polyEval(A, B, C, t); |
| } |
| if (SPath.nearlyEqual(xt, x)) { |
| if (x != x2 || y != endY) { |
| // don't test end points; they're start points |
| _onCurveCount += 1; |
| return 0; |
| } |
| } |
| return xt < x ? dir : 0; |
| } |
| |
| /// Chops a non-monotonic quadratic curve, returns subdivisions and writes |
| /// result into [buffer]. |
| static int _chopQuadAtExtrema(Float32List buffer) { |
| final double x0 = buffer[0]; |
| final double y0 = buffer[1]; |
| final double x1 = buffer[2]; |
| final double y1 = buffer[3]; |
| final double x2 = buffer[4]; |
| final double y2 = buffer[5]; |
| final double? tValueAtExtrema = validUnitDivide(y0 - y1, y0 - y1 - y1 + y2); |
| if (tValueAtExtrema != null) { |
| // Chop quad at t value by interpolating along p0-p1 and p1-p2. |
| final double p01x = x0 + (tValueAtExtrema * (x1 - x0)); |
| final double p01y = y0 + (tValueAtExtrema * (y1 - y0)); |
| final double p12x = x1 + (tValueAtExtrema * (x2 - x1)); |
| final double p12y = y1 + (tValueAtExtrema * (y2 - y1)); |
| final double cx = p01x + (tValueAtExtrema * (p12x - p01x)); |
| final double cy = p01y + (tValueAtExtrema * (p12y - p01y)); |
| buffer[2] = p01x; |
| buffer[3] = p01y; |
| buffer[4] = cx; |
| buffer[5] = cy; |
| buffer[6] = p12x; |
| buffer[7] = p12y; |
| buffer[8] = x2; |
| buffer[9] = y2; |
| return 1; |
| } |
| // if we get here, we need to force output to be monotonic, even though |
| // we couldn't compute a unit divide value (probably underflow). |
| buffer[3] = (y0 - y1).abs() < (y1 - y2).abs() ? y0 : y2; |
| return 0; |
| } |
| |
| static bool _isQuadMonotonic(Float32List quad) { |
| final double y0 = quad[1]; |
| final double y1 = quad[3]; |
| final double y2 = quad[5]; |
| if (y0 == y1) { |
| return true; |
| } |
| if (y0 < y1) { |
| return y1 <= y2; |
| } else { |
| return y1 >= y2; |
| } |
| } |
| |
| void _computeConicWinding(double weight) { |
| final Conic conic = Conic(_buffer[0], _buffer[1], _buffer[2], _buffer[3], |
| _buffer[4], _buffer[5], weight); |
| // If the data points are very large, the conic may not be monotonic but may also |
| // fail to chop. Then, the chopper does not split the original conic in two. |
| final bool isMono = _isQuadMonotonic(_buffer); |
| final List<Conic> conics = <Conic>[]; |
| conic.chopAtYExtrema(conics); |
| _computeMonoConicWinding(conics[0]); |
| if (!isMono && conics.length == 2) { |
| _computeMonoConicWinding(conics[1]); |
| } |
| } |
| |
| void _computeMonoConicWinding(Conic conic) { |
| double y0 = conic.p0y; |
| double y2 = conic.p2y; |
| int dir = 1; |
| if (y0 > y2) { |
| final double swap = y0; |
| y0 = y2; |
| y2 = swap; |
| dir = -1; |
| } |
| if (y < y0 || y > y2) { |
| return; |
| } |
| if (_checkOnCurve(x, y, conic.p0x, conic.p0y, conic.p2x, conic.p2y)) { |
| _onCurveCount += 1; |
| return; |
| } |
| if (y == y2) { |
| return; |
| } |
| |
| double A = conic.p2y; |
| double B = conic.p1y * conic.fW - y * conic.fW + y; |
| double C = conic.p0y; |
| // A = a + c - 2*(b*w - yCept*w + yCept) |
| A += C - 2 * B; |
| // B = b*w - w * yCept + yCept - a |
| B -= C; |
| C -= y; |
| final QuadRoots quadRoots = QuadRoots(); |
| final int n = quadRoots.findRoots(A, 2 * B, C); |
| assert(n <= 1); |
| double xt; |
| if (0 == n) { |
| // zero roots are returned only when y0 == y |
| // Need [0] if dir == 1 |
| // and [2] if dir == -1 |
| xt = dir == 1 ? conic.p0x : conic.p2x; |
| } else { |
| final double root = quadRoots.root0!; |
| xt = |
| Conic.evalNumerator(conic.p0x, conic.p1x, conic.p2x, conic.fW, root) / |
| Conic.evalDenominator(conic.fW, root); |
| } |
| if (SPath.nearlyEqual(xt, x)) { |
| if (x != conic.p2x || y != conic.p2y) { |
| // don't test end points; they're start points |
| _onCurveCount += 1; |
| return; |
| } |
| } |
| _w += xt < x ? dir : 0; |
| } |
| |
| void _computeCubicWinding() { |
| final int n = chopCubicAtYExtrema(_buffer, _buffer); |
| for (int i = 0; i <= n; ++i) { |
| _windingMonoCubic(i * 3 * 2); |
| } |
| } |
| |
| void _windingMonoCubic(int bufferIndex) { |
| final int bufferStartPos = bufferIndex; |
| final double px0 = _buffer[bufferIndex++]; |
| final double py0 = _buffer[bufferIndex++]; |
| final double px1 = _buffer[bufferIndex++]; |
| bufferIndex++; |
| final double px2 = _buffer[bufferIndex++]; |
| bufferIndex++; |
| final double px3 = _buffer[bufferIndex++]; |
| final double py3 = _buffer[bufferIndex++]; |
| |
| double y0 = py0; |
| double y3 = py3; |
| |
| int dir = 1; |
| if (y0 > y3) { |
| final double swap = y0; |
| y0 = y3; |
| y3 = swap; |
| dir = -1; |
| } |
| if (y < y0 || y > y3) { |
| return; |
| } |
| if (_checkOnCurve(x, y, px0, py0, px3, py3)) { |
| _onCurveCount += 1; |
| return; |
| } |
| if (y == y3) { |
| return; |
| } |
| |
| // Quickly reject or accept |
| final double min = math.min(px0, math.min(px1, math.min(px2, px3))); |
| final double max = math.max(px0, math.max(px1, math.max(px2, px3))); |
| if (x < min) { |
| return; |
| } |
| if (x > max) { |
| _w += dir; |
| return; |
| } |
| // Compute the actual x(t) value. |
| final double? t = chopMonoAtY(_buffer, bufferStartPos, y); |
| if (t == null) { |
| return; |
| } |
| final double xt = evalCubicPts(px0, px1, px2, px3, t); |
| if (SPath.nearlyEqual(xt, x)) { |
| if (x != px3 || y != py3) { |
| // don't test end points; they're start points |
| _onCurveCount += 1; |
| return; |
| } |
| } |
| _w += xt < x ? dir : 0; |
| } |
| } |