未验证 提交 f49bd050 编写于 作者: Y Yegor 提交者: GitHub

implement SkPath.computeMetrics (#18667)

* implement SkPath.computeMetrics
上级 175a92b4
# Build configuration used by CanvasKit tests. When building for CanvasKit
# we must pass FLUTTER_WEB_USE_SKIA=true to dart2js.
#
# See also `build.html.yaml`.
targets:
$default:
builders:
build_web_compilers|entrypoint:
options:
compiler: dart2js
dart2js_args:
- --no-minify
- --enable-asserts
- -DFLUTTER_WEB_USE_SKIA=true
generate_for:
include:
- test/canvaskit/**.dart
# Build configuration used by HTML (non-CanvasKit) tests.
#
# See also `build.canvaskit.yaml`.
targets:
$default:
builders:
......@@ -12,4 +15,4 @@ targets:
- test/**.dart
exclude:
- test/**vm_test.dart
- test/canvaskit/**.dart
......@@ -13,6 +13,7 @@ Environment get environment {
_environment ??= Environment();
return _environment;
}
Environment _environment;
/// Contains various environment variables, such as common file paths and command-line options.
......@@ -20,14 +21,26 @@ class Environment {
factory Environment() {
final io.File self = io.File.fromUri(io.Platform.script);
final io.Directory engineSrcDir = self.parent.parent.parent.parent.parent;
final io.Directory engineToolsDir = io.Directory(pathlib.join(engineSrcDir.path, 'flutter', 'tools'));
final io.Directory outDir = io.Directory(pathlib.join(engineSrcDir.path, 'out'));
final io.Directory hostDebugUnoptDir = io.Directory(pathlib.join(outDir.path, 'host_debug_unopt'));
final io.Directory dartSdkDir = io.Directory(pathlib.join(hostDebugUnoptDir.path, 'dart-sdk'));
final io.Directory webUiRootDir = io.Directory(pathlib.join(engineSrcDir.path, 'flutter', 'lib', 'web_ui'));
final io.Directory integrationTestsDir = io.Directory(pathlib.join(engineSrcDir.path, 'flutter', 'e2etests', 'web'));
for (io.Directory expectedDirectory in <io.Directory>[engineSrcDir, outDir, hostDebugUnoptDir, dartSdkDir, webUiRootDir]) {
final io.Directory engineToolsDir =
io.Directory(pathlib.join(engineSrcDir.path, 'flutter', 'tools'));
final io.Directory outDir =
io.Directory(pathlib.join(engineSrcDir.path, 'out'));
final io.Directory hostDebugUnoptDir =
io.Directory(pathlib.join(outDir.path, 'host_debug_unopt'));
final io.Directory dartSdkDir =
io.Directory(pathlib.join(hostDebugUnoptDir.path, 'dart-sdk'));
final io.Directory webUiRootDir = io.Directory(
pathlib.join(engineSrcDir.path, 'flutter', 'lib', 'web_ui'));
final io.Directory integrationTestsDir = io.Directory(
pathlib.join(engineSrcDir.path, 'flutter', 'e2etests', 'web'));
for (io.Directory expectedDirectory in <io.Directory>[
engineSrcDir,
outDir,
hostDebugUnoptDir,
dartSdkDir,
webUiRootDir
]) {
if (!expectedDirectory.existsSync()) {
throw ToolException('$expectedDirectory does not exist.');
}
......@@ -89,63 +102,71 @@ class Environment {
String get pubExecutable => pathlib.join(dartSdkDir.path, 'bin', 'pub');
/// The "dart2js" executable file.
String get dart2jsExecutable => pathlib.join(dartSdkDir.path, 'bin', 'dart2js');
String get dart2jsExecutable =>
pathlib.join(dartSdkDir.path, 'bin', 'dart2js');
/// Path to where github.com/flutter/engine is checked out inside the engine workspace.
io.Directory get flutterDirectory => io.Directory(pathlib.join(engineSrcDir.path, 'flutter'));
io.Directory get flutterDirectory =>
io.Directory(pathlib.join(engineSrcDir.path, 'flutter'));
io.Directory get webSdkRootDir => io.Directory(pathlib.join(
flutterDirectory.path,
'web_sdk',
));
flutterDirectory.path,
'web_sdk',
));
/// Path to the "web_engine_tester" package.
io.Directory get webEngineTesterRootDir => io.Directory(pathlib.join(
webSdkRootDir.path,
'web_engine_tester',
));
webSdkRootDir.path,
'web_engine_tester',
));
/// Path to the "build" directory, generated by "package:build_runner".
///
/// This is where compiled output goes.
io.Directory get webUiBuildDir => io.Directory(pathlib.join(
webUiRootDir.path,
'build',
));
webUiRootDir.path,
'build',
));
/// Path to the ".dart_tool" directory, generated by various Dart tools.
io.Directory get webUiDartToolDir => io.Directory(pathlib.join(
webUiRootDir.path,
'.dart_tool',
));
/// Path to the ".dart_tool" directory living under `engine/src/flutter`.
///
/// This is a designated area for tool downloads which can be used by
/// multiple platforms. For exampe: Flutter repo for e2e tests.
webUiRootDir.path,
'.dart_tool',
));
/// Path to the ".dart_tool" directory living under `engine/src/flutter`.
///
/// This is a designated area for tool downloads which can be used by
/// multiple platforms. For exampe: Flutter repo for e2e tests.
io.Directory get engineDartToolDir => io.Directory(pathlib.join(
engineSrcDir.path,
'flutter',
'.dart_tool',
));
engineSrcDir.path,
'flutter',
'.dart_tool',
));
/// Path to the "dev" directory containing engine developer tools and
/// configuration files.
io.Directory get webUiDevDir => io.Directory(pathlib.join(
webUiRootDir.path,
'dev',
));
webUiRootDir.path,
'dev',
));
/// Path to the "test" directory containing web engine tests.
io.Directory get webUiTestDir => io.Directory(pathlib.join(
webUiRootDir.path,
'test',
));
/// Path to the clone of the flutter/goldens repository.
io.Directory get webUiGoldensRepositoryDirectory => io.Directory(pathlib.join(
webUiDartToolDir.path,
'goldens',
));
webUiDartToolDir.path,
'goldens',
));
/// Path to the script that clones the Flutter repo.
io.File get cloneFlutterScript => io.File(pathlib.join(
engineToolsDir.path,
'clone_flutter.sh',
));
engineToolsDir.path,
'clone_flutter.sh',
));
/// Path to flutter.
///
......@@ -153,10 +174,9 @@ class Environment {
///
/// Only use [cloneFlutterScript] to clone flutter to the engine build.
io.File get flutterCommand => io.File(pathlib.join(
engineDartToolDir.path,
'flutter',
'bin',
'flutter',
));
engineDartToolDir.path,
'flutter',
'bin',
'flutter',
));
}
......@@ -57,17 +57,17 @@ class TestCommand extends Command<bool> with ArgUtils {
'tests.',
)
..addFlag('use-system-flutter',
defaultsTo: false,
help: 'integration tests are using flutter repository for various tasks'
', such as flutter drive, flutter pub get. If this flag is set, felt '
'will use flutter command without cloning the repository. This flag '
'can save internet bandwidth. However use with caution. Note that '
'since flutter repo is always synced to youngest commit older than '
'the engine commit for the tests running in CI, the tests results '
'won\'t be consistent with CIs when this flag is set. flutter '
'command should be set in the PATH for this flag to be useful.'
'This flag can also be used to test local Flutter changes.'
)
defaultsTo: false,
help:
'integration tests are using flutter repository for various tasks'
', such as flutter drive, flutter pub get. If this flag is set, felt '
'will use flutter command without cloning the repository. This flag '
'can save internet bandwidth. However use with caution. Note that '
'since flutter repo is always synced to youngest commit older than '
'the engine commit for the tests running in CI, the tests results '
'won\'t be consistent with CIs when this flag is set. flutter '
'command should be set in the PATH for this flag to be useful.'
'This flag can also be used to test local Flutter changes.')
..addFlag(
'update-screenshot-goldens',
defaultsTo: false,
......@@ -159,8 +159,6 @@ class TestCommand extends Command<bool> with ArgUtils {
await _runPubGet();
}
await _buildTests(targets: targetFiles);
// Many tabs will be left open after Safari runs, quit Safari during
// cleanup.
if (browser == 'safari') {
......@@ -180,14 +178,57 @@ class TestCommand extends Command<bool> with ArgUtils {
});
}
await _buildTargets();
if (runAllTests) {
await _runAllTests();
await _runAllTestsForCurrentPlatform();
} else {
await _runTargetTests(targetFiles);
await _runSpecificTests(targetFiles);
}
return true;
}
/// Builds all test targets that will be run.
Future<void> _buildTargets() async {
final Stopwatch stopwatch = Stopwatch()..start();
List<FilePath> allTargets;
if (runAllTests) {
allTargets = environment.webUiTestDir
.listSync(recursive: true)
.whereType<io.File>()
.where((io.File f) => f.path.endsWith('_test.dart'))
.map<FilePath>((io.File f) => FilePath.fromWebUi(
path.relative(f.path, from: environment.webUiRootDir.path)))
.toList();
} else {
allTargets = targetFiles;
}
// Separate HTML targets from CanvasKit targets because the two use
// different dart2js options (and different build.*.yaml files).
final List<FilePath> htmlTargets = <FilePath>[];
final List<FilePath> canvasKitTargets = <FilePath>[];
final String canvasKitTestDirectory =
path.join(environment.webUiTestDir.path, 'canvaskit');
for (FilePath target in allTargets) {
if (path.isWithin(canvasKitTestDirectory, target.absolute)) {
canvasKitTargets.add(target);
} else {
htmlTargets.add(target);
}
}
if (htmlTargets.isNotEmpty) {
await _buildTests(targets: htmlTargets, forCanvasKit: false);
}
if (canvasKitTargets.isNotEmpty) {
await _buildTests(targets: canvasKitTargets, forCanvasKit: true);
}
stopwatch.stop();
print('The build took ${stopwatch.elapsedMilliseconds ~/ 1000} seconds.');
}
/// Whether to start the browser in debug mode.
///
/// In this mode the browser pauses before running the test to allow
......@@ -222,12 +263,18 @@ class TestCommand extends Command<bool> with ArgUtils {
/// ".dart_tool/goldens".
bool get doUpdateScreenshotGoldens => boolArg('update-screenshot-goldens');
Future<void> _runTargetTests(List<FilePath> targets) async {
/// Runs all tests specified in [targets].
///
/// Unlike [_runAllTestsForCurrentPlatform], this does not filter targets
/// by platform/browser capabilites, and instead attempts to run all of
/// them.
Future<void> _runSpecificTests(List<FilePath> targets) async {
await _runTestBatch(targets, concurrency: 1, expectFailure: false);
_checkExitCode();
}
Future<void> _runAllTests() async {
/// Runs as many tests as possible on the current OS/browser combination.
Future<void> _runAllTestsForCurrentPlatform() async {
final io.Directory testDir = io.Directory(path.join(
environment.webUiRootDir.path,
'test',
......@@ -373,21 +420,34 @@ class TestCommand extends Command<bool> with ArgUtils {
timestampFile.writeAsStringSync(timestamp);
}
Future<void> _buildTests({List<FilePath> targets}) async {
/// Builds the specific test [targets].
///
/// [targets] must not be null.
///
/// When building for CanvasKit we have to use a separate `build.canvaskit.yaml`
/// config file. Otherwise, `build.html.yaml` is used. Because `build_runner`
/// overwrites the output directories, we redirect the CanvasKit output to a
/// separate directory, then copy the files back to `build/test`.
Future<void> _buildTests({List<FilePath> targets, bool forCanvasKit}) async {
print(
'Building ${targets.length} targets for ${forCanvasKit ? 'CanvasKit' : 'HTML'}');
final String canvasKitOutputRelativePath =
path.join('.dart_tool', 'canvaskit_tests');
List<String> arguments = <String>[
'run',
'build_runner',
'build',
'test',
'-o',
'build',
if (targets != null)
for (FilePath path in targets) ...[
'--build-filter=${path.relativeToWebUi}.js',
'--build-filter=${path.relativeToWebUi}.browser_test.dart.js',
],
forCanvasKit ? canvasKitOutputRelativePath : 'build',
'--config',
// CanvasKit uses `build.canvaskit.yaml`, which HTML Uses `build.html.yaml`.
forCanvasKit ? 'canvaskit' : 'html',
for (FilePath path in targets) ...[
'--build-filter=${path.relativeToWebUi}.js',
'--build-filter=${path.relativeToWebUi}.browser_test.dart.js',
],
];
final Stopwatch stopwatch = Stopwatch()..start();
final int exitCode = await runProcess(
environment.pubExecutable,
......@@ -401,16 +461,28 @@ class TestCommand extends Command<bool> with ArgUtils {
// In a testing on a 32-core 132GB workstation increasing this number to
// 32 sped up the build from ~4min to ~1.5min.
if (io.Platform.environment.containsKey('BUILD_MAX_WORKERS_PER_TASK'))
'BUILD_MAX_WORKERS_PER_TASK': io.Platform.environment['BUILD_MAX_WORKERS_PER_TASK'],
'BUILD_MAX_WORKERS_PER_TASK':
io.Platform.environment['BUILD_MAX_WORKERS_PER_TASK'],
},
);
stopwatch.stop();
print('The build took ${stopwatch.elapsedMilliseconds ~/ 1000} seconds.');
if (exitCode != 0) {
throw ToolException(
'Failed to compile tests. Compiler exited with exit code $exitCode');
}
if (forCanvasKit) {
final io.Directory canvasKitTemporaryOutputDirectory = io.Directory(
path.join(environment.webUiRootDir.path, canvasKitOutputRelativePath,
'test', 'canvaskit'));
final io.Directory canvasKitOutputDirectory = io.Directory(
path.join(environment.webUiBuildDir.path, 'test', 'canvaskit'));
if (await canvasKitOutputDirectory.exists()) {
await canvasKitOutputDirectory.delete(recursive: true);
}
await canvasKitTemporaryOutputDirectory
.rename(canvasKitOutputDirectory.path);
}
}
/// Runs a batch of tests.
......
......@@ -11,13 +11,17 @@ part of engine;
class SkPath implements ui.Path {
js.JsObject _skPath;
/// Cached constructor function for `SkPath`, so we don't have to look it up
/// every time we construct a new path.
static final js.JsFunction _skPathConstructor = canvasKit['SkPath'];
SkPath() {
_skPath = js.JsObject(canvasKit['SkPath']);
_skPath = js.JsObject(_skPathConstructor);
fillType = ui.PathFillType.nonZero;
}
SkPath.from(SkPath other) {
_skPath = js.JsObject(canvasKit['SkPath'], <js.JsObject>[other._skPath]);
_skPath = js.JsObject(_skPathConstructor, <js.JsObject>[other._skPath]);
fillType = other.fillType;
}
......@@ -323,4 +327,9 @@ class SkPath implements ui.Path {
String toSvgString() {
return _skPath.callMethod('toSVGString');
}
/// Return `true` if this path contains no segments.
bool get isEmpty {
return _skPath.callMethod('isEmpty');
}
}
......@@ -7,120 +7,97 @@ part of engine;
class SkPathMetrics extends IterableBase<ui.PathMetric>
implements ui.PathMetrics {
SkPathMetrics(SkPath path, bool forceClosed)
: _iterator = SkPathMetricIterator._(_SkPathMeasure(path, forceClosed));
SkPathMetrics(this._path, this._forceClosed);
final Iterator<ui.PathMetric> _iterator;
final SkPath _path;
final bool _forceClosed;
/// The [SkPath.isEmpty] case is special-cased to avoid booting the WASM machinery just to find out there are no contours.
@override
Iterator<ui.PathMetric> get iterator => _iterator;
Iterator<ui.PathMetric> get iterator => _path.isEmpty ? const SkPathMetricIteratorEmpty._() : SkContourMeasureIter(_path, _forceClosed);
}
class SkPathMetricIterator implements Iterator<ui.PathMetric> {
SkPathMetricIterator._(this._pathMeasure) : assert(_pathMeasure != null);
class SkContourMeasureIter implements Iterator<ui.PathMetric> {
/// Cached constructor function for `SkContourMeasureIter`, so we don't have to look it
/// up every time we're constructing a new instance.
static final js.JsFunction _skContourMeasureIterConstructor = canvasKit['SkContourMeasureIter'];
_SkPathMetric _pathMetric;
_SkPathMeasure _pathMeasure;
SkContourMeasureIter(SkPath path, bool forceClosed)
: _skObject = js.JsObject(_skContourMeasureIterConstructor, <dynamic>[
path._skPath,
forceClosed,
1,
]);
/// The JavaScript `SkContourMeasureIter` object.
final js.JsObject _skObject;
/// A monotonically increasing counter used to generate [ui.PathMetric.contourIndex].
///
/// CanvasKit does not supply the contour index. We have to add it ourselves.
int _contourIndexCounter = 0;
@override
ui.PathMetric get current => _pathMetric;
ui.PathMetric get current => _current;
SkContourMeasure _current;
@override
bool moveNext() {
if (_pathMeasure._nextContour()) {
_pathMetric = _SkPathMetric._(_pathMeasure);
return true;
final js.JsObject skContourMeasure = _skObject.callMethod('next');
if (skContourMeasure == null) {
_current = null;
return false;
}
_pathMetric = null;
return false;
_current = SkContourMeasure(_contourIndexCounter, skContourMeasure);
_contourIndexCounter += 1;
return true;
}
}
class _SkPathMetric implements ui.PathMetric {
_SkPathMetric._(this._measure)
: assert(_measure != null),
length = _measure.length(_measure.currentContourIndex),
isClosed = _measure.isClosed(_measure.currentContourIndex),
contourIndex = _measure.currentContourIndex;
class SkContourMeasure implements ui.PathMetric {
SkContourMeasure(this.contourIndex, this._skObject);
@override
final double length;
@override
final bool isClosed;
final js.JsObject _skObject;
@override
final int contourIndex;
final _SkPathMeasure _measure;
@override
ui.Tangent getTangentForOffset(double distance) {
return _measure.getTangentForOffset(contourIndex, distance);
}
@override
ui.Path extractPath(double start, double end, {bool startWithMoveTo = true}) {
return _measure.extractPath(contourIndex, start, end,
startWithMoveTo: startWithMoveTo);
final js.JsObject skPath = _skObject
.callMethod('getSegment', <dynamic>[start, end, startWithMoveTo]);
return SkPath._fromSkPath(skPath);
}
@override
String toString() => 'PathMetric{length: $length, isClosed: $isClosed, '
'contourIndex: $contourIndex}';
}
class _SkPathMeasure {
_SkPathMeasure(SkPath path, bool forceClosed) {
currentContourIndex = -1;
pathMeasure = js.JsObject(canvasKit['SkPathMeasure'], <dynamic>[
path._skPath,
forceClosed,
1,
]);
}
js.JsObject pathMeasure;
double length(int contourIndex) {
assert(contourIndex == currentContourIndex,
'PathMetrics are invalid if it is not the current contour.');
return pathMeasure.callMethod('getLength');
}
ui.Tangent getTangentForOffset(int contourIndex, double distance) {
assert(contourIndex == currentContourIndex,
'PathMetrics are invalid if it is not the current contour.');
final js.JsObject posTan =
pathMeasure.callMethod('getPosTan', <double>[distance]);
ui.Tangent getTangentForOffset(double distance) {
final js.JsObject posTan = _skObject.callMethod('getPosTan', <double>[distance]);
return ui.Tangent(
ui.Offset(posTan[0], posTan[1]),
ui.Offset(posTan[2], posTan[3]),
);
}
ui.Path extractPath(int contourIndex, double start, double end,
{bool startWithMoveTo = true}) {
assert(contourIndex == currentContourIndex,
'PathMetrics are invalid if it is not the current contour.');
final js.JsObject skPath = pathMeasure
.callMethod('getSegment', <dynamic>[start, end, startWithMoveTo]);
return SkPath._fromSkPath(skPath);
@override
bool get isClosed {
return _skObject.callMethod('isClosed');
}
bool isClosed(int contourIndex) {
assert(contourIndex == currentContourIndex,
'PathMetrics are invalid if it is not the current contour.');
return pathMeasure.callMethod('isClosed');
@override
double get length {
return _skObject.callMethod('length');
}
}
bool _nextContour() {
final bool next = pathMeasure.callMethod('nextContour');
if (next) {
currentContourIndex++;
}
return next;
}
class SkPathMetricIteratorEmpty implements Iterator<ui.PathMetric> {
const SkPathMetricIteratorEmpty._();
@override
ui.PathMetric get current => null;
int currentContourIndex;
@override
bool moveNext() {
return false;
}
}
......@@ -17,6 +17,8 @@ dev_dependencies:
build_runner: 1.7.2
build_test: 1.0.0
build_web_compilers: 2.7.1
# TODO(yjbanov): the Dart SDK complains about nonVirtual for some reason.
petitparser: 3.0.2
yaml: 2.2.0
watcher: 0.9.7+12
web_engine_tester:
......
// 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
import 'package:test/test.dart';
import 'package:ui/src/engine.dart';
import 'package:ui/ui.dart' as ui;
void main() {
setUpAll(() async {
await ui.webOnlyInitializePlatform();
});
test('Using CanvasKit', () {
expect(experimentalUseSkia, true);
});
test(SkPathMetrics, () {
final ui.Path path = ui.Path();
expect(path, isA<SkPath>());
expect(path.computeMetrics().length, 0);
path.addRect(ui.Rect.fromLTRB(0, 0, 10, 10));
final ui.PathMetric metric = path.computeMetrics().single;
expect(metric.contourIndex, 0);
expect(metric.extractPath(0, 0.5).computeMetrics().length, 1);
final ui.Tangent tangent1 = metric.getTangentForOffset(5);
expect(tangent1.position, ui.Offset(5, 0));
expect(tangent1.vector, ui.Offset(1, 0));
final ui.Tangent tangent2 = metric.getTangentForOffset(15);
expect(tangent2.position, ui.Offset(10, 5));
expect(tangent2.vector, ui.Offset(0, 1));
expect(metric.isClosed, true);
path.addOval(ui.Rect.fromLTRB(10, 10, 100, 100));
expect(path.computeMetrics().length, 2);
// Path metrics can be iterated over multiple times.
final ui.PathMetrics metrics = path.computeMetrics();
expect(metrics.toList().length, 2);
expect(metrics.toList().length, 2);
expect(metrics.toList().length, 2);
// Can simultaneously iterate over multiple metrics from the same path.
final ui.PathMetrics metrics1 = path.computeMetrics();
final ui.PathMetrics metrics2 = path.computeMetrics();
final Iterator<ui.PathMetric> iter1 = metrics1.iterator;
final Iterator<ui.PathMetric> iter2 = metrics2.iterator;
expect(iter1.moveNext(), true);
expect(iter2.moveNext(), true);
expect(iter1.current, isNotNull);
expect(iter2.current, isNotNull);
expect(iter1.moveNext(), true);
expect(iter2.moveNext(), true);
expect(iter1.current, isNotNull);
expect(iter2.current, isNotNull);
expect(iter1.moveNext(), false);
expect(iter2.moveNext(), false);
expect(iter1.current, isNull);
expect(iter2.current, isNull);
});
}
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册