diff --git a/lib/ui/BUILD.gn b/lib/ui/BUILD.gn index 6a75cd22c8614f4ef8226171db27eabd5aca9776..14d6d4c483e8681c47738c802bde271859e32482 100644 --- a/lib/ui/BUILD.gn +++ b/lib/ui/BUILD.gn @@ -36,6 +36,8 @@ source_set("ui") { "painting/paint.h", "painting/path.cc", "painting/path.h", + "painting/path_measure.cc", + "painting/path_measure.h", "painting/picture.cc", "painting/picture.h", "painting/picture_recorder.cc", diff --git a/lib/ui/dart_ui.cc b/lib/ui/dart_ui.cc index 59d083a1917783e2a41ecc213e97b38a906b466e..824152e28e8d07a2bc106aa2664944cda016145f 100644 --- a/lib/ui/dart_ui.cc +++ b/lib/ui/dart_ui.cc @@ -15,6 +15,7 @@ #include "flutter/lib/ui/painting/image_filter.h" #include "flutter/lib/ui/painting/image_shader.h" #include "flutter/lib/ui/painting/path.h" +#include "flutter/lib/ui/painting/path_measure.h" #include "flutter/lib/ui/painting/picture.h" #include "flutter/lib/ui/painting/picture_recorder.h" #include "flutter/lib/ui/painting/vertices.h" @@ -53,6 +54,7 @@ void DartUI::InitForGlobal() { CanvasGradient::RegisterNatives(g_natives); CanvasImage::RegisterNatives(g_natives); CanvasPath::RegisterNatives(g_natives); + CanvasPathMeasure::RegisterNatives(g_natives); Codec::RegisterNatives(g_natives); DartRuntimeHooks::RegisterNatives(g_natives); FrameInfo::RegisterNatives(g_natives); diff --git a/lib/ui/painting.dart b/lib/ui/painting.dart index c08d3cf46fde4a4a3604d5447c2bf8562e6e3ec0..561a8db64774d14263d2b37648809ec6ed8dba95 100644 --- a/lib/ui/painting.dart +++ b/lib/ui/painting.dart @@ -39,6 +39,12 @@ bool _offsetIsValid(Offset offset) { return true; } +bool _matrix4IsValid(Float64List matrix4) { + assert(matrix4 != null, 'Matrix4 argument was null.'); + assert(matrix4.length == 16, 'Matrix4 must have 16 entries.'); + return true; +} + bool _radiusIsValid(Radius radius) { assert(radius != null, 'Radius argument was null.'); assert(!radius.x.isNaN && !radius.y.isNaN, 'Radius argument contained a NaN value.'); @@ -1390,6 +1396,62 @@ enum PathFillType { evenOdd, } +/// Strategies for combining paths. +/// +/// See also: +/// +/// * [Path.combine], which uses this enum to decide how to combine two paths. +// Must be kept in sync with SkPathOp +enum PathOperation { + /// Subtract the second path from the first path. + /// + /// For example, if the two paths are overlapping circles of equal diameter + /// but differing centers, the result would be a crescent portion of the + /// first circle that was not overlapped by the second circle. + /// + /// See also: + /// + /// * [reverseDifference], which is the same but subtracting the first path + /// from the second. + difference, + /// Create a new path that is the intersection of the two paths, leaving the + /// overlapping pieces of the path. + /// + /// For example, if the two paths are overlapping circles of equal diameter + /// but differing centers, the result would be only the overlapping portion + /// of the two circles. + /// + /// See also: + /// * [xor], which is the inverse of this operation + intersect, + /// Create a new path that is the union (inclusive-or) of the two paths. + /// + /// For example, if the two paths are overlapping circles of equal diameter + /// but differing centers, the result would be a figure-eight like shape + /// matching the outter boundaries of both circles. + union, + /// Create a new path that is the exclusive-or of the two paths, leaving + /// everything but the overlapping pieces of the path. + /// + /// For example, if the two paths are overlapping circles of equal diameter + /// but differing centers, the figure-eight like shape less the overlapping parts + /// + /// See also: + /// * [intersect], which is the inverse of this operation + xor, + /// Subtract the first path from the second path. + /// + /// For example, if the two paths are overlapping circles of equal diameter + /// but differing centers, the result would be a crescent portion of the + /// second circle that was not overlapped by the first circle. + /// + /// See also: + /// + /// * [difference], which is the same but subtracting the second path + /// from the frist. + reverseDifference, +} + /// A complex, one-dimensional subset of a plane. /// /// A path consists of a number of subpaths, and a _current point_. @@ -1412,6 +1474,15 @@ class Path extends NativeFieldWrapperClass2 { Path() { _constructor(); } void _constructor() native 'Path_constructor'; + /// 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. + factory Path.from(Path source) { + return source._clone(); + } + Path _clone() native 'Path_clone'; + /// Determines how the interior of this path is calculated. /// /// Defaults to the non-zero winding rule, [PathFillType.nonZero]. @@ -1609,23 +1680,43 @@ class Path extends NativeFieldWrapperClass2 { } void _addRRect(Float32List rrect) native 'Path_addRRect'; - /// Adds a new subpath that consists of the given path offset by the given - /// offset. - void addPath(Path path, Offset offset) { + /// 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. + void addPath(Path path, Offset offset, {Float64List matrix4}) { assert(path != null); // path is checked on the engine side assert(_offsetIsValid(offset)); - _addPath(path, offset.dx, offset.dy); + if (matrix4 != null) { + assert(_matrix4IsValid(matrix4)); + _addPathWithMatrix(path, offset.dx, offset.dy, matrix4); + } else { + _addPath(path, offset.dx, offset.dy); + } } void _addPath(Path path, double dx, double dy) native 'Path_addPath'; - + void _addPathWithMatrix(Path path, double dx, double dy, Float64List matrix) native 'Path_addPathWithMatrix'; + /// Adds the given path to this path by extending the current segment of this /// path with the the first segment of the given path. - void extendWithPath(Path path, Offset 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. + void extendWithPath(Path path, Offset offset, {Float64List matrix4}) { assert(path != null); // path is checked on the engine side assert(_offsetIsValid(offset)); - _extendWithPath(path, offset.dx, offset.dy); + if (matrix4 != null) { + assert(_matrix4IsValid(matrix4)); + _extendWithPathAndMatrix(path, offset.dx, offset.dy, matrix4); + } else { + _extendWithPath(path, offset.dx, offset.dy); + } } void _extendWithPath(Path path, double dx, double dy) native 'Path_extendWithPath'; + void _extendWithPathAndMatrix(Path path, double dx, double dy, Float64List matrix) native 'Path_extendWithPathAndMatrix'; /// Closes the last subpath, as if a straight line had been drawn /// from the current point to the first point of the subpath. @@ -1660,12 +1751,207 @@ class Path extends NativeFieldWrapperClass2 { /// Returns a copy of the path with all the segments of every /// subpath transformed by the given matrix. Path transform(Float64List matrix4) { - assert(matrix4 != null); - if (matrix4.length != 16) - throw new ArgumentError('"matrix4" must have 16 entries.'); + assert(_matrix4IsValid(matrix4)); return _transform(matrix4); } Path _transform(Float64List matrix4) native 'Path_transform'; + + /// Computes the bounding rectangle for this path. + Rect getBounds() { + final Float32List rect = _getBounds(); + return new Rect.fromLTRB(rect[0], rect[1], rect[2], rect[3]); + } + Float32List _getBounds() native 'Path_getBounds'; + + /// Combines the two paths according to the manner specified by the given + /// `operation`. + /// + /// The resulting path will be constructed from non-overlapping contours. The + /// curve order is reduced where possible so that cubics may be turned into + /// quadratics, and quadratics maybe turned into lines. + static Path combine(PathOperation operation, Path path1, Path path2) { + assert(path1 != null); + assert(path2 != null); + final Path path = new Path(); + if (path._op(path1, path2, operation.index)) { + return path; + } + throw new StateError('Path.combine() failed. This may be due an invalid path; in particular, check for NaN values.'); + } + bool _op(Path path1, Path path2, int operation) native 'Path_op'; + + /// 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. + PathMetrics computeMetrics({bool forceClosed: false}) { + return new PathMetrics._(this, forceClosed); + } +} + +/// The geometric description of a tangent: the angle at a point. +/// +/// See also: +/// * [PathMetric.getTangentForOffset], which returns the tangent of an offset along a path. +class Tangent { + /// Creates a [Tangent] with the given values. + /// + /// The arguments must not be null. + const Tangent(this.position, this.vector) + : assert(position != null), + assert(vector != null); + + /// Creates a [Tangent] based on the angle rather than the vector. + /// + /// The [vector] is computed to be the unit vector at the given angle, interpreted + /// as clockwise radians from the x axis. + factory Tangent.fromAngle(Offset position, double angle) { + return new Tangent(position, new Offset(math.cos(angle), math.sin(angle))); + } + + /// Position of the tangent. + /// + /// When used with [PathMetric.getTangentForOffset], this represents the precise + /// position that the given offset along the path corresponds to. + final Offset position; + + /// The vector of the curve at [position]. + /// + /// When used with [PathMetric.getTangentForOffset], this is the vector of the + /// curve that is at the given offset along the path (i.e. the direction of the + /// curve at [position]). + final Offset vector; + + /// The direction of the curve at [position]. + /// + /// When used with [PathMetric.getTangentForOffset], this is the angle of the + /// curve that is the given offset along the path (i.e. the direction of the + /// curve at [position]). + /// + /// This value is in radians, with 0.0 meaning pointing along the x axis in + /// the positive x-axis direction, positive numbers pointing downward toward + /// the negative y-axis, i.e. in a clockwise direction, and negative numbers + /// pointing upward toward the positive y-axis, i.e. in a counter-clockwise + /// direction. + // flip the sign to be consistent with [Path.arcTo]'s `sweepAngle` + double get angle => -math.atan2(vector.dy, vector.dx); +} + +/// An iterable collection of [PathMetric] objects describing a [Path]. +/// +/// A [PathMetrics] object is created by using the [Path.computeMetrics] method, +/// and represents the path as it stood at the time of the call. Subsequent +/// modifications of the path do not affect the [PathMetrics] object. +/// +/// Each path metric corresponds to a segment, or contour, of a path. +/// +/// For example, a path consisting of a [Path.lineTo], a [Path.moveTo], and +/// another [Path.lineTo] will contain two contours and thus be represented by +/// two [PathMetric] objects. +/// +/// When iterating across a [PathMetrics]' contours, the [PathMetric] objects are only +/// valid until the next one is obtained. +class PathMetrics extends collection.IterableBase { + PathMetrics._(Path path, bool forceClosed) : + _iterator = new PathMetricIterator._(new PathMetric._(path, forceClosed)); + + final Iterator _iterator; + + @override + Iterator get iterator => _iterator; +} + +/// Tracks iteration from one segment of a path to the next for measurement. +class PathMetricIterator implements Iterator { + PathMetricIterator._(this._pathMetric); + + PathMetric _pathMetric; + bool _firstTime = true; + + @override + PathMetric get current => _firstTime ? null : _pathMetric; + + @override + bool moveNext() { + // PathMetric isn't a normal iterable - it's already initialized to its + // first Path. Should only call _moveNext when done with the first one. + if (_firstTime == true) { + _firstTime = false; + return true; + } else if (_pathMetric?._moveNext() == true) { + return true; + } + _pathMetric = null; + return false; + } +} + +/// Utilities for measuring a [Path] and extracting subpaths. +/// +/// Iterate over the object returned by [Path.computeMetrics] to obtain +/// [PathMetric] objects. +/// +/// Once created, metrics will only be valid while the iterator is at the given +/// contour. When the next contour's [PathMetric] is obtained, this object +/// becomes invalid. +class PathMetric extends NativeFieldWrapperClass2 { + /// Create a new empty [Path] object. + PathMetric._(Path path, bool forceClosed) { _constructor(path, forceClosed); } + void _constructor(Path path, bool forceClosed) native 'PathMeasure_constructor'; + + /// Return the total length of the current contour. + double get length native 'PathMeasure_getLength'; + + /// Computes the position of hte current contour at the given offset, and the + /// angle of the path at that point. + /// + /// For example, calling this method with a distance of 1.41 for a line from + /// 0.0,0.0 to 2.0,2.0 would give a point 1.0,1.0 and the angle 45 degrees + /// (but in radians). + /// + /// Returns null if the contour has zero [length]. + /// + /// The distance is clamped to the [length] of the current contour. + Tangent getTangentForOffset(double distance) { + final Float32List posTan = _getPosTan(distance); + // first entry == 0 indicates that Skia returned false + if (posTan[0] == 0.0) { + return null; + } else { + return new Tangent( + new Offset(posTan[1], posTan[2]), + new Offset(posTan[3], posTan[4]) + ); + } + } + Float32List _getPosTan(double distance) native 'PathMeasure_getPosTan'; + + /// Given a start and stop distance, return the intervening segment(s). + /// + /// `start` and `end` are pinned to legal values (0..[length]) + /// Returns null if the segment is 0 length or `start` > `stop`. + /// Begin the segment with a moveTo if `startWithMoveTo` is true. + Path extractPath(double start, double end, {bool startWithMoveTo: true}) native 'PathMeasure_getSegment'; + + /// Whether the contour is closed. + /// + /// Returns true if the contour ends with a call to [Path.close] (which may + /// have been implied when using [Path.addRect]) or if `forceClosed` was + /// specified as true in the call to [Path.computeMetrics]. Returns false + /// otherwise. + bool get isClosed native 'PathMeasure_isClosed'; + + // Move to the next contour in the path. + // + // A path can have a next contour if [Path.moveTo] was called after drawing began. + // Return true if one exists, or false. + // + // This is not exactly congruent with a regular [Iterator.moveNext]. + // Typically, [Iterator.moveNext] should be called before accessing the + // [Iterator.current]. In this case, the [PathMetric] is valid before + // calling `_moveNext` - `_moveNext` should be called after the first + // iteration is done instead of before. + bool _moveNext() native 'PathMeasure_nextContour'; } /// Styles to use for blurs in [MaskFilter] objects. diff --git a/lib/ui/painting/path.cc b/lib/ui/painting/path.cc index ee85ca2c6943115e4ef8d3dec36d6ff448a8447e..ffd9f4b7c5b028577685b9e2a7df3617f12708ca 100644 --- a/lib/ui/painting/path.cc +++ b/lib/ui/painting/path.cc @@ -38,6 +38,7 @@ IMPLEMENT_WRAPPERTYPEINFO(ui, Path); V(Path, contains) \ V(Path, cubicTo) \ V(Path, extendWithPath) \ + V(Path, extendWithPathAndMatrix) \ V(Path, getFillType) \ V(Path, lineTo) \ V(Path, moveTo) \ @@ -51,7 +52,11 @@ IMPLEMENT_WRAPPERTYPEINFO(ui, Path); V(Path, reset) \ V(Path, setFillType) \ V(Path, shift) \ - V(Path, transform) + V(Path, transform) \ + V(Path, getBounds) \ + V(Path, addPathWithMatrix) \ + V(Path, op) \ + V(Path, clone) FOR_EACH_BINDING(DART_NATIVE_CALLBACK) @@ -207,6 +212,17 @@ void CanvasPath::addPath(CanvasPath* path, double dx, double dy) { path_.addPath(path->path(), dx, dy, SkPath::kAppend_AddPathMode); } +void CanvasPath::addPathWithMatrix(CanvasPath* path, double dx, double dy, tonic::Float64List& matrix4) { + if (!path) + Dart_ThrowException(ToDart("Path.addPathWithMatrix called with non-genuine Path.")); + + SkMatrix matrix = ToSkMatrix(matrix4); + matrix.setTranslateX(matrix.getTranslateX() + dx); + matrix.setTranslateY(matrix.getTranslateY() + dy); + path_.addPath(path->path(), matrix, SkPath::kAppend_AddPathMode); + matrix4.Release(); +} + void CanvasPath::extendWithPath(CanvasPath* path, double dx, double dy) { if (!path) Dart_ThrowException( @@ -214,6 +230,17 @@ void CanvasPath::extendWithPath(CanvasPath* path, double dx, double dy) { path_.addPath(path->path(), dx, dy, SkPath::kExtend_AddPathMode); } +void CanvasPath::extendWithPathAndMatrix(CanvasPath* path, double dx, double dy, tonic::Float64List& matrix4) { + if (!path) + Dart_ThrowException(ToDart("Path.addPathWithMatrix called with non-genuine Path.")); + + SkMatrix matrix = ToSkMatrix(matrix4); + matrix.setTranslateX(matrix.getTranslateX() + dx); + matrix.setTranslateY(matrix.getTranslateY() + dy); + path_.addPath(path->path(), matrix, SkPath::kExtend_AddPathMode); + matrix4.Release(); +} + void CanvasPath::close() { path_.close(); } @@ -239,4 +266,27 @@ fxl::RefPtr CanvasPath::transform(tonic::Float64List& matrix4) { return path; } +tonic::Float32List CanvasPath::getBounds() { + tonic::Float32List rect(Dart_NewTypedData(Dart_TypedData_kFloat32, 4)); + const SkRect& bounds = path_.getBounds(); + rect[0] = bounds.left(); + rect[1] = bounds.top(); + rect[2] = bounds.right(); + rect[3] = bounds.bottom(); + return rect; +} + + +bool CanvasPath::op(CanvasPath* path1, CanvasPath* path2, int operation) { + return Op(path1->path(), path2->path(), (SkPathOp)operation, &path_); +} + +fxl::RefPtr CanvasPath::clone() { + fxl::RefPtr path = CanvasPath::Create(); + // per Skia docs, this will create a fast copy + // data is shared until the source path or dest path are mutated + path->path_ = path_; + return path; +} + } // namespace blink diff --git a/lib/ui/painting/path.h b/lib/ui/painting/path.h index 900ca0e3f57ef13d60b02d99064fbed5364b4884..afe670b72ff2037cff2f2707dab6cd36c09c781a 100644 --- a/lib/ui/painting/path.h +++ b/lib/ui/painting/path.h @@ -10,6 +10,7 @@ #include "lib/tonic/typed_data/float32_list.h" #include "lib/tonic/typed_data/float64_list.h" #include "third_party/skia/include/core/SkPath.h" +#include "third_party/skia/include/pathops/SkPathOps.h" namespace tonic { class DartLibraryNatives; @@ -28,6 +29,12 @@ class CanvasPath : public fxl::RefCountedThreadSafe, return fxl::MakeRefCounted(); } + static fxl::RefPtr CreateFrom(const SkPath& src) { + fxl::RefPtr path = CanvasPath::Create(); + path->path_ = src; + return path; + } + int getFillType(); void setFillType(int fill_type); @@ -78,12 +85,23 @@ class CanvasPath : public fxl::RefCountedThreadSafe, void addPolygon(const tonic::Float32List& points, bool close); void addRRect(const RRect& rrect); void addPath(CanvasPath* path, double dx, double dy); + void addPathWithMatrix(CanvasPath* path, + double dx, + double dy, + tonic::Float64List& matrix4); void extendWithPath(CanvasPath* path, double dx, double dy); + void extendWithPathAndMatrix(CanvasPath* path, + double dx, + double dy, + tonic::Float64List& matrix4); void close(); void reset(); bool contains(double x, double y); fxl::RefPtr shift(double dx, double dy); fxl::RefPtr transform(tonic::Float64List& matrix4); + tonic::Float32List getBounds(); + bool op(CanvasPath* path1, CanvasPath* path2, int operation); + fxl::RefPtr clone(); const SkPath& path() const { return path_; } diff --git a/lib/ui/painting/path_measure.cc b/lib/ui/painting/path_measure.cc new file mode 100644 index 0000000000000000000000000000000000000000..5bff424d7798d317df1c01aa8d249e4be32b17e6 --- /dev/null +++ b/lib/ui/painting/path_measure.cc @@ -0,0 +1,111 @@ +// Copyright 2015 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "flutter/lib/ui/painting/path_measure.h" + +#include + +#include "flutter/lib/ui/painting/matrix.h" +#include "lib/tonic/converter/dart_converter.h" +#include "lib/tonic/dart_args.h" +#include "lib/tonic/dart_binding_macros.h" +#include "lib/tonic/dart_library_natives.h" + +using tonic::ToDart; + +namespace blink { + +typedef CanvasPathMeasure PathMeasure; + +static void PathMeasure_constructor(Dart_NativeArguments args) { + DartCallConstructor(&CanvasPathMeasure::Create, args); +} + +IMPLEMENT_WRAPPERTYPEINFO(ui, PathMeasure); + +#define FOR_EACH_BINDING(V) \ + V(PathMeasure, setPath) \ + V(PathMeasure, getLength) \ + V(PathMeasure, getPosTan) \ + V(PathMeasure, getSegment) \ + V(PathMeasure, isClosed) \ + V(PathMeasure, nextContour) + +FOR_EACH_BINDING(DART_NATIVE_CALLBACK) + +void CanvasPathMeasure::RegisterNatives(tonic::DartLibraryNatives* natives) { + natives->Register( + {{"PathMeasure_constructor", PathMeasure_constructor, 3, true}, + FOR_EACH_BINDING(DART_REGISTER_NATIVE)}); +} + +fxl::RefPtr CanvasPathMeasure::Create(const CanvasPath* path, + bool forceClosed) { + fxl::RefPtr pathMeasure = + fxl::MakeRefCounted(); + if (path) { + const SkPath skPath = path->path(); + SkScalar resScale = 1; + pathMeasure->path_measure_ = + std::make_unique(skPath, forceClosed, resScale); + } else { + pathMeasure->path_measure_ = std::make_unique(); + } + return pathMeasure; +} + +CanvasPathMeasure::CanvasPathMeasure() {} + +CanvasPathMeasure::~CanvasPathMeasure() {} + +void CanvasPathMeasure::setPath(const CanvasPath* path, bool isClosed) { + const SkPath* skPath = &(path->path()); + path_measure_->setPath(skPath, isClosed); +} + +float CanvasPathMeasure::getLength() { + return path_measure_->getLength(); +} + +tonic::Float32List CanvasPathMeasure::getPosTan(float distance) { + SkPoint pos; + SkVector tan; + bool success = path_measure_->getPosTan(distance, &pos, &tan); + + tonic::Float32List posTan(Dart_NewTypedData(Dart_TypedData_kFloat32, 5)); + if (success) { + posTan[0] = 1; // dart code will check for this for success + posTan[1] = pos.x(); + posTan[2] = pos.y(); + posTan[3] = tan.x(); + posTan[4] = tan.y(); + } else { + posTan[0] = 0; // dart code will check for this for failure + } + + return posTan; +} + +fxl::RefPtr CanvasPathMeasure::getSegment(float startD, + float stopD, + bool startWithMoveTo) { + SkPath dst; + bool success = + path_measure_->getSegment(startD, stopD, &dst, startWithMoveTo); + if (!success) { + return CanvasPath::Create(); + } else { + return CanvasPath::CreateFrom(dst); + } +} + +bool CanvasPathMeasure::isClosed() { + return path_measure_->isClosed(); +} + +bool CanvasPathMeasure::nextContour() { + return path_measure_->nextContour(); +} + +} // namespace blink diff --git a/lib/ui/painting/path_measure.h b/lib/ui/painting/path_measure.h new file mode 100644 index 0000000000000000000000000000000000000000..7862081a88ec139b8ecbb3f55ffcc37675e81b7b --- /dev/null +++ b/lib/ui/painting/path_measure.h @@ -0,0 +1,51 @@ +// Copyright 2015 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef FLUTTER_LIB_UI_PAINTING_PATH_MEASURE_H_ +#define FLUTTER_LIB_UI_PAINTING_PATH_MEASURE_H_ + +#include "flutter/lib/ui/painting/path.h" +#include "lib/tonic/dart_wrappable.h" +#include "lib/tonic/typed_data/float64_list.h" +#include "third_party/skia/include/core/SkPath.h" +#include "third_party/skia/include/core/SkPathMeasure.h" + +namespace tonic { +class DartLibraryNatives; +} // namespace tonic + +// Be sure that the client doesn't modify a path on us before Skia finishes +// See AOSP's reasoning in PathMeasure.cpp + +namespace blink { + +class CanvasPathMeasure : public fxl::RefCountedThreadSafe, + public tonic::DartWrappable { + DEFINE_WRAPPERTYPEINFO(); + FRIEND_MAKE_REF_COUNTED(CanvasPathMeasure); + + public: + ~CanvasPathMeasure() override; + static fxl::RefPtr Create(const CanvasPath* path, bool forceClosed); + + void setPath(const CanvasPath* path, bool isClosed); + float getLength(); + tonic::Float32List getPosTan(float distance); + fxl::RefPtr getSegment(float startD, float stopD, bool startWithMoveTo); + bool isClosed(); + bool nextContour(); + + static void RegisterNatives(tonic::DartLibraryNatives* natives); + + const SkPathMeasure& pathMeasure() const { return *path_measure_; } + + private: + CanvasPathMeasure(); + + std::unique_ptr path_measure_; +}; + +} // namespace blink + +#endif // FLUTTER_LIB_UI_PAINTING_PATH_MEASURE_H_ diff --git a/lib/ui/ui.dart b/lib/ui/ui.dart index 5d6bc3ef213065c8b3a8eb9b8039f7589b5b4ed8..149cbde2101d4e0bb2eb8946d46985a586a7f21c 100644 --- a/lib/ui/ui.dart +++ b/lib/ui/ui.dart @@ -13,6 +13,7 @@ library dart.ui; import 'dart:_internal' hide Symbol; import 'dart:async'; +import 'dart:collection' as collection; import 'dart:convert'; import 'dart:developer' as developer; import 'dart:math' as math; diff --git a/testing/dart/path_test.dart b/testing/dart/path_test.dart new file mode 100644 index 0000000000000000000000000000000000000000..732f22304892bb876793c6bf487940ad3750c8de --- /dev/null +++ b/testing/dart/path_test.dart @@ -0,0 +1,202 @@ +// Copyright 2018 The Chromium 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:typed_data' show Float64List; +import 'dart:ui'; + +import 'package:test/test.dart'; + +void main() { + test('path getBounds', () { + final Rect r = new Rect.fromLTRB(1.0, 3.0, 5.0, 7.0); + final Path p = new Path()..addRect(r); + expect(p.getBounds(), equals(r)); + p.lineTo(20.0, 15.0); + expect(p.getBounds(), equals(new Rect.fromLTRB(1.0, 3.0, 20.0, 15.0))); + }); + + test('path combine rect', () { + final Rect c1 = + new Rect.fromCircle(center: const Offset(10.0, 10.0), radius: 10.0); + final Rect c2 = + new Rect.fromCircle(center: const Offset(5.0, 5.0), radius: 10.0); + final Rect c1UnionC2 = c1.expandToInclude(c2); + final Rect c1IntersectC2 = c1.intersect(c2); + final Path pathCircle1 = new Path()..addRect(c1); + final Path pathCircle2 = new Path()..addRect(c2); + + final Path difference = + Path.combine(PathOperation.difference, pathCircle1, pathCircle2); + expect(difference.getBounds(), equals(c1)); + + final Path reverseDifference = + Path.combine(PathOperation.reverseDifference, pathCircle1, pathCircle2); + expect(reverseDifference.getBounds(), equals(c2)); + + final Path union = + Path.combine(PathOperation.union, pathCircle1, pathCircle2); + expect(union.getBounds(), equals(c1UnionC2)); + + final Path intersect = + Path.combine(PathOperation.intersect, pathCircle1, pathCircle2); + expect(intersect.getBounds(), equals(c1IntersectC2)); + + // the bounds on this will be the same as union - but would draw a missing inside piece. + final Path xor = Path.combine(PathOperation.xor, pathCircle1, pathCircle2); + expect(xor.getBounds(), equals(c1UnionC2)); + }); + + test('path combine oval', () { + final Rect c1 = + new Rect.fromCircle(center: const Offset(10.0, 10.0), radius: 10.0); + final Rect c2 = + new Rect.fromCircle(center: const Offset(5.0, 5.0), radius: 10.0); + final Rect c1UnionC2 = c1.expandToInclude(c2); + final Rect c1IntersectC2 = c1.intersect(c2); + final Path pathCircle1 = new Path()..addOval(c1); + final Path pathCircle2 = new Path()..addOval(c2); + + final Path difference = + Path.combine(PathOperation.difference, pathCircle1, pathCircle2); + + expect(difference.getBounds().top, closeTo(0.88, 0.01)); + + final Path reverseDifference = + Path.combine(PathOperation.reverseDifference, pathCircle1, pathCircle2); + expect(reverseDifference.getBounds().right, + closeTo(14.11, 0.01)); + + final Path union = + Path.combine(PathOperation.union, pathCircle1, pathCircle2); + expect(union.getBounds(), equals(c1UnionC2)); + + final Path intersect = + Path.combine(PathOperation.intersect, pathCircle1, pathCircle2); + expect(intersect.getBounds(), equals(c1IntersectC2)); + + // the bounds on this will be the same as union - but would draw a missing inside piece. + final Path xor = Path.combine(PathOperation.xor, pathCircle1, pathCircle2); + expect(xor.getBounds(), equals(c1UnionC2)); + }); + + test('path clone', () { + final Path p1 = new Path()..lineTo(20.0, 20.0); + final Path p2 = new Path.from(p1); + + expect(p1.getBounds(), equals(p2.getBounds())); + + p1.lineTo(10.0, 30.0); + expect(p1.getBounds().bottom, equals(p2.getBounds().bottom + 10)); + }); + + test('transformation tests', () { + final Rect bounds = new Rect.fromLTRB(0.0, 0.0, 10.0, 10.0); + final Path p = new Path()..addRect(bounds); + final Float64List scaleMatrix = new Float64List.fromList([ + 2.5, 0.0, 0.0, 0.0, // first col + 0.0, 0.5, 0.0, 0.0, // second col + 0.0, 0.0, 1.0, 0.0, // third col + 0.0, 0.0, 0.0, 1.0, // fourth col + ]); + + expect(p.getBounds(), equals(bounds)); + final Path pTransformed = p.transform(scaleMatrix); + + expect(pTransformed.getBounds(), + equals(new Rect.fromLTRB(0.0, 0.0, 10 * 2.5, 10 * 0.5))); + + final Path p2 = new Path()..lineTo(10.0, 10.0); + + p.addPath(p2, const Offset(10.0, 10.0)); + expect(p.getBounds(), equals(new Rect.fromLTRB(0.0, 0.0, 20.0, 20.0))); + + p.addPath(p2, const Offset(20.0, 20.0), matrix4: scaleMatrix); + expect(p.getBounds(), + equals(new Rect.fromLTRB(0.0, 0.0, 20 + (10 * 2.5), 20 + (10 * .5)))); + + p.extendWithPath(p2, const Offset(0.0, 0.0)); + expect(p.getBounds(), equals(new Rect.fromLTRB(0.0, 0.0, 45.0, 25.0))); + + p.extendWithPath(p2, const Offset(45.0, 25.0), matrix4: scaleMatrix); + expect(p.getBounds(), equals(new Rect.fromLTRB(0.0, 0.0, 70.0, 30.0))); + }); + + test('path metrics tests', () { + final Path simpleHorizontalLine = new Path()..lineTo(10.0, 0.0); + + // basic tests on horizontal line + final PathMetrics simpleHorizontalMetrics = + simpleHorizontalLine.computeMetrics(); + expect(simpleHorizontalMetrics.iterator.current, isNull); + expect(simpleHorizontalMetrics.iterator.moveNext(), isTrue); + expect(simpleHorizontalMetrics.iterator.current, isNotNull); + expect(simpleHorizontalMetrics.iterator.current.length, equals(10.0)); + expect(simpleHorizontalMetrics.iterator.current.isClosed, isFalse); + final Path simpleExtract = + simpleHorizontalMetrics.iterator.current.extractPath(1.0, 9.0); + expect(simpleExtract.getBounds(), + equals(new Rect.fromLTRB(1.0, 0.0, 9.0, 0.0))); + final Tangent posTan = + simpleHorizontalMetrics.iterator.current.getTangentForOffset(1.0); + expect(posTan, isNotNull); + expect(posTan.position, equals(const Offset(1.0, 0.0))); + expect(posTan.angle, equals(0.0)); + + expect(simpleHorizontalMetrics.iterator.moveNext(), isFalse); + expect(simpleHorizontalMetrics.iterator.current, isNull); + + // test with forceClosed + final PathMetrics simpleMetricsClosed = + simpleHorizontalLine.computeMetrics(forceClosed: true); + expect(simpleMetricsClosed.iterator.current, isNull); + expect(simpleMetricsClosed.iterator.moveNext(), isTrue); + expect(simpleMetricsClosed.iterator.current, isNotNull); + expect(simpleMetricsClosed.iterator.current.length, + equals(20.0)); // because we forced close + expect(simpleMetricsClosed.iterator.current.isClosed, isTrue); + final Path simpleExtract2 = + simpleMetricsClosed.iterator.current.extractPath(1.0, 9.0); + expect(simpleExtract2.getBounds(), + equals(new Rect.fromLTRB(1.0, 0.0, 9.0, 0.0))); + expect(simpleMetricsClosed.iterator.moveNext(), isFalse); + + // test getTangentForOffset with vertical line + final Path simpleVerticalLine = new Path()..lineTo(0.0, 10.0); + final PathMetrics simpleMetricsVertical = + simpleVerticalLine.computeMetrics()..iterator.moveNext(); + final Tangent posTanVertical = + simpleMetricsVertical.iterator.current.getTangentForOffset(5.0); + expect(posTanVertical.position, equals(const Offset(0.0, 5.0))); + expect(posTanVertical.angle, + closeTo(-1.5708, .0001)); // 90 degrees + + // test getTangentForOffset with diagonal line + final Path simpleDiagonalLine = new Path()..lineTo(10.0, 10.0); + final PathMetrics simpleMetricsDiagonal = + simpleDiagonalLine.computeMetrics()..iterator.moveNext(); + final double midPoint = simpleMetricsDiagonal.iterator.current.length / 2; + final Tangent posTanDiagonal = + simpleMetricsDiagonal.iterator.current.getTangentForOffset(midPoint); + expect(posTanDiagonal.position, equals(new Offset(5.0, 5.0))); + expect(posTanDiagonal.angle, + closeTo(-0.7853981633974483, .00001)); // ~45 degrees + + // test a multi-contour path + final Path multiContour = new Path() + ..lineTo(0.0, 10.0) + ..moveTo(10.0, 10.0) + ..lineTo(10.0, 15.0); + + final PathMetrics multiContourMetric = multiContour.computeMetrics(); + expect(multiContourMetric.iterator.current, isNull); + expect(multiContourMetric.iterator.moveNext(), isTrue); + expect(multiContourMetric.iterator.current, isNotNull); + expect(multiContourMetric.iterator.current.length, equals(10.0)); + expect(multiContourMetric.iterator.moveNext(), isTrue); + expect(multiContourMetric.iterator.current, isNotNull); + expect(multiContourMetric.iterator.current.length, equals(5.0)); + expect(multiContourMetric.iterator.moveNext(), isFalse); + expect(multiContourMetric.iterator.current, isNull); + }); +} diff --git a/testing/dart/window_hooks_integration_test.dart b/testing/dart/window_hooks_integration_test.dart index 767b329829165b4f4a6d1d9ee39facc939dc27d2..5b5ff619a43756e0361c684c483debbfbea8584b 100644 --- a/testing/dart/window_hooks_integration_test.dart +++ b/testing/dart/window_hooks_integration_test.dart @@ -9,6 +9,9 @@ import 'dart:developer' as developer; import 'dart:math' as math; import 'dart:nativewrappers'; +// this needs to be imported because painting.dart expects it this way +import 'dart:collection' as collection; + import 'package:test/test.dart'; // HACK: these parts are to get access to private functions tested here. diff --git a/travis/licenses_golden/licenses_flutter b/travis/licenses_golden/licenses_flutter index 090275a17e7eb9f4ed821904bcaae5d38f0c4374..0f131c42a50d7afe3d1323546b1b7c783feda1e8 100644 --- a/travis/licenses_golden/licenses_flutter +++ b/travis/licenses_golden/licenses_flutter @@ -9947,6 +9947,8 @@ FILE: ../../../flutter/lib/ui/painting/paint.cc FILE: ../../../flutter/lib/ui/painting/paint.h FILE: ../../../flutter/lib/ui/painting/path.cc FILE: ../../../flutter/lib/ui/painting/path.h +FILE: ../../../flutter/lib/ui/painting/path_measure.cc +FILE: ../../../flutter/lib/ui/painting/path_measure.h FILE: ../../../flutter/lib/ui/painting/picture.cc FILE: ../../../flutter/lib/ui/painting/picture.h FILE: ../../../flutter/lib/ui/painting/picture_recorder.cc