mirror of
https://github.com/flutter/flutter.git
synced 2026-02-20 02:29:02 +08:00
Lazy paths and frame object arenas (#168996)
The lifecycle of `Path` objects are currently not managed by the user. That is to say, there is no `dispose` method on path objects and therefore no explicit way to detect when the user is done with the path object and the native-side object can be exposed. As of right now, we use `FinalizationRegistry` to clean up the native-side objects when the dart-side objects are garbage collected. However, this has a number of issues: * Adding objects to the finalization registry actually ends up prolonging their lifetime in V8, since the V8 garbage collector will only collect them in a major GC and not a minor GC once they are registered with the finalization registry. See the following Chrome bug: https://issues.chromium.org/issues/340777103 * We can run into OOM issues where the linear memory of canvaskit/skwasm exceeds 2GB if the collection of paths go on too long. * Even if the paths do get collected by the GC, they often happen infrequently enough that paths over many frames have accumulated and are being collected all at once. This gap can often be dozens or hundreds of frames long, and when collection does occur it is freeing a lot of paths at once, which causes a janky frame. I have seen this take upwards of 800ms on my M1 Macbook Pro. There are some more details in https://github.com/flutter/flutter/issues/153678 This PR alleviates this issue by creating a `LazyPath` object. This object is added to an arena that explicitly collects the underlying native objects at the end of each frame. The object also tracks the API calls made to it so that if it is actually used across a frame boundary that we can recreate the native object if it was freed. Running our benchmarks, this has a non-trivial performance cost to building and using these paths (30-50% in a microbenchmark, 3-6% in a broader full app benchmark). However, as a team we've decided that this cost is worth it to avoid OOM issues as well as the non-deterministic jank associated with large collections of these objects.
This commit is contained in:
parent
ed275039e1
commit
a11524896e
@ -52156,6 +52156,7 @@ ORIGIN: ../../../flutter/lib/web_ui/lib/semantics.dart + ../../../flutter/LICENS
|
||||
ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine.dart + ../../../flutter/LICENSE
|
||||
ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/alarm_clock.dart + ../../../flutter/LICENSE
|
||||
ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/app_bootstrap.dart + ../../../flutter/LICENSE
|
||||
ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/arena.dart + ../../../flutter/LICENSE
|
||||
ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/browser_detection.dart + ../../../flutter/LICENSE
|
||||
ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/canvas.dart + ../../../flutter/LICENSE
|
||||
ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/canvaskit_api.dart + ../../../flutter/LICENSE
|
||||
@ -52217,6 +52218,7 @@ ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/js_interop/js_typed_data.dart
|
||||
ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/key_map.g.dart + ../../../flutter/LICENSE
|
||||
ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/keyboard_binding.dart + ../../../flutter/LICENSE
|
||||
ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/layers.dart + ../../../flutter/LICENSE
|
||||
ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/lazy_path.dart + ../../../flutter/LICENSE
|
||||
ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/mouse/context_menu.dart + ../../../flutter/LICENSE
|
||||
ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/mouse/cursor.dart + ../../../flutter/LICENSE
|
||||
ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/mouse/prevent_default.dart + ../../../flutter/LICENSE
|
||||
@ -55182,6 +55184,7 @@ FILE: ../../../flutter/lib/web_ui/lib/semantics.dart
|
||||
FILE: ../../../flutter/lib/web_ui/lib/src/engine.dart
|
||||
FILE: ../../../flutter/lib/web_ui/lib/src/engine/alarm_clock.dart
|
||||
FILE: ../../../flutter/lib/web_ui/lib/src/engine/app_bootstrap.dart
|
||||
FILE: ../../../flutter/lib/web_ui/lib/src/engine/arena.dart
|
||||
FILE: ../../../flutter/lib/web_ui/lib/src/engine/browser_detection.dart
|
||||
FILE: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/canvas.dart
|
||||
FILE: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/canvaskit_api.dart
|
||||
@ -55243,6 +55246,7 @@ FILE: ../../../flutter/lib/web_ui/lib/src/engine/js_interop/js_typed_data.dart
|
||||
FILE: ../../../flutter/lib/web_ui/lib/src/engine/key_map.g.dart
|
||||
FILE: ../../../flutter/lib/web_ui/lib/src/engine/keyboard_binding.dart
|
||||
FILE: ../../../flutter/lib/web_ui/lib/src/engine/layers.dart
|
||||
FILE: ../../../flutter/lib/web_ui/lib/src/engine/lazy_path.dart
|
||||
FILE: ../../../flutter/lib/web_ui/lib/src/engine/mouse/context_menu.dart
|
||||
FILE: ../../../flutter/lib/web_ui/lib/src/engine/mouse/cursor.dart
|
||||
FILE: ../../../flutter/lib/web_ui/lib/src/engine/mouse/prevent_default.dart
|
||||
|
||||
@ -17,6 +17,7 @@ library engine;
|
||||
|
||||
export 'engine/alarm_clock.dart';
|
||||
export 'engine/app_bootstrap.dart';
|
||||
export 'engine/arena.dart';
|
||||
export 'engine/browser_detection.dart';
|
||||
export 'engine/canvaskit/canvas.dart';
|
||||
export 'engine/canvaskit/canvaskit_api.dart';
|
||||
@ -77,6 +78,7 @@ export 'engine/js_interop/js_typed_data.dart';
|
||||
export 'engine/key_map.g.dart';
|
||||
export 'engine/keyboard_binding.dart';
|
||||
export 'engine/layers.dart';
|
||||
export 'engine/lazy_path.dart';
|
||||
export 'engine/mouse/context_menu.dart';
|
||||
export 'engine/mouse/cursor.dart';
|
||||
export 'engine/mouse/prevent_default.dart';
|
||||
|
||||
22
engine/src/flutter/lib/web_ui/lib/src/engine/arena.dart
Normal file
22
engine/src/flutter/lib/web_ui/lib/src/engine/arena.dart
Normal file
@ -0,0 +1,22 @@
|
||||
// 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.
|
||||
|
||||
abstract class Collectable {
|
||||
void collect();
|
||||
}
|
||||
|
||||
class Arena {
|
||||
List<Collectable> _collectables = <Collectable>[];
|
||||
void add(Collectable collectable) {
|
||||
_collectables.add(collectable);
|
||||
}
|
||||
|
||||
void collect() {
|
||||
final List<Collectable> collectables = _collectables;
|
||||
_collectables = <Collectable>[];
|
||||
for (final collect in collectables) {
|
||||
collect.collect();
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -4,20 +4,9 @@
|
||||
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:ui/src/engine.dart';
|
||||
import 'package:ui/ui.dart' as ui;
|
||||
|
||||
import '../validators.dart';
|
||||
import '../vector_math.dart';
|
||||
import 'canvas.dart';
|
||||
import 'canvaskit_api.dart';
|
||||
import 'image.dart';
|
||||
import 'painting.dart';
|
||||
import 'path.dart';
|
||||
import 'picture.dart';
|
||||
import 'picture_recorder.dart';
|
||||
import 'text.dart';
|
||||
import 'vertices.dart';
|
||||
|
||||
/// An implementation of [ui.Canvas] that is backed by a CanvasKit canvas.
|
||||
class CanvasKitCanvas implements ui.Canvas {
|
||||
factory CanvasKitCanvas(ui.PictureRecorder recorder, [ui.Rect? cullRect]) {
|
||||
@ -140,7 +129,7 @@ class CanvasKitCanvas implements ui.Canvas {
|
||||
|
||||
@override
|
||||
void clipPath(ui.Path path, {bool doAntiAlias = true}) {
|
||||
_canvas.clipPath(path as CkPath, doAntiAlias);
|
||||
_canvas.clipPath((path as LazyPath).builtPath as CkPath, doAntiAlias);
|
||||
}
|
||||
|
||||
@override
|
||||
@ -264,7 +253,7 @@ class CanvasKitCanvas implements ui.Canvas {
|
||||
|
||||
@override
|
||||
void drawPath(ui.Path path, ui.Paint paint) {
|
||||
_canvas.drawPath(path as CkPath, paint as CkPaint);
|
||||
_canvas.drawPath((path as LazyPath).builtPath as CkPath, paint as CkPaint);
|
||||
}
|
||||
|
||||
@override
|
||||
@ -436,6 +425,11 @@ class CanvasKitCanvas implements ui.Canvas {
|
||||
}
|
||||
|
||||
void _drawShadow(ui.Path path, ui.Color color, double elevation, bool transparentOccluder) {
|
||||
_canvas.drawShadow(path as CkPath, color, elevation, transparentOccluder);
|
||||
_canvas.drawShadow(
|
||||
(path as LazyPath).builtPath as CkPath,
|
||||
color,
|
||||
elevation,
|
||||
transparentOccluder,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -4,14 +4,9 @@
|
||||
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:ui/src/engine.dart';
|
||||
import 'package:ui/ui.dart' as ui;
|
||||
|
||||
import '../vector_math.dart';
|
||||
import 'layer.dart';
|
||||
import 'layer_tree.dart';
|
||||
import 'path.dart';
|
||||
import 'picture.dart';
|
||||
|
||||
class LayerScene implements ui.Scene {
|
||||
LayerScene(RootLayer rootLayer) : layerTree = LayerTree(rootLayer);
|
||||
|
||||
@ -114,7 +109,9 @@ class LayerSceneBuilder implements ui.SceneBuilder {
|
||||
ui.Clip clipBehavior = ui.Clip.antiAlias,
|
||||
ui.EngineLayer? oldLayer,
|
||||
}) {
|
||||
return pushLayer<ClipPathEngineLayer>(ClipPathEngineLayer(path as CkPath, clipBehavior));
|
||||
return pushLayer<ClipPathEngineLayer>(
|
||||
ClipPathEngineLayer((path as LazyPath).builtPath as CkPath, clipBehavior),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
|
||||
@ -7,7 +7,7 @@ import 'dart:typed_data';
|
||||
|
||||
import 'package:ui/ui.dart' as ui;
|
||||
|
||||
import '../scene_painting.dart';
|
||||
import '../lazy_path.dart';
|
||||
import '../vector_math.dart';
|
||||
import 'canvaskit_api.dart';
|
||||
import 'native_memory.dart';
|
||||
@ -16,7 +16,7 @@ import 'path_metrics.dart';
|
||||
/// An implementation of [ui.Path] which is backed by an `SkPath`.
|
||||
///
|
||||
/// The `SkPath` is required for `CkCanvas` methods which take a path.
|
||||
class CkPath implements ScenePath {
|
||||
class CkPath implements DisposablePath {
|
||||
factory CkPath() {
|
||||
final SkPath skPath = SkPath();
|
||||
skPath.setFillType(toSkFillType(ui.PathFillType.nonZero));
|
||||
@ -44,6 +44,11 @@ class CkPath implements ScenePath {
|
||||
|
||||
ui.PathFillType _fillType;
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_ref.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
ui.PathFillType get fillType => _fillType;
|
||||
|
||||
@ -155,7 +160,7 @@ class CkPath implements ScenePath {
|
||||
}
|
||||
|
||||
@override
|
||||
ui.PathMetrics computeMetrics({bool forceClosed = false}) {
|
||||
CkPathMetrics computeMetrics({bool forceClosed = false}) {
|
||||
return CkPathMetrics(this, forceClosed);
|
||||
}
|
||||
|
||||
@ -310,3 +315,13 @@ class CkPath implements ScenePath {
|
||||
return skiaObject.isEmpty();
|
||||
}
|
||||
}
|
||||
|
||||
class CkPathConstructors implements DisposablePathConstructors {
|
||||
@override
|
||||
CkPath createNew() => CkPath();
|
||||
|
||||
@override
|
||||
CkPath combinePaths(ui.PathOperation operation, DisposablePath path1, DisposablePath path2) {
|
||||
return CkPath.combine(operation, path1 as CkPath, path2 as CkPath);
|
||||
}
|
||||
}
|
||||
|
||||
@ -5,13 +5,10 @@
|
||||
import 'dart:collection';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:ui/src/engine.dart';
|
||||
import 'package:ui/ui.dart' as ui;
|
||||
|
||||
import 'canvaskit_api.dart';
|
||||
import 'native_memory.dart';
|
||||
import 'path.dart';
|
||||
|
||||
class CkPathMetrics extends IterableBase<ui.PathMetric> implements ui.PathMetrics {
|
||||
class CkPathMetrics extends IterableBase<ui.PathMetric> implements DisposablePathMetrics {
|
||||
CkPathMetrics(this._path, this._forceClosed);
|
||||
|
||||
final CkPath _path;
|
||||
@ -19,11 +16,11 @@ class CkPathMetrics extends IterableBase<ui.PathMetric> implements ui.PathMetric
|
||||
|
||||
/// The [CkPath.isEmpty] case is special-cased to avoid booting the WASM machinery just to find out there are no contours.
|
||||
@override
|
||||
late final Iterator<ui.PathMetric> iterator =
|
||||
late final DisposablePathMetricIterator iterator =
|
||||
_path.isEmpty ? const CkPathMetricIteratorEmpty._() : CkContourMeasureIter(this);
|
||||
}
|
||||
|
||||
class CkContourMeasureIter implements Iterator<ui.PathMetric> {
|
||||
class CkContourMeasureIter implements DisposablePathMetricIterator {
|
||||
CkContourMeasureIter(this._metrics) {
|
||||
_ref = UniqueRef<SkContourMeasureIter>(
|
||||
this,
|
||||
@ -32,6 +29,11 @@ class CkContourMeasureIter implements Iterator<ui.PathMetric> {
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_ref.dispose();
|
||||
}
|
||||
|
||||
final CkPathMetrics _metrics;
|
||||
late final UniqueRef<SkContourMeasureIter> _ref;
|
||||
|
||||
@ -43,8 +45,8 @@ class CkContourMeasureIter implements Iterator<ui.PathMetric> {
|
||||
int _contourIndexCounter = 0;
|
||||
|
||||
@override
|
||||
ui.PathMetric get current {
|
||||
final ui.PathMetric? currentMetric = _current;
|
||||
CkContourMeasure get current {
|
||||
final CkContourMeasure? currentMetric = _current;
|
||||
if (currentMetric == null) {
|
||||
throw RangeError(
|
||||
'PathMetricIterator is not pointing to a PathMetric. This can happen in two situations:\n'
|
||||
@ -71,7 +73,7 @@ class CkContourMeasureIter implements Iterator<ui.PathMetric> {
|
||||
}
|
||||
}
|
||||
|
||||
class CkContourMeasure implements ui.PathMetric {
|
||||
class CkContourMeasure implements DisposablePathMetric {
|
||||
CkContourMeasure(this._metrics, SkContourMeasure skiaObject, this.contourIndex) {
|
||||
_ref = UniqueRef<SkContourMeasure>(this, skiaObject, 'PathMetric');
|
||||
}
|
||||
@ -85,11 +87,16 @@ class CkContourMeasure implements ui.PathMetric {
|
||||
|
||||
SkContourMeasure get skiaObject => _ref.nativeObject;
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_ref.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
final int contourIndex;
|
||||
|
||||
@override
|
||||
ui.Path extractPath(double start, double end, {bool startWithMoveTo = true}) {
|
||||
CkPath extractPath(double start, double end, {bool startWithMoveTo = true}) {
|
||||
final SkPath skPath = skiaObject.getSegment(start, end, startWithMoveTo);
|
||||
return CkPath.fromSkPath(skPath, _metrics._path.fillType);
|
||||
}
|
||||
@ -111,11 +118,11 @@ class CkContourMeasure implements ui.PathMetric {
|
||||
}
|
||||
}
|
||||
|
||||
class CkPathMetricIteratorEmpty implements Iterator<ui.PathMetric> {
|
||||
class CkPathMetricIteratorEmpty implements DisposablePathMetricIterator {
|
||||
const CkPathMetricIteratorEmpty._();
|
||||
|
||||
@override
|
||||
ui.PathMetric get current {
|
||||
CkContourMeasure get current {
|
||||
throw RangeError('PathMetric iterator is empty.');
|
||||
}
|
||||
|
||||
@ -123,4 +130,7 @@ class CkPathMetricIteratorEmpty implements Iterator<ui.PathMetric> {
|
||||
bool moveNext() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {}
|
||||
}
|
||||
|
||||
@ -328,15 +328,17 @@ class CanvasKitRenderer implements Renderer {
|
||||
ui.FilterQuality? filterQuality,
|
||||
) => CkImageShader(image, tmx, tmy, matrix4, filterQuality);
|
||||
|
||||
@override
|
||||
ui.Path createPath() => CkPath();
|
||||
CkPathConstructors pathConstructors = CkPathConstructors();
|
||||
|
||||
@override
|
||||
ui.Path copyPath(ui.Path src) => CkPath.from(src as CkPath);
|
||||
ui.Path createPath() => LazyPath(pathConstructors);
|
||||
|
||||
@override
|
||||
ui.Path copyPath(ui.Path src) => LazyPath.fromLazyPath(src as LazyPath);
|
||||
|
||||
@override
|
||||
ui.Path combinePaths(ui.PathOperation op, ui.Path path1, ui.Path path2) =>
|
||||
CkPath.combine(op, path1, path2);
|
||||
LazyPath.combined(op, path1 as LazyPath, path2 as LazyPath);
|
||||
|
||||
@override
|
||||
ui.TextStyle createTextStyle({
|
||||
|
||||
738
engine/src/flutter/lib/web_ui/lib/src/engine/lazy_path.dart
Normal file
738
engine/src/flutter/lib/web_ui/lib/src/engine/lazy_path.dart
Normal file
@ -0,0 +1,738 @@
|
||||
// Copyright 2013 The Flutter Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'dart:collection';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:ui/src/engine.dart';
|
||||
import 'package:ui/ui.dart' as ui;
|
||||
|
||||
abstract class DisposablePath implements ScenePath {
|
||||
@override
|
||||
DisposablePathMetrics computeMetrics({bool forceClosed = false});
|
||||
|
||||
void dispose();
|
||||
}
|
||||
|
||||
abstract class DisposablePathMetricIterator implements Iterator<ui.PathMetric> {
|
||||
@override
|
||||
DisposablePathMetric get current;
|
||||
|
||||
void dispose();
|
||||
}
|
||||
|
||||
abstract class DisposablePathMetrics implements ui.PathMetrics {
|
||||
@override
|
||||
DisposablePathMetricIterator get iterator;
|
||||
}
|
||||
|
||||
abstract class DisposablePathMetric implements ui.PathMetric {
|
||||
@override
|
||||
DisposablePath extractPath(double start, double end, {bool startWithMoveTo = true});
|
||||
|
||||
void dispose();
|
||||
}
|
||||
|
||||
sealed class PathCommand {
|
||||
void apply(DisposablePath path);
|
||||
}
|
||||
|
||||
final class MoveToCommand implements PathCommand {
|
||||
MoveToCommand(this.x, this.y);
|
||||
|
||||
final double x;
|
||||
final double y;
|
||||
|
||||
@override
|
||||
void apply(DisposablePath path) {
|
||||
path.moveTo(x, y);
|
||||
}
|
||||
}
|
||||
|
||||
final class RelativeMoveToCommand implements PathCommand {
|
||||
RelativeMoveToCommand(this.dx, this.dy);
|
||||
|
||||
final double dx;
|
||||
final double dy;
|
||||
|
||||
@override
|
||||
void apply(DisposablePath path) {
|
||||
path.relativeMoveTo(dx, dy);
|
||||
}
|
||||
}
|
||||
|
||||
final class LineToCommand implements PathCommand {
|
||||
LineToCommand(this.x, this.y);
|
||||
|
||||
final double x;
|
||||
final double y;
|
||||
|
||||
@override
|
||||
void apply(DisposablePath path) {
|
||||
path.lineTo(x, y);
|
||||
}
|
||||
}
|
||||
|
||||
final class RelativeLineToCommand implements PathCommand {
|
||||
RelativeLineToCommand(this.dx, this.dy);
|
||||
|
||||
final double dx;
|
||||
final double dy;
|
||||
|
||||
@override
|
||||
void apply(DisposablePath path) {
|
||||
path.relativeLineTo(dx, dy);
|
||||
}
|
||||
}
|
||||
|
||||
final class QuadraticBezierToCommand implements PathCommand {
|
||||
QuadraticBezierToCommand(this.x1, this.y1, this.x2, this.y2);
|
||||
|
||||
final double x1;
|
||||
final double y1;
|
||||
final double x2;
|
||||
final double y2;
|
||||
|
||||
@override
|
||||
void apply(DisposablePath path) {
|
||||
path.quadraticBezierTo(x1, y1, x2, y2);
|
||||
}
|
||||
}
|
||||
|
||||
final class RelativeQuadraticBezierToCommand implements PathCommand {
|
||||
RelativeQuadraticBezierToCommand(this.x1, this.y1, this.x2, this.y2);
|
||||
|
||||
final double x1;
|
||||
final double y1;
|
||||
final double x2;
|
||||
final double y2;
|
||||
|
||||
@override
|
||||
void apply(DisposablePath path) {
|
||||
path.relativeQuadraticBezierTo(x1, y1, x2, y2);
|
||||
}
|
||||
}
|
||||
|
||||
final class CubicToCommand implements PathCommand {
|
||||
CubicToCommand(this.x1, this.y1, this.x2, this.y2, this.x3, this.y3);
|
||||
|
||||
final double x1;
|
||||
final double y1;
|
||||
final double x2;
|
||||
final double y2;
|
||||
final double x3;
|
||||
final double y3;
|
||||
|
||||
@override
|
||||
void apply(DisposablePath path) {
|
||||
path.cubicTo(x1, y1, x2, y2, x3, y3);
|
||||
}
|
||||
}
|
||||
|
||||
final class RelativeCubicToCommand implements PathCommand {
|
||||
RelativeCubicToCommand(this.x1, this.y1, this.x2, this.y2, this.x3, this.y3);
|
||||
|
||||
final double x1;
|
||||
final double y1;
|
||||
final double x2;
|
||||
final double y2;
|
||||
final double x3;
|
||||
final double y3;
|
||||
|
||||
@override
|
||||
void apply(DisposablePath path) {
|
||||
path.relativeCubicTo(x1, y1, x2, y2, x3, y3);
|
||||
}
|
||||
}
|
||||
|
||||
final class ConicToCommand implements PathCommand {
|
||||
ConicToCommand(this.x1, this.y1, this.x2, this.y2, this.w);
|
||||
|
||||
final double x1;
|
||||
final double y1;
|
||||
final double x2;
|
||||
final double y2;
|
||||
final double w;
|
||||
|
||||
@override
|
||||
void apply(DisposablePath path) {
|
||||
path.conicTo(x1, y1, x2, y2, w);
|
||||
}
|
||||
}
|
||||
|
||||
final class RelativeConicToCommand implements PathCommand {
|
||||
RelativeConicToCommand(this.x1, this.y1, this.x2, this.y2, this.w);
|
||||
|
||||
final double x1;
|
||||
final double y1;
|
||||
final double x2;
|
||||
final double y2;
|
||||
final double w;
|
||||
|
||||
@override
|
||||
void apply(DisposablePath path) {
|
||||
path.relativeConicTo(x1, y1, x2, y2, w);
|
||||
}
|
||||
}
|
||||
|
||||
final class ArcToCommand implements PathCommand {
|
||||
ArcToCommand(this.rect, this.startAngle, this.sweepAngle, this.forceMoveTo);
|
||||
|
||||
final ui.Rect rect;
|
||||
final double startAngle;
|
||||
final double sweepAngle;
|
||||
bool forceMoveTo;
|
||||
|
||||
@override
|
||||
void apply(DisposablePath path) {
|
||||
path.arcTo(rect, startAngle, sweepAngle, forceMoveTo);
|
||||
}
|
||||
}
|
||||
|
||||
final class ArcToPointCommand implements PathCommand {
|
||||
ArcToPointCommand(
|
||||
this.arcEnd, {
|
||||
required this.radius,
|
||||
required this.rotation,
|
||||
required this.largeArc,
|
||||
required this.clockwise,
|
||||
});
|
||||
|
||||
ui.Offset arcEnd;
|
||||
ui.Radius radius;
|
||||
double rotation;
|
||||
bool largeArc;
|
||||
bool clockwise;
|
||||
|
||||
@override
|
||||
void apply(DisposablePath path) {
|
||||
path.arcToPoint(
|
||||
arcEnd,
|
||||
radius: radius,
|
||||
rotation: rotation,
|
||||
largeArc: largeArc,
|
||||
clockwise: clockwise,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
final class RelativeArcToPointCommand implements PathCommand {
|
||||
RelativeArcToPointCommand(
|
||||
this.arcEndDelta, {
|
||||
required this.radius,
|
||||
required this.rotation,
|
||||
required this.largeArc,
|
||||
required this.clockwise,
|
||||
});
|
||||
|
||||
ui.Offset arcEndDelta;
|
||||
ui.Radius radius;
|
||||
double rotation;
|
||||
bool largeArc;
|
||||
bool clockwise;
|
||||
|
||||
@override
|
||||
void apply(DisposablePath path) {
|
||||
path.relativeArcToPoint(
|
||||
arcEndDelta,
|
||||
radius: radius,
|
||||
rotation: rotation,
|
||||
largeArc: largeArc,
|
||||
clockwise: clockwise,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
final class AddRectCommand implements PathCommand {
|
||||
AddRectCommand(this.rect);
|
||||
|
||||
final ui.Rect rect;
|
||||
|
||||
@override
|
||||
void apply(DisposablePath path) {
|
||||
path.addRect(rect);
|
||||
}
|
||||
}
|
||||
|
||||
final class AddOvalCommand implements PathCommand {
|
||||
AddOvalCommand(this.oval);
|
||||
|
||||
final ui.Rect oval;
|
||||
|
||||
@override
|
||||
void apply(DisposablePath path) {
|
||||
path.addOval(oval);
|
||||
}
|
||||
}
|
||||
|
||||
final class AddArcCommand implements PathCommand {
|
||||
AddArcCommand(this.oval, this.startAngle, this.sweepAngle);
|
||||
|
||||
final ui.Rect oval;
|
||||
final double startAngle;
|
||||
final double sweepAngle;
|
||||
|
||||
@override
|
||||
void apply(DisposablePath path) {
|
||||
path.addArc(oval, startAngle, sweepAngle);
|
||||
}
|
||||
}
|
||||
|
||||
final class AddPolygonCommand implements PathCommand {
|
||||
AddPolygonCommand(this.points, this.close);
|
||||
|
||||
final List<ui.Offset> points;
|
||||
final bool close;
|
||||
|
||||
@override
|
||||
void apply(DisposablePath path) {
|
||||
path.addPolygon(points, close);
|
||||
}
|
||||
}
|
||||
|
||||
final class AddRRectCommand implements PathCommand {
|
||||
AddRRectCommand(this.rrect);
|
||||
|
||||
final ui.RRect rrect;
|
||||
|
||||
@override
|
||||
void apply(DisposablePath path) {
|
||||
path.addRRect(rrect);
|
||||
}
|
||||
}
|
||||
|
||||
final class AddRSuperellipseCommand implements PathCommand {
|
||||
AddRSuperellipseCommand(this.rSuperellipse);
|
||||
|
||||
final ui.RSuperellipse rSuperellipse;
|
||||
|
||||
@override
|
||||
void apply(DisposablePath path) {
|
||||
path.addRSuperellipse(rSuperellipse);
|
||||
}
|
||||
}
|
||||
|
||||
final class AddPathCommand implements PathCommand {
|
||||
AddPathCommand(this.path, this.offset, {this.matrix4});
|
||||
|
||||
final LazyPath path;
|
||||
final ui.Offset offset;
|
||||
final Float64List? matrix4;
|
||||
|
||||
@override
|
||||
void apply(DisposablePath p) {
|
||||
p.addPath(path.builtPath, offset, matrix4: matrix4);
|
||||
}
|
||||
}
|
||||
|
||||
final class ExtendWithPathCommand implements PathCommand {
|
||||
ExtendWithPathCommand(this.path, this.offset, {this.matrix4});
|
||||
|
||||
final LazyPath path;
|
||||
final ui.Offset offset;
|
||||
final Float64List? matrix4;
|
||||
|
||||
@override
|
||||
void apply(DisposablePath p) {
|
||||
p.extendWithPath(path.builtPath, offset, matrix4: matrix4);
|
||||
}
|
||||
}
|
||||
|
||||
final class ClosePathCommand implements PathCommand {
|
||||
@override
|
||||
void apply(DisposablePath path) {
|
||||
path.close();
|
||||
}
|
||||
}
|
||||
|
||||
abstract class DisposablePathConstructors {
|
||||
DisposablePath createNew();
|
||||
DisposablePath combinePaths(
|
||||
ui.PathOperation operation,
|
||||
DisposablePath path1,
|
||||
DisposablePath path2,
|
||||
);
|
||||
}
|
||||
|
||||
class LazyPath implements ScenePath, Collectable {
|
||||
factory LazyPath(DisposablePathConstructors constructors) =>
|
||||
LazyPath._(constructors, ui.PathFillType.nonZero, () => constructors.createNew());
|
||||
LazyPath._(this.constructors, this._fillType, this.initializer) : _commands = [];
|
||||
LazyPath.fromLazyPath(LazyPath other)
|
||||
: _fillType = other._fillType,
|
||||
constructors = other.constructors,
|
||||
initializer = other.initializer,
|
||||
_commands = List.from(other._commands);
|
||||
factory LazyPath.shifted(LazyPath basePath, ui.Offset offset) {
|
||||
final pathCopy = LazyPath.fromLazyPath(basePath);
|
||||
return LazyPath._(
|
||||
pathCopy.constructors,
|
||||
pathCopy._fillType,
|
||||
() => pathCopy.builtPath.shift(offset) as DisposablePath,
|
||||
);
|
||||
}
|
||||
factory LazyPath.transformed(LazyPath basePath, Float64List matrix4) {
|
||||
final pathCopy = LazyPath.fromLazyPath(basePath);
|
||||
return LazyPath._(
|
||||
pathCopy.constructors,
|
||||
pathCopy._fillType,
|
||||
() => pathCopy.builtPath.transform(matrix4) as DisposablePath,
|
||||
);
|
||||
}
|
||||
factory LazyPath.combined(ui.PathOperation operation, LazyPath path1, LazyPath path2) {
|
||||
final pathCopy1 = LazyPath.fromLazyPath(path1);
|
||||
final pathCopy2 = LazyPath.fromLazyPath(path2);
|
||||
return LazyPath._(
|
||||
pathCopy1.constructors,
|
||||
pathCopy1._fillType,
|
||||
() =>
|
||||
pathCopy1.constructors.combinePaths(operation, pathCopy1.builtPath, pathCopy2.builtPath),
|
||||
);
|
||||
}
|
||||
LazyPath extracted(
|
||||
LazyPathMetric pathMetric,
|
||||
double start,
|
||||
double end, {
|
||||
bool startWithMoveTo = true,
|
||||
}) {
|
||||
return LazyPath._(
|
||||
constructors,
|
||||
pathMetric.iterator.path._fillType,
|
||||
() => pathMetric.buildExtractedPath(start, end, startWithMoveTo: startWithMoveTo),
|
||||
);
|
||||
}
|
||||
|
||||
DisposablePathConstructors constructors;
|
||||
DisposablePath Function() initializer;
|
||||
|
||||
ui.PathFillType _fillType;
|
||||
|
||||
@override
|
||||
ui.PathFillType get fillType => _fillType;
|
||||
|
||||
@override
|
||||
set fillType(ui.PathFillType fillType) {
|
||||
_fillType = fillType;
|
||||
_cachedPath?.fillType = fillType;
|
||||
}
|
||||
|
||||
DisposablePath? _cachedPath;
|
||||
final List<PathCommand> _commands;
|
||||
|
||||
DisposablePath get builtPath {
|
||||
if (_cachedPath != null) {
|
||||
return _cachedPath!;
|
||||
}
|
||||
final DisposablePath path = initializer();
|
||||
path.fillType = _fillType;
|
||||
for (final command in _commands) {
|
||||
command.apply(path);
|
||||
}
|
||||
|
||||
_cachedPath = path;
|
||||
EnginePlatformDispatcher.instance.frameArena.add(this);
|
||||
return path;
|
||||
}
|
||||
|
||||
void _addCommand(PathCommand command) {
|
||||
_commands.add(command);
|
||||
if (_cachedPath != null) {
|
||||
command.apply(_cachedPath!);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void moveTo(double x, double y) {
|
||||
_addCommand(MoveToCommand(x, y));
|
||||
}
|
||||
|
||||
@override
|
||||
void relativeMoveTo(double dx, double dy) {
|
||||
_addCommand(RelativeMoveToCommand(dx, dy));
|
||||
}
|
||||
|
||||
@override
|
||||
void lineTo(double x, double y) {
|
||||
_addCommand(LineToCommand(x, y));
|
||||
}
|
||||
|
||||
@override
|
||||
void relativeLineTo(double dx, double dy) {
|
||||
_addCommand(RelativeLineToCommand(dx, dy));
|
||||
}
|
||||
|
||||
@override
|
||||
void quadraticBezierTo(double x1, double y1, double x2, double y2) {
|
||||
_addCommand(QuadraticBezierToCommand(x1, y1, x2, y2));
|
||||
}
|
||||
|
||||
@override
|
||||
void relativeQuadraticBezierTo(double x1, double y1, double x2, double y2) {
|
||||
_addCommand(RelativeQuadraticBezierToCommand(x1, y1, x2, y2));
|
||||
}
|
||||
|
||||
@override
|
||||
void cubicTo(double x1, double y1, double x2, double y2, double x3, double y3) {
|
||||
_addCommand(CubicToCommand(x1, y1, x2, y2, x3, y3));
|
||||
}
|
||||
|
||||
@override
|
||||
void relativeCubicTo(double x1, double y1, double x2, double y2, double x3, double y3) {
|
||||
_addCommand(RelativeCubicToCommand(x1, y1, x2, y2, x3, y3));
|
||||
}
|
||||
|
||||
@override
|
||||
void conicTo(double x1, double y1, double x2, double y2, double w) {
|
||||
_addCommand(ConicToCommand(x1, y1, x2, y2, w));
|
||||
}
|
||||
|
||||
@override
|
||||
void relativeConicTo(double x1, double y1, double x2, double y2, double w) {
|
||||
_addCommand(RelativeConicToCommand(x1, y1, x2, y2, w));
|
||||
}
|
||||
|
||||
@override
|
||||
void arcTo(ui.Rect rect, double startAngle, double sweepAngle, bool forceMoveTo) {
|
||||
_addCommand(ArcToCommand(rect, startAngle, sweepAngle, forceMoveTo));
|
||||
}
|
||||
|
||||
@override
|
||||
void arcToPoint(
|
||||
ui.Offset arcEnd, {
|
||||
ui.Radius radius = ui.Radius.zero,
|
||||
double rotation = 0.0,
|
||||
bool largeArc = false,
|
||||
bool clockwise = true,
|
||||
}) {
|
||||
_addCommand(
|
||||
ArcToPointCommand(
|
||||
arcEnd,
|
||||
radius: radius,
|
||||
rotation: rotation,
|
||||
largeArc: largeArc,
|
||||
clockwise: clockwise,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void relativeArcToPoint(
|
||||
ui.Offset arcEndDelta, {
|
||||
ui.Radius radius = ui.Radius.zero,
|
||||
double rotation = 0.0,
|
||||
bool largeArc = false,
|
||||
bool clockwise = true,
|
||||
}) {
|
||||
_addCommand(
|
||||
RelativeArcToPointCommand(
|
||||
arcEndDelta,
|
||||
radius: radius,
|
||||
rotation: rotation,
|
||||
largeArc: largeArc,
|
||||
clockwise: clockwise,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void addRect(ui.Rect rect) {
|
||||
_addCommand(AddRectCommand(rect));
|
||||
}
|
||||
|
||||
@override
|
||||
void addOval(ui.Rect oval) {
|
||||
_addCommand(AddOvalCommand(oval));
|
||||
}
|
||||
|
||||
@override
|
||||
void addArc(ui.Rect oval, double startAngle, double sweepAngle) {
|
||||
_addCommand(AddArcCommand(oval, startAngle, sweepAngle));
|
||||
}
|
||||
|
||||
@override
|
||||
void addPolygon(List<ui.Offset> points, bool close) {
|
||||
_addCommand(AddPolygonCommand(points, close));
|
||||
}
|
||||
|
||||
@override
|
||||
void addRRect(ui.RRect rrect) {
|
||||
_addCommand(AddRRectCommand(rrect));
|
||||
}
|
||||
|
||||
@override
|
||||
void addRSuperellipse(ui.RSuperellipse rSuperellipse) {
|
||||
_addCommand(AddRSuperellipseCommand(rSuperellipse));
|
||||
}
|
||||
|
||||
@override
|
||||
void addPath(ui.Path path, ui.Offset offset, {Float64List? matrix4}) {
|
||||
_addCommand(AddPathCommand(path as LazyPath, offset, matrix4: matrix4));
|
||||
}
|
||||
|
||||
@override
|
||||
void extendWithPath(ui.Path path, ui.Offset offset, {Float64List? matrix4}) {
|
||||
_addCommand(ExtendWithPathCommand(path as LazyPath, offset, matrix4: matrix4));
|
||||
}
|
||||
|
||||
@override
|
||||
void close() {
|
||||
_addCommand(ClosePathCommand());
|
||||
}
|
||||
|
||||
@override
|
||||
void reset() {
|
||||
_commands.clear();
|
||||
_fillType = ui.PathFillType.nonZero;
|
||||
_cachedPath?.dispose();
|
||||
_cachedPath = null;
|
||||
initializer = constructors.createNew;
|
||||
}
|
||||
|
||||
@override
|
||||
bool contains(ui.Offset point) {
|
||||
return builtPath.contains(point);
|
||||
}
|
||||
|
||||
@override
|
||||
ui.Path shift(ui.Offset offset) {
|
||||
return LazyPath.shifted(this, offset);
|
||||
}
|
||||
|
||||
@override
|
||||
ui.Path transform(Float64List matrix4) {
|
||||
return LazyPath.transformed(this, matrix4);
|
||||
}
|
||||
|
||||
@override
|
||||
ui.Rect getBounds() {
|
||||
return builtPath.getBounds();
|
||||
}
|
||||
|
||||
@override
|
||||
LazyPathMetrics computeMetrics({bool forceClosed = false}) {
|
||||
return LazyPathMetrics(path: this, forceClosed: forceClosed);
|
||||
}
|
||||
|
||||
@override
|
||||
void collect() {
|
||||
_cachedPath?.dispose();
|
||||
_cachedPath = null;
|
||||
}
|
||||
|
||||
@override
|
||||
String toSvgString() {
|
||||
return builtPath.toSvgString();
|
||||
}
|
||||
}
|
||||
|
||||
class LazyPathMetrics extends IterableBase<ui.PathMetric> implements ui.PathMetrics {
|
||||
LazyPathMetrics({required LazyPath path, required bool forceClosed})
|
||||
: iterator = LazyPathMetricIterator(path, forceClosed);
|
||||
|
||||
@override
|
||||
final LazyPathMetricIterator iterator;
|
||||
}
|
||||
|
||||
class LazyPathMetricIterator implements Iterator<ui.PathMetric>, Collectable {
|
||||
LazyPathMetricIterator(this.path, this.forceClosed);
|
||||
|
||||
final LazyPath path;
|
||||
final bool forceClosed;
|
||||
int _nextIndex = 0;
|
||||
DisposablePathMetricIterator? _cachedIterator;
|
||||
final List<DisposablePathMetric> _metrics = [];
|
||||
bool _isAtEnd = false;
|
||||
|
||||
@override
|
||||
ui.PathMetric get current {
|
||||
if (_nextIndex == 0 || _isAtEnd) {
|
||||
throw RangeError(
|
||||
'PathMetricIterator is not pointing to a PathMetric. This can happen in two situations:\n'
|
||||
'- The iteration has not started yet. If so, call "moveNext" to start iteration.\n'
|
||||
'- The iterator ran out of elements. If so, check that "moveNext" returns true prior to calling "current".',
|
||||
);
|
||||
}
|
||||
return LazyPathMetric(this, _nextIndex - 1);
|
||||
}
|
||||
|
||||
@override
|
||||
bool moveNext() {
|
||||
if (_isAtEnd) {
|
||||
return false;
|
||||
}
|
||||
buildIterator();
|
||||
assert(_cachedIterator != null);
|
||||
assert(_nextIndex == _metrics.length);
|
||||
_nextIndex++;
|
||||
if (_cachedIterator!.moveNext()) {
|
||||
_metrics.add(_cachedIterator!.current);
|
||||
return true;
|
||||
} else {
|
||||
_isAtEnd = true;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void collect() {
|
||||
_cachedIterator?.dispose();
|
||||
_cachedIterator = null;
|
||||
|
||||
for (final metric in _metrics) {
|
||||
metric.dispose();
|
||||
}
|
||||
_metrics.clear();
|
||||
}
|
||||
|
||||
void buildIterator() {
|
||||
if (_cachedIterator != null) {
|
||||
return;
|
||||
}
|
||||
_cachedIterator = path.builtPath.computeMetrics(forceClosed: forceClosed).iterator;
|
||||
for (int i = 0; i < _nextIndex; i++) {
|
||||
if (_cachedIterator!.moveNext()) {
|
||||
_metrics.add(_cachedIterator!.current);
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
EnginePlatformDispatcher.instance.frameArena.add(this);
|
||||
}
|
||||
|
||||
DisposablePathMetric builtMetricAtIndex(int index) {
|
||||
buildIterator();
|
||||
return _metrics[index];
|
||||
}
|
||||
}
|
||||
|
||||
class LazyPathMetric implements ui.PathMetric {
|
||||
LazyPathMetric(this.iterator, this.contourIndex);
|
||||
|
||||
final LazyPathMetricIterator iterator;
|
||||
|
||||
@override
|
||||
final int contourIndex;
|
||||
|
||||
DisposablePathMetric get builtMetric => iterator.builtMetricAtIndex(contourIndex);
|
||||
|
||||
@override
|
||||
ui.Path extractPath(double start, double end, {bool startWithMoveTo = true}) {
|
||||
return iterator.path.extracted(this, start, end, startWithMoveTo: startWithMoveTo);
|
||||
}
|
||||
|
||||
DisposablePath buildExtractedPath(double start, double end, {required bool startWithMoveTo}) {
|
||||
return builtMetric.extractPath(start, end, startWithMoveTo: startWithMoveTo);
|
||||
}
|
||||
|
||||
@override
|
||||
ui.Tangent? getTangentForOffset(double distance) {
|
||||
return builtMetric.getTangentForOffset(distance);
|
||||
}
|
||||
|
||||
@override
|
||||
bool get isClosed => builtMetric.isClosed;
|
||||
|
||||
@override
|
||||
double get length => builtMetric.length;
|
||||
}
|
||||
@ -44,6 +44,8 @@ class EnginePlatformDispatcher extends ui.PlatformDispatcher {
|
||||
|
||||
late StreamSubscription<int> _onViewDisposedListener;
|
||||
|
||||
final Arena frameArena = Arena();
|
||||
|
||||
/// The [EnginePlatformDispatcher] singleton.
|
||||
static EnginePlatformDispatcher get instance => _instance;
|
||||
static final EnginePlatformDispatcher _instance = EnginePlatformDispatcher();
|
||||
@ -260,6 +262,7 @@ class EnginePlatformDispatcher extends ui.PlatformDispatcher {
|
||||
_viewsRenderedInCurrentFrame = <ui.FlutterView>{};
|
||||
invoke(_onDrawFrame, _onDrawFrameZone);
|
||||
_viewsRenderedInCurrentFrame = null;
|
||||
frameArena.collect();
|
||||
}
|
||||
|
||||
/// A callback that is invoked when pointer data is available.
|
||||
@ -740,7 +743,7 @@ class EnginePlatformDispatcher extends ui.PlatformDispatcher {
|
||||
// order to perform golden tests in Flutter framework because on the HTML
|
||||
// renderer, golden tests render to DOM and then take a browser screenshot,
|
||||
// https://github.com/flutter/flutter/issues/137073.
|
||||
if (shouldRender || renderer.rendererTag == 'html') {
|
||||
if (shouldRender) {
|
||||
await renderer.renderScene(scene, target);
|
||||
}
|
||||
}
|
||||
|
||||
@ -143,8 +143,7 @@ class SkwasmCanvas implements SceneCanvas {
|
||||
|
||||
@override
|
||||
void clipPath(ui.Path path, {bool doAntiAlias = true}) {
|
||||
path as SkwasmPath;
|
||||
canvasClipPath(_handle, path.handle, doAntiAlias);
|
||||
canvasClipPath(_handle, ((path as LazyPath).builtPath as SkwasmPath).handle, doAntiAlias);
|
||||
}
|
||||
|
||||
@override
|
||||
@ -238,9 +237,8 @@ class SkwasmCanvas implements SceneCanvas {
|
||||
|
||||
@override
|
||||
void drawPath(ui.Path path, ui.Paint paint) {
|
||||
path as SkwasmPath;
|
||||
final paintHandle = (paint as SkwasmPaint).toRawPaint();
|
||||
canvasDrawPath(_handle, path.handle, paintHandle);
|
||||
canvasDrawPath(_handle, ((path as LazyPath).builtPath as SkwasmPath).handle, paintHandle);
|
||||
paintDispose(paintHandle);
|
||||
}
|
||||
|
||||
@ -391,10 +389,9 @@ class SkwasmCanvas implements SceneCanvas {
|
||||
|
||||
@override
|
||||
void drawShadow(ui.Path path, ui.Color color, double elevation, bool transparentOccluder) {
|
||||
path as SkwasmPath;
|
||||
canvasDrawShadow(
|
||||
_handle,
|
||||
path.handle,
|
||||
((path as LazyPath).builtPath as SkwasmPath).handle,
|
||||
elevation,
|
||||
EngineFlutterDisplay.instance.devicePixelRatio,
|
||||
color.value,
|
||||
|
||||
@ -14,7 +14,7 @@ enum PathDirection { clockwise, counterClockwise }
|
||||
|
||||
enum PathArcSize { small, large }
|
||||
|
||||
class SkwasmPath extends SkwasmObjectWrapper<RawPath> implements ScenePath {
|
||||
class SkwasmPath extends SkwasmObjectWrapper<RawPath> implements ScenePath, DisposablePath {
|
||||
factory SkwasmPath() {
|
||||
return SkwasmPath.fromHandle(pathCreate());
|
||||
}
|
||||
@ -234,7 +234,7 @@ class SkwasmPath extends SkwasmObjectWrapper<RawPath> implements ScenePath {
|
||||
SkwasmPath.fromHandle(pathCombine(operation.index, path1.handle, path2.handle));
|
||||
|
||||
@override
|
||||
ui.PathMetrics computeMetrics({bool forceClosed = false}) {
|
||||
SkwasmPathMetrics computeMetrics({bool forceClosed = false}) {
|
||||
return SkwasmPathMetrics(path: this, forceClosed: forceClosed);
|
||||
}
|
||||
|
||||
@ -249,3 +249,13 @@ class SkwasmPath extends SkwasmObjectWrapper<RawPath> implements ScenePath {
|
||||
return svgString;
|
||||
}
|
||||
}
|
||||
|
||||
class SkwasmPathConstructors implements DisposablePathConstructors {
|
||||
@override
|
||||
SkwasmPath createNew() => SkwasmPath();
|
||||
|
||||
@override
|
||||
SkwasmPath combinePaths(ui.PathOperation operation, DisposablePath path1, DisposablePath path2) {
|
||||
return SkwasmPath.combine(operation, path1 as SkwasmPath, path2 as SkwasmPath);
|
||||
}
|
||||
}
|
||||
|
||||
@ -5,21 +5,22 @@
|
||||
import 'dart:collection';
|
||||
import 'dart:ffi';
|
||||
|
||||
import 'package:ui/src/engine.dart';
|
||||
import 'package:ui/src/engine/skwasm/skwasm_impl.dart';
|
||||
import 'package:ui/ui.dart' as ui;
|
||||
|
||||
class SkwasmPathMetrics extends IterableBase<ui.PathMetric> implements ui.PathMetrics {
|
||||
class SkwasmPathMetrics extends IterableBase<ui.PathMetric> implements DisposablePathMetrics {
|
||||
SkwasmPathMetrics({required this.path, required this.forceClosed});
|
||||
|
||||
SkwasmPath path;
|
||||
bool forceClosed;
|
||||
|
||||
@override
|
||||
late Iterator<ui.PathMetric> iterator = SkwasmPathMetricIterator(path, forceClosed);
|
||||
late DisposablePathMetricIterator iterator = SkwasmPathMetricIterator(path, forceClosed);
|
||||
}
|
||||
|
||||
class SkwasmPathMetricIterator extends SkwasmObjectWrapper<RawContourMeasureIter>
|
||||
implements Iterator<ui.PathMetric> {
|
||||
implements DisposablePathMetricIterator {
|
||||
SkwasmPathMetricIterator(SkwasmPath path, bool forceClosed)
|
||||
: super(contourMeasureIterCreate(path.handle, forceClosed, 1.0), _registry);
|
||||
|
||||
@ -32,7 +33,7 @@ class SkwasmPathMetricIterator extends SkwasmObjectWrapper<RawContourMeasureIter
|
||||
int _nextIndex = 0;
|
||||
|
||||
@override
|
||||
ui.PathMetric get current {
|
||||
DisposablePathMetric get current {
|
||||
if (_current == null) {
|
||||
throw RangeError(
|
||||
'PathMetricIterator is not pointing to a PathMetric. This can happen in two situations:\n'
|
||||
@ -57,7 +58,8 @@ class SkwasmPathMetricIterator extends SkwasmObjectWrapper<RawContourMeasureIter
|
||||
}
|
||||
}
|
||||
|
||||
class SkwasmPathMetric extends SkwasmObjectWrapper<RawContourMeasure> implements ui.PathMetric {
|
||||
class SkwasmPathMetric extends SkwasmObjectWrapper<RawContourMeasure>
|
||||
implements DisposablePathMetric {
|
||||
SkwasmPathMetric(ContourMeasureHandle handle, this.contourIndex) : super(handle, _registry);
|
||||
|
||||
static final SkwasmFinalizationRegistry<RawContourMeasure> _registry =
|
||||
@ -69,7 +71,7 @@ class SkwasmPathMetric extends SkwasmObjectWrapper<RawContourMeasure> implements
|
||||
final int contourIndex;
|
||||
|
||||
@override
|
||||
ui.Path extractPath(double start, double end, {bool startWithMoveTo = true}) {
|
||||
DisposablePath extractPath(double start, double end, {bool startWithMoveTo = true}) {
|
||||
return SkwasmPath.fromHandle(contourMeasureGetSegment(handle, start, end, startWithMoveTo));
|
||||
}
|
||||
|
||||
|
||||
@ -21,17 +21,19 @@ class SkwasmRenderer implements Renderer {
|
||||
|
||||
bool get isMultiThreaded => skwasmIsMultiThreaded();
|
||||
|
||||
SkwasmPathConstructors pathConstructors = SkwasmPathConstructors();
|
||||
|
||||
@override
|
||||
final SkwasmFontCollection fontCollection = SkwasmFontCollection();
|
||||
|
||||
@override
|
||||
ui.Path combinePaths(ui.PathOperation op, ui.Path path1, ui.Path path2) {
|
||||
return SkwasmPath.combine(op, path1 as SkwasmPath, path2 as SkwasmPath);
|
||||
return LazyPath.combined(op, path1 as LazyPath, path2 as LazyPath);
|
||||
}
|
||||
|
||||
@override
|
||||
ui.Path copyPath(ui.Path src) {
|
||||
return SkwasmPath.from(src as SkwasmPath);
|
||||
return LazyPath.fromLazyPath(src as LazyPath);
|
||||
}
|
||||
|
||||
@override
|
||||
@ -153,7 +155,7 @@ class SkwasmRenderer implements Renderer {
|
||||
);
|
||||
|
||||
@override
|
||||
ui.Path createPath() => SkwasmPath();
|
||||
ui.Path createPath() => LazyPath(pathConstructors);
|
||||
|
||||
@override
|
||||
ui.PictureRecorder createPictureRecorder() => SkwasmPictureRecorder();
|
||||
|
||||
@ -206,23 +206,21 @@ void _pathOpTests() {
|
||||
});
|
||||
|
||||
test('Path.combine test', () {
|
||||
final ui.Path path1 = ui.Path();
|
||||
expect(path1, isA<CkPath>());
|
||||
final CkPath path1 = CkPath();
|
||||
path1.addRect(const ui.Rect.fromLTRB(0, 0, 10, 10));
|
||||
path1.addOval(const ui.Rect.fromLTRB(10, 10, 100, 100));
|
||||
|
||||
final ui.Path path2 = ui.Path();
|
||||
expect(path2, isA<CkPath>());
|
||||
final CkPath path2 = CkPath();
|
||||
path2.addRect(const ui.Rect.fromLTRB(5, 5, 15, 15));
|
||||
path2.addOval(const ui.Rect.fromLTRB(15, 15, 105, 105));
|
||||
|
||||
final ui.Path union = ui.Path.combine(ui.PathOperation.union, path1, path2);
|
||||
final ui.Path union = CkPath.combine(ui.PathOperation.union, path1, path2);
|
||||
expect(union, isA<CkPath>());
|
||||
expect(union.getBounds(), const ui.Rect.fromLTRB(0, 0, 105, 105));
|
||||
|
||||
// Smoke-test other operations.
|
||||
for (final ui.PathOperation operation in ui.PathOperation.values) {
|
||||
final ui.Path combined = ui.Path.combine(operation, path1, path2);
|
||||
final ui.Path combined = CkPath.combine(operation, path1, path2);
|
||||
expect(combined, isA<CkPath>());
|
||||
}
|
||||
});
|
||||
|
||||
@ -23,8 +23,7 @@ void testMain() {
|
||||
});
|
||||
|
||||
test(CkPathMetrics, () {
|
||||
final ui.Path path = ui.Path();
|
||||
expect(path, isA<CkPath>());
|
||||
final CkPath path = CkPath();
|
||||
expect(path.computeMetrics().length, 0);
|
||||
|
||||
path.addRect(const ui.Rect.fromLTRB(0, 0, 10, 10));
|
||||
@ -65,8 +64,7 @@ void testMain() {
|
||||
});
|
||||
|
||||
test('CkPath.reset', () {
|
||||
final ui.Path path = ui.Path();
|
||||
expect(path, isA<CkPath>());
|
||||
final CkPath path = CkPath();
|
||||
path.addRect(const ui.Rect.fromLTRB(0, 0, 10, 10));
|
||||
expect(path.contains(const ui.Offset(5, 5)), isTrue);
|
||||
|
||||
@ -95,8 +93,7 @@ void testMain() {
|
||||
});
|
||||
|
||||
test('CkContourMeasure iteration', () {
|
||||
final ui.Path path = ui.Path();
|
||||
expect(path, isA<CkPath>());
|
||||
final CkPath path = CkPath();
|
||||
path.addRect(const ui.Rect.fromLTRB(0, 0, 10, 10));
|
||||
path.addRect(const ui.Rect.fromLTRB(20, 20, 30, 30));
|
||||
path.addRect(const ui.Rect.fromLTRB(40, 40, 50, 50));
|
||||
@ -111,22 +108,21 @@ void testMain() {
|
||||
});
|
||||
|
||||
test('CkContourMeasure index', () {
|
||||
final ui.Path path = ui.Path();
|
||||
expect(path, isA<CkPath>());
|
||||
final CkPath path = CkPath();
|
||||
path.addRect(const ui.Rect.fromLTRB(0, 0, 10, 10));
|
||||
path.addRect(const ui.Rect.fromLTRB(20, 20, 30, 30));
|
||||
path.addRect(const ui.Rect.fromLTRB(40, 40, 50, 50));
|
||||
|
||||
final ui.PathMetrics metrics = path.computeMetrics();
|
||||
final CkPathMetrics metrics = path.computeMetrics();
|
||||
final CkContourMeasureIter iterator = metrics.iterator as CkContourMeasureIter;
|
||||
|
||||
expect(iterator.moveNext(), isTrue);
|
||||
final CkContourMeasure measure0 = iterator.current as CkContourMeasure;
|
||||
final CkContourMeasure measure0 = iterator.current;
|
||||
expect(measure0.contourIndex, 0);
|
||||
expect(measure0.extractPath(0, 15).getBounds(), const ui.Rect.fromLTRB(0, 0, 10, 5));
|
||||
|
||||
expect(iterator.moveNext(), isTrue);
|
||||
final CkContourMeasure measure1 = iterator.current as CkContourMeasure;
|
||||
final CkContourMeasure measure1 = iterator.current;
|
||||
expect(measure1.contourIndex, 1);
|
||||
expect(measure1.extractPath(0, 15).getBounds(), const ui.Rect.fromLTRB(20, 20, 30, 25));
|
||||
});
|
||||
@ -135,12 +131,11 @@ void testMain() {
|
||||
const ui.Rect rect1 = ui.Rect.fromLTRB(0, 0, 10, 10);
|
||||
const ui.Rect rect2 = ui.Rect.fromLTRB(10, 10, 20, 20);
|
||||
|
||||
final ui.Path original = ui.Path();
|
||||
final CkPath original = CkPath();
|
||||
original.addRect(rect1);
|
||||
expect(original, isA<CkPath>());
|
||||
expect(original.getBounds(), rect1);
|
||||
|
||||
final ui.Path copy = ui.Path.from(original);
|
||||
final ui.Path copy = CkPath.from(original);
|
||||
expect(copy, isA<CkPath>());
|
||||
expect(copy.getBounds(), rect1);
|
||||
|
||||
|
||||
297
engine/src/flutter/lib/web_ui/test/engine/lazy_path_test.dart
Normal file
297
engine/src/flutter/lib/web_ui/test/engine/lazy_path_test.dart
Normal file
@ -0,0 +1,297 @@
|
||||
// Copyright 2013 The Flutter Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'dart:collection';
|
||||
import 'dart:typed_data';
|
||||
import 'package:test/bootstrap/browser.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
import 'package:ui/src/engine.dart';
|
||||
import 'package:ui/ui.dart';
|
||||
|
||||
void main() {
|
||||
internalBootstrapBrowserTest(() => testMain);
|
||||
}
|
||||
|
||||
class FakePath implements DisposablePath {
|
||||
int _apiCallCount = 0;
|
||||
|
||||
int get apiCallCount => _apiCallCount;
|
||||
|
||||
bool isDisposed = false;
|
||||
|
||||
final List<FakePathMetrics> computedMetrics = [];
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
isDisposed = true;
|
||||
}
|
||||
|
||||
@override
|
||||
PathFillType fillType = PathFillType.nonZero;
|
||||
|
||||
@override
|
||||
void moveTo(double x, double y) {
|
||||
_apiCallCount++;
|
||||
}
|
||||
|
||||
@override
|
||||
void relativeMoveTo(double dx, double dy) {
|
||||
_apiCallCount++;
|
||||
}
|
||||
|
||||
@override
|
||||
void lineTo(double x, double y) {
|
||||
_apiCallCount++;
|
||||
}
|
||||
|
||||
@override
|
||||
void relativeLineTo(double dx, double dy) {
|
||||
_apiCallCount++;
|
||||
}
|
||||
|
||||
@override
|
||||
void quadraticBezierTo(double x1, double y1, double x2, double y2) {
|
||||
_apiCallCount++;
|
||||
}
|
||||
|
||||
@override
|
||||
void relativeQuadraticBezierTo(double x1, double y1, double x2, double y2) {
|
||||
_apiCallCount++;
|
||||
}
|
||||
|
||||
@override
|
||||
void cubicTo(double x1, double y1, double x2, double y2, double x3, double y3) {
|
||||
_apiCallCount++;
|
||||
}
|
||||
|
||||
@override
|
||||
void relativeCubicTo(double x1, double y1, double x2, double y2, double x3, double y3) {
|
||||
_apiCallCount++;
|
||||
}
|
||||
|
||||
@override
|
||||
void conicTo(double x1, double y1, double x2, double y2, double w) {
|
||||
_apiCallCount++;
|
||||
}
|
||||
|
||||
@override
|
||||
void relativeConicTo(double x1, double y1, double x2, double y2, double w) {
|
||||
_apiCallCount++;
|
||||
}
|
||||
|
||||
@override
|
||||
void arcTo(Rect rect, double startAngle, double sweepAngle, bool forceMoveTo) {
|
||||
_apiCallCount++;
|
||||
}
|
||||
|
||||
@override
|
||||
void arcToPoint(
|
||||
Offset arcEnd, {
|
||||
Radius radius = Radius.zero,
|
||||
double rotation = 0.0,
|
||||
bool largeArc = false,
|
||||
bool clockwise = true,
|
||||
}) {
|
||||
_apiCallCount++;
|
||||
}
|
||||
|
||||
@override
|
||||
void relativeArcToPoint(
|
||||
Offset arcEndDelta, {
|
||||
Radius radius = Radius.zero,
|
||||
double rotation = 0.0,
|
||||
bool largeArc = false,
|
||||
bool clockwise = true,
|
||||
}) {
|
||||
_apiCallCount++;
|
||||
}
|
||||
|
||||
@override
|
||||
void addRect(Rect rect) {
|
||||
_apiCallCount++;
|
||||
}
|
||||
|
||||
@override
|
||||
void addOval(Rect oval) {
|
||||
_apiCallCount++;
|
||||
}
|
||||
|
||||
@override
|
||||
void addArc(Rect oval, double startAngle, double sweepAngle) {
|
||||
_apiCallCount++;
|
||||
}
|
||||
|
||||
@override
|
||||
void addPolygon(List<Offset> points, bool close) {
|
||||
_apiCallCount++;
|
||||
}
|
||||
|
||||
@override
|
||||
void addRRect(RRect rrect) {
|
||||
_apiCallCount++;
|
||||
}
|
||||
|
||||
@override
|
||||
void addRSuperellipse(RSuperellipse rsuperellipse) {
|
||||
_apiCallCount++;
|
||||
}
|
||||
|
||||
@override
|
||||
void addPath(Path path, Offset offset, {Float64List? matrix4}) {
|
||||
_apiCallCount++;
|
||||
}
|
||||
|
||||
@override
|
||||
void extendWithPath(Path path, Offset offset, {Float64List? matrix4}) {
|
||||
_apiCallCount++;
|
||||
}
|
||||
|
||||
@override
|
||||
void close() {
|
||||
_apiCallCount++;
|
||||
}
|
||||
|
||||
@override
|
||||
void reset() {
|
||||
_apiCallCount++;
|
||||
}
|
||||
|
||||
@override
|
||||
bool contains(Offset point) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@override
|
||||
Path shift(Offset offset) {
|
||||
return this;
|
||||
}
|
||||
|
||||
@override
|
||||
Path transform(Float64List matrix4) {
|
||||
return this;
|
||||
}
|
||||
|
||||
@override
|
||||
Rect getBounds() {
|
||||
return Rect.zero;
|
||||
}
|
||||
|
||||
@override
|
||||
FakePathMetrics computeMetrics({bool forceClosed = false}) {
|
||||
final metrics = FakePathMetrics();
|
||||
computedMetrics.add(metrics);
|
||||
return metrics;
|
||||
}
|
||||
|
||||
@override
|
||||
String toSvgString() {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
class FakePathMetrics extends IterableBase<PathMetric> implements DisposablePathMetrics {
|
||||
@override
|
||||
final FakePathMetricsIterator iterator = FakePathMetricsIterator();
|
||||
}
|
||||
|
||||
class FakePathMetricsIterator implements DisposablePathMetricIterator {
|
||||
bool isDisposed = false;
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
isDisposed = true;
|
||||
}
|
||||
|
||||
@override
|
||||
bool moveNext() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@override
|
||||
Never get current {
|
||||
throw RangeError('nope');
|
||||
}
|
||||
}
|
||||
|
||||
class FakePathConstructors implements DisposablePathConstructors {
|
||||
final List<FakePath> createdPaths = [];
|
||||
|
||||
@override
|
||||
FakePath createNew() {
|
||||
final path = FakePath();
|
||||
createdPaths.add(path);
|
||||
return path;
|
||||
}
|
||||
|
||||
@override
|
||||
FakePath combinePaths(PathOperation operation, DisposablePath path1, DisposablePath path2) {
|
||||
final path = FakePath();
|
||||
createdPaths.add(path);
|
||||
return path;
|
||||
}
|
||||
}
|
||||
|
||||
void testMain() {
|
||||
test('LazyPath lifecycle', () {
|
||||
final constructors = FakePathConstructors();
|
||||
final LazyPath path = LazyPath(constructors);
|
||||
expect(constructors.createdPaths, isEmpty);
|
||||
|
||||
path.moveTo(0, 0);
|
||||
path.relativeMoveTo(0, 0);
|
||||
path.lineTo(0, 0);
|
||||
path.relativeLineTo(0, 0);
|
||||
path.quadraticBezierTo(0, 0, 0, 0);
|
||||
path.relativeQuadraticBezierTo(0, 0, 0, 0);
|
||||
path.cubicTo(0, 0, 0, 0, 0, 0);
|
||||
path.relativeCubicTo(0, 0, 0, 0, 0, 0);
|
||||
path.conicTo(0, 0, 0, 0, 0);
|
||||
path.relativeConicTo(0, 0, 0, 0, 0);
|
||||
path.arcTo(Rect.zero, 0, 0, false);
|
||||
path.arcToPoint(Offset.zero);
|
||||
path.relativeArcToPoint(Offset.zero);
|
||||
path.addRect(Rect.zero);
|
||||
path.addOval(Rect.zero);
|
||||
path.addArc(Rect.zero, 0, 0);
|
||||
path.addPolygon([], false);
|
||||
path.addRRect(RRect.zero);
|
||||
path.addRSuperellipse(RSuperellipse.zero);
|
||||
path.close();
|
||||
|
||||
expect(constructors.createdPaths, isEmpty);
|
||||
|
||||
path.getBounds();
|
||||
|
||||
expect(constructors.createdPaths.length, 1);
|
||||
final disposablePath = constructors.createdPaths.first;
|
||||
|
||||
expect(disposablePath.isDisposed, false);
|
||||
expect(disposablePath.apiCallCount, 20);
|
||||
|
||||
final metrics = path.computeMetrics();
|
||||
expect(metrics.iterator.moveNext(), false);
|
||||
expect(disposablePath.computedMetrics.length, 1);
|
||||
final disposableMetrics = disposablePath.computedMetrics.first;
|
||||
expect(disposableMetrics.iterator.isDisposed, false);
|
||||
|
||||
EnginePlatformDispatcher.instance.frameArena.collect();
|
||||
|
||||
expect(constructors.createdPaths.length, 1);
|
||||
expect(disposablePath.isDisposed, true);
|
||||
expect(disposablePath.computedMetrics.length, 1);
|
||||
expect(disposableMetrics.iterator.isDisposed, true);
|
||||
|
||||
path.getBounds();
|
||||
|
||||
expect(constructors.createdPaths.length, 2);
|
||||
final resurrectedPath = constructors.createdPaths.last;
|
||||
|
||||
expect(resurrectedPath.isDisposed, false);
|
||||
expect(resurrectedPath.apiCallCount, 20);
|
||||
expect(metrics.iterator.moveNext(), false);
|
||||
|
||||
EnginePlatformDispatcher.instance.frameArena.collect();
|
||||
});
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user