diff --git a/packages/vector_graphics_compiler/bin/vector_graphics_compiler.dart b/packages/vector_graphics_compiler/bin/vector_graphics_compiler.dart deleted file mode 100644 index 8856b63f5568..000000000000 --- a/packages/vector_graphics_compiler/bin/vector_graphics_compiler.dart +++ /dev/null @@ -1 +0,0 @@ -void main(List arguments) {} diff --git a/packages/vector_graphics_compiler/lib/src/geometry/basic_types.dart b/packages/vector_graphics_compiler/lib/src/geometry/basic_types.dart new file mode 100644 index 000000000000..e55619ea8057 --- /dev/null +++ b/packages/vector_graphics_compiler/lib/src/geometry/basic_types.dart @@ -0,0 +1,122 @@ +import 'dart:math' as math; +import 'package:meta/meta.dart'; + +/// An immutable position in two-dimensional space. +/// +/// This class is roughly compatible with dart:ui's Offset. +@immutable +class Point { + /// Creates a point object with x,y coordinates. + const Point(this.x, this.y); + + static const Point zero = Point(0, 0); + + /// The offset along the x-axis of this point. + final double x; + + /// The offset along the y-axis of this point. + final double y; + + @override + int get hashCode => Object.hash(x, y); + + @override + bool operator ==(Object other) { + return other is Point && other.x == x && other.y == y; + } + + Point operator /(double divisor) { + return Point(x / divisor, y / divisor); + } + + Point operator *(double multiplicand) { + return Point(x * multiplicand, y * multiplicand); + } + + @override + String toString() => 'Point($x, $y)'; +} + +/// An immutable, 2D, axis-aligned, floating-point rectangle whose coordinates +/// are relative to a given origin. +@immutable +class Rect { + /// Creates a rectangle from the specified left, top, right, and bottom + /// positions. + const Rect.fromLTRB(this.left, this.top, this.right, this.bottom); + + /// Creates a rectangle from the specified left and top positions with width + /// and height dimensions. + const Rect.fromLTWH(double left, double top, double width, double height) + : this.fromLTRB(left, top, left + width, top + height); + + /// Creates a rectangle representing a circle with centerpoint `x,`y` and + /// radius `r`. + const Rect.fromCircle(double x, double y, double r) + : this.fromLTRB(x - r, y - r, x + r, y + r); + + /// A rectangle covering the entire coordinate space, equal to dart:ui's + /// definition. + static const Rect largest = Rect.fromLTRB(-1e9, -1e9, 1e9, 1e9); + + /// A rectangle with the top, left, right, and bottom edges all at zero. + static const Rect zero = Rect.fromLTRB(0, 0, 0, 0); + + /// The x-axis offset of left edge. + final double left; + + /// The y-axis offset of the top edge. + final double top; + + /// The x-axis offset of the right edge. + final double right; + + /// The y-axis offset of the bottom edge. + final double bottom; + + /// The width of the rectangle. + double get width => right - left; + + /// The height of the rectangle. + double get height => bottom - top; + + /// The top left corner of the rect. + Point get topLeft => Point(left, top); + + /// The top right corner of the rect. + Point get topRight => Point(right, top); + + /// The bottom left corner of the rect. + Point get bottomLeft => Point(bottom, left); + + /// The bottom right corner of the rect. + Point get bottomRight => Point(bottom, right); + + /// The size of the rectangle, expressed as a [Point]. + Point get size => Point(width, height); + + /// Creates the smallest rectangle that covers the edges of this and `other`. + Rect expanded(Rect other) { + return Rect.fromLTRB( + math.min(left, other.left), + math.min(top, other.top), + math.max(right, other.right), + math.max(bottom, other.bottom), + ); + } + + @override + String toString() => 'Rect.fromLTRB($left, $top, $right, $bottom)'; + + @override + int get hashCode => Object.hash(left, top, right, bottom); + + @override + bool operator ==(Object other) { + return other is Rect && + other.left == left && + other.top == top && + other.right == right && + other.bottom == bottom; + } +} diff --git a/packages/vector_graphics_compiler/lib/src/geometry/matrix.dart b/packages/vector_graphics_compiler/lib/src/geometry/matrix.dart new file mode 100644 index 000000000000..e70b186cf790 --- /dev/null +++ b/packages/vector_graphics_compiler/lib/src/geometry/matrix.dart @@ -0,0 +1,193 @@ +import 'dart:math' as math; +import 'dart:typed_data'; + +import 'package:meta/meta.dart'; + +import 'basic_types.dart'; + +/// An immutable affine matrix, a 3x3 column-major-order matrix in which the +/// last row is always set to the identity values, i.e. `0 0 1`. +@immutable +class AffineMatrix { + /// Creates an immutable affine matrix. To work with the identity matrix, use + /// the [identity] property. + const AffineMatrix( + this.a, + this.b, + this.c, + this.d, + this.e, + this.f, [ + this._m4_10 = 1.0, + ]); + + /// The identity affine matrix. + static const AffineMatrix identity = AffineMatrix(1, 0, 0, 1, 0, 0); + + /// The 0,0 position of the matrix. + final double a; + + /// The 1,0 position of the matrix. + final double b; + + /// The 0,1 position of the matrix. + final double c; + + /// The 1,1 position of the matrix. + final double d; + + /// The 2,0 position of the matrix. + final double e; + + /// The 2,1 position of the matrix. + final double f; + + /// Translations can affect this value, so we have to track it. + final double _m4_10; + + /// Creates a new affine matrix rotated by `radians`. + AffineMatrix rotated(double radians) { + if (radians == 0) { + return this; + } + final double cosAngle = math.cos(radians); + final double sinAngle = math.sin(radians); + return AffineMatrix( + (a * cosAngle) + (c * sinAngle), + (b * cosAngle) + (d * sinAngle), + (a * -sinAngle) + (c * cosAngle), + (b * -sinAngle) + (d * cosAngle), + e, + f, + _m4_10, + ); + } + + /// Creates a new affine matrix rotated by `x` and `y`. + /// + /// If `y` is not specified, it is defaulted to the same value as `x`. + AffineMatrix scaled(double x, [double? y]) { + y ??= x; + if (x == 1 && y == 1) { + return this; + } + return AffineMatrix( + a * x, + b * x, + c * y, + d * y, + e, + f, + _m4_10 * x, + ); + } + + /// Creates a new affine matrix, translated along the x and y axis. + AffineMatrix translated(double x, double y) { + return AffineMatrix( + a, + b, + c, + d, + (a * x) + (c * y) + e, + (b * x) + (d * y) + f, + _m4_10, + ); + } + + /// Creates a new affine matrix of this concatenated with `other`. + AffineMatrix multiplied(AffineMatrix other) { + return AffineMatrix( + (a * other.a) + (c * other.b), + (b * other.a) + (d * other.b), + (a * other.c) + (c * other.d), + (b * other.c) + (d * other.d), + (a * other.e) + (c * other.f) + e, + (b * other.e) + (d * other.f) + f, + _m4_10, + ); + } + + /// Maps `point` using the values of this matrix. + Point transformPoint(Point point) { + return Point( + (a * point.x) + (c * point.y) + e, + (b * point.x) + (d * point.y) + f, + ); + } + + /// Maps `rect` using the values of this matrix. + Rect transformRect(Rect rect) { + final double x = rect.left; + final double y = rect.top; + final double w = rect.width; + final double h = rect.height; + + final double wx = a * w; + final double hx = c * h; + final double rx = a * x + c * y; + + final double wy = b * w; + final double hy = d * h; + final double ry = b * x + d * y; + + double left = rx; + double right = rx; + if (wx < 0) { + left += wx; + } else { + right += wx; + } + if (hx < 0) { + left += hx; + } else { + right += hx; + } + + double top = ry; + double bottom = ry; + if (wy < 0) { + top += wy; + } else { + bottom += wy; + } + if (hy < 0) { + top += hy; + } else { + bottom += hy; + } + + return Rect.fromLTRB(left, top, right, bottom); + } + + /// Creates a typed data representatino of this matrix suitable for use with + /// `package:vector_math_64` (and, by extension, Flutter/dart:ui). + Float64List toMatrix4() { + return Float64List.fromList([ + a, b, 0, 0, // + c, d, 0, 0, // + 0, 0, _m4_10, 0, // + e, f, 0, 1.0, // + ]); + } + + @override + int get hashCode => Object.hash(a, b, c, d, e, f, _m4_10); + + @override + bool operator ==(Object other) { + return other is AffineMatrix && + other.a == a && + other.b == b && + other.d == d && + other.e == e && + other._m4_10 == _m4_10; + } + + @override + String toString() => ''' +[ $a, $c, $e ] +[ $b, $d, $f ] +[ 0.0, 0.0, 1.0 ] +'''; +} diff --git a/packages/vector_graphics_compiler/lib/src/geometry/path.dart b/packages/vector_graphics_compiler/lib/src/geometry/path.dart new file mode 100644 index 000000000000..e61c09b27533 --- /dev/null +++ b/packages/vector_graphics_compiler/lib/src/geometry/path.dart @@ -0,0 +1,476 @@ +import 'package:meta/meta.dart'; +import 'package:path_parsing/path_parsing.dart'; + +import 'basic_types.dart'; +import 'matrix.dart'; +import '../util.dart'; + +// This is a magic number used by impeller for radius approximation: +// https://github.com/flutter/impeller/blob/a2478aa4939a9a08c6c3810f72e0db42e7383a07/geometry/path_builder.cc#L9 +// See https://spencermortensen.com/articles/bezier-circle/ for more information. +const double _kArcApproximationMagic = 0.551915024494; + +/// Specifies the winding rule that decies how the interior of a [Path] is +/// calculated. +/// +/// This enum is used by the [Path.fillType] property. +/// +/// It is compatible with the same enum in `dart:ui`. +enum PathFillType { + /// The interior is defined by a non-zero sum of signed edge crossings. + /// + /// For a given point, the point is considered to be on the inside of the path + /// if a line drawn from the point to infinity crosses lines going clockwise + /// around the point a different number of times than it crosses lines going + /// counter-clockwise around that point. + nonZero, + + /// The interior is defined by an odd number of edge crossings. + /// + /// For a given point, the point is considered to be on the inside of the path + /// if a line drawn from the point to infinity crosses an odd number of lines. + evenOdd, +} + +/// The available types of path verbs. +/// +/// Used by [PathCommand.type]. +enum PathCommandType { + /// A path verb that picks up the pen to move it to another coordinate, + /// starting a new contour. + move, + + /// A path verb that draws a line from the current point to a specified + /// coordinate. + line, + + /// A path verb that draws a Bezier curve from the current point to a + /// specified point using two control points. + cubic, + + /// A path verb that draws a line from the current point to the starting + /// point of the current contour. + close, +} + +/// An abstract, immutable representation of a path verb and its associated +/// points. +/// +/// [Path] objects are collections of [PathCommand]s. To create a path object, +/// use a [PathBuilder]. To create a path object from an SVG path definition, +/// use [parseSvgPathData]. +@immutable +abstract class PathCommand { + const PathCommand._(this.type); + + /// The type of this path command. + final PathCommandType type; + + /// Returns a new path command transformed by `matrix`. + PathCommand transformed(AffineMatrix matrix); +} + +class LineToCommand extends PathCommand { + const LineToCommand(this.x, this.y) : super._(PathCommandType.line); + + /// The absolute offset of the destination point for this path from the x + /// axis. + final double x; + + /// The absolute offset of the destination point for this path from the y + /// axis. + final double y; + + @override + LineToCommand transformed(AffineMatrix matrix) { + final Point xy = matrix.transformPoint(Point(x, y)); + return LineToCommand(xy.x, xy.y); + } + + @override + int get hashCode => Object.hash(type, x, y); + + @override + bool operator ==(Object other) { + return other is LineToCommand && other.x == x && other.y == y; + } + + @override + String toString() { + return '..lineTo($x, $y)'; + } +} + +class MoveToCommand extends PathCommand { + const MoveToCommand(this.x, this.y) : super._(PathCommandType.move); + + /// The absolute offset of the destination point for this path from the x + /// axis. + final double x; + + /// The absolute offset of the destination point for this path from the y + /// axis. + final double y; + + @override + MoveToCommand transformed(AffineMatrix matrix) { + final Point xy = matrix.transformPoint(Point(x, y)); + return MoveToCommand(xy.x, xy.y); + } + + @override + int get hashCode => Object.hash(type, x, y); + + @override + bool operator ==(Object other) { + return other is MoveToCommand && other.x == x && other.y == y; + } + + @override + String toString() { + return '..moveTo($x, $y)'; + } +} + +class CubicToCommand extends PathCommand { + const CubicToCommand(this.x1, this.y1, this.x2, this.y2, this.x3, this.y3) + : super._(PathCommandType.cubic); + + /// The absolute offset of the first control point for this path from the x + /// axis. + final double x1; + + /// The absolute offset of the first control point for this path from the y + /// axis. + final double y1; + + /// The absolute offset of the second control point for this path from the x + /// axis. + final double x2; + + /// The absolute offset of the second control point for this path from the x + /// axis. + final double y2; + + /// The absolute offset of the destination point for this path from the x + /// axis. + final double x3; + + /// The absolute offset of the destination point for this path from the y + /// axis. + final double y3; + + @override + CubicToCommand transformed(AffineMatrix matrix) { + final Point xy1 = matrix.transformPoint(Point(x1, y1)); + final Point xy2 = matrix.transformPoint(Point(x2, y2)); + final Point xy3 = matrix.transformPoint(Point(x3, y3)); + return CubicToCommand(xy1.x, xy1.y, xy2.x, xy2.y, xy3.x, xy3.y); + } + + @override + int get hashCode => Object.hash(type, x1, y1, x2, y2, x3, y3); + + @override + bool operator ==(Object other) { + return other is CubicToCommand && + other.x1 == x1 && + other.y1 == y1 && + other.x2 == x2 && + other.y2 == y2 && + other.x3 == x3 && + other.y3 == y3; + } + + @override + String toString() { + return '..cubicTo($x1, $y1, $x2, $y2, $x3, $y3)'; + } +} + +class CloseCommand extends PathCommand { + const CloseCommand() : super._(PathCommandType.close); + + @override + CloseCommand transformed(AffineMatrix matrix) { + return this; + } + + @override + int get hashCode => type.hashCode; + + @override + bool operator ==(Object other) { + return other is CloseCommand; + } + + @override + String toString() { + return '..close()'; + } +} + +/// Creates a new builder of [Path] objects. +class PathBuilder implements PathProxy { + /// Creates a new path builder for paths of the specified fill type. + /// + /// By default, will create non-zero filled paths. + PathBuilder([this.fillType = PathFillType.nonZero]); + + /// Creates a new mutable path builder object from an existing [Path]. + PathBuilder.fromPath(Path path) { + addPath(path); + fillType = path.fillType; + } + + final List _commands = []; + + Point _currentSubPathPoint = Point.zero; + + /// The last destination point used by this builder. + Point get currentPoint => _currentPoint; + Point _currentPoint = Point.zero; + + @override + void close() { + _commands.add(const CloseCommand()); + _currentPoint = _currentSubPathPoint; + } + + @override + void cubicTo( + double x1, + double y1, + double x2, + double y2, + double x3, + double y3, + ) { + _commands.add(CubicToCommand(x1, y1, x2, y2, x3, y3)); + _currentPoint = Point(x3, y3); + } + + @override + void lineTo(double x, double y) { + _commands.add(LineToCommand(x, y)); + _currentPoint = Point(x, y); + } + + @override + void moveTo(double x, double y) { + _commands.add(MoveToCommand(x, y)); + _currentPoint = _currentSubPathPoint = Point(x, y); + } + + /// Adds the commands of an existing path to the new path being created. + void addPath(Path other) { + _commands.addAll(other._commands); + } + + /// Adds an oval command to new path. + void addOval(Rect oval) { + final Point r = Point(oval.width * 0.5, oval.height * 0.5); + final Point c = Point( + oval.left + (oval.width * 0.5), + oval.top + (oval.height * 0.5), + ); + final Point m = Point( + _kArcApproximationMagic * r.x, + _kArcApproximationMagic * r.y, + ); + + moveTo(c.x, c.y - r.y); + + // Top right arc. + cubicTo(c.x + m.x, c.y - r.y, c.x + r.x, c.y - m.y, c.x + r.x, c.y); + + // Bottom right arc. + cubicTo(c.x + r.x, c.y + m.y, c.x + m.x, c.y + r.y, c.x, c.y + r.y); + + // Bottom left arc. + cubicTo(c.x - m.x, c.y + r.y, c.x - r.x, c.y + m.y, c.x - r.x, c.y); + + // Top left arc. + cubicTo(c.x - r.x, c.y - m.y, c.x - m.x, c.y - r.y, c.x, c.y - r.y); + + close(); + } + + /// Adds a rectangle to the new path. + void addRect(Rect rect) { + lineTo(rect.top, rect.right); + lineTo(rect.bottom, rect.right); + lineTo(rect.bottom, rect.left); + close(); + } + + /// Adds a rounded rectangle to the new path. + void addRRect(Rect rect, double rx, double ry) { + if (rx == 0 && ry == 0) { + addRect(rect); + return; + } + + final magicRadius = Point(rx, ry) * _kArcApproximationMagic; + + moveTo(rect.left + rx, rect.top); + + // Top line. + lineTo(rect.left + rect.width - rx, rect.top); + + // Top right arc. + // + cubicTo( + rect.left + rect.width - rx + magicRadius.x, + rect.top, + rect.left + rect.width, + rect.top + ry - magicRadius.y, + rect.left + rect.width, + rect.top + ry, + ); + + // Right line. + lineTo(rect.left + rect.width, rect.top + rect.height - ry); + + // Bottom right arc. + cubicTo( + rect.left + rect.width, + rect.top + rect.height - ry + magicRadius.y, + rect.left + rect.width - rx + magicRadius.x, + rect.top + rect.height, + rect.left + rect.width - rx, + rect.top + rect.height, + ); + + // Bottom line. + lineTo(rect.left + rx, rect.top + rect.height); + + // Bottom left arc. + cubicTo( + rect.left + rx - magicRadius.x, + rect.top + rect.height, + rect.left, + rect.top + rect.height - ry + magicRadius.y, + rect.left, + rect.top + rect.height - ry); + + // Left line. + lineTo(rect.left, rect.top + ry); + + // Top left arc. + cubicTo( + rect.left, + rect.top + ry - magicRadius.y, + rect.left + rx - magicRadius.x, + rect.top, + rect.left + rx, + rect.top, + ); + + close(); + } + + /// The fill type to use for the new path. + late PathFillType fillType; + + /// Creates a new [Path] object from the commands in this path. + /// + /// If `reset` is set to false, this builder can be used to create multiple + /// path objects with the same commands. By default, the builder will reset + /// to an initial state. + Path toPath({bool reset = true}) { + // TODO: bounds + Rect bounds = Rect.zero; + + final Path path = Path( + commands: _commands, + fillType: fillType, + bounds: bounds, + ); + + if (reset) { + _commands.clear(); + } + return path; + } +} + +/// An immutable collection of [PathCommand]s. +@immutable +class Path { + /// Creates a new immutable collection of [PathCommand]s. + Path({ + List commands = const [], + this.fillType = PathFillType.nonZero, + required this.bounds, + }) { + _commands.addAll(commands); + } + + /// Whether this path has any commands. + bool get isEmpty => _commands.isEmpty; + + /// The commands this path contains. + Iterable get commands => _commands; + + final List _commands = []; + + /// The fill type of this path, defaulting to [PathFillType.nonZero]. + final PathFillType fillType; + + /// The bounds of this path object. + final Rect bounds; + + Path transformed(AffineMatrix matrix) { + final List commands = []; + for (final PathCommand command in _commands) { + commands.add(command.transformed(matrix)); + } + return Path( + commands: commands, + fillType: fillType, + // TODO: is this safe? What the commands have degenerated? Should probably + // recalculate this. + bounds: matrix.transformRect(bounds), + ); + } + + @override + int get hashCode => Object.hash(Object.hashAll(_commands), fillType); + + @override + bool operator ==(Object other) { + return other is Path && + listEquals(_commands, other._commands) && + other.fillType == fillType && + other.bounds == bounds; + } + + @override + String toString() { + final StringBuffer buffer = StringBuffer('Path()'); + if (fillType != PathFillType.nonZero) { + buffer.write('\n ..fillType = $fillType'); + } + for (final command in commands) { + buffer.write('\n $command'); + } + buffer.write(';'); + return buffer.toString(); + } +} + +/// Creates a new [Path] object from an SVG path data string. +Path parseSvgPathData(String svg) { + if (svg == '') { + return Path(bounds: Rect.zero); + } + + final SvgPathStringSource parser = SvgPathStringSource(svg); + final PathBuilder pathBuilder = PathBuilder(); + final SvgPathNormalizer normalizer = SvgPathNormalizer(); + for (PathSegmentData seg in parser.parseSegments()) { + normalizer.emitSegment(seg, pathBuilder); + } + return pathBuilder.toPath(); +} diff --git a/packages/vector_graphics_compiler/lib/src/util.dart b/packages/vector_graphics_compiler/lib/src/util.dart new file mode 100644 index 000000000000..0361371c7aca --- /dev/null +++ b/packages/vector_graphics_compiler/lib/src/util.dart @@ -0,0 +1,17 @@ +bool listEquals(List? a, List? b) { + if (a == null) { + return b == null; + } + if (b == null || a.length != b.length) { + return false; + } + if (identical(a, b)) { + return true; + } + for (int index = 0; index < a.length; index += 1) { + if (a[index] != b[index]) { + return false; + } + } + return true; +} diff --git a/packages/vector_graphics_compiler/lib/vector_graphics_compiler.dart b/packages/vector_graphics_compiler/lib/vector_graphics_compiler.dart new file mode 100644 index 000000000000..cc43578009eb --- /dev/null +++ b/packages/vector_graphics_compiler/lib/vector_graphics_compiler.dart @@ -0,0 +1,3 @@ +export 'src/geometry/basic_types.dart'; +export 'src/geometry/matrix.dart'; +export 'src/geometry/path.dart'; diff --git a/packages/vector_graphics_compiler/pubspec.yaml b/packages/vector_graphics_compiler/pubspec.yaml index 013c13302ab1..a756fc1ce0b4 100644 --- a/packages/vector_graphics_compiler/pubspec.yaml +++ b/packages/vector_graphics_compiler/pubspec.yaml @@ -6,5 +6,11 @@ homepage: https://github.com/dnfield/vector_graphics environment: sdk: '>=2.12.0 <3.0.0' +dependencies: + meta: ^1.7.0 + path_parsing: ^1.0.0 + dev_dependencies: flutter_lints: ^1.0.0 + test: ^1.20.1 + vector_math: ^2.1.2 diff --git a/packages/vector_graphics_compiler/test/basic_types_test.dart b/packages/vector_graphics_compiler/test/basic_types_test.dart new file mode 100644 index 000000000000..6e29041a8ceb --- /dev/null +++ b/packages/vector_graphics_compiler/test/basic_types_test.dart @@ -0,0 +1,31 @@ +import 'package:vector_graphics_compiler/vector_graphics_compiler.dart'; + +import 'package:test/test.dart'; + +void main() { + test('Point tests', () { + expect(Point.zero.x, 0); + expect(Point.zero.y, 0); + + expect(const Point(5, 5) / 2, const Point(2.5, 2.5)); + expect(const Point(5, 5) * 2, const Point(10, 10)); + }); + + test('Rect tests', () { + expect(Rect.zero.left, 0); + expect(Rect.zero.top, 0); + expect(Rect.zero.right, 0); + expect(Rect.zero.bottom, 0); + + expect( + const Rect.fromLTRB(1, 2, 3, 4) + .expanded(const Rect.fromLTRB(0, 0, 10, 10)), + const Rect.fromLTRB(0, 0, 10, 10), + ); + + expect( + const Rect.fromCircle(10, 10, 5), + const Rect.fromLTWH(5, 5, 10, 10), + ); + }); +} diff --git a/packages/vector_graphics_compiler/test/matrix_test.dart b/packages/vector_graphics_compiler/test/matrix_test.dart new file mode 100644 index 000000000000..71c660a7d1ba --- /dev/null +++ b/packages/vector_graphics_compiler/test/matrix_test.dart @@ -0,0 +1,107 @@ +import 'dart:math' as math; + +import 'package:vector_graphics_compiler/vector_graphics_compiler.dart'; + +import 'package:test/test.dart'; +import 'package:vector_math/vector_math_64.dart'; + +void main() { + test('Identity matrix', () { + expect(AffineMatrix.identity.toMatrix4(), Matrix4.identity().storage); + }); + + test('Multiply', () { + const matrix1 = AffineMatrix(2, 2, 3, 4, 5, 6); + const matrix2 = AffineMatrix(7, 8, 9, 10, 11, 12); + + final matrix4_1 = Matrix4.fromFloat64List(matrix1.toMatrix4()); + final matrix4_2 = Matrix4.fromFloat64List(matrix2.toMatrix4()); + expect( + matrix1.multiplied(matrix2).toMatrix4(), + matrix4_1.multiplied(matrix4_2).storage, + ); + }); + + test('Scale', () { + const matrix1 = AffineMatrix(2, 2, 3, 4, 5, 6); + + final matrix4_1 = Matrix4.fromFloat64List(matrix1.toMatrix4()); + expect( + matrix1.scaled(2, 3).toMatrix4(), + matrix4_1.scaled(2.0, 3.0).storage, + ); + + expect( + matrix1.scaled(2).toMatrix4(), + matrix4_1.scaled(2.0, 2.0).storage, + ); + }); + + test('Scale and multiply', () { + const matrix1 = AffineMatrix(2, 2, 3, 4, 5, 6); + const matrix2 = AffineMatrix(7, 8, 9, 10, 11, 12); + + final matrix4_1 = Matrix4.fromFloat64List(matrix1.toMatrix4()); + final matrix4_2 = Matrix4.fromFloat64List(matrix2.toMatrix4()); + expect( + matrix1.scaled(2, 3).multiplied(matrix2).toMatrix4(), + matrix4_1.scaled(2.0, 3.0).multiplied(matrix4_2).storage, + ); + }); + + test('Translate', () { + const matrix1 = AffineMatrix(2, 2, 3, 4, 5, 6); + + final matrix4_1 = Matrix4.fromFloat64List(matrix1.toMatrix4()); + matrix4_1.translate(2.0, 3.0); + expect( + matrix1.translated(2, 3).toMatrix4(), + matrix4_1.storage, + ); + }); + + test('Rotate', () { + const matrix1 = AffineMatrix(2, 2, 3, 4, 5, 6); + + final matrix4_1 = Matrix4.fromFloat64List(matrix1.toMatrix4()); + matrix4_1.rotateZ(31.0); + expect( + matrix1.rotated(31).toMatrix4(), + matrix4_1.storage, + ); + }); + + test('transformRect', () { + const epsillon = .0000001; + const Rect rectangle20x20 = Rect.fromLTRB(10, 20, 30, 40); + + // Identity + expect( + AffineMatrix.identity.transformRect(rectangle20x20), + rectangle20x20, + ); + + // 2D Scaling + expect( + AffineMatrix.identity.scaled(2).transformRect(rectangle20x20), + const Rect.fromLTRB(20, 40, 60, 80), + ); + + // Rotation + final rotatedRect = AffineMatrix.identity + .rotated(math.pi / 2.0) + .transformRect(rectangle20x20); + expect(rotatedRect.left + 40, lessThan(epsillon)); + expect(rotatedRect.top - 10, lessThan(epsillon)); + expect(rotatedRect.right + 20, lessThan(epsillon)); + expect(rotatedRect.bottom - 30, lessThan(epsillon)); + }); + + test('== and hashCode account for hidden field', () { + const AffineMatrix matrixA = AffineMatrix.identity; + const AffineMatrix matrixB = AffineMatrix(1, 0, 0, 1, 0, 0, 0); + + expect(matrixA != matrixB, true); + expect(matrixA.hashCode != matrixB.hashCode, true); + }); +} diff --git a/packages/vector_graphics_compiler/test/path_test.dart b/packages/vector_graphics_compiler/test/path_test.dart new file mode 100644 index 000000000000..fced481ea7db --- /dev/null +++ b/packages/vector_graphics_compiler/test/path_test.dart @@ -0,0 +1,200 @@ +import 'dart:math' as math; + +import 'package:vector_graphics_compiler/vector_graphics_compiler.dart'; + +import 'package:test/test.dart'; + +void main() { + test('SVG Path tests', () { + Path path = parseSvgPathData( + 'M22.1595 3.80852C19.6789 1.35254 16.3807 -4.80966e-07 12.8727 ' + '-4.80966e-07C9.36452 -4.80966e-07 6.06642 1.35254 3.58579 3.80852C1.77297 5.60333 ' + '0.53896 7.8599 0.0171889 10.3343C-0.0738999 10.7666 0.206109 11.1901 0.64265 11.2803' + 'C1.07908 11.3706 1.50711 11.0934 1.5982 10.661C2.05552 8.49195 3.13775 6.51338 4.72783 ' + '4.9391C9.21893 0.492838 16.5262 0.492728 21.0173 4.9391C25.5082 9.38548 25.5082 16.6202 ' + '21.0173 21.0667C16.5265 25.5132 9.21893 25.5133 4.72805 21.0669C3.17644 19.5307 2.10538 ' + '17.6035 1.63081 15.4937C1.53386 15.0627 1.10252 14.7908 0.66697 14.887C0.231645 14.983 ' + '-0.0427272 15.4103 0.0542205 15.8413C0.595668 18.2481 1.81686 20.4461 3.5859 22.1976' + 'C6.14623 24.7325 9.50955 26 12.8727 26C16.236 26 19.5991 24.7326 22.1595 22.1976C27.2802 ' + '17.1277 27.2802 8.87841 22.1595 3.80852Z'); + expect( + path.toString(), + 'Path()\n' + ' ..moveTo(22.1595, 3.80852)\n' + ' ..cubicTo(19.6789, 1.35254, 16.3807, -4.809659999999999e-7, 12.8727, -4.809659999999999e-7)\n' + ' ..cubicTo(9.36452, -4.809659999999999e-7, 6.06642, 1.35254, 3.5857900000000003, 3.80852)\n' + ' ..cubicTo(1.77297, 5.60333, 0.53896, 7.8599, 0.017188900000000007, 10.3343)\n' + ' ..cubicTo(-0.0738999, 10.7666, 0.20610900000000001, 11.1901, 0.6426500000000002, 11.2803)\n' + ' ..cubicTo(1.07908, 11.3706, 1.50711, 11.0934, 1.5982, 10.661)\n' + ' ..cubicTo(2.05552, 8.49195, 3.13775, 6.51338, 4.72783, 4.9391)\n' + ' ..cubicTo(9.21893, 0.49283800000000005, 16.5262, 0.49272800000000005, 21.0173, 4.9391)\n' + ' ..cubicTo(25.5082, 9.38548, 25.5082, 16.6202, 21.0173, 21.0667)\n' + ' ..cubicTo(16.5265, 25.5132, 9.21893, 25.5133, 4.72805, 21.0669)\n' + ' ..cubicTo(3.17644, 19.5307, 2.10538, 17.6035, 1.63081, 15.4937)\n' + ' ..cubicTo(1.53386, 15.0627, 1.10252, 14.7908, 0.6669700000000002, 14.887)\n' + ' ..cubicTo(0.23164500000000002, 14.983, -0.04272720000000001, 15.4103, 0.05422050000000001, 15.8413)\n' + ' ..cubicTo(0.5956680000000001, 18.2481, 1.8168600000000001, 20.4461, 3.5859, 22.1976)\n' + ' ..cubicTo(6.14623, 24.7325, 9.50955, 26.0, 12.8727, 26.0)\n' + ' ..cubicTo(16.236, 26.0, 19.5991, 24.7326, 22.1595, 22.1976)\n' + ' ..cubicTo(27.2802, 17.1277, 27.2802, 8.87841, 22.1595, 3.80852)\n' + ' ..close();', + ); + + path = parseSvgPathData('M10 10L20 20'); + + expect( + path.toString(), + 'Path()\n' + ' ..moveTo(10.0, 10.0)\n' + ' ..lineTo(20.0, 20.0);', + ); + }); + + test('addRect', () { + final PathBuilder builder = PathBuilder() + ..addRect(const Rect.fromLTRB(10, 10, 20, 20)); + + expect( + builder.toPath().toString(), + 'Path()\n' + ' ..lineTo(10.0, 20.0)\n' + ' ..lineTo(20.0, 20.0)\n' + ' ..lineTo(20.0, 10.0)\n' + ' ..close();', + ); + }); + + test('addOval', () { + final PathBuilder builder = PathBuilder() + ..addOval(const Rect.fromLTRB(10, 10, 20, 20)) + ..addOval(const Rect.fromLTRB(50, 50, 80, 70)); + expect( + builder.toPath().toString(), + 'Path()\n' + ' ..moveTo(15.0, 10.0)\n' + ' ..cubicTo(17.75957512247, 10.0, 20.0, 12.24042487753, 20.0, 15.0)\n' + ' ..cubicTo(20.0, 17.75957512247, 17.75957512247, 20.0, 15.0, 20.0)\n' + ' ..cubicTo(12.24042487753, 20.0, 10.0, 17.75957512247, 10.0, 15.0)\n' + ' ..cubicTo(10.0, 12.24042487753, 12.24042487753, 10.0, 15.0, 10.0)\n' + ' ..close()\n' + ' ..moveTo(65.0, 50.0)\n' + ' ..cubicTo(73.27872536741, 50.0, 80.0, 54.48084975506, 80.0, 60.0)\n' + ' ..cubicTo(80.0, 65.51915024494, 73.27872536741, 70.0, 65.0, 70.0)\n' + ' ..cubicTo(56.72127463259, 70.0, 50.0, 65.51915024494, 50.0, 60.0)\n' + ' ..cubicTo(50.0, 54.48084975506, 56.72127463259, 50.0, 65.0, 50.0)\n' + ' ..close();', + ); + }); + + test('addRRect', () { + final PathBuilder builder = PathBuilder() + ..addRRect(const Rect.fromLTRB(20, 20, 60, 60), 5, 5); + expect( + builder.toPath().toString(), + 'Path()\n' + ' ..moveTo(25.0, 20.0)\n' + ' ..lineTo(55.0, 20.0)\n' + ' ..cubicTo(57.75957512247, 20.0, 60.0, 22.24042487753, 60.0, 25.0)\n' + ' ..lineTo(60.0, 55.0)\n' + ' ..cubicTo(60.0, 57.75957512247, 57.75957512247, 60.0, 55.0, 60.0)\n' + ' ..lineTo(25.0, 60.0)\n' + ' ..cubicTo(22.24042487753, 60.0, 20.0, 57.75957512247, 20.0, 55.0)\n' + ' ..lineTo(20.0, 25.0)\n' + ' ..cubicTo(20.0, 22.24042487753, 22.24042487753, 20.0, 25.0, 20.0)\n' + ' ..close();', + ); + }); + + test('reset/no reset', () { + final PathBuilder builder = PathBuilder()..lineTo(10, 10); + + final Path a = builder.toPath(reset: false); + final Path b = builder.toPath(); + final Path c = builder.toPath(); + + expect(a, b); + expect(identical(a, b), false); + expect(a != c, true); + expect(c.isEmpty, true); + }); + + test('PathBuilder.fromPath', () { + final PathBuilder builder = PathBuilder()..lineTo(10, 10); + + final Path a = builder.toPath(); + + final PathBuilder builderA = PathBuilder.fromPath(a); + final Path b = builderA.toPath(); + + expect(a, b); + expect(identical(a, b), false); + }); + + test('transforms', () { + Path path = parseSvgPathData( + 'M22.1595 3.80852C19.6789 1.35254 16.3807 -4.80966e-07 12.8727 ' + '-4.80966e-07C9.36452 -4.80966e-07 6.06642 1.35254 3.58579 3.80852C1.77297 5.60333 ' + '0.53896 7.8599 0.0171889 10.3343C-0.0738999 10.7666 0.206109 11.1901 0.64265 11.2803' + 'C1.07908 11.3706 1.50711 11.0934 1.5982 10.661C2.05552 8.49195 3.13775 6.51338 4.72783 ' + '4.9391C9.21893 0.492838 16.5262 0.492728 21.0173 4.9391C25.5082 9.38548 25.5082 16.6202 ' + '21.0173 21.0667C16.5265 25.5132 9.21893 25.5133 4.72805 21.0669C3.17644 19.5307 2.10538 ' + '17.6035 1.63081 15.4937C1.53386 15.0627 1.10252 14.7908 0.66697 14.887C0.231645 14.983 ' + '-0.0427272 15.4103 0.0542205 15.8413C0.595668 18.2481 1.81686 20.4461 3.5859 22.1976' + 'C6.14623 24.7325 9.50955 26 12.8727 26C16.236 26 19.5991 24.7326 22.1595 22.1976C27.2802 ' + '17.1277 27.2802 8.87841 22.1595 3.80852Z'); + expect( + path.transformed(AffineMatrix.identity).toString(), + 'Path()\n' + ' ..moveTo(22.1595, 3.80852)\n' + ' ..cubicTo(19.6789, 1.35254, 16.3807, -4.809659999999999e-7, 12.8727, -4.809659999999999e-7)\n' + ' ..cubicTo(9.36452, -4.809659999999999e-7, 6.06642, 1.35254, 3.5857900000000003, 3.80852)\n' + ' ..cubicTo(1.77297, 5.60333, 0.53896, 7.8599, 0.017188900000000007, 10.3343)\n' + ' ..cubicTo(-0.0738999, 10.7666, 0.20610900000000001, 11.1901, 0.6426500000000002, 11.2803)\n' + ' ..cubicTo(1.07908, 11.3706, 1.50711, 11.0934, 1.5982, 10.661)\n' + ' ..cubicTo(2.05552, 8.49195, 3.13775, 6.51338, 4.72783, 4.9391)\n' + ' ..cubicTo(9.21893, 0.49283800000000005, 16.5262, 0.49272800000000005, 21.0173, 4.9391)\n' + ' ..cubicTo(25.5082, 9.38548, 25.5082, 16.6202, 21.0173, 21.0667)\n' + ' ..cubicTo(16.5265, 25.5132, 9.21893, 25.5133, 4.72805, 21.0669)\n' + ' ..cubicTo(3.17644, 19.5307, 2.10538, 17.6035, 1.63081, 15.4937)\n' + ' ..cubicTo(1.53386, 15.0627, 1.10252, 14.7908, 0.6669700000000002, 14.887)\n' + ' ..cubicTo(0.23164500000000002, 14.983, -0.04272720000000001, 15.4103, 0.05422050000000001, 15.8413)\n' + ' ..cubicTo(0.5956680000000001, 18.2481, 1.8168600000000001, 20.4461, 3.5859, 22.1976)\n' + ' ..cubicTo(6.14623, 24.7325, 9.50955, 26.0, 12.8727, 26.0)\n' + ' ..cubicTo(16.236, 26.0, 19.5991, 24.7326, 22.1595, 22.1976)\n' + ' ..cubicTo(27.2802, 17.1277, 27.2802, 8.87841, 22.1595, 3.80852)\n' + ' ..close();', + ); + + expect( + path.transformed(AffineMatrix.identity.rotated(math.pi / 2)).toString(), + 'Path()\n' + ' ..moveTo(-3.808519999999999, 22.1595)\n' + ' ..cubicTo(-1.352539999999999, 19.6789, 4.809660010030285e-7, 16.3807, 4.809660007882255e-7, 12.8727)\n' + ' ..cubicTo(4.809660005734114e-7, 9.36452, -1.3525399999999996, 6.06642, -3.80852, 3.5857900000000007)\n' + ' ..cubicTo(-5.60333, 1.7729700000000004, -7.8599, 0.5389600000000004, -10.3343, 0.01718890000000064)\n' + ' ..cubicTo(-10.7666, -0.07389989999999934, -11.1901, 0.2061090000000007, -11.2803, 0.6426500000000008)\n' + ' ..cubicTo(-11.3706, 1.0790800000000007, -11.0934, 1.5071100000000006, -10.661, 1.5982000000000007)\n' + ' ..cubicTo(-8.49195, 2.0555200000000005, -6.51338, 3.1377500000000005, -4.9391, 4.72783)\n' + ' ..cubicTo(-0.4928379999999995, 9.21893, -0.49272799999999906, 16.5262, -4.939099999999999, 21.0173)\n' + ' ..cubicTo(-9.385479999999998, 25.5082, -16.6202, 25.5082, -21.0667, 21.0173)\n' + ' ..cubicTo(-25.5132, 16.5265, -25.5133, 9.218930000000002, -21.0669, 4.7280500000000005)\n' + ' ..cubicTo(-19.5307, 3.1764400000000013, -17.6035, 2.1053800000000007, -15.4937, 1.630810000000001)\n' + ' ..cubicTo(-15.0627, 1.533860000000001, -14.7908, 1.1025200000000008, -14.887, 0.6669700000000011)\n' + ' ..cubicTo(-14.983, 0.23164500000000093, -15.4103, -0.04272719999999906, -15.8413, 0.05422050000000098)\n' + ' ..cubicTo(-18.2481, 0.5956680000000012, -20.4461, 1.8168600000000015, -22.1976, 3.5859000000000014)\n' + ' ..cubicTo(-24.7325, 6.146230000000002, -26.0, 9.509550000000003, -26.0, 12.872700000000002)\n' + ' ..cubicTo(-26.0, 16.236, -24.7326, 19.5991, -22.1976, 22.1595)\n' + ' ..cubicTo(-17.1277, 27.2802, -8.878409999999999, 27.2802, -3.808519999999999, 22.1595)\n' + ' ..close();', + ); + + path = parseSvgPathData('M10 10L20 20'); + + expect( + path.transformed(AffineMatrix.identity.translated(10, 10)).toString(), + 'Path()\n' + ' ..moveTo(20.0, 20.0)\n' + ' ..lineTo(30.0, 30.0);', + ); + }); +} diff --git a/packages/vector_graphics_compiler/test/util_test.dart b/packages/vector_graphics_compiler/test/util_test.dart new file mode 100644 index 000000000000..0f242f5da315 --- /dev/null +++ b/packages/vector_graphics_compiler/test/util_test.dart @@ -0,0 +1,19 @@ +import 'package:test/test.dart'; +import 'package:vector_graphics_compiler/src/util.dart'; + +void main() { + test('listEquals', () { + final List listA = [1, 2, 3]; + final List listB = [1, 2, 3]; + final List listC = [1, 2]; + final List listD = [3, 2, 1]; + + expect(listEquals(null, null), isTrue); + expect(listEquals(listA, null), isFalse); + expect(listEquals(null, listB), isFalse); + expect(listEquals(listA, listA), isTrue); + expect(listEquals(listA, listB), isTrue); + expect(listEquals(listA, listC), isFalse); + expect(listEquals(listA, listD), isFalse); + }); +}