| // 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. |
| |
| // @dart = 2.6 |
| 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, |
| /// 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. |
| /// |
| /// Paths can be drawn on canvases using [Canvas.drawPath], and can |
| /// used to create clip regions using [Canvas.clipPath]. |
| class SurfacePath implements ui.Path { |
| final List<Subpath> subpaths; |
| ui.PathFillType _fillType = ui.PathFillType.nonZero; |
| |
| Subpath get _currentSubpath => subpaths.isEmpty ? null : subpaths.last; |
| |
| List<PathCommand> get _commands => _currentSubpath?.commands; |
| |
| /// The current x-coordinate for this path. |
| double get _currentX => _currentSubpath?.currentX ?? 0.0; |
| |
| /// The current y-coordinate for this path. |
| double get _currentY => _currentSubpath?.currentY ?? 0.0; |
| |
| /// Recorder used for hit testing paths. |
| static ui.RawRecordingCanvas _rawRecorder; |
| |
| SurfacePath() : subpaths = <Subpath>[]; |
| |
| /// 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._shallowCopy(SurfacePath source) |
| : subpaths = List<Subpath>.from(source.subpaths); |
| |
| SurfacePath._clone(this.subpaths, this._fillType); |
| |
| static List<Subpath> _deepCopy(List<Subpath> 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<Subpath> 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; |
| } |
| |
| /// Determines how the interior of this path is calculated. |
| /// |
| /// 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); |
| } |
| |
| /// Sets the current point to (x, y). |
| void _setCurrentPoint(double x, double y) { |
| _currentSubpath.currentX = x; |
| _currentSubpath.currentY = y; |
| } |
| |
| /// Starts a new subpath at the given coordinate. |
| @override |
| void moveTo(double x, double y) { |
| _openNewSubpath(x, y); |
| _commands.add(MoveTo(x, y)); |
| } |
| |
| /// 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)); |
| } |
| |
| /// 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); |
| } |
| _commands.add(LineTo(x, y)); |
| _setCurrentPoint(x, y); |
| } |
| |
| /// 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)); |
| } |
| } |
| |
| /// Adds a quadratic bezier segment that curves from the current |
| /// point to the given point (x2,y2), using the control point |
| /// (x1,y1). |
| @override |
| void quadraticBezierTo(double x1, double y1, double x2, double y2) { |
| _ensurePathStarted(); |
| _commands.add(QuadraticCurveTo(x1, y1, x2, y2)); |
| _setCurrentPoint(x2, y2); |
| } |
| |
| /// Adds a quadratic 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. |
| @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); |
| } |
| |
| /// Adds a cubic bezier segment that curves from the current point |
| /// to the given point (x3,y3), using the control points (x1,y1) and |
| /// (x2,y2). |
| @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); |
| } |
| |
| /// Adds a cubic bezier segment that curves from the current point |
| /// to the point at the offset (x3,y3) from the current point, using |
| /// the control points at the offsets (x1,y1) and (x2,y2) from the |
| /// current point. |
| @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); |
| } |
| |
| /// 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) { |
| final List<ui.Offset> 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); |
| } |
| } |
| |
| /// 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) { |
| conicTo(_currentX + x1, _currentY + y1, _currentX + x2, _currentY + y2, w); |
| } |
| |
| /// If the `forceMoveTo` argument is false, adds a straight line |
| /// segment and an arc segment. |
| /// |
| /// If the `forceMoveTo` argument is true, starts a new subpath |
| /// consisting of an arc segment. |
| /// |
| /// In either case, the arc segment 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. |
| /// |
| /// The line segment added if `forceMoveTo` is false starts at the |
| /// current point and ends at the start of the arc. |
| @override |
| 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 (forceMoveTo) { |
| _openNewSubpath(startX, startY); |
| } else { |
| lineTo(startX, startY); |
| } |
| _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); |
| } |
| |
| /// Appends up to four conic curves weighted to describe an oval of `radius` |
| /// and rotated by `rotation`. |
| /// |
| /// The first curve begins from the last point in the path and the last ends |
| /// at `arcEnd`. The curves follow a path in a direction determined by |
| /// `clockwise` and `largeArc` in such a way that the sweep angle |
| /// is always less than 360 degrees. |
| /// |
| /// A simple line is appended if either either radii are zero or the last |
| /// point in the path is `arcEnd`. The radii are scaled to fit the last path |
| /// point if both are greater than zero but too small to describe an arc. |
| /// |
| /// See Conversion from endpoint to center parametrization described in |
| /// https://www.w3.org/TR/SVG/implnote.html#ArcConversionEndpointToCenter |
| /// as reference for implementation. |
| @override |
| void arcToPoint( |
| ui.Offset arcEnd, { |
| ui.Radius radius = ui.Radius.zero, |
| double rotation = 0.0, |
| bool largeArc = false, |
| bool clockwise = true, |
| }) { |
| 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. |
| |
| 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; |
| |
| // 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; |
| } |
| |
| // 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; |
| |
| // Convert rotation or radians. |
| final double xAxisRotation = math.pi * rotation / 180.0; |
| |
| // Cache cos/sin value. |
| final double cosXAxisRotation = math.cos(xAxisRotation); |
| final double sinXAxisRotation = math.sin(xAxisRotation); |
| |
| // Calculate rotate midpoint as x/yPrime. |
| final double xPrime = |
| (cosXAxisRotation * midPointX) + (sinXAxisRotation * midPointY); |
| final double yPrime = |
| (-sinXAxisRotation * midPointX) + (cosXAxisRotation * midPointY); |
| |
| // Check if the radii are big enough to draw the arc, scale radii if not. |
| // http://www.w3.org/TR/SVG/implnote.html#ArcCorrectionOutOfRangeRadii |
| double rxSquare = rx * rx; |
| double rySquare = ry * ry; |
| final double xPrimeSquare = xPrime * xPrime; |
| final double yPrimeSquare = yPrime * yPrime; |
| |
| double radiiScale = (xPrimeSquare / rxSquare) + (yPrimeSquare / rySquare); |
| if (radiiScale > 1) { |
| radiiScale = math.sqrt(radiiScale); |
| rx *= radiiScale; |
| ry *= radiiScale; |
| rxSquare = rx * rx; |
| 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)); |
| 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); |
| |
| // 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; |
| } |
| |
| _commands.add(Ellipse(cx, cy, rx, ry, xAxisRotation, startAngle, |
| startAngle + sweepAngle, sweepAngle.isNegative)); |
| |
| _setCurrentPoint(arcEnd.dx, arcEnd.dy); |
| } |
| |
| /// Appends up to four conic curves weighted to describe an oval of `radius` |
| /// and rotated by `rotation`. |
| /// |
| /// The last path point is described by (px, py). |
| /// |
| /// The first curve begins from the last point in the path and the last ends |
| /// at `arcEndDelta.dx + px` and `arcEndDelta.dy + py`. The curves follow a |
| /// path in a direction determined by `clockwise` and `largeArc` |
| /// in such a way that the sweep angle is always less than 360 degrees. |
| /// |
| /// A simple line is appended if either either radii are zero, or, both |
| /// `arcEndDelta.dx` and `arcEndDelta.dy` are zero. The radii are scaled to |
| /// fit the last path point if both are greater than zero but too small to |
| /// describe an arc. |
| @override |
| void relativeArcToPoint( |
| ui.Offset arcEndDelta, { |
| ui.Radius radius = ui.Radius.zero, |
| double rotation = 0.0, |
| bool largeArc = false, |
| bool clockwise = true, |
| }) { |
| assert(offsetIsValid(arcEndDelta)); |
| assert(radiusIsValid(radius)); |
| arcToPoint( |
| ui.Offset(_currentX + arcEndDelta.dx, _currentY + 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. |
| /// |
| /// To add a circle, pass an appropriate rectangle as `oval`. |
| /// [Rect.fromCircle] can be used to easily describe the circle's center |
| /// [Offset] and radius. |
| @override |
| void addOval(ui.Rect oval) { |
| 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)); |
| } |
| |
| /// 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. |
| @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); |
| } |
| |
| /// Adds a new subpath with a sequence of line segments that connect the given |
| /// points. |
| /// |
| /// If `close` is true, a final line segment will be added that connects the |
| /// last point to the first point. |
| /// |
| /// The `points` argument is interpreted as offsets from the origin. |
| @override |
| void addPolygon(List<ui.Offset> points, bool close) { |
| assert(points != null); |
| if (points.isEmpty) { |
| 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); |
| } |
| if (close) { |
| this.close(); |
| } else { |
| _setCurrentPoint(points.last.dx, points.last.dy); |
| } |
| } |
| |
| /// Adds a new subpath that consists of the straight lines and |
| /// curves needed to form the rounded rectangle described by the |
| /// argument. |
| @override |
| void addRRect(ui.RRect rrect) { |
| assert(rrectIsValid(rrect)); |
| |
| // 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)); |
| } |
| |
| /// Adds a new subpath that consists of the given `path` offset by the given |
| /// `offset`. |
| /// |
| /// If `matrix4` is specified, the path will be transformed by this matrix |
| /// after the matrix is translated by the given offset. The matrix is a 4x4 |
| /// matrix stored in column major order. |
| @override |
| void addPath(ui.Path path, ui.Offset offset, {Float64List matrix4}) { |
| assert(path != null); // path is checked on the engine side |
| assert(offsetIsValid(offset)); |
| if (matrix4 != null) { |
| _addPathWithMatrix(path, offset.dx, offset.dy, toMatrix32(matrix4)); |
| } else { |
| _addPath(path, offset.dx, offset.dy); |
| } |
| } |
| |
| void _addPath(SurfacePath path, double dx, double dy) { |
| if (dx == 0.0 && dy == 0.0) { |
| subpaths.addAll(path.subpaths); |
| } else { |
| subpaths.addAll(path |
| ._transform(Matrix4.translationValues(dx, dy, 0.0).storage) |
| .subpaths); |
| } |
| } |
| |
| 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); |
| } |
| |
| /// Adds the given path to this path by extending the current segment of this |
| /// path with the first segment of the given path. |
| /// |
| /// If `matrix4` is specified, the path will be transformed by this matrix |
| /// after the matrix is translated by the given `offset`. The matrix is a 4x4 |
| /// matrix stored in column major order. |
| @override |
| void extendWithPath(ui.Path path, ui.Offset offset, {Float64List matrix4}) { |
| 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, offset.dx, offset.dy, matrix32); |
| } else { |
| _extendWithPath(path, 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(); |
| } |
| |
| /// Tests to see if the given point is within the path. (That is, whether the |
| /// point would be in the visible portion of the path if the path was used |
| /// with [Canvas.clipPath].) |
| /// |
| /// The `point` argument is interpreted as an offset from the origin. |
| /// |
| /// Returns true if the point is in the path, and false otherwise. |
| /// |
| /// 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. |
| @override |
| bool contains(ui.Offset point) { |
| assert(offsetIsValid(point)); |
| final int subPathCount = subpaths.length; |
| if (subPathCount == 0) { |
| return false; |
| } |
| 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); |
| } |
| 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; |
| _rawRecorder ??= |
| ui.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; |
| } |
| |
| /// 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<Subpath> shiftedSubPaths = <Subpath>[]; |
| for (final Subpath subPath in subpaths) { |
| shiftedSubPaths.add(subPath.shift(offset)); |
| } |
| return SurfacePath._clone(shiftedSubPaths, fillType); |
| } |
| |
| /// 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 _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); |
| } |
| } |
| return transformedPath; |
| } |
| |
| /// Computes the bounding rectangle for this path. |
| /// |
| /// A path containing only axis-aligned points on the same straight line will |
| /// have no area, and therefore `Rect.isEmpty` will return true for such a |
| /// path. Consider checking `rect.width + rect.height > 0.0` instead, or |
| /// using the [computeMetrics] API to check the path length. |
| /// |
| /// For many more elaborate paths, the bounds may be inaccurate. For example, |
| /// when a path contains a circle, the points used to compute the bounds are |
| /// the circle's implied control points, which form a square around the |
| /// circle; if the circle has a transformation applied using [transform] then |
| /// that square is rotated, and the (axis-aligned, non-rotated) bounding box |
| /// therefore ends up grossly overestimating the actual area covered by the |
| /// circle. |
| // 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; |
| 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; |
| curX = minX = maxX = cmd.x; |
| curY = minY = maxY = cmd.y; |
| break; |
| case PathCommandTypes.lineTo: |
| final LineTo cmd = op; |
| curX = minX = maxX = cmd.x; |
| curY = minY = maxY = cmd.y; |
| break; |
| case PathCommandTypes.ellipse: |
| final Ellipse cmd = op; |
| // 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; |
| 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 num t1 = (x1 - cpX) / denom; |
| if ((t1 >= 0) && (t1 <= 1.0)) { |
| // Solve (x,y) for curve at t = tx to find extrema |
| final num tprime = 1.0 - t1; |
| final num extremaX = (tprime * tprime * x1) + |
| (2 * t1 * tprime * cpX) + |
| (t1 * t1 * x2); |
| final num 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 num t2 = (y1 - cpY) / denom; |
| if ((t2 >= 0) && (t2 <= 1.0)) { |
| final num tprime2 = 1.0 - t2; |
| final num extrema2X = (tprime2 * tprime2 * x1) + |
| (2 * t2 * tprime2 * cpX) + |
| (t2 * t2 * x2); |
| final num 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; |
| 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 |
| num 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 num t = -b / (2 * a); |
| final num 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); |
| num t = (-b - s) / (2 * a); |
| num 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 |
| num 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 num t = -b / (2 * a); |
| final num 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 num t = (-b - s) / (2 * a); |
| final num 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 num t2 = (-b + s) / (2 * a); |
| final num 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); |
| } |
| } |
| } |
| } |
| break; |
| case PathCommandTypes.rect: |
| final RectCommand cmd = op; |
| 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; |
| 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); |
| } |
| } |
| } |
| } |
| return ltrbInitialized |
| ? ui.Rect.fromLTRB(left, top, right, bottom) |
| : ui.Rect.zero; |
| } |
| |
| /// Creates a [PathMetrics] object for this path. |
| /// |
| /// If `forceClosed` is set to true, the contours of the path will be measured |
| /// as if they had been closed, even if they were not explicitly closed. |
| @override |
| SurfacePathMetrics computeMetrics({bool forceClosed = false}) { |
| return SurfacePathMetrics._(this, forceClosed); |
| } |
| |
| /// Detects if path is rounded rectangle and returns rounded rectangle or |
| /// null. |
| /// |
| /// 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; |
| } |
| |
| /// 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; |
| } |
| |
| /// Detects if path is simple oval and returns [Ellipse] 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; |
| } |
| |
| /// Serializes this path to a value that's sent to a CSS custom painter for |
| /// painting. |
| List<dynamic> webOnlySerializeToCssPaint() { |
| final List<dynamic> serializedSubpaths = <dynamic>[]; |
| for (int i = 0; i < subpaths.length; i++) { |
| serializedSubpaths.add(subpaths[i].serializeToCssPaint()); |
| } |
| return serializedSubpaths; |
| } |
| |
| @override |
| String toString() { |
| if (assertionsEnabled) { |
| return 'Path(${subpaths.join(', ')})'; |
| } 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; |
| } |