LCOV - code coverage report
Current view: top level - path_parser/model/commands - elliptical_arc_command.dart (source / functions) Hit Total Coverage
Test: lcov.info Lines: 151 151 100.0 %
Date: 2022-02-22 16:00:34 Functions: 0 0 -

          Line data    Source code
       1             : import 'dart:math';
       2             : 
       3             : import 'package:mrx_icon_font_gen/parser/path/model/arguments/coordinate_pair.dart';
       4             : import 'package:mrx_icon_font_gen/parser/path/model/arguments/coordinate_pair_sequence.dart';
       5             : import 'package:mrx_icon_font_gen/parser/path/model/arguments/curve_to_coordinate_sequence.dart';
       6             : import 'package:mrx_icon_font_gen/parser/path/model/arguments/elliptical_arc_argument.dart';
       7             : import 'package:mrx_icon_font_gen/parser/path/model/arguments/elliptical_arc_argument_sequence.dart';
       8             : import 'package:mrx_icon_font_gen/parser/path/model/command.dart';
       9             : import 'package:mrx_icon_font_gen/parser/path/model/commands/curve_to_command.dart';
      10             : import 'package:mrx_icon_font_gen/parser/path/model/commands/line_to_command.dart';
      11             : import 'package:vector_math/vector_math_64.dart';
      12             : 
      13             : class EllipticalArcCommand extends Command {
      14           2 :   @override
      15             :   String get absoluteCommandName => 'A';
      16             : 
      17           2 :   @override
      18             :   String get relativeCommandName => 'a';
      19             : 
      20           2 :   EllipticalArcCommand({
      21             :     String? command,
      22             :     bool? isRelative,
      23             :     required EllipticalArcArgumentSequence commandArguments,
      24           2 :   }) : super(
      25             :           command: command,
      26             :           isRelative: isRelative,
      27             :           commandArguments: commandArguments,
      28             :         );
      29             : 
      30           1 :   @override
      31             :   List<Command> applyTransformation(
      32             :     Matrix3 transform,
      33             :     CoordinatePair startPoint,
      34             :   ) {
      35             :     final EllipticalArcArgumentSequence argumentSequence =
      36           1 :         commandArguments as EllipticalArcArgumentSequence;
      37           1 :     final List<Command> bezierCurves = [];
      38             :     CoordinatePair lastPoint = startPoint;
      39           2 :     for (final arguments in argumentSequence.ellipticalArcArguments) {
      40             :       final List<Command> newBezierCurves =
      41           1 :           _convertToBezierCurves(arguments, lastPoint);
      42           2 :       for (final Command curve in newBezierCurves) {
      43           2 :         bezierCurves.addAll(curve.applyTransformation(transform, lastPoint));
      44           1 :         lastPoint = curve.getLastPoint(lastPoint);
      45             :       }
      46             :     }
      47             :     return bezierCurves;
      48             :   }
      49             : 
      50             :   // This function is a port of SVGAndroidRenderer::arcTo method from
      51             :   // BigBadaboom's androidsvg library (Apache 2 license).
      52           1 :   List<Command> _convertToBezierCurves(
      53             :     EllipticalArcArgument arguments,
      54             :     CoordinatePair startPoint,
      55             :   ) {
      56           4 :     final double x = arguments.x + (isAbsolute ? 0 : startPoint.x);
      57           4 :     final double y = arguments.y + (isAbsolute ? 0 : startPoint.y);
      58             : 
      59           4 :     if ((startPoint.x == x && startPoint.y == y) ||
      60           2 :         arguments.rx == 0.0 ||
      61           2 :         arguments.ry == 0.0) {
      62           1 :       return [
      63           1 :         LineToCommand(
      64           1 :           isRelative: isRelative,
      65           1 :           commandArguments: CoordinatePairSequence(
      66           1 :             coordinatePairs: [
      67           1 :               CoordinatePair(
      68             :                 x: x,
      69             :                 y: y,
      70             :               ),
      71             :             ],
      72             :           ),
      73             :         ),
      74             :       ];
      75             :     }
      76             : 
      77             :     // Sign of the radii is ignored (behaviour specified by the spec)
      78           2 :     double rx = arguments.rx.abs();
      79           2 :     double ry = arguments.ry.abs();
      80             : 
      81           1 :     final double twoPi = pi * 2.0;
      82             : 
      83             :     // Convert angle from degrees to radians
      84             :     final double angleRad =
      85           4 :         arguments.xAxisRotation.remainder(360.0) * twoPi / 360.0;
      86           1 :     final double cosAngle = cos(angleRad);
      87           1 :     final double sinAngle = sin(angleRad);
      88             : 
      89             :     // We simplify the calculations by transforming the arc so that the origin is at the
      90             :     // midpoint calculated above followed by a rotation to line up the coordinate axes
      91             :     // with the axes of the ellipse.
      92             : 
      93             :     // Compute the midpoint of the line between the current and the end point
      94           3 :     final double dx2 = (startPoint.x - x) / 2.0;
      95           3 :     final double dy2 = (startPoint.y - y) / 2.0;
      96             : 
      97             :     // Step 1 : Compute (x1', y1')
      98             :     // x1,y1 is the midpoint vector rotated to take the arc's angle out of consideration
      99           3 :     final double x1 = (cosAngle * dx2 + sinAngle * dy2);
     100           4 :     final double y1 = (-sinAngle * dx2 + cosAngle * dy2);
     101             : 
     102           1 :     double rxSq = rx * rx;
     103           1 :     double rySq = ry * ry;
     104           1 :     final double x1Sq = x1 * x1;
     105           1 :     final double y1Sq = y1 * y1;
     106             : 
     107             :     // Check that radii are large enough.
     108             :     // If they are not, the spec says to scale them up so they are.
     109             :     // This is to compensate for potential rounding errors/differences between SVG implementations.
     110           3 :     final double radiiCheck = x1Sq / rxSq + y1Sq / rySq;
     111           1 :     if (radiiCheck > 0.99999) {
     112           2 :       final double radiiScale = sqrt(radiiCheck) * 1.00001;
     113           1 :       rx = radiiScale * rx;
     114           1 :       ry = radiiScale * ry;
     115           1 :       rxSq = rx * rx;
     116           1 :       rySq = ry * ry;
     117             :     }
     118             : 
     119             :     // Step 2 : Compute (cx1, cy1) - the transformed centre point
     120           3 :     double sign = (arguments.largeArcFlag == arguments.sweepFlag) ? -1 : 1;
     121           1 :     final double sq = max(
     122             :       0,
     123           6 :       ((rxSq * rySq) - (rxSq * y1Sq) - (rySq * x1Sq)) /
     124           3 :           ((rxSq * y1Sq) + (rySq * x1Sq)),
     125             :     );
     126           2 :     final double coefficient = (sign * sqrt(sq));
     127           3 :     final double cx1 = coefficient * ((rx * y1) / ry);
     128           4 :     final double cy1 = coefficient * -((ry * x1) / rx);
     129             : 
     130             :     // Step 3 : Compute (cx, cy) from (cx1, cy1)
     131           3 :     final double sx2 = (startPoint.x + x) / 2.0;
     132           3 :     final double sy2 = (startPoint.y + y) / 2.0;
     133           4 :     final double cx = sx2 + (cosAngle * cx1 - sinAngle * cy1);
     134           4 :     final double cy = sy2 + (sinAngle * cx1 + cosAngle * cy1);
     135             : 
     136             :     // Step 4 : Compute the angleStart (angle1) and the angleExtent (dangle)
     137           2 :     final double ux = (x1 - cx1) / rx;
     138           2 :     final double uy = (y1 - cy1) / ry;
     139           3 :     final double vx = (-x1 - cx1) / rx;
     140           3 :     final double vy = (-y1 - cy1) / ry;
     141             : 
     142             :     // Angle between two vectors is +/- arcCos( u.v / len(u) * len(v))
     143             :     // Where '.' is the dot product. And +/- is calculated from the sign of the cross product (u x v)
     144             : 
     145             :     // Compute the start angle
     146             :     // The angle between (ux,uy) and the 0deg angle (1,0)
     147           4 :     double n = sqrt((ux * ux) + (uy * uy)); // len(u) * len(1,0) == len(u)
     148             :     double p = ux; // u.v == (ux,uy).(1,0) == (1 * ux) + (0 * uy) == ux
     149           2 :     sign = (uy < 0) ? -1.0 : 1.0; // u x v == (1 * uy - ux * 0) == uy
     150             :     // No need for checkedArcCos() here. (p >= n) should always be true.
     151           3 :     double angleStart = sign * acos(p / n);
     152             : 
     153             :     // Compute the angle extent
     154           8 :     n = sqrt((ux * ux + uy * uy) * (vx * vx + vy * vy));
     155           3 :     p = ux * vx + uy * vy;
     156           5 :     sign = (ux * vy - uy * vx < 0) ? -1.0 : 1.0;
     157           3 :     double angleExtent = sign * checkedArcCos(p / n);
     158             : 
     159           3 :     if (arguments.sweepFlag == 0 && angleExtent > 0) {
     160           1 :       angleExtent -= twoPi;
     161           3 :     } else if (arguments.sweepFlag == 1 && angleExtent < 0) {
     162           1 :       angleExtent += twoPi;
     163             :     }
     164           1 :     angleExtent = angleExtent.remainder(twoPi);
     165           1 :     angleStart = angleStart.remainder(twoPi);
     166             : 
     167             :     // Many elliptical arc implementations including the Java2D and Android ones, only
     168             :     // support arcs that are axis aligned. Therefore we need to substitute the arc
     169             :     // with bezier curves. The following method call will generate the beziers for
     170             :     // a unit circle that covers the arc angles we want.
     171           1 :     List<CoordinatePair> bezierPoints = arcToBeziers(angleStart, angleExtent);
     172             : 
     173             :     // Calculate a transformation matrix that will move and scale these bezier points to the correct location.
     174             :     // translate
     175           3 :     Matrix3 m = Matrix3.identity()..setColumn(2, Vector3(cx, cy, 1.0));
     176             :     // rotate
     177           1 :     m.multiply(
     178           1 :       Matrix3.identity()
     179           3 :         ..setRow(0, Vector3(cosAngle, -sinAngle, 0.0))
     180           2 :         ..setRow(1, Vector3(sinAngle, cosAngle, 0.0)),
     181             :     );
     182             :     // scale
     183           1 :     m.multiply(
     184           1 :       Matrix3.identity()
     185           2 :         ..setRow(0, Vector3(rx, 0.0, 0.0))
     186           2 :         ..setRow(1, Vector3(0.0, ry, 0.0)),
     187             :     );
     188             : 
     189           3 :     for (int i = 0; i < bezierPoints.length; i++) {
     190             :       final Vector3 transformedPoint =
     191           6 :           m.transform(Vector3(bezierPoints[i].x, bezierPoints[i].y, 1.0));
     192           2 :       bezierPoints[i] = CoordinatePair(
     193           1 :         x: transformedPoint.x,
     194           1 :         y: transformedPoint.y,
     195             :       );
     196             :     }
     197             : 
     198             :     // The last point in the bezier set should match exactly the last coordinates pair in the arc (ie: x,y). But
     199             :     // considering all the mathematical manipulation we have been doing, it is bound to be off by a tiny
     200             :     // fraction. Experiments show that it can be up to around 0.00002.  So why don't we just set it to
     201             :     // exactly what it ought to be.
     202           4 :     bezierPoints[bezierPoints.length - 1] = CoordinatePair(
     203             :       x: x,
     204             :       y: y,
     205             :     );
     206             : 
     207           1 :     List<Command> bezierCurves = [];
     208             :     // Final step is to add the bezier curves to the path
     209           3 :     for (int i = 0; i < bezierPoints.length; i += 3) {
     210           1 :       bezierCurves.add(
     211           1 :         CurveToCommand(
     212             :           isRelative: false,
     213           1 :           commandArguments: CurveToCoordinateSequence(
     214           1 :             coordinatePairTriplets: [
     215           1 :               CoordinatePairTriplet(
     216           1 :                 coordinatePairs: [
     217           1 :                   bezierPoints[i],
     218           2 :                   bezierPoints[i + 1],
     219           2 :                   bezierPoints[i + 2],
     220             :                 ],
     221             :               )
     222             :             ],
     223             :           ),
     224             :         ),
     225             :       );
     226             :     }
     227             :     return bezierCurves;
     228             :   }
     229             : 
     230           1 :   double checkedArcCos(double val) {
     231           2 :     return (val < -1.0)
     232             :         ? pi
     233           1 :         : (val > 1.0)
     234             :             ? 0
     235           1 :             : acos(val);
     236             :   }
     237             : 
     238           1 :   List<CoordinatePair> arcToBeziers(double angleStart, double angleExtent) {
     239             :     int numSegments =
     240           4 :         (angleExtent.abs() * 2.0 / pi).ceil(); // (angleExtent / 90deg)
     241             : 
     242           1 :     double angleIncrement = angleExtent / numSegments;
     243             : 
     244             :     // The length of each control point vector is given by the following formula.
     245           1 :     double controlLength = 4.0 /
     246           1 :         3.0 *
     247           3 :         sin(angleIncrement / 2.0) /
     248           3 :         (1.0 + cos(angleIncrement / 2.0));
     249             : 
     250           1 :     List<CoordinatePair> coordinatePairs = [];
     251             : 
     252           2 :     for (int i = 0; i < numSegments; i++) {
     253           2 :       double angle = angleStart + i * angleIncrement;
     254             :       // Calculate the control vector at this angle
     255           1 :       double dx = cos(angle);
     256           1 :       double dy = sin(angle);
     257             :       // First control point
     258           1 :       coordinatePairs.add(
     259           1 :         CoordinatePair(
     260           2 :           x: dx - controlLength * dy,
     261           2 :           y: dy + controlLength * dx,
     262             :         ),
     263             :       );
     264             :       // Second control point
     265           1 :       angle += angleIncrement;
     266           1 :       dx = cos(angle);
     267           1 :       dy = sin(angle);
     268           1 :       coordinatePairs.add(
     269           1 :         CoordinatePair(
     270           2 :           x: dx + controlLength * dy,
     271           2 :           y: dy - controlLength * dx,
     272             :         ),
     273             :       );
     274             :       // Endpoint of bezier
     275           1 :       coordinatePairs.add(
     276           1 :         CoordinatePair(
     277             :           x: dx,
     278             :           y: dy,
     279             :         ),
     280             :       );
     281             :     }
     282             :     return coordinatePairs;
     283             :   }
     284             : 
     285           1 :   @override
     286             :   CoordinatePair getLastPoint(CoordinatePair startPoint) {
     287           1 :     if (isAbsolute) {
     288             :       final EllipticalArcArgument lastPoint =
     289           1 :           (commandArguments as EllipticalArcArgumentSequence)
     290           1 :               .ellipticalArcArguments
     291           1 :               .last;
     292           1 :       return CoordinatePair(
     293           1 :         x: lastPoint.x,
     294           1 :         y: lastPoint.y,
     295             :       );
     296             :     }
     297           1 :     double x = startPoint.x;
     298           1 :     double y = startPoint.y;
     299             :     for (final EllipticalArcArgument eaa
     300           1 :         in (commandArguments as EllipticalArcArgumentSequence)
     301           2 :             .ellipticalArcArguments) {
     302           2 :       x += eaa.x;
     303           2 :       y += eaa.y;
     304             :     }
     305           1 :     return CoordinatePair(x: x, y: y);
     306             :   }
     307             : 
     308           2 :   @override
     309             :   bool operator ==(Object other) {
     310           2 :     if (other is! EllipticalArcCommand) {
     311             :       return false;
     312             :     }
     313           6 :     return command == other.command &&
     314           4 :         commandArguments as EllipticalArcArgumentSequence ==
     315           2 :             other.commandArguments as EllipticalArcArgumentSequence;
     316             :   }
     317             : 
     318           1 :   @override
     319           3 :   int get hashCode => Object.hash(command, commandArguments);
     320             : }

Generated by: LCOV version 1.15