mirror of
https://github.com/flutter/flutter.git
synced 2026-02-20 02:29:02 +08:00
Add support for measuring, combining, and getting bounds of Path objects (flutter/engine#4799)
* add path measure * fix typo * getBound and addPathWithMatrix * Add myself to Authors, add PathOps * fix linting issues * update licenses_flutter to add new files * Use matrix4 instead of matrix3 for consistency/interop * put pubspec back * fix bug in getSegment * fix typo * Add return value for PathOp * refactoring from review * refactoring from review - still TBD on computeMetrics() * add doc * lint issue * fix computeMetrics, add Path.from * add missing wireup for clone * change PathMetrics to iterable, fix bug with angle on Tangent * prefer std::make_unique * cleanup docs * add path measure * fix typo * getBound and addPathWithMatrix * Add myself to Authors, add PathOps * fix linting issues * update licenses_flutter to add new files * Use matrix4 instead of matrix3 for consistency/interop * put pubspec back * fix bug in getSegment * fix typo * Add return value for PathOp * refactoring from review * refactoring from review - still TBD on computeMetrics() * add doc * lint issue * fix computeMetrics, add Path.from * add missing wireup for clone * change PathMetrics to iterable, fix bug with angle on Tangent * prefer std::make_unique * cleanup docs * fix iterator bug * remove unnecessary clone for computeMetrics * fix some doc issues * fix PathMeasure iterator, extendWithPath, isClosed, and pubspec.lock * get rid of orElse; use StateException * StateError, not StateException * doc improvements and nits * add unit tests, fix bugs found during testing * fix two uncommited doc changes * one more * change sign of tangent angle, update docs * update unit tests for inverted angle * update tangent to include vector * Doc fixes
This commit is contained in:
parent
a34c18c8dc
commit
db8570675d
@ -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",
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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.');
|
||||
@ -1431,6 +1437,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_.
|
||||
@ -1453,6 +1515,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].
|
||||
@ -1650,23 +1721,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.
|
||||
@ -1701,12 +1792,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<PathMetric> {
|
||||
PathMetrics._(Path path, bool forceClosed) :
|
||||
_iterator = new PathMetricIterator._(new PathMetric._(path, forceClosed));
|
||||
|
||||
final Iterator<PathMetric> _iterator;
|
||||
|
||||
@override
|
||||
Iterator<PathMetric> get iterator => _iterator;
|
||||
}
|
||||
|
||||
/// Tracks iteration from one segment of a path to the next for measurement.
|
||||
class PathMetricIterator implements Iterator<PathMetric> {
|
||||
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.
|
||||
|
||||
@ -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> 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> CanvasPath::clone() {
|
||||
fxl::RefPtr<CanvasPath> 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
|
||||
|
||||
@ -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<CanvasPath>,
|
||||
return fxl::MakeRefCounted<CanvasPath>();
|
||||
}
|
||||
|
||||
static fxl::RefPtr<CanvasPath> CreateFrom(const SkPath& src) {
|
||||
fxl::RefPtr<CanvasPath> path = CanvasPath::Create();
|
||||
path->path_ = src;
|
||||
return path;
|
||||
}
|
||||
|
||||
int getFillType();
|
||||
void setFillType(int fill_type);
|
||||
|
||||
@ -78,12 +85,23 @@ class CanvasPath : public fxl::RefCountedThreadSafe<CanvasPath>,
|
||||
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<CanvasPath> shift(double dx, double dy);
|
||||
fxl::RefPtr<CanvasPath> transform(tonic::Float64List& matrix4);
|
||||
tonic::Float32List getBounds();
|
||||
bool op(CanvasPath* path1, CanvasPath* path2, int operation);
|
||||
fxl::RefPtr<CanvasPath> clone();
|
||||
|
||||
const SkPath& path() const { return path_; }
|
||||
|
||||
|
||||
110
engine/src/flutter/lib/ui/painting/path_measure.cc
Normal file
110
engine/src/flutter/lib/ui/painting/path_measure.cc
Normal file
@ -0,0 +1,110 @@
|
||||
// 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 <math.h>
|
||||
|
||||
#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> CanvasPathMeasure::Create(const CanvasPath* path,
|
||||
bool forceClosed) {
|
||||
fxl::RefPtr<CanvasPathMeasure> pathMeasure =
|
||||
fxl::MakeRefCounted<CanvasPathMeasure>();
|
||||
if (path) {
|
||||
const SkPath skPath = path->path();
|
||||
pathMeasure->path_measure_ =
|
||||
std::make_unique<SkPathMeasure>(skPath, forceClosed, 1);
|
||||
} else {
|
||||
pathMeasure->path_measure_ = std::make_unique<SkPathMeasure>();
|
||||
}
|
||||
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<CanvasPath> 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
|
||||
51
engine/src/flutter/lib/ui/painting/path_measure.h
Normal file
51
engine/src/flutter/lib/ui/painting/path_measure.h
Normal file
@ -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<CanvasPathMeasure>,
|
||||
public tonic::DartWrappable {
|
||||
DEFINE_WRAPPERTYPEINFO();
|
||||
FRIEND_MAKE_REF_COUNTED(CanvasPathMeasure);
|
||||
|
||||
public:
|
||||
~CanvasPathMeasure() override;
|
||||
static fxl::RefPtr<CanvasPathMeasure> Create(const CanvasPath* path, bool forceClosed);
|
||||
|
||||
void setPath(const CanvasPath* path, bool isClosed);
|
||||
float getLength();
|
||||
tonic::Float32List getPosTan(float distance);
|
||||
fxl::RefPtr<CanvasPath> 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<SkPathMeasure> path_measure_;
|
||||
};
|
||||
|
||||
} // namespace blink
|
||||
|
||||
#endif // FLUTTER_LIB_UI_PAINTING_PATH_MEASURE_H_
|
||||
@ -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;
|
||||
|
||||
225
engine/src/flutter/testing/dart/path_test.dart
Normal file
225
engine/src/flutter/testing/dart/path_test.dart
Normal file
@ -0,0 +1,225 @@
|
||||
// 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';
|
||||
|
||||
Matcher moreOrLessEquals(double value, {double epsilon: 1e-10}) {
|
||||
return new _MoreOrLessEquals(value, epsilon);
|
||||
}
|
||||
|
||||
class _MoreOrLessEquals extends Matcher {
|
||||
const _MoreOrLessEquals(this.value, this.epsilon);
|
||||
|
||||
final double value;
|
||||
final double epsilon;
|
||||
|
||||
@override
|
||||
bool matches(Object object, Map<dynamic, dynamic> matchState) {
|
||||
if (object is! double) return false;
|
||||
if (object == value) return true;
|
||||
final double test = object;
|
||||
return (test - value).abs() <= epsilon;
|
||||
}
|
||||
|
||||
@override
|
||||
Description describe(Description description) =>
|
||||
description.add('$value (±$epsilon)');
|
||||
}
|
||||
|
||||
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, moreOrLessEquals(0.88, epsilon: 0.01));
|
||||
|
||||
final Path reverseDifference =
|
||||
Path.combine(PathOperation.reverseDifference, pathCircle1, pathCircle2);
|
||||
expect(reverseDifference.getBounds().right,
|
||||
moreOrLessEquals(14.11, epsilon: 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,
|
||||
moreOrLessEquals(-1.5708, epsilon: .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,
|
||||
moreOrLessEquals(-0.7853981633974483, epsilon: .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);
|
||||
});
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user