From 193cc2ca6403458bff970fc4ca76e72229d00f7e Mon Sep 17 00:00:00 2001 From: Ian Hickson Date: Fri, 2 Dec 2022 00:34:23 -0800 Subject: [PATCH] Documentation and other cleanup in dart:ui, plus a small performance improvement. * Remove nonsensical (and I think unnecessary?) comment. * Document willChangeHint, isComplexHint * Fix grammer in Rect docs * Remove runtimeType.toString twice * Add detail to ImageShader constructor docs. * Document Vertices and drawVertices! * Update checks in Vertices constructors (and add tests). * Fix some typos. * Minor other doc improvements. * Fold in @jonahwilliams' performance improvement from https://github.com/flutter/engine/pull/38041 --- flow/layers/display_list_raster_cache_item.cc | 4 - lib/ui/compositing.dart | 20 +- lib/ui/geometry.dart | 2 +- lib/ui/painting.dart | 284 +++++++++++++----- lib/web_ui/lib/canvas.dart | 4 +- .../lib/src/engine/html/render_vertices.dart | 2 +- testing/dart/BUILD.gn | 1 + testing/dart/compositing_test.dart | 2 +- testing/dart/painting_test.dart | 63 ++++ testing/dart/path_test.dart | 15 + 10 files changed, 308 insertions(+), 89 deletions(-) create mode 100644 testing/dart/painting_test.dart diff --git a/flow/layers/display_list_raster_cache_item.cc b/flow/layers/display_list_raster_cache_item.cc index cb774e4498dd8..37a1a6bf1a3c5 100644 --- a/flow/layers/display_list_raster_cache_item.cc +++ b/flow/layers/display_list_raster_cache_item.cc @@ -104,10 +104,6 @@ void DisplayListRasterCacheItem::PrerollFinalize(PrerollContext* context, } auto* raster_cache = context->raster_cache; SkRect bounds = display_list_->bounds().makeOffset(offset_.x(), offset_.y()); - // We must to create an entry whenever if the react is intersect. - // if the rect is intersect we will get the entry access_count to confirm if - // it great than the threshold. Otherwise we only increase the entry - // access_count. bool visible = !context->state_stack.content_culled(bounds); int accesses = raster_cache->MarkSeen(key_id_, matrix, visible); if (!visible || accesses <= raster_cache->access_threshold()) { diff --git a/lib/ui/compositing.dart b/lib/ui/compositing.dart index b039e67ff1030..2c2c6dd8eb5fc 100644 --- a/lib/ui/compositing.dart +++ b/lib/ui/compositing.dart @@ -748,7 +748,25 @@ class SceneBuilder extends NativeFieldWrapperClass1 { /// Adds a [Picture] to the scene. /// - /// The picture is rasterized at the given offset. + /// The picture is rasterized at the given `offset`. + /// + /// The rendering _may_ be cached to reduce the cost of painting the picture + /// if it is reused in subsequent frames. Whether a picture is cached or not + /// depends on the backend implementation. When caching is considered, the + /// choice to cache or not cache is a heuristic based on how often the picture + /// is being painted and the cost of painting the picture. To disable this + /// caching, set `willChangeHint` to true. To force the caching to happen (in + /// backends that do caching), set `isComplexHint` to true. When both are set, + /// `willChangeHint` prevails. + /// + /// In general, setting these hints is not very useful. Backends that cache + /// pictures only do so for pictures that have been rendered three times + /// already; setting `willChangeHint` to true to avoid caching an animating + /// picture that changes every frame is therefore redundant, the picture + /// wouldn't have been cached anyway. Similarly, backends that cache pictures + /// are relatively aggressive about doing so, such that any image complicated + /// enough to warrant caching is probably already being cached even without + /// `isComplexHint` being set to true. void addPicture( Offset offset, Picture picture, { diff --git a/lib/ui/geometry.dart b/lib/ui/geometry.dart index 9945ba8666898..c6f300b65bf52 100644 --- a/lib/ui/geometry.dart +++ b/lib/ui/geometry.dart @@ -625,7 +625,7 @@ class Size extends OffsetBase { /// An immutable, 2D, axis-aligned, floating-point rectangle whose coordinates /// are relative to a given origin. /// -/// A Rect can be created with one its constructors or from an [Offset] and a +/// A Rect can be created with one of its constructors or from an [Offset] and a /// [Size] using the `&` operator: /// /// ```dart diff --git a/lib/ui/painting.dart b/lib/ui/painting.dart index 891a18923b9f5..939c00e842819 100644 --- a/lib/ui/painting.dart +++ b/lib/ui/painting.dart @@ -3001,7 +3001,7 @@ class PathMetric { } @override - String toString() => '$runtimeType{length: $length, isClosed: $isClosed, contourIndex:$contourIndex}'; + String toString() => 'PathMetric(length: $length, isClosed: $isClosed, contourIndex: $contourIndex)'; } class _PathMeasure extends NativeFieldWrapperClass1 { @@ -4103,13 +4103,25 @@ class Gradient extends Shader { /// A shader (as used by [Paint.shader]) that tiles an image. class ImageShader extends Shader { - /// Creates an image-tiling shader. The first argument specifies the image to - /// tile. The second and third arguments specify the [TileMode] for the x - /// direction and y direction respectively. The fourth argument gives the - /// matrix to apply to the effect. All the arguments are required and must not - /// be null, except for [filterQuality]. If [filterQuality] is not specified - /// at construction time it will be deduced from the environment where it is used, - /// such as from [Paint.filterQuality]. + /// Creates an image-tiling shader. + /// + /// The first argument specifies the image to render. The + /// [decodeImageFromList] function can be used to decode an image from bytes + /// into the form expected here. (In production code, starting from + /// [instantiateImageCodec] may be preferable.) + /// + /// The second and third arguments specify the [TileMode] for the x direction + /// and y direction respectively. [TileMode.repeated] can be used for tiling + /// images. + /// + /// The fourth argument gives the matrix to apply to the effect. The + /// expression `Matrix4.identity().storage` creates a [Float64List] + /// prepopulated with the identity matrix. + /// + /// All the arguments are required and must not be null, except for + /// [filterQuality]. If [filterQuality] is not specified at construction time + /// it will be deduced from the environment where it is used, such as from + /// [Paint.filterQuality]. @pragma('vm:entry-point') ImageShader(Image image, TileMode tmx, TileMode tmy, Float64List matrix4, { FilterQuality? filterQuality, @@ -4362,46 +4374,119 @@ enum VertexMode { /// Draw each sliding window of three points as the vertices of a triangle. triangleStrip, - /// Draw the first point and each sliding window of two points as the vertices of a triangle. + /// Draw the first point and each sliding window of two points as the vertices + /// of a triangle. + /// + /// This mode is not natively supported by most backends, and is instead + /// implemented by unrolling the points into the equivalent + /// [VertexMode.triangles], which is generally more efficient. triangleFan, } /// A set of vertex data used by [Canvas.drawVertices]. +/// +/// Vertex data consists of a series of points in the canvas coordinate space. +/// Based on the [VertexMode], these points are interpreted either as +/// independent triangles ([VertexMode.triangles]), as a sliding window of +/// points forming a chain of triangles each sharing one side with the next +/// ([VertexMode.triangleStrip]), or as a fan of triangles with a single shared +/// point ([VertexMode.triangleFan]). +/// +/// Each point can be associated with a color. Each triangle is painted as a +/// gradient that blends between the three colors at the three points of that +/// triangle. If no colors are specified, transparent black is assumed for all +/// the points. +/// +/// These colors are then blended with the [Paint] specified in the call to +/// [Canvas.drawVertices]. This paint is either a solid color ([Paint.color]), +/// or a bitmap, specified using a shader ([Paint.shader]), typically either a +/// gradient ([Gradient]) or image ([ImageFilter]). The bitmap uses the same +/// coordinate space as the canvas (in the case of an [ImageFilter], this is +/// notably different than the coordinate space of the source image; the source +/// image is tiled according to the filter's configuration, and the image that +/// is sampled when painting the triangles is the infinite one after all the +/// repeating is applied.) +/// +/// Each point in the [Vertices] is associated with a specific point on this +/// image. Each triangle is painted by sampling points from this image by +/// interpolating between the three points of the image corresponding to the +/// three points of the triangle. +/// +/// The [Vertices.new] constructor configures all this using lists of [Offset] +/// and [Color] objects. The [Vertices.raw] constructor instead uses +/// [Float32List], [Int32List], and [Uint16List] objects, which more closely +/// corresponds to the data format used internally and therefore reduces some of +/// the conversion overhead. The raw constructor is useful if the data is coming +/// from another source (e.g. a file) and can therefore be parsed directly into +/// the underlying representation. class Vertices extends NativeFieldWrapperClass1 { /// Creates a set of vertex data for use with [Canvas.drawVertices]. /// - /// The [mode] and [positions] parameters must not be null. - /// The [positions] parameter is a list of triangular mesh vertices(xy). - /// - /// If the [textureCoordinates] or [colors] parameters are provided, they must - /// be the same length as [positions]. - /// - /// The [textureCoordinates] parameter is used to cutout - /// the image set in the image shader. - /// The cut part is applied to the triangular mesh. - /// Note that the [textureCoordinates] are the coordinates on the image. - /// - /// If the [indices] parameter is provided, all values in the list must be - /// valid index values for [positions]. - /// e.g. The [indices] parameter for a simple triangle is [0,1,2]. + /// The `mode` parameter describes how the points should be interpreted: as + /// independent triangles ([VertexMode.triangles]), as a sliding window of + /// points forming a chain of triangles each sharing one side with the next + /// ([VertexMode.triangleStrip]), or as a fan of triangles with a single + /// shared point ([VertexMode.triangleFan]). + /// + /// The `positions` parameter provides the points in the canvas space that + /// will be use to draw the triangles. + /// + /// The `colors` parameter, if specified, provides the color for each point in + /// `positions`. Each triangle is painted as a gradient that blends between + /// the three colors at the three points of that triangle. (These colors are + /// then blended with the [Paint] specified in the call to + /// [Canvas.drawVertices].) + /// + /// The `textureCoordinates` parameter, if specified, provides the points in + /// the [Paint] image to sample for the corresponding points in `positions`. + /// + /// If the `colors` or `textureCoordinates` parameters are specified, they must + /// be the same length as `positions`. + /// + /// The `indices` parameter specifies the order in which the points should be + /// painted. If it is omitted (or present but empty), the points are processed + /// in the order they are given in `positions`, as if the `indices` was a list + /// from 0 to n-1, where _n_ is the number of entries in `positions`. The + /// `indices` parameter, if present and non-empty, must have at least three + /// entries, but may be of any length beyond this. Indicies may refer to + /// offsets in the positions array multiple times, or may skip positions + /// entirely. + /// + /// If the `indices` parameter is specified, all values in the list must be + /// valid index values for `positions`. + /// + /// The `mode` and `positions` parameters must not be null. + /// + /// This constructor converts its parameters into [dart:typed_data] lists + /// (e.g. using [Float32List]s for the coordinates) before sending them to the + /// Flutter engine. If the data provided to this constructor is not already in + /// [List] form, consider using the [Vertices.raw] constructor instead to + /// avoid converting the data twice. Vertices( VertexMode mode, List positions, { - List? textureCoordinates, List? colors, + List? textureCoordinates, List? indices, }) : assert(mode != null), assert(positions != null) { - if (textureCoordinates != null && textureCoordinates.length != positions.length) { - throw ArgumentError('"positions" and "textureCoordinates" lengths must match.'); - } if (colors != null && colors.length != positions.length) { throw ArgumentError('"positions" and "colors" lengths must match.'); } - if (indices != null && indices.any((int i) => i < 0 || i >= positions.length)) { - throw ArgumentError('"indices" values must be valid indices in the positions list.'); + if (textureCoordinates != null && textureCoordinates.length != positions.length) { + throw ArgumentError('"positions" and "textureCoordinates" lengths must match.'); + } + if (indices != null) { + for (int index = 0; index < indices.length; index += 1) { + if (indices[index] >= positions.length) { + throw ArgumentError( + '"indices" values must be valid indices in the positions list ' + '(i.e. numbers in the range 0..${positions.length - 1}), ' + 'but indices[$index] is ${indices[index]}, which is too big.', + ); + } + } } - final Float32List encodedPositions = _encodePointList(positions); final Float32List? encodedTextureCoordinates = (textureCoordinates != null) ? _encodePointList(textureCoordinates) @@ -4418,51 +4503,78 @@ class Vertices extends NativeFieldWrapperClass1 { } } - /// Creates a set of vertex data for use with [Canvas.drawVertices], directly - /// using the encoding methods of [Vertices.new]. - /// Note that this constructor uses raw typed data lists, - /// so it runs faster than the [Vertices()] constructor - /// because it doesn't require any conversion from Dart lists. - /// - /// The [mode] parameter must not be null. - /// - /// The [positions] parameter is a list of triangular mesh vertices and - /// is interpreted as a list of repeated pairs of x,y coordinates. - /// It must not be null. - /// - /// The [textureCoordinates] list is interpreted as a list of repeated pairs - /// of x,y coordinates, and must be the same length of [positions] if it - /// is not null. - /// The [textureCoordinates] parameter is used to cutout - /// the image set in the image shader. - /// The cut part is applied to the triangular mesh. - /// Note that the [textureCoordinates] are the coordinates on the image. - /// - /// The [colors] list is interpreted as a list of ARGB encoded colors, similar - /// to [Color.value]. It must be half length of [positions] if it is not - /// null. - /// - /// If the [indices] list is provided, all values in the list must be - /// valid index values for [positions]. - /// e.g. The [indices] parameter for a simple triangle is [0,1,2]. + /// Creates a set of vertex data for use with [Canvas.drawVertices], using the + /// encoding expected by the Flutter engine. + /// + /// The `mode` parameter describes how the points should be interpreted: as + /// independent triangles ([VertexMode.triangles]), as a sliding window of + /// points forming a chain of triangles each sharing one side with the next + /// ([VertexMode.triangleStrip]), or as a fan of triangles with a single + /// shared point ([VertexMode.triangleFan]). + /// + /// The `positions` parameter provides the points in the canvas space that + /// will be use to draw the triangles. Each point is represented as two + /// numbers in the list, the first giving the x coordinate and the second + /// giving the y coordinate. (As a result, the list must have an even number + /// of entries.) + /// + /// The `colors` parameter, if specified, provides the color for each point in + /// `positions`. Each color is represented as ARGB with 8 bit color channels + /// (like [Color.value]'s internal representation), and the list, if + /// specified, must therefore be half the length of `positions`. Each triangle + /// is painted as a gradient that blends between the three colors at the three + /// points of that triangle. (These colors are then blended with the [Paint] + /// specified in the call to [Canvas.drawVertices].) + /// + /// The `textureCoordinates` parameter, if specified, provides the points in + /// the [Paint] image to sample for the corresponding points in `positions`. + /// Each point is represented as two numbers in the list, the first giving the + /// x coordinate and the second giving the y coordinate. This list, if + /// specified, must be the same length as `positions`. + /// + /// The `indices` parameter specifies the order in which the points should be + /// painted. If it is omitted (or present but empty), the points are processed + /// in the order they are given in `positions`, as if the `indices` was a list + /// from 0 to n-2, where _n_ is the number of pairs in `positions` (i.e. half + /// the length of `positions`). The `indices` parameter, if present and + /// non-empty, must have at least three entries, but may be of any length + /// beyond this. Indicies may refer to offsets in the positions array multiple + /// times, or may skip positions entirely. + /// + /// If the `indices` parameter is specified, all values in the list must be + /// valid index values for pairs in `positions`. For example, if there are 12 + /// numbers in `positions` (representing 6 coordinates), the `indicies` must + /// be numbers in the range 0..5 inclusive. + /// + /// The `mode` and `positions` parameters must not be null. Vertices.raw( VertexMode mode, Float32List positions, { - Float32List? textureCoordinates, Int32List? colors, + Float32List? textureCoordinates, Uint16List? indices, }) : assert(mode != null), assert(positions != null) { - if (textureCoordinates != null && textureCoordinates.length != positions.length) { - throw ArgumentError('"positions" and "textureCoordinates" lengths must match.'); + if (positions.length % 2 != 0) { + throw ArgumentError('"positions" must have an even number of entries (each coordinate is an x,y pair).'); } if (colors != null && colors.length * 2 != positions.length) { throw ArgumentError('"positions" and "colors" lengths must match.'); } - if (indices != null && indices.any((int i) => i < 0 || i >= positions.length)) { - throw ArgumentError('"indices" values must be valid indices in the positions list.'); + if (textureCoordinates != null && textureCoordinates.length != positions.length) { + throw ArgumentError('"positions" and "textureCoordinates" lengths must match.'); + } + if (indices != null) { + for (int index = 0; index < indices.length; index += 1) { + if (indices[index] * 2 >= positions.length) { + throw ArgumentError( + '"indices" values must be valid indices in the positions list ' + '(i.e. numbers in the range 0..${positions.length ~/ 2 - 1}), ' + 'but indices[$index] is ${indices[index]}, which is too big.', + ); + } + } } - if (!_init(this, mode.index, positions, textureCoordinates, colors, indices)) { throw ArgumentError('Invalid configuration for vertices.'); } @@ -4493,7 +4605,7 @@ class Vertices extends NativeFieldWrapperClass1 { external void _dispose(); bool _disposed = false; - /// Whether this reference to the underlying picture is [dispose]d. + /// Whether this reference to the underlying vertex data is [dispose]d. /// /// This only returns a valid value if asserts are enabled, and must not be /// used otherwise. @@ -4503,13 +4615,13 @@ class Vertices extends NativeFieldWrapperClass1 { disposed = _disposed; return true; }()); - return disposed ?? (throw StateError('$runtimeType.debugDisposed is only available when asserts are enabled.')); + return disposed ?? (throw StateError('Vertices.debugDisposed is only available when asserts are enabled.')); } } /// Defines how a list of points is interpreted when drawing a set of points. /// -/// Used by [Canvas.drawPoints]. +/// Used by [Canvas.drawPoints] and [Canvas.drawRawPoints]. // These enum values must be kept in sync with SkCanvas::PointMode. enum PointMode { /// Draw each point separately. @@ -4531,7 +4643,7 @@ enum PointMode { /// [Paint.style]). lines, - /// Draw the entire sequence of point as one line. + /// Draw the entire sequence of points as one line. /// /// The lines are stroked as described by the [Paint] (ignoring /// [Paint.style]). @@ -5286,6 +5398,9 @@ class Canvas extends NativeFieldWrapperClass1 { /// /// The `points` argument is interpreted as offsets from the origin. /// + /// The `paint` is used for each point ([PointMode.points]) or line + /// ([PointMode.lines] or [PointMode.polygon]), ignoring [Paint.style]. + /// /// See also: /// /// * [drawRawPoints], which takes `points` as a [Float32List] rather than a @@ -5302,6 +5417,9 @@ class Canvas extends NativeFieldWrapperClass1 { /// The `points` argument is interpreted as a list of pairs of floating point /// numbers, where each pair represents an x and y offset from the origin. /// + /// The `paint` is used for each point ([PointMode.points]) or line + /// ([PointMode.lines] or [PointMode.polygon]), ignoring [Paint.style]. + /// /// See also: /// /// * [drawPoints], which takes `points` as a [List] rather than a @@ -5319,18 +5437,26 @@ class Canvas extends NativeFieldWrapperClass1 { @FfiNative, Handle, Handle, Int32, Handle)>('Canvas::drawPoints') external void _drawPoints(List? paintObjects, ByteData paintData, int pointMode, Float32List points); - /// Draws the set of [Vertices] onto the canvas. - /// - /// The [blendMode] parameter is used to control how the colors in - /// the [vertices] are combined with the colors in the [paint]. - /// If there are no colors specified in [vertices] then the [blendMode] has - /// no effect. If there are colors in the [vertices], - /// then the color taken from the [Shader] or [Color] in the [paint] is - /// blended with the colors specified in the [vertices] using - /// the [blendMode] parameter. - /// For purposes of this blending, - /// the colors from the [paint] are considered the source and the colors from - /// the [vertices] are considered the destination. + /// Draws a set of [Vertices] onto the canvas as one or more triangles. + /// + /// The [Paint.color] property specifies the default color to use for the + /// triangles. + /// + /// The [Paint.shader] property, if set, overrides the color entirely, + /// replacing it with the colors from the specified [ImageShader], [Gradient], + /// or other shader. + /// + /// The `blendMode` parameter is used to control how the colors in the + /// `vertices` are combined with the colors in the `paint`. If there are no + /// colors specified in `vertices` then the `blendMode` has no effect. If + /// there are colors in the `vertices`, then the color taken from the + /// [Paint.shader] or [Paint.color] in the `paint` is blended with the colors + /// specified in the `vertices` using the `blendMode` parameter. For the + /// purposes of this blending, the colors from the `paint` parameter are + /// considered the source, and the colors from the `vertices` are considered + /// the destination. [BlendMode.dstOver] ignores the `paint` and uses only the + /// colors of the `vertices`; [BlendMode.srcOver] ignores the colors of the + /// `vertices` and uses only the colors in the `paint`. /// /// All parameters must not be null. /// diff --git a/lib/web_ui/lib/canvas.dart b/lib/web_ui/lib/canvas.dart index afed170c199a3..6e845799be22a 100644 --- a/lib/web_ui/lib/canvas.dart +++ b/lib/web_ui/lib/canvas.dart @@ -25,8 +25,8 @@ abstract class Vertices { factory Vertices( VertexMode mode, List positions, { - List? textureCoordinates, List? colors, + List? textureCoordinates, List? indices, }) { return engine.renderer.createVertices(mode, @@ -38,8 +38,8 @@ abstract class Vertices { factory Vertices.raw( VertexMode mode, Float32List positions, { - Float32List? textureCoordinates, Int32List? colors, + Float32List? textureCoordinates, Uint16List? indices, }) { return engine.renderer.createVerticesRaw(mode, diff --git a/lib/web_ui/lib/src/engine/html/render_vertices.dart b/lib/web_ui/lib/src/engine/html/render_vertices.dart index 2b062f8f5f607..9480bd952c3f0 100644 --- a/lib/web_ui/lib/src/engine/html/render_vertices.dart +++ b/lib/web_ui/lib/src/engine/html/render_vertices.dart @@ -70,7 +70,7 @@ class SurfaceVertices implements ui.Vertices { if (assertionsEnabled) { return _disposed; } - throw StateError('Vertices.debugDisposed is only avialalbe when asserts are enabled.'); + throw StateError('Vertices.debugDisposed is only available when asserts are enabled.'); } } diff --git a/testing/dart/BUILD.gn b/testing/dart/BUILD.gn index 04035d13d8f2e..1c372c5ed9ea1 100644 --- a/testing/dart/BUILD.gn +++ b/testing/dart/BUILD.gn @@ -32,6 +32,7 @@ tests = [ "lerp_test.dart", "locale_test.dart", "mask_filter_test.dart", + "painting_test.dart", "paragraph_builder_test.dart", "paragraph_test.dart", "path_test.dart", diff --git a/testing/dart/compositing_test.dart b/testing/dart/compositing_test.dart index a679a40e27e3f..03f5fbd706da2 100644 --- a/testing/dart/compositing_test.dart +++ b/testing/dart/compositing_test.dart @@ -2,7 +2,7 @@ // 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 ByteData, Float64List; +import 'dart:typed_data'; import 'dart:ui'; import 'package:litetest/litetest.dart'; diff --git a/testing/dart/painting_test.dart b/testing/dart/painting_test.dart new file mode 100644 index 0000000000000..680f639a8481f --- /dev/null +++ b/testing/dart/painting_test.dart @@ -0,0 +1,63 @@ +// 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:typed_data'; +import 'dart:ui'; + +import 'package:litetest/litetest.dart'; + +void main() { + test('Vertices checks', () { + try { + Vertices( + VertexMode.triangles, + const [Offset.zero, Offset.zero, Offset.zero], + indices: Uint16List.fromList(const [0, 2, 5]), + ); + throw 'Vertices did not throw the expected error.'; + } on ArgumentError catch (e) { + expect('$e', 'Invalid argument(s): "indices" values must be valid indices in the positions list (i.e. numbers in the range 0..2), but indices[2] is 5, which is too big.'); + } + Vertices( // This one does not throw. + VertexMode.triangles, + const [Offset.zero], + ).dispose(); + Vertices( // This one should not throw. + VertexMode.triangles, + const [Offset.zero, Offset.zero, Offset.zero], + indices: Uint16List.fromList(const [0, 2, 1, 2, 0, 1, 2, 0]), // Uint16List implements List so this is ok. + ).dispose(); + }); + + test('Vertices.raw checks', () { + try { + Vertices.raw( + VertexMode.triangles, + Float32List.fromList(const [0.0]), + ); + throw 'Vertices.raw did not throw the expected error.'; + } on ArgumentError catch (e) { + expect('$e', 'Invalid argument(s): "positions" must have an even number of entries (each coordinate is an x,y pair).'); + } + try { + Vertices.raw( + VertexMode.triangles, + Float32List.fromList(const [0.0, 0.0, 0.0, 0.0, 0.0, 0.0]), + indices: Uint16List.fromList(const [0, 2, 5]), + ); + throw 'Vertices.raw did not throw the expected error.'; + } on ArgumentError catch (e) { + expect('$e', 'Invalid argument(s): "indices" values must be valid indices in the positions list (i.e. numbers in the range 0..2), but indices[2] is 5, which is too big.'); + } + Vertices.raw( // This one does not throw. + VertexMode.triangles, + Float32List.fromList(const [0.0, 0.0]), + ).dispose(); + Vertices.raw( // This one should not throw. + VertexMode.triangles, + Float32List.fromList(const [0.0, 0.0, 0.0, 0.0, 0.0, 0.0]), + indices: Uint16List.fromList(const [0, 2, 1, 2, 0, 1, 2, 0]), + ).dispose(); + }); +} diff --git a/testing/dart/path_test.dart b/testing/dart/path_test.dart index 8b2647fd1750e..306797a32395c 100644 --- a/testing/dart/path_test.dart +++ b/testing/dart/path_test.dart @@ -225,4 +225,19 @@ void main() { expect(newFirstMetric.getTangentForOffset(4.0)!.vector, const Offset(0.0, 1.0)); expect(newFirstMetric.extractPath(4.0, 10.0).computeMetrics().first.length, 6.0); }); + + test('PathMetrics on a mutated path', () { + final Path path = Path() + ..lineTo(0, 30) + ..lineTo(40, 30) + ..moveTo(100, 0) + ..lineTo(100, 30) + ..lineTo(140, 30) + ..close(); + final PathMetrics metrics = path.computeMetrics(); + expect(metrics.toString(), + '(PathMetric(length: 70.0, isClosed: false, contourIndex: 0), ' + 'PathMetric(length: 120.0, isClosed: true, contourIndex: 1))', + ); + }); }