diff --git a/lib/svg/parser.dart b/lib/svg/parser.dart index a3b9248..d24002a 100644 --- a/lib/svg/parser.dart +++ b/lib/svg/parser.dart @@ -31,11 +31,11 @@ const _commands = { final _commandPattern = RegExp("(?=[${_commands.join('')}])"); final _floatPattern = RegExp(r"^[-+]?[0-9]*\.?[0-9]+(?:[eE][-+]?[0-9]+)?"); -class _ParserResult { +class ParserResult { final T value; final String remaining; - const _ParserResult({required this.value, required this.remaining}); + const ParserResult({required this.value, required this.remaining}); } class InvalidPathError implements Exception { @@ -73,7 +73,7 @@ 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'."); @@ -86,10 +86,10 @@ _ParserResult _parseNumber(String stringToParse) { stringToParse.substring(0, start) + stringToParse.substring(end); stringToParse = _stripArray(stringToParse); - return _ParserResult(value: number, remaining: 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'."); @@ -97,18 +97,18 @@ _ParserResult _parseUnsignedNumber(String stringToParse) { return number; } -_ParserResult _parseCoordinatePair(String stringToParse) { +ParserResult _parseCoordinatePair(String stringToParse) { final x = _parseNumber(stringToParse); final y = _parseNumber(x.remaining); - return _ParserResult(value: Point(x.value, y.value), remaining: y.remaining); + return ParserResult(value: Point(x.value, y.value), remaining: y.remaining); } -_ParserResult _parseflag(String stringToParse) { +ParserResult _parseflag(String stringToParse) { final flag = stringToParse[0]; stringToParse = stringToParse.substring(1); stringToParse = _stripArray(stringToParse); - if (flag == '0') return _ParserResult(value: false, remaining: stringToParse); - if (flag == '1') return _ParserResult(value: true, remaining: stringToParse); + if (flag == '0') return ParserResult(value: false, remaining: stringToParse); + if (flag == '1') return ParserResult(value: true, remaining: stringToParse); throw InvalidPathError("Expected either 1 or 0, got '$flag'"); } @@ -120,19 +120,26 @@ const fieldParsers = { "f": _parseflag, }; -class _Command { +class Command { final String command; final String args; - const _Command({required this.command, required this.args}); + const Command({required this.command, required this.args}); @override - String toString() => 'Command: $command $args'; + bool operator ==(Object other) => + other is Command && command == other.command && args == other.args; + + @override + int get hashCode => command.hashCode ^ args.hashCode; + + @override + String toString() => '$command $args'; } // Splits path into commands and arguments -List<_Command> _commandifyPath(String pathdef) { - List<_Command> tokens = []; +List commandifyPath(String pathdef) { + List tokens = []; List token = []; for (String c in pathdef.split(_commandPattern)) { String x = c[0]; @@ -142,7 +149,7 @@ List<_Command> _commandifyPath(String pathdef) { throw InvalidPathError("Path does not start with a command: $pathdef"); } if (token.isNotEmpty) { - tokens.add(_Command(command: token[0], args: token[1])); + tokens.add(Command(command: token[0], args: token[1])); // yield token; } if (x == "z" || x == "Z") { @@ -156,7 +163,7 @@ List<_Command> _commandifyPath(String pathdef) { token.add(y); } } - tokens.add(_Command(command: token[0], args: token[1])); + tokens.add(Command(command: token[0], args: token[1])); // yield token; return tokens; } @@ -168,12 +175,23 @@ class Token { const Token({required this.command, required this.args}); @override - String toString() => 'Token: $command ($args)'; + bool operator ==(Object other) => + other is Token && + command == other.command && + args.length == other.args.length && + ![for (int i = 0; i < args.length; i++) args[i] == other.args[i]] + .any((b) => !b); + + @override + int get hashCode => command.hashCode ^ args.hashCode; + + @override + String toString() => '$command ($args)'; } -List _tokenizePath(String pathdef) { +List tokenizePath(String pathdef) { List tokens = []; - for (final token in _commandifyPath(pathdef)) { + for (final token in commandifyPath(pathdef)) { String command = token.command; String args = token.args; @@ -218,7 +236,7 @@ Path parsePath(String pathdef) { String? lastCommand; Point currentPos = Point.zero; - for (final token in _tokenizePath(pathdef)) { + for (final token in tokenizePath(pathdef)) { final command = token.command.toUpperCase(); final absolute = token.command.toUpperCase() == token.command; if (command == "M") { diff --git a/test/svg/svg_test.dart b/test/svg/generation_test.dart similarity index 95% rename from test/svg/svg_test.dart rename to test/svg/generation_test.dart index 4bd8520..525d6e0 100644 --- a/test/svg/svg_test.dart +++ b/test/svg/generation_test.dart @@ -2,7 +2,7 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:kanimaji/svg/parser.dart'; void main() { - test('Test SVG Paths', () { + test('Test generating SVG path strings', () { 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", diff --git a/test/svg/parser_test.dart b/test/svg/parser_test.dart index bd8c922..b1b527c 100644 --- a/test/svg/parser_test.dart +++ b/test/svg/parser_test.dart @@ -4,353 +4,416 @@ 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)), - ]), + group("Examples from the SVG spec", () { + test( + "[Path 1]: MLLz", + () => expect( + parsePath("M 100 100 L 300 100 L 200 300 z"), + Path.fromSegments(const [ + 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)), + ]), + )); + + // + test( + "[Path 2]: MLMLLz (Z command behavior when there is multiple subpaths)", + () => expect( + parsePath("M 0 0 L 50 20 M 100 100 L 300 100 L 200 300 z"), + 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)), + ]), + ), ); - // 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)), - ]), + test( + "[Path 3]: ML", + () => expect( + parsePath("M 100 100 L 200 200"), + parsePath("M100 100L200 200"), + ), ); - 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), - ), - ]), + test( + "[Path 4]: MLL", + () => expect( + parsePath("M 100 200 L 200 100 L -100 -200"), + parsePath("M 100 200 L 200 100 -100 -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), - ), - ]), + test( + "[Path 5]: MCS", + () => expect( + parsePath("""M100,200 C100,100 250,100 250,200 + S400,300 400,200"""), + 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,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), - ), - ]), + test( + "[Path 6]: MC", + () => expect( + parsePath("M100,200 C100,100 400,100 400,200"), + 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,800 C175,700 325,700 400,800"); - expect( - path1, - Path.fromSegments(const [ - Move(to: Point(100, 800)), - CubicBezier( - start: Point(100, 800), - control1: Point(175, 700), - control2: Point(325, 700), - end: Point(400, 800), - ), - ]), + test( + "[Path 7]: MC", + () => expect( + parsePath("M100,500 C25,400 475,400 400,500"), + 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 = parsePath("M600,200 C675,100 975,100 900,200"); - expect( - path1, - Path.fromSegments(const [ - Move(to: Point(600, 200)), - CubicBezier( - start: Point(600, 200), - control1: Point(675, 100), - control2: Point(975, 100), - end: Point(900, 200), - ), - ]), + test( + "[Path 8]: MC", + () => expect( + parsePath("M100,800 C175,700 325,700 400,800"), + Path.fromSegments(const [ + Move(to: Point(100, 800)), + CubicBezier( + start: Point(100, 800), + control1: Point(175, 700), + control2: Point(325, 700), + end: Point(400, 800), + ), + ]), + ), ); - path1 = parsePath("M600,500 C600,350 900,650 900,500"); - expect( - path1, - Path.fromSegments(const [ - Move(to: Point(600, 500)), - CubicBezier( - start: Point(600, 500), - control1: Point(600, 350), - control2: Point(900, 650), - end: Point(900, 500), - ), - ]), + test( + "[Path 9]: MC", + () => expect( + parsePath("M600,200 C675,100 975,100 900,200"), + Path.fromSegments(const [ + Move(to: Point(600, 200)), + CubicBezier( + start: Point(600, 200), + control1: Point(675, 100), + control2: Point(975, 100), + end: Point(900, 200), + ), + ]), + ), ); - path1 = parsePath("""M600,800 C625,700 725,700 750,800 - S875,900 900,800"""); - expect( - path1, - Path.fromSegments(const [ - Move(to: Point(600, 800)), - CubicBezier( - start: Point(600, 800), - control1: Point(625, 700), - control2: Point(725, 700), - end: Point(750, 800)), - CubicBezier( - start: Point(750, 800), - control1: Point(775, 900), - control2: Point(875, 900), - end: Point(900, 800), - ), - ]), + test( + "[Path 10]: MC", + () => expect( + parsePath("M600,500 C600,350 900,650 900,500"), + Path.fromSegments(const [ + Move(to: Point(600, 500)), + CubicBezier( + start: Point(600, 500), + control1: Point(600, 350), + control2: Point(900, 650), + end: Point(900, 500), + ), + ]), + ), ); - path1 = parsePath("M200,300 Q400,50 600,300 T1000,300"); - expect( - path1, - Path.fromSegments(const [ - Move(to: Point(200, 300)), - QuadraticBezier( - start: Point(200, 300), - control: Point(400, 50), - end: Point(600, 300), - ), - QuadraticBezier( - start: Point(600, 300), - control: Point(800, 550), - end: Point(1000, 300), - ), - ]), + test( + "[Path 11]: MCS", + () => expect( + parsePath("""M600,800 C625,700 725,700 750,800 + S875,900 900,800"""), + Path.fromSegments(const [ + Move(to: Point(600, 800)), + CubicBezier( + start: Point(600, 800), + control1: Point(625, 700), + control2: Point(725, 700), + end: Point(750, 800)), + CubicBezier( + start: Point(750, 800), + control1: Point(775, 900), + control2: Point(875, 900), + end: Point(900, 800), + ), + ]), + ), ); - path1 = parsePath("M300,200 h-150 a150,150 0 1,0 150,-150 z"); - expect( - path1, - Path.fromSegments(const [ - Move(to: Point(300, 200)), - Line(start: Point(300, 200), end: Point(150, 200)), - Arc( - start: Point(150, 200), - radius: Point(150, 150), - rotation: 0, - arc: true, - sweep: false, - end: Point(300, 50), - ), - Close(start: Point(300, 50), end: Point(300, 200)), - ]), + test( + "[Path 12]: MQT", + () => expect( + parsePath("M200,300 Q400,50 600,300 T1000,300"), + Path.fromSegments(const [ + Move(to: Point(200, 300)), + QuadraticBezier( + start: Point(200, 300), + control: Point(400, 50), + end: Point(600, 300), + ), + QuadraticBezier( + start: Point(600, 300), + control: Point(800, 550), + end: Point(1000, 300), + ), + ]), + ), ); - path1 = parsePath("M275,175 v-150 a150,150 0 0,0 -150,150 z"); - expect( - path1, - Path.fromSegments(const [ - Move(to: Point(275, 175)), - Line(start: Point(275, 175), end: Point(275, 25)), - Arc( - start: Point(275, 25), - radius: Point(150, 150), - rotation: 0, - arc: false, - sweep: false, - end: Point(125, 175), - ), - Close(start: Point(125, 175), end: Point(275, 175)), - ]), + test( + "[Path 13]: Mhaz", + () => expect( + parsePath("M300,200 h-150 a150,150 0 1,0 150,-150 z"), + Path.fromSegments(const [ + Move(to: Point(300, 200)), + Line(start: Point(300, 200), end: Point(150, 200)), + Arc( + start: Point(150, 200), + radius: Point(150, 150), + rotation: 0, + arc: true, + sweep: false, + end: Point(300, 50), + ), + Close(start: Point(300, 50), end: Point(300, 200)), + ]), + ), ); - path1 = parsePath("M275,175 v-150 a150,150 0 0,0 -150,150 L 275,175 z"); - expect( - path1, - Path.fromSegments(const [ - Move(to: Point(275, 175)), - Line(start: Point(275, 175), end: Point(275, 25)), - Arc( - start: Point(275, 25), - radius: Point(150, 150), - rotation: 0, - arc: false, - sweep: false, - end: Point(125, 175), - ), - Line(start: Point(125, 175), end: Point(275, 175)), - Close(start: Point(275, 175), end: Point(275, 175)), - ]), + test( + "[Path 14]: Mvaz", + () => expect( + parsePath("M275,175 v-150 a150,150 0 0,0 -150,150 z"), + Path.fromSegments(const [ + Move(to: Point(275, 175)), + Line(start: Point(275, 175), end: Point(275, 25)), + Arc( + start: Point(275, 25), + radius: Point(150, 150), + rotation: 0, + arc: false, + sweep: false, + end: Point(125, 175), + ), + Close(start: Point(125, 175), end: Point(275, 175)), + ]), + ), ); - path1 = parsePath("""M600,350 l 50,-25 + test( + "[Path 15]: MvaLz", + () => expect( + parsePath("M275,175 v-150 a150,150 0 0,0 -150,150 L 275,175 z"), + Path.fromSegments(const [ + Move(to: Point(275, 175)), + Line(start: Point(275, 175), end: Point(275, 25)), + Arc( + start: Point(275, 25), + radius: Point(150, 150), + rotation: 0, + arc: false, + sweep: false, + end: Point(125, 175), + ), + Line(start: Point(125, 175), end: Point(275, 175)), + Close(start: Point(275, 175), end: Point(275, 175)), + ]), + ), + ); + + test( + "[Path 16]: Mlalalalal", + () => expect( + parsePath("""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"""); - expect( - path1, - Path.fromSegments(const [ - Move(to: Point(600, 350)), - Line(start: Point(600, 350), end: Point(650, 325)), - Arc( - start: Point(650, 325), - radius: Point(25, 25), - rotation: -30, - arc: false, - sweep: true, - end: Point(700, 300), - ), - Line(start: Point(700, 300), end: Point(750, 275)), - Arc( - start: Point(750, 275), - radius: Point(25, 50), - rotation: -30, - arc: false, - sweep: true, - end: Point(800, 250), - ), - Line(start: Point(800, 250), end: Point(850, 225)), - Arc( - start: Point(850, 225), - radius: Point(25, 75), - rotation: -30, - arc: false, - sweep: true, - end: Point(900, 200), - ), - Line(start: Point(900, 200), end: Point(950, 175)), - Arc( - start: Point(950, 175), - radius: Point(25, 100), - rotation: -30, - arc: false, - sweep: true, - end: Point(1000, 150), - ), - Line(start: Point(1000, 150), end: Point(1050, 125)), - ]), + a25,100 -30 0,1 50,-25 l 50,-25"""), + Path.fromSegments(const [ + Move(to: Point(600, 350)), + Line(start: Point(600, 350), end: Point(650, 325)), + Arc( + start: Point(650, 325), + radius: Point(25, 25), + rotation: -30, + arc: false, + sweep: true, + end: Point(700, 300), + ), + Line(start: Point(700, 300), end: Point(750, 275)), + Arc( + start: Point(750, 275), + radius: Point(25, 50), + rotation: -30, + arc: false, + sweep: true, + end: Point(800, 250), + ), + Line(start: Point(800, 250), end: Point(850, 225)), + Arc( + start: Point(850, 225), + radius: Point(25, 75), + rotation: -30, + arc: false, + sweep: true, + end: Point(900, 200), + ), + Line(start: Point(900, 200), end: Point(950, 175)), + Arc( + start: Point(950, 175), + radius: Point(25, 100), + rotation: -30, + arc: false, + sweep: true, + end: Point(1000, 150), + ), + Line(start: Point(1000, 150), end: Point(1050, 125)), + ]), + ), ); }); - // def test_others(self): - // # Other paths that need testing: + group("Other paths that need testing", () { + test( + "Relative moveto", + () => expect( + parsePath("M 0 0 L 50 20 m 50 80 L 300 100 L 200 300 z"), + Path.fromSegments(const [ + Move(to: Point.zero), + Line(start: Point(0, 0), 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)), + ]), + ), + ); - // # Relative moveto: - // path1 = parsePath("M 0 0 L 50 20 m 50 80 L 300 100 L 200 300 z") - // expect( - // path1, - // Path.fromSegments(const [ - // Move(0j), - // Line(start: Point(0, 0), 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(Point(200, 300), Point(100, 100)), - // ), - // ) + // + test( + "Initial smooth and relative CubicBezier", + () => expect( + parsePath("""M100,200 s 150,-100 150,0"""), + Path.fromSegments(const [ + Move(to: Point(100, 200)), + CubicBezier( + start: Point(100, 200), + control1: Point(100, 200), + control2: Point(250, 100), + end: Point(250, 200), + ), + ]), + ), + ); - // # Initial smooth and relative CubicBezier - // path1 = parsePath("""M100,200 s 150,-100 150,0""") - // expect( - // path1, - // Path.fromSegments(const [ - // Move(to: Point(100, 200)), - // CubicBezier(start: Point(100, 200), control1: Point(100, 200), control2: Point(250, 100), end: Point(250, 200),), - // ), - // ) + test( + "Initial smooth and relative QuadraticBezier", + () => expect( + parsePath("""M100,200 t 150,0"""), + Path.fromSegments(const [ + Move(to: Point(100, 200)), + QuadraticBezier( + start: Point(100, 200), + control: Point(100, 200), + end: Point(250, 200)), + ]), + ), + ); - // # Initial smooth and relative QuadraticBezier - // path1 = parsePath("""M100,200 t 150,0""") - // expect( - // path1, - // Path.fromSegments(const [Move(Point(100, 200)), QuadraticBezier(start: Point(100, 200), control: Point(100, 200), end: Point(250, 200)),), - // ) + test( + "Relative QuadraticBezier", + () => expect( + parsePath("""M100,200 q 0,0 150,0"""), + Path.fromSegments(const [ + Move(to: Point(100, 200)), + QuadraticBezier( + start: Point(100, 200), + control: Point(100, 200), + end: Point(250, 200)), + ]), + ), + ); + }); - // # Relative QuadraticBezier - // path1 = parsePath("""M100,200 q 0,0 150,0""") - // expect( - // path1, - // Path.fromSegments(const [Move(Point(100, 200)), QuadraticBezier(start: Point(100, 200), control: Point(100, 200), end: Point(250, 200)),), - // ) + test( + "You don't need spaces before a minus-sign", + () => expect( + parsePath("M100,200c10-5,20-10,30-20"), + parsePath("M 100 200 c 10 -5 20 -10 30 -20"), + ), + ); - // def test_negative(self): - // """You don't need spaces before a minus-sign""" - // path1 = parsePath("M100,200c10-5,20-10,30-20") - // path2 = parsePath("M 100 200 c 10 -5 20 -10 30 -20") - // expect(path1, path2) + test( + '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. + expect( + parsePath("M-3.4e38 3.4E+38L-3.4E-38,3.4e-38"), + Path.fromSegments(const [ + Move(to: Point(-3.4e38, 3.4e38)), + Line(start: Point(-3.4e38, 3.4e38), end: Point(-3.4e-38, 3.4e-38)) + ]), + ), + ); - // 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 = parsePath("M-3.4e38 3.4E+38L-3.4E-38,3.4e-38") - // path2 = Path.fromSegments(const [ - // Move(-3.4e38 + 3.4e38j), Line(-3.4e38 + 3.4e38j, -3.4e-38 + 3.4e-38j) - // ) - // expect(path1, path2) + test( + 'Errors', + () => expect( + parsePath("M 100 100 L 200 200 Z 100 200"), + throwsA(const TypeMatcher()), + ), + ); - // def test_errors(self): - // self.assertRaises(ValueError, parsePath, "M 100 100 L 200 200 Z 100 200") + test( + "Nonpath: It's possible in SVG to create paths that has zero length, " + 'we need to handle that.', + () => expect(parsePath("M10.236,100.184").d(), "M 10.236,100.184"), + ); - // def test_non_path(self): - // # It's possible in SVG to create paths that has zero length, - // # we need to handle that. - - // path = parsePath("M10.236,100.184") - // expect(path.d(), "M 10.236,100.184") - - // def test_issue_45(self): - // path = parsePath( - // "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()) + test('svg.path library, issue 45', () { + final path = parsePath("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"); + expect(path.d(), contains("A 14.5445,14.5445 0 0,0 1672.2372,-54.8161 Z")); + }); } diff --git a/test/svg/path_test.dart b/test/svg/path_test.dart new file mode 100644 index 0000000..cb56a23 --- /dev/null +++ b/test/svg/path_test.dart @@ -0,0 +1,676 @@ +// import unittest +import 'dart:math' show sqrt, pi; + +import 'package:flutter_test/flutter_test.dart'; +import 'package:kanimaji/common/point.dart'; +import 'package:kanimaji/svg/path.dart'; + + +// from ..path import CubicBezier, QuadraticBezier, Line, Arc, Move, Close, Path + +// Most of these test points are not calculated serparately, as that would +// take too long and be too error prone. Instead the curves have been verified +// to be correct visually, by drawing them with the turtle module, with code +// like this: +// +// import turtle +// t = turtle.Turtle() +// t.penup() +// +// for arc in (path1, path2): +// p = arc.point(0) +// t.goto(p.real - 500, -p.imag + 300) +// t.dot(3, 'black') +// t.pendown() +// for x in range(1, 101): +// p = arc.point(x * 0.01) +// t.goto(p.real - 500, -p.imag + 300) +// t.penup() +// t.dot(3, 'black') +// +// raw_input() +// +// After the paths have been verified to be correct this way, the testing of +// points along the paths has been added as regression tests, to make sure +// nobody changes the way curves are drawn by mistake. Therefore, do not take +// these points religiously. They might be subtly wrong, unless otherwise +// noted. + +// class MoreOrLessEqualsToPoint extends CustomMatcher { +// MoreOrLessEqualsToPoint(matcher) : super("Widget with price that is", "price", matcher); +// featureValueOf(actual) => (actual as Point).x; +// } + +class MoreOrLessEqualsToPoint extends Matcher { + const MoreOrLessEqualsToPoint(); + + @override + bool matches(covariant Finder finder, Map matchState) + => finder is Point && finder.x + + @override + Description describe(Description description) => description.add('in card'); +} + +void main() { + group('Line tests', () { + +test('Test lines', () { + + // These points are calculated, and not just regression tests. + const line1 = Line(start:Point.zero, end:Point(400, 0)); + expect(line1.point(0), moreOrLessEquals(0)); + // expect(line1.point(0.3), moreOrLessEquals(const Point(120, 0))); + // expect(line1.point(0.5), moreOrLessEquals(const Point(200, 0))); + // expect(line1.point(0.9), moreOrLessEquals(const Point(360, 0))); + // expect(line1.point(1), moreOrLessEquals(const Point(400, 0))); + expect(line1.size(), moreOrLessEquals(400)); + + const line2 = Line(start: Point(400, 0), end:Point(400, 300)); + // expect(line2.point(0), (400 + 0j)) + // expect(line2.point(0.3), (400 + 90j)) + // expect(line2.point(0.5), (400 + 150j)) + // expect(line2.point(0.9), (400 + 270j)) + // expect(line2.point(1), (400 + 300j)) + expect(line2.size(), moreOrLessEquals(300)); + + const line3 = Line(start:Point(400, 300), end:Point.zero); + // expect(line3.point(0), (400 + 300j)) + // expect(line3.point(0.3), (280 + 210j)) + // expect(line3.point(0.5), (200 + 150j)) + // expect(line3.point(0.9), (40 + 30j)) + // expect(line3.point(1), (0j)) + expect(line3.size(), moreOrLessEquals(500)); +}); + + def test_equality(self): + // This is to test the __eq__ and __ne__ methods, so we can't use + // assertEqual and assertNotEqual + line = Line(0j, 400 + 0j) + self.assertTrue(line == Line(0, 400)) + self.assertTrue(line != Line(100, 400)) + self.assertFalse(line == str(line)) + self.assertTrue(line != str(line)) + self.assertFalse( + CubicBezier(600 + 500j, 600 + 350j, 900 + 650j, 900 + 500j) == line + ) + + }); + + + +// class CubicBezierTest(unittest.TestCase): +// def test_approx_circle(self): +// """This is a approximate circle drawn in Inkscape""" + +// arc1 = CubicBezier( +// complex(0, 0), +// complex(0, 109.66797), +// complex(-88.90345, 198.57142), +// complex(-198.57142, 198.57142), +// ) + +// self.assertAlmostEqual(arc1.point(0), (0j)) +// self.assertAlmostEqual(arc1.point(0.1), (-2.59896457 + 32.20931647j)) +// self.assertAlmostEqual(arc1.point(0.2), (-10.12330256 + 62.76392816j)) +// self.assertAlmostEqual(arc1.point(0.3), (-22.16418039 + 91.25500149j)) +// self.assertAlmostEqual(arc1.point(0.4), (-38.31276448 + 117.27370288j)) +// self.assertAlmostEqual(arc1.point(0.5), (-58.16022125 + 140.41119875j)) +// self.assertAlmostEqual(arc1.point(0.6), (-81.29771712 + 160.25865552j)) +// self.assertAlmostEqual(arc1.point(0.7), (-107.31641851 + 176.40723961j)) +// self.assertAlmostEqual(arc1.point(0.8), (-135.80749184 + 188.44811744j)) +// self.assertAlmostEqual(arc1.point(0.9), (-166.36210353 + 195.97245543j)) +// self.assertAlmostEqual(arc1.point(1), (-198.57142 + 198.57142j)) + +// arc2 = CubicBezier( +// complex(-198.57142, 198.57142), +// complex(-109.66797 - 198.57142, 0 + 198.57142), +// complex(-198.57143 - 198.57142, -88.90345 + 198.57142), +// complex(-198.57143 - 198.57142, 0), +// ) + +// self.assertAlmostEqual(arc2.point(0), (-198.57142 + 198.57142j)) +// self.assertAlmostEqual(arc2.point(0.1), (-230.78073675 + 195.97245543j)) +// self.assertAlmostEqual(arc2.point(0.2), (-261.3353492 + 188.44811744j)) +// self.assertAlmostEqual(arc2.point(0.3), (-289.82642365 + 176.40723961j)) +// self.assertAlmostEqual(arc2.point(0.4), (-315.8451264 + 160.25865552j)) +// self.assertAlmostEqual(arc2.point(0.5), (-338.98262375 + 140.41119875j)) +// self.assertAlmostEqual(arc2.point(0.6), (-358.830082 + 117.27370288j)) +// self.assertAlmostEqual(arc2.point(0.7), (-374.97866745 + 91.25500149j)) +// self.assertAlmostEqual(arc2.point(0.8), (-387.0195464 + 62.76392816j)) +// self.assertAlmostEqual(arc2.point(0.9), (-394.54388515 + 32.20931647j)) +// self.assertAlmostEqual(arc2.point(1), (-397.14285 + 0j)) + +// arc3 = CubicBezier( +// complex(-198.57143 - 198.57142, 0), +// complex(0 - 198.57143 - 198.57142, -109.66797), +// complex(88.90346 - 198.57143 - 198.57142, -198.57143), +// complex(-198.57142, -198.57143), +// ) + +// self.assertAlmostEqual(arc3.point(0), (-397.14285 + 0j)) +// self.assertAlmostEqual(arc3.point(0.1), (-394.54388515 - 32.20931675j)) +// self.assertAlmostEqual(arc3.point(0.2), (-387.0195464 - 62.7639292j)) +// self.assertAlmostEqual(arc3.point(0.3), (-374.97866745 - 91.25500365j)) +// self.assertAlmostEqual(arc3.point(0.4), (-358.830082 - 117.2737064j)) +// self.assertAlmostEqual(arc3.point(0.5), (-338.98262375 - 140.41120375j)) +// self.assertAlmostEqual(arc3.point(0.6), (-315.8451264 - 160.258662j)) +// self.assertAlmostEqual(arc3.point(0.7), (-289.82642365 - 176.40724745j)) +// self.assertAlmostEqual(arc3.point(0.8), (-261.3353492 - 188.4481264j)) +// self.assertAlmostEqual(arc3.point(0.9), (-230.78073675 - 195.97246515j)) +// self.assertAlmostEqual(arc3.point(1), (-198.57142 - 198.57143j)) + +// arc4 = CubicBezier( +// complex(-198.57142, -198.57143), +// complex(109.66797 - 198.57142, 0 - 198.57143), +// complex(0, 88.90346 - 198.57143), +// complex(0, 0), +// ) + +// self.assertAlmostEqual(arc4.point(0), (-198.57142 - 198.57143j)) +// self.assertAlmostEqual(arc4.point(0.1), (-166.36210353 - 195.97246515j)) +// self.assertAlmostEqual(arc4.point(0.2), (-135.80749184 - 188.4481264j)) +// self.assertAlmostEqual(arc4.point(0.3), (-107.31641851 - 176.40724745j)) +// self.assertAlmostEqual(arc4.point(0.4), (-81.29771712 - 160.258662j)) +// self.assertAlmostEqual(arc4.point(0.5), (-58.16022125 - 140.41120375j)) +// self.assertAlmostEqual(arc4.point(0.6), (-38.31276448 - 117.2737064j)) +// self.assertAlmostEqual(arc4.point(0.7), (-22.16418039 - 91.25500365j)) +// self.assertAlmostEqual(arc4.point(0.8), (-10.12330256 - 62.7639292j)) +// self.assertAlmostEqual(arc4.point(0.9), (-2.59896457 - 32.20931675j)) +// self.assertAlmostEqual(arc4.point(1), (0j)) + +// def test_svg_examples(self): + +// # M100,200 C100,100 250,100 250,200 +// path1 = CubicBezier(100 + 200j, 100 + 100j, 250 + 100j, 250 + 200j) +// self.assertAlmostEqual(path1.point(0), (100 + 200j)) +// self.assertAlmostEqual(path1.point(0.3), (132.4 + 137j)) +// self.assertAlmostEqual(path1.point(0.5), (175 + 125j)) +// self.assertAlmostEqual(path1.point(0.9), (245.8 + 173j)) +// self.assertAlmostEqual(path1.point(1), (250 + 200j)) + +// # S400,300 400,200 +// path2 = CubicBezier(250 + 200j, 250 + 300j, 400 + 300j, 400 + 200j) +// self.assertAlmostEqual(path2.point(0), (250 + 200j)) +// self.assertAlmostEqual(path2.point(0.3), (282.4 + 263j)) +// self.assertAlmostEqual(path2.point(0.5), (325 + 275j)) +// self.assertAlmostEqual(path2.point(0.9), (395.8 + 227j)) +// self.assertAlmostEqual(path2.point(1), (400 + 200j)) + +// # M100,200 C100,100 400,100 400,200 +// path3 = CubicBezier(100 + 200j, 100 + 100j, 400 + 100j, 400 + 200j) +// self.assertAlmostEqual(path3.point(0), (100 + 200j)) +// self.assertAlmostEqual(path3.point(0.3), (164.8 + 137j)) +// self.assertAlmostEqual(path3.point(0.5), (250 + 125j)) +// self.assertAlmostEqual(path3.point(0.9), (391.6 + 173j)) +// self.assertAlmostEqual(path3.point(1), (400 + 200j)) + +// # M100,500 C25,400 475,400 400,500 +// path4 = CubicBezier(100 + 500j, 25 + 400j, 475 + 400j, 400 + 500j) +// self.assertAlmostEqual(path4.point(0), (100 + 500j)) +// self.assertAlmostEqual(path4.point(0.3), (145.9 + 437j)) +// self.assertAlmostEqual(path4.point(0.5), (250 + 425j)) +// self.assertAlmostEqual(path4.point(0.9), (407.8 + 473j)) +// self.assertAlmostEqual(path4.point(1), (400 + 500j)) + +// # M100,800 C175,700 325,700 400,800 +// path5 = CubicBezier(100 + 800j, 175 + 700j, 325 + 700j, 400 + 800j) +// self.assertAlmostEqual(path5.point(0), (100 + 800j)) +// self.assertAlmostEqual(path5.point(0.3), (183.7 + 737j)) +// self.assertAlmostEqual(path5.point(0.5), (250 + 725j)) +// self.assertAlmostEqual(path5.point(0.9), (375.4 + 773j)) +// self.assertAlmostEqual(path5.point(1), (400 + 800j)) + +// # M600,200 C675,100 975,100 900,200 +// path6 = CubicBezier(600 + 200j, 675 + 100j, 975 + 100j, 900 + 200j) +// self.assertAlmostEqual(path6.point(0), (600 + 200j)) +// self.assertAlmostEqual(path6.point(0.3), (712.05 + 137j)) +// self.assertAlmostEqual(path6.point(0.5), (806.25 + 125j)) +// self.assertAlmostEqual(path6.point(0.9), (911.85 + 173j)) +// self.assertAlmostEqual(path6.point(1), (900 + 200j)) + +// # M600,500 C600,350 900,650 900,500 +// path7 = CubicBezier(600 + 500j, 600 + 350j, 900 + 650j, 900 + 500j) +// self.assertAlmostEqual(path7.point(0), (600 + 500j)) +// self.assertAlmostEqual(path7.point(0.3), (664.8 + 462.2j)) +// self.assertAlmostEqual(path7.point(0.5), (750 + 500j)) +// self.assertAlmostEqual(path7.point(0.9), (891.6 + 532.4j)) +// self.assertAlmostEqual(path7.point(1), (900 + 500j)) + +// # M600,800 C625,700 725,700 750,800 +// path8 = CubicBezier(600 + 800j, 625 + 700j, 725 + 700j, 750 + 800j) +// self.assertAlmostEqual(path8.point(0), (600 + 800j)) +// self.assertAlmostEqual(path8.point(0.3), (638.7 + 737j)) +// self.assertAlmostEqual(path8.point(0.5), (675 + 725j)) +// self.assertAlmostEqual(path8.point(0.9), (740.4 + 773j)) +// self.assertAlmostEqual(path8.point(1), (750 + 800j)) + +// # S875,900 900,800 +// inversion = (750 + 800j) + (750 + 800j) - (725 + 700j) +// path9 = CubicBezier(750 + 800j, inversion, 875 + 900j, 900 + 800j) +// self.assertAlmostEqual(path9.point(0), (750 + 800j)) +// self.assertAlmostEqual(path9.point(0.3), (788.7 + 863j)) +// self.assertAlmostEqual(path9.point(0.5), (825 + 875j)) +// self.assertAlmostEqual(path9.point(0.9), (890.4 + 827j)) +// self.assertAlmostEqual(path9.point(1), (900 + 800j)) + +// def test_length(self): + +// # A straight line: +// arc = CubicBezier( +// complex(0, 0), complex(0, 0), complex(0, 100), complex(0, 100) +// ) + +// self.assertAlmostEqual(arc.length(), 100) + +// # A diagonal line: +// arc = CubicBezier( +// complex(0, 0), complex(0, 0), complex(100, 100), complex(100, 100) +// ) + +// self.assertAlmostEqual(arc.length(), sqrt(2 * 100 * 100)) + +// # A quarter circle arc with radius 100: +// kappa = ( +// 4 * (sqrt(2) - 1) / 3 +// ) # http://www.whizkidtech.redprince.net/bezier/circle/ + +// arc = CubicBezier( +// complex(0, 0), +// complex(0, kappa * 100), +// complex(100 - kappa * 100, 100), +// complex(100, 100), +// ) + +// # We can't compare with pi*50 here, because this is just an +// # approximation of a circle arc. pi*50 is 157.079632679 +// # So this is just yet another "warn if this changes" test. +// # This value is not verified to be correct. +// self.assertAlmostEqual(arc.length(), 157.1016698) + +// # A recursive solution has also been suggested, but for CubicBezier +// # curves it could get a false solution on curves where the midpoint is on a +// # straight line between the start and end. For example, the following +// # curve would get solved as a straight line and get the length 300. +// # Make sure this is not the case. +// arc = CubicBezier( +// complex(600, 500), complex(600, 350), complex(900, 650), complex(900, 500) +// ) +// self.assertTrue(arc.length() > 300.0) + +// def test_equality(self): +// # This is to test the __eq__ and __ne__ methods, so we can't use +// # assertEqual and assertNotEqual +// segment = CubicBezier( +// complex(600, 500), complex(600, 350), complex(900, 650), complex(900, 500) +// ) + +// self.assertTrue( +// segment == CubicBezier(600 + 500j, 600 + 350j, 900 + 650j, 900 + 500j) +// ) +// self.assertTrue( +// segment != CubicBezier(600 + 501j, 600 + 350j, 900 + 650j, 900 + 500j) +// ) +// self.assertTrue(segment != Line(0, 400)) + + +// class QuadraticBezierTest(unittest.TestCase): +// def test_svg_examples(self): +// """These is the path in the SVG specs""" +// # M200,300 Q400,50 600,300 T1000,300 +// path1 = QuadraticBezier(200 + 300j, 400 + 50j, 600 + 300j) +// self.assertAlmostEqual(path1.point(0), (200 + 300j)) +// self.assertAlmostEqual(path1.point(0.3), (320 + 195j)) +// self.assertAlmostEqual(path1.point(0.5), (400 + 175j)) +// self.assertAlmostEqual(path1.point(0.9), (560 + 255j)) +// self.assertAlmostEqual(path1.point(1), (600 + 300j)) + +// # T1000, 300 +// inversion = (600 + 300j) + (600 + 300j) - (400 + 50j) +// path2 = QuadraticBezier(600 + 300j, inversion, 1000 + 300j) +// self.assertAlmostEqual(path2.point(0), (600 + 300j)) +// self.assertAlmostEqual(path2.point(0.3), (720 + 405j)) +// self.assertAlmostEqual(path2.point(0.5), (800 + 425j)) +// self.assertAlmostEqual(path2.point(0.9), (960 + 345j)) +// self.assertAlmostEqual(path2.point(1), (1000 + 300j)) + +// def test_length(self): +// # expected results calculated with +// # svg.path.segment_length(q, 0, 1, q.start, q.end, 1e-14, 20, 0) +// q1 = QuadraticBezier(200 + 300j, 400 + 50j, 600 + 300j) +// q2 = QuadraticBezier(200 + 300j, 400 + 50j, 500 + 200j) +// closedq = QuadraticBezier(6 + 2j, 5 - 1j, 6 + 2j) +// linq1 = QuadraticBezier(1, 2, 3) +// linq2 = QuadraticBezier(1 + 3j, 2 + 5j, -9 - 17j) +// nodalq = QuadraticBezier(1, 1, 1) +// tests = [ +// (q1, 487.77109389525975), +// (q2, 379.90458193489155), +// (closedq, 3.1622776601683795), +// (linq1, 2), +// (linq2, 22.73335777124786), +// (nodalq, 0), +// ] +// for q, exp_res in tests: +// self.assertAlmostEqual(q.length(), exp_res) + +// def test_equality(self): +// # This is to test the __eq__ and __ne__ methods, so we can't use +// # assertEqual and assertNotEqual +// segment = QuadraticBezier(200 + 300j, 400 + 50j, 600 + 300j) +// self.assertTrue(segment == QuadraticBezier(200 + 300j, 400 + 50j, 600 + 300j)) +// self.assertTrue(segment != QuadraticBezier(200 + 301j, 400 + 50j, 600 + 300j)) +// self.assertFalse(segment == Arc(0j, 100 + 50j, 0, 0, 0, 100 + 50j)) +// self.assertTrue(Arc(0j, 100 + 50j, 0, 0, 0, 100 + 50j) != segment) + + +// class ArcTest(unittest.TestCase): +// def test_points(self): +// arc1 = Arc(0j, 100 + 50j, 0, 0, 0, 100 + 50j) +// self.assertAlmostEqual(arc1.center, 100 + 0j) +// self.assertAlmostEqual(arc1.theta, 180.0) +// self.assertAlmostEqual(arc1.delta, -90.0) + +// self.assertAlmostEqual(arc1.point(0.0), (0j)) +// self.assertAlmostEqual(arc1.point(0.1), (1.23116594049 + 7.82172325201j)) +// self.assertAlmostEqual(arc1.point(0.2), (4.89434837048 + 15.4508497187j)) +// self.assertAlmostEqual(arc1.point(0.3), (10.8993475812 + 22.699524987j)) +// self.assertAlmostEqual(arc1.point(0.4), (19.0983005625 + 29.3892626146j)) +// self.assertAlmostEqual(arc1.point(0.5), (29.2893218813 + 35.3553390593j)) +// self.assertAlmostEqual(arc1.point(0.6), (41.2214747708 + 40.4508497187j)) +// self.assertAlmostEqual(arc1.point(0.7), (54.6009500260 + 44.5503262094j)) +// self.assertAlmostEqual(arc1.point(0.8), (69.0983005625 + 47.5528258148j)) +// self.assertAlmostEqual(arc1.point(0.9), (84.3565534960 + 49.3844170298j)) +// self.assertAlmostEqual(arc1.point(1.0), (100 + 50j)) + +// arc2 = Arc(0j, 100 + 50j, 0, 1, 0, 100 + 50j) +// self.assertAlmostEqual(arc2.center, 50j) +// self.assertAlmostEqual(arc2.theta, 270.0) +// self.assertAlmostEqual(arc2.delta, -270.0) + +// self.assertAlmostEqual(arc2.point(0.0), (0j)) +// self.assertAlmostEqual(arc2.point(0.1), (-45.399049974 + 5.44967379058j)) +// self.assertAlmostEqual(arc2.point(0.2), (-80.9016994375 + 20.6107373854j)) +// self.assertAlmostEqual(arc2.point(0.3), (-98.7688340595 + 42.178276748j)) +// self.assertAlmostEqual(arc2.point(0.4), (-95.1056516295 + 65.4508497187j)) +// self.assertAlmostEqual(arc2.point(0.5), (-70.7106781187 + 85.3553390593j)) +// self.assertAlmostEqual(arc2.point(0.6), (-30.9016994375 + 97.5528258148j)) +// self.assertAlmostEqual(arc2.point(0.7), (15.643446504 + 99.3844170298j)) +// self.assertAlmostEqual(arc2.point(0.8), (58.7785252292 + 90.4508497187j)) +// self.assertAlmostEqual(arc2.point(0.9), (89.1006524188 + 72.699524987j)) +// self.assertAlmostEqual(arc2.point(1.0), (100 + 50j)) + +// arc3 = Arc(0j, 100 + 50j, 0, 0, 1, 100 + 50j) +// self.assertAlmostEqual(arc3.center, 50j) +// self.assertAlmostEqual(arc3.theta, 270.0) +// self.assertAlmostEqual(arc3.delta, 90.0) + +// self.assertAlmostEqual(arc3.point(0.0), (0j)) +// self.assertAlmostEqual(arc3.point(0.1), (15.643446504 + 0.615582970243j)) +// self.assertAlmostEqual(arc3.point(0.2), (30.9016994375 + 2.44717418524j)) +// self.assertAlmostEqual(arc3.point(0.3), (45.399049974 + 5.44967379058j)) +// self.assertAlmostEqual(arc3.point(0.4), (58.7785252292 + 9.54915028125j)) +// self.assertAlmostEqual(arc3.point(0.5), (70.7106781187 + 14.6446609407j)) +// self.assertAlmostEqual(arc3.point(0.6), (80.9016994375 + 20.6107373854j)) +// self.assertAlmostEqual(arc3.point(0.7), (89.1006524188 + 27.300475013j)) +// self.assertAlmostEqual(arc3.point(0.8), (95.1056516295 + 34.5491502813j)) +// self.assertAlmostEqual(arc3.point(0.9), (98.7688340595 + 42.178276748j)) +// self.assertAlmostEqual(arc3.point(1.0), (100 + 50j)) + +// arc4 = Arc(0j, 100 + 50j, 0, 1, 1, 100 + 50j) +// self.assertAlmostEqual(arc4.center, 100 + 0j) +// self.assertAlmostEqual(arc4.theta, 180.0) +// self.assertAlmostEqual(arc4.delta, 270.0) + +// self.assertAlmostEqual(arc4.point(0.0), (0j)) +// self.assertAlmostEqual(arc4.point(0.1), (10.8993475812 - 22.699524987j)) +// self.assertAlmostEqual(arc4.point(0.2), (41.2214747708 - 40.4508497187j)) +// self.assertAlmostEqual(arc4.point(0.3), (84.3565534960 - 49.3844170298j)) +// self.assertAlmostEqual(arc4.point(0.4), (130.901699437 - 47.5528258148j)) +// self.assertAlmostEqual(arc4.point(0.5), (170.710678119 - 35.3553390593j)) +// self.assertAlmostEqual(arc4.point(0.6), (195.105651630 - 15.4508497187j)) +// self.assertAlmostEqual(arc4.point(0.7), (198.768834060 + 7.82172325201j)) +// self.assertAlmostEqual(arc4.point(0.8), (180.901699437 + 29.3892626146j)) +// self.assertAlmostEqual(arc4.point(0.9), (145.399049974 + 44.5503262094j)) +// self.assertAlmostEqual(arc4.point(1.0), (100 + 50j)) + +// def test_length(self): +// # I'll test the length calculations by making a circle, in two parts. +// arc1 = Arc(0j, 100 + 100j, 0, 0, 0, 200 + 0j) +// arc2 = Arc(200 + 0j, 100 + 100j, 0, 0, 0, 0j) +// self.assertAlmostEqual(arc1.length(), pi * 100) +// self.assertAlmostEqual(arc2.length(), pi * 100) + +// def test_length_out_of_range(self): +// # See F.6.2 Out-of-range parameters + +// # If the endpoints (x1, y1) and (x2, y2) are identical, then this is +// # equivalent to omitting the elliptical arc segment entirely. +// arc = Arc(0j, 100 + 100j, 0, 0, 0, 0j) +// self.assertAlmostEqual(arc.length(), 0) + +// # If rx = 0 or ry = 0 then this arc is treated as a straight +// # line segment (a "lineto") joining the endpoints. +// arc = Arc(0j, 0j, 0, 0, 0, 200 + 0j) +// self.assertAlmostEqual(arc.length(), 200) + +// # If rx or ry have negative signs, these are dropped; +// # the absolute value is used instead. +// arc = Arc(200 + 0j, -100 - 100j, 0, 0, 0, 0j) +// self.assertAlmostEqual(arc.length(), pi * 100) + +// # If rx, ry and φ are such that there is no solution (basically, +// # the ellipse is not big enough to reach from (x1, y1) to (x2, y2)) +// # then the ellipse is scaled up uniformly until there is exactly +// # one solution (until the ellipse is just big enough). +// arc = Arc(0j, 1 + 1j, 0, 0, 0, 200 + 0j) +// self.assertAlmostEqual(arc.length(), pi * 100) + +// # φ is taken mod 360 degrees. +// arc = Arc(200 + 0j, -100 - 100j, 720, 0, 0, 0j) +// self.assertAlmostEqual(arc.length(), pi * 100) + +// def test_equality(self): +// # This is to test the __eq__ and __ne__ methods, so we can't use +// # assertEqual and assertNotEqual +// segment = Arc(0j, 100 + 50j, 0, 0, 0, 100 + 50j) +// self.assertTrue(segment == Arc(0j, 100 + 50j, 0, 0, 0, 100 + 50j)) +// self.assertTrue(segment != Arc(0j, 100 + 50j, 0, 1, 0, 100 + 50j)) + +// def test_issue25(self): +// # This raised a math domain error +// Arc( +// (725.307482225571 - 915.5548199281527j), +// (202.79421639137703 + 148.77294617167183j), +// 225.6910319606926, +// 1, +// 1, +// (-624.6375539637027 + 896.5483089399895j), +// ) + + +// class TestPath(unittest.TestCase): +// def test_circle(self): +// arc1 = Arc(0j, 100 + 100j, 0, 0, 0, 200 + 0j) +// arc2 = Arc(200 + 0j, 100 + 100j, 0, 0, 0, 0j) +// path = Path(arc1, arc2) +// self.assertAlmostEqual(path.point(0.0), (0j)) +// self.assertAlmostEqual(path.point(0.25), (100 + 100j)) +// self.assertAlmostEqual(path.point(0.5), (200 + 0j)) +// self.assertAlmostEqual(path.point(0.75), (100 - 100j)) +// self.assertAlmostEqual(path.point(1.0), (0j)) +// self.assertAlmostEqual(path.length(), pi * 200) + +// def test_svg_specs(self): +// """The paths that are in the SVG specs""" + +// # Big pie: M300,200 h-150 a150,150 0 1,0 150,-150 z +// path = Path( +// Line(300 + 200j, 150 + 200j), +// Arc(150 + 200j, 150 + 150j, 0, 1, 0, 300 + 50j), +// Line(300 + 50j, 300 + 200j), +// ) +// # The points and length for this path are calculated and not regression tests. +// self.assertAlmostEqual(path.point(0.0), (300 + 200j)) +// self.assertAlmostEqual(path.point(0.14897825542), (150 + 200j)) +// self.assertAlmostEqual(path.point(0.5), (406.066017177 + 306.066017177j)) +// self.assertAlmostEqual(path.point(1 - 0.14897825542), (300 + 50j)) +// self.assertAlmostEqual(path.point(1.0), (300 + 200j)) +// # The errors seem to accumulate. Still 6 decimal places is more than good enough. +// self.assertAlmostEqual(path.length(), pi * 225 + 300, places=6) + +// # Little pie: M275,175 v-150 a150,150 0 0,0 -150,150 z +// path = Path( +// Line(275 + 175j, 275 + 25j), +// Arc(275 + 25j, 150 + 150j, 0, 0, 0, 125 + 175j), +// Line(125 + 175j, 275 + 175j), +// ) +// # The points and length for this path are calculated and not regression tests. +// self.assertAlmostEqual(path.point(0.0), (275 + 175j)) +// self.assertAlmostEqual(path.point(0.2800495767557787), (275 + 25j)) +// self.assertAlmostEqual( +// path.point(0.5), (168.93398282201787 + 68.93398282201787j) +// ) +// self.assertAlmostEqual(path.point(1 - 0.2800495767557787), (125 + 175j)) +// self.assertAlmostEqual(path.point(1.0), (275 + 175j)) +// # The errors seem to accumulate. Still 6 decimal places is more than good enough. +// self.assertAlmostEqual(path.length(), pi * 75 + 300, places=6) + +// # Bumpy 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 +// path = Path( +// 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), +// ) + +// # These are *not* calculated, but just regression tests. Be skeptical. +// self.assertAlmostEqual(path.point(0.0), (600 + 350j)) +// self.assertAlmostEqual(path.point(0.3), (755.23979927 + 212.1820209585j)) +// self.assertAlmostEqual(path.point(0.5), (827.73074926 + 147.8241574162j)) +// self.assertAlmostEqual(path.point(0.9), (971.28435780 + 106.3023526073j)) +// self.assertAlmostEqual(path.point(1.0), (1050 + 125j)) +// self.assertAlmostEqual(path.length(), 928.388639381) + +// def test_repr(self): +// path = Path( +// Line(start=600 + 350j, end=650 + 325j), +// Arc( +// start=650 + 325j, +// radius=25 + 25j, +// rotation=-30, +// arc=0, +// sweep=1, +// end=700 + 300j, +// ), +// CubicBezier( +// start=700 + 300j, +// control1=800 + 400j, +// control2=750 + 200j, +// end=600 + 100j, +// ), +// QuadraticBezier(start=600 + 100j, control=600, end=600 + 300j), +// ) +// self.assertEqual(eval(repr(path)), path) + +// def test_reverse(self): +// # Currently you can't reverse paths. +// self.assertRaises(NotImplementedError, Path().reverse) + +// def test_equality(self): +// # This is to test the __eq__ and __ne__ methods, so we can't use +// # assertEqual and assertNotEqual +// path1 = Path( +// Line(start=600 + 350j, end=650 + 325j), +// Arc( +// start=650 + 325j, +// radius=25 + 25j, +// rotation=-30, +// arc=0, +// sweep=1, +// end=700 + 300j, +// ), +// CubicBezier( +// start=700 + 300j, +// control1=800 + 400j, +// control2=750 + 200j, +// end=600 + 100j, +// ), +// QuadraticBezier(start=600 + 100j, control=600, end=600 + 300j), +// ) +// path2 = Path( +// Line(start=600 + 350j, end=650 + 325j), +// Arc( +// start=650 + 325j, +// radius=25 + 25j, +// rotation=-30, +// arc=0, +// sweep=1, +// end=700 + 300j, +// ), +// CubicBezier( +// start=700 + 300j, +// control1=800 + 400j, +// control2=750 + 200j, +// end=600 + 100j, +// ), +// QuadraticBezier(start=600 + 100j, control=600, end=600 + 300j), +// ) + +// self.assertTrue(path1 == path2) +// # Modify path2: +// path2[0].start = 601 + 350j +// self.assertTrue(path1 != path2) + +// # Modify back: +// path2[0].start = 600 + 350j +// self.assertFalse(path1 != path2) + +// # Get rid of the last segment: +// del path2[-1] +// self.assertFalse(path1 == path2) + +// # It's not equal to a list of it's segments +// self.assertTrue(path1 != path1[:]) +// self.assertFalse(path1 == path1[:]) + +// def test_non_arc(self): +// # And arc with the same start and end is a noop. +// segment = Arc(0j + 70j, 35 + 35j, 0, 1, 0, 0 + 70j) +// self.assertEqual(segment.length(), 0) +// self.assertEqual(segment.point(0.5), segment.start) + +// def test_zero_paths(self): +// move_only = Path(Move(0)) +// self.assertEqual(move_only.point(0), 0 + 0j) +// self.assertEqual(move_only.point(0.5), 0 + 0j) +// self.assertEqual(move_only.point(1), 0 + 0j) +// self.assertEqual(move_only.length(), 0) + +// move_onlyz = Path(Move(0), Close(0, 0)) +// self.assertEqual(move_onlyz.point(0), 0 + 0j) +// self.assertEqual(move_onlyz.point(0.5), 0 + 0j) +// self.assertEqual(move_onlyz.point(1), 0 + 0j) +// self.assertEqual(move_onlyz.length(), 0) + +// zero_line = Path(Move(0), Line(0, 0)) +// self.assertEqual(zero_line.point(0), 0 + 0j) +// self.assertEqual(zero_line.point(0.5), 0 + 0j) +// self.assertEqual(zero_line.point(1), 0 + 0j) +// self.assertEqual(zero_line.length(), 0) + +// only_line = Path(Line(1 + 1j, 1 + 1j)) +// self.assertEqual(only_line.point(0), 1 + 1j) +// self.assertEqual(only_line.point(0.5), 1 + 1j) +// self.assertEqual(only_line.point(1), 1 + 1j) +// self.assertEqual(only_line.length(), 0) +} \ No newline at end of file diff --git a/test/svg/tokenizer_test.dart b/test/svg/tokenizer_test.dart new file mode 100644 index 0000000..7e2cb43 --- /dev/null +++ b/test/svg/tokenizer_test.dart @@ -0,0 +1,123 @@ +// import pytest +// from svg.path import parser + +import 'package:flutter_test/flutter_test.dart'; +import 'package:kanimaji/common/point.dart'; +import 'package:kanimaji/svg/parser.dart' show Command, Token, commandifyPath, parsePath, tokenizePath; + +class TokenizerTest { + final String pathdef; + final List commands; + final List tokens; + + const TokenizerTest({ + required this.pathdef, + required this.commands, + required this.tokens, + }); +} + +final List tokenizerTests = [ + const TokenizerTest( + pathdef: "M 100 100 L 300 100 L 200 300 z", + commands: [ + Command(command: "M", args: "100 100"), + Command(command: "L", args: "300 100"), + Command(command: "L", args: "200 300"), + Command(command: "z", args: ""), + ], + tokens: [ + Token(command: "M", args: [Point(100, 100)]), + Token(command: "L", args: [Point(300, 100)]), + Token(command: "L", args: [Point(200, 300)]), + Token(command: "z", args: []) + ], + ), + const TokenizerTest( + pathdef: + "M 5 1 v 7.344 A 3.574 3.574 0 003.5 8 3.515 3.515 0 000 11.5 C 0 13.421 1.579 15 3.5 15 " + "A 3.517 3.517 0 007 11.531 v -7.53 h 6 v 4.343 A 3.574 3.574 0 0011.5 8 3.515 3.515 0 008 11.5 " + "c 0 1.921 1.579 3.5 3.5 3.5 1.9 0 3.465 -1.546 3.5 -3.437 V 1 z", + commands: [ + Command(command: "M", args: "5 1"), + Command(command: "v", args: "7.344"), + Command( + command: "A", args: "3.574 3.574 0 003.5 8 3.515 3.515 0 000 11.5"), + Command(command: "C", args: "0 13.421 1.579 15 3.5 15"), + Command(command: "A", args: "3.517 3.517 0 007 11.531"), + Command(command: "v", args: "-7.53"), + Command(command: "h", args: "6"), + Command(command: "v", args: "4.343"), + Command( + command: "A", args: "3.574 3.574 0 0011.5 8 3.515 3.515 0 008 11.5"), + Command( + command: "c", + args: "0 1.921 1.579 3.5 3.5 3.5 1.9 0 3.465 -1.546 3.5 -3.437"), + Command(command: "V", args: "1"), + Command(command: "z", args: ""), + ], + tokens: [ + Token(command: "M", args: [Point(5, 1)]), + Token(command: "v", args: [7.344]), + Token(command: "A", args: [3.574, 3.574, 0, false, false, Point(3.5, 8)]), + Token( + command: "A", args: [3.515, 3.515, 0, false, false, Point(0, 11.5)]), + Token( + command: "C", + args: [Point(0, 13.421), Point(1.579, 15), Point(3.5, 15)]), + Token( + command: "A", + args: [3.517, 3.517, 0, false, false, Point(7, 11.531)]), + Token(command: "v", args: [-7.53]), + Token(command: "h", args: [6]), + Token(command: "v", args: [4.343]), + Token( + command: "A", args: [3.574, 3.574, 0, false, false, Point(11.5, 8)]), + Token( + command: "A", args: [3.515, 3.515, 0, false, false, Point(8, 11.5)]), + Token( + command: "c", + args: [Point(0, 1.921), Point(1.579, 3.5), Point(3.5, 3.5)]), + Token( + command: "c", + args: [Point(1.9, 0), Point(3.465, -1.546), Point(3.5, -3.437)]), + Token(command: "V", args: [1]), + Token(command: "z", args: []), + ], + ), + const TokenizerTest( + pathdef: "M 600,350 L 650,325 A 25,25 -30 0,1 700,300 L 750,275", + commands: [ + Command(command: "M", args: "600,350"), + Command(command: "L", args: "650,325"), + Command(command: "A", args: "25,25 -30 0,1 700,300"), + Command(command: "L", args: "750,275"), + ], + tokens: [ + Token(command: "M", args: [Point(600, 350)]), + Token(command: "L", args: [Point(650, 325)]), + Token(command: "A", args: [25, 25, -30, false, true, Point(700, 300)]), + Token(command: "L", args: [Point(750, 275)]), + ], + ), +]; + +void main() { + test('Test commandifier', () { + for (final tokenizerTest in tokenizerTests) { + expect(commandifyPath(tokenizerTest.pathdef), tokenizerTest.commands); + } + }); + + test('Test tokenizer', () { + for (final tokenizerTest in tokenizerTests) { + expect(tokenizePath(tokenizerTest.pathdef), tokenizerTest.tokens); + } + }); + + test('Test parser', () { + for (final tokenizerTest in tokenizerTests) { + parsePath(tokenizerTest.pathdef); + } + }); +} \ No newline at end of file