diff --git a/lib/common/Point.dart b/lib/common/Point.dart index 29cef06..ab16dbc 100644 --- a/lib/common/Point.dart +++ b/lib/common/Point.dart @@ -13,6 +13,13 @@ class Point { operator *(covariant Point p) => Point(x * p.x, y * p.y); operator /(covariant Point p) => Point(x / p.x, y / p.y); + @override + bool operator ==(Object other) => + other is Point && x == other.x && y == other.y; + + @override + int get hashCode => x.hashCode ^ y.hashCode; + Point addX(num n) => Point(x + n, y); Point addY(num n) => Point(x, y + n); Point add(num n) => Point(x + n, y + n); @@ -38,4 +45,4 @@ class Point { @override String toString() => '($x,$y)'; -} \ No newline at end of file +} diff --git a/lib/svg/parser.dart b/lib/svg/parser.dart index 0120a13..59ca422 100644 --- a/lib/svg/parser.dart +++ b/lib/svg/parser.dart @@ -73,13 +73,13 @@ String _stripArray(String stringToParse) { return stringToParse; } -_ParserResult _parseNumber(String stringToParse) { +_ParserResult _parseNumber(String stringToParse) { final res = _floatPattern.firstMatch(stringToParse); if (res == null) { throw InvalidPathError("Expected a number, got '$stringToParse'."); } - final number = double.parse(res.group(0)!); + final number = num.parse(res.group(0)!); final start = res.start; final end = res.end; stringToParse = @@ -89,7 +89,7 @@ _ParserResult _parseNumber(String stringToParse) { return _ParserResult(value: number, remaining: stringToParse); } -_ParserResult _parseUnsignedNumber(String stringToParse) { +_ParserResult _parseUnsignedNumber(String stringToParse) { final number = _parseNumber(stringToParse); if (number.value < 0) { throw InvalidPathError("Expected a non-negative number, got '$number'."); @@ -137,6 +137,7 @@ List<_Command> _commandifyPath(String pathdef) { for (String c in pathdef.split(_commandPattern)) { String x = c[0]; String? y = (c.length > 1) ? c.substring(1).trim() : null; + if (!_commands.contains(x)) { throw InvalidPathError("Path does not start with a command: $pathdef"); } @@ -146,9 +147,8 @@ List<_Command> _commandifyPath(String pathdef) { } if (x == "z" || x == "Z") { // The end command takes no arguments, so add a blank one - token.addAll([x, ""]); + token = [x, ""]; } else { - // token = [x, x.substring(1).trim()]; token = [x]; } @@ -242,7 +242,7 @@ Path parsePath(String pathdef) { segments.add(Line(start: currentPos, end: pos)); currentPos = pos; } else if (command == "H") { - double hpos = token.args[0] as double; + num hpos = token.args[0] as num; if (!absolute) { hpos += currentPos.x; } @@ -250,7 +250,7 @@ Path parsePath(String pathdef) { segments.add(Line(start: currentPos, end: pos)); currentPos = pos; } else if (command == "V") { - double vpos = token.args[0] as double; + num vpos = token.args[0] as num; if (!absolute) { vpos += currentPos.y; } @@ -304,10 +304,11 @@ Path parsePath(String pathdef) { } segments.add( CubicBezier( - start: currentPos, - control1: control1, - control2: control2, - end: end), + start: currentPos, + control1: control1, + control2: control2, + end: end, + ), ); currentPos = end; } else if (command == "Q") { @@ -354,8 +355,8 @@ Path parsePath(String pathdef) { } else if (command == "A") { // For some reason I implemented the Arc with a complex radius. // That doesn't really make much sense, but... *shrugs* - final radius = Point(token.args[0] as double, token.args[1] as double); - final rotation = token.args[2] as double; + final radius = Point(token.args[0] as num, token.args[1] as num); + final rotation = token.args[2] as num; final arc = token.args[3] as bool; final sweep = token.args[4] as bool; Point end = token.args[5] as Point; diff --git a/lib/svg/path.dart b/lib/svg/path.dart index 86b7a08..8a85036 100644 --- a/lib/svg/path.dart +++ b/lib/svg/path.dart @@ -9,30 +9,34 @@ import 'package:bisection/extension.dart'; import '../common/Point.dart'; -double radians(num n) => n * pi / 180; -double degrees(num n) => n * 180 / pi; +num radians(num n) => n * pi / 180; +num degrees(num n) => n * 180 / pi; const defaultMinDepth = 5; const defaultError = 1e-12; +extension _RemovePointIfInt on num { + num get removePointIfInt => truncate() == this ? truncate() : this; +} + /// Recursively approximates the length by straight lines -double segmentLength({ +num segmentLength({ required SvgPath curve, required num start, required num end, required Point startPoint, required Point endPoint, - required double error, + required num error, required int minDepth, - required double depth, + required num depth, }) { num mid = (start + end) / 2; Point midPoint = curve.point(mid); - double length = (endPoint - startPoint).abs(); - double firstHalf = (midPoint - startPoint).abs(); - double secondHalf = (endPoint - midPoint).abs(); + num length = (endPoint - startPoint).abs(); + num firstHalf = (midPoint - startPoint).abs(); + num secondHalf = (endPoint - midPoint).abs(); - double length2 = firstHalf + secondHalf; + num length2 = firstHalf + secondHalf; if ((length2 - length > error) || (depth < minDepth)) { // Calculate the length of each segment: depth += 1; @@ -70,11 +74,18 @@ abstract class SvgPath { required this.end, }); + @override + bool operator ==(Object other) => + other is SvgPath && start == other.start && end == other.end; + + @override + int get hashCode => start.hashCode ^ end.hashCode; + /// Calculate the x,y position at a certain position of the path Point point(num pos); /// Calculate the length of the path up to a certain position - double size({double error = defaultError, int minDepth = defaultMinDepth}); + num size({num error = defaultError, int minDepth = defaultMinDepth}); } abstract class Bezier extends SvgPath { @@ -83,6 +94,12 @@ abstract class Bezier extends SvgPath { required Point end, }) : super(start: start, end: end); + @override + bool operator ==(Object other) => other is Bezier && super == other; + + @override + int get hashCode => super.hashCode + 0; + /// Checks if this segment would be a smooth segment following the previous bool isSmoothFrom(Object? previous); } @@ -95,11 +112,17 @@ class Linear extends SvgPath { required Point end, }) : super(start: start, end: end); + @override + bool operator ==(Object other) => other is Linear && super == other; + + @override + int get hashCode => super.hashCode + 0; + @override Point point(num pos) => start + (end - start).times(pos); @override - double size({double error = defaultError, int minDepth = defaultMinDepth}) { + num size({num error = defaultError, int minDepth = defaultMinDepth}) { final distance = end - start; return sqrt(distance.x * distance.x + distance.y * distance.y); } @@ -111,6 +134,12 @@ class Line extends Linear { required Point end, }) : super(start: start, end: end); + @override + bool operator ==(Object other) => other is Line && super == other; + + @override + int get hashCode => super.hashCode + 0; + @override String toString() { return "Line(start=$start, end=$end)"; @@ -128,6 +157,16 @@ class CubicBezier extends Bezier { required Point end, }) : super(start: start, end: end); + @override + bool operator ==(Object other) => + other is CubicBezier && + control1 == other.control1 && + control2 == other.control2 && + super == other; + + @override + int get hashCode => super.hashCode ^ control1.hashCode ^ control2.hashCode; + @override String toString() => "CubicBezier(start=$start, control1=$control1, " "control2=$control2, end=$end)"; @@ -146,18 +185,19 @@ class CubicBezier extends Bezier { end.times(math.pow(pos, 3)); @override - double size({double error = defaultError, int minDepth = defaultMinDepth}) { + num size({num error = defaultError, int minDepth = defaultMinDepth}) { final startPoint = point(0); final endPoint = point(1); return segmentLength( - curve: this, - start: 0, - end: 1, - startPoint: startPoint, - endPoint: endPoint, - error: error, - minDepth: minDepth, - depth: 0); + curve: this, + start: 0, + end: 1, + startPoint: startPoint, + endPoint: endPoint, + error: error, + minDepth: minDepth, + depth: 0, + ); } } @@ -168,10 +208,14 @@ class QuadraticBezier extends Bezier { required Point start, required Point end, required this.control, - }) : super( - start: start, - end: end, - ); + }) : super(start: start, end: end); + + @override + bool operator ==(Object other) => + other is QuadraticBezier && control == other.control && super == other; + + @override + int get hashCode => super.hashCode ^ control.hashCode; @override String toString() => @@ -190,12 +234,12 @@ class QuadraticBezier extends Bezier { end.times(math.pow(pos, 2)); @override - double size({double error = defaultError, int minDepth = defaultMinDepth}) { + num size({num error = defaultError, int minDepth = defaultMinDepth}) { final Point a = start - control.times(2) + end; final Point b = (control - start).times(2); final num aDotB = a.x * b.x + a.y * b.y; - late final double s; + late final num s; if (a.abs() < 1e-12) { s = b.abs(); } else if ((aDotB + a.abs() * b.abs()).abs() < 1e-12) { @@ -208,11 +252,11 @@ class QuadraticBezier extends Bezier { final num B = 4 * (a.x * b.x + a.y * b.y); final num C = b.x * b.x + b.y * b.y; - final double sabc = 2 * sqrt(A + B + C); - final double a2 = sqrt(A); - final double a32 = 2 * A * a2; - final double c2 = 2 * sqrt(C); - final double bA = B / a2; + final num sabc = 2 * sqrt(A + B + C); + final num a2 = sqrt(A); + final num a32 = 2 * A * a2; + final num c2 = 2 * sqrt(C); + final num bA = B / a2; s = (a32 * sabc + a2 * B * (sabc - c2) + @@ -227,7 +271,7 @@ class QuadraticBezier extends Bezier { /// large and sweep are 1 or 0 (True/False also work) class Arc extends SvgPath { final Point radius; - final double rotation; + final num rotation; final bool arc; final bool sweep; late final num radiusScale; @@ -246,6 +290,23 @@ class Arc extends SvgPath { _parameterize(); } + @override + bool operator ==(Object other) => + other is Arc && + radius == other.radius && + rotation == other.rotation && + arc == other.arc && + sweep == other.sweep && + super == other; + + @override + int get hashCode => + super.hashCode ^ + radius.hashCode ^ + rotation.hashCode ^ + arc.hashCode ^ + sweep.hashCode; + @override String toString() => 'Arc(start=$start, radius=$radius, rotation=$rotation, ' 'arc=$arc, sweep=$sweep, end=$end)'; @@ -289,7 +350,7 @@ class Arc extends SvgPath { final t1 = rxSq * y1primSq; final t2 = rySq * x1primSq; - double c = sqrt(((rxSq * rySq - t1 - t2) / (t1 + t2)).abs()); + num c = sqrt(((rxSq * rySq - t1 - t2) / (t1 + t2)).abs()); if (arc == sweep) { c = -c; @@ -352,7 +413,7 @@ class Arc extends SvgPath { /// integration, and in that case it's simpler to just do a geometric /// approximation, as for cubic bezier curves. @override - double size({double error = defaultError, minDepth = defaultMinDepth}) { + num size({num error = defaultError, minDepth = defaultMinDepth}) { // This is equivalent of omitting the segment if (start == end) return 0; @@ -387,6 +448,13 @@ class Arc extends SvgPath { class Move extends SvgPath { const Move({required Point to}) : super(start: to, end: to); + + @override + bool operator ==(Object other) => other is Move && super == other; + + @override + int get hashCode => super.hashCode + 0; + @override String toString() => "Move(to=$start)"; @@ -394,8 +462,7 @@ class Move extends SvgPath { Point point(num pos) => start; @override - double size({double error = defaultError, int minDepth = defaultMinDepth}) => - 0; + num size({num error = defaultError, int minDepth = defaultMinDepth}) => 0; } /// Represents the closepath command @@ -405,10 +472,12 @@ class Close extends Linear { required Point end, }) : super(start: start, end: end); - // def __eq__(self, other): - // if not isinstance(other, Close): - // return NotImplemented - // return self.start == other.start and self.end == other.end + + @override + bool operator ==(Object other) => other is Close && super == other; + + @override + int get hashCode => super.hashCode + 0; @override String toString() => "Close(start=$start, end=$end)"; @@ -425,6 +494,14 @@ class Path extends ListBase { segments = []; } + Path.fromSegments(this.segments); + + @override + bool operator ==(Object other) => other is Path && segments == other.segments; + + @override + int get hashCode => segments.hashCode; + @override SvgPath operator [](int index) => segments[index]!; @@ -445,7 +522,7 @@ class Path extends ListBase { 'Path(${[for (final s in segments) s.toString()].join(", ")})'; void _calcLengths( - {double error = defaultError, int minDepth = defaultMinDepth}) { + {num error = defaultError, int minDepth = defaultMinDepth}) { if (_memoizedLength != null) return; final lengths = [ @@ -466,7 +543,7 @@ class Path extends ListBase { } } - Point point({required num pos, double error = defaultError}) { + Point point({required num pos, num error = defaultError}) { // Shortcuts if (pos == 0.0) { return segments[0]!.point(pos); @@ -505,7 +582,7 @@ class Path extends ListBase { SvgPath? previousSegment; final end = last.end; - String formatNumber(num n) => n.toString(); + String formatNumber(num n) => n.removePointIfInt.toString(); String coord(Point p) => '${formatNumber(p.x)},${formatNumber(p.y)}'; for (final segment in this) { @@ -518,7 +595,7 @@ class Path extends ListBase { } else if (segment is Move || (currentPos != start) || (start == end && previousSegment is! Move)) { - parts.add("M ${coord(start)}"); + parts.add("M ${coord(segment.start)}"); } if (segment is Line) { @@ -540,7 +617,7 @@ class Path extends ListBase { } else if (segment is Arc) { parts.add( "A ${coord(segment.radius)} ${formatNumber(segment.rotation)} " - "${(segment.arc ? 1 : 0).toDouble},${(segment.sweep ? 1 : 0).toDouble} ${coord(end)}", + "${segment.arc ? 1 : 0},${segment.sweep ? 1 : 0} ${coord(segment.end)}", ); } @@ -548,6 +625,6 @@ class Path extends ListBase { previousSegment = segment; } - return parts.join(" "); + return parts.join(" ").toUpperCase(); } } diff --git a/test/kanimaji_test.dart b/test/kanimaji_test.dart deleted file mode 100644 index 7061791..0000000 --- a/test/kanimaji_test.dart +++ /dev/null @@ -1,12 +0,0 @@ -import 'package:flutter_test/flutter_test.dart'; - -import 'package:kanimaji/kanimaji.dart'; - -void main() { - test('adds one to input values', () { - final calculator = Calculator(); - expect(calculator.addOne(2), 3); - expect(calculator.addOne(-7), -6); - expect(calculator.addOne(0), 1); - }); -} diff --git a/test/svg/parser_test.dart b/test/svg/parser_test.dart new file mode 100644 index 0000000..f0d06bf --- /dev/null +++ b/test/svg/parser_test.dart @@ -0,0 +1,279 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:kanimaji/common/Point.dart'; +import 'package:kanimaji/svg/parser.dart'; +import 'package:kanimaji/svg/path.dart'; + +void main() { + /// """Examples from the SVG spec""" + test("svg_examples", () { + Path path1 = parsePath("M 100 100 L 300 100 L 200 300 z"); + expect( + path1, + Path.fromSegments([ + const Move(to: Point(100, 100)), + const Line(start: Point(100, 100), end: Point(300, 100)), + const Line(start: Point(300, 100), end: Point(200, 300)), + const Close(start: Point(200, 300), end: Point(100, 100)), + ]), + ); + + // for Z command behavior when there is multiple subpaths + path1 = parsePath("M 0 0 L 50 20 M 100 100 L 300 100 L 200 300 z"); + expect( + path1, + Path.fromSegments(const [ + Move(to: Point.zero), + Line(start: Point.zero, end: Point(50, 20)), + Move(to: Point(100, 100)), + Line(start: Point(100, 100), end: Point(300, 100)), + Line(start: Point(300, 100), end: Point(200, 300)), + Close(start: Point(200, 300), end: Point(100, 100)), + ]), + ); + + path1 = parsePath("M 100 100 L 200 200"); + Path path2 = parsePath("M100 100L200 200"); + expect(path1, path2); + + path1 = parsePath("M 100 200 L 200 100 L -100 -200"); + path2 = parsePath("M 100 200 L 200 100 -100 -200"); + expect(path1, path2); + + path1 = parsePath("""M100,200 C100,100 250,100 250,200 + S400,300 400,200"""); + expect( + path1, + Path.fromSegments(const [ + Move(to: Point(100, 200)), + CubicBezier( + start: Point(100, 200), + control1: Point(100, 100), + control2: Point(250, 100), + end: Point(250, 200), + ), + CubicBezier( + start: Point(250, 200), + control1: Point(250, 300), + control2: Point(400, 300), + end: Point(400, 200), + ), + ]), + ); + + path1 = parsePath("M100,200 C100,100 400,100 400,200"); + expect( + path1, + Path.fromSegments(const [ + Move(to: Point(100, 200)), + CubicBezier( + start: Point(100, 200), + control1: Point(100, 100), + control2: Point(400, 100), + end: Point(400, 200), + ), + ]), + ); + + path1 = parsePath("M100,500 C25,400 475,400 400,500"); + expect( + path1, + Path.fromSegments(const [ + Move(to: Point(100, 500)), + CubicBezier( + start: Point(100, 500), + control1: Point(25, 400), + control2: Point(475, 400), + end: Point(400, 500), + ), + ]), + ); + + // path1 = parse_path("M100,800 C175,700 325,700 400,800") + // self.assertEqual( + // path1, + // Path( + // Move(100 + 800j), + // CubicBezier(100 + 800j, 175 + 700j, 325 + 700j, 400 + 800j), + // ), + // ) + + // path1 = parse_path("M600,200 C675,100 975,100 900,200") + // self.assertEqual( + // path1, + // Path( + // Move(600 + 200j), + // CubicBezier(600 + 200j, 675 + 100j, 975 + 100j, 900 + 200j), + // ), + // ) + + // path1 = parse_path("M600,500 C600,350 900,650 900,500") + // self.assertEqual( + // path1, + // Path( + // Move(600 + 500j), + // CubicBezier(600 + 500j, 600 + 350j, 900 + 650j, 900 + 500j), + // ), + // ) + + // path1 = parse_path( + // """M600,800 C625,700 725,700 750,800 + // S875,900 900,800""" + // ) + // self.assertEqual( + // path1, + // Path( + // Move(600 + 800j), + // CubicBezier(600 + 800j, 625 + 700j, 725 + 700j, 750 + 800j), + // CubicBezier(750 + 800j, 775 + 900j, 875 + 900j, 900 + 800j), + // ), + // ) + + // path1 = parse_path("M200,300 Q400,50 600,300 T1000,300") + // self.assertEqual( + // path1, + // Path( + // Move(200 + 300j), + // QuadraticBezier(200 + 300j, 400 + 50j, 600 + 300j), + // QuadraticBezier(600 + 300j, 800 + 550j, 1000 + 300j), + // ), + // ) + + // path1 = parse_path("M300,200 h-150 a150,150 0 1,0 150,-150 z") + // self.assertEqual( + // path1, + // Path( + // Move(300 + 200j), + // Line(300 + 200j, 150 + 200j), + // Arc(150 + 200j, 150 + 150j, 0, 1, 0, 300 + 50j), + // Close(300 + 50j, 300 + 200j), + // ), + // ) + + // path1 = parse_path("M275,175 v-150 a150,150 0 0,0 -150,150 z") + // self.assertEqual( + // path1, + // Path( + // Move(275 + 175j), + // Line(275 + 175j, 275 + 25j), + // Arc(275 + 25j, 150 + 150j, 0, 0, 0, 125 + 175j), + // Close(125 + 175j, 275 + 175j), + // ), + // ) + + // path1 = parse_path("M275,175 v-150 a150,150 0 0,0 -150,150 L 275,175 z") + // self.assertEqual( + // path1, + // Path( + // Move(275 + 175j), + // Line(275 + 175j, 275 + 25j), + // Arc(275 + 25j, 150 + 150j, 0, 0, 0, 125 + 175j), + // Line(125 + 175j, 275 + 175j), + // Close(275 + 175j, 275 + 175j), + // ), + // ) + + // path1 = parse_path( + // """M600,350 l 50,-25 + // a25,25 -30 0,1 50,-25 l 50,-25 + // a25,50 -30 0,1 50,-25 l 50,-25 + // a25,75 -30 0,1 50,-25 l 50,-25 + // a25,100 -30 0,1 50,-25 l 50,-25""" + // ) + // self.assertEqual( + // path1, + // Path( + // Move(600 + 350j), + // Line(600 + 350j, 650 + 325j), + // Arc(650 + 325j, 25 + 25j, -30, 0, 1, 700 + 300j), + // Line(700 + 300j, 750 + 275j), + // Arc(750 + 275j, 25 + 50j, -30, 0, 1, 800 + 250j), + // Line(800 + 250j, 850 + 225j), + // Arc(850 + 225j, 25 + 75j, -30, 0, 1, 900 + 200j), + // Line(900 + 200j, 950 + 175j), + // Arc(950 + 175j, 25 + 100j, -30, 0, 1, 1000 + 150j), + // Line(1000 + 150j, 1050 + 125j), + // ), + // ); + }); + + // def test_others(self): + // # Other paths that need testing: + + // # Relative moveto: + // path1 = parse_path("M 0 0 L 50 20 m 50 80 L 300 100 L 200 300 z") + // self.assertEqual( + // path1, + // Path( + // Move(0j), + // Line(0 + 0j, 50 + 20j), + // Move(100 + 100j), + // Line(100 + 100j, 300 + 100j), + // Line(300 + 100j, 200 + 300j), + // Close(200 + 300j, 100 + 100j), + // ), + // ) + + // # Initial smooth and relative CubicBezier + // path1 = parse_path("""M100,200 s 150,-100 150,0""") + // self.assertEqual( + // path1, + // Path( + // Move(100 + 200j), + // CubicBezier(100 + 200j, 100 + 200j, 250 + 100j, 250 + 200j), + // ), + // ) + + // # Initial smooth and relative QuadraticBezier + // path1 = parse_path("""M100,200 t 150,0""") + // self.assertEqual( + // path1, + // Path(Move(100 + 200j), QuadraticBezier(100 + 200j, 100 + 200j, 250 + 200j)), + // ) + + // # Relative QuadraticBezier + // path1 = parse_path("""M100,200 q 0,0 150,0""") + // self.assertEqual( + // path1, + // Path(Move(100 + 200j), QuadraticBezier(100 + 200j, 100 + 200j, 250 + 200j)), + // ) + + // def test_negative(self): + // """You don't need spaces before a minus-sign""" + // path1 = parse_path("M100,200c10-5,20-10,30-20") + // path2 = parse_path("M 100 200 c 10 -5 20 -10 30 -20") + // self.assertEqual(path1, path2) + + // def test_numbers(self): + // """Exponents and other number format cases""" + // # It can be e or E, the plus is optional, and a minimum of +/-3.4e38 must be supported. + // path1 = parse_path("M-3.4e38 3.4E+38L-3.4E-38,3.4e-38") + // path2 = Path( + // Move(-3.4e38 + 3.4e38j), Line(-3.4e38 + 3.4e38j, -3.4e-38 + 3.4e-38j) + // ) + // self.assertEqual(path1, path2) + + // def test_errors(self): + // self.assertRaises(ValueError, parse_path, "M 100 100 L 200 200 Z 100 200") + + // def test_non_path(self): + // # It's possible in SVG to create paths that has zero length, + // # we need to handle that. + + // path = parse_path("M10.236,100.184") + // self.assertEqual(path.d(), "M 10.236,100.184") + + // def test_issue_45(self): + // path = parse_path( + // "m 1672.2372,-54.8161 " + // "a 14.5445,14.5445 0 0 0 -11.3152,23.6652 " + // "l 27.2573,27.2572 27.2572,-27.2572 " + // "a 14.5445,14.5445 0 0 0 -11.3012,-23.634 " + // "a 14.5445,14.5445 0 0 0 -11.414,5.4625 " + // "l -4.542,4.5420 " + // "l -4.5437,-4.5420 " + // "a 14.5445,14.5445 0 0 0 -11.3984,-5.4937 " + // "z" + // ) + + // self.assertIn("A 14.5445,14.5445 0 0,0 1672.24,-54.8161 Z", path.d()) +} diff --git a/test/svg/svg_test.dart b/test/svg/svg_test.dart new file mode 100644 index 0000000..4bd8520 --- /dev/null +++ b/test/svg/svg_test.dart @@ -0,0 +1,28 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:kanimaji/svg/parser.dart'; + +void main() { + test('Test SVG Paths', () { + final paths = [ + "M 100,100 L 300,100 L 200,300 Z", + "M 0,0 L 50,20 M 100,100 L 300,100 L 200,300 Z", + "M 100,100 L 200,200", + "M 100,200 L 200,100 L -100,-200", + "M 100,200 C 100,100 250,100 250,200 S 400,300 400,200", + "M 100,200 C 100,100 400,100 400,200", + "M 100,500 C 25,400 475,400 400,500", + "M 100,800 C 175,700 325,700 400,800", + "M 600,200 C 675,100 975,100 900,200", + "M 600,500 C 600,350 900,650 900,500", + "M 600,800 C 625,700 725,700 750,800 S 875,900 900,800", + "M 200,300 Q 400,50 600,300 T 1000,300", + "M -3.4E+38,3.4E+38 L -3.4E-38,3.4E-38", + "M 0,0 L 50,20 M 50,20 L 200,100 Z", + "M 600,350 L 650,325 A 25,25 -30 0,1 700,300 L 750,275", + ]; + + for (final path in paths) { + expect(parsePath(path).d(), path); + } + }); +}