diff --git a/lib/svg/path.dart b/lib/svg/path.dart index 18eb530..3718ff2 100644 --- a/lib/svg/path.dart +++ b/lib/svg/path.dart @@ -274,21 +274,19 @@ class Arc extends SvgPath { final num rotation; final bool arc; final bool sweep; - late final num radiusScale; - late final Point center; - late final num theta; - late final num delta; + // late final num radiusScale; + // late final Point center; + // late final num theta; + // late num delta; - Arc({ + const Arc({ required Point start, required Point end, required this.radius, required this.rotation, required this.arc, required this.sweep, - }) : super(start: start, end: end) { - _parameterize(); - } + }) : super(start: start, end: end); @override bool operator ==(Object other) => @@ -311,68 +309,53 @@ class Arc extends SvgPath { String toString() => 'Arc(start=$start, radius=$radius, rotation=$rotation, ' 'arc=$arc, sweep=$sweep, end=$end)'; - /// Conversion from endpoint to center parameterization - /// http://www.w3.org/TR/SVG/implnote.html#ArcImplementationNotes - void _parameterize() { - // This is equivalent of omitting the segment, so do nothing - if (start == end) return; - // This should be treated as a straight line - if (radius.x == 0 || radius.y == 0) return; + // Conversion from endpoint to center parameterization + // http://www.w3.org/TR/SVG/implnote.html#ArcImplementationNotes + num get _cosr => cos(radians(rotation)); + num get _sinr => sin(radians(rotation)); + num get _dx => (start.x - end.x) / 2; + num get _dy => (start.y - end.y) / 2; + num get _x1prim => _cosr * _dx + _sinr * _dy; + num get _x1primSq => _x1prim * _x1prim; + num get _y1prim => -_sinr * _dx + _cosr * _dy; + num get _y1primSq => _y1prim * _y1prim; + num get _rx => (radiusScale > 1 ? radiusScale : 1) * radius.x; + num get _ry => (radiusScale > 1 ? radiusScale : 1) * radius.y; + num get _rxSq => _rx * _rx; + num get _rySq => _ry * _ry; + num get _ux => (_x1prim - _cxprim) / _rx; + num get _uy => (_y1prim - _cyprim) / _ry; + num get _vx => (-_x1prim - _cxprim) / _rx; + num get _vy => (-_y1prim - _cyprim) / _ry; + num get t1 => _rxSq * _y1primSq; + num get t2 => _rySq * _x1primSq; + num get c => + (arc == sweep ? 1 : -1) * + sqrt(((_rxSq * _rySq - t1 - t2) / (t1 + t2)).abs()); + num get _cxprim => c * _rx * _y1prim / _ry; + num get _cyprim => -c * _ry * _x1prim / _rx; - final cosr = cos(radians(rotation)); - final sinr = sin(radians(rotation)); - final dx = (start.x - end.x) / 2; - final dy = (start.y - end.y) / 2; - final x1prim = cosr * dx + sinr * dy; - final x1primSq = x1prim * x1prim; - final y1prim = -sinr * dx + cosr * dy; - final y1primSq = y1prim * y1prim; + num get radiusScale { + final rs = (_x1primSq / (radius.x * radius.x)) + + (_y1primSq / (radius.y * radius.y)); + return rs > 1 ? sqrt(rs) : 1; + } - num rx = radius.x; - num rxSq = rx * rx; - num ry = radius.y; - num rySq = ry * ry; + Point get center => Point( + (_cosr * _cxprim - _sinr * _cyprim) + ((start.x + end.x) / 2), + (_sinr * _cxprim + _cosr * _cyprim) + ((start.y + end.y) / 2), + ); + + num get theta { + final num n = sqrt(_ux * _ux + _uy * _uy); + final num p = _ux; + return (((_uy < 0) ? -1 : 1) * degrees(acos(p / n))) % 360; + } - // Correct out of range radii - num radiusScale = (x1primSq / rxSq) + (y1primSq / rySq); - if (radiusScale > 1) { - radiusScale = sqrt(radiusScale); - rx *= radiusScale; - ry *= radiusScale; - rxSq = rx * rx; - rySq = ry * ry; - this.radiusScale = radiusScale; - } else { - // SVG spec only scales UP - this.radiusScale = 1; - } - - final t1 = rxSq * y1primSq; - final t2 = rySq * x1primSq; - num c = sqrt(((rxSq * rySq - t1 - t2) / (t1 + t2)).abs()); - - if (arc == sweep) { - c = -c; - } - final cxprim = c * rx * y1prim / ry; - final cyprim = -c * ry * x1prim / rx; - - center = Point( - (cosr * cxprim - sinr * cyprim) + ((start.x + end.x) / 2), - (sinr * cxprim + cosr * cyprim) + ((start.y + end.y) / 2), - ); - - final ux = (x1prim - cxprim) / rx; - final uy = (y1prim - cyprim) / ry; - final vx = (-x1prim - cxprim) / rx; - final vy = (-y1prim - cyprim) / ry; - num n = sqrt(ux * ux + uy * uy); - num p = ux; - theta = (((uy < 0) ? -1 : 1) * degrees(acos(p / n))) % 360; - - n = sqrt((ux * ux + uy * uy) * (vx * vx + vy * vy)); - p = ux * vx + uy * vy; + num get delta { + final num n = sqrt((_ux * _ux + _uy * _uy) * (_vx * _vx + _vy * _vy)); + final num p = _ux * _vx + _uy * _vy; num d = p / n; // In certain cases the above calculation can through inaccuracies // become just slightly out of range, f ex -1.0000000000000002. @@ -381,8 +364,8 @@ class Arc extends SvgPath { } else if (d < -1.0) { d = -1.0; } - delta = ((((ux * vy - uy * vx) < 0) ? -1 : 1) * degrees(acos(d))) % 360; - if (!sweep) delta -= 360; + + return ((((_ux * _vy - _uy * _vx) < 0) ? -1 : 1) * degrees(acos(d))) % 360 - (!sweep ? 360 : 0); } @override @@ -448,7 +431,6 @@ 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; @@ -472,7 +454,6 @@ class Close extends Linear { required Point end, }) : super(start: start, end: end); - @override bool operator ==(Object other) => other is Close && super == other; diff --git a/test/svg/parser_test.dart b/test/svg/parser_test.dart index 1568abe..bd8c922 100644 --- a/test/svg/parser_test.dart +++ b/test/svg/parser_test.dart @@ -88,182 +88,259 @@ void main() { ]), ); - // 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 = 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), + ), + ]), + ); - // 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 = 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), + ), + ]), + ); - // 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 = 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), + ), + ]), + ); - // 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 = 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), + ), + ]), + ); - // 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 = 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), + ), + ]), + ); - // 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 = 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)), + ]), + ); - // 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 = 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)), + ]), + ); - // 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 = 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)), + ]), + ); - // 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), - // ), - // ); + path1 = 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)), + ]), + ); }); // 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 = parsePath("M 0 0 L 50 20 m 50 80 L 300 100 L 200 300 z") + // expect( // path1, - // Path( + // Path.fromSegments(const [ // 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), + // 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)), // ), // ) // # Initial smooth and relative CubicBezier - // path1 = parse_path("""M100,200 s 150,-100 150,0""") - // self.assertEqual( + // path1 = parsePath("""M100,200 s 150,-100 150,0""") + // expect( // path1, - // Path( - // Move(100 + 200j), - // CubicBezier(100 + 200j, 100 + 200j, 250 + 100j, 250 + 200j), + // 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 QuadraticBezier - // path1 = parse_path("""M100,200 t 150,0""") - // self.assertEqual( + // path1 = parsePath("""M100,200 t 150,0""") + // expect( // path1, - // Path(Move(100 + 200j), QuadraticBezier(100 + 200j, 100 + 200j, 250 + 200j)), + // Path.fromSegments(const [Move(Point(100, 200)), QuadraticBezier(start: Point(100, 200), control: Point(100, 200), end: Point(250, 200)),), // ) // # Relative QuadraticBezier - // path1 = parse_path("""M100,200 q 0,0 150,0""") - // self.assertEqual( + // path1 = parsePath("""M100,200 q 0,0 150,0""") + // expect( // path1, - // Path(Move(100 + 200j), QuadraticBezier(100 + 200j, 100 + 200j, 250 + 200j)), + // Path.fromSegments(const [Move(Point(100, 200)), QuadraticBezier(start: Point(100, 200), control: Point(100, 200), end: Point(250, 200)),), // ) // 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) + // path1 = parsePath("M100,200c10-5,20-10,30-20") + // path2 = parsePath("M 100 200 c 10 -5 20 -10 30 -20") + // expect(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( + // 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) // ) - // self.assertEqual(path1, path2) + // expect(path1, path2) // def test_errors(self): - // self.assertRaises(ValueError, parse_path, "M 100 100 L 200 200 Z 100 200") + // self.assertRaises(ValueError, parsePath, "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") + // path = parsePath("M10.236,100.184") + // expect(path.d(), "M 10.236,100.184") // def test_issue_45(self): - // path = parse_path( + // 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 "