diff --git a/ci/licenses_golden/licenses_flutter b/ci/licenses_golden/licenses_flutter index 20215273959f..8433b2263d88 100755 --- a/ci/licenses_golden/licenses_flutter +++ b/ci/licenses_golden/licenses_flutter @@ -492,9 +492,14 @@ FILE: ../../../flutter/lib/web_ui/lib/src/engine/surface/offset.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/surface/opacity.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/surface/painting.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/surface/path/conic.dart +FILE: ../../../flutter/lib/web_ui/lib/src/engine/surface/path/cubic.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/surface/path/path.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/surface/path/path_metrics.dart +FILE: ../../../flutter/lib/web_ui/lib/src/engine/surface/path/path_ref.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/surface/path/path_to_svg.dart +FILE: ../../../flutter/lib/web_ui/lib/src/engine/surface/path/path_utils.dart +FILE: ../../../flutter/lib/web_ui/lib/src/engine/surface/path/path_windings.dart +FILE: ../../../flutter/lib/web_ui/lib/src/engine/surface/path/tangent.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/surface/picture.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/surface/platform_view.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/surface/recording_canvas.dart diff --git a/lib/web_ui/lib/src/engine.dart b/lib/web_ui/lib/src/engine.dart index 6f613549aed0..64114b5455c2 100644 --- a/lib/web_ui/lib/src/engine.dart +++ b/lib/web_ui/lib/src/engine.dart @@ -100,9 +100,14 @@ part 'engine/surface/offset.dart'; part 'engine/surface/opacity.dart'; part 'engine/surface/painting.dart'; part 'engine/surface/path/conic.dart'; +part 'engine/surface/path/cubic.dart'; part 'engine/surface/path/path.dart'; part 'engine/surface/path/path_metrics.dart'; +part 'engine/surface/path/path_ref.dart'; part 'engine/surface/path/path_to_svg.dart'; +part 'engine/surface/path/path_utils.dart'; +part 'engine/surface/path/path_windings.dart'; +part 'engine/surface/path/tangent.dart'; part 'engine/surface/picture.dart'; part 'engine/surface/platform_view.dart'; part 'engine/surface/recording_canvas.dart'; diff --git a/lib/web_ui/lib/src/engine/canvas_pool.dart b/lib/web_ui/lib/src/engine/canvas_pool.dart index dee43849534d..14331c0767cf 100644 --- a/lib/web_ui/lib/src/engine/canvas_pool.dart +++ b/lib/web_ui/lib/src/engine/canvas_pool.dart @@ -509,69 +509,47 @@ class _CanvasPool extends _SaveStackTracking { } } + // Float buffer used for path iteration. + static Float32List _runBuffer = Float32List(PathRefIterator.kMaxBufferSize); + /// 'Runs' the given [path] by applying all of its commands to the canvas. void _runPath(html.CanvasRenderingContext2D ctx, SurfacePath path) { ctx.beginPath(); - final List subpaths = path.subpaths; - final int subpathCount = subpaths.length; - for (int subPathIndex = 0; subPathIndex < subpathCount; subPathIndex++) { - final Subpath subpath = subpaths[subPathIndex]; - final List commands = subpath.commands; - final int commandCount = commands.length; - for (int c = 0; c < commandCount; c++) { - final PathCommand command = commands[c]; - switch (command.type) { - case PathCommandTypes.bezierCurveTo: - final BezierCurveTo curve = command as BezierCurveTo; - ctx.bezierCurveTo( - curve.x1, curve.y1, curve.x2, curve.y2, curve.x3, curve.y3); - break; - case PathCommandTypes.close: - ctx.closePath(); - break; - case PathCommandTypes.ellipse: - final Ellipse ellipse = command as Ellipse; - if (c == 0) { - // Ellipses that start a new path need to set start point, - // otherwise it incorrectly uses last point. - ctx.moveTo(subpath.startX, subpath.startY); - } - DomRenderer.ellipse(ctx, - ellipse.x, - ellipse.y, - ellipse.radiusX, - ellipse.radiusY, - ellipse.rotation, - ellipse.startAngle, - ellipse.endAngle, - ellipse.anticlockwise); - break; - case PathCommandTypes.lineTo: - final LineTo lineTo = command as LineTo; - ctx.lineTo(lineTo.x, lineTo.y); - break; - case PathCommandTypes.moveTo: - final MoveTo moveTo = command as MoveTo; - ctx.moveTo(moveTo.x, moveTo.y); - break; - case PathCommandTypes.rRect: - final RRectCommand rrectCommand = command as RRectCommand; - _RRectToCanvasRenderer(ctx) - .render(rrectCommand.rrect, startNewPath: false); - break; - case PathCommandTypes.rect: - final RectCommand rectCommand = command as RectCommand; - ctx.rect(rectCommand.x, rectCommand.y, rectCommand.width, - rectCommand.height); - break; - case PathCommandTypes.quadraticCurveTo: - final QuadraticCurveTo quadraticCurveTo = command as QuadraticCurveTo; - ctx.quadraticCurveTo(quadraticCurveTo.x1, quadraticCurveTo.y1, - quadraticCurveTo.x2, quadraticCurveTo.y2); - break; - default: - throw UnimplementedError('Unknown path command $command'); - } + final Float32List p = _runBuffer; + final PathRefIterator iter = PathRefIterator(path.pathRef); + int verb = 0; + while ((verb = iter.next(p)) != SPath.kDoneVerb) { + switch (verb) { + case SPath.kMoveVerb: + ctx.moveTo(p[0], p[1]); + break; + case SPath.kLineVerb: + ctx.lineTo(p[2], p[3]); + break; + case SPath.kCubicVerb: + ctx.bezierCurveTo(p[2], p[3], p[4], p[5], p[6], p[7]); + break; + case SPath.kQuadVerb: + ctx.quadraticCurveTo(p[2], p[3], p[4], p[5]); + break; + case SPath.kConicVerb: + final double w = iter.conicWeight; + Conic conic = Conic(p[0], p[1], p[2], p[3], p[4], p[5], w); + List points = conic.toQuads(); + final int len = points.length; + for (int i = 1; i < len; i += 2) { + final double p1x = points[i].dx; + final double p1y = points[i].dy; + final double p2x = points[i + 1].dx; + final double p2y = points[i + 1].dy; + ctx.quadraticCurveTo(p1x, p1y, p2x, p2y); + } + break; + case SPath.kCloseVerb: + ctx.closePath(); + break; + default: + throw UnimplementedError('Unknown path verb $verb'); } } } diff --git a/lib/web_ui/lib/src/engine/surface/clip.dart b/lib/web_ui/lib/src/engine/surface/clip.dart index d49be6ab45db..75d7957f770c 100644 --- a/lib/web_ui/lib/src/engine/surface/clip.dart +++ b/lib/web_ui/lib/src/engine/surface/clip.dart @@ -2,7 +2,6 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. - part of engine; /// Mixin used by surfaces that clip their contents using an overflowing DOM @@ -261,15 +260,15 @@ class PersistedPhysicalShape extends PersistedContainerSurface } return; } else { - final Ellipse? ellipse = path.webOnlyPathAsCircle; - if (ellipse != null) { - final double rx = ellipse.radiusX; - final double ry = ellipse.radiusY; + final ui.Rect? ovalRect = path.webOnlyPathAsCircle; + if (ovalRect != null) { + final double rx = ovalRect.width / 2.0; + final double ry = ovalRect.height / 2.0; final String borderRadius = rx == ry ? '${rx}px ' : '${rx}px ${ry}px '; final html.CssStyleDeclaration style = rootElement!.style; - final double left = ellipse.x - rx; - final double top = ellipse.y - ry; + final double left = ovalRect.left; + final double top = ovalRect.top; style ..left = '${left}px' ..top = '${top}px' diff --git a/lib/web_ui/lib/src/engine/surface/path/conic.dart b/lib/web_ui/lib/src/engine/surface/path/conic.dart index 4db6a5f2a7c1..1c0e48a9c9c0 100644 --- a/lib/web_ui/lib/src/engine/surface/path/conic.dart +++ b/lib/web_ui/lib/src/engine/surface/path/conic.dart @@ -78,10 +78,6 @@ class Conic { return pointList; } - static bool _between(double a, double b, double c) { - return (a - b) * (c - b) <= 0; - } - // Subdivides a conic and writes to points list. static void _subdivide(Conic src, int level, List pointList) { assert(level >= 0); @@ -99,30 +95,30 @@ class Conic { final double startY = src.p0y; final double endY = src.p2y; final double cpY = src.p1y; - if (_between(startY, cpY, endY)) { + if (SPath.between(startY, cpY, endY)) { // Ensure that chopped conics maintain their y-order. final double midY = conic0.p2y; - if (!_between(startY, midY, endY)) { + if (!SPath.between(startY, midY, endY)) { // The computed midpoint is outside end points, move it to // closer one. final double closerY = (midY - startY).abs() < (midY - endY).abs() ? startY : endY; conic0.p2y = conic1.p0y = closerY; } - if (!_between(startY, conic0.p1y, conic0.p2y)) { + if (!SPath.between(startY, conic0.p1y, conic0.p2y)) { // First control point not between start and end points, move it // to start. conic0.p1y = startY; } - if (!_between(conic1.p0y, conic1.p1y, endY)) { + if (!SPath.between(conic1.p0y, conic1.p1y, endY)) { // Second control point not between start and end points, move it // to end. conic1.p1y = endY; } // Verify that conics points are ordered. - assert(_between(startY, conic0.p1y, conic0.p2y)); - assert(_between(conic0.p1y, conic0.p2y, conic1.p1y)); - assert(_between(conic0.p2y, conic1.p1y, endY)); + assert(SPath.between(startY, conic0.p1y, conic0.p2y)); + assert(SPath.between(conic0.p1y, conic0.p2y, conic1.p1y)); + assert(SPath.between(conic0.p2y, conic1.p1y, endY)); } --level; _subdivide(conic0, level, pointList); @@ -152,6 +148,118 @@ class Conic { (p2y + wp1.dy) * scale, p2x, p2y, newW); } + void chopAtYExtrema(List dst) { + double? t = _findYExtrema(); + if (t == null) { + dst.add(this); + return; + } + if (!_chopAt(t, dst, cleanupMiddle: true)) { + // If chop can't return finite values, don't chop. + dst.add(this); + return; + } + } + + /////////////////////////////////////////////////////////////////////////////// + // + // NURB representation for conics. Helpful explanations at: + // + // http://citeseerx.ist.psu.edu/viewdoc/ + // download?doi=10.1.1.44.5740&rep=rep1&type=ps + // and + // http://www.cs.mtu.edu/~shene/COURSES/cs3621/NOTES/spline/NURBS/RB-conics.html + // + // F = (A (1 - t)^2 + C t^2 + 2 B (1 - t) t w) + // ------------------------------------------ + // ((1 - t)^2 + t^2 + 2 (1 - t) t w) + // + // = {t^2 (P0 + P2 - 2 P1 w), t (-2 P0 + 2 P1 w), P0} + // ------------------------------------------------ + // {t^2 (2 - 2 w), t (-2 + 2 w), 1} + // + // F' = 2 (C t (1 + t (-1 + w)) - A (-1 + t) (t (-1 + w) - w) + B (1 - 2 t) w) + // + // t^2 : (2 P0 - 2 P2 - 2 P0 w + 2 P2 w) + // t^1 : (-2 P0 + 2 P2 + 4 P0 w - 4 P1 w) + // t^0 : -2 P0 w + 2 P1 w + // + // We disregard magnitude, so we can freely ignore the denominator of F', and + // divide the numerator by 2 + // + // coeff[0] for t^2 + // coeff[1] for t^1 + // coeff[2] for t^0 + // + double? _findYExtrema() { + final double p20 = p2y - p0y; + final double p10 = p1y - p0y; + final double wP10 = fW * p10; + final double coeff0 = fW * p20 - p20; + final double coeff1 = p20 - 2 * wP10; + final double coeff2 = wP10; + final _QuadRoots quadRoots = _QuadRoots(); + int rootCount = quadRoots.findRoots(coeff0, coeff1, coeff2); + assert(rootCount == 0 || rootCount == 1); + if (rootCount == 1) { + return quadRoots.root0; + } + return null; + } + + bool _chopAt(double t, List dst, {bool cleanupMiddle = false}) { + // Map conic to 3D. + final double tx0 = p0x; + final double ty0 = p0y; + final double tz0 = 1; + final double tx1 = p1x * fW; + final double ty1 = p1y * fW; + final double tz1 = fW; + final double tx2 = p2x; + final double ty2 = p2y; + final double tz2 = 1; + // Now interpolate each dimension. + final double dx0 = tx0 + (tx1 - tx0) * t; + final double dx2 = tx1 + (tx2 - tx1) * t; + final double dx1 = dx0 + (dx2 - dx0) * t; + final double dy0 = ty0 + (ty1 - ty0) * t; + final double dy2 = ty1 + (ty2 - ty1) * t; + final double dy1 = dy0 + (dy2 - dy0) * t; + final double dz0 = tz0 + (tz1 - tz0) * t; + final double dz2 = tz1 + (tz2 - tz1) * t; + final double dz1 = dz0 + (dz2 - dz0) * t; + // Compute new weights. + final double root = math.sqrt(dz1); + if (_nearlyEqual(root, 0)) { + return false; + } + final double w0 = dz0 / root; + final double w2 = dz2 / root; + if (_nearlyEqual(dz0, 0) || _nearlyEqual(dz1, 0) || _nearlyEqual(dz2, 0)) { + return false; + } + // Now we can construct the 2 conics by projecting 3D down to 2D. + final double chopPointX = dx1 / dz1; + final double chopPointY = dy1 / dz1; + + double cp0y = dy0 / dz0; + double cp1y = dy2 / dz2; + if (cleanupMiddle) { + // Clean-up the middle, since we know t was meant to be at + // an Y-extrema. + cp0y = chopPointY; + cp1y = chopPointY; + } + + final Conic conic0 = + Conic(p0x, p0y, dx0 / dz0, cp0y, chopPointX, chopPointY, w0); + final Conic conic1 = + Conic(chopPointX, chopPointY, dx2 / dz2, cp1y, p2x, p2y, w2); + dst.add(conic0); + dst.add(conic1); + return true; + } + /// Computes number of binary subdivisions of the curve given /// the tolerance. /// @@ -186,6 +294,179 @@ class Conic { } return pow2; } + + ui.Offset evalTangentAt(double t) { + // The derivative equation returns a zero tangent vector when t is 0 or 1, + // and the control point is equal to the end point. + // In this case, use the conic endpoints to compute the tangent. + if ((t == 0 && p0x == p1x && p0y == p1y) || + (t == 1 && p1x == p2x && p1y == p2y)) { + return ui.Offset(p2x - p0x, p2y - p0y); + } + double p20x = p2x - p0x; + double p20y = p2y - p0y; + double p10x = p1x - p0x; + double p10y = p1y - p0y; + + double cx = fW * p10x; + double cy = fW * p10y; + double ax = fW * p20x - p20x; + double ay = fW * p20y - p20y; + double bx = p20x - cx - cx; + double by = p20y - cy - cy; + _SkQuadCoefficients quadC = _SkQuadCoefficients(ax, ay, bx, by, cx, cy); + return ui.Offset(quadC.evalX(t), quadC.evalY(t)); + } +} + +double _conicEvalNumerator( + double p0, double p1, double p2, double w, double t) { + assert(t >= 0 && t <= 1); + final double src2w = p1 * w; + final C = p0; + final A = p2 - 2 * src2w + C; + final B = 2 * (src2w - C); + return polyEval(A, B, C, t); +} + +double _conicEvalDenominator(double w, double t) { + double B = 2 * (w - 1); + double C = 1; + double A = -B; + return polyEval(A, B, C, t); +} + +class _QuadBounds { + double minX = 0; + double minY = 0; + double maxX = 0; + double maxY = 0; + void calculateBounds(Float32List points, int pointIndex) { + final double x1 = points[pointIndex++]; + final double y1 = points[pointIndex++]; + final double cpX = points[pointIndex++]; + final double cpY = points[pointIndex++]; + final double x2 = points[pointIndex++]; + final double y2 = points[pointIndex++]; + + minX = math.min(x1, x2); + minY = math.min(y1, y2); + maxX = math.max(x1, x2); + maxY = math.max(y1, y2); + + // At extrema's derivative = 0. + // Solve for + // -2x1+2tx1 + 2cpX + 4tcpX + 2tx2 = 0 + // -2x1 + 2cpX +2t(x1 + 2cpX + x2) = 0 + // t = (x1 - cpX) / (x1 - 2cpX + x2) + double denom = x1 - (2 * cpX) + x2; + if (denom.abs() > SPath.scalarNearlyZero) { + final double t1 = (x1 - cpX) / denom; + if ((t1 >= 0) && (t1 <= 1.0)) { + // Solve (x,y) for curve at t = tx to find extrema + final double tprime = 1.0 - t1; + final double extremaX = + (tprime * tprime * x1) + (2 * t1 * tprime * cpX) + (t1 * t1 * x2); + final double extremaY = + (tprime * tprime * y1) + (2 * t1 * tprime * cpY) + (t1 * t1 * y2); + // Expand bounds. + minX = math.min(minX, extremaX); + maxX = math.max(maxX, extremaX); + minY = math.min(minY, extremaY); + maxY = math.max(maxY, extremaY); + } + } + // Now calculate dy/dt = 0 + denom = y1 - (2 * cpY) + y2; + if (denom.abs() > SPath.scalarNearlyZero) { + final double t2 = (y1 - cpY) / denom; + if ((t2 >= 0) && (t2 <= 1.0)) { + final double tprime2 = 1.0 - t2; + final double extrema2X = (tprime2 * tprime2 * x1) + + (2 * t2 * tprime2 * cpX) + + (t2 * t2 * x2); + final double extrema2Y = (tprime2 * tprime2 * y1) + + (2 * t2 * tprime2 * cpY) + + (t2 * t2 * y2); + // Expand bounds. + minX = math.min(minX, extrema2X); + maxX = math.max(maxX, extrema2X); + minY = math.min(minY, extrema2Y); + maxY = math.max(maxY, extrema2Y); + } + } + } +} + +class _ConicBounds { + double minX = 0; + double minY = 0; + double maxX = 0; + double maxY = 0; + void calculateBounds(Float32List points, double w, int pointIndex) { + final double x1 = points[pointIndex++]; + final double y1 = points[pointIndex++]; + final double cpX = points[pointIndex++]; + final double cpY = points[pointIndex++]; + final double x2 = points[pointIndex++]; + final double y2 = points[pointIndex++]; + + minX = math.min(x1, x2); + minY = math.min(y1, y2); + maxX = math.max(x1, x2); + maxY = math.max(y1, y2); + + // {t^2 (P0 + P2 - 2 P1 w), t (-2 P0 + 2 P1 w), P0} + // ------------------------------------------------ + // {t^2 (2 - 2 w), t (-2 + 2 w), 1} + // Calculate coefficients and solve root. + _QuadRoots roots = _QuadRoots(); + final double P20x = x2 - x1; + final double P10x = cpX - x1; + final double wP10x = w * P10x; + double ax = w * P20x - P20x; + double bx = P20x - 2 * wP10x; + double cx = wP10x; + int n = roots.findRoots(ax, bx, cx); + if (n != 0) { + final double t1 = roots.root0!; + if ((t1 >= 0) && (t1 <= 1.0)) { + final double denom = _conicEvalDenominator(w, t1); + double numerator = _conicEvalNumerator(x1, cpX, x2, w, t1); + final double extremaX = numerator / denom; + numerator = _conicEvalNumerator(y1, cpY, y2, w, t1); + final double extremaY = numerator / denom; + // Expand bounds. + minX = math.min(minX, extremaX); + maxX = math.max(maxX, extremaX); + minY = math.min(minY, extremaY); + maxY = math.max(maxY, extremaY); + } + } + final double P20y = y2 - y1; + final double P10y = cpY - y1; + final double wP10y = w * P10y; + double a = w * P20y - P20y; + double b = P20y - 2 * wP10y; + double c = wP10y; + n = roots.findRoots(a, b, c); + + if (n != 0) { + final double t2 = roots.root0!; + if ((t2 >= 0) && (t2 <= 1.0)) { + final double denom = _conicEvalDenominator(w, t2); + double numerator = _conicEvalNumerator(x1, cpX, x2, w, t2); + final double extrema2X = numerator / denom; + numerator = _conicEvalNumerator(y1, cpY, y2, w, t2); + final double extrema2Y = numerator / denom; + // Expand bounds. + minX = math.min(minX, extrema2X); + maxX = math.max(maxX, extrema2X); + minY = math.min(minY, extrema2Y); + maxY = math.max(maxY, extrema2Y); + } + } + } } class _ConicPair { diff --git a/lib/web_ui/lib/src/engine/surface/path/cubic.dart b/lib/web_ui/lib/src/engine/surface/path/cubic.dart new file mode 100644 index 000000000000..a2358cd6d3ee --- /dev/null +++ b/lib/web_ui/lib/src/engine/surface/path/cubic.dart @@ -0,0 +1,414 @@ +// 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. + +part of engine; + +/// Chops cubic at Y extrema points and writes result to [dest]. +/// +/// [points] and [dest] are allowed to share underlying storage as long. +int _chopCubicAtYExtrema(Float32List points, Float32List dest) { + final double y0 = points[1]; + final double y1 = points[3]; + final double y2 = points[5]; + final double y3 = points[7]; + _QuadRoots _quadRoots = _findCubicExtrema(y0, y1, y2, y3); + final List roots = _quadRoots.roots; + if (roots.isEmpty) { + // No roots, just use input cubic. + return 0; + } + _chopCubicAt(roots, points, dest); + final int rootCount = roots.length; + if (rootCount > 0) { + // Cleanup to ensure Y extrema are flat. + dest[5] = dest[9] = dest[7]; + if (rootCount == 2) { + dest[11] = dest[15] = dest[13]; + } + } + return rootCount; +} + +_QuadRoots _findCubicExtrema(double a, double b, double c, double d) { + // A,B,C scaled by 1/3 to simplify + final double A = d - a + 3 * (b - c); + final double B = 2 * (a - b - b + c); + final double C = b - a; + return _QuadRoots()..findRoots(A, B, C); +} + +/// Subdivides cubic curve for a list of t values. +void _chopCubicAt( + List tValues, Float32List points, Float32List outPts) { + if (assertionsEnabled) { + for (int i = 0; i < tValues.length - 1; i++) { + final double tValue = tValues[i]; + assert(tValue > 0 && tValue < 1, + 'Not expecting to chop curve at start, end points'); + } + for (int i = 0; i < tValues.length - 1; i++) { + final double tValue = tValues[i]; + final double nextTValue = tValues[i + 1]; + assert( + nextTValue > tValue, 'Expecting t value to monotonically increase'); + } + } + int rootCount = tValues.length; + if (0 == rootCount) { + for (int i = 0; i < 8; i++) { + outPts[i] = points[i]; + } + } else { + // Chop curve at t value and loop through right side of curve + // while normalizing t value based on prior t. + double? t = tValues[0]; + int bufferPos = 0; + for (int i = 0; i < rootCount; i++) { + _chopCubicAtT(points, bufferPos, outPts, bufferPos, t!); + if (i == rootCount - 1) { + break; + } + bufferPos += 6; + + // watch out in case the renormalized t isn't in range + if ((t = _validUnitDivide( + tValues[i + 1] - tValues[i], 1.0 - tValues[i])) == + null) { + // Can't renormalize last point, just create a degenerate cubic. + outPts[bufferPos + 4] = outPts[bufferPos + 5] = + outPts[bufferPos + 6] = points[bufferPos + 3]; + break; + } + } + } +} + +/// Subdivides cubic curve at [t] and writes to [outPts] at position [outIndex]. +/// +/// The cubic points are read from [points] at [bufferPos] offset. +void _chopCubicAtT(Float32List points, int bufferPos, Float32List outPts, + int outIndex, double t) { + assert(t > 0 && t < 1); + final double p3y = points[bufferPos + 7]; + final double p0x = points[bufferPos + 0]; + final double p0y = points[bufferPos + 1]; + final double p1x = points[bufferPos + 2]; + final double p1y = points[bufferPos + 3]; + final double p2x = points[bufferPos + 4]; + final double p2y = points[bufferPos + 5]; + final double p3x = points[bufferPos + 6]; + // If startT == 0 chop at end point and return curve. + final double ab1x = _interpolate(p0x, p1x, t); + final double ab1y = _interpolate(p0y, p1y, t); + final double bc1x = _interpolate(p1x, p2x, t); + final double bc1y = _interpolate(p1y, p2y, t); + final double cd1x = _interpolate(p2x, p3x, t); + final double cd1y = _interpolate(p2y, p3y, t); + final double abc1x = _interpolate(ab1x, bc1x, t); + final double abc1y = _interpolate(ab1y, bc1y, t); + final double bcd1x = _interpolate(bc1x, cd1x, t); + final double bcd1y = _interpolate(bc1y, cd1y, t); + final double abcd1x = _interpolate(abc1x, bcd1x, t); + final double abcd1y = _interpolate(abc1y, bcd1y, t); + + // Return left side of curve. + outPts[outIndex++] = p0x; + outPts[outIndex++] = p0y; + outPts[outIndex++] = ab1x; + outPts[outIndex++] = ab1y; + outPts[outIndex++] = abc1x; + outPts[outIndex++] = abc1y; + outPts[outIndex++] = abcd1x; + outPts[outIndex++] = abcd1y; + // Return right side of curve. + outPts[outIndex++] = bcd1x; + outPts[outIndex++] = bcd1y; + outPts[outIndex++] = cd1x; + outPts[outIndex++] = cd1y; + outPts[outIndex++] = p3x; + outPts[outIndex++] = p3y; +} + +// Returns t at Y for cubic curve. null if y is out of range. +// +// Options are Newton Raphson (quadratic convergence with typically +// 3 iterations or bisection with 16 iterations. +double? _chopMonoAtY(Float32List _buffer, int bufferStartPos, double y) { + // Translate curve points relative to y. + final double ycrv0 = _buffer[1 + bufferStartPos] - y; + final double ycrv1 = _buffer[3 + bufferStartPos] - y; + final double ycrv2 = _buffer[5 + bufferStartPos] - y; + final double ycrv3 = _buffer[7 + bufferStartPos] - y; + // Positive and negative function parameters. + double tNeg, tPos; + // Set initial t points to converge from. + if (ycrv0 < 0) { + if (ycrv3 < 0) { + // Start and end points out of range. + return null; + } + tNeg = 0; + tPos = 1.0; + } else if (ycrv0 > 0) { + tNeg = 1.0; + tPos = 0; + } else { + // Start is at y. + return 0.0; + } + + // Bisection / linear convergance. + final double tolerance = 1.0 / 65536; + do { + final double tMid = (tPos + tNeg) / 2.0; + final double y01 = ycrv0 + (ycrv1 - ycrv0) * tMid; + final double y12 = ycrv1 + (ycrv2 - ycrv1) * tMid; + final double y23 = ycrv2 + (ycrv3 - ycrv2) * tMid; + final double y012 = y01 + (y12 - y01) * tMid; + final double y123 = y12 + (y23 - y12) * tMid; + final double y0123 = y012 + (y123 - y012) * tMid; + if (y0123 == 0) { + return tMid; + } + if (y0123 < 0) { + tNeg = tMid; + } else { + tPos = tMid; + } + } while (((tPos - tNeg).abs() > tolerance)); + return (tNeg + tPos) / 2; +} + +double _evalCubicPts(double c0, double c1, double c2, double c3, double t) { + double A = c3 + 3 * (c1 - c2) - c0; + double B = 3 * (c2 - c1 - c1 + c0); + double C = 3 * (c1 - c0); + double D = c0; + return polyEval4(A, B, C, D, t); +} + +// Reusable class to compute bounds without object allocation. +class _CubicBounds { + double minX = 0.0; + double maxX = 0.0; + double minY = 0.0; + double maxY = 0.0; + + /// Sets resulting bounds as [minX], [minY], [maxX], [maxY]. + /// + /// The cubic is defined by 4 points (8 floats) in [points]. + void calculateBounds(Float32List points, int pointIndex) { + final double startX = points[pointIndex++]; + final double startY = points[pointIndex++]; + final double cpX1 = points[pointIndex++]; + final double cpY1 = points[pointIndex++]; + final double cpX2 = points[pointIndex++]; + final double cpY2 = points[pointIndex++]; + final double endX = points[pointIndex++]; + final double endY = points[pointIndex++]; + // Bounding box is defined by all points on the curve where + // monotonicity changes. + minX = math.min(startX, endX); + minY = math.min(startY, endY); + maxX = math.max(startX, endX); + maxY = math.max(startY, endY); + + double extremaX; + double extremaY; + double a, b, c; + + // Check for simple case of strong ordering before calculating + // extrema + if (!(((startX < cpX1) && (cpX1 < cpX2) && (cpX2 < endX)) || + ((startX > cpX1) && (cpX1 > cpX2) && (cpX2 > endX)))) { + // The extrema point is dx/dt B(t) = 0 + // The derivative of B(t) for cubic bezier is a quadratic equation + // with multiple roots + // B'(t) = a*t*t + b*t + c*t + a = -startX + (3 * (cpX1 - cpX2)) + endX; + b = 2 * (startX - (2 * cpX1) + cpX2); + c = -startX + cpX1; + + // Now find roots for quadratic equation with known coefficients + // a,b,c + // The roots are (-b+-sqrt(b*b-4*a*c)) / 2a + num s = (b * b) - (4 * a * c); + // If s is negative, we have no real roots + if ((s >= 0.0) && (a.abs() > SPath.scalarNearlyZero)) { + if (s == 0.0) { + // we have only 1 root + final double t = -b / (2 * a); + final double tprime = 1.0 - t; + if ((t >= 0.0) && (t <= 1.0)) { + extremaX = ((tprime * tprime * tprime) * startX) + + ((3 * tprime * tprime * t) * cpX1) + + ((3 * tprime * t * t) * cpX2) + + (t * t * t * endX); + minX = math.min(extremaX, minX); + maxX = math.max(extremaX, maxX); + } + } else { + // we have 2 roots + s = math.sqrt(s); + double t = (-b - s) / (2 * a); + double tprime = 1.0 - t; + if ((t >= 0.0) && (t <= 1.0)) { + extremaX = ((tprime * tprime * tprime) * startX) + + ((3 * tprime * tprime * t) * cpX1) + + ((3 * tprime * t * t) * cpX2) + + (t * t * t * endX); + minX = math.min(extremaX, minX); + maxX = math.max(extremaX, maxX); + } + // check 2nd root + t = (-b + s) / (2 * a); + tprime = 1.0 - t; + if ((t >= 0.0) && (t <= 1.0)) { + extremaX = ((tprime * tprime * tprime) * startX) + + ((3 * tprime * tprime * t) * cpX1) + + ((3 * tprime * t * t) * cpX2) + + (t * t * t * endX); + + minX = math.min(extremaX, minX); + maxX = math.max(extremaX, maxX); + } + } + } + } + + // Now calc extremes for dy/dt = 0 just like above + if (!(((startY < cpY1) && (cpY1 < cpY2) && (cpY2 < endY)) || + ((startY > cpY1) && (cpY1 > cpY2) && (cpY2 > endY)))) { + // The extrema point is dy/dt B(t) = 0 + // The derivative of B(t) for cubic bezier is a quadratic equation + // with multiple roots + // B'(t) = a*t*t + b*t + c*t + a = -startY + (3 * (cpY1 - cpY2)) + endY; + b = 2 * (startY - (2 * cpY1) + cpY2); + c = -startY + cpY1; + + // Now find roots for quadratic equation with known coefficients + // a,b,c + // The roots are (-b+-sqrt(b*b-4*a*c)) / 2a + double s = (b * b) - (4 * a * c); + // If s is negative, we have no real roots + if ((s >= 0.0) && (a.abs() > SPath.scalarNearlyZero)) { + if (s == 0.0) { + // we have only 1 root + final double t = -b / (2 * a); + final double tprime = 1.0 - t; + if ((t >= 0.0) && (t <= 1.0)) { + extremaY = ((tprime * tprime * tprime) * startY) + + ((3 * tprime * tprime * t) * cpY1) + + ((3 * tprime * t * t) * cpY2) + + (t * t * t * endY); + minY = math.min(extremaY, minY); + maxY = math.max(extremaY, maxY); + } + } else { + // we have 2 roots + s = math.sqrt(s); + final double t = (-b - s) / (2 * a); + final double tprime = 1.0 - t; + if ((t >= 0.0) && (t <= 1.0)) { + extremaY = ((tprime * tprime * tprime) * startY) + + ((3 * tprime * tprime * t) * cpY1) + + ((3 * tprime * t * t) * cpY2) + + (t * t * t * endY); + minY = math.min(extremaY, minY); + maxY = math.max(extremaY, maxY); + } + // check 2nd root + final double t2 = (-b + s) / (2 * a); + final double tprime2 = 1.0 - t2; + if ((t2 >= 0.0) && (t2 <= 1.0)) { + extremaY = ((tprime2 * tprime2 * tprime2) * startY) + + ((3 * tprime2 * tprime2 * t2) * cpY1) + + ((3 * tprime2 * t2 * t2) * cpY2) + + (t2 * t2 * t2 * endY); + minY = math.min(extremaY, minY); + maxY = math.max(extremaY, maxY); + } + } + } + } + } +} + +/// Chops cubic spline at startT and stopT, writes result to buffer. +void _chopCubicBetweenT( + List points, double startT, double stopT, Float32List buffer) { + assert(startT != 0 || stopT != 0); + final double p3y = points[7]; + final double p0x = points[0]; + final double p0y = points[1]; + final double p1x = points[2]; + final double p1y = points[3]; + final double p2x = points[4]; + final double p2y = points[5]; + final double p3x = points[6]; + // If startT == 0 chop at end point and return curve. + final bool chopStart = startT != 0; + final double t = chopStart ? startT : stopT; + + final double ab1x = _interpolate(p0x, p1x, t); + final double ab1y = _interpolate(p0y, p1y, t); + final double bc1x = _interpolate(p1x, p2x, t); + final double bc1y = _interpolate(p1y, p2y, t); + final double cd1x = _interpolate(p2x, p3x, t); + final double cd1y = _interpolate(p2y, p3y, t); + final double abc1x = _interpolate(ab1x, bc1x, t); + final double abc1y = _interpolate(ab1y, bc1y, t); + final double bcd1x = _interpolate(bc1x, cd1x, t); + final double bcd1y = _interpolate(bc1y, cd1y, t); + final double abcd1x = _interpolate(abc1x, bcd1x, t); + final double abcd1y = _interpolate(abc1y, bcd1y, t); + if (!chopStart) { + // Return left side of curve. + buffer[0] = p0x; + buffer[1] = p0y; + buffer[2] = ab1x; + buffer[3] = ab1y; + buffer[4] = abc1x; + buffer[5] = abc1y; + buffer[6] = abcd1x; + buffer[7] = abcd1y; + return; + } + if (stopT == 1) { + // Return right side of curve. + buffer[0] = abcd1x; + buffer[1] = abcd1y; + buffer[2] = bcd1x; + buffer[3] = bcd1y; + buffer[4] = cd1x; + buffer[5] = cd1y; + buffer[6] = p3x; + buffer[7] = p3y; + return; + } + // We chopped at startT, now the right hand side of curve is at + // abcd1, bcd1, cd1, p3x, p3y. Chop this part using endT; + final double endT = (stopT - startT) / (1 - startT); + final double ab2x = _interpolate(abcd1x, bcd1x, endT); + final double ab2y = _interpolate(abcd1y, bcd1y, endT); + final double bc2x = _interpolate(bcd1x, cd1x, endT); + final double bc2y = _interpolate(bcd1y, cd1y, endT); + final double cd2x = _interpolate(cd1x, p3x, endT); + final double cd2y = _interpolate(cd1y, p3y, endT); + final double abc2x = _interpolate(ab2x, bc2x, endT); + final double abc2y = _interpolate(ab2y, bc2y, endT); + final double bcd2x = _interpolate(bc2x, cd2x, endT); + final double bcd2y = _interpolate(bc2y, cd2y, endT); + final double abcd2x = _interpolate(abc2x, bcd2x, endT); + final double abcd2y = _interpolate(abc2y, bcd2y, endT); + buffer[0] = abcd1x; + buffer[1] = abcd1y; + buffer[2] = ab2x; + buffer[3] = ab2y; + buffer[4] = abc2x; + buffer[5] = abc2y; + buffer[6] = abcd2x; + buffer[7] = abcd2y; +} diff --git a/lib/web_ui/lib/src/engine/surface/path/path.dart b/lib/web_ui/lib/src/engine/surface/path/path.dart index fed66470e089..acba238d5e05 100644 --- a/lib/web_ui/lib/src/engine/surface/path/path.dart +++ b/lib/web_ui/lib/src/engine/surface/path/path.dart @@ -6,64 +6,73 @@ part of engine; /// A complex, one-dimensional subset of a plane. /// -/// A path consists of a number of subpaths, and a _current point_. -/// -/// Subpaths consist of segments of various types, such as lines, +/// Path consist of segments of various types, such as lines, /// arcs, or beziers. Subpaths can be open or closed, and can /// self-intersect. /// -/// Closed subpaths enclose a (possibly discontiguous) region of the -/// plane based on the current [fillType]. -/// -/// The _current point_ is initially at the origin. After each -/// operation adding a segment to a subpath, the current point is -/// updated to the end of that segment. +/// Stores the verbs and points as they are given to us, with exceptions: +/// - we only record "Close" if it was immediately preceeded by Move | Line | Quad | Cubic +/// - we insert a Move(0,0) if Line | Quad | Cubic is our first command /// -/// Paths can be drawn on canvases using [Canvas.drawPath], and can -/// used to create clip regions using [Canvas.clipPath]. +/// The iterator does more cleanup, especially if forceClose == true +/// 1. If we encounter degenerate segments, remove them +/// 2. if we encounter Close, return a cons'd up Line() first (if the curr-pt != start-pt) +/// 3. if we encounter Move without a preceeding Close, and forceClose is true, goto #2 +/// 4. if we encounter Line | Quad | Cubic after Close, cons up a Move class SurfacePath implements ui.Path { - final List subpaths; - ui.PathFillType _fillType = ui.PathFillType.nonZero; - - Subpath get _currentSubpath => subpaths.last; - - List get _commands => _currentSubpath.commands; - - /// The current x-coordinate for this path. - double get _currentX => subpaths.isNotEmpty ? _currentSubpath.currentX : 0.0; + // Flag to require a moveTo if we begin with something else, + // for example empty path lineTo call will inject moveTo. + static const int kInitialLastMoveToIndexValue = -1; - /// The current y-coordinate for this path. - double get _currentY => subpaths.isNotEmpty ? _currentSubpath.currentY : 0.0; + PathRef pathRef; + ui.PathFillType _fillType = ui.PathFillType.nonZero; + // Skia supports inverse winding as part of path fill type. + // For Flutter inverse is always false. + bool _isInverseFillType = false; + // Store point index + 1 of last moveTo instruction. + // If contour has been closed or path is in initial state, the value is + // negated. + int fLastMoveToIndex = kInitialLastMoveToIndexValue; + int _convexityType = SPathConvexityType.kUnknown; + int _firstDirection = SPathDirection.kUnknown; + + SurfacePath() : pathRef = PathRef() { + _resetFields(); + } - /// Recorder used for hit testing paths. - static RawRecordingCanvas? _rawRecorder; + void _resetFields() { + fLastMoveToIndex = kInitialLastMoveToIndexValue; + _fillType = ui.PathFillType.nonZero; + _resetAfterEdit(); + } - SurfacePath() : subpaths = []; + void _resetAfterEdit() { + _convexityType = SPathConvexityType.kUnknown; + _firstDirection = SPathDirection.kUnknown; + } /// Creates a copy of another [Path]. - /// - /// This copy is fast and does not require additional memory unless either - /// the `source` path or the path returned by this constructor are modified. - SurfacePath.from(SurfacePath source) : subpaths = _deepCopy(source.subpaths); + SurfacePath.from(SurfacePath source) + : pathRef = PathRef()..copy(source.pathRef, 0, 0) { + _copyFields(source); + } + + /// Creates a shifted copy of another [Path]. + SurfacePath.shiftedFrom(SurfacePath source, double offsetX, double offsetY) + : pathRef = PathRef.shiftedFrom(source.pathRef, offsetX, offsetY) { + _copyFields(source); + } SurfacePath._shallowCopy(SurfacePath source) - : subpaths = List.from(source.subpaths); - - SurfacePath._clone(this.subpaths, this._fillType); - - static List _deepCopy(List source) { - // The last sub path can potentially still be mutated by calling ops. - // Copy all sub paths except the last active one which needs a deep copy. - final List paths = []; - int len = source.length; - if (len != 0) { - --len; - for (int i = 0; i < len; i++) { - paths.add(source[i]); - } - paths.add(source[len].shift(const ui.Offset(0, 0))); - } - return paths; + : pathRef = PathRef._shallowCopy(source.pathRef) { + _copyFields(source); + } + + void _copyFields(SurfacePath source) { + _fillType = source._fillType; + fLastMoveToIndex = source.fLastMoveToIndex; + _convexityType = source._convexityType; + _firstDirection = source._firstDirection; } /// Determines how the interior of this path is calculated. @@ -71,66 +80,156 @@ class SurfacePath implements ui.Path { /// Defaults to the non-zero winding rule, [PathFillType.nonZero]. @override ui.PathFillType get fillType => _fillType; + @override set fillType(ui.PathFillType value) { _fillType = value; } - /// Opens a new subpath with starting point (x, y). - void _openNewSubpath(double x, double y) { - subpaths.add(Subpath(x, y)); - _setCurrentPoint(x, y); + /// Returns true if [SurfacePath] contain equal verbs and equal weights. + bool isInterpolatable(SurfacePath compare) => + compare.pathRef.countVerbs() == pathRef.countVerbs() && + compare.pathRef.countPoints() == pathRef.countPoints() && + compare.pathRef.countWeights() == pathRef.countWeights(); + + bool interpolate(SurfacePath ending, double weight, SurfacePath out) { + int pointCount = pathRef.countPoints(); + if (pointCount != ending.pathRef.countPoints()) { + return false; + } + if (pointCount == 0) { + return true; + } + out.reset(); + out._addPath(this, 0, 0, null, SPathAddPathMode.kAppend); + PathRef.interpolate(ending.pathRef, weight, out.pathRef); + return true; + } + + /// Clears the [Path] object, returning it to the + /// same state it had when it was created. The _current point_ is + /// reset to the origin. + @override + void reset() { + if (pathRef.countVerbs() != 0) { + pathRef = PathRef(); + _resetFields(); + } } - /// Sets the current point to (x, y). - void _setCurrentPoint(double x, double y) { - _currentSubpath.currentX = x; - _currentSubpath.currentY = y; + /// Sets [SurfacePath] to its initial state, preserving internal storage. + /// Removes verb array, SkPoint array, and weights, and sets FillType to + /// kWinding. Internal storage associated with SkPath is retained. + /// + /// Use rewind() instead of reset() if SkPath storage will be reused and + /// performance is critical. + void rewind() { + pathRef.rewind(); + _resetFields(); + } + + /// Returns if contour is closed. + /// + /// Contour is closed if [SurfacePath] verb array was last modified by + /// close(). When stroked, closed contour draws join instead of cap at first + /// and last point. + bool get isLastContourClosed { + int verbCount = pathRef.countVerbs(); + return verbCount == 0 + ? false + : (pathRef.atVerb(verbCount - 1) == SPathVerb.kClose); + } + + /// Returns true for finite SkPoint array values between negative SK_ScalarMax + /// and positive SK_ScalarMax. Returns false for any SkPoint array value of + /// SK_ScalarInfinity, SK_ScalarNegativeInfinity, or SK_ScalarNaN. + bool get isFinite { + _debugValidate(); + return pathRef.isFinite; + } + + void _debugValidate() { + // TODO. + } + + /// Return true if path is a single line and returns points in out. + bool isLine(Float32List out) { + assert(out.length >= 4); + int verbCount = pathRef.countPoints(); + if (2 == verbCount && + pathRef.atVerb(0) == SPathVerb.kMove && + pathRef.atVerb(1) != SPathVerb.kLine) { + out[0] = pathRef.points[0]; + out[1] = pathRef.points[1]; + out[2] = pathRef.points[2]; + out[3] = pathRef.points[3]; + return true; + } + return false; } /// Starts a new subpath at the given coordinate. @override void moveTo(double x, double y) { - _openNewSubpath(x, y); - _commands.add(MoveTo(x, y)); + // remember our index + fLastMoveToIndex = pathRef.countPoints() + 1; + int pointIndex = pathRef.growForVerb(SPathVerb.kMove, 0); + pathRef.setPoint(pointIndex, x, y); + _resetAfterEdit(); } /// Starts a new subpath at the given offset from the current point. @override void relativeMoveTo(double dx, double dy) { - final double newX = _currentX + dx; - final double newY = _currentY + dy; - _openNewSubpath(newX, newY); - _commands.add(MoveTo(newX, newY)); + int pointCount = pathRef.countPoints(); + if (pointCount == 0) { + moveTo(dx, dy); + } else { + int pointIndex = (pointCount - 1) * 2; + final double lastPointX = pathRef.points[pointIndex++]; + final double lastPointY = pathRef.points[pointIndex]; + moveTo(lastPointX + dx, lastPointY + dy); + } + } + + void _injectMoveToIfNeeded() { + if (fLastMoveToIndex < 0) { + double x, y; + if (pathRef.countPoints() == 0) { + x = y = 0.0; + } else { + int pointIndex = 2 * (-fLastMoveToIndex - 1); + x = pathRef.points[pointIndex++]; + y = pathRef.points[pointIndex]; + } + moveTo(x, y); + } } /// Adds a straight line segment from the current point to the given /// point. @override void lineTo(double x, double y) { - if (subpaths.isEmpty) { - moveTo(0.0, 0.0); + if (fLastMoveToIndex < 0) { + _injectMoveToIfNeeded(); } - _commands.add(LineTo(x, y)); - _setCurrentPoint(x, y); + int pointIndex = pathRef.growForVerb(SPathVerb.kLine, 0); + pathRef.setPoint(pointIndex, x, y); + _resetAfterEdit(); } /// Adds a straight line segment from the current point to the point /// at the given offset from the current point. @override void relativeLineTo(double dx, double dy) { - final double newX = _currentX + dx; - final double newY = _currentY + dy; - if (subpaths.isEmpty) { - moveTo(0.0, 0.0); - } - _commands.add(LineTo(newX, newY)); - _setCurrentPoint(newX, newY); - } - - void _ensurePathStarted() { - if (subpaths.isEmpty) { - subpaths.add(Subpath(0.0, 0.0)); + int pointCount = pathRef.countPoints(); + if (pointCount == 0) { + lineTo(dx, dy); + } else { + int pointIndex = (pointCount - 1) * 2; + final double lastPointX = pathRef.points[pointIndex++]; + final double lastPointY = pathRef.points[pointIndex]; + lineTo(lastPointX + dx, lastPointY + dy); } } @@ -139,9 +238,8 @@ class SurfacePath implements ui.Path { /// (x1,y1). @override void quadraticBezierTo(double x1, double y1, double x2, double y2) { - _ensurePathStarted(); - _commands.add(QuadraticCurveTo(x1, y1, x2, y2)); - _setCurrentPoint(x2, y2); + _injectMoveToIfNeeded(); + _quadTo(x1, y1, x2, y2); } /// Adds a quadratic bezier segment that curves from the current @@ -150,10 +248,57 @@ class SurfacePath implements ui.Path { /// point. @override void relativeQuadraticBezierTo(double x1, double y1, double x2, double y2) { - _ensurePathStarted(); - _commands.add(QuadraticCurveTo( - x1 + _currentX, y1 + _currentY, x2 + _currentX, y2 + _currentY)); - _setCurrentPoint(x2 + _currentX, y2 + _currentY); + int pointCount = pathRef.countPoints(); + if (pointCount == 0) { + quadraticBezierTo(x1, y1, x2, y2); + } else { + int pointIndex = (pointCount - 1) * 2; + final double lastPointX = pathRef.points[pointIndex++]; + final double lastPointY = pathRef.points[pointIndex]; + quadraticBezierTo( + x1 + lastPointX, y1 + lastPointY, x2 + lastPointX, y2 + lastPointY); + } + } + + void _quadTo(double x1, double y1, double x2, double y2) { + int pointIndex = pathRef.growForVerb(SPathVerb.kQuad, 0); + pathRef.setPoint(pointIndex, x1, y1); + pathRef.setPoint(pointIndex + 1, x2, y2); + _resetAfterEdit(); + } + + /// Adds a bezier segment that curves from the current point to the + /// given point (x2,y2), using the control points (x1,y1) and the + /// weight w. If the weight is greater than 1, then the curve is a + /// hyperbola; if the weight equals 1, it's a parabola; and if it is + /// less than 1, it is an ellipse. + @override + void conicTo(double x1, double y1, double x2, double y2, double w) { + _injectMoveToIfNeeded(); + int pointIndex = pathRef.growForVerb(SPathVerb.kConic, w); + pathRef.setPoint(pointIndex, x1, y1); + pathRef.setPoint(pointIndex + 1, x2, y2); + _resetAfterEdit(); + } + + /// Adds a bezier segment that curves from the current point to the + /// point at the offset (x2,y2) from the current point, using the + /// control point at the offset (x1,y1) from the current point and + /// the weight w. If the weight is greater than 1, then the curve is + /// a hyperbola; if the weight equals 1, it's a parabola; and if it + /// is less than 1, it is an ellipse. + @override + void relativeConicTo(double x1, double y1, double x2, double y2, double w) { + int pointCount = pathRef.countPoints(); + if (pointCount == 0) { + conicTo(x1, y1, x2, y2, w); + } else { + int pointIndex = (pointCount - 1) * 2; + final double lastPointX = pathRef.points[pointIndex++]; + final double lastPointY = pathRef.points[pointIndex]; + conicTo(lastPointX + x1, lastPointY + y1, lastPointX + x2, + lastPointY + y2, w); + } } /// Adds a cubic bezier segment that curves from the current point @@ -162,9 +307,12 @@ class SurfacePath implements ui.Path { @override void cubicTo( double x1, double y1, double x2, double y2, double x3, double y3) { - _ensurePathStarted(); - _commands.add(BezierCurveTo(x1, y1, x2, y2, x3, y3)); - _setCurrentPoint(x3, y3); + _injectMoveToIfNeeded(); + int pointIndex = pathRef.growForVerb(SPathVerb.kCubic, 0); + pathRef.setPoint(pointIndex, x1, y1); + pathRef.setPoint(pointIndex + 1, x2, y2); + pathRef.setPoint(pointIndex + 2, x3, y3); + _resetAfterEdit(); } /// Adds a cubic bezier segment that curves from the current point @@ -174,37 +322,84 @@ class SurfacePath implements ui.Path { @override void relativeCubicTo( double x1, double y1, double x2, double y2, double x3, double y3) { - _ensurePathStarted(); - _commands.add(BezierCurveTo(x1 + _currentX, y1 + _currentY, x2 + _currentX, - y2 + _currentY, x3 + _currentX, y3 + _currentY)); - _setCurrentPoint(x3 + _currentX, y3 + _currentY); + int pointCount = pathRef.countPoints(); + if (pointCount == 0) { + cubicTo(x1, y1, x2, y2, x3, y3); + } else { + int pointIndex = (pointCount - 1) * 2; + final double lastPointX = pathRef.points[pointIndex++]; + final double lastPointY = pathRef.points[pointIndex]; + cubicTo(x1 + lastPointX, y1 + lastPointY, x2 + lastPointX, + y2 + lastPointY, x3 + lastPointX, y3 + lastPointY); + } } - /// Adds a bezier segment that curves from the current point to the - /// given point (x2,y2), using the control points (x1,y1) and the - /// weight w. If the weight is greater than 1, then the curve is a - /// hyperbola; if the weight equals 1, it's a parabola; and if it is - /// less than 1, it is an ellipse. + /// Closes the last subpath, as if a straight line had been drawn + /// from the current point to the first point of the subpath. @override - void conicTo(double x1, double y1, double x2, double y2, double w) { - final List quads = - Conic(_currentX, _currentY, x1, y1, x2, y2, w).toQuads(); - final int len = quads.length; - for (int i = 1; i < len; i += 2) { - quadraticBezierTo( - quads[i].dx, quads[i].dy, quads[i + 1].dx, quads[i + 1].dy); + void close() { + _debugValidate(); + // Don't add verb if it is the first instruction or close as already + // been added. + final int verbCount = pathRef.countVerbs(); + if (verbCount != 0 && pathRef.atVerb(verbCount - 1) != SPathVerb.kClose) { + pathRef.growForVerb(SPathVerb.kClose, 0); } + if (fLastMoveToIndex >= 0) { + // Signal that we need a moveTo to follow next if not specified. + fLastMoveToIndex = -fLastMoveToIndex; + } + _resetAfterEdit(); } - /// Adds a bezier segment that curves from the current point to the - /// point at the offset (x2,y2) from the current point, using the - /// control point at the offset (x1,y1) from the current point and - /// the weight w. If the weight is greater than 1, then the curve is - /// a hyperbola; if the weight equals 1, it's a parabola; and if it - /// is less than 1, it is an ellipse. + /// Adds a new subpath that consists of four lines that outline the + /// given rectangle. @override - void relativeConicTo(double x1, double y1, double x2, double y2, double w) { - conicTo(_currentX + x1, _currentY + y1, _currentX + x2, _currentY + y2, w); + void addRect(ui.Rect rect) { + addRectWithDirection(rect, SPathDirection.kCW, 0); + } + + bool _hasOnlyMoveTos() { + final int verbCount = pathRef.countVerbs(); + for (int i = 0; i < verbCount; i++) { + switch (pathRef.atVerb(i)) { + case SPathVerb.kLine: + case SPathVerb.kQuad: + case SPathVerb.kConic: + case SPathVerb.kCubic: + return false; + } + } + return true; + } + + void addRectWithDirection(ui.Rect rect, int direction, int startIndex) { + assert(direction != SPathDirection.kUnknown); + bool isRect = _hasOnlyMoveTos(); + // SkAutoDisableDirectionCheck. + int finalDirection = + _hasOnlyMoveTos() ? direction : SPathDirection.kUnknown; + int pointIndex0 = pathRef.growForVerb(SPathVerb.kMove, 0); + int pointIndex1 = pathRef.growForVerb(SPathVerb.kLine, 0); + int pointIndex2 = pathRef.growForVerb(SPathVerb.kLine, 0); + int pointIndex3 = pathRef.growForVerb(SPathVerb.kLine, 0); + pathRef.growForVerb(SPathVerb.kClose, 0); + if (direction == SPathDirection.kCW) { + pathRef.setPoint(pointIndex0, rect.left, rect.top); + pathRef.setPoint(pointIndex1, rect.right, rect.top); + pathRef.setPoint(pointIndex2, rect.right, rect.bottom); + pathRef.setPoint(pointIndex3, rect.left, rect.bottom); + } else { + pathRef.setPoint(pointIndex3, rect.left, rect.bottom); + pathRef.setPoint(pointIndex2, rect.right, rect.bottom); + pathRef.setPoint(pointIndex1, rect.right, rect.top); + pathRef.setPoint(pointIndex0, rect.left, rect.top); + } + pathRef.setIsRect(isRect, direction == SPathDirection.kCCW, 0); + _resetAfterEdit(); + // SkAutoDisableDirectionCheck. + _firstDirection = finalDirection; + // TODO: optimize by setting pathRef bounds if bounds are already computed. } /// If the `forceMoveTo` argument is false, adds a straight line @@ -227,21 +422,220 @@ class SurfacePath implements ui.Path { void arcTo( ui.Rect rect, double startAngle, double sweepAngle, bool forceMoveTo) { assert(rectIsValid(rect)); - final ui.Offset center = rect.center; - final double radiusX = rect.width / 2; - final double radiusY = rect.height / 2; - final double startX = radiusX * math.cos(startAngle) + center.dx; - final double startY = radiusY * math.sin(startAngle) + center.dy; + // If width or height is 0, we still stroke a line, only abort if both + // are empty. + if (rect.width == 0 && rect.height == 0) { + return; + } + if (pathRef.countPoints() == 0) { + forceMoveTo = true; + } + final ui.Offset? lonePoint = _arcIsLonePoint(rect, startAngle, sweepAngle); + if (lonePoint != null) { + if (forceMoveTo) { + moveTo(lonePoint.dx, lonePoint.dy); + } else { + lineTo(lonePoint.dx, lonePoint.dy); + } + } + // Convert angles to unit vectors. + double stopAngle = startAngle + sweepAngle; + final double cosStart = math.cos(startAngle); + final double sinStart = math.sin(startAngle); + double cosStop = math.cos(stopAngle); + double sinStop = math.sin(stopAngle); + + // If the sweep angle is nearly (but less than) 360, then due to precision + // loss in radians-conversion and/or sin/cos, we may end up with coincident + // vectors, which will fool quad arc build into doing nothing (bad) instead + // of drawing a nearly complete circle (good). + // e.g. canvas.drawArc(0, 359.99, ...) + // -vs- canvas.drawArc(0, 359.9, ...) + // Detect this edge case, and tweak the stop vector. + if (_nearlyEqual(cosStart, cosStop) && _nearlyEqual(sinStart, sinStop)) { + final double sweep = sweepAngle.abs() * 180.0 / math.pi; + if (sweep <= 360 && sweep > 359) { + // Use tiny angle (in radians) to tweak. + double deltaRad = sweepAngle < 0 ? -1.0 / 512.0 : 1.0 / 512.0; + do { + stopAngle -= deltaRad; + cosStop = math.cos(stopAngle); + sinStop = math.sin(stopAngle); + } while (cosStart == cosStop && sinStart == sinStop); + } + } + final int dir = sweepAngle > 0 ? SPathDirection.kCW : SPathDirection.kCCW; + final double endAngle = startAngle + sweepAngle; + final double radiusX = rect.width / 2.0; + final double radiusY = rect.height / 2.0; + final double px = rect.center.dx + (radiusX * math.cos(endAngle)); + final double py = rect.center.dy + (radiusY * math.sin(endAngle)); + // At this point, we know that the arc is not a lone point, but + // startV == stopV indicates that the sweepAngle is too small such that + // angles_to_unit_vectors cannot handle it + if (cosStart == cosStop && sinStart == sinStop) { + // Add moveTo to start point if forceMoveTo is true. Otherwise a lineTo + // unless we're sufficiently close to start point currently. This prevents + // spurious lineTos when adding a series of contiguous arcs from the same + // oval. + if (forceMoveTo) { + moveTo(px, py); + } else { + _lineToIfNotTooCloseToLastPoint(px, py); + } + // We are done with tiny sweep approximated by line. + return; + } + + // Convert arc defined by start/end unit vectors to conics (max 5). + + // Dot product + final double x = (cosStart * cosStop) + (sinStart * sinStop); + // Cross product + double y = (cosStart * sinStop) - (sinStart * cosStop); + final double absY = y.abs(); + // Check for coincident vectors (angle is nearly 0 or 180). + // The cross product for angles 0 and 180 will be zero, we use the + // dot product sign to distinguish between the two. + if (absY <= SPath.scalarNearlyZero && + x > 0 && + ((y >= 0 && dir == SPathDirection.kCW) || + (y <= 0 && dir == SPathDirection.kCCW))) { + // No conics, just use single line to connect point. + if (forceMoveTo) { + moveTo(px, py); + } else { + _lineToIfNotTooCloseToLastPoint(px, py); + } + return; + } + + // Normalize to clockwise + if (dir == SPathDirection.kCCW) { + y = -y; + } + + // Use 1 conic per quadrant of a circle. + // 0..90 -> quadrant 0 + // 90..180 -> quadrant 1 + // 180..270 -> quadrant 2 + // 270..360 -> quadrant 3 + + const List quadPoints = [ + ui.Offset(1, 0), + ui.Offset(1, 1), + ui.Offset(0, 1), + ui.Offset(-1, 1), + ui.Offset(-1, 0), + ui.Offset(-1, -1), + ui.Offset(0, -1), + ui.Offset(1, -1), + ]; + + int quadrant = 0; + if (0 == y) { + // 180 degrees between vectors. + quadrant = 2; + assert((x + 1).abs() <= SPath.scalarNearlyZero); + } else if (0 == x) { + // Dot product 0 means 90 degrees between vectors. + assert((absY - 1) <= SPath.scalarNearlyZero); + quadrant = y > 0 ? 1 : 3; // 90 or 270 + } else { + if (y < 0) { + quadrant += 2; + } + if ((x < 0) != (y < 0)) { + quadrant += 1; + } + } + + List conics = []; + + const double quadrantWeight = SPath.scalarRoot2Over2; + int conicCount = quadrant; + for (int i = 0; i < conicCount; i++) { + int quadPointIndex = i * 2; + final ui.Offset p0 = quadPoints[quadPointIndex]; + final ui.Offset p1 = quadPoints[quadPointIndex + 1]; + final ui.Offset p2 = quadPoints[quadPointIndex + 2]; + conics + .add(Conic(p0.dx, p0.dy, p1.dx, p1.dy, p2.dx, p2.dy, quadrantWeight)); + } + + // Now compute any remaining ( < 90degree ) arc for last conic. + final double finalPx = x; + final double finalPy = y; + ui.Offset lastQuadrantPoint = quadPoints[quadrant * 2]; + // Dot product between last quadrant vector and last point on arc. + final double dot = (x * lastQuadrantPoint.dx) + (y * lastQuadrantPoint.dy); + if (dot < 1) { + // Compute the bisector vector and then rescale to be the off curve point. + // Length is cos(theta/2) using half angle identity we get + // length = sqrt(2 / (1 + cos(theta)). We already have cos from computing + // dot. Computed weight is cos(theta/2). + double offCurveX = lastQuadrantPoint.dx + x; + double offCurveY = lastQuadrantPoint.dy + y; + final double cosThetaOver2 = math.sqrt((1.0 + dot) / 2.0); + double unscaledLength = + math.sqrt((offCurveX * offCurveX) + (offCurveY * offCurveY)); + assert(unscaledLength > SPath.scalarNearlyZero); + offCurveX /= cosThetaOver2 * unscaledLength; + offCurveY /= cosThetaOver2 * unscaledLength; + if (!_nearlyEqual(offCurveX, lastQuadrantPoint.dx) || + !_nearlyEqual(offCurveY, lastQuadrantPoint.dy)) { + conics.add(Conic(lastQuadrantPoint.dx, lastQuadrantPoint.dy, offCurveX, + offCurveY, finalPx, finalPy, cosThetaOver2)); + ++conicCount; + } + } + // Any points we generate based on unit vectors cos/sinStart , cos/sinStop + // we rotate to start vector, scale by rect.width/2 rect.height/2 and + // then translate to center point. + final double scaleX = rect.width / 2; + final double scaleY = + dir == SPathDirection.kCCW ? -rect.height / 2 : rect.height / 2; + final double centerX = rect.center.dx; + final double centerY = rect.center.dy; + for (Conic conic in conics) { + double x = conic.p0x; + double y = conic.p0y; + conic.p0x = (cosStart * x - sinStart * y) * scaleX + centerX; + conic.p0y = (cosStart * y + sinStart * x) * scaleY + centerY; + x = conic.p1x; + y = conic.p1y; + conic.p1x = (cosStart * x - sinStart * y) * scaleX + centerX; + conic.p1y = (cosStart * y + sinStart * x) * scaleY + centerY; + x = conic.p2x; + y = conic.p2y; + conic.p2x = (cosStart * x - sinStart * y) * scaleX + centerX; + conic.p2y = (cosStart * y + sinStart * x) * scaleY + centerY; + } + // Now output points. + final double firstConicPx = conics[0].p0x; + final double firstConicPy = conics[0].p0y; if (forceMoveTo) { - _openNewSubpath(startX, startY); + moveTo(firstConicPx, firstConicPy); } else { - lineTo(startX, startY); + _lineToIfNotTooCloseToLastPoint(firstConicPx, firstConicPy); + } + for (int i = 0; i < conicCount; i++) { + Conic conic = conics[i]; + conicTo(conic.p1x, conic.p1y, conic.p2x, conic.p2y, conic.fW); } - _commands.add(Ellipse(center.dx, center.dy, radiusX, radiusY, 0.0, - startAngle, startAngle + sweepAngle, sweepAngle.isNegative)); + _resetAfterEdit(); + } - _setCurrentPoint(radiusX * math.cos(startAngle + sweepAngle) + center.dx, - radiusY * math.sin(startAngle + sweepAngle) + center.dy); + void _lineToIfNotTooCloseToLastPoint(double px, double py) { + final int pointCount = pathRef.countPoints(); + if (pointCount != 0) { + final ui.Offset lastPoint = pathRef.atPoint(pointCount - 1); + final double lastPointX = lastPoint.dx; + final double lastPointY = lastPoint.dy; + if (!_nearlyEqual(px, lastPointX) || !_nearlyEqual(py, lastPointY)) { + lineTo(px, py); + } + } } /// Appends up to four conic curves weighted to describe an oval of `radius` @@ -269,37 +663,46 @@ class SurfacePath implements ui.Path { }) { assert(offsetIsValid(arcEnd)); assert(radiusIsValid(radius)); - // _currentX, _currentY are the coordinates of start point on path, - // arcEnd is final point of arc. - // rx,ry are the radii of the eclipse (semi-major/semi-minor axis) - // largeArc is false if arc is spanning less than or equal to 180 degrees. - // clockwise is false if arc sweeps through decreasing angles or true - // if sweeping through increasing angles. - // rotation is the angle from the x-axis of the current coordinate - // system to the x-axis of the eclipse. + _injectMoveToIfNeeded(); + final int pointCount = pathRef.countPoints(); + double lastPointX, lastPointY; + if (pointCount == 0) { + lastPointX = lastPointY = 0; + } else { + int pointIndex = (pointCount - 1) * 2; + lastPointX = pathRef.points[pointIndex++]; + lastPointY = pathRef.points[pointIndex]; + } + // lastPointX, lastPointY are the coordinates of start point on path, + // x,y is final point of arc. + double x = arcEnd.dx; + double y = arcEnd.dy; + + // rx,ry are the radii of the eclipse (semi-major/semi-minor axis) double rx = radius.x.abs(); double ry = radius.y.abs(); // If the current point and target point for the arc are identical, it // should be treated as a zero length path. This ensures continuity in // animations. - final bool isSamePoint = _currentX == arcEnd.dx && _currentY == arcEnd.dy; + final bool isSamePoint = lastPointX == x && lastPointY == y; // If rx = 0 or ry = 0 then this arc is treated as a straight line segment // (a "lineto") joining the endpoints. // http://www.w3.org/TR/SVG/implnote.html#ArcOutOfRangeParameters if (isSamePoint || rx.toInt() == 0 || ry.toInt() == 0) { - _commands.add(LineTo(arcEnd.dx, arcEnd.dy)); - _setCurrentPoint(arcEnd.dx, arcEnd.dy); - return; + if (rx == 0 || ry == 0) { + lineTo(x, y); + return; + } } // As an intermediate point to finding center parametrization, place the // origin on the midpoint between start/end points and rotate to align // coordinate axis with axes of the ellipse. - final double midPointX = (_currentX - arcEnd.dx) / 2.0; - final double midPointY = (_currentY - arcEnd.dy) / 2.0; + final double midPointX = (lastPointX - x) / 2.0; + final double midPointY = (lastPointY - y) / 2.0; // Convert rotation or radians. final double xAxisRotation = math.pi * rotation / 180.0; @@ -308,7 +711,7 @@ class SurfacePath implements ui.Path { final double cosXAxisRotation = math.cos(xAxisRotation); final double sinXAxisRotation = math.sin(xAxisRotation); - // Calculate rotate midpoint as x/yPrime. + // Calculate rotated midpoint. final double xPrime = (cosXAxisRotation * midPointX) + (sinXAxisRotation * midPointY); final double yPrime = @@ -330,46 +733,100 @@ class SurfacePath implements ui.Path { rySquare = ry * ry; } - // Compute transformed center. eq. 5.2 - final double distanceSquare = - (rxSquare * yPrimeSquare) + rySquare * xPrimeSquare; - final double cNumerator = (rxSquare * rySquare) - distanceSquare; - double scaleFactor = math.sqrt(math.max(cNumerator / distanceSquare, 0.0)); + // Switch to unit vectors + double unitPts0x = + (lastPointX * cosXAxisRotation + lastPointY * sinXAxisRotation) / rx; + double unitPts0y = + (lastPointY * cosXAxisRotation - lastPointX * sinXAxisRotation) / ry; + double unitPts1x = (x * cosXAxisRotation + y * sinXAxisRotation) / rx; + double unitPts1y = (y * cosXAxisRotation - x * sinXAxisRotation) / ry; + double deltaX = unitPts1x - unitPts0x; + double deltaY = unitPts1y - unitPts0y; + + final double d = deltaX * deltaX + deltaY * deltaY; + double scaleFactor = math.sqrt(math.max(1 / d - 0.25, 0.0)); + // largeArc is false if arc is spanning less than or equal to 180 degrees. + // clockwise is false if arc sweeps through decreasing angles or true + // if sweeping through increasing angles. + // rotation is the angle from the x-axis of the current coordinate + // system to the x-axis of the eclipse. if (largeArc == clockwise) { scaleFactor = -scaleFactor; } - // Ready to compute transformed center. - final double cxPrime = scaleFactor * ((rx * yPrime) / ry); - final double cyPrime = scaleFactor * (-(ry * xPrime) / rx); - - // Rotate to find actual center. - final double cx = cosXAxisRotation * cxPrime - - sinXAxisRotation * cyPrime + - ((_currentX + arcEnd.dx) / 2.0); - final double cy = sinXAxisRotation * cxPrime + - cosXAxisRotation * cyPrime + - ((_currentY + arcEnd.dy) / 2.0); + deltaX *= scaleFactor; + deltaY *= scaleFactor; + // Compute transformed center. eq. 5.2 + double centerPointX = (unitPts0x + unitPts1x) / 2 - deltaY; + double centerPointY = (unitPts0y + unitPts1y) / 2 + deltaX; + unitPts0x -= centerPointX; + unitPts0y -= centerPointY; + unitPts1x -= centerPointX; + unitPts1y -= centerPointY; // Calculate start angle and sweep. - // Start vector is from midpoint of start/end points to transformed center. - final double startVectorX = (xPrime - cxPrime) / rx; - final double startVectorY = (yPrime - cyPrime) / ry; - - final double startAngle = math.atan2(startVectorY, startVectorX); - final double endVectorX = (-xPrime - cxPrime) / rx; - final double endVectorY = (-yPrime - cyPrime) / ry; - double sweepAngle = math.atan2(endVectorY, endVectorX) - startAngle; - - if (clockwise && sweepAngle < 0) { - sweepAngle += math.pi * 2.0; - } else if (!clockwise && sweepAngle > 0) { - sweepAngle -= math.pi * 2.0; + final double theta1 = math.atan2(unitPts0y, unitPts0x); + final double theta2 = math.atan2(unitPts1y, unitPts1x); + double thetaArc = theta2 - theta1; + if (clockwise && thetaArc < 0) { + thetaArc += math.pi * 2.0; + } else if (!clockwise && thetaArc > 0) { + thetaArc -= math.pi * 2.0; + } + // Guard against tiny angles. See skbug.com/9272. + if (thetaArc.abs() < (math.pi / (1000.0 * 1000.0))) { + lineTo(x, y); + return; } - _commands.add(Ellipse(cx, cy, rx, ry, xAxisRotation, startAngle, - startAngle + sweepAngle, sweepAngle.isNegative)); + // The arc may be slightly bigger than 1/4 circle, so allow up to 1/3rd. + final int segments = + (thetaArc / (2.0 * math.pi / 3.0)).abs().ceil().toInt(); + final double thetaWidth = thetaArc / segments; + final double t = math.tan(thetaWidth / 2.0); + if (!t.isFinite) { + return; + } - _setCurrentPoint(arcEnd.dx, arcEnd.dy); + final double w = math.sqrt(0.5 + math.cos(thetaWidth) * 0.5); + double startTheta = theta1; + + // Computing the arc width introduces rounding errors that cause arcs + // to start outside their marks. A round rect may lose convexity as a + // result. If the input values are on integers, place the conic on + // integers as well. + bool expectIntegers = _nearlyEqual((math.pi / 2 - thetaWidth.abs()), 0) && + _isInteger(rx) && + _isInteger(ry) && + _isInteger(x) && + _isInteger(y); + + for (int i = 0; i < segments; i++) { + final double endTheta = startTheta + thetaWidth; + final double sinEndTheta = _snapToZero(math.sin(endTheta)); + final double cosEndTheta = _snapToZero(math.cos(endTheta)); + double unitPts1x = cosEndTheta + centerPointX; + double unitPts1y = sinEndTheta + centerPointY; + double unitPts0x = unitPts1x + t * sinEndTheta; + double unitPts0y = unitPts1y - t * cosEndTheta; + unitPts0x = unitPts0x * rx; + unitPts0y = unitPts0y * ry; + unitPts1x = unitPts1x * rx; + unitPts1y = unitPts1y * ry; + double xStart = + unitPts0x * cosXAxisRotation - unitPts0y * sinXAxisRotation; + double yStart = + unitPts0y * cosXAxisRotation + unitPts0x * sinXAxisRotation; + double xEnd = unitPts1x * cosXAxisRotation - unitPts1y * sinXAxisRotation; + double yEnd = unitPts1y * cosXAxisRotation + unitPts1x * sinXAxisRotation; + if (expectIntegers) { + xStart = (xStart + 0.5).floorToDouble(); + yStart = (yStart + 0.5).floorToDouble(); + xEnd = (xEnd + 0.5).floorToDouble(); + yEnd = (yEnd + 0.5).floorToDouble(); + } + conicTo(xStart, yStart, xEnd, yEnd, w); + startTheta = endTheta; + } } /// Appends up to four conic curves weighted to describe an oval of `radius` @@ -396,23 +853,24 @@ class SurfacePath implements ui.Path { }) { assert(offsetIsValid(arcEndDelta)); assert(radiusIsValid(radius)); + + int pointCount = pathRef.countPoints(); + double lastPointX, lastPointY; + if (pointCount == 0) { + lastPointX = lastPointY = 0; + } else { + int pointIndex = (pointCount - 1) * 2; + lastPointX = pathRef.points[pointIndex++]; + lastPointY = pathRef.points[pointIndex]; + } arcToPoint( - ui.Offset(_currentX + arcEndDelta.dx, _currentY + arcEndDelta.dy), + ui.Offset(lastPointX + arcEndDelta.dx, lastPointY + arcEndDelta.dy), radius: radius, rotation: rotation, largeArc: largeArc, clockwise: clockwise); } - /// Adds a new subpath that consists of four lines that outline the - /// given rectangle. - @override - void addRect(ui.Rect rect) { - assert(rectIsValid(rect)); - _openNewSubpath(rect.left, rect.top); - _commands.add(RectCommand(rect.left, rect.top, rect.width, rect.height)); - } - /// Adds a new subpath that consists of a curve that forms the /// ellipse that fills the given rectangle. /// @@ -421,38 +879,80 @@ class SurfacePath implements ui.Path { /// [Offset] and radius. @override void addOval(ui.Rect oval) { + _addOval(oval, SPathDirection.kCW, 0); + } + + void _addOval(ui.Rect oval, int direction, int startIndex) { assert(rectIsValid(oval)); - final ui.Offset center = oval.center; - final double radiusX = oval.width / 2; - final double radiusY = oval.height / 2; - - /// At startAngle = 0, the path will begin at center + cos(0) * radius. - _openNewSubpath(center.dx + radiusX, center.dy); - _commands.add(Ellipse( - center.dx, center.dy, radiusX, radiusY, 0.0, 0.0, 2 * math.pi, false)); + assert(direction != SPathDirection.kUnknown); + bool isOval = _hasOnlyMoveTos(); + + final double weight = SPath.scalarRoot2Over2; + final double left = oval.left; + final double right = oval.right; + final double centerX = (left + right) / 2.0; + final double top = oval.top; + final double bottom = oval.bottom; + final double centerY = (top + bottom) / 2.0; + if (direction == SPathDirection.kCW) { + moveTo(right, centerY); + conicTo(right, bottom, centerX, bottom, weight); + conicTo(left, bottom, left, centerY, weight); + conicTo(left, top, centerX, top, weight); + conicTo(right, top, right, centerY, weight); + } else { + moveTo(right, centerY); + conicTo(right, top, centerX, top, weight); + conicTo(left, top, left, centerY, weight); + conicTo(left, bottom, centerX, bottom, weight); + conicTo(right, bottom, right, centerY, weight); + } + close(); + pathRef.setIsOval(isOval, direction == SPathDirection.kCCW, 0); + _resetAfterEdit(); + // AutoDisableDirectionCheck + if (isOval) { + _firstDirection = direction; + } else { + _firstDirection = SPathDirection.kUnknown; + } } - /// Adds a new subpath with one arc segment that consists of the arc - /// that follows the edge of the oval bounded by the given - /// rectangle, from startAngle radians around the oval up to - /// startAngle + sweepAngle radians around the oval, with zero - /// radians being the point on the right hand side of the oval that - /// crosses the horizontal line that intersects the center of the - /// rectangle and with positive angles going clockwise around the - /// oval. + /// Appends arc to path, as the start of new contour. Arc added is part of + /// ellipse bounded by oval, from startAngle through sweepAngle. Both + /// startAngle and sweepAngle are measured in degrees, + /// where zero degrees is aligned with the positive x-axis, + /// and positive sweeps extends arc clockwise. + /// + /// If sweepAngle <= -360, or sweepAngle >= 360; and startAngle modulo 90 + /// is nearly zero, append oval instead of arc. Otherwise, sweepAngle values + /// are treated modulo 360, and arc may or may not draw depending on numeric + /// rounding. @override void addArc(ui.Rect oval, double startAngle, double sweepAngle) { assert(rectIsValid(oval)); - final ui.Offset center = oval.center; - final double radiusX = oval.width / 2; - final double radiusY = oval.height / 2; - _openNewSubpath(radiusX * math.cos(startAngle) + center.dx, - radiusY * math.sin(startAngle) + center.dy); - _commands.add(Ellipse(center.dx, center.dy, radiusX, radiusY, 0.0, - startAngle, startAngle + sweepAngle, sweepAngle.isNegative)); - - _setCurrentPoint(radiusX * math.cos(startAngle + sweepAngle) + center.dx, - radiusY * math.sin(startAngle + sweepAngle) + center.dy); + if (0 == sweepAngle) { + return; + } + const double kFullCircleAngle = math.pi * 2; + + if (sweepAngle >= kFullCircleAngle || sweepAngle <= -kFullCircleAngle) { + // We can treat the arc as an oval if it begins at one of our legal starting positions. + final double startOver90 = startAngle / (math.pi / 2.0); + final double startOver90I = (startOver90 + 0.5).floorToDouble(); + final double error = startOver90 - startOver90I; + if (_nearlyEqual(error, 0)) { + // Index 1 is at startAngle == 0. + double startIndex = startOver90I + 1.0 % 4.0; + startIndex = startIndex < 0 ? startIndex + 4.0 : startIndex; + _addOval( + oval, + sweepAngle > 0 ? SPathDirection.kCW : SPathDirection.kCCW, + startIndex.toInt()); + return; + } + } + arcTo(oval, startAngle, sweepAngle, true); } /// Adds a new subpath with a sequence of line segments that connect the given @@ -464,21 +964,21 @@ class SurfacePath implements ui.Path { /// The `points` argument is interpreted as offsets from the origin. @override void addPolygon(List points, bool close) { - assert(points != null); // ignore: unnecessary_null_comparison - if (points.isEmpty) { + final int pointCount = points.length; + if (pointCount <= 0) { return; } - - moveTo(points.first.dx, points.first.dy); - for (int i = 1; i < points.length; i++) { - final ui.Offset point = points[i]; - lineTo(point.dx, point.dy); + int pointIndex = pathRef.growForVerb(SPathVerb.kMove, 0); + pathRef.setPoint(pointIndex, points[0].dx, points[0].dy); + pathRef.growForRepeatedVerb(SPathVerb.kLine, pointCount - 1); + for (int i = 1; i < pointCount; i++) { + pathRef.setPoint(pointIndex + i, points[i].dx, points[i].dy); } if (close) { this.close(); - } else { - _setCurrentPoint(points.last.dx, points.last.dy); } + _resetAfterEdit(); + _debugValidate(); } /// Adds a new subpath that consists of the straight lines and @@ -486,14 +986,63 @@ class SurfacePath implements ui.Path { /// argument. @override void addRRect(ui.RRect rrect) { - assert(rrectIsValid(rrect)); + _addRRect(rrect, SPathDirection.kCW, 6); + } - // Set the current point to the top left corner of the rectangle (the - // point on the top of the rectangle farthest to the left that isn't in - // the rounded corner). - // TODO(het): Is this the current point in Flutter? - _openNewSubpath(rrect.tallMiddleRect.left, rrect.top); - _commands.add(RRectCommand(rrect)); + void _addRRect(ui.RRect rrect, int direction, int startIndex) { + assert(rrectIsValid(rrect)); + assert(direction != SPathDirection.kUnknown); + + bool isRRect = _hasOnlyMoveTos(); + ui.Rect bounds = rrect.outerRect; + if (rrect.isRect || rrect.isEmpty) { + // degenerate(rect) => radii points are collapsing. + addRectWithDirection(bounds, direction, (startIndex + 1) ~/ 2); + } else if (_isRRectOval(rrect)) { + // degenerate(oval) => line points are collapsing. + _addOval(bounds, direction, startIndex ~/ 2); + } else { + final double weight = SPath.scalarRoot2Over2; + double left = bounds.left; + double right = bounds.right; + double top = bounds.top; + double bottom = bounds.bottom; + final double width = right - left; + final double height = bottom - top; + // Proportionally scale down all radii to fit. Find the minimum ratio + // of a side and the radii on that side (for all four sides) and use + // that to scale down _all_ the radii. This algorithm is from the + // W3 spec (http://www.w3.org/TR/css3-background/) section 5.5 + final double tlRadiusX = math.max(0, rrect.tlRadiusX); + final double trRadiusX = math.max(0, rrect.trRadiusX); + final double blRadiusX = math.max(0, rrect.blRadiusX); + final double brRadiusX = math.max(0, rrect.brRadiusX); + final double tlRadiusY = math.max(0, rrect.tlRadiusY); + final double trRadiusY = math.max(0, rrect.trRadiusY); + final double blRadiusY = math.max(0, rrect.blRadiusY); + final double brRadiusY = math.max(0, rrect.brRadiusY); + double scale = _computeMinScale(tlRadiusX, trRadiusX, width, 1.0); + scale = _computeMinScale(blRadiusX, brRadiusX, width, scale); + scale = _computeMinScale(tlRadiusY, trRadiusY, height, scale); + scale = _computeMinScale(blRadiusY, brRadiusY, height, scale); + + // Inlined version of: + moveTo(left, bottom - scale * blRadiusY); + lineTo(left, top + scale * tlRadiusY); + conicTo(left, top, left + scale * tlRadiusX, top, weight); + lineTo(right - scale * trRadiusX, top); + conicTo(right, top, right, top + scale * trRadiusY, weight); + lineTo(right, bottom - scale * brRadiusY); + conicTo(right, bottom, right - scale * brRadiusX, bottom, weight); + lineTo(left + scale * blRadiusX, bottom); + conicTo(left, bottom, left, bottom - scale * blRadiusY, weight); + close(); + // SkAutoDisableDirectionCheck. + _firstDirection = isRRect ? direction : SPathDirection.kUnknown; + pathRef.setIsRRect( + isRRect, direction == SPathDirection.kCCW, startIndex % 8, rrect); + } + _debugValidate(); } /// Adds a new subpath that consists of the given `path` offset by the given @@ -504,33 +1053,112 @@ class SurfacePath implements ui.Path { /// matrix stored in column major order. @override void addPath(ui.Path path, ui.Offset offset, {Float64List? matrix4}) { - // ignore: unnecessary_null_comparison - assert(path != null); // path is checked on the engine side - assert(offsetIsValid(offset)); - if (matrix4 != null) { - _addPathWithMatrix( - path as SurfacePath, offset.dx, offset.dy, toMatrix32(matrix4)); - } else { - _addPath(path as SurfacePath, offset.dx, offset.dy); - } + _addPath(path, offset.dx, offset.dy, + matrix4 == null ? null : toMatrix32(matrix4), SPathAddPathMode.kAppend); } - void _addPath(SurfacePath path, double dx, double dy) { - if (dx == 0.0 && dy == 0.0) { - subpaths.addAll(path.subpaths); + void _addPath(ui.Path path, double offsetX, double offsetY, + Float32List? matrix4, int mode) { + SurfacePath source = path as SurfacePath; + if (source.pathRef.isEmpty) { + return; + } + // Detect if we're trying to add ourself, set source to a copy. + if (source.pathRef == pathRef) { + source = SurfacePath.from(this); + } + + final int previousPointCount = pathRef.countPoints(); + + // Fast path add points,verbs if matrix doesn't have perspective and + // we are not extending. + if (mode == SPathAddPathMode.kAppend && + (matrix4 == null || _isSimple2dTransform(matrix4))) { + pathRef._append(source.pathRef); } else { - subpaths.addAll(path - ._transform(Matrix4.translationValues(dx, dy, 0.0).storage) - .subpaths); + bool firstVerb = true; + final PathRefIterator iter = PathRefIterator(source.pathRef); + final Float32List outPts = Float32List(PathRefIterator.kMaxBufferSize); + int verb; + while ((verb = iter.next(outPts)) != SPath.kDoneVerb) { + switch (verb) { + case SPath.kMoveVerb: + double point0X = matrix4 == null + ? outPts[0] + offsetX + : (matrix4[0] * (outPts[0] + offsetX)) + + (matrix4[4] * (outPts[1] + offsetY)) + + matrix4[12]; + double point0Y = matrix4 == null + ? outPts[1] + offsetY + : (matrix4[1] * (outPts[0] + offsetX)) + + (matrix4[5] * (outPts[1] + offsetY)) + + matrix4[13] + + offsetY; + if (firstVerb && !pathRef.isEmpty) { + assert(mode == SPathAddPathMode.kExtend); + // In case last contour is closed inject move to. + _injectMoveToIfNeeded(); + double lastPointX; + double lastPointY; + if (previousPointCount == 0) { + lastPointX = lastPointY = 0; + } else { + int listIndex = 2 * (previousPointCount - 1); + lastPointX = pathRef.points[listIndex++]; + lastPointY = pathRef.points[listIndex]; + } + // don't add lineTo if it is degenerate. + if (fLastMoveToIndex < 0 || + (previousPointCount != 0) || + lastPointX != point0X || + lastPointY != point0Y) { + lineTo(outPts[0], outPts[1]); + } + } else { + moveTo(outPts[0], outPts[1]); + } + break; + case SPath.kLineVerb: + lineTo(outPts[2], outPts[3]); + break; + case SPath.kQuadVerb: + _quadTo(outPts[2], outPts[3], outPts[4], outPts[5]); + break; + case SPath.kConicVerb: + conicTo( + outPts[2], outPts[3], outPts[4], outPts[5], iter.conicWeight); + break; + case SPath.kCubicVerb: + cubicTo(outPts[2], outPts[3], outPts[4], outPts[5], outPts[6], + outPts[7]); + break; + case SPath.kCloseVerb: + close(); + break; + } + firstVerb = false; + } } - } - void _addPathWithMatrix( - SurfacePath path, double dx, double dy, Float32List matrix) { - assert(matrix4IsValid(matrix)); - final Matrix4 transform = Matrix4.fromFloat32List(matrix); - transform.translate(dx, dy); - subpaths.addAll(path._transform(transform.storage).subpaths); + // Shift [fLastMoveToIndex] by existing point count. + if (source.fLastMoveToIndex >= 0) { + fLastMoveToIndex = previousPointCount + source.fLastMoveToIndex; + } + // Translate/transform all points. + int newPointCount = pathRef.countPoints(); + final Float32List points = pathRef.points; + for (int p = previousPointCount * 2; p < (newPointCount * 2); p += 2) { + if (matrix4 == null) { + points[p] += offsetX; + points[p + 1] += offsetY; + } else { + final double x = offsetX + points[p]; + final double y = offsetY + points[p + 1]; + points[p] = (matrix4[0] * (x)) + (matrix4[4] * y) + matrix4[12]; + points[p + 1] = (matrix4[1] * x) + (matrix4[5] * y) + matrix4[13]; + } + } + _resetAfterEdit(); } /// Adds the given path to this path by extending the current segment of this @@ -541,51 +1169,9 @@ class SurfacePath implements ui.Path { /// matrix stored in column major order. @override void extendWithPath(ui.Path path, ui.Offset offset, {Float64List? matrix4}) { - // ignore: unnecessary_null_comparison - assert(path != null); // path is checked on the engine side assert(offsetIsValid(offset)); - if (matrix4 != null) { - final Float32List matrix32 = toMatrix32(matrix4); - assert(matrix4IsValid(matrix32)); - _extendWithPathAndMatrix( - path as SurfacePath, offset.dx, offset.dy, matrix32); - } else { - _extendWithPath(path as SurfacePath, offset.dx, offset.dy); - } - } - - void _extendWithPath(SurfacePath path, double dx, double dy) { - if (dx == 0.0 && dy == 0.0) { - assert(path.subpaths.length == 1); - _ensurePathStarted(); - _commands.addAll(path.subpaths.single.commands); - _setCurrentPoint( - path.subpaths.single.currentX, path.subpaths.single.currentY); - } else { - throw UnimplementedError('Cannot extend path with non-zero offset'); - } - } - - void _extendWithPathAndMatrix( - SurfacePath path, double dx, double dy, Float32List matrix) { - throw UnimplementedError('Cannot extend path with transform matrix'); - } - - /// Closes the last subpath, as if a straight line had been drawn - /// from the current point to the first point of the subpath. - @override - void close() { - _ensurePathStarted(); - _commands.add(const CloseCommand()); - _setCurrentPoint(_currentSubpath.startX, _currentSubpath.startY); - } - - /// Clears the [Path] object of all subpaths, returning it to the - /// same state it had when it was created. The _current point_ is - /// reset to the origin. - @override - void reset() { - subpaths.clear(); + _addPath(path, offset.dx, offset.dy, + matrix4 == null ? null : toMatrix32(matrix4), SPathAddPathMode.kExtend); } /// Tests to see if the given point is within the path. (That is, whether the @@ -598,126 +1184,242 @@ class SurfacePath implements ui.Path { /// /// Note: Not very efficient, it creates a canvas, plays path and calls /// Context2D isPointInPath. If performance becomes issue, retaining - /// [RawRecordingCanvas] can remove create/remove rootElement cost. + /// RawRecordingCanvas can remove create/remove rootElement cost. @override bool contains(ui.Offset point) { assert(offsetIsValid(point)); - final int subPathCount = subpaths.length; - if (subPathCount == 0) { - return false; + bool isInverse = _isInverseFillType; + if (pathRef.isEmpty) { + return isInverse; } - final double pointX = point.dx; - final double pointY = point.dy; - if (subPathCount == 1) { - // Optimize for rect/roundrect checks. - final Subpath subPath = subpaths[0]; - if (subPath.commands.length == 1) { - final PathCommand cmd = subPath.commands[0]; - if (cmd is RectCommand) { - if (pointY < cmd.y || pointY > (cmd.y + cmd.height)) { - return false; - } - if (pointX < cmd.x || pointX > (cmd.x + cmd.width)) { - return false; - } - return true; - } else if (cmd is RRectCommand) { - final ui.RRect rRect = cmd.rrect; - if (pointY < rRect.top || pointY > rRect.bottom) { - return false; - } - if (pointX < rRect.left || pointX > rRect.right) { - return false; - } - final double rRectWidth = rRect.width; - final double rRectHeight = rRect.height; - final double tlRadiusX = math.min(rRect.tlRadiusX, rRectWidth / 2.0); - final double tlRadiusY = math.min(rRect.tlRadiusY, rRectHeight / 2.0); - if (pointX < (rRect.left + tlRadiusX) && - pointY < (rRect.top + tlRadiusY)) { - // Top left corner - return _ellipseContains(pointX, pointY, rRect.left + tlRadiusX, - rRect.top + tlRadiusY, tlRadiusX, tlRadiusY); - } - final double trRadiusX = math.min(rRect.trRadiusX, rRectWidth / 2.0); - final double trRadiusY = math.min(rRect.trRadiusY, rRectHeight / 2.0); - if (pointX >= (rRect.right - trRadiusX) && - pointY < (rRect.top + trRadiusY)) { - // Top right corner - return _ellipseContains(pointX, pointY, rRect.right - trRadiusX, - rRect.top + trRadiusY, trRadiusX, trRadiusY); - } - final double brRadiusX = math.min(rRect.brRadiusX, rRectWidth / 2.0); - final double brRadiusY = math.min(rRect.brRadiusY, rRectHeight / 2.0); - if (pointX >= (rRect.right - brRadiusX) && - pointY >= (rRect.bottom - brRadiusY)) { - // Bottom right corner - return _ellipseContains(pointX, pointY, rRect.right - brRadiusX, - rRect.bottom - brRadiusY, trRadiusX, trRadiusY); - } - final double blRadiusX = math.min(rRect.blRadiusX, rRectWidth / 2.0); - final double blRadiusY = math.min(rRect.blRadiusY, rRectHeight / 2.0); - if (pointX < (rRect.left + blRadiusX) && - pointY >= (rRect.bottom - blRadiusY)) { - // Bottom left corner - return _ellipseContains(pointX, pointY, rRect.left + blRadiusX, - rRect.bottom - blRadiusY, trRadiusX, trRadiusY); + // Check bounds including right/bottom. + final ui.Rect bounds = getBounds(); + final double x = point.dx; + final double y = point.dy; + if (x < bounds.left || y < bounds.top || x > bounds.right || + y > bounds.bottom) { + return isInverse; + } + final PathWinding windings = PathWinding(pathRef, point.dx, point.dy); + bool evenOddFill = ui.PathFillType.evenOdd == _fillType; + int w = windings.w; + if (evenOddFill) { + w &= 1; + } + if (w != 0) { + return !isInverse; + } + final int onCurveCount = windings.onCurveCount; + if (onCurveCount <= 1) { + return (onCurveCount != 0) ^ isInverse; + } + if ((onCurveCount & 1) != 0 || evenOddFill) { + return (onCurveCount & 1) != 0 ^ (isInverse ? 1 : 0); + } + // If the point touches an even number of curves, and the fill is winding, + // check for coincidence. Count coincidence as places where the on curve + // points have identical tangents. + final PathIterator iter = PathIterator(pathRef, true); + final Float32List _buffer = Float32List(8 + 10); + List tangents = []; + bool done = false; + do { + int oldCount = tangents.length; + switch (iter.next(_buffer)) { + case SPath.kMoveVerb: + case SPath.kCloseVerb: + break; + case SPath.kLineVerb: + tangentLine(_buffer, x, y, tangents); + break; + case SPath.kQuadVerb: + tangentQuad(_buffer, x, y, tangents); + break; + case SPath.kConicVerb: + tangentConic(_buffer, x, y, iter.conicWeight, tangents); + break; + case SPath.kCubicVerb: + tangentCubic(_buffer, x, y, tangents); + break; + case SPath.kDoneVerb: + done = true; + break; + } + if (tangents.length > oldCount) { + int last = tangents.length - 1; + final ui.Offset tangent = tangents[last]; + if (_nearlyEqual(_lengthSquaredOffset(tangent), 0)) { + tangents.remove(last); + } else { + for (int index = 0; index < last; ++index) { + final ui.Offset test = tangents[index]; + double crossProduct = test.dx * tangent.dy - test.dy * tangent.dx; + if (_nearlyEqual(crossProduct, 0) && + SPath.scalarSignedAsInt(tangent.dx * test.dx) <= 0 && + SPath.scalarSignedAsInt(tangent.dy * test.dy) <= 0) { + ui.Offset offset = tangents.removeAt(last); + if (index != tangents.length) { + tangents[index] = offset; + } + break; + } } - return true; } - // TODO: For improved performance, handle Ellipse special case. } - } - final ui.Size size = window.physicalSize; - // If device pixel ratio has changed we can't reuse prior raw recorder. - if (_rawRecorder != null && - _rawRecorder!._devicePixelRatio != - EngineWindow.browserDevicePixelRatio) { - _rawRecorder = null; - } - final double dpr = window.devicePixelRatio; - final RawRecordingCanvas rawRecorder = _rawRecorder ??= - RawRecordingCanvas(ui.Size(size.width / dpr, size.height / dpr)); - // Account for the shift due to padding. - rawRecorder.translate(-BitmapCanvas.kPaddingPixels.toDouble(), - -BitmapCanvas.kPaddingPixels.toDouble()); - rawRecorder.drawPath( - this, (SurfacePaint()..color = const ui.Color(0xFF000000)).paintData); - final double recorderDevicePixelRatio = rawRecorder._devicePixelRatio; - final bool result = rawRecorder._canvasPool.context.isPointInPath( - pointX * recorderDevicePixelRatio, pointY * recorderDevicePixelRatio); - rawRecorder.dispose(); - return result; + } while (!done); + return tangents.length == 0 ? isInverse : !isInverse; } /// Returns a copy of the path with all the segments of every /// subpath translated by the given offset. @override - SurfacePath shift(ui.Offset offset) { - assert(offsetIsValid(offset)); - final List shiftedSubPaths = []; - for (final Subpath subPath in subpaths) { - shiftedSubPaths.add(subPath.shift(offset)); - } - return SurfacePath._clone(shiftedSubPaths, fillType); - } + SurfacePath shift(ui.Offset offset) => + SurfacePath.shiftedFrom(this, offset.dx, offset.dy); /// Returns a copy of the path with all the segments of every /// sub path transformed by the given matrix. @override SurfacePath transform(Float64List matrix4) { - return _transform(toMatrix32(matrix4)); + SurfacePath newPath = SurfacePath.from(this); + newPath._transform(matrix4); + return newPath; } - SurfacePath _transform(Float32List matrix) { - assert(matrix4IsValid(matrix)); - final SurfacePath transformedPath = SurfacePath(); - for (final Subpath subPath in subpaths) { - for (final PathCommand cmd in subPath.commands) { - cmd.transform(matrix, transformedPath); + void _transform(Float64List m) { + pathRef.startEdit(); + final int pointCount = pathRef.countPoints(); + final Float32List points = pathRef.points; + for (int i = 0, len = pointCount * 2; i < len; i += 2) { + final double x = points[i]; + final double y = points[i + 1]; + final double w = 1.0 / ((m[3] * x) + (m[7] * y) + m[15]); + final double transformedX = ((m[0] * x) + (m[4] * y) + m[12]) * w; + final double transformedY = ((m[1] * x) + (m[5] * y) + m[13]) * w; + points[i] = transformedX; + points[i + 1] = transformedY; + } + // TODO: optimize for axis aligned or scale/translate type transforms. + _convexityType = SPathConvexityType.kUnknown; + } + + void setConvexityType(int value) { + _convexityType = value; + } + + int _setComputedConvexity(int value) { + assert(value != SPathConvexityType.kUnknown); + setConvexityType(value); + return value; + } + + /// Returns the convexity type, computing if needed. Never returns kUnknown. + int get convexityType { + if (_convexityType != SPathConvexityType.kUnknown) { + return _convexityType; + } + return _internalGetConvexity(); + } + + /// Returns the current convexity type, skips computing if unknown. + /// + /// Provides a signal to path users if convexity has been calculated in + /// which case _firstDirection is a valid result. + int getConvexityTypeOrUnknown() => _convexityType; + + /// Returns true if the path is convex. If necessary, it will first compute + /// the convexity. + bool get isConvex => SPathConvexityType.kConvex == convexityType; + + // Computes convexity and first direction. + int _internalGetConvexity() { + final Float32List pts = Float32List(20); + PathIterator iter = PathIterator(pathRef, true); + // Check to see if path changes direction more than three times as quick + // concave test. + int pointCount = pathRef.countPoints(); + // Last moveTo index may exceed point count if data comes from fuzzer. + if (0 < fLastMoveToIndex && fLastMoveToIndex < pointCount) { + pointCount = fLastMoveToIndex; + } + if (pointCount > 3) { + int pointIndex = 0; + // only consider the last of the initial move tos + while (SPath.kMoveVerb == iter.next(pts)) { + pointIndex++; + } + --pointIndex; + int convexity = + Convexicator.bySign(pathRef, pointIndex, pointCount - pointIndex); + if (SPathConvexityType.kConcave == convexity) { + setConvexityType(SPathConvexityType.kConcave); + return SPathConvexityType.kConcave; + } else if (SPathConvexityType.kUnknown == convexity) { + return SPathConvexityType.kUnknown; + } + iter = PathIterator(pathRef, true); + } else if (!pathRef.isFinite) { + return SPathConvexityType.kUnknown; + } + // Path passed quick concave check, now compute actual convexity. + int contourCount = 0; + int count; + Convexicator state = Convexicator(); + int verb; + while ((verb = iter.next(pts)) != SPath.kDoneVerb) { + switch (verb) { + case SPath.kMoveVerb: + // If we have more than 1 contour bail out. + if (++contourCount > 1) { + return _setComputedConvexity(SPathConvexityType.kConcave); + } + state.setMovePt(pts[0], pts[1]); + count = 0; + break; + case SPath.kLineVerb: + count = 1; + break; + case SPath.kQuadVerb: + count = 2; + break; + case SPath.kConicVerb: + count = 2; + break; + case SPath.kCubicVerb: + count = 3; + break; + case SPath.kCloseVerb: + if (!state.close()) { + if (!state.isFinite) { + return SPathConvexityType.kUnknown; + } + return _setComputedConvexity(SPathConvexityType.kConcave); + } + count = 0; + break; + default: + return _setComputedConvexity(SPathConvexityType.kConcave); + } + for (int i = 2, len = count * 2; i <= len; i += 2) { + if (!state.addPoint(pts[i], pts[i + 1])) { + if (!state.isFinite) { + return SPathConvexityType.kUnknown; + } + return _setComputedConvexity(SPathConvexityType.kConcave); + } + } + } + + if (this._firstDirection == SPathDirection.kUnknown) { + if (state.firstDirection == SPathDirection.kUnknown && + !pathRef.getBounds().isEmpty) { + return _setComputedConvexity(state.reversals < 3 + ? SPathConvexityType.kConvex + : SPathConvexityType.kConcave); } + _firstDirection = state.firstDirection; } - return transformedPath; + _setComputedConvexity(SPathConvexityType.kConvex); + return _convexityType; } /// Computes the bounding rectangle for this path. @@ -737,328 +1439,76 @@ class SurfacePath implements ui.Path { // see https://skia.org/user/api/SkPath_Reference#SkPath_getBounds @override ui.Rect getBounds() { - // Sufficiently small number for curve eq. - const double epsilon = 0.000000001; + if (pathRef.isRRect != -1 || pathRef.isOval != -1) { + return pathRef.getBounds(); + } + if (!pathRef.fBoundsIsDirty && pathRef.cachedBounds != null) { + return pathRef.cachedBounds!; + } bool ltrbInitialized = false; double left = 0.0, top = 0.0, right = 0.0, bottom = 0.0; - double curX = 0.0; - double curY = 0.0; double minX = 0.0, maxX = 0.0, minY = 0.0, maxY = 0.0; - for (Subpath subpath in subpaths) { - for (PathCommand op in subpath.commands) { - bool skipBounds = false; - switch (op.type) { - case PathCommandTypes.moveTo: - final MoveTo cmd = op as MoveTo; - curX = minX = maxX = cmd.x; - curY = minY = maxY = cmd.y; - break; - case PathCommandTypes.lineTo: - final LineTo cmd = op as LineTo; - curX = minX = maxX = cmd.x; - curY = minY = maxY = cmd.y; - break; - case PathCommandTypes.ellipse: - final Ellipse cmd = op as Ellipse; - // Rotate 4 corners of bounding box. - final double rx = cmd.radiusX; - final double ry = cmd.radiusY; - final double cosVal = math.cos(cmd.rotation); - final double sinVal = math.sin(cmd.rotation); - final double rxCos = rx * cosVal; - final double ryCos = ry * cosVal; - final double rxSin = rx * sinVal; - final double rySin = ry * sinVal; - - final double leftDeltaX = rxCos - rySin; - final double rightDeltaX = -rxCos - rySin; - final double topDeltaY = ryCos + rxSin; - final double bottomDeltaY = ryCos - rxSin; - - final double centerX = cmd.x; - final double centerY = cmd.y; - - double rotatedX = centerX + leftDeltaX; - double rotatedY = centerY + topDeltaY; - minX = maxX = rotatedX; - minY = maxY = rotatedY; - - rotatedX = centerX + rightDeltaX; - rotatedY = centerY + bottomDeltaY; - minX = math.min(minX, rotatedX); - maxX = math.max(maxX, rotatedX); - minY = math.min(minY, rotatedY); - maxY = math.max(maxY, rotatedY); - - rotatedX = centerX - leftDeltaX; - rotatedY = centerY - topDeltaY; - minX = math.min(minX, rotatedX); - maxX = math.max(maxX, rotatedX); - minY = math.min(minY, rotatedY); - maxY = math.max(maxY, rotatedY); - - rotatedX = centerX - rightDeltaX; - rotatedY = centerY - bottomDeltaY; - minX = math.min(minX, rotatedX); - maxX = math.max(maxX, rotatedX); - minY = math.min(minY, rotatedY); - maxY = math.max(maxY, rotatedY); - - curX = centerX + cmd.radiusX; - curY = centerY; - break; - case PathCommandTypes.quadraticCurveTo: - final QuadraticCurveTo cmd = op as QuadraticCurveTo; - final double x1 = curX; - final double y1 = curY; - final double cpX = cmd.x1; - final double cpY = cmd.y1; - final double x2 = cmd.x2; - final double y2 = cmd.y2; - - minX = math.min(x1, x2); - minY = math.min(y1, y2); - maxX = math.max(x1, x2); - maxY = math.max(y1, y2); - - // Curve equation : (1-t)(1-t)P1 + 2t(1-t)CP + t*t*P2. - // At extrema's derivative = 0. - // Solve for - // -2x1+2tx1 + 2cpX + 4tcpX + 2tx2 = 0 - // -2x1 + 2cpX +2t(x1 + 2cpX + x2) = 0 - // t = (x1 - cpX) / (x1 - 2cpX + x2) - - double denom = x1 - (2 * cpX) + x2; - if (denom.abs() > epsilon) { - final double t1 = (x1 - cpX) / denom; - if ((t1 >= 0) && (t1 <= 1.0)) { - // Solve (x,y) for curve at t = tx to find extrema - final double tprime = 1.0 - t1; - final double extremaX = (tprime * tprime * x1) + - (2 * t1 * tprime * cpX) + - (t1 * t1 * x2); - final double extremaY = (tprime * tprime * y1) + - (2 * t1 * tprime * cpY) + - (t1 * t1 * y2); - // Expand bounds. - minX = math.min(minX, extremaX); - maxX = math.max(maxX, extremaX); - minY = math.min(minY, extremaY); - maxY = math.max(maxY, extremaY); - } - } - // Now calculate dy/dt = 0 - denom = y1 - (2 * cpY) + y2; - if (denom.abs() > epsilon) { - final double t2 = (y1 - cpY) / denom; - if ((t2 >= 0) && (t2 <= 1.0)) { - final double tprime2 = 1.0 - t2; - final double extrema2X = (tprime2 * tprime2 * x1) + - (2 * t2 * tprime2 * cpX) + - (t2 * t2 * x2); - final double extrema2Y = (tprime2 * tprime2 * y1) + - (2 * t2 * tprime2 * cpY) + - (t2 * t2 * y2); - // Expand bounds. - minX = math.min(minX, extrema2X); - maxX = math.max(maxX, extrema2X); - minY = math.min(minY, extrema2Y); - maxY = math.max(maxY, extrema2Y); - } - } - curX = x2; - curY = y2; - break; - case PathCommandTypes.bezierCurveTo: - final BezierCurveTo cmd = op as BezierCurveTo; - final double startX = curX; - final double startY = curY; - final double cpX1 = cmd.x1; - final double cpY1 = cmd.y1; - final double cpX2 = cmd.x2; - final double cpY2 = cmd.y2; - final double endX = cmd.x3; - final double endY = cmd.y3; - // Bounding box is defined by all points on the curve where - // monotonicity changes. - minX = math.min(startX, endX); - minY = math.min(startY, endY); - maxX = math.max(startX, endX); - maxY = math.max(startY, endY); - - double extremaX; - double extremaY; - double a, b, c; - - // Check for simple case of strong ordering before calculating - // extrema - if (!(((startX < cpX1) && (cpX1 < cpX2) && (cpX2 < endX)) || - ((startX > cpX1) && (cpX1 > cpX2) && (cpX2 > endX)))) { - // The extrema point is dx/dt B(t) = 0 - // The derivative of B(t) for cubic bezier is a quadratic equation - // with multiple roots - // B'(t) = a*t*t + b*t + c*t - a = -startX + (3 * (cpX1 - cpX2)) + endX; - b = 2 * (startX - (2 * cpX1) + cpX2); - c = -startX + cpX1; - - // Now find roots for quadratic equation with known coefficients - // a,b,c - // The roots are (-b+-sqrt(b*b-4*a*c)) / 2a - double s = (b * b) - (4 * a * c); - // If s is negative, we have no real roots - if ((s >= 0.0) && (a.abs() > epsilon)) { - if (s == 0.0) { - // we have only 1 root - final double t = -b / (2 * a); - final double tprime = 1.0 - t; - if ((t >= 0.0) && (t <= 1.0)) { - extremaX = ((tprime * tprime * tprime) * startX) + - ((3 * tprime * tprime * t) * cpX1) + - ((3 * tprime * t * t) * cpX2) + - (t * t * t * endX); - minX = math.min(extremaX, minX); - maxX = math.max(extremaX, maxX); - } - } else { - // we have 2 roots - s = math.sqrt(s); - double t = (-b - s) / (2 * a); - double tprime = 1.0 - t; - if ((t >= 0.0) && (t <= 1.0)) { - extremaX = ((tprime * tprime * tprime) * startX) + - ((3 * tprime * tprime * t) * cpX1) + - ((3 * tprime * t * t) * cpX2) + - (t * t * t * endX); - minX = math.min(extremaX, minX); - maxX = math.max(extremaX, maxX); - } - // check 2nd root - t = (-b + s) / (2 * a); - tprime = 1.0 - t; - if ((t >= 0.0) && (t <= 1.0)) { - extremaX = ((tprime * tprime * tprime) * startX) + - ((3 * tprime * tprime * t) * cpX1) + - ((3 * tprime * t * t) * cpX2) + - (t * t * t * endX); - - minX = math.min(extremaX, minX); - maxX = math.max(extremaX, maxX); - } - } - } - } - - // Now calc extremes for dy/dt = 0 just like above - if (!(((startY < cpY1) && (cpY1 < cpY2) && (cpY2 < endY)) || - ((startY > cpY1) && (cpY1 > cpY2) && (cpY2 > endY)))) { - // The extrema point is dy/dt B(t) = 0 - // The derivative of B(t) for cubic bezier is a quadratic equation - // with multiple roots - // B'(t) = a*t*t + b*t + c*t - a = -startY + (3 * (cpY1 - cpY2)) + endY; - b = 2 * (startY - (2 * cpY1) + cpY2); - c = -startY + cpY1; - - // Now find roots for quadratic equation with known coefficients - // a,b,c - // The roots are (-b+-sqrt(b*b-4*a*c)) / 2a - double s = (b * b) - (4 * a * c); - // If s is negative, we have no real roots - if ((s >= 0.0) && (a.abs() > epsilon)) { - if (s == 0.0) { - // we have only 1 root - final double t = -b / (2 * a); - final double tprime = 1.0 - t; - if ((t >= 0.0) && (t <= 1.0)) { - extremaY = ((tprime * tprime * tprime) * startY) + - ((3 * tprime * tprime * t) * cpY1) + - ((3 * tprime * t * t) * cpY2) + - (t * t * t * endY); - minY = math.min(extremaY, minY); - maxY = math.max(extremaY, maxY); - } - } else { - // we have 2 roots - s = math.sqrt(s); - final double t = (-b - s) / (2 * a); - final double tprime = 1.0 - t; - if ((t >= 0.0) && (t <= 1.0)) { - extremaY = ((tprime * tprime * tprime) * startY) + - ((3 * tprime * tprime * t) * cpY1) + - ((3 * tprime * t * t) * cpY2) + - (t * t * t * endY); - minY = math.min(extremaY, minY); - maxY = math.max(extremaY, maxY); - } - // check 2nd root - final double t2 = (-b + s) / (2 * a); - final double tprime2 = 1.0 - t2; - if ((t2 >= 0.0) && (t2 <= 1.0)) { - extremaY = ((tprime2 * tprime2 * tprime2) * startY) + - ((3 * tprime2 * tprime2 * t2) * cpY1) + - ((3 * tprime2 * t2 * t2) * cpY2) + - (t2 * t2 * t2 * endY); - minY = math.min(extremaY, minY); - maxY = math.max(extremaY, maxY); - } - } - } - } - curX = endX; - curY = endY; - break; - case PathCommandTypes.rect: - final RectCommand cmd = op as RectCommand; - minX = cmd.x; - double width = cmd.width; - if (cmd.width < 0) { - minX -= width; - width = -width; - } - minY = cmd.y; - double height = cmd.height; - if (cmd.height < 0) { - minY -= height; - height = -height; - } - curX = minX; - maxX = minX + width; - curY = minY; - maxY = minY + height; - break; - case PathCommandTypes.rRect: - final RRectCommand cmd = op as RRectCommand; - final ui.RRect rRect = cmd.rrect; - curX = minX = rRect.left; - maxX = rRect.left + rRect.width; - curY = minY = rRect.top; - maxY = rRect.top + rRect.height; - break; - case PathCommandTypes.close: - default: - skipBounds = false; - break; - } - if (!skipBounds) { - if (!ltrbInitialized) { - left = minX; - right = maxX; - top = minY; - bottom = maxY; - ltrbInitialized = true; - } else { - left = math.min(left, minX); - right = math.max(right, maxX); - top = math.min(top, minY); - bottom = math.max(bottom, maxY); - } - } + final PathRefIterator iter = PathRefIterator(pathRef); + final Float32List points = pathRef.points; + int verb; + _CubicBounds? cubicBounds; + _QuadBounds? quadBounds; + _ConicBounds? conicBounds; + while ((verb = iter.nextIndex()) != SPath.kDoneVerb) { + final int pIndex = iter.iterIndex; + switch (verb) { + case SPath.kMoveVerb: + minX = maxX = points[pIndex]; + minY = maxY = points[pIndex + 1]; + break; + case SPath.kLineVerb: + minX = maxX = points[pIndex + 2]; + minY = maxY = points[pIndex + 3]; + break; + case SPath.kQuadVerb: + quadBounds ??= _QuadBounds(); + quadBounds.calculateBounds(points, pIndex); + minX = quadBounds.minX; + minY = quadBounds.minY; + maxX = quadBounds.maxX; + maxY = quadBounds.maxY; + break; + case SPath.kConicVerb: + conicBounds ??= _ConicBounds(); + conicBounds.calculateBounds(points, iter.conicWeight, pIndex); + minX = conicBounds.minX; + minY = conicBounds.minY; + maxX = conicBounds.maxX; + maxY = conicBounds.maxY; + break; + case SPath.kCubicVerb: + cubicBounds ??= _CubicBounds(); + cubicBounds.calculateBounds(points, pIndex); + minX = cubicBounds.minX; + minY = cubicBounds.minY; + maxX = cubicBounds.maxX; + maxY = cubicBounds.maxY; + break; + } + if (!ltrbInitialized) { + left = minX; + right = maxX; + top = minY; + bottom = maxY; + ltrbInitialized = true; + } else { + left = math.min(left, minX); + right = math.max(right, maxX); + top = math.min(top, minY); + bottom = math.max(bottom, maxY); } } - return ltrbInitialized + ui.Rect newBounds = ltrbInitialized ? ui.Rect.fromLTRB(left, top, right, bottom) : ui.Rect.zero; + pathRef.getBounds(); + pathRef.cachedBounds = newBounds; + return newBounds; } /// Creates a [PathMetrics] object for this path. @@ -1075,83 +1525,125 @@ class SurfacePath implements ui.Path { /// /// Used for web optimization of physical shape represented as /// a persistent div. - ui.RRect? get webOnlyPathAsRoundedRect { - if (subpaths.length != 1) { - return null; - } - final Subpath subPath = subpaths[0]; - if (subPath.commands.length != 1) { - return null; - } - final PathCommand command = subPath.commands[0]; - return (command is RRectCommand) ? command.rrect : null; - } + ui.RRect? get webOnlyPathAsRoundedRect => pathRef.getRRect(); /// Detects if path is simple rectangle and returns rectangle or null. /// /// Used for web optimization of physical shape represented as /// a persistent div. - ui.Rect? get webOnlyPathAsRect { - if (subpaths.length != 1) { - return null; - } - final Subpath subPath = subpaths[0]; - if (subPath.commands.length != 1) { - return null; - } - final PathCommand command = subPath.commands[0]; - return (command is RectCommand) - ? ui.Rect.fromLTWH(command.x, command.y, command.width, command.height) - : null; - } + ui.Rect? get webOnlyPathAsRect => pathRef.getRect(); - /// Detects if path is simple oval and returns [Ellipse] or null. + /// Detects if path is simple oval and returns bounding rectangle or null. /// /// Used for web optimization of physical shape represented as /// a persistent div. - Ellipse? get webOnlyPathAsCircle { - if (subpaths.length != 1) { - return null; - } - final Subpath subPath = subpaths[0]; - if (subPath.commands.length != 1) { - return null; - } - final PathCommand command = subPath.commands[0]; - if (command is Ellipse) { - final Ellipse ellipse = command; - if ((ellipse.endAngle - ellipse.startAngle) % (2 * math.pi) == 0.0) { - return ellipse; - } - } - return null; - } + ui.Rect? get webOnlyPathAsCircle => + pathRef.isOval == -1 ? null : pathRef.getBounds(); /// Serializes this path to a value that's sent to a CSS custom painter for /// painting. List webOnlySerializeToCssPaint() { - final List serializedSubpaths = []; - for (int i = 0; i < subpaths.length; i++) { - serializedSubpaths.add(subpaths[i].serializeToCssPaint()); - } - return serializedSubpaths; + throw UnimplementedError(); } + /// Returns if Path is empty. + /// Empty Path may have FillType but has no points, verbs or weights. + /// Constructor, reset and rewind makes SkPath empty. + bool get isEmpty => 0 == pathRef.countVerbs(); + @override String toString() { if (assertionsEnabled) { - return 'Path(${subpaths.join(', ')})'; + final StringBuffer sb = StringBuffer(); + sb.write('Path('); + final PathRefIterator iter = PathRefIterator(pathRef); + final Float32List points = pathRef.points; + int verb; + while ((verb = iter.nextIndex()) != SPath.kDoneVerb) { + final int pIndex = iter.iterIndex; + switch (verb) { + case SPath.kMoveVerb: + sb.write('MoveTo(${points[pIndex]}, ${points[pIndex + 1]})'); + break; + case SPath.kLineVerb: + sb.write('LineTo(${points[pIndex + 2]}, ${points[pIndex + 3]})'); + break; + case SPath.kQuadVerb: + sb.write('Quad(${points[pIndex + 2]}, ${points[pIndex + 3]},' + ' ${points[pIndex + 3]}, ${points[pIndex + 4]})'); + break; + case SPath.kConicVerb: + sb.write('Conic(${points[pIndex + 2]}, ${points[pIndex + 3]},' + ' ${points[pIndex + 3]}, ${points[pIndex + 4]}, w = ${iter.conicWeight})'); + break; + case SPath.kCubicVerb: + sb.write('Cubic(${points[pIndex + 2]}, ${points[pIndex + 3]},' + ' ${points[pIndex + 3]}, ${points[pIndex + 4]}, ' + ' ${points[pIndex + 5]}, ${points[pIndex + 6]})'); + break; + case SPath.kCloseVerb: + sb.write('Close()'); + break; + } + if (iter.peek() != SPath.kDoneVerb) { + sb.write(' '); + } + } + sb.write(')'); + return sb.toString(); } else { return super.toString(); } } } -// Returns true if point is inside ellipse. -bool _ellipseContains(double px, double py, double centerX, double centerY, - double radiusX, double radiusY) { - final double dx = px - centerX; - final double dy = py - centerY; - return ((dx * dx) / (radiusX * radiusX)) + ((dy * dy) / (radiusY * radiusY)) < - 1.0; +// Returns Offset if arc is lone point and should be approximated with +// moveTo/lineTo. +ui.Offset? _arcIsLonePoint(ui.Rect oval, double startAngle, double sweepAngle) { + if (0 == sweepAngle && (0 == startAngle || 360.0 == startAngle)) { + // This path can be used to move into and out of ovals. If not + // treated as a special case the moves can distort the oval's + // bounding box (and break the circle special case). + return ui.Offset(oval.right, oval.center.dy); + } + return null; +} + +// Computed scaling factor for opposing sides with corner radius given +// a [limit] max width or height. +double _computeMinScale( + double radius1, double radius2, double limit, double scale) { + final double totalRadius = radius1 + radius2; + if (totalRadius <= limit) { + // Radii fit within the limit so return existing scale factor. + return scale; + } + return math.min(limit / totalRadius, scale); } + +bool _isSimple2dTransform(Float32List m) => + m[15] == + 1.0 && // start reading from the last element to eliminate range checks in subsequent reads. + m[14] == 0.0 && // z translation is NOT simple + // m[13] - y translation is simple + // m[12] - x translation is simple + m[11] == 0.0 && + m[10] == 1.0 && + m[9] == 0.0 && + m[8] == 0.0 && + m[7] == 0.0 && + m[6] == 0.0 && + // m[5] - scale y is simple + // m[4] - 2D rotation is simple + m[3] == 0.0 && + m[2] == 0.0; +// m[1] - 2D rotation is simple +// m[0] - scale x is simple + +double _lengthSquaredOffset(ui.Offset offset) { + final double dx = offset.dx; + final double dy = offset.dy; + return dx * dx + dy * dy; +} + +double _lengthSquared(double dx, double dy) => dx * dx + dy * dy; diff --git a/lib/web_ui/lib/src/engine/surface/path/path_metrics.dart b/lib/web_ui/lib/src/engine/surface/path/path_metrics.dart index b5730d311dbd..2a0b388411ac 100644 --- a/lib/web_ui/lib/src/engine/surface/path/path_metrics.dart +++ b/lib/web_ui/lib/src/engine/surface/path/path_metrics.dart @@ -34,23 +34,27 @@ class SurfacePathMetrics extends IterableBase /// Maintains a single instance of computed segments for set of PathMetric /// objects exposed through iterator. +/// +/// [resScale] controls the precision of measure when values > 1. class _SurfacePathMeasure { - _SurfacePathMeasure(this._path, this.forceClosed); + _SurfacePathMeasure(this._path, this.forceClosed, {this.resScale = 1.0}) + : + // nextContour will increment this to the zero based index. + _currentContourIndex = -1, + _pathIterator = PathIterator(_path.pathRef, forceClosed); + final double resScale; final SurfacePath _path; + PathIterator _pathIterator; final List<_PathContourMeasure> _contours = []; // If the contour ends with a call to [Path.close] (which may // have been implied when using [Path.addRect]) final bool forceClosed; - // nextContour will increment this to the zero based index. - int _currentContourIndex = -1; + int _currentContourIndex; int get currentContourIndex => _currentContourIndex; - // Iterator index into [Path.subPaths] - int _subPathIndex = -1; - double length(int contourIndex) { assert(contourIndex <= currentContourIndex, 'Iterator must be advanced before index $contourIndex can be used.'); @@ -85,6 +89,9 @@ class _SurfacePathMeasure { return next; } + // Iterator index into next contour. + int _verbIterIndex = 0; + // Move to the next contour in the path. // // A path can have a next contour if [Path.moveTo] was called after drawing @@ -96,12 +103,13 @@ class _SurfacePathMeasure { // calling `_moveNext` - `_moveNext` should be called after the first // iteration is done instead of before. bool _nativeNextContour() { - if (_subPathIndex == (_path.subpaths.length - 1)) { + if (_verbIterIndex == _path.pathRef.countVerbs()) { return false; } - ++_subPathIndex; - _contours - .add(_PathContourMeasure(_path.subpaths[_subPathIndex], forceClosed)); + _PathContourMeasure measure = + _PathContourMeasure(_path.pathRef, _pathIterator, forceClosed); + _verbIterIndex = measure.verbEndIndex; + _contours.add(measure); return true; } @@ -114,21 +122,23 @@ class _SurfacePathMeasure { /// Builds segments for a single contour to measure distance, compute tangent /// and extract a sub path. class _PathContourMeasure { - _PathContourMeasure(this.subPath, this.forceClosed) { - _buildSegments(); + _PathContourMeasure(this.pathRef, PathIterator iter, this.forceClosed) { + _verbEndIndex = _buildSegments(iter); } + final PathRef pathRef; + int _verbEndIndex = 0; final List<_PathSegment> _segments = []; // Allocate buffer large enough for returning cubic curve chop result. // 2 floats for each coordinate x (start, end & control point 1 & 2). static final Float32List _buffer = Float32List(8); - final Subpath subPath; final bool forceClosed; double get length => _contourLength; bool get isClosed => _isClosed; + int get verbEndIndex => _verbEndIndex; - double _contourLength = 0; + double _contourLength = 0.0; bool _isClosed = false; ui.Tangent? getTangentForOffset(double distance) { @@ -237,183 +247,117 @@ class _PathContourMeasure { _PathSegment segment, double startT, double stopT, ui.Path path) { final List points = segment.points; switch (segment.segmentType) { - case PathCommandTypes.lineTo: + case SPath.kLineVerb: final double toX = (points[2] * stopT) + (points[0] * (1.0 - stopT)); final double toY = (points[3] * stopT) + (points[1] * (1.0 - stopT)); path.lineTo(toX, toY); break; - case PathCommandTypes.bezierCurveTo: - _chopCubicAt(points, startT, stopT, _buffer); + case SPath.kCubicVerb: + _chopCubicBetweenT(points, startT, stopT, _buffer); path.cubicTo(_buffer[2], _buffer[3], _buffer[4], _buffer[5], _buffer[6], _buffer[7]); break; - case PathCommandTypes.quadraticCurveTo: - _chopQuadAt(points, startT, stopT, _buffer); + case SPath.kQuadVerb: + _chopQuadBetweenT(points, startT, stopT, _buffer); path.quadraticBezierTo(_buffer[2], _buffer[3], _buffer[4], _buffer[5]); break; + case SPath.kConicVerb: + // Implement this once we start writing out conic segments. + throw UnimplementedError(); default: throw UnsupportedError('Invalid segment type'); } } - void _buildSegments() { + /// Builds segments from contour starting at verb [_verbStartIndex] and + /// returns next contour verb index. + int _buildSegments(PathIterator iter) { assert(_segments.isEmpty, '_buildSegments should be called once'); _isClosed = false; double distance = 0.0; bool haveSeenMoveTo = false; - final List commands = subPath.commands; - double currentX = 0.0, currentY = 0.0; - final Function lineToHandler = (double x, double y) { - final double dx = currentX - x; - final double dy = currentY - y; + final Function lineToHandler = + (double fromX, double fromY, double x, double y) { + final double dx = fromX - x; + final double dy = fromY - y; final double prevDistance = distance; distance += math.sqrt(dx * dx + dy * dy); // As we accumulate distance, we have to check that the result of += // actually made it larger, since a very small delta might be > 0, but // still have no effect on distance (if distance >>> delta). if (distance > prevDistance) { - _segments.add(_PathSegment( - PathCommandTypes.lineTo, distance, [currentX, currentY, x, y])); + _segments + .add(_PathSegment(SPath.kLineVerb, distance, [fromX, fromY, x, y])); } - currentX = x; - currentY = y; }; - - // TODO(yjbanov): make late final (https://github.com/dart-lang/sdk/issues/42422) - _EllipseSegmentResult? ellipseResult; - for (PathCommand command in commands) { - switch (command.type) { - case PathCommandTypes.moveTo: - final MoveTo moveTo = command as MoveTo; - currentX = moveTo.x; - currentY = moveTo.y; + int verb = 0; + final Float32List points = Float32List(PathRefIterator.kMaxBufferSize); + do { + if (iter.peek() == SPath.kMoveVerb && haveSeenMoveTo) { + break; + } + verb = iter.next(points); + switch (verb) { + case SPath.kMoveVerb: haveSeenMoveTo = true; break; - case PathCommandTypes.lineTo: + case SPath.kLineVerb: assert(haveSeenMoveTo); - final LineTo lineTo = command as LineTo; - lineToHandler(lineTo.x, lineTo.y); + lineToHandler(points[0], points[1], points[2], points[3]); break; - case PathCommandTypes.bezierCurveTo: + case SPath.kCubicVerb: assert(haveSeenMoveTo); - final BezierCurveTo curve = command as BezierCurveTo; // Compute cubic curve distance. distance = _computeCubicSegments( - currentX, - currentY, - curve.x1, - curve.y1, - curve.x2, - curve.y2, - curve.x3, - curve.y3, + points[0], + points[1], + points[2], + points[3], + points[4], + points[5], + points[6], + points[7], distance, 0, _kMaxTValue, _segments); break; - case PathCommandTypes.quadraticCurveTo: + case SPath.kConicVerb: assert(haveSeenMoveTo); - final QuadraticCurveTo quadraticCurveTo = command as QuadraticCurveTo; - // Compute quad curve distance. - distance = _computeQuadSegments( - currentX, - currentY, - quadraticCurveTo.x1, - quadraticCurveTo.y1, - quadraticCurveTo.x2, - quadraticCurveTo.y2, - distance, - 0, - _kMaxTValue); + final double w = iter.conicWeight; + Conic conic = Conic(points[0], points[1], points[2], points[3], + points[4], points[5], w); + List conicPoints = conic.toQuads(); + final int len = conicPoints.length; + double startX = conicPoints[0].dx; + double startY = conicPoints[0].dy; + for (int i = 1; i < len; i += 2) { + final double p1x = conicPoints[i].dx; + final double p1y = conicPoints[i].dy; + final double p2x = conicPoints[i + 1].dx; + final double p2y = conicPoints[i + 1].dy; + distance = _computeQuadSegments( + startX, startY, p1x, p1y, p2x, p2y, distance, 0, _kMaxTValue); + startX = p2x; + startY = p2y; + } break; - case PathCommandTypes.close: - break; - case PathCommandTypes.ellipse: - final Ellipse ellipse = command as Ellipse; - ellipseResult ??= _EllipseSegmentResult(); - _computeEllipseSegments( - currentX, - currentY, - distance, - ellipse.x, - ellipse.y, - ellipse.startAngle, - ellipse.endAngle, - ellipse.rotation, - ellipse.radiusX, - ellipse.radiusY, - ellipse.anticlockwise, - ellipseResult!, - _segments); - distance = ellipseResult!.distance; - currentX = ellipseResult!.endPointX; - currentY = ellipseResult!.endPointY; - _isClosed = true; - break; - case PathCommandTypes.rRect: - final RRectCommand rrectCommand = command as RRectCommand; - final ui.RRect rrect = rrectCommand.rrect; - RRectMetricsRenderer(moveToCallback: (double x, double y) { - currentX = x; - currentY = y; - _isClosed = true; - haveSeenMoveTo = true; - }, lineToCallback: (double x, double y) { - lineToHandler(x, y); - }, ellipseCallback: (double centerX, - double centerY, - double radiusX, - double radiusY, - double rotation, - double startAngle, - double endAngle, - bool antiClockwise) { - ellipseResult ??= _EllipseSegmentResult(); - _computeEllipseSegments( - currentX, - currentY, - distance, - centerX, - centerY, - startAngle, - endAngle, - rotation, - radiusX, - radiusY, - antiClockwise, - ellipseResult!, - _segments); - distance = ellipseResult!.distance; - currentX = ellipseResult!.endPointX; - currentY = ellipseResult!.endPointY; - }).render(rrect); - _isClosed = true; - break; - case PathCommandTypes.rect: - final RectCommand rectCommand = command as RectCommand; - final double x = rectCommand.x; - final double y = rectCommand.y; - final double width = rectCommand.width; - final double height = rectCommand.height; - currentX = x; - currentY = y; - lineToHandler(x + width, y); - lineToHandler(x + width, y + height); - lineToHandler(x, y + height); - lineToHandler(x, y); - _isClosed = true; + case SPath.kQuadVerb: + assert(haveSeenMoveTo); + // Compute quad curve distance. + distance = _computeQuadSegments(points[0], points[1], points[2], + points[3], points[4], points[5], distance, 0, _kMaxTValue); break; + case SPath.kCloseVerb: + _contourLength = distance; + return iter._verbIndex; default: - throw UnimplementedError('Unknown path command $command'); + break; } - } - if (!_isClosed && forceClosed && _segments.isNotEmpty) { - _PathSegment firstSegment = _segments.first; - lineToHandler(firstSegment.points[0], firstSegment.points[1]); - } + } while (verb != SPath.kDoneVerb); _contourLength = distance; + return iter._verbIndex; } static bool _tspanBigEnough(int tSpan) => (tSpan >> 10) != 0; @@ -487,7 +431,7 @@ class _PathContourMeasure { final double prevDistance = distance; distance += startToEndDistance; if (distance > prevDistance) { - segments.add(_PathSegment(PathCommandTypes.bezierCurveTo, distance, + segments.add(_PathSegment(SPath.kCubicVerb, distance, [x0, y0, x1, y1, x2, y2, x3, y3])); } } @@ -529,88 +473,17 @@ class _PathContourMeasure { final double prevDistance = distance; distance += startToEndDistance; if (distance > prevDistance) { - _segments.add(_PathSegment(PathCommandTypes.quadraticCurveTo, distance, - [x0, y0, x1, y1, x2, y2])); + _segments.add(_PathSegment( + SPath.kQuadVerb, distance, [x0, y0, x1, y1, x2, y2])); } } return distance; } - - // Create segments by converting arc to cubics. - // See http://www.w3.org/TR/SVG/implnote.html#ArcConversionEndpointToCenter. - static void _computeEllipseSegments( - double startX, - double startY, - double distance, - double cx, - double cy, - double startAngle, - double endAngle, - double rotation, - double radiusX, - double radiusY, - bool anticlockwise, - _EllipseSegmentResult result, - List<_PathSegment> segments) { - final double endX = cx + (radiusX * math.cos(endAngle)); - final double endY = cy + (radiusY * math.sin(endAngle)); - result.endPointX = endX; - result.endPointY = endY; - // Check for http://www.w3.org/TR/SVG/implnote.html#ArcOutOfRangeParameters - // Treat as line segment from start to end if arc has zero radii. - // If start and end point are the same treat as zero length path. - if ((radiusX == 0 || radiusY == 0) || (startX == endX && startY == endY)) { - result.distance = distance; - return; - } - final double rxAbs = radiusX.abs(); - final double ryAbs = radiusY.abs(); - - final double theta1 = startAngle; - final double theta2 = endAngle; - final double thetaArc = theta2 - theta1; - - // Add 0.01f to make sure we have enough segments when thetaArc is close - // to pi/2. - final int numSegments = (thetaArc / ((math.pi / 2.0) + 0.01)).abs().ceil(); - double x0 = startX; - double y0 = startY; - for (int segmentIndex = 0; segmentIndex < numSegments; segmentIndex++) { - final double startTheta = - theta1 + (segmentIndex * thetaArc / numSegments); - final double endTheta = - theta1 + ((segmentIndex + 1) * thetaArc / numSegments); - final double t = (4.0 / 3.0) * math.tan((endTheta - startTheta) / 4); - if (!t.isFinite) { - result.distance = distance; - return; - } - final double sinStartTheta = math.sin(startTheta); - final double cosStartTheta = math.cos(startTheta); - final double sinEndTheta = math.sin(endTheta); - final double cosEndTheta = math.cos(endTheta); - - // Compute cubic segment start, control point and end (target). - final double p1x = rxAbs * (cosStartTheta - t * sinStartTheta) + cx; - final double p1y = ryAbs * (sinStartTheta + t * cosStartTheta) + cy; - final double targetPointX = rxAbs * cosEndTheta + cx; - final double targetPointY = ryAbs * sinEndTheta + cy; - final double p2x = targetPointX + rxAbs * (t * sinEndTheta); - final double p2y = targetPointY + ryAbs * (-t * cosEndTheta); - - distance = _computeCubicSegments(x0, y0, p1x, p1y, p2x, p2y, targetPointX, - targetPointY, distance, 0, _kMaxTValue, segments); - x0 = targetPointX; - y0 = targetPointY; - } - result.distance = distance; - } } /// Tracks iteration from one segment of a path to the next for measurement. class SurfacePathMetricIterator implements Iterator { - // ignore: unnecessary_null_comparison - SurfacePathMetricIterator._(this._pathMeasure) : assert(_pathMeasure != null); + SurfacePathMetricIterator._(this._pathMeasure); SurfacePathMetric? _pathMetric; _SurfacePathMeasure _pathMeasure; @@ -653,8 +526,7 @@ const double _fTolerance = 0.5; /// to maintain consistency with native platforms. class SurfacePathMetric implements ui.PathMetric { SurfacePathMetric._(this._measure) - : assert(_measure != null), // ignore: unnecessary_null_comparison - length = _measure.length(_measure.currentContourIndex), + : length = _measure.length(_measure.currentContourIndex), isClosed = _measure.isClosed(_measure.currentContourIndex), contourIndex = _measure.currentContourIndex; @@ -716,12 +588,6 @@ class SurfacePathMetric implements ui.PathMetric { String toString() => 'PathMetric'; } -class _EllipseSegmentResult { - double endPointX = 0; - double endPointY = 0; - double distance = 0; -} - // Given a vector dx, dy representing slope, normalize and return as [ui.Offset]. ui.Offset _normalizeSlope(double dx, double dy) { final double length = math.sqrt(dx * dx + dy * dy); @@ -732,10 +598,7 @@ ui.Offset _normalizeSlope(double dx, double dy) { class _SurfaceTangent extends ui.Tangent { const _SurfaceTangent(ui.Offset position, ui.Offset vector, this.t) - : assert(position != null), // ignore: unnecessary_null_comparison - assert(vector != null), // ignore: unnecessary_null_comparison - assert(t != null), // ignore: unnecessary_null_comparison - super(position, vector); + : super(position, vector); // Normalized distance of tangent point from start of a contour. final double t; @@ -750,16 +613,16 @@ class _PathSegment { _SurfaceTangent computeTangent(double t) { switch (segmentType) { - case PathCommandTypes.lineTo: + case SPath.kLineVerb: // Simple line. Position is simple interpolation from start to end point. final double xAtDistance = (points[2] * t) + (points[0] * (1.0 - t)); final double yAtDistance = (points[3] * t) + (points[1] * (1.0 - t)); return _SurfaceTangent(ui.Offset(xAtDistance, yAtDistance), _normalizeSlope(points[2] - points[0], points[3] - points[1]), t); - case PathCommandTypes.bezierCurveTo: + case SPath.kCubicVerb: return tangentForCubicAt(t, points[0], points[1], points[2], points[3], points[4], points[5], points[6], points[7]); - case PathCommandTypes.quadraticCurveTo: + case SPath.kQuadVerb: return tangentForQuadAt(t, points[0], points[1], points[2], points[3], points[4], points[5]); default: @@ -856,85 +719,8 @@ class _SkCubicCoefficients { double evalY(double t) => (((ay * t + by) * t) + cy) * t + dy; } -/// Chops cubic spline at startT and stopT, writes result to buffer. -void _chopCubicAt( - List points, double startT, double stopT, Float32List buffer) { - assert(startT != 0 || stopT != 0); - final double p3y = points[7]; - final double p0x = points[0]; - final double p0y = points[1]; - final double p1x = points[2]; - final double p1y = points[3]; - final double p2x = points[4]; - final double p2y = points[5]; - final double p3x = points[6]; - // If startT == 0 chop at end point and return curve. - final bool chopStart = startT != 0; - final double t = chopStart ? startT : stopT; - - final double ab1x = _interpolate(p0x, p1x, t); - final double ab1y = _interpolate(p0y, p1y, t); - final double bc1x = _interpolate(p1x, p2x, t); - final double bc1y = _interpolate(p1y, p2y, t); - final double cd1x = _interpolate(p2x, p3x, t); - final double cd1y = _interpolate(p2y, p3y, t); - final double abc1x = _interpolate(ab1x, bc1x, t); - final double abc1y = _interpolate(ab1y, bc1y, t); - final double bcd1x = _interpolate(bc1x, cd1x, t); - final double bcd1y = _interpolate(bc1y, cd1y, t); - final double abcd1x = _interpolate(abc1x, bcd1x, t); - final double abcd1y = _interpolate(abc1y, bcd1y, t); - if (!chopStart) { - // Return left side of curve. - buffer[0] = p0x; - buffer[1] = p0y; - buffer[2] = ab1x; - buffer[3] = ab1y; - buffer[4] = abc1x; - buffer[5] = abc1y; - buffer[6] = abcd1x; - buffer[7] = abcd1y; - return; - } - if (stopT == 1) { - // Return right side of curve. - buffer[0] = abcd1x; - buffer[1] = abcd1y; - buffer[2] = bcd1x; - buffer[3] = bcd1y; - buffer[4] = cd1x; - buffer[5] = cd1y; - buffer[6] = p3x; - buffer[7] = p3y; - return; - } - // We chopped at startT, now the right hand side of curve is at - // abcd1, bcd1, cd1, p3x, p3y. Chop this part using endT; - final double endT = (stopT - startT) / (1 - startT); - final double ab2x = _interpolate(abcd1x, bcd1x, endT); - final double ab2y = _interpolate(abcd1y, bcd1y, endT); - final double bc2x = _interpolate(bcd1x, cd1x, endT); - final double bc2y = _interpolate(bcd1y, cd1y, endT); - final double cd2x = _interpolate(cd1x, p3x, endT); - final double cd2y = _interpolate(cd1y, p3y, endT); - final double abc2x = _interpolate(ab2x, bc2x, endT); - final double abc2y = _interpolate(ab2y, bc2y, endT); - final double bcd2x = _interpolate(bc2x, cd2x, endT); - final double bcd2y = _interpolate(bc2y, cd2y, endT); - final double abcd2x = _interpolate(abc2x, bcd2x, endT); - final double abcd2y = _interpolate(abc2y, bcd2y, endT); - buffer[0] = abcd1x; - buffer[1] = abcd1y; - buffer[2] = ab2x; - buffer[3] = ab2y; - buffer[4] = abc2x; - buffer[5] = abc2y; - buffer[6] = abcd2x; - buffer[7] = abcd2y; -} - /// Chops quadratic curve at startT and stopT and writes result to buffer. -void _chopQuadAt( +void _chopQuadBetweenT( List points, double startT, double stopT, Float32List buffer) { assert(startT != 0 || stopT != 0); final double p2y = points[5]; @@ -991,8 +777,3 @@ void _chopQuadAt( buffer[4] = abc2x; buffer[5] = abc2y; } - -// Interpolate between two doubles (Not using lerpDouble here since it null -// checks and treats values as 0). -double _interpolate(double startValue, double endValue, double t) => - (startValue * (1 - t)) + endValue * t; diff --git a/lib/web_ui/lib/src/engine/surface/path/path_ref.dart b/lib/web_ui/lib/src/engine/surface/path/path_ref.dart new file mode 100644 index 000000000000..b096b083aa10 --- /dev/null +++ b/lib/web_ui/lib/src/engine/surface/path/path_ref.dart @@ -0,0 +1,1020 @@ +// 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. + +part of engine; + +/// Stores the path verbs, points and conic weights. +/// +/// This is a Dart port of Skia SkPathRef class. +/// For reference Flutter Gallery average points array size is 5.9, max 25 +/// we start with [_pointsCapacity] 10 to reduce allocations during growth. +/// +/// Unlike native skia GenID is not supported since we don't have requirement +/// to update caches due to content changes. +class PathRef { + PathRef() + : _fPoints = Float32List(kInitialPointsCapacity * 2), + _fVerbs = Uint8List(kInitialVerbsCapacity) { + _fPointsCapacity = kInitialPointsCapacity; + _fVerbsCapacity = kInitialVerbsCapacity; + _resetFields(); + } + + // Value to use to check against to insert move(0,0) when a command + // is added without moveTo. + static const int kInitialLastMoveToIndex = -1; + + // SerializationOffsets + static const int kLegacyRRectOrOvalStartIdx_SerializationShift = + 28; // requires 3 bits, ignored. + static const int kLegacyRRectOrOvalIsCCW_SerializationShift = + 27; // requires 1 bit, ignored. + static const int kLegacyIsRRect_SerializationShift = + 26; // requires 1 bit, ignored. + static const int kIsFinite_SerializationShift = 25; // requires 1 bit + static const int kLegacyIsOval_SerializationShift = + 24; // requires 1 bit, ignored. + static const int kSegmentMask_SerializationShift = + 0; // requires 4 bits (deprecated) + + static const int kInitialPointsCapacity = 8; + static const int kInitialVerbsCapacity = 8; + + ui.Rect? fBounds; + ui.Rect? cachedBounds; + int _fPointsCapacity = 0; + int _fPointsLength = 0; + int _fVerbsCapacity = 0; + Float32List _fPoints; + Uint8List _fVerbs; + int _fVerbsLength = 0; + int _conicWeightsCapacity = 0; + Float32List? _conicWeights; + int _conicWeightsLength = 0; + + // Resets state to initial except points and verbs storage. + void _resetFields() { + fBoundsIsDirty = true; // this also invalidates fIsFinite + fSegmentMask = 0; + fIsOval = false; + fIsRRect = false; + fIsRect = false; + // The next two values don't matter unless fIsOval or fIsRRect are true. + fRRectOrOvalIsCCW = false; + fRRectOrOvalStartIdx = 0xAC; + assert(() { + debugValidate(); + return true; + }()); + } + + /// Given a point index stores [x],[y]. + void setPoint(int pointIndex, double x, double y) { + assert(pointIndex < _fPointsLength); + int index = pointIndex * 2; + _fPoints[index] = x; + _fPoints[index + 1] = y; + } + + /// Creates a copy of the path by pointing new path to a current + /// points,verbs and weights arrays. If original path is mutated by adding + /// more verbs, this copy only returns path at the time of copy and shares + /// typed arrays of original path. + PathRef._shallowCopy(PathRef ref) + : _fPoints = ref._fPoints, + _fVerbs = ref._fVerbs { + _fVerbsCapacity = ref._fVerbsCapacity; + _fVerbsLength = ref._fVerbsLength; + + _fPointsCapacity = ref._fPointsCapacity; + _fPointsLength = ref._fPointsLength; + + _conicWeightsCapacity = ref._conicWeightsCapacity; + _conicWeightsLength = ref._conicWeightsLength; + _conicWeights = ref._conicWeights; + fBoundsIsDirty = ref.fBoundsIsDirty; + if (!fBoundsIsDirty) { + fBounds = ref.fBounds; + cachedBounds = ref.cachedBounds; + fIsFinite = ref.fIsFinite; + } + fSegmentMask = ref.fSegmentMask; + fIsOval = ref.fIsOval; + fIsRRect = ref.fIsRRect; + fIsRect = ref.fIsRect; + fRRectOrOvalIsCCW = ref.fRRectOrOvalIsCCW; + fRRectOrOvalStartIdx = ref.fRRectOrOvalStartIdx; + debugValidate(); + } + + Float32List get points => _fPoints; + Float32List? get conicWeights => _conicWeights; + + int countPoints() => _fPointsLength; + int countVerbs() => _fVerbsLength; + int countWeights() => _conicWeightsLength; + + /// Convenience method for reading verb at index. + int atVerb(int index) { + return _fVerbs[index]; + } + + ui.Offset atPoint(int index) { + return ui.Offset(_fPoints[index * 2], _fPoints[index * 2 + 1]); + } + + double atWeight(int index) { + return _conicWeights![index]; + } + + /// Returns true if all of the points in this path are finite, meaning + /// there are no infinities and no NaNs. + bool get isFinite { + if (fBoundsIsDirty) { + _computeBounds(); + } + return fIsFinite; + } + + /// Returns a mask, where each bit corresponding to a SegmentMask is + /// set if the path contains 1 or more segments of that type. + /// Returns 0 for an empty path (no segments). + int get segmentMasks => fSegmentMask; + + /// Returns start index if the path is an oval or -1 if not. + /// + /// Tracking whether a path is an oval is considered an + /// optimization for performance and so some paths that are in + /// fact ovals can report false. + int get isOval => fIsOval ? fRRectOrOvalStartIdx : -1; + bool get isOvalCCW => fRRectOrOvalIsCCW; + + int get isRRect => fIsRRect ? fRRectOrOvalStartIdx : -1; + int get isRect => fIsRect ? fRRectOrOvalStartIdx : -1; + ui.RRect? getRRect() => fIsRRect ? _getRRect() : null; + ui.Rect? getRect() => fIsRect ? _getRect() : null; + bool get isRectCCW => fRRectOrOvalIsCCW; + + bool get hasComputedBounds => !fBoundsIsDirty; + + /// Returns the bounds of the path's points. If the path contains 0 or 1 + /// points, the bounds is set to (0,0,0,0), and isEmpty() will return true. + /// Note: this bounds may be larger than the actual shape, since curves + /// do not extend as far as their control points. + ui.Rect getBounds() { + if (fBoundsIsDirty) { + _computeBounds(); + } + return fBounds!; + } + + /// Reconstructs Rect from path commands. + ui.Rect _getRect() { + return ui.Rect.fromLTRB( + atPoint(0).dx, atPoint(0).dy, atPoint(1).dx, atPoint(2).dy); + } + + /// Reconstructs RRect from path commands. + /// + /// Expect 4 Conics and lines between. + /// Use conic points to calculate corner radius. + ui.RRect _getRRect() { + ui.Rect bounds = getBounds(); + // Radii x,y of 4 corners + final List radii = []; + final PathRefIterator iter = PathRefIterator(this); + final Float32List pts = Float32List(PathRefIterator.kMaxBufferSize); + int verb = iter.next(pts); + assert(SPath.kMoveVerb == verb); + int cornerIndex = 0; + while ((verb = iter.next(pts)) != SPath.kDoneVerb) { + if (SPath.kConicVerb == verb) { + final double controlPx = pts[2]; + final double controlPy = pts[3]; + double vector1_0x = controlPx - pts[0]; + double vector1_0y = controlPy - pts[1]; + double vector2_1x = pts[4] - pts[2]; + double vector2_1y = pts[5] - pts[3]; + double dx, dy; + // Depending on the corner we have control point at same + // horizontal position as startpoint or same vertical position. + // The location delta of control point specifies corner radius. + if (vector1_0x != 0.0) { + // For CW : Top right or bottom left corners. + assert(vector2_1x == 0.0 && vector1_0y == 0.0); + dx = vector1_0x.abs(); + dy = vector2_1y.abs(); + } else if (vector1_0y != 0.0) { + assert(vector2_1x == 0.0 || vector2_1y == 0.0); + dx = vector2_1x.abs(); + dy = vector1_0y.abs(); + } else { + assert(vector2_1y == 0.0); + dx = vector1_0x.abs(); + dy = vector1_0y.abs(); + } + if (assertionsEnabled) { + final int checkCornerIndex = _nearlyEqual(controlPx, bounds.left) + ? (_nearlyEqual(controlPy, bounds.top) + ? _Corner.kUpperLeft + : _Corner.kLowerLeft) + : (_nearlyEqual(controlPy, bounds.top) + ? _Corner.kUpperRight + : _Corner.kLowerRight); + assert(checkCornerIndex == cornerIndex); + } + radii.add(ui.Radius.elliptical(dx, dy)); + ++cornerIndex; + } else { + assert((verb == SPath.kLineVerb && + ((pts[2] - pts[0]) == 0 || (pts[3] - pts[1]) == 0)) || + verb == SPath.kCloseVerb); + } + } + return ui.RRect.fromRectAndCorners(bounds, + topLeft: radii[_Corner.kUpperLeft], + topRight: radii[_Corner.kUpperRight], + bottomRight: radii[_Corner.kLowerRight], + bottomLeft: radii[_Corner.kLowerLeft]); + } + + bool operator ==(Object other) { + if (identical(this, other)) { + return true; + } + if (other.runtimeType != runtimeType) { + return false; + } + return equals(other as PathRef); + } + + bool equals(PathRef ref) { + // We explicitly check fSegmentMask as a quick-reject. We could skip it, + // since it is only a cache of info in the fVerbs, but its a fast way to + // notice a difference + if (fSegmentMask != ref.fSegmentMask) { + return false; + } + + final int pointCount = countPoints(); + if (pointCount != ref.countPoints()) { + return false; + } + for (int i = 0, len = pointCount * 2; i < len; i++) { + if (_fPoints[i] != ref._fPoints[i]) { + return false; + } + } + + if (_conicWeights == null) { + if (ref._conicWeights != null) { + return false; + } + } else { + if (ref._conicWeights == null) { + return false; + } + final int weightCount = _conicWeights!.length; + if (ref._conicWeights!.length != weightCount) { + return false; + } + for (int i = 0; i < weightCount; i++) { + if (_conicWeights![i] != ref._conicWeights![i]) { + return false; + } + } + } + final int verbCount = countVerbs(); + if (verbCount != ref.countVerbs()) { + return false; + } + for (int i = 0; i < verbCount; i++) { + if (_fVerbs[i] != ref._fVerbs[i]) { + return false; + } + } + if (ref.countVerbs() == 0) { + assert(ref.countPoints() == 0); + } + return true; + } + + static Float32List _fPointsFromSource( + PathRef source, double offsetX, double offsetY) { + final int sourceLength = source._fPointsLength; + final int sourceCapacity = source._fPointsCapacity; + final Float32List dest = Float32List(sourceCapacity * 2); + final Float32List sourcePoints = source.points; + for (int i = 0, len = sourceLength * 2; i < len; i += 2) { + dest[i] = sourcePoints[i] + offsetX; + dest[i + 1] = sourcePoints[i + 1] + offsetY; + } + return dest; + } + + static Uint8List _fVerbsFromSource(PathRef source) { + Uint8List verbs = Uint8List(source._fVerbsCapacity); + verbs.setAll(0, source._fVerbs); + return verbs; + } + + /// Returns a new path by translating [source] by [offsetX], [offsetY]. + PathRef.shiftedFrom(PathRef source, double offsetX, double offsetY) + : _fPoints = _fPointsFromSource(source, offsetX, offsetY), + _fVerbs = _fVerbsFromSource(source) { + _conicWeightsCapacity = source._conicWeightsCapacity; + _conicWeightsLength = source._conicWeightsLength; + if (source._conicWeights != null) { + _conicWeights = Float32List(_conicWeightsCapacity); + _conicWeights!.setAll(0, source._conicWeights!); + } + _fVerbsCapacity = source._fVerbsCapacity; + _fVerbsLength = source._fVerbsLength; + + _fPointsCapacity = source._fPointsCapacity; + _fPointsLength = source._fPointsLength; + fBoundsIsDirty = source.fBoundsIsDirty; + if (!fBoundsIsDirty) { + fBounds = source.fBounds!.translate(offsetX, offsetY); + cachedBounds = source.cachedBounds?.translate(offsetX, offsetY); + fIsFinite = source.fIsFinite; + } + fSegmentMask = source.fSegmentMask; + fIsOval = source.fIsOval; + fIsRRect = source.fIsRRect; + fIsRect = source.fIsRect; + fRRectOrOvalIsCCW = source.fRRectOrOvalIsCCW; + fRRectOrOvalStartIdx = source.fRRectOrOvalStartIdx; + debugValidate(); + } + + /// Copies contents from a source path [ref]. + void copy( + PathRef ref, int additionalReserveVerbs, int additionalReservePoints) { + ref.debugValidate(); + final int verbCount = ref.countVerbs(); + final int pointCount = ref.countPoints(); + final int weightCount = ref.countWeights(); + resetToSize(verbCount, pointCount, weightCount, additionalReserveVerbs, + additionalReservePoints); + + js_util.callMethod(_fVerbs, 'set', [ref._fVerbs]); + js_util.callMethod(_fPoints, 'set', [ref._fPoints]); + if (ref._conicWeights == null) { + _conicWeights = null; + } else { + _conicWeights!.setAll(0, ref._conicWeights!); + } + assert(verbCount == 0 || _fVerbs[0] != 0); + fBoundsIsDirty = ref.fBoundsIsDirty; + if (!fBoundsIsDirty) { + fBounds = ref.fBounds; + cachedBounds = ref.cachedBounds; + fIsFinite = ref.fIsFinite; + } + fSegmentMask = ref.fSegmentMask; + fIsOval = ref.fIsOval; + fIsRRect = ref.fIsRRect; + fIsRect = ref.fIsRect; + fRRectOrOvalIsCCW = ref.fRRectOrOvalIsCCW; + fRRectOrOvalStartIdx = ref.fRRectOrOvalStartIdx; + debugValidate(); + } + + void _resizePoints(int newLength) { + if (newLength > _fPointsCapacity) { + _fPointsCapacity = newLength + 10; + Float32List newPoints = Float32List(_fPointsCapacity * 2); + js_util.callMethod(newPoints, 'set', [_fPoints]); + _fPoints = newPoints; + } + _fPointsLength = newLength; + } + + void _resizeVerbs(int newLength) { + if (newLength > _fVerbsCapacity) { + _fVerbsCapacity = newLength + 8; + Uint8List newVerbs = Uint8List(_fVerbsCapacity); + js_util.callMethod(newVerbs, 'set', [_fVerbs]); + _fVerbs = newVerbs; + } + _fVerbsLength = newLength; + } + + void _resizeConicWeights(int newLength) { + if (newLength > _conicWeightsCapacity) { + _conicWeightsCapacity = newLength + 4; + Float32List newWeights = Float32List(_conicWeightsCapacity); + if (_conicWeights != null) { + js_util.callMethod(newWeights, 'set', [_conicWeights]); + } + _conicWeights = newWeights; + } + _conicWeightsLength = newLength; + } + + void _append(PathRef source) { + final int pointCount = source.countPoints(); + final int curLength = _fPointsLength; + final int newPointCount = curLength + pointCount; + startEdit(); + _resizePoints(newPointCount); + final Float32List sourcePoints = source.points; + for (int source = pointCount * 2 - 1, dst = newPointCount * 2 - 1; + source >= 0; + source--, dst--) { + _fPoints[dst] = sourcePoints[source]; + } + final int verbCount = countVerbs(); + final int newVerbCount = source.countVerbs(); + _resizeVerbs(verbCount + newVerbCount); + for (int i = 0; i < newVerbCount; i++) { + _fVerbs[verbCount + i] = source._fVerbs[i]; + } + if (source._conicWeights != null) { + final int weightCount = countWeights(); + final int newWeightCount = source.countWeights(); + _resizeConicWeights(weightCount + newWeightCount); + Float32List sourceWeights = source._conicWeights!; + Float32List dest = _conicWeights!; + for (int i = 0; i < newWeightCount; i++) { + dest[weightCount + i] = sourceWeights[i]; + } + } + fBoundsIsDirty = true; + } + + /// Doesn't read fSegmentMask, but (re)computes it from the verbs array + int computeSegmentMask() { + Uint8List verbs = _fVerbs; + int mask = 0; + int verbCount = countVerbs(); + for (int i = 0; i < verbCount; ++i) { + switch (verbs[i]) { + case SPath.kLineVerb: + mask |= SPath.kLineSegmentMask; + break; + case SPath.kQuadVerb: + mask |= SPath.kQuadSegmentMask; + break; + case SPath.kConicVerb: + mask |= SPath.kConicSegmentMask; + break; + case SPath.kCubicVerb: + mask |= SPath.kCubicSegmentMask; + break; + default: + break; + } + } + return mask; + } + + /// This is incorrectly defined as instance method on SkPathRef although + /// SkPath instance method first makes a copy of itself into out and + /// then interpolates based on weight. + static void interpolate(PathRef ending, double weight, PathRef out) { + assert(out.countPoints() == ending.countPoints()); + final int count = out.countPoints() * 2; + final Float32List outValues = out.points; + final Float32List inValues = ending.points; + for (int index = 0; index < count; ++index) { + outValues[index] = + outValues[index] * weight + inValues[index] * (1.0 - weight); + } + out.fBoundsIsDirty = true; + out.startEdit(); + } + + /// Computes bounds and fIsFinite based on points. + /// + /// Used by getBounds() and cached. + void _computeBounds() { + debugValidate(); + assert(fBoundsIsDirty); + int pointCount = countPoints(); + fBoundsIsDirty = false; + cachedBounds = null; + double accum = 0; + if (pointCount == 0) { + fBounds = ui.Rect.zero; + fIsFinite = true; + } else { + double minX, maxX, minY, maxY; + minX = maxX = _fPoints[0]; + accum *= minX; + minY = maxY = _fPoints[1]; + accum *= minY; + for (int i = 2, len = 2 * pointCount; i < len; i += 2) { + final double x = _fPoints[i]; + accum *= x; + final double y = _fPoints[i + 1]; + accum *= y; + minX = math.min(minX, x); + minY = math.min(minY, y); + maxX = math.max(maxX, x); + maxY = math.max(maxY, y); + } + bool allFinite = (accum * 0 == 0); + if (allFinite) { + fBounds = ui.Rect.fromLTRB(minX, minY, maxX, maxY); + fIsFinite = true; + } else { + fBounds = ui.Rect.zero; + fIsFinite = false; + } + } + } + + /// Sets to initial state preserving internal storage. + void rewind() { + _fPointsLength = 0; + _fVerbsLength = 0; + _conicWeightsLength = 0; + _resetFields(); + } + + /// Resets the path ref with verbCount verbs and pointCount points, all + /// uninitialized. Also allocates space for reserveVerb additional verbs + /// and reservePoints additional points. + void resetToSize(int verbCount, int pointCount, int conicCount, + [int reserveVerbs = 0, int reservePoints = 0]) { + debugValidate(); + fBoundsIsDirty = true; // this also invalidates fIsFinite + + fSegmentMask = 0; + startEdit(); + + _resizePoints(pointCount + reservePoints); + _resizeVerbs(verbCount + reserveVerbs); + _resizeConicWeights(conicCount); + debugValidate(); + } + + /// Increases the verb count 1, records the new verb, and creates room for + /// the requisite number of additional points. A pointer to the first point + /// is returned. Any new points are uninitialized. + int growForVerb(int verb, double weight) { + debugValidate(); + int pCnt; + int mask = 0; + switch (verb) { + case SPath.kMoveVerb: + pCnt = 1; + break; + case SPath.kLineVerb: + mask = SPath.kLineSegmentMask; + pCnt = 1; + break; + case SPath.kQuadVerb: + mask = SPath.kQuadSegmentMask; + pCnt = 2; + break; + case SPath.kConicVerb: + mask = SPath.kConicSegmentMask; + pCnt = 2; + break; + case SPath.kCubicVerb: + mask = SPath.kCubicSegmentMask; + pCnt = 3; + break; + case SPath.kCloseVerb: + pCnt = 0; + break; + case SPath.kDoneVerb: + if (assertionsEnabled) { + throw Exception("growForVerb called for kDone"); + } + pCnt = 0; + break; + default: + if (assertionsEnabled) { + throw Exception("default is not reached"); + } + pCnt = 0; + break; + } + + fSegmentMask |= mask; + fBoundsIsDirty = true; // this also invalidates fIsFinite + startEdit(); + + int verbCount = countVerbs(); + _resizeVerbs(verbCount + 1); + _fVerbs[verbCount] = verb; + + if (SPath.kConicVerb == verb) { + final int weightCount = countWeights(); + _resizeConicWeights(weightCount + 1); + _conicWeights![weightCount] = weight; + } + final int ptsIndex = _fPointsLength; + _resizePoints(ptsIndex + pCnt); + debugValidate(); + return ptsIndex; + } + + /// Increases the verb count by numVbs and point count by the required amount. + /// The new points are uninitialized. All the new verbs are set to the + /// specified verb. If 'verb' is kConic_Verb, 'weights' will return a + /// pointer to the uninitialized conic weights. + /// + /// This is an optimized version for [SPath.addPolygon]. + int growForRepeatedVerb(int /*SkPath::Verb*/ verb, int numVbs) { + debugValidate(); + startEdit(); + int pCnt; + int mask = 0; + switch (verb) { + case SPath.kMoveVerb: + pCnt = numVbs; + break; + case SPath.kLineVerb: + mask = SPath.kLineSegmentMask; + pCnt = numVbs; + break; + case SPath.kQuadVerb: + mask = SPath.kQuadSegmentMask; + pCnt = 2 * numVbs; + break; + case SPath.kConicVerb: + mask = SPath.kConicSegmentMask; + pCnt = 2 * numVbs; + break; + case SPath.kCubicVerb: + mask = SPath.kCubicSegmentMask; + pCnt = 3 * numVbs; + break; + case SPath.kCloseVerb: + pCnt = 0; + break; + case SPath.kDoneVerb: + if (assertionsEnabled) { + throw Exception("growForVerb called for kDone"); + } + pCnt = 0; + break; + default: + if (assertionsEnabled) { + throw Exception("default is not reached"); + } + pCnt = 0; + break; + } + + fSegmentMask |= mask; + fBoundsIsDirty = true; // this also invalidates fIsFinite + startEdit(); + + if (SPath.kConicVerb == verb) { + _resizeConicWeights(countWeights() + numVbs); + } + int verbCount = countVerbs(); + _resizeVerbs(verbCount + numVbs); + for (int i = 0; i < numVbs; i++) { + _fVerbs[verbCount + i] = verb; + } + + final int ptsIndex = _fPointsLength; + _resizePoints(ptsIndex + pCnt); + debugValidate(); + return ptsIndex; + } + + /// Concatenates all verbs from 'path' onto our own verbs array. Increases the point count by the + /// number of points in 'path', and the conic weight count by the number of conics in 'path'. + /// + /// Returns pointers to the uninitialized points and conic weights data. + void growForVerbsInPath(PathRef path) { + debugValidate(); + startEdit(); + fSegmentMask |= path.fSegmentMask; + fBoundsIsDirty = true; // this also invalidates fIsFinite + + int numVerbs = path.countVerbs(); + if (numVerbs != 0) { + int curLength = countVerbs(); + _resizePoints(curLength + numVerbs); + _fVerbs.setAll(curLength, path._fVerbs); + } + + final int numPts = path.countPoints(); + if (numPts != 0) { + int curLength = countPoints(); + _resizePoints(curLength + numPts); + _fPoints.setAll(curLength * 2, path._fPoints); + } + + final int numConics = path.countWeights(); + if (numConics != 0) { + int curLength = countWeights(); + _resizeConicWeights(curLength + numConics); + final Float32List sourceWeights = path._conicWeights!; + final Float32List destWeights = _conicWeights!; + for (int i = 0; i < numConics; i++) { + destWeights[curLength + i] = sourceWeights[i]; + } + } + + debugValidate(); + } + + /// Resets higher level curve detection before a new edit is started. + /// + /// SurfacePath.addOval, addRRect will set these flags after the verbs and + /// points are added. + void startEdit() { + fIsOval = false; + fIsRRect = false; + fIsRect = false; + cachedBounds = null; + } + + void setIsOval(bool isOval, bool isCCW, int start) { + fIsOval = isOval; + fRRectOrOvalIsCCW = isCCW; + fRRectOrOvalStartIdx = start; + } + + void setIsRRect(bool isRRect, bool isCCW, int start, ui.RRect rrect) { + fIsRRect = isRRect; + fRRectOrOvalIsCCW = isCCW; + fRRectOrOvalStartIdx = start; + } + + void setIsRect(bool isRect, bool isCCW, int start) { + fIsRect = isRect; + fRRectOrOvalIsCCW = isCCW; + fRRectOrOvalStartIdx = start; + } + + Float32List getPoints() { + debugValidate(); + return _fPoints; + } + + static const int kMinSize = 256; + + bool fBoundsIsDirty = true; + bool fIsFinite = true; // only meaningful if bounds are valid + + bool fIsOval = false; + bool fIsRRect = false; + bool fIsRect = false; + // Both the circle and rrect special cases have a notion of direction and starting point + // The next two variables store that information for either. + bool fRRectOrOvalIsCCW = false; + int fRRectOrOvalStartIdx = -1; + int fSegmentMask = 0; + + bool get isValid { + if (fIsOval || fIsRRect) { + // Currently we don't allow both of these to be set. + if (fIsOval == fIsRRect) { + return false; + } + if (fIsOval) { + if (fRRectOrOvalStartIdx >= 4) { + return false; + } + } else { + if (fRRectOrOvalStartIdx >= 8) { + return false; + } + } + } + if (fIsRect) { + if (fIsOval || fIsRRect) { + return false; + } + if (fRRectOrOvalStartIdx >= 4) { + return false; + } + } + + if (!fBoundsIsDirty && !fBounds!.isEmpty) { + bool isFinite = true; + ui.Rect bounds = fBounds!; + final double boundsLeft = bounds.left; + final double boundsTop = bounds.top; + final double boundsRight = bounds.right; + final double boundsBottom = bounds.bottom; + for (int i = 0, len = _fPointsLength * 2; i < len; i += 2) { + final double pointX = _fPoints[i]; + final double pointY = _fPoints[i + 1]; + double tolerance = 0.0001; + final bool pointIsFinite = pointX.isFinite && pointY.isFinite; + if (pointIsFinite && + (pointX + tolerance < boundsLeft || + pointY + tolerance < boundsTop || + pointX - tolerance > boundsRight || + pointY - tolerance > boundsBottom)) { + return false; + } + if (!pointIsFinite) { + isFinite = false; + } + } + if (fIsFinite != isFinite) { + // Inconsistent state. Cached [fIsFinite] doesn't match what we found. + return false; + } + } + return true; + } + + bool get isEmpty => countVerbs() == 0; + + void debugValidate() { + assert(isValid); + } + + /// Returns point index of maximum y in path points. + int findMaxY(int pointIndex, int count) { + assert(count > 0); + // move to y component. + double max = _fPoints[pointIndex * 2 + 1]; + int firstIndex = pointIndex; + for (int i = 1; i < count; i++) { + double y = _fPoints[(pointIndex + i) * 2]; + if (y > max) { + max = y; + firstIndex = pointIndex + i; + } + } + return firstIndex; + } + + /// Returns index of point that is different from point at [index]. + /// + /// Used to get previous/next points that dont coincide for calculating + /// cross product at a point. + int findDiffPoint(int index, int n, int inc) { + int i = index; + for (;;) { + i = (i + inc) % n; + if (i == index) { + // we wrapped around, so abort + break; + } + if (_fPoints[index * 2] != _fPoints[i * 2] || + _fPoints[index * 2 + 1] != _fPoints[i * 2 + 1]) { + // found a different point, success! + break; + } + } + return i; + } +} + +class PathRefIterator { + final PathRef pathRef; + int _conicWeightIndex = -1; + int _verbIndex = 0; + int _pointIndex = 0; + + PathRefIterator(this.pathRef) { + _pointIndex = 0; + if (!pathRef.isFinite) { + // Don't allow iteration through non-finite points, prepare to return + // done verb. + _verbIndex = pathRef.countVerbs(); + } + } + + /// Maximum buffer size required for points in [next] calls. + static const int kMaxBufferSize = 8; + + int iterIndex = 0; + + /// Returns current point index. + int get pointIndex => _pointIndex ~/ 2; + + /// Advances to start of next contour (move verb). + /// + /// Usage: + /// int startPointIndex = PathRefIterator._pointIndex; + /// int nextContourPointIndex = iter.skipToNextContour(); + /// int pointCountInContour = nextContourPointIndex - startPointIndex; + int skipToNextContour() { + int verb = -1; + int curPointIndex = _pointIndex; + do { + curPointIndex = _pointIndex; + verb = nextIndex(); + } while ( + verb != SPath.kDoneVerb && (iterIndex == 0 || verb != SPath.kMoveVerb)); + return (verb == SPath.kDoneVerb ? _pointIndex : curPointIndex) ~/ 2; + } + + /// Returns next verb and [iterIndex] with location of first point. + int nextIndex() { + if (_verbIndex == pathRef.countVerbs()) { + return SPath.kDoneVerb; + } + int verb = pathRef._fVerbs[_verbIndex++]; + switch (verb) { + case SPath.kMoveVerb: + iterIndex = _pointIndex; + _pointIndex += 2; + break; + case SPath.kLineVerb: + iterIndex = _pointIndex - 2; + _pointIndex += 2; + break; + case SPath.kConicVerb: + _conicWeightIndex++; + iterIndex = _pointIndex - 2; + _pointIndex += 4; + break; + case SPath.kQuadVerb: + iterIndex = _pointIndex - 2; + _pointIndex += 4; + break; + case SPath.kCubicVerb: + iterIndex = _pointIndex - 2; + _pointIndex += 6; + break; + case SPath.kCloseVerb: + break; + case SPath.kDoneVerb: + assert(_verbIndex == pathRef.countVerbs()); + break; + default: + throw FormatException('Unsupport Path verb $verb'); + } + return verb; + } + + // Returns next verb and reads associated points into [outPts]. + int next(Float32List outPts) { + if (_verbIndex == pathRef.countVerbs()) { + return SPath.kDoneVerb; + } + int verb = pathRef._fVerbs[_verbIndex++]; + final Float32List points = pathRef.points; + int pointIndex = _pointIndex; + switch (verb) { + case SPath.kMoveVerb: + outPts[0] = points[pointIndex++]; + outPts[1] = points[pointIndex++]; + break; + case SPath.kLineVerb: + outPts[0] = points[pointIndex - 2]; + outPts[1] = points[pointIndex - 1]; + outPts[2] = points[pointIndex++]; + outPts[3] = points[pointIndex++]; + break; + case SPath.kConicVerb: + _conicWeightIndex++; + outPts[0] = points[pointIndex - 2]; + outPts[1] = points[pointIndex - 1]; + outPts[2] = points[pointIndex++]; + outPts[3] = points[pointIndex++]; + outPts[4] = points[pointIndex++]; + outPts[5] = points[pointIndex++]; + break; + case SPath.kQuadVerb: + outPts[0] = points[pointIndex - 2]; + outPts[1] = points[pointIndex - 1]; + outPts[2] = points[pointIndex++]; + outPts[3] = points[pointIndex++]; + outPts[4] = points[pointIndex++]; + outPts[5] = points[pointIndex++]; + break; + case SPath.kCubicVerb: + outPts[0] = points[pointIndex - 2]; + outPts[1] = points[pointIndex - 1]; + outPts[2] = points[pointIndex++]; + outPts[3] = points[pointIndex++]; + outPts[4] = points[pointIndex++]; + outPts[5] = points[pointIndex++]; + outPts[6] = points[pointIndex++]; + outPts[7] = points[pointIndex++]; + break; + case SPath.kCloseVerb: + break; + case SPath.kDoneVerb: + assert(_verbIndex == pathRef.countVerbs()); + break; + default: + throw FormatException('Unsupport Path verb $verb'); + } + _pointIndex = pointIndex; + return verb; + } + + double get conicWeight => pathRef._conicWeights![_conicWeightIndex]; + + int peek() => _verbIndex < pathRef.countVerbs() + ? pathRef._fVerbs[_verbIndex] + : SPath.kDoneVerb; +} + +class _Corner { + static const int kUpperLeft = 0; + static const int kUpperRight = 1; + static const int kLowerRight = 2; + static const int kLowerLeft = 3; +} diff --git a/lib/web_ui/lib/src/engine/surface/path/path_to_svg.dart b/lib/web_ui/lib/src/engine/surface/path/path_to_svg.dart index 0765c797858b..71968c9334a7 100644 --- a/lib/web_ui/lib/src/engine/surface/path/path_to_svg.dart +++ b/lib/web_ui/lib/src/engine/surface/path/path_to_svg.dart @@ -8,184 +8,45 @@ part of engine; /// element. void pathToSvg(SurfacePath path, StringBuffer sb, {double offsetX = 0, double offsetY = 0}) { - for (Subpath subPath in path.subpaths) { - for (PathCommand command in subPath.commands) { - switch (command.type) { - case PathCommandTypes.moveTo: - final MoveTo moveTo = command as MoveTo; - sb.write('M ${moveTo.x + offsetX} ${moveTo.y + offsetY}'); - break; - case PathCommandTypes.lineTo: - final LineTo lineTo = command as LineTo; - sb.write('L ${lineTo.x + offsetX} ${lineTo.y + offsetY}'); - break; - case PathCommandTypes.bezierCurveTo: - final BezierCurveTo curve = command as BezierCurveTo; - sb.write('C ${curve.x1 + offsetX} ${curve.y1 + offsetY} ' - '${curve.x2 + offsetX} ${curve.y2 + offsetY} ${curve.x3 + offsetX} ${curve.y3 + offsetY}'); - break; - case PathCommandTypes.quadraticCurveTo: - final QuadraticCurveTo quadraticCurveTo = command as QuadraticCurveTo; - sb.write( - 'Q ${quadraticCurveTo.x1 + offsetX} ${quadraticCurveTo.y1 + offsetY} ' - '${quadraticCurveTo.x2 + offsetX} ${quadraticCurveTo.y2 + offsetY}'); - break; - case PathCommandTypes.close: - sb.write('Z'); - break; - case PathCommandTypes.ellipse: - final Ellipse ellipse = command as Ellipse; - // Handle edge case where start and end points are the same by drawing - // 2 half arcs. - if ((ellipse.endAngle - ellipse.startAngle) % (2 * math.pi) == 0.0) { - _writeEllipse( - sb, - ellipse.x + offsetX, - ellipse.y + offsetY, - ellipse.radiusX, - ellipse.radiusY, - ellipse.rotation, - -math.pi, - 0, - ellipse.anticlockwise, - moveToStartPoint: true); - _writeEllipse( - sb, - ellipse.x + offsetX, - ellipse.y + offsetY, - ellipse.radiusX, - ellipse.radiusY, - ellipse.rotation, - 0, - math.pi, - ellipse.anticlockwise); - } else { - _writeEllipse( - sb, - ellipse.x + offsetX, - ellipse.y + offsetY, - ellipse.radiusX, - ellipse.radiusY, - ellipse.rotation, - ellipse.startAngle, - ellipse.endAngle, - ellipse.anticlockwise); - } - break; - case PathCommandTypes.rRect: - final RRectCommand rrectCommand = command as RRectCommand; - final ui.RRect rrect = rrectCommand.rrect; - double left = rrect.left + offsetX; - double right = rrect.right + offsetX; - double top = rrect.top + offsetY; - double bottom = rrect.bottom + offsetY; - if (left > right) { - left = right; - right = rrect.left + offsetX; - } - if (top > bottom) { - top = bottom; - bottom = rrect.top + offsetY; - } - final double trRadiusX = rrect.trRadiusX.abs(); - final double tlRadiusX = rrect.tlRadiusX.abs(); - final double trRadiusY = rrect.trRadiusY.abs(); - final double tlRadiusY = rrect.tlRadiusY.abs(); - final double blRadiusX = rrect.blRadiusX.abs(); - final double brRadiusX = rrect.brRadiusX.abs(); - final double blRadiusY = rrect.blRadiusY.abs(); - final double brRadiusY = rrect.brRadiusY.abs(); - - sb.write('M ${left + trRadiusX} $top '); - // Top side and top-right corner - sb.write('L ${right - trRadiusX} $top '); - _writeEllipse(sb, right - trRadiusX, top + trRadiusY, trRadiusX, - trRadiusY, 0, 1.5 * math.pi, 2.0 * math.pi, false); - // Right side and bottom-right corner - sb.write('L $right ${bottom - brRadiusY} '); - _writeEllipse(sb, right - brRadiusX, bottom - brRadiusY, brRadiusX, - brRadiusY, 0, 0, 0.5 * math.pi, false); - // Bottom side and bottom-left corner - sb.write('L ${left + blRadiusX} $bottom '); - _writeEllipse(sb, left + blRadiusX, bottom - blRadiusY, blRadiusX, - blRadiusY, 0, 0.5 * math.pi, math.pi, false); - // Left side and top-left corner - sb.write('L $left ${top + tlRadiusY} '); - _writeEllipse( - sb, - left + tlRadiusX, - top + tlRadiusY, - tlRadiusX, - tlRadiusY, - 0, - math.pi, - 1.5 * math.pi, - false, - ); - break; - case PathCommandTypes.rect: - final RectCommand rectCommand = command as RectCommand; - final bool horizontalSwap = rectCommand.width < 0; - final double left = offsetX + - (horizontalSwap - ? rectCommand.x - rectCommand.width - : rectCommand.x); - final double width = - horizontalSwap ? -rectCommand.width : rectCommand.width; - final bool verticalSwap = rectCommand.height < 0; - final double top = offsetY + - (verticalSwap - ? rectCommand.y - rectCommand.height - : rectCommand.y); - final double height = - verticalSwap ? -rectCommand.height : rectCommand.height; - sb.write('M $left $top '); - sb.write('L ${left + width} $top '); - sb.write('L ${left + width} ${top + height} '); - sb.write('L $left ${top + height} '); - sb.write('L $left $top '); - break; - default: - throw UnimplementedError('Unknown path command $command'); - } + final PathRefIterator iter = PathRefIterator(path.pathRef); + int verb = 0; + final Float32List outPts = Float32List(PathRefIterator.kMaxBufferSize); + while ((verb = iter.next(outPts)) != SPath.kDoneVerb) { + switch (verb) { + case SPath.kMoveVerb: + sb.write('M ${outPts[0] + offsetX} ${outPts[1] + offsetY}'); + break; + case SPath.kLineVerb: + sb.write('L ${outPts[2] + offsetX} ${outPts[3] + offsetY}'); + break; + case SPath.kCubicVerb: + sb.write('C ${outPts[2] + offsetX} ${outPts[3] + offsetY} ' + '${outPts[4] + offsetX} ${outPts[5] + offsetY} ${outPts[6] + offsetX} ${outPts[7] + offsetY}'); + break; + case SPath.kQuadVerb: + sb.write('Q ${outPts[2] + offsetX} ${outPts[3] + offsetY} ' + '${outPts[4] + offsetX} ${outPts[5] + offsetY}'); + break; + case SPath.kConicVerb: + final double w = iter.conicWeight; + Conic conic = Conic(outPts[0], outPts[1], outPts[2], outPts[3], + outPts[4], outPts[5], w); + List points = conic.toQuads(); + final int len = points.length; + for (int i = 1; i < len; i += 2) { + final double p1x = points[i].dx; + final double p1y = points[i].dy; + final double p2x = points[i + 1].dx; + final double p2y = points[i + 1].dy; + sb.write('Q ${p1x + offsetX} ${p1y + offsetY} ' + '${p2x + offsetX} ${p2y + offsetY}'); + } + break; + case SPath.kCloseVerb: + sb.write('Z'); + break; + default: + throw UnimplementedError('Unknown path verb $verb'); } } } - -// See https://www.w3.org/TR/SVG/implnote.html B.2.3. Conversion from center to -// endpoint parameterization. -void _writeEllipse( - StringBuffer sb, - double cx, - double cy, - double radiusX, - double radiusY, - double rotation, - double startAngle, - double endAngle, - bool antiClockwise, - {bool moveToStartPoint = false}) { - final double cosRotation = math.cos(rotation); - final double sinRotation = math.sin(rotation); - final double x = math.cos(startAngle) * radiusX; - final double y = math.sin(startAngle) * radiusY; - - final double startPx = cx + (cosRotation * x - sinRotation * y); - final double startPy = cy + (sinRotation * x + cosRotation * y); - - final double xe = math.cos(endAngle) * radiusX; - final double ye = math.sin(endAngle) * radiusY; - - final double endPx = cx + (cosRotation * xe - sinRotation * ye); - final double endPy = cy + (sinRotation * xe + cosRotation * ye); - - final double delta = endAngle - startAngle; - final bool largeArc = delta.abs() > math.pi; - - final double rotationDeg = rotation / math.pi * 180.0; - if (moveToStartPoint) { - sb.write('M $startPx $startPy '); - } - sb.write('A $radiusX $radiusY $rotationDeg ' - '${largeArc ? 1 : 0} ${antiClockwise ? 0 : 1} $endPx $endPy'); -} diff --git a/lib/web_ui/lib/src/engine/surface/path/path_utils.dart b/lib/web_ui/lib/src/engine/surface/path/path_utils.dart new file mode 100644 index 000000000000..e7ae601762f3 --- /dev/null +++ b/lib/web_ui/lib/src/engine/surface/path/path_utils.dart @@ -0,0 +1,412 @@ +// 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. + +part of engine; + +/// Mask used to keep track of types of verbs used in a path segment. +class SPathSegmentMask { + static const int kLine_SkPathSegmentMask = 1 << 0; + static const int kQuad_SkPathSegmentMask = 1 << 1; + static const int kConic_SkPathSegmentMask = 1 << 2; + static const int kCubic_SkPathSegmentMask = 1 << 3; +} + +/// Types of path operations. +class SPathVerb { + static const int kMove = 1; // 1 point + static const int kLine = 2; // 2 points + static const int kQuad = 3; // 3 points + static const int kConic = 4; // 3 points + 1 weight + static const int kCubic = 5; // 4 points + static const int kClose = 6; // 0 points +} + +class SPath { + static const int kMoveVerb = SPathVerb.kMove; + static const int kLineVerb = SPathVerb.kLine; + static const int kQuadVerb = SPathVerb.kQuad; + static const int kConicVerb = SPathVerb.kConic; + static const int kCubicVerb = SPathVerb.kCubic; + static const int kCloseVerb = SPathVerb.kClose; + static const int kDoneVerb = SPathVerb.kClose + 1; + + static const int kLineSegmentMask = SPathSegmentMask.kLine_SkPathSegmentMask; + static const int kQuadSegmentMask = SPathSegmentMask.kQuad_SkPathSegmentMask; + static const int kConicSegmentMask = + SPathSegmentMask.kConic_SkPathSegmentMask; + static const int kCubicSegmentMask = + SPathSegmentMask.kCubic_SkPathSegmentMask; + + static const double scalarNearlyZero = 1.0 / (1 << 12); + + /// Square root of 2 divided by 2. Useful for sin45 = cos45 = 1/sqrt(2). + static const double scalarRoot2Over2 = 0.707106781; + + /// True if (a <= b <= c) || (a >= b >= c) + static bool between(double a, double b, double c) { + return (a - b) * (c - b) <= 0; + } + + /// Returns -1 || 0 || 1 depending on the sign of value: + /// -1 if x < 0 + /// 0 if x == 0 + /// 1 if x > 0 + static int scalarSignedAsInt(double x) { + return x < 0 ? -1 : ((x > 0) ? 1 : 0); + } +} + +class SPathAddPathMode { + // Append to destination unaltered. + static const int kAppend = 0; + // Add line if prior contour is not closed. + static const int kExtend = 1; +} + +class SPathDirection { + /// Uninitialized value for empty paths. + static const int kUnknown = -1; + + /// clockwise direction for adding closed contours. + static const int kCW = 0; + + /// counter-clockwise direction for adding closed contours. + static const int kCCW = 1; +} + +class SPathConvexityType { + static const int kUnknown = -1; + static const int kConvex = 0; + static const int kConcave = 1; +} + +class SPathSegmentState { + /// The current contour is empty. Starting processing or have just closed + /// a contour. + static const int kEmptyContour = 0; + + /// Have seen a move, but nothing else. + static const int kAfterMove = 1; + + /// Have seen a primitive but not yet closed the path. Also the initial state. + static const int kAfterPrimitive = 2; +} + +/// Quadratic roots. See Numerical Recipes in C. +/// +/// Q = -1/2 (B + sign(B) sqrt[B*B - 4*A*C]) +/// x1 = Q / A +/// x2 = C / Q +class _QuadRoots { + double? root0; + double? root1; + + _QuadRoots(); + + /// Returns roots as list. + List get roots => (root0 == null) + ? [] + : (root1 == null ? [root0!] : [root0!, root1!]); + + int findRoots(double a, double b, double c) { + int rootCount = 0; + if (a == 0) { + root0 = _validUnitDivide(-c, b); + return root0 == null ? 0 : 1; + } + + double dr = b * b - 4 * a * c; + if (dr < 0) { + return 0; + } + dr = math.sqrt(dr); + if (!dr.isFinite) { + return 0; + } + + double q = (b < 0) ? -(b - dr) / 2 : -(b + dr) / 2; + double? res = _validUnitDivide(q, a); + if (res != null) { + root0 = res; + ++rootCount; + } + res = _validUnitDivide(c, q); + if (res != null) { + if (rootCount == 0) { + root0 = res; + ++rootCount; + } else { + root1 = res; + ++rootCount; + } + } + if (rootCount == 2) { + if (root0! > root1!) { + final double swap = root0!; + root0 = root1; + root1 = swap; + } else if (root0 == root1) { + return 1; // skip the double root + } + } + return rootCount; + } +} + +double? _validUnitDivide(double numer, double denom) { + if (numer < 0) { + numer = -numer; + denom = -denom; + } + if (denom == 0 || numer == 0 || numer >= denom) { + return null; + } + final double r = numer / denom; + if (r.isNaN) { + return null; + } + if (r == 0) { + // catch underflow if numer <<<< denom + return null; + } + return r; +} + +// Snaps a value to zero if almost zero (within tolerance). +double _snapToZero(double value) => _nearlyEqual(value, 0.0) ? 0.0 : value; + +bool _nearlyEqual(double value1, double value2) => + (value1 - value2).abs() < SPath.scalarNearlyZero; + +bool _isInteger(double value) => value.floor() == value; + +bool _isRRectOval(ui.RRect rrect) { + if ((rrect.tlRadiusX + rrect.trRadiusX) != rrect.width) { + return false; + } + if ((rrect.tlRadiusY + rrect.trRadiusY) != rrect.height) { + return false; + } + if (rrect.tlRadiusX != rrect.blRadiusX || + rrect.trRadiusX != rrect.brRadiusX || + rrect.tlRadiusY != rrect.blRadiusY || + rrect.trRadiusY != rrect.brRadiusY) { + return false; + } + return true; +} + +/// Evaluates degree 2 polynomial (quadratic). +double polyEval(double A, double B, double C, double t) => (A * t + B) * t + C; + +/// Evaluates degree 3 polynomial (cubic). +double polyEval4(double A, double B, double C, double D, double t) => + ((A * t + B) * t + C) * t + D; + +// Interpolate between two doubles (Not using lerpDouble here since it null +// checks and treats values as 0). +double _interpolate(double startValue, double endValue, double t) => + (startValue * (1 - t)) + endValue * t; + +double _dotProduct(double x0, double y0, double x1, double y1) { + return x0 * x1 + y0 * y1; +} + +// Helper class for computing convexity for a single contour. +// +// Iteratively looks at angle (using cross product) between consecutive vectors +// formed by path. +class Convexicator { + static const int kValueNeverReturnedBySign = 2; + + // Second point of contour start that forms a vector. + // Used to handle close operator to compute angle between last vector and + // first. + double? firstVectorEndPointX; + double? firstVectorEndPointY; + + double? priorX; + double? priorY; + + double? lastX; + double? lastY; + + double? currX; + double? currY; + + // Last vector to use to compute angle. + double? lastVecX; + double? lastVecY; + + bool _isFinite = true; + int _firstDirection = SPathDirection.kUnknown; + int _reversals = 0; + + /// SPathDirection of contour. + int get firstDirection => _firstDirection; + + DirChange _expectedDirection = DirChange.kInvalid; + + void setMovePt(double x, double y) { + currX = priorX = lastX = x; + currY = priorY = lastY = y; + } + + bool addPoint(double x, double y) { + if (x == currX && y == currY) { + // Skip zero length vector. + return true; + } + currX = x; + currY = y; + final double vecX = currX! - lastX!; + final double vecY = currY! - lastY!; + if (priorX == lastX && priorY == lastY) { + // First non-zero vector. + lastVecX = vecX; + lastVecY = vecY; + firstVectorEndPointX = x; + firstVectorEndPointY = y; + } else if (!_addVector(vecX, vecY)) { + return false; + } + priorX = lastX; + priorY = lastY; + lastX = x; + lastY = y; + return true; + } + + bool close() { + // Add another point from path closing point to end of first vector. + return addPoint(firstVectorEndPointX!, firstVectorEndPointY!); + } + + bool get isFinite => _isFinite; + + int get reversals => _reversals; + + DirChange _directionChange(double curVecX, double curVecY) { + // Cross product = ||lastVec|| * ||curVec|| * sin(theta) * N + // sin(theta) angle between two vectors is positive for angles 0..180 and + // negative for greater, providing left or right direction. + double lastX = lastVecX!; + double lastY = lastVecY!; + double cross = lastX * curVecY - lastY * curVecX; + if (!cross.isFinite) { + return DirChange.kUnknown; + } + // Detect straight and backwards direction change. + // Instead of comparing absolute crossproduct size, compare + // largest component double+crossproduct. + final double smallest = + math.min(curVecX, math.min(curVecY, math.min(lastX, lastY))); + final double largest = math.max( + math.max(curVecX, math.max(curVecY, math.max(lastX, lastY))), + -smallest); + if (_nearlyEqual(largest, largest + cross)) { + final double nearlyZeroSquared = + SPath.scalarNearlyZero * SPath.scalarNearlyZero; + if (_nearlyEqual(_lengthSquared(lastX, lastY), nearlyZeroSquared) || + _nearlyEqual(_lengthSquared(curVecX, curVecY), nearlyZeroSquared)) { + // Length of either vector is smaller than tolerance to be able + // to compute direction. + return DirChange.kUnknown; + } + // The vectors are parallel, sign of dot product gives us direction. + // cosine is positive for straight -90 < Theta < 90 + return _dotProduct(lastX, lastY, curVecX, curVecY) < 0 + ? DirChange.kBackwards + : DirChange.kStraight; + } + return cross > 0 ? DirChange.kRight : DirChange.kLeft; + } + + bool _addVector(double curVecX, double curVecY) { + DirChange dir = _directionChange(curVecX, curVecY); + final bool isDirectionRight = dir == DirChange.kRight; + if (dir == DirChange.kLeft || isDirectionRight) { + if (_expectedDirection == DirChange.kInvalid) { + // First valid direction. From this point on expect always left. + _expectedDirection = dir; + _firstDirection = + isDirectionRight ? SPathDirection.kCW : SPathDirection.kCCW; + } else if (dir != _expectedDirection) { + _firstDirection = SPathDirection.kUnknown; + return false; + } + lastVecX = curVecX; + lastVecY = curVecY; + } else { + switch (dir) { + case DirChange.kBackwards: + // Allow path to reverse direction twice. + // Given path.moveTo(0,0) lineTo(1,1) + // - First reversal: direction change formed by line (0,0 1,1), + // line (1,1 0,0) + // - Second reversal: direction change formed by line (1,1 0,0), + // line (0,0 1,1) + lastVecX = curVecX; + lastVecY = curVecY; + return ++_reversals < 3; + case DirChange.kUnknown: + return _isFinite = false; + default: + break; + } + } + return true; + } + + // Quick test to detect concave by looking at number of changes in direction + // of vectors formed by path points (excluding control points). + static int bySign(PathRef pathRef, int pointIndex, int numPoints) { + int lastPointIndex = pointIndex + numPoints; + int currentPoint = pointIndex++; + int firstPointIndex = currentPoint; + int signChangeCountX = 0; + int signChangeCountY = 0; + int lastSx = kValueNeverReturnedBySign; + int lastSy = kValueNeverReturnedBySign; + for (int outerLoop = 0; outerLoop < 2; ++outerLoop) { + while (pointIndex != lastPointIndex) { + double vecX = pathRef._fPoints[pointIndex * 2] - + pathRef._fPoints[currentPoint * 2]; + double vecY = pathRef._fPoints[pointIndex * 2 + 1] - + pathRef._fPoints[currentPoint * 2 + 1]; + if (!(vecX == 0 && vecY == 0)) { + // Give up if vector construction failed. + // give up if vector construction failed + if (!(vecX.isFinite && vecY.isFinite)) { + return SPathConvexityType.kUnknown; + } + int sx = vecX < 0 ? 1 : 0; + int sy = vecY < 0 ? 1 : 0; + signChangeCountX += (sx != lastSx) ? 1 : 0; + signChangeCountY += (sy != lastSy) ? 1 : 0; + if (signChangeCountX > 3 || signChangeCountY > 3) { + return SPathConvexityType.kConcave; + } + lastSx = sx; + lastSy = sy; + } + currentPoint = pointIndex++; + if (outerLoop != 0) { + break; + } + } + pointIndex = firstPointIndex; + } + return SPathConvexityType.kConvex; + } +} + +enum DirChange { + kUnknown, + kLeft, + kRight, + kStraight, + kBackwards, // if double back, allow simple lines to be convex + kInvalid +} diff --git a/lib/web_ui/lib/src/engine/surface/path/path_windings.dart b/lib/web_ui/lib/src/engine/surface/path/path_windings.dart new file mode 100644 index 000000000000..5f6be800b346 --- /dev/null +++ b/lib/web_ui/lib/src/engine/surface/path/path_windings.dart @@ -0,0 +1,544 @@ +// 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. + +part of engine; + +/// 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) { + 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; + } + + _QuadRoots quadRoots = _QuadRoots(); + final int n = quadRoots.findRoots(startY - 2 * y1 + endY, 2 * (y1 - startY), endY - 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 (_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]; + 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. + double p01x = x0 + (tValueAtExtrema * (x1 - x0)); + double p01y = y0 + (tValueAtExtrema * (y1 - y0)); + double p12x = x1 + (tValueAtExtrema * (x2 - x1)); + double p12y = y1 + (tValueAtExtrema * (y2 - y1)); + double cx = p01x + (tValueAtExtrema * (p12x - p01x)); + 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) { + 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. + bool isMono = _isQuadMonotonic(_buffer); + List conics = []; + 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(); + 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 = + _conicEvalNumerator(conic.p0x, conic.p1x, conic.p2x, conic.fW, root) / + _conicEvalDenominator(conic.fW, root); + } + if (_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() { + 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. + double? t = _chopMonoAtY(_buffer, bufferStartPos, y); + if (t == null) { + return; + } + double xt = _evalCubicPts(px0, px1, px2, px3, t); + if (_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; + } +} + +// Iterates through path including generating closing segments. +class PathIterator { + PathIterator(this.pathRef, bool forceClose) + : _forceClose = forceClose, + _verbCount = pathRef.countVerbs() { + _pointIndex = 0; + if (!pathRef.isFinite) { + // Don't allow iteration through non-finite points, prepare to return + // done verb. + _verbIndex = pathRef.countVerbs(); + } + } + + final PathRef pathRef; + final bool _forceClose; + final int _verbCount; + + bool _needClose = false; + int _segmentState = SPathSegmentState.kEmptyContour; + int _conicWeightIndex = -1; + double _lastPointX = 0; + double _lastPointY = 0; + double _moveToX = 0; + double _moveToY = 0; + int _verbIndex = 0; + int _pointIndex = 0; + + /// Returns true if first contour on path is closed. + bool isClosedContour() { + if (_verbCount == 0 || _verbIndex == _verbCount) { + return false; + } + if (_forceClose) { + return true; + } + int verbIndex = 0; + // Skip starting moveTo. + if (pathRef.atVerb(verbIndex) == SPath.kMoveVerb) { + ++verbIndex; + } + while (verbIndex < _verbCount) { + int verb = pathRef.atVerb(verbIndex++); + if (SPath.kMoveVerb == verb) { + break; + } + if (SPath.kCloseVerb == verb) { + return true; + } + } + return false; + } + + int _autoClose(Float32List outPts) { + if (_lastPointX != _moveToX || _lastPointY != _moveToY) { + // Handle special case where comparison above will return true for + // NaN != NaN although it should be false. + if (_lastPointX.isNaN || + _lastPointY.isNaN || + _moveToX.isNaN || + _moveToY.isNaN) { + return SPath.kCloseVerb; + } + outPts[0] = _lastPointX; + outPts[1] = _lastPointY; + outPts[2] = _moveToX; + outPts[3] = _moveToY; + _lastPointX = _moveToX; + _lastPointY = _moveToY; + return SPath.kLineVerb; + } else { + outPts[0] = _moveToX; + outPts[1] = _moveToY; + return SPath.kCloseVerb; + } + } + + // Returns true if caller should use moveTo, false if last point of + // previous primitive. + ui.Offset _constructMoveTo() { + if (_segmentState == SPathSegmentState.kAfterMove) { + // Set the first return point to move point. + _segmentState = SPathSegmentState.kAfterPrimitive; + return ui.Offset(_moveToX, _moveToY); + } + return ui.Offset( + pathRef.points[_pointIndex - 2], pathRef.points[_pointIndex - 1]); + } + + int peek() => pathRef._fVerbs[_verbIndex]; + + // Returns next verb and reads associated points into [outPts]. + int next(Float32List outPts) { + if (_verbIndex == pathRef.countVerbs()) { + // Close the curve if requested and if there is some curve to close + if (_needClose && _segmentState == SPathSegmentState.kAfterPrimitive) { + if (SPath.kLineVerb == _autoClose(outPts)) { + return SPath.kLineVerb; + } + _needClose = false; + return SPath.kCloseVerb; + } + return SPath.kDoneVerb; + } + int verb = pathRef._fVerbs[_verbIndex++]; + switch (verb) { + case SPath.kMoveVerb: + if (_needClose) { + // Move back one verb. + _verbIndex--; + final int autoVerb = _autoClose(outPts); + if (autoVerb == SPath.kCloseVerb) { + _needClose = false; + } + return autoVerb; + } + if (_verbIndex == _verbCount) { + return SPath.kDoneVerb; + } + double offsetX = pathRef.points[_pointIndex++]; + double offsetY = pathRef.points[_pointIndex++]; + _moveToX = offsetX; + _moveToY = offsetY; + outPts[0] = offsetX; + outPts[1] = offsetY; + _segmentState = SPathSegmentState.kAfterMove; + _lastPointX = _moveToX; + _lastPointY = _moveToY; + _needClose = _forceClose; + break; + case SPath.kLineVerb: + final ui.Offset start = _constructMoveTo(); + double offsetX = pathRef.points[_pointIndex++]; + double offsetY = pathRef.points[_pointIndex++]; + outPts[0] = start.dx; + outPts[1] = start.dy; + outPts[2] = offsetX; + outPts[3] = offsetY; + _lastPointX = offsetX; + _lastPointY = offsetY; + break; + case SPath.kConicVerb: + _conicWeightIndex++; + final ui.Offset start = _constructMoveTo(); + outPts[0] = start.dx; + outPts[1] = start.dy; + outPts[2] = pathRef.points[_pointIndex++]; + outPts[3] = pathRef.points[_pointIndex++]; + _lastPointX = outPts[4] = pathRef.points[_pointIndex++]; + _lastPointY = outPts[5] = pathRef.points[_pointIndex++]; + break; + case SPath.kQuadVerb: + final ui.Offset start = _constructMoveTo(); + outPts[0] = start.dx; + outPts[1] = start.dy; + outPts[2] = pathRef.points[_pointIndex++]; + outPts[3] = pathRef.points[_pointIndex++]; + _lastPointX = outPts[4] = pathRef.points[_pointIndex++]; + _lastPointY = outPts[5] = pathRef.points[_pointIndex++]; + break; + case SPath.kCubicVerb: + final ui.Offset start = _constructMoveTo(); + outPts[0] = start.dx; + outPts[1] = start.dy; + outPts[2] = pathRef.points[_pointIndex++]; + outPts[3] = pathRef.points[_pointIndex++]; + outPts[4] = pathRef.points[_pointIndex++]; + outPts[5] = pathRef.points[_pointIndex++]; + _lastPointX = outPts[6] = pathRef.points[_pointIndex++]; + _lastPointY = outPts[7] = pathRef.points[_pointIndex++]; + break; + case SPath.kCloseVerb: + verb = _autoClose(outPts); + if (verb == SPath.kLineVerb) { + // Move back one verb since we constructed line for this close verb. + _verbIndex--; + } else { + _needClose = false; + _segmentState = SPathSegmentState.kEmptyContour; + } + _lastPointX = _moveToX; + _lastPointY = _moveToY; + break; + case SPath.kDoneVerb: + assert(_verbIndex == pathRef.countVerbs()); + break; + default: + throw FormatException('Unsupport Path verb $verb'); + } + return verb; + } + + double get conicWeight => pathRef.atWeight(_conicWeightIndex); +} diff --git a/lib/web_ui/lib/src/engine/surface/path/tangent.dart b/lib/web_ui/lib/src/engine/surface/path/tangent.dart new file mode 100644 index 000000000000..305a1dc400d6 --- /dev/null +++ b/lib/web_ui/lib/src/engine/surface/path/tangent.dart @@ -0,0 +1,198 @@ +// 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. + +part of engine; + +/// Computes tangent at point x,y on a line. +void tangentLine( + Float32List pts, double x, double y, List tangents) { + final double y0 = pts[1]; + final double y1 = pts[3]; + if (!SPath.between(y0, y, y1)) { + return; + } + final double x0 = pts[0]; + final double x1 = pts[2]; + if (!SPath.between(x0, x, x1)) { + return; + } + final double dx = x1 - x0; + final double dy = y1 - y0; + if (!_nearlyEqual((x - x0) * dy, dx * (y - y0))) { + return; + } + tangents.add(ui.Offset(dx, dy)); +} + +/// Computes tangent at point x,y on a quadratic curve. +void tangentQuad( + Float32List pts, double x, double y, List tangents) { + final double y0 = pts[1]; + final double y1 = pts[3]; + final double y2 = pts[5]; + if (!SPath.between(y0, y, y1) && !SPath.between(y1, y, y2)) { + return; + } + final double x0 = pts[0]; + final double x1 = pts[2]; + final double x2 = pts[4]; + if (!SPath.between(x0, x, x1) && !SPath.between(x1, x, x2)) { + return; + } + final _QuadRoots roots = _QuadRoots(); + int n = roots.findRoots(y0 - 2 * y1 + y2, 2 * (y1 - y0), y0 - y); + for (int index = 0; index < n; ++index) { + double t = index == 0 ? roots.root0! : roots.root1!; + double C = x0; + double A = x2 - 2 * x1 + C; + double B = 2 * (x1 - C); + double xt = polyEval(A, B, C, t); + if (!_nearlyEqual(x, xt)) { + continue; + } + tangents.add(_evalQuadTangentAt(x0, y0, x1, y1, x2, y2, t)); + } +} + +ui.Offset _evalQuadTangentAt(double x0, double y0, double x1, double y1, + double x2, double y2, double t) { + // The derivative of a quad equation is 2(b - a +(a - 2b +c)t). + // This returns a zero tangent vector when t is 0 or 1, and the control + // point is equal to the end point. In this case, use the quad end points to + // compute the tangent. + + if ((t == 0 && x0 == x1 && y0 == y1) || (t == 1 && x1 == x2 && y1 == y2)) { + return ui.Offset(x2 - x0, y2 - y0); + } + assert(t >= 0 && t <= 1.0); + + double bx = x1 - x0; + double by = y1 - y0; + double ax = x2 - x1 - bx; + double ay = y2 - y1 - by; + double tx = ax * t + bx; + double ty = ay * t + by; + return ui.Offset(tx * 2, ty * 2); +} + +/// Computes tangent at point x,y on a conic curve. +void tangentConic(Float32List pts, double x, double y, double weight, + List tangents) { + final double y0 = pts[1]; + final double y1 = pts[3]; + final double y2 = pts[5]; + if (!SPath.between(y0, y, y1) && !SPath.between(y1, y, y2)) { + return; + } + final double x0 = pts[0]; + final double x1 = pts[2]; + final double x2 = pts[4]; + if (!SPath.between(x0, x, x1) && !SPath.between(x1, x, x2)) { + return; + } + // Check extrema. + double A = y2; + double B = y1 * weight - y * weight + y; + double C = y0; + // 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(); + int n = quadRoots.findRoots(A, 2 * B, C); + for (int index = 0; index < n; ++index) { + double t = index == 0 ? quadRoots.root0! : quadRoots.root1!; + double xt = _conicEvalNumerator(x0, x1, x2, weight, t) / + _conicEvalDenominator(weight, t); + if (!_nearlyEqual(x, xt)) { + continue; + } + Conic conic = Conic(x0, y0, x1, y1, x2, y2, weight); + tangents.add(conic.evalTangentAt(t)); + } +} + +/// Computes tangent at point x,y on a cubic curve. +void tangentCubic( + Float32List pts, double x, double y, List tangents) { + final double y3 = pts[7]; + final double y0 = pts[1]; + final double y1 = pts[3]; + final double y2 = pts[5]; + if (!SPath.between(y0, y, y1) && + !SPath.between(y1, y, y2) && + !SPath.between(y2, y, y3)) { + return; + } + final double x0 = pts[0]; + final double x1 = pts[2]; + final double x2 = pts[4]; + final double x3 = pts[6]; + if (!SPath.between(x0, x, x1) && + !SPath.between(x1, x, x2) && + !SPath.between(x2, x, x3)) { + return; + } + final Float32List dst = Float32List(20); + int n = _chopCubicAtYExtrema(pts, dst); + for (int i = 0; i <= n; ++i) { + int bufferPos = i * 6; + double? t = _chopMonoAtY(dst, i * 6, y); + if (t == null) { + continue; + } + double xt = _evalCubicPts(dst[bufferPos], dst[bufferPos + 2], + dst[bufferPos + 4], dst[bufferPos + 6], t); + if (!_nearlyEqual(x, xt)) { + continue; + } + tangents.add(_evalCubicTangentAt(dst, bufferPos, t)); + } +} + +ui.Offset _evalCubicTangentAt(Float32List points, int bufferPos, double t) { + assert(t >= 0 && t <= 1.0); + final double y3 = points[7 + bufferPos]; + final double y0 = points[1 + bufferPos]; + final double y1 = points[3 + bufferPos]; + final double y2 = points[5 + bufferPos]; + final double x0 = points[0 + bufferPos]; + final double x1 = points[2 + bufferPos]; + final double x2 = points[4 + bufferPos]; + final double x3 = points[6 + bufferPos]; + // The derivative equation returns a zero tangent vector when t is 0 or 1, + // and the adjacent control point is equal to the end point. In this case, + // use the next control point or the end points to compute the tangent. + if ((t == 0 && x0 == x1 && y0 == y1) || (t == 1 && x2 == x3 && y2 == y3)) { + double dx, dy; + if (t == 0) { + dx = x2 - x0; + dy = y2 - y0; + } else { + dx = x3 - x1; + dy = y3 - y1; + } + if (dx == 0 && dy == 0) { + dx = x3 - x0; + dy = y3 - y0; + } + return ui.Offset(dx, dy); + } else { + return _evalCubicDerivative(x0, y0, x1, y1, x2, y2, x3, y3, t); + } +} + +ui.Offset _evalCubicDerivative(double x0, double y0, double x1, double y1, + double x2, double y2, double x3, double y3, double t) { + final _SkQuadCoefficients coeff = _SkQuadCoefficients( + x3 + 3 * (x1 - x2) - x0, + y3 + 3 * (y1 - y2) - y0, + 2 * (x2 - (2 * x1) + x0), + 2 * (y2 - (2 * y1) + y0), + x1 - x0, + y1 - y0, + ); + return ui.Offset(coeff.evalX(t), coeff.evalY(t)); +} diff --git a/lib/web_ui/lib/src/engine/surface/recording_canvas.dart b/lib/web_ui/lib/src/engine/surface/recording_canvas.dart index 7ae1c8edcb2e..853fcd18072e 100644 --- a/lib/web_ui/lib/src/engine/surface/recording_canvas.dart +++ b/lib/web_ui/lib/src/engine/surface/recording_canvas.dart @@ -466,7 +466,7 @@ class RecordingCanvas { } } SurfacePath sPath = path as SurfacePath; - if (sPath.subpaths.isNotEmpty) { + if (!sPath.pathRef.isEmpty) { _hasArbitraryPaint = true; _didDraw = true; ui.Rect pathBounds = sPath.getBounds(); @@ -1099,12 +1099,18 @@ class PaintDrawDRRect extends DrawCommand { final ui.RRect outer; final ui.RRect inner; final SurfacePaintData paint; - - PaintDrawDRRect(this.outer, this.inner, this.paint); + ui.Path? path; + PaintDrawDRRect(this.outer, this.inner, this.paint) { + path = ui.Path() + ..fillType = ui.PathFillType.evenOdd + ..addRRect(outer) + ..addRRect(inner) + ..close(); + } @override void apply(EngineCanvas? canvas) { - canvas!.drawDRRect(outer, inner, paint); + canvas!.drawPath(path!, paint); } @override @@ -1570,7 +1576,7 @@ class Ellipse extends PathCommand { anticlockwise ? startAngle - endAngle : endAngle - startAngle, matrix4, bezierPath); - targetPath._addPathWithMatrix(bezierPath as SurfacePath, 0, 0, matrix4); + targetPath._addPath(bezierPath, 0, 0, matrix4, SPathAddPathMode.kAppend); } void _drawArcWithBezier( @@ -1848,7 +1854,7 @@ class RRectCommand extends PathCommand { void transform(Float32List matrix4, SurfacePath targetPath) { final ui.Path roundRectPath = ui.Path(); _RRectToPathRenderer(roundRectPath).render(rrect); - targetPath._addPathWithMatrix(roundRectPath as SurfacePath, 0, 0, matrix4); + targetPath._addPath(roundRectPath, 0, 0, matrix4, SPathAddPathMode.kAppend); } @override diff --git a/lib/web_ui/test/engine/path_metrics_test.dart b/lib/web_ui/test/engine/path_metrics_test.dart index b31a452b6a9c..f4181f3917a4 100644 --- a/lib/web_ui/test/engine/path_metrics_test.dart +++ b/lib/web_ui/test/engine/path_metrics_test.dart @@ -2,7 +2,6 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -// @dart = 2.6 import 'dart:math' as math; import 'package:ui/ui.dart'; @@ -103,7 +102,7 @@ void main() { path.addRRect(RRect.fromLTRBR(20, 30, 220, 130, Radius.elliptical(8, 4))); final List contourLengths = computeLengths(path.computeMetrics()); expect(contourLengths.length, 1); - expect(contourLengths[0], within(distance: kTolerance, from: 590.361)); + expect(contourLengths[0], within(distance: kTolerance, from: 590.408)); }); test('arcToPoint < 90 degrees', () { @@ -132,7 +131,7 @@ void main() { rotation: 0.0); final List contourLengths = computeLengths(path.computeMetrics()); expect(contourLengths.length, 1); - expect(contourLengths[0], within(distance: kTolerance, from: 156.994)); + expect(contourLengths[0], within(distance: kTolerance, from: 156.827)); }); test('arcToPoint 180 degrees', () { @@ -161,7 +160,7 @@ void main() { rotation: 0.0); final List contourLengths = computeLengths(path.computeMetrics()); expect(contourLengths.length, 1); - expect(contourLengths[0], within(distance: kTolerance, from: 313.989)); + expect(contourLengths[0], within(distance: kTolerance, from: 313.654)); }); test('arcToPoint 270 degrees', () { @@ -190,7 +189,7 @@ void main() { rotation: 0.0); final List contourLengths = computeLengths(path.computeMetrics()); expect(contourLengths.length, 1); - expect(contourLengths[0], within(distance: kTolerance, from: 470.983)); + expect(contourLengths[0], within(distance: kTolerance, from: 470.482)); }); test('arcToPoint 270 degrees rx!=ry', () { @@ -219,7 +218,7 @@ void main() { rotation: 0.0); final List contourLengths = computeLengths(path.computeMetrics()); expect(contourLengths.length, 1); - expect(contourLengths[0], within(distance: kTolerance, from: 363.090)); + expect(contourLengths[0], within(distance: kTolerance, from: 362.733)); }); }); } diff --git a/lib/web_ui/test/engine/recording_canvas_test.dart b/lib/web_ui/test/engine/recording_canvas_test.dart index 5610c9c1f4de..3e5d4a884d80 100644 --- a/lib/web_ui/test/engine/recording_canvas_test.dart +++ b/lib/web_ui/test/engine/recording_canvas_test.dart @@ -30,9 +30,8 @@ void main() { underTest.apply(mockCanvas, screenRect); _expectDrawDRRectCall(mockCanvas, { - 'outer': rrect, - 'inner': rrect.deflate(1), - 'paint': somePaint.paintData, + 'path': 'Path(MoveTo(10, 47) LineTo(10, 13) Conic(10, 10, 10, 13, w = 0.7071067690849304) LineTo(47, 10) Conic(50, 10, 10, 50, w = 0.7071067690849304) LineTo(50, 47) Conic(50, 50, 50, 47, w = 0.7071067690849304) LineTo(13, 50) Conic(10, 50, 50, 10, w = 0.7071067690849304) Close() MoveTo(11, 47) LineTo(11, 13) Conic(11, 11, 11, 13, w = 0.7071067690849304) LineTo(47, 11) Conic(49, 11, 11, 49, w = 0.7071067690849304) LineTo(49, 47) Conic(49, 49, 49, 47, w = 0.7071067690849304) LineTo(13, 49) Conic(11, 49, 49, 11, w = 0.7071067690849304) Close())', + 'paint': somePaint.paintData, }); }); @@ -82,8 +81,7 @@ void main() { // Expect to draw, even when inner has negative radii (which get ignored by canvas) _expectDrawDRRectCall(mockCanvas, { - 'outer': outer, - 'inner': inner, + 'path': 'Path(MoveTo(0, 42) LineTo(0, 6) Conic(0, 0, 0, 6, w = 0.7071067690849304) LineTo(88, 0) Conic(88, 0, 0, 88, w = 0.7071067690849304) LineTo(88, 48) Conic(88, 48, 48, 88, w = 0.7071067690849304) LineTo(6, 48) Conic(0, 48, 48, 0, w = 0.7071067690849304) Close() MoveTo(1, 42) LineTo(1, 6) Conic(1, 1, 1, 6, w = 0.7071067690849304) LineTo(87, 1) Conic(87, 1, 1, 87, w = 0.7071067690849304) LineTo(87, 47) Conic(87, 47, 47, 87, w = 0.7071067690849304) LineTo(6, 47) Conic(1, 47, 47, 1, w = 0.7071067690849304) Close())', 'paint': somePaint.paintData, }); }); @@ -99,8 +97,7 @@ void main() { underTest.apply(mockCanvas, screenRect); _expectDrawDRRectCall(mockCanvas, { - 'outer': outer, - 'inner': inner, + 'path': 'Path(MoveTo(10, 20) LineTo(30, 20) LineTo(30, 40) LineTo(10, 40) Close() MoveTo(12, 22) LineTo(28, 22) LineTo(28, 38) LineTo(12, 38) Close())', 'paint': somePaint.paintData, }); }); @@ -197,6 +194,11 @@ void _expectDrawDRRectCall( MockEngineCanvas mock, Map expectedArguments) { expect(mock.methodCallLog.length, equals(2)); MockCanvasCall mockCall = mock.methodCallLog[0]; - expect(mockCall.methodName, equals('drawDRRect')); - expect(mockCall.arguments, equals(expectedArguments)); + expect(mockCall.methodName, equals('drawPath')); + Map argMap = mockCall.arguments as Map; + Map argContents = {}; + argMap.forEach((String key, dynamic value) { + argContents[key] = value is SurfacePath ? value.toString() : value; + }); + expect(argContents, equals(expectedArguments)); } diff --git a/lib/web_ui/test/golden_tests/engine/path_to_svg_golden_test.dart b/lib/web_ui/test/golden_tests/engine/path_to_svg_golden_test.dart index bbfd9df35fc6..f657f36a5ae0 100644 --- a/lib/web_ui/test/golden_tests/engine/path_to_svg_golden_test.dart +++ b/lib/web_ui/test/golden_tests/engine/path_to_svg_golden_test.dart @@ -14,7 +14,7 @@ import 'package:web_engine_tester/golden_tester.dart'; void main() async { final Rect region = Rect.fromLTWH(8, 8, 600, 800); // Compensate for old scuba tester padding - Future testPath(Path path, String scubaFileName, {Paint paint}) async { + Future testPath(Path path, String scubaFileName, {Paint paint, double maxDiffRatePercent = null}) async { const Rect canvasBounds = Rect.fromLTWH(0, 0, 600, 800); final BitmapCanvas bitmapCanvas = BitmapCanvas(canvasBounds); final RecordingCanvas canvas = RecordingCanvas(canvasBounds); @@ -40,7 +40,7 @@ void main() async { canvas.endRecording(); canvas.apply(bitmapCanvas, canvasBounds); - await matchGoldenFile('$scubaFileName.png', region: region); + await matchGoldenFile('$scubaFileName.png', region: region, maxDiffRatePercent: maxDiffRatePercent); bitmapCanvas.rootElement.remove(); svgElement.remove(); @@ -106,7 +106,7 @@ void main() async { final Path path = Path(); path.addRect(const Rect.fromLTRB(15, 15, 60, 20)); path.addRect(const Rect.fromLTRB(35, 160, 15, 100)); - await testPath(path, 'svg_rect'); + await testPath(path, 'svg_rect', maxDiffRatePercent: 1.0); }); test('render notch', () async { diff --git a/lib/web_ui/test/golden_tests/engine/path_transform_test.dart b/lib/web_ui/test/golden_tests/engine/path_transform_test.dart index 89164c9712ad..8e85871af3b0 100644 --- a/lib/web_ui/test/golden_tests/engine/path_transform_test.dart +++ b/lib/web_ui/test/golden_tests/engine/path_transform_test.dart @@ -20,6 +20,7 @@ void main() async { // Commit a recording canvas to a bitmap, and compare with the expected Future _checkScreenshot(RecordingCanvas rc, String fileName, {Rect region = const Rect.fromLTWH(0, 0, 500, 500), + double maxDiffRatePercent = null, bool write = false}) async { final EngineCanvas engineCanvas = BitmapCanvas(screenRect); rc.endRecording(); @@ -30,7 +31,7 @@ void main() async { try { sceneElement.append(engineCanvas.rootElement); html.document.body.append(sceneElement); - await matchGoldenFile('$fileName.png', region: region); + await matchGoldenFile('$fileName.png', region: region, maxDiffRatePercent: maxDiffRatePercent); } finally { // The page is reused across tests, so remove the element after taking the // Scuba screenshot. @@ -190,7 +191,8 @@ void main() async { ..style = PaintingStyle.stroke ..strokeWidth = 2.0 ..color = const Color.fromRGBO(0, 128, 255, 1.0)); - await _checkScreenshot(rc, 'path_transform_with_arc'); + await _checkScreenshot(rc, 'path_transform_with_arc', + maxDiffRatePercent: 1.4); }); test('Should draw transformed rrect.', () async { diff --git a/lib/web_ui/test/golden_tests/engine/recording_canvas_golden_test.dart b/lib/web_ui/test/golden_tests/engine/recording_canvas_golden_test.dart index c974485c1263..618c7e9ac973 100644 --- a/lib/web_ui/test/golden_tests/engine/recording_canvas_golden_test.dart +++ b/lib/web_ui/test/golden_tests/engine/recording_canvas_golden_test.dart @@ -641,7 +641,7 @@ void main() async { await matchGoldenFile( 'paint_spread_bounds.png', region: const Rect.fromLTRB(0, 0, 250, 600), - maxDiffRatePercent: 0.0, + maxDiffRatePercent: 0.01, pixelComparison: PixelComparison.precise, ); } finally { diff --git a/lib/web_ui/test/path_test.dart b/lib/web_ui/test/path_test.dart index 08de1029fa16..4eb93ef271ed 100644 --- a/lib/web_ui/test/path_test.dart +++ b/lib/web_ui/test/path_test.dart @@ -2,9 +2,9 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -// @dart = 2.6 import 'dart:js_util' as js_util; import 'dart:html' as html; +import 'dart:typed_data'; import 'package:test/test.dart'; import 'package:ui/ui.dart' hide window; import 'package:ui/src/engine.dart'; @@ -12,307 +12,522 @@ import 'package:ui/src/engine.dart'; import 'matchers.dart'; void main() { - test('Should have no subpaths when created', () { - final SurfacePath path = SurfacePath(); - expect(path.subpaths.length, 0); + group('Path', () { + test('Should have no subpaths when created', () { + final SurfacePath path = SurfacePath(); + expect(path.isEmpty, true); + }); + + test('LineTo should add command', () { + final SurfacePath path = SurfacePath(); + path.moveTo(5.0, 10.0); + path.lineTo(20.0, 40.0); + path.lineTo(30.0, 50.0); + expect(path.pathRef.countPoints(), 3); + expect(path.pathRef.atPoint(2).dx, 30.0); + expect(path.pathRef.atPoint(2).dy, 50.0); + }); + + test('LineTo should add moveTo 0,0 when first call to Path API', () { + final SurfacePath path = SurfacePath(); + path.lineTo(20.0, 40.0); + expect(path.pathRef.countPoints(), 2); + expect(path.pathRef.atPoint(0).dx, 0); + expect(path.pathRef.atPoint(0).dy, 0); + expect(path.pathRef.atPoint(1).dx, 20.0); + expect(path.pathRef.atPoint(1).dy, 40.0); + }); + + test('relativeLineTo should increments currentX', () { + final SurfacePath path = SurfacePath(); + path.moveTo(5.0, 10.0); + path.lineTo(20.0, 40.0); + path.relativeLineTo(5.0, 5.0); + expect(path.pathRef.countPoints(), 3); + expect(path.pathRef.atPoint(2).dx, 25.0); + expect(path.pathRef.atPoint(2).dy, 45.0); + }); + + test('Should allow calling relativeLineTo before moveTo', () { + final SurfacePath path = SurfacePath(); + path.relativeLineTo(5.0, 5.0); + path.moveTo(5.0, 10.0); + expect(path.pathRef.countPoints(), 3); + expect(path.pathRef.atPoint(1).dx, 5.0); + expect(path.pathRef.atPoint(1).dy, 5.0); + expect(path.pathRef.atPoint(2).dx, 5.0); + expect(path.pathRef.atPoint(2).dy, 10.0); + }); + + test('Should allow relativeLineTo after reset', () { + final SurfacePath path = SurfacePath(); + final Path subPath = Path(); + subPath.moveTo(50.0, 60.0); + subPath.lineTo(200.0, 200.0); + path.extendWithPath(subPath, const Offset(0.0, 0.0)); + path.reset(); + path.relativeLineTo(5.0, 5.0); + expect(path.pathRef.countPoints(), 2); + expect(path.pathRef.atPoint(0).dx, 0); + expect(path.pathRef.atPoint(0).dy, 0); + expect(path.pathRef.atPoint(1).dx, 5.0); + }); + + test('Should detect rectangular path', () { + final SurfacePath path = SurfacePath(); + path.addRect(const Rect.fromLTWH(1.0, 2.0, 3.0, 4.0)); + expect(path.webOnlyPathAsRect, const Rect.fromLTWH(1.0, 2.0, 3.0, 4.0)); + }); + + test('Should detect non rectangular path if empty', () { + final SurfacePath path = SurfacePath(); + expect(path.webOnlyPathAsRect, null); + }); + + test('Should detect non rectangular path if there are multiple subpaths', + () { + final SurfacePath path = SurfacePath(); + path.addRect(const Rect.fromLTWH(1.0, 2.0, 3.0, 4.0)); + path.addRect(const Rect.fromLTWH(5.0, 6.0, 7.0, 8.0)); + expect(path.webOnlyPathAsRect, null); + }); + + test('Should detect rounded rectangular path', () { + final SurfacePath path = SurfacePath(); + path.addRRect(RRect.fromRectAndRadius( + const Rect.fromLTRB(1.0, 2.0, 30.0, 40.0), + const Radius.circular(2.0))); + expect( + path.webOnlyPathAsRoundedRect, + RRect.fromRectAndRadius(const Rect.fromLTRB(1.0, 2.0, 30.0, 40.0), + const Radius.circular(2.0))); + }); + + test('Should detect non rounded rectangular path if empty', () { + final SurfacePath path = SurfacePath(); + expect(path.webOnlyPathAsRoundedRect, null); + }); + + test('Should detect rectangular path is not round', () { + final SurfacePath path = SurfacePath(); + path.addRect(const Rect.fromLTWH(1.0, 2.0, 3.0, 4.0)); + expect(path.webOnlyPathAsRoundedRect, null); + }); + + test( + 'Should detect non rounded rectangular path if there are ' + 'multiple subpaths', () { + final SurfacePath path = SurfacePath(); + path.addRRect(RRect.fromRectAndRadius( + const Rect.fromLTWH(1.0, 2.0, 3.0, 4.0), const Radius.circular(2.0))); + path.addRRect(RRect.fromRectAndRadius( + const Rect.fromLTWH(1.0, 2.0, 3.0, 4.0), const Radius.circular(2.0))); + expect(path.webOnlyPathAsRoundedRect, null); + }); + + test('Should compute bounds as empty for empty and moveTo only path', () { + final Path emptyPath = Path(); + expect(emptyPath.getBounds(), Rect.zero); + + final SurfacePath path = SurfacePath(); + path.moveTo(50, 60); + expect(path.getBounds(), const Rect.fromLTRB(50, 60, 50, 60)); + }); + + test('Should compute bounds for multiple addRect calls', () { + final Path emptyPath = Path(); + expect(emptyPath.getBounds(), Rect.zero); + + final SurfacePath path = SurfacePath(); + path.addRect(Rect.fromLTWH(0, 0, 270, 45)); + path.addRect(Rect.fromLTWH(134.5, 0, 1, 45)); + expect(path.getBounds(), const Rect.fromLTRB(0, 0, 270, 45)); + }); + + test('Should compute bounds for addRRect', () { + SurfacePath path = SurfacePath(); + final Rect bounds = Rect.fromLTRB(30, 40, 400, 300); + RRect rrect = RRect.fromRectAndCorners(bounds, + topLeft: Radius.elliptical(1, 2), + topRight: Radius.elliptical(3, 4), + bottomLeft: Radius.elliptical(5, 6), + bottomRight: Radius.elliptical(7, 8)); + path.addRRect(rrect); + expect(path.getBounds(), bounds); + expect(path.webOnlyPathAsRoundedRect, rrect); + path = SurfacePath(); + rrect = RRect.fromRectAndCorners(bounds, + topLeft: Radius.elliptical(0, 2), + topRight: Radius.elliptical(3, 4), + bottomLeft: Radius.elliptical(5, 6), + bottomRight: Radius.elliptical(7, 8)); + path.addRRect(rrect); + expect(path.getBounds(), bounds); + expect(path.webOnlyPathAsRoundedRect, rrect); + path = SurfacePath(); + rrect = RRect.fromRectAndCorners(bounds, + topLeft: Radius.elliptical(0, 0), + topRight: Radius.elliptical(3, 4), + bottomLeft: Radius.elliptical(5, 6), + bottomRight: Radius.elliptical(7, 8)); + path.addRRect(rrect); + expect(path.getBounds(), bounds); + expect(path.webOnlyPathAsRoundedRect, rrect); + path = SurfacePath(); + rrect = RRect.fromRectAndCorners(bounds, + topLeft: Radius.elliptical(1, 2), + topRight: Radius.elliptical(0, 0), + bottomLeft: Radius.elliptical(5, 6), + bottomRight: Radius.elliptical(7, 8)); + path.addRRect(rrect); + expect(path.getBounds(), bounds); + expect(path.webOnlyPathAsRoundedRect, rrect); + path = SurfacePath(); + rrect = RRect.fromRectAndCorners(bounds, + topLeft: Radius.elliptical(1, 2), + topRight: Radius.elliptical(3, 4), + bottomLeft: Radius.elliptical(0, 0), + bottomRight: Radius.elliptical(7, 8)); + path.addRRect(rrect); + expect(path.getBounds(), bounds); + expect(path.webOnlyPathAsRoundedRect, rrect); + path = SurfacePath(); + rrect = RRect.fromRectAndCorners(bounds, + topLeft: Radius.elliptical(1, 2), + topRight: Radius.elliptical(3, 4), + bottomLeft: Radius.elliptical(5, 6), + bottomRight: Radius.elliptical(0, 0)); + path.addRRect(rrect); + expect(path.getBounds(), bounds); + expect(path.webOnlyPathAsRoundedRect, rrect); + }); + + test('Should compute bounds for lines', () { + final SurfacePath path = SurfacePath(); + path.moveTo(25, 30); + path.lineTo(100, 200); + expect(path.getBounds(), const Rect.fromLTRB(25, 30, 100, 200)); + + final SurfacePath path2 = SurfacePath(); + path2.moveTo(250, 300); + path2.lineTo(50, 60); + expect(path2.getBounds(), const Rect.fromLTRB(50, 60, 250, 300)); + }); + + test('Should compute bounds for polygon', () { + final SurfacePath path = SurfacePath(); + path.addPolygon([ + Offset(50, 100), + Offset(250, 100), + Offset(152, 180), + Offset(159, 200), + Offset(151, 190) + ], true); + expect(path.getBounds(), const Rect.fromLTRB(50, 100, 250, 200)); + }); + + test('Should compute bounds for quadraticBezierTo', () { + final SurfacePath path1 = SurfacePath(); + path1.moveTo(285.2, 682.1); + path1.quadraticBezierTo(432.0, 431.4, 594.9, 681.2); + expect( + path1.getBounds(), + within( + distance: 0.1, + from: const Rect.fromLTRB(285.2, 556.5, 594.9, 682.1))); + + // Control point below start , end. + final SurfacePath path2 = SurfacePath(); + path2.moveTo(285.2, 682.1); + path2.quadraticBezierTo(447.4, 946.8, 594.9, 681.2); + expect( + path2.getBounds(), + within( + distance: 0.1, + from: const Rect.fromLTRB(285.2, 681.2, 594.9, 814.2))); + + // Control point to the right of end point. + final SurfacePath path3 = SurfacePath(); + path3.moveTo(468.3, 685.6); + path3.quadraticBezierTo(644.7, 555.2, 594.9, 681.2); + expect( + path3.getBounds(), + within( + distance: 0.1, + from: const Rect.fromLTRB(468.3, 619.3, 605.9, 685.6))); + }); + + test('Should compute bounds for cubicTo', () { + final SurfacePath path1 = SurfacePath(); + path1.moveTo(220, 300); + path1.cubicTo(230, 120, 400, 125, 410, 280); + expect( + path1.getBounds(), + within( + distance: 0.1, + from: const Rect.fromLTRB(220.0, 164.3, 410.0, 300.0))); + + // control point 1 to the right of control point 2 + final SurfacePath path2 = SurfacePath(); + path2.moveTo(220, 300); + path2.cubicTo(564.2, 13.7, 400.0, 125.0, 410.0, 280.0); + expect( + path2.getBounds(), + within( + distance: 0.1, + from: const Rect.fromLTRB(220.0, 122.8, 440.5, 300.0))); + + // control point 1 to the right of control point 2 inflection + final SurfacePath path3 = SurfacePath(); + path3.moveTo(220, 300); + path3.cubicTo(839.8, 67.9, 400.0, 125.0, 410.0, 280.0); + expect( + path3.getBounds(), + within( + distance: 0.1, + from: const Rect.fromLTRB(220.0, 144.5, 552.1, 300.0))); + + // control point 1 below and between start and end points + final SurfacePath path4 = SurfacePath(); + path4.moveTo(220.0, 300.0); + path4.cubicTo(354.8, 388.3, 400.0, 125.0, 410.0, 280.0); + expect( + path4.getBounds(), + within( + distance: 0.1, + from: const Rect.fromLTRB(220.0, 230.0, 410.0, 318.6))); + + // control points inverted below + final SurfacePath path5 = SurfacePath(); + path5.moveTo(220.0, 300.0); + path5.cubicTo(366.5, 487.3, 256.4, 489.9, 410.0, 280.0); + expect( + path5.getBounds(), + within( + distance: 0.1, + from: const Rect.fromLTRB(220.0, 280.0, 410.0, 439.0))); + + // control points inverted below wide + final SurfacePath path6 = SurfacePath(); + path6.moveTo(220.0, 300.0); + path6.cubicTo(496.1, 485.5, 121.4, 491.6, 410.0, 280.0); + expect( + path6.getBounds(), + within( + distance: 0.1, + from: const Rect.fromLTRB(220.0, 280.0, 410.0, 439.0))); + + // control point 2 and end point swapped + final SurfacePath path7 = SurfacePath(); + path7.moveTo(220.0, 300.0); + path7.cubicTo(230.0, 120.0, 394.5, 296.1, 382.3, 124.1); + expect( + path7.getBounds(), + within( + distance: 0.1, + from: const Rect.fromLTRB(220.0, 124.1, 382.9, 300.0))); + }); + + // Regression test for https://github.com/flutter/flutter/issues/46813. + test('Should deep copy path', () { + final SurfacePath path = SurfacePath(); + path.moveTo(25, 30); + path.lineTo(100, 200); + expect(path.getBounds(), const Rect.fromLTRB(25, 30, 100, 200)); + + final SurfacePath path2 = SurfacePath.from(path); + path2.lineTo(250, 300); + expect(path2.getBounds(), const Rect.fromLTRB(25, 30, 250, 300)); + // Expect original path to stay the same. + expect(path.getBounds(), const Rect.fromLTRB(25, 30, 100, 200)); + }); + + test('Should handle contains inclusive right,bottom coordinates', () { + final Path path = Path(); + path.moveTo(50, 60); + path.lineTo(110, 60); + path.lineTo(110, 190); + path.lineTo(50, 190); + path.close(); + expect(path.contains(Offset(80, 190)), true); + expect(path.contains(Offset(110, 80)), true); + expect(path.contains(Offset(110, 190)), true); + expect(path.contains(Offset(110, 191)), false); + }); + + test('Should not contain top-left of beveled border', () { + final Path path = Path(); + path.moveTo(10, 25); + path.lineTo(15, 20); + path.lineTo(25, 20); + path.lineTo(30, 25); + path.lineTo(30, 35); + path.lineTo(25, 40); + path.lineTo(15, 40); + path.lineTo(10, 35); + path.close(); + expect(path.contains(Offset(10, 20)), false); + }); + + test('Computes contains for cubic curves', () { + final Path path = Path(); + path.moveTo(10, 25); + path.cubicTo(10, 20, 10, 20, 20, 15); + path.lineTo(25, 20); + path.cubicTo(30, 20, 30, 20, 30, 25); + path.lineTo(30, 35); + path.cubicTo(30, 40, 30, 40, 25, 40); + path.lineTo(15, 40); + path.cubicTo(10, 40, 10, 40, 10, 35); + path.close(); + expect(path.contains(Offset(10, 20)), false); + expect(path.contains(Offset(30, 40)), false); + }); + + // Regression test for https://github.com/flutter/flutter/issues/44470 + test('Should handle contains for devicepixelratio != 1.0', () { + js_util.setProperty(html.window, 'devicePixelRatio', 4.0); + window.debugOverrideDevicePixelRatio(4.0); + final Path path = Path() + ..moveTo(50, 0) + ..lineTo(100, 100) + ..lineTo(0, 100) + ..lineTo(50, 0) + ..close(); + expect(path.contains(Offset(50, 50)), isTrue); + js_util.setProperty(html.window, 'devicePixelRatio', 1.0); + window.debugOverrideDevicePixelRatio(1.0); + // TODO: Investigate failure on CI. Locally this passes. + // [Exception... "Failure" nsresult: "0x80004005 (NS_ERROR_FAILURE)" + }, skip: browserEngine == BrowserEngine.firefox); + + // Path contains should handle case where invalid RRect with large + // corner radius is used for hit test. Use case is a RenderPhysicalShape + // with a clipper that contains RRect of width/height 50 but corner radius + // of 100. + // + // Regression test for https://github.com/flutter/flutter/issues/48887 + test('Should hit test correctly for malformed rrect', () { + // Correctly formed rrect. + final Path path1 = Path() + ..addRRect(RRect.fromLTRBR(50, 50, 100, 100, Radius.circular(20))); + expect(path1.contains(Offset(75, 75)), isTrue); + expect(path1.contains(Offset(52, 75)), isTrue); + expect(path1.contains(Offset(50, 50)), isFalse); + expect(path1.contains(Offset(100, 50)), isFalse); + expect(path1.contains(Offset(100, 100)), isFalse); + expect(path1.contains(Offset(50, 100)), isFalse); + + final Path path2 = Path() + ..addRRect(RRect.fromLTRBR(50, 50, 100, 100, Radius.circular(100))); + expect(path2.contains(Offset(75, 75)), isTrue); + expect(path2.contains(Offset(52, 75)), isTrue); + expect(path2.contains(Offset(50, 50)), isFalse); + expect(path2.contains(Offset(100, 50)), isFalse); + expect(path2.contains(Offset(100, 100)), isFalse); + expect(path2.contains(Offset(50, 100)), isFalse); + }); + + test('Should set segment masks', () { + final SurfacePath path = SurfacePath(); + path.pathRef.computeSegmentMask(); + expect(path.pathRef.segmentMasks, 0); + path.moveTo(20, 40); + path.pathRef.computeSegmentMask(); + expect(path.pathRef.segmentMasks, 0); + path.lineTo(200, 40); + path.pathRef.computeSegmentMask(); + expect( + path.pathRef.segmentMasks, SPathSegmentMask.kLine_SkPathSegmentMask); + }); + + test('Should convert conic to quad when approximation error is small', () { + final Conic conic = Conic(120.0, 20.0, 160.99470420829266, 20.0, + 190.19301120261332, 34.38770865870253, 0.9252691032413082); + expect(conic.toQuads().length, 3); + }); + + test('Should be able to construct from empty path', () { + final SurfacePath path = SurfacePath(); + final SurfacePath? path2 = SurfacePath.from(path); + assert(path2 != null, true); + }); }); - test('LineTo should add command', () { - final SurfacePath path = SurfacePath(); - path.moveTo(5.0, 10.0); - path.lineTo(20.0, 40.0); - path.lineTo(30.0, 50.0); - expect(path.subpaths.length, 1); - expect(path.subpaths[0].currentX, 30.0); - expect(path.subpaths[0].currentY, 50.0); + group('PathRef', () { + test('Should return empty when created', () { + final PathRef pathRef = PathRef(); + expect(pathRef.isEmpty, true); + }); + + test('Should return non-empty when mutated', () { + final PathRef pathRef = PathRef(); + pathRef.growForVerb(SPath.kMoveVerb, 0); + expect(pathRef.isEmpty, false); + }); }); - - test('LineTo should add moveTo 0,0 when first call to Path API', () { - final SurfacePath path = SurfacePath(); - path.lineTo(20.0, 40.0); - expect(path.subpaths.length, 1); - expect(path.subpaths[0].currentX, 20.0); - expect(path.subpaths[0].currentY, 40.0); - }); - - test('relativeLineTo should increments currentX', () { - final SurfacePath path = SurfacePath(); - path.moveTo(5.0, 10.0); - path.lineTo(20.0, 40.0); - path.relativeLineTo(5.0, 5.0); - expect(path.subpaths.length, 1); - expect(path.subpaths[0].currentX, 25.0); - expect(path.subpaths[0].currentY, 45.0); - }); - - test('Should allow calling relativeLineTo before moveTo', () { - final SurfacePath path = SurfacePath(); - path.relativeLineTo(5.0, 5.0); - path.moveTo(5.0, 10.0); - expect(path.subpaths.length, 2); - expect(path.subpaths[0].currentX, 5.0); - expect(path.subpaths[0].currentY, 5.0); - expect(path.subpaths[1].currentX, 5.0); - expect(path.subpaths[1].currentY, 10.0); - }); - - test('Should allow relativeLineTo after reset', () { - final SurfacePath path = SurfacePath(); - final Path subPath = Path(); - subPath.moveTo(50.0, 60.0); - subPath.lineTo(200.0, 200.0); - path.extendWithPath(subPath, const Offset(0.0, 0.0)); - path.reset(); - path.relativeLineTo(5.0, 5.0); - expect(path.subpaths.length, 1); - expect(path.subpaths[0].currentX, 5.0); - expect(path.subpaths[0].currentY, 5.0); - }); - - test('Should detect rectangular path', () { - final SurfacePath path = SurfacePath(); - path.addRect(const Rect.fromLTWH(1.0, 2.0, 3.0, 4.0)); - expect(path.webOnlyPathAsRect, const Rect.fromLTWH(1.0, 2.0, 3.0, 4.0)); - }); - - test('Should detect non rectangular path if empty', () { - final SurfacePath path = SurfacePath(); - expect(path.webOnlyPathAsRect, null); - }); - - test('Should detect non rectangular path if there are multiple subpaths', () { - final SurfacePath path = SurfacePath(); - path.addRect(const Rect.fromLTWH(1.0, 2.0, 3.0, 4.0)); - path.addRect(const Rect.fromLTWH(5.0, 6.0, 7.0, 8.0)); - expect(path.webOnlyPathAsRect, null); - }); - - test('Should detect rounded rectangular path', () { - final SurfacePath path = SurfacePath(); - path.addRRect(RRect.fromRectAndRadius( - const Rect.fromLTWH(1.0, 2.0, 3.0, 4.0), const Radius.circular(2.0))); - expect( - path.webOnlyPathAsRoundedRect, - RRect.fromRectAndRadius(const Rect.fromLTWH(1.0, 2.0, 3.0, 4.0), - const Radius.circular(2.0))); - }); - - test('Should detect non rounded rectangular path if empty', () { - final SurfacePath path = SurfacePath(); - expect(path.webOnlyPathAsRoundedRect, null); - }); - - test('Should detect rectangular path is not round', () { - final SurfacePath path = SurfacePath(); - path.addRect(const Rect.fromLTWH(1.0, 2.0, 3.0, 4.0)); - expect(path.webOnlyPathAsRoundedRect, null); - }); - - test( - 'Should detect non rounded rectangular path if there are ' - 'multiple subpaths', () { - final SurfacePath path = SurfacePath(); - path.addRRect(RRect.fromRectAndRadius( - const Rect.fromLTWH(1.0, 2.0, 3.0, 4.0), const Radius.circular(2.0))); - path.addRRect(RRect.fromRectAndRadius( - const Rect.fromLTWH(1.0, 2.0, 3.0, 4.0), const Radius.circular(2.0))); - expect(path.webOnlyPathAsRoundedRect, null); - }); - - test('Should compute bounds as empty for empty and moveTo only path', () { - final Path emptyPath = Path(); - expect(emptyPath.getBounds(), Rect.zero); - - final SurfacePath path = SurfacePath(); - path.moveTo(50, 60); - expect(path.getBounds(), const Rect.fromLTRB(50, 60, 50, 60)); - }); - - test('Should compute bounds for multiple addRect calls', () { - final Path emptyPath = Path(); - expect(emptyPath.getBounds(), Rect.zero); - - final SurfacePath path = SurfacePath(); - path.addRect(Rect.fromLTWH(0, 0, 270, 45)); - path.addRect(Rect.fromLTWH(134.5, 0, 1, 45)); - expect(path.getBounds(), const Rect.fromLTRB(0, 0, 270, 45)); - }); - - test('Should compute bounds for lines', () { - final SurfacePath path = SurfacePath(); - path.moveTo(25, 30); - path.lineTo(100, 200); - expect(path.getBounds(), const Rect.fromLTRB(25, 30, 100, 200)); - - final SurfacePath path2 = SurfacePath(); - path2.moveTo(250, 300); - path2.lineTo(50, 60); - expect(path2.getBounds(), const Rect.fromLTRB(50, 60, 250, 300)); - }); - - test('Should compute bounds for quadraticBezierTo', () { - final SurfacePath path1 = SurfacePath(); - path1.moveTo(285.2, 682.1); - path1.quadraticBezierTo(432.0, 431.4, 594.9, 681.2); - expect( - path1.getBounds(), - within( - distance: 0.1, - from: const Rect.fromLTRB(285.2, 556.5, 594.9, 682.1))); - - // Control point below start , end. - final SurfacePath path2 = SurfacePath(); - path2.moveTo(285.2, 682.1); - path2.quadraticBezierTo(447.4, 946.8, 594.9, 681.2); - expect( - path2.getBounds(), - within( - distance: 0.1, - from: const Rect.fromLTRB(285.2, 681.2, 594.9, 814.2))); - - // Control point to the right of end point. - final SurfacePath path3 = SurfacePath(); - path3.moveTo(468.3, 685.6); - path3.quadraticBezierTo(644.7, 555.2, 594.9, 681.2); - expect( - path3.getBounds(), - within( - distance: 0.1, - from: const Rect.fromLTRB(468.3, 619.3, 605.9, 685.6))); - }); - - test('Should compute bounds for cubicTo', () { - final SurfacePath path1 = SurfacePath(); - path1.moveTo(220, 300); - path1.cubicTo(230, 120, 400, 125, 410, 280); - expect( - path1.getBounds(), - within( - distance: 0.1, - from: const Rect.fromLTRB(220.0, 164.3, 410.0, 300.0))); - - // control point 1 to the right of control point 2 - final SurfacePath path2 = SurfacePath(); - path2.moveTo(220, 300); - path2.cubicTo(564.2, 13.7, 400.0, 125.0, 410.0, 280.0); - expect( - path2.getBounds(), - within( - distance: 0.1, - from: const Rect.fromLTRB(220.0, 122.8, 440.5, 300.0))); - - // control point 1 to the right of control point 2 inflection - final SurfacePath path3 = SurfacePath(); - path3.moveTo(220, 300); - path3.cubicTo(839.8, 67.9, 400.0, 125.0, 410.0, 280.0); - expect( - path3.getBounds(), - within( - distance: 0.1, - from: const Rect.fromLTRB(220.0, 144.5, 552.1, 300.0))); - - // control point 1 below and between start and end points - final SurfacePath path4 = SurfacePath(); - path4.moveTo(220.0, 300.0); - path4.cubicTo(354.8, 388.3, 400.0, 125.0, 410.0, 280.0); - expect( - path4.getBounds(), - within( - distance: 0.1, - from: const Rect.fromLTRB(220.0, 230.0, 410.0, 318.6))); - - // control points inverted below - final SurfacePath path5 = SurfacePath(); - path5.moveTo(220.0, 300.0); - path5.cubicTo(366.5, 487.3, 256.4, 489.9, 410.0, 280.0); - expect( - path5.getBounds(), - within( - distance: 0.1, - from: const Rect.fromLTRB(220.0, 280.0, 410.0, 439.0))); - - // control points inverted below wide - final SurfacePath path6 = SurfacePath(); - path6.moveTo(220.0, 300.0); - path6.cubicTo(496.1, 485.5, 121.4, 491.6, 410.0, 280.0); - expect( - path6.getBounds(), - within( - distance: 0.1, - from: const Rect.fromLTRB(220.0, 280.0, 410.0, 439.0))); - - // control point 2 and end point swapped - final SurfacePath path7 = SurfacePath(); - path7.moveTo(220.0, 300.0); - path7.cubicTo(230.0, 120.0, 394.5, 296.1, 382.3, 124.1); - expect( - path7.getBounds(), - within( - distance: 0.1, - from: const Rect.fromLTRB(220.0, 124.1, 382.9, 300.0))); - }); - - // Regression test for https://github.com/flutter/flutter/issues/46813. - test('Should deep copy path', () { - final SurfacePath path = SurfacePath(); - path.moveTo(25, 30); - path.lineTo(100, 200); - expect(path.getBounds(), const Rect.fromLTRB(25, 30, 100, 200)); - - final SurfacePath path2 = SurfacePath.from(path); - path2.lineTo(250, 300); - expect(path2.getBounds(), const Rect.fromLTRB(25, 30, 250, 300)); - // Expect original path to stay the same. - expect(path.getBounds(), const Rect.fromLTRB(25, 30, 100, 200)); - }); - - // Regression test for https://github.com/flutter/flutter/issues/44470 - test('Should handle contains for devicepixelratio != 1.0', () { - js_util.setProperty(html.window, 'devicePixelRatio', 4.0); - window.debugOverrideDevicePixelRatio(4.0); - final path = Path() - ..moveTo(50, 0) - ..lineTo(100, 100) - ..lineTo(0, 100) - ..lineTo(50, 0) - ..close(); - expect(path.contains(Offset(50, 50)), isTrue); - js_util.setProperty(html.window, 'devicePixelRatio', 1.0); - window.debugOverrideDevicePixelRatio(1.0); - // TODO: Investigate failure on CI. Locally this passes. - // [Exception... "Failure" nsresult: "0x80004005 (NS_ERROR_FAILURE)" - }, skip: browserEngine == BrowserEngine.firefox); - - // Path contains should handle case where invalid RRect with large - // corner radius is used for hit test. Use case is a RenderPhysicalShape - // with a clipper that contains RRect of width/height 50 but corner radius - // of 100. - // - // Regression test for https://github.com/flutter/flutter/issues/48887 - test('Should hit test correctly for malformed rrect', () { - // Correctly formed rrect. - final path1 = Path() - ..addRRect(RRect.fromLTRBR(50, 50, 100, 100, Radius.circular(20))); - expect(path1.contains(Offset(75, 75)), isTrue); - expect(path1.contains(Offset(52, 75)), isTrue); - expect(path1.contains(Offset(50, 50)), isFalse); - expect(path1.contains(Offset(100, 50)), isFalse); - expect(path1.contains(Offset(100, 100)), isFalse); - expect(path1.contains(Offset(50, 100)), isFalse); - - final path2 = Path() - ..addRRect(RRect.fromLTRBR(50, 50, 100, 100, Radius.circular(100))); - expect(path2.contains(Offset(75, 75)), isTrue); - expect(path2.contains(Offset(52, 75)), isTrue); - expect(path2.contains(Offset(50, 50)), isFalse); - expect(path2.contains(Offset(100, 50)), isFalse); - expect(path2.contains(Offset(100, 100)), isFalse); - expect(path2.contains(Offset(50, 100)), isFalse); - }); - - test('Should convert conic to quad when approximation error is small', () { - Conic conic = Conic(120.0, 20.0, 160.99470420829266, 20.0, - 190.19301120261332, 34.38770865870253, 0.9252691032413082); - expect(conic.toQuads().length, 3); + group('PathRefIterator', () { + test('Should iterate through empty path', () { + final Float32List points = Float32List(20); + final PathRef pathRef = PathRef(); + final PathRefIterator iter = PathRefIterator(pathRef); + expect(iter.next(points), SPath.kDoneVerb); + }); + + test('Should iterate through verbs', () { + final Float32List points = Float32List(20); + final PathRef pathRef = PathRef(); + pathRef.growForVerb(SPath.kMoveVerb, 0); + pathRef.growForVerb(SPath.kLineVerb, 0); + pathRef.growForVerb(SPath.kQuadVerb, 0); + pathRef.growForVerb(SPath.kCubicVerb, 0); + pathRef.growForVerb(SPath.kConicVerb, 0.8); + pathRef.growForVerb(SPath.kLineVerb, 0.8); + final PathRefIterator iter = PathRefIterator(pathRef); + expect(iter.next(points), SPath.kMoveVerb); + expect(iter.next(points), SPath.kLineVerb); + expect(iter.next(points), SPath.kQuadVerb); + expect(iter.next(points), SPath.kCubicVerb); + expect(iter.next(points), SPath.kConicVerb); + expect(iter.next(points), SPath.kLineVerb); + expect(iter.next(points), SPath.kDoneVerb); + }); + + test('Should iterate by index through empty path', () { + final PathRef pathRef = PathRef(); + final PathRefIterator iter = PathRefIterator(pathRef); + expect(iter.nextIndex(), SPath.kDoneVerb); + }); + + test('Should iterate through contours', () { + final PathRef pathRef = PathRef(); + pathRef.growForVerb(SPath.kMoveVerb, 0); + pathRef.growForVerb(SPath.kLineVerb, 0); + pathRef.growForVerb(SPath.kQuadVerb, 0); + pathRef.growForVerb(SPath.kCubicVerb, 0); + pathRef.growForVerb(SPath.kMoveVerb, 0); + pathRef.growForVerb(SPath.kConicVerb, 0.8); + pathRef.growForVerb(SPath.kLineVerb, 0.8); + pathRef.growForVerb(SPath.kCloseVerb, 0.8); + pathRef.growForVerb(SPath.kMoveVerb, 0); + pathRef.growForVerb(SPath.kLineVerb, 0); + pathRef.growForVerb(SPath.kLineVerb, 0); + final PathRefIterator iter = PathRefIterator(pathRef); + int start = iter.pointIndex; + int end = iter.skipToNextContour(); + expect(end - start, 7); + + start = end; + end = iter.skipToNextContour(); + expect(end - start, 4); + + start = end; + end = iter.skipToNextContour(); + expect(end - start, 3); + + start = end; + end = iter.skipToNextContour(); + expect(start, end); + }); }); } diff --git a/lib/web_ui/test/path_winding_test.dart b/lib/web_ui/test/path_winding_test.dart new file mode 100644 index 000000000000..683a7eb474ae --- /dev/null +++ b/lib/web_ui/test/path_winding_test.dart @@ -0,0 +1,451 @@ +// 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 'package:test/test.dart'; +import 'package:ui/ui.dart' hide window; +import 'package:ui/src/engine.dart'; + +/// Test winding and convexity of a path. +void main() { + group('Convexity', () { + test('Empty path should be convex', () { + final SurfacePath path = SurfacePath(); + expect(path.isConvex, true); + }); + + test('Circle should be convex', () { + final SurfacePath path = SurfacePath(); + path.addOval(Rect.fromLTRB(0, 0, 20, 20)); + expect(path.isConvex, true); + // 2nd circle. + path.addOval(Rect.fromLTRB(0, 0, 20, 20)); + expect(path.isConvex, false); + }); + + test('addRect should be convex', () { + SurfacePath path = SurfacePath(); + path.addRect(Rect.fromLTRB(0, 0, 20, 20)); + assert(path.isConvex, true); + + path = SurfacePath(); + path.addRectWithDirection( + Rect.fromLTRB(0, 0, 20, 20), SPathDirection.kCW, 0); + assert(path.isConvex, true); + + path = SurfacePath(); + path.addRectWithDirection( + Rect.fromLTRB(0, 0, 20, 20), SPathDirection.kCCW, 0); + assert(path.isConvex, true); + }); + + test('Quad should be convex', () { + final SurfacePath path = SurfacePath(); + path.quadraticBezierTo(100, 100, 50, 50); + expect(path.isConvex, true); + }); + + test('moveto/lineto convexity', () { + final List testCases = [ + LineTestCase('', SPathConvexityType.kConvex, SPathDirection.kUnknown), + LineTestCase( + '0 0', SPathConvexityType.kConvex, SPathDirection.kUnknown), + LineTestCase( + '0 0 10 10', SPathConvexityType.kConvex, SPathDirection.kUnknown), + LineTestCase('0 0 10 10 20 20 0 0 10 10', SPathConvexityType.kConcave, + SPathDirection.kUnknown), + LineTestCase( + '0 0 10 10 10 20', SPathConvexityType.kConvex, SPathDirection.kCW), + LineTestCase( + '0 0 10 10 10 0', SPathConvexityType.kConvex, SPathDirection.kCCW), + LineTestCase('0 0 10 10 10 0 0 10', SPathConvexityType.kConcave, null), + LineTestCase('0 0 10 0 0 10 -10 -10', SPathConvexityType.kConcave, + SPathDirection.kCW), + ]; + + for (LineTestCase testCase in testCases) { + final SurfacePath path = SurfacePath(); + setFromString(path, testCase.pathContent); + expect(path.convexityType, testCase.convexity); + } + }); + + test('Convexity of path with infinite points should return unknown', () { + final List nonFinitePts = [ + Offset(double.infinity, 0), + Offset(0, double.infinity), + Offset(double.infinity, double.infinity), + Offset(double.negativeInfinity, 0), + Offset(0, double.negativeInfinity), + Offset(double.negativeInfinity, double.negativeInfinity), + Offset(double.negativeInfinity, double.infinity), + Offset(double.infinity, double.negativeInfinity), + Offset(double.nan, 0), + Offset(0, double.nan), + Offset(double.nan, double.nan) + ]; + final int nonFinitePointsCount = nonFinitePts.length; + + final List axisAlignedPts = [ + Offset(kScalarMax, 0), + Offset(0, kScalarMax), + Offset(kScalarMin, 0), + Offset(0, kScalarMin) + ]; + final int axisAlignedPointsCount = axisAlignedPts.length; + + final SurfacePath path = SurfacePath(); + + for (int index = 0; + index < (13 * nonFinitePointsCount * axisAlignedPointsCount); + index++) { + final int i = index % nonFinitePointsCount; + final int f = index % axisAlignedPointsCount; + final int g = (f + 1) % axisAlignedPointsCount; + path.reset(); + switch (index % 13) { + case 0: + path.lineTo(nonFinitePts[i].dx, nonFinitePts[i].dy); + break; + case 1: + path.quadraticBezierTo(nonFinitePts[i].dx, nonFinitePts[i].dy, + nonFinitePts[i].dx, nonFinitePts[i].dy); + break; + case 2: + path.quadraticBezierTo(nonFinitePts[i].dx, nonFinitePts[i].dy, + axisAlignedPts[f].dx, axisAlignedPts[f].dy); + break; + case 3: + path.quadraticBezierTo(axisAlignedPts[f].dx, axisAlignedPts[f].dy, + nonFinitePts[i].dx, nonFinitePts[i].dy); + break; + case 4: + path.cubicTo( + nonFinitePts[i].dx, + nonFinitePts[i].dy, + axisAlignedPts[f].dx, + axisAlignedPts[f].dy, + axisAlignedPts[f].dx, + axisAlignedPts[f].dy); + break; + case 5: + path.cubicTo( + axisAlignedPts[f].dx, + axisAlignedPts[f].dy, + nonFinitePts[i].dx, + nonFinitePts[i].dy, + axisAlignedPts[f].dx, + axisAlignedPts[f].dy); + break; + case 6: + path.cubicTo( + axisAlignedPts[f].dx, + axisAlignedPts[f].dy, + axisAlignedPts[f].dx, + axisAlignedPts[f].dy, + nonFinitePts[i].dx, + nonFinitePts[i].dy); + break; + case 7: + path.cubicTo( + nonFinitePts[i].dx, + nonFinitePts[i].dy, + nonFinitePts[i].dx, + nonFinitePts[i].dy, + axisAlignedPts[f].dx, + axisAlignedPts[f].dy); + break; + case 8: + path.cubicTo( + nonFinitePts[i].dx, + nonFinitePts[i].dy, + axisAlignedPts[f].dx, + axisAlignedPts[f].dy, + nonFinitePts[i].dx, + nonFinitePts[i].dy); + break; + case 9: + path.cubicTo( + axisAlignedPts[f].dx, + axisAlignedPts[f].dy, + nonFinitePts[i].dx, + nonFinitePts[i].dy, + nonFinitePts[i].dx, + nonFinitePts[i].dy); + break; + case 10: + path.cubicTo( + nonFinitePts[i].dx, + nonFinitePts[i].dy, + nonFinitePts[i].dx, + nonFinitePts[i].dy, + nonFinitePts[i].dx, + nonFinitePts[i].dy); + break; + case 11: + path.cubicTo( + nonFinitePts[i].dx, + nonFinitePts[i].dy, + axisAlignedPts[f].dx, + axisAlignedPts[f].dy, + axisAlignedPts[g].dx, + axisAlignedPts[g].dy); + break; + case 12: + path.moveTo(nonFinitePts[i].dx, nonFinitePts[i].dy); + break; + } + expect(path.convexityType, SPathConvexityType.kUnknown); + } + + for (int index = 0; index < (11 * axisAlignedPointsCount); ++index) { + int f = index % axisAlignedPointsCount; + int g = (f + 1) % axisAlignedPointsCount; + path.reset(); + int curveSelect = index % 11; + switch (curveSelect) { + case 0: + path.moveTo(axisAlignedPts[f].dx, axisAlignedPts[f].dy); + break; + case 1: + path.lineTo(axisAlignedPts[f].dx, axisAlignedPts[f].dy); + break; + case 2: + path.quadraticBezierTo(axisAlignedPts[f].dx, axisAlignedPts[f].dy, + axisAlignedPts[f].dx, axisAlignedPts[f].dy); + break; + case 3: + path.quadraticBezierTo(axisAlignedPts[f].dx, axisAlignedPts[f].dy, + axisAlignedPts[g].dx, axisAlignedPts[g].dy); + break; + case 4: + path.quadraticBezierTo(axisAlignedPts[g].dx, axisAlignedPts[g].dy, + axisAlignedPts[f].dx, axisAlignedPts[f].dy); + break; + case 5: + path.cubicTo( + axisAlignedPts[f].dx, + axisAlignedPts[f].dy, + axisAlignedPts[f].dx, + axisAlignedPts[f].dy, + axisAlignedPts[f].dx, + axisAlignedPts[f].dy); + break; + case 6: + path.cubicTo( + axisAlignedPts[f].dx, + axisAlignedPts[f].dy, + axisAlignedPts[f].dx, + axisAlignedPts[f].dy, + axisAlignedPts[g].dx, + axisAlignedPts[g].dy); + break; + case 7: + path.cubicTo( + axisAlignedPts[f].dx, + axisAlignedPts[f].dy, + axisAlignedPts[g].dx, + axisAlignedPts[g].dy, + axisAlignedPts[f].dx, + axisAlignedPts[f].dy); + break; + case 8: + path.cubicTo( + axisAlignedPts[f].dx, + axisAlignedPts[f].dy, + axisAlignedPts[g].dx, + axisAlignedPts[g].dy, + axisAlignedPts[g].dx, + axisAlignedPts[g].dy); + break; + case 9: + path.cubicTo( + axisAlignedPts[g].dx, + axisAlignedPts[g].dy, + axisAlignedPts[f].dx, + axisAlignedPts[f].dy, + axisAlignedPts[f].dx, + axisAlignedPts[f].dy); + break; + case 10: + path.cubicTo( + axisAlignedPts[g].dx, + axisAlignedPts[g].dy, + axisAlignedPts[f].dx, + axisAlignedPts[f].dy, + axisAlignedPts[g].dx, + axisAlignedPts[g].dy); + break; + } + if (curveSelect != 7 && curveSelect != 10) { + int result = path.convexityType; + expect(result, SPathConvexityType.kConvex); + } else { + // we make a copy so that we don't cache the result on the passed + // in path. + SurfacePath path2 = SurfacePath.from(path); + int c = path2.convexityType; + assert(SPathConvexityType.kUnknown == c || + SPathConvexityType.kConcave == c); + } + } + }); + test('Concave lines path', () { + final SurfacePath path = SurfacePath(); + path.moveTo(-0.284071773, -0.0622361786); + path.lineTo(-0.284072, -0.0622351); + path.lineTo(-0.28407, -0.0622307); + path.lineTo(-0.284067, -0.0622182); + path.lineTo(-0.284084, -0.0622269); + path.lineTo(-0.284072, -0.0622362); + path.close(); + expect(path.convexityType, SPathConvexityType.kConcave); + }); + + test('Single moveTo origin', () { + final SurfacePath path = SurfacePath(); + path.moveTo(0, 0); + expect(path.convexityType, SPathConvexityType.kConvex); + }); + + test('Single diagonal line', () { + final SurfacePath path = SurfacePath(); + path.moveTo(12, 20); + path.lineTo(-12, -20); + expect(path.convexityType, SPathConvexityType.kConvex); + }); + + test('TriLeft', () { + final SurfacePath path = SurfacePath(); + path.moveTo(0, 0); + path.lineTo(1, 0); + path.lineTo(1, 1); + path.close(); + expect(path.convexityType, SPathConvexityType.kConvex); + }); + + test('TriRight', () { + final SurfacePath path = SurfacePath(); + path.moveTo(0, 0); + path.lineTo(-1, 0); + path.lineTo(1, 1); + path.close(); + expect(path.convexityType, SPathConvexityType.kConvex); + }); + + test('square', () { + final SurfacePath path = SurfacePath(); + path.moveTo(0, 0); + path.lineTo(1, 0); + path.lineTo(1, 1); + path.lineTo(0, 1); + path.close(); + expect(path.convexityType, SPathConvexityType.kConvex); + }); + + test('redundant square', () { + final SurfacePath redundantSquare = SurfacePath(); + redundantSquare.moveTo(0, 0); + redundantSquare.lineTo(0, 0); + redundantSquare.lineTo(0, 0); + redundantSquare.lineTo(1, 0); + redundantSquare.lineTo(1, 0); + redundantSquare.lineTo(1, 0); + redundantSquare.lineTo(1, 1); + redundantSquare.lineTo(1, 1); + redundantSquare.lineTo(1, 1); + redundantSquare.lineTo(0, 1); + redundantSquare.lineTo(0, 1); + redundantSquare.lineTo(0, 1); + redundantSquare.close(); + expect(redundantSquare.convexityType, SPathConvexityType.kConvex); + }); + + test('bowtie', () { + final SurfacePath bowTie = SurfacePath(); + bowTie.moveTo(0, 0); + bowTie.lineTo(0, 0); + bowTie.lineTo(0, 0); + bowTie.lineTo(1, 1); + bowTie.lineTo(1, 1); + bowTie.lineTo(1, 1); + bowTie.lineTo(1, 0); + bowTie.lineTo(1, 0); + bowTie.lineTo(1, 0); + bowTie.lineTo(0, 1); + bowTie.lineTo(0, 1); + bowTie.lineTo(0, 1); + bowTie.close(); + expect(bowTie.convexityType, SPathConvexityType.kConcave); + }); + + test('sprial', () { + final SurfacePath spiral = SurfacePath(); + spiral.moveTo(0, 0); + spiral.lineTo(100, 0); + spiral.lineTo(100, 100); + spiral.lineTo(0, 100); + spiral.lineTo(0, 50); + spiral.lineTo(50, 50); + spiral.lineTo(50, 75); + spiral.close(); + expect(spiral.convexityType, SPathConvexityType.kConcave); + }); + + test('dent', () { + final SurfacePath dent = SurfacePath(); + dent.moveTo(0, 0); + dent.lineTo(100, 100); + dent.lineTo(0, 100); + dent.lineTo(-50, 200); + dent.lineTo(-200, 100); + dent.close(); + expect(dent.convexityType, SPathConvexityType.kConcave); + }); + + test('degenerate segments1', () { + final SurfacePath strokedSin = SurfacePath(); + for (int i = 0; i < 2000; i++) { + double x = i.toDouble() / 2.0; + double y = 500 - (x + math.sin(x / 100) * 40) / 3; + if (0 == i) { + strokedSin.moveTo(x, y); + } else { + strokedSin.lineTo(x, y); + } + } + expect(strokedSin.convexityType, SPathConvexityType.kConcave); + }); + }); +} + +class LineTestCase { + final String pathContent; + final int convexity; + final int? direction; + LineTestCase(this.pathContent, this.convexity, this.direction); +} + +/// Parses a string of the format "mx my lx1 ly1 lx2 ly2..." into a path +/// with moveTo/lineTo instructions for points. +void setFromString(SurfacePath path, String value) { + bool first = true; + List points = value.split(' '); + if (points.length < 2) { + return; + } + for (int i = 0; i < points.length; i += 2) { + if (first) { + path.moveTo(double.parse(points[i]), double.parse(points[i + 1])); + first = false; + } else { + path.lineTo(double.parse(points[i]), double.parse(points[i + 1])); + } + } +} + +// Scalar max is based on 32 bit float since [PathRef] stores values in +// Float32List. +const double kScalarMax = 3.402823466e+38; +const double kScalarMin = -kScalarMax;