content: /*
   * Copyright (C) 2010 The Android Open Source Project
   *
   * Licensed under the Apache License, Version 2.0 (the "License");
   * you may not use this file except in compliance with the License.
   * You may obtain a copy of the License at
   *
   *      http://www.apache.org/licenses/LICENSE-2.0
   *
   * Unless required by applicable law or agreed to in writing, software
   * distributed under the License is distributed on an "AS IS" BASIS,
   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   * See the License for the specific language governing permissions and
   * limitations under the License.
   */
  
  package android.graphics;
  
  import com.android.ide.common.rendering.api.LayoutLog;
  import com.android.layoutlib.bridge.Bridge;
  import com.android.layoutlib.bridge.impl.DelegateManager;
  import com.android.tools.layoutlib.annotations.LayoutlibDelegate;
  
  import android.annotation.NonNull;
  import android.graphics.Path.Direction;
  import android.graphics.Path.FillType;
  
  import java.awt.Shape;
  import java.awt.geom.AffineTransform;
  import java.awt.geom.Arc2D;
  import java.awt.geom.Area;
  import java.awt.geom.Ellipse2D;
  import java.awt.geom.GeneralPath;
  import java.awt.geom.Path2D;
  import java.awt.geom.PathIterator;
  import java.awt.geom.Point2D;
  import java.awt.geom.Rectangle2D;
  import java.awt.geom.RoundRectangle2D;
  import java.util.ArrayList;
  
  import libcore.util.NativeAllocationRegistry_Delegate;
  
  /**
   * Delegate implementing the native methods of android.graphics.Path
   *
   * Through the layoutlib_create tool, the original native methods of Path have been replaced
   * by calls to methods of the same name in this delegate class.
   *
   * This class behaves like the original native implementation, but in Java, keeping previously
   * native data into its own objects and mapping them to int that are sent back and forth between
   * it and the original Path class.
   *
   * @see DelegateManager
   *
   */
  public final class Path_Delegate {
  
      // ---- delegate manager ----
      private static final DelegateManager<Path_Delegate> sManager =
              new DelegateManager<Path_Delegate>(Path_Delegate.class);
  
      private static final float EPSILON = 1e-4f;
  
      private static long sFinalizer = -1;
  
      // ---- delegate data ----
      private FillType mFillType = FillType.WINDING;
      private Path2D mPath = new Path2D.Double();
  
      private float mLastX = 0;
      private float mLastY = 0;
  
      // true if the path contains does not contain a curve or line.
      private boolean mCachedIsEmpty = true;
  
      // ---- Public Helper methods ----
  
      public static Path_Delegate getDelegate(long nPath) {
          return sManager.getDelegate(nPath);
      }
  
      public Path2D getJavaShape() {
          return mPath;
      }
  
      public void setJavaShape(Shape shape) {
          reset();
          mPath.append(shape, false /*connect*/);
      }
  
      public void reset() {
          mPath.reset();
          mLastX = 0;
          mLastY = 0;
      }
  
      public void setPathIterator(PathIterator iterator) {
          reset();
          mPath.append(iterator, false /*connect*/);
      }
  
      // ---- native methods ----
  
      @LayoutlibDelegate
      /*package*/ static long nInit() {
          // create the delegate
          Path_Delegate newDelegate = new Path_Delegate();
  
          return sManager.addNewDelegate(newDelegate);
      }
  
      @LayoutlibDelegate
      /*package*/ static long nInit(long nPath) {
          // create the delegate
          Path_Delegate newDelegate = new Path_Delegate();
  
          // get the delegate to copy, which could be null if nPath is 0
          Path_Delegate pathDelegate = sManager.getDelegate(nPath);
          if (pathDelegate != null) {
              newDelegate.set(pathDelegate);
          }
  
          return sManager.addNewDelegate(newDelegate);
      }
  
      @LayoutlibDelegate
      /*package*/ static void nReset(long nPath) {
          Path_Delegate pathDelegate = sManager.getDelegate(nPath);
          if (pathDelegate == null) {
              return;
          }
  
          pathDelegate.reset();
      }
  
      @LayoutlibDelegate
      /*package*/ static void nRewind(long nPath) {
          // call out to reset since there's nothing to optimize in
          // terms of data structs.
          nReset(nPath);
      }
  
      @LayoutlibDelegate
      /*package*/ static void nSet(long native_dst, long nSrc) {
          Path_Delegate pathDstDelegate = sManager.getDelegate(native_dst);
          if (pathDstDelegate == null) {
              return;
          }
  
          Path_Delegate pathSrcDelegate = sManager.getDelegate(nSrc);
          if (pathSrcDelegate == null) {
              return;
          }
  
          pathDstDelegate.set(pathSrcDelegate);
      }
  
      @LayoutlibDelegate
      /*package*/ static boolean nIsConvex(long nPath) {
          Bridge.getLog().fidelityWarning(LayoutLog.TAG_UNSUPPORTED,
                  "Path.isConvex is not supported.", null, null);
          return true;
      }
  
      @LayoutlibDelegate
      /*package*/ static int nGetFillType(long nPath) {
          Path_Delegate pathDelegate = sManager.getDelegate(nPath);
          if (pathDelegate == null) {
              return 0;
          }
  
          return pathDelegate.mFillType.nativeInt;
      }
  
      @LayoutlibDelegate
      public static void nSetFillType(long nPath, int ft) {
          Path_Delegate pathDelegate = sManager.getDelegate(nPath);
          if (pathDelegate == null) {
              return;
          }
  
          pathDelegate.setFillType(Path.sFillTypeArray[ft]);
      }
  
      @LayoutlibDelegate
      /*package*/ static boolean nIsEmpty(long nPath) {
          Path_Delegate pathDelegate = sManager.getDelegate(nPath);
          return pathDelegate == null || pathDelegate.isEmpty();
  
      }
  
      @LayoutlibDelegate
      /*package*/ static boolean nIsRect(long nPath, RectF rect) {
          Path_Delegate pathDelegate = sManager.getDelegate(nPath);
          if (pathDelegate == null) {
              return false;
          }
  
          // create an Area that can test if the path is a rect
          Area area = new Area(pathDelegate.mPath);
          if (area.isRectangular()) {
              if (rect != null) {
                  pathDelegate.fillBounds(rect);
              }
  
              return true;
          }
  
          return false;
      }
  
      @LayoutlibDelegate
      /*package*/ static void nComputeBounds(long nPath, RectF bounds) {
          Path_Delegate pathDelegate = sManager.getDelegate(nPath);
          if (pathDelegate == null) {
              return;
          }
  
          pathDelegate.fillBounds(bounds);
      }
  
      @LayoutlibDelegate
      /*package*/ static void nIncReserve(long nPath, int extraPtCount) {
          // since we use a java2D path, there's no way to pre-allocate new points,
          // so we do nothing.
      }
  
      @LayoutlibDelegate
      /*package*/ static void nMoveTo(long nPath, float x, float y) {
          Path_Delegate pathDelegate = sManager.getDelegate(nPath);
          if (pathDelegate == null) {
              return;
          }
  
          pathDelegate.moveTo(x, y);
      }
  
      @LayoutlibDelegate
      /*package*/ static void nRMoveTo(long nPath, float dx, float dy) {
          Path_Delegate pathDelegate = sManager.getDelegate(nPath);
          if (pathDelegate == null) {
              return;
          }
  
          pathDelegate.rMoveTo(dx, dy);
      }
  
      @LayoutlibDelegate
      /*package*/ static void nLineTo(long nPath, float x, float y) {
          Path_Delegate pathDelegate = sManager.getDelegate(nPath);
          if (pathDelegate == null) {
              return;
          }
  
          pathDelegate.lineTo(x, y);
      }
  
      @LayoutlibDelegate
      /*package*/ static void nRLineTo(long nPath, float dx, float dy) {
          Path_Delegate pathDelegate = sManager.getDelegate(nPath);
          if (pathDelegate == null) {
              return;
          }
  
          pathDelegate.rLineTo(dx, dy);
      }
  
      @LayoutlibDelegate
      /*package*/ static void nQuadTo(long nPath, float x1, float y1, float x2, float y2) {
          Path_Delegate pathDelegate = sManager.getDelegate(nPath);
          if (pathDelegate == null) {
              return;
          }
  
          pathDelegate.quadTo(x1, y1, x2, y2);
      }
  
      @LayoutlibDelegate
      /*package*/ static void nRQuadTo(long nPath, float dx1, float dy1, float dx2, float dy2) {
          Path_Delegate pathDelegate = sManager.getDelegate(nPath);
          if (pathDelegate == null) {
              return;
          }
  
          pathDelegate.rQuadTo(dx1, dy1, dx2, dy2);
      }
  
      @LayoutlibDelegate
      /*package*/ static void nCubicTo(long nPath, float x1, float y1,
              float x2, float y2, float x3, float y3) {
          Path_Delegate pathDelegate = sManager.getDelegate(nPath);
          if (pathDelegate == null) {
              return;
          }
  
          pathDelegate.cubicTo(x1, y1, x2, y2, x3, y3);
      }
  
      @LayoutlibDelegate
      /*package*/ static void nRCubicTo(long nPath, float x1, float y1,
              float x2, float y2, float x3, float y3) {
          Path_Delegate pathDelegate = sManager.getDelegate(nPath);
          if (pathDelegate == null) {
              return;
          }
  
          pathDelegate.rCubicTo(x1, y1, x2, y2, x3, y3);
      }
  
      @LayoutlibDelegate
      /*package*/ static void nArcTo(long nPath, float left, float top, float right,
              float bottom,
                      float startAngle, float sweepAngle, boolean forceMoveTo) {
          Path_Delegate pathDelegate = sManager.getDelegate(nPath);
          if (pathDelegate == null) {
              return;
          }
  
          pathDelegate.arcTo(left, top, right, bottom, startAngle, sweepAngle, forceMoveTo);
      }
  
      @LayoutlibDelegate
      /*package*/ static void nClose(long nPath) {
          Path_Delegate pathDelegate = sManager.getDelegate(nPath);
          if (pathDelegate == null) {
              return;
          }
  
          pathDelegate.close();
      }
  
      @LayoutlibDelegate
      /*package*/ static void nAddRect(long nPath,
              float left, float top, float right, float bottom, int dir) {
          Path_Delegate pathDelegate = sManager.getDelegate(nPath);
          if (pathDelegate == null) {
              return;
          }
  
          pathDelegate.addRect(left, top, right, bottom, dir);
      }
  
      @LayoutlibDelegate
      /*package*/ static void nAddOval(long nPath, float left, float top, float right,
              float bottom, int dir) {
          Path_Delegate pathDelegate = sManager.getDelegate(nPath);
          if (pathDelegate == null) {
              return;
          }
  
          pathDelegate.mPath.append(new Ellipse2D.Float(
                  left, top, right - left, bottom - top), false);
      }
  
      @LayoutlibDelegate
      /*package*/ static void nAddCircle(long nPath, float x, float y, float radius, int dir) {
          Path_Delegate pathDelegate = sManager.getDelegate(nPath);
          if (pathDelegate == null) {
              return;
          }
  
          // because x/y is the center of the circle, need to offset this by the radius
          pathDelegate.mPath.append(new Ellipse2D.Float(
                  x - radius, y - radius, radius * 2, radius * 2), false);
      }
  
      @LayoutlibDelegate
      /*package*/ static void nAddArc(long nPath, float left, float top, float right,
              float bottom, float startAngle, float sweepAngle) {
          Path_Delegate pathDelegate = sManager.getDelegate(nPath);
          if (pathDelegate == null) {
              return;
          }
  
          // because x/y is the center of the circle, need to offset this by the radius
          pathDelegate.mPath.append(new Arc2D.Float(
                  left, top, right - left, bottom - top,
                  -startAngle, -sweepAngle, Arc2D.OPEN), false);
      }
  
      @LayoutlibDelegate
      /*package*/ static void nAddRoundRect(long nPath, float left, float top, float right,
              float bottom, float rx, float ry, int dir) {
  
          Path_Delegate pathDelegate = sManager.getDelegate(nPath);
          if (pathDelegate == null) {
              return;
          }
  
          pathDelegate.mPath.append(new RoundRectangle2D.Float(
                  left, top, right - left, bottom - top, rx * 2, ry * 2), false);
      }
  
      @LayoutlibDelegate
      /*package*/ static void nAddRoundRect(long nPath, float left, float top, float right,
              float bottom, float[] radii, int dir) {
  
          Path_Delegate pathDelegate = sManager.getDelegate(nPath);
          if (pathDelegate == null) {
              return;
          }
  
          float[] cornerDimensions = new float[radii.length];
          for (int i = 0; i < radii.length; i++) {
              cornerDimensions[i] = 2 * radii[i];
          }
          pathDelegate.mPath.append(new RoundRectangle(left, top, right - left, bottom - top,
                  cornerDimensions), false);
      }
  
      @LayoutlibDelegate
      /*package*/ static void nAddPath(long nPath, long src, float dx, float dy) {
          addPath(nPath, src, AffineTransform.getTranslateInstance(dx, dy));
      }
  
      @LayoutlibDelegate
      /*package*/ static void nAddPath(long nPath, long src) {
          addPath(nPath, src, null /*transform*/);
      }
  
      @LayoutlibDelegate
      /*package*/ static void nAddPath(long nPath, long src, long matrix) {
          Matrix_Delegate matrixDelegate = Matrix_Delegate.getDelegate(matrix);
          if (matrixDelegate == null) {
              return;
          }
  
          addPath(nPath, src, matrixDelegate.getAffineTransform());
      }
  
      @LayoutlibDelegate
      /*package*/ static void nOffset(long nPath, float dx, float dy) {
          Path_Delegate pathDelegate = sManager.getDelegate(nPath);
          if (pathDelegate == null) {
              return;
          }
  
          pathDelegate.offset(dx, dy);
      }
  
      @LayoutlibDelegate
      /*package*/ static void nSetLastPoint(long nPath, float dx, float dy) {
          Path_Delegate pathDelegate = sManager.getDelegate(nPath);
          if (pathDelegate == null) {
              return;
          }
  
          pathDelegate.mLastX = dx;
          pathDelegate.mLastY = dy;
      }
  
      @LayoutlibDelegate
      /*package*/ static void nTransform(long nPath, long matrix,
                                                  long dst_path) {
          Path_Delegate pathDelegate = sManager.getDelegate(nPath);
          if (pathDelegate == null) {
              return;
          }
  
          Matrix_Delegate matrixDelegate = Matrix_Delegate.getDelegate(matrix);
          if (matrixDelegate == null) {
              return;
          }
  
          // this can be null if dst_path is 0
          Path_Delegate dstDelegate = sManager.getDelegate(dst_path);
  
          pathDelegate.transform(matrixDelegate, dstDelegate);
      }
  
      @LayoutlibDelegate
      /*package*/ static void nTransform(long nPath, long matrix) {
          nTransform(nPath, matrix, 0);
      }
  
      @LayoutlibDelegate
      /*package*/ static boolean nOp(long nPath1, long nPath2, int op, long result) {
          Bridge.getLog().error(LayoutLog.TAG_UNSUPPORTED, "Path.op() not supported", null);
          return false;
      }
  
      @LayoutlibDelegate
      /*package*/ static long nGetFinalizer() {
          synchronized (Path_Delegate.class) {
              if (sFinalizer == -1) {
                  sFinalizer = NativeAllocationRegistry_Delegate.createFinalizer(
                          sManager::removeJavaReferenceFor);
              }
          }
          return sFinalizer;
      }
  
      @LayoutlibDelegate
      /*package*/ static float[] nApproximate(long nPath, float error) {
          Path_Delegate pathDelegate = sManager.getDelegate(nPath);
          if (pathDelegate == null) {
              return null;
          }
          // Get a FlatteningIterator
          PathIterator iterator = pathDelegate.getJavaShape().getPathIterator(null, error);
  
          float segment[] = new float[6];
          float totalLength = 0;
          ArrayList<Point2D.Float> points = new ArrayList<Point2D.Float>();
          Point2D.Float previousPoint = null;
          while (!iterator.isDone()) {
              int type = iterator.currentSegment(segment);
              Point2D.Float currentPoint = new Point2D.Float(segment[0], segment[1]);
              // MoveTo shouldn't affect the length
              if (previousPoint != null && type != PathIterator.SEG_MOVETO) {
                  totalLength += currentPoint.distance(previousPoint);
              }
              previousPoint = currentPoint;
              points.add(currentPoint);
              iterator.next();
          }
  
          int nPoints = points.size();
          float[] result = new float[nPoints * 3];
          previousPoint = null;
          // Distance that we've covered so far. Used to calculate the fraction of the path that
          // we've covered up to this point.
          float walkedDistance = .0f;
          for (int i = 0; i < nPoints; i++) {
              Point2D.Float point = points.get(i);
              float distance = previousPoint != null ? (float) previousPoint.distance(point) : .0f;
              walkedDistance += distance;
              result[i * 3] = walkedDistance / totalLength;
              result[i * 3 + 1] = point.x;
              result[i * 3 + 2] = point.y;
  
              previousPoint = point;
          }
  
          return result;
      }
  
      // ---- Private helper methods ----
  
      private void set(Path_Delegate delegate) {
          mPath.reset();
          setFillType(delegate.mFillType);
          mPath.append(delegate.mPath, false /*connect*/);
      }
  
      private void setFillType(FillType fillType) {
          mFillType = fillType;
          mPath.setWindingRule(getWindingRule(fillType));
      }
  
      /**
       * Returns the Java2D winding rules matching a given Android {@link FillType}.
       * @param type the android fill type
       * @return the matching java2d winding rule.
       */
      private static int getWindingRule(FillType type) {
          switch (type) {
              case WINDING:
              case INVERSE_WINDING:
                  return GeneralPath.WIND_NON_ZERO;
              case EVEN_ODD:
              case INVERSE_EVEN_ODD:
                  return GeneralPath.WIND_EVEN_ODD;
  
              default:
                  assert false;
                  return GeneralPath.WIND_NON_ZERO;
          }
      }
  
      @NonNull
      private static Direction getDirection(int direction) {
          for (Direction d : Direction.values()) {
              if (direction == d.nativeInt) {
                  return d;
              }
          }
  
          assert false;
          return null;
      }
  
      public static void addPath(long destPath, long srcPath, AffineTransform transform) {
          Path_Delegate destPathDelegate = sManager.getDelegate(destPath);
          if (destPathDelegate == null) {
              return;
          }
  
          Path_Delegate srcPathDelegate = sManager.getDelegate(srcPath);
          if (srcPathDelegate == null) {
              return;
          }
  
          if (transform != null) {
              destPathDelegate.mPath.append(
                      srcPathDelegate.mPath.getPathIterator(transform), false);
          } else {
              destPathDelegate.mPath.append(srcPathDelegate.mPath, false);
          }
      }
  
  
      /**
       * Returns whether the path already contains any points.
       * Note that this is different to
       * {@link #isEmpty} because if all elements are {@link PathIterator#SEG_MOVETO},
       * {@link #isEmpty} will return true while hasPoints will return false.
       */
      public boolean hasPoints() {
          return !mPath.getPathIterator(null).isDone();
      }
  
      /**
       * Returns whether the path is empty (contains no lines or curves).
       * @see Path#isEmpty
       */
      public boolean isEmpty() {
          if (!mCachedIsEmpty) {
              return false;
          }
  
          float[] coords = new float[6];
          mCachedIsEmpty = Boolean.TRUE;
          for (PathIterator it = mPath.getPathIterator(null); !it.isDone(); it.next()) {
              int type = it.currentSegment(coords);
              if (type != PathIterator.SEG_MOVETO) {
                  // Once we know that the path is not empty, we do not need to check again unless
                  // Path#reset is called.
                  mCachedIsEmpty = false;
                  return false;
              }
          }
  
          return true;
      }
  
      /**
       * Fills the given {@link RectF} with the path bounds.
       * @param bounds the RectF to be filled.
       */
      public void fillBounds(RectF bounds) {
          Rectangle2D rect = mPath.getBounds2D();
          bounds.left = (float)rect.getMinX();
          bounds.right = (float)rect.getMaxX();
          bounds.top = (float)rect.getMinY();
          bounds.bottom = (float)rect.getMaxY();
      }
  
      /**
       * Set the beginning of the next contour to the point (x,y).
       *
       * @param x The x-coordinate of the start of a new contour
       * @param y The y-coordinate of the start of a new contour
       */
      public void moveTo(float x, float y) {
          mPath.moveTo(mLastX = x, mLastY = y);
      }
  
      /**
       * Set the beginning of the next contour relative to the last point on the
       * previous contour. If there is no previous contour, this is treated the
       * same as moveTo().
       *
       * @param dx The amount to add to the x-coordinate of the end of the
       *           previous contour, to specify the start of a new contour
       * @param dy The amount to add to the y-coordinate of the end of the
       *           previous contour, to specify the start of a new contour
       */
      public void rMoveTo(float dx, float dy) {
          dx += mLastX;
          dy += mLastY;
          mPath.moveTo(mLastX = dx, mLastY = dy);
      }
  
      /**
       * Add a line from the last point to the specified point (x,y).
       * If no moveTo() call has been made for this contour, the first point is
       * automatically set to (0,0).
       *
       * @param x The x-coordinate of the end of a line
       * @param y The y-coordinate of the end of a line
       */
      public void lineTo(float x, float y) {
          if (!hasPoints()) {
              mPath.moveTo(mLastX = 0, mLastY = 0);
          }
          mPath.lineTo(mLastX = x, mLastY = y);
      }
  
      /**
       * Same as lineTo, but the coordinates are considered relative to the last
       * point on this contour. If there is no previous point, then a moveTo(0,0)
       * is inserted automatically.
       *
       * @param dx The amount to add to the x-coordinate of the previous point on
       *           this contour, to specify a line
       * @param dy The amount to add to the y-coordinate of the previous point on
       *           this contour, to specify a line
       */
      public void rLineTo(float dx, float dy) {
          if (!hasPoints()) {
              mPath.moveTo(mLastX = 0, mLastY = 0);
          }
  
          if (Math.abs(dx) < EPSILON && Math.abs(dy) < EPSILON) {
              // The delta is so small that this shouldn't generate a line
              return;
          }
  
          dx += mLastX;
          dy += mLastY;
          mPath.lineTo(mLastX = dx, mLastY = dy);
      }
  
      /**
       * Add a quadratic bezier from the last point, approaching control point
       * (x1,y1), and ending at (x2,y2). If no moveTo() call has been made for
       * this contour, the first point is automatically set to (0,0).
       *
       * @param x1 The x-coordinate of the control point on a quadratic curve
       * @param y1 The y-coordinate of the control point on a quadratic curve
       * @param x2 The x-coordinate of the end point on a quadratic curve
       * @param y2 The y-coordinate of the end point on a quadratic curve
       */
      public void quadTo(float x1, float y1, float x2, float y2) {
          mPath.quadTo(x1, y1, mLastX = x2, mLastY = y2);
      }
  
      /**
       * Same as quadTo, but the coordinates are considered relative to the last
       * point on this contour. If there is no previous point, then a moveTo(0,0)
       * is inserted automatically.
       *
       * @param dx1 The amount to add to the x-coordinate of the last point on
       *            this contour, for the control point of a quadratic curve
       * @param dy1 The amount to add to the y-coordinate of the last point on
       *            this contour, for the control point of a quadratic curve
       * @param dx2 The amount to add to the x-coordinate of the last point on
       *            this contour, for the end point of a quadratic curve
       * @param dy2 The amount to add to the y-coordinate of the last point on
       *            this contour, for the end point of a quadratic curve
       */
      public void rQuadTo(float dx1, float dy1, float dx2, float dy2) {
          if (!hasPoints()) {
              mPath.moveTo(mLastX = 0, mLastY = 0);
          }
          dx1 += mLastX;
          dy1 += mLastY;
          dx2 += mLastX;
          dy2 += mLastY;
          mPath.quadTo(dx1, dy1, mLastX = dx2, mLastY = dy2);
      }
  
      /**
       * Add a cubic bezier from the last point, approaching control points
       * (x1,y1) and (x2,y2), and ending at (x3,y3). If no moveTo() call has been
       * made for this contour, the first point is automatically set to (0,0).
       *
       * @param x1 The x-coordinate of the 1st control point on a cubic curve
       * @param y1 The y-coordinate of the 1st control point on a cubic curve
       * @param x2 The x-coordinate of the 2nd control point on a cubic curve
       * @param y2 The y-coordinate of the 2nd control point on a cubic curve
       * @param x3 The x-coordinate of the end point on a cubic curve
       * @param y3 The y-coordinate of the end point on a cubic curve
       */
      public void cubicTo(float x1, float y1, float x2, float y2,
                          float x3, float y3) {
          if (!hasPoints()) {
              mPath.moveTo(0, 0);
          }
          mPath.curveTo(x1, y1, x2, y2, mLastX = x3, mLastY = y3);
      }
  
      /**
       * Same as cubicTo, but the coordinates are considered relative to the
       * current point on this contour. If there is no previous point, then a
       * moveTo(0,0) is inserted automatically.
       */
      public void rCubicTo(float dx1, float dy1, float dx2, float dy2,
                           float dx3, float dy3) {
          if (!hasPoints()) {
              mPath.moveTo(mLastX = 0, mLastY = 0);
          }
          dx1 += mLastX;
          dy1 += mLastY;
          dx2 += mLastX;
          dy2 += mLastY;
          dx3 += mLastX;
          dy3 += mLastY;
          mPath.curveTo(dx1, dy1, dx2, dy2, mLastX = dx3, mLastY = dy3);
      }
  
      /**
       * Append the specified arc to the path as a new contour. If the start of
       * the path is different from the path's current last point, then an
       * automatic lineTo() is added to connect the current contour to the
       * start of the arc. However, if the path is empty, then we call moveTo()
       * with the first point of the arc. The sweep angle is tread mod 360.
       *
       * @param left        The left of oval defining shape and size of the arc
       * @param top         The top of oval defining shape and size of the arc
       * @param right       The right of oval defining shape and size of the arc
       * @param bottom      The bottom of oval defining shape and size of the arc
       * @param startAngle  Starting angle (in degrees) where the arc begins
       * @param sweepAngle  Sweep angle (in degrees) measured clockwise, treated
       *                    mod 360.
       * @param forceMoveTo If true, always begin a new contour with the arc
       */
      public void arcTo(float left, float top, float right, float bottom, float startAngle,
              float sweepAngle,
              boolean forceMoveTo) {
          Arc2D arc = new Arc2D.Float(left, top, right - left, bottom - top, -startAngle,
                  -sweepAngle, Arc2D.OPEN);
          mPath.append(arc, true /*connect*/);
  
          resetLastPointFromPath();
      }
  
      /**
       * Close the current contour. If the current point is not equal to the
       * first point of the contour, a line segment is automatically added.
       */
      public void close() {
          mPath.closePath();
      }
  
      private void resetLastPointFromPath() {
          Point2D last = mPath.getCurrentPoint();
          mLastX = (float) last.getX();
          mLastY = (float) last.getY();
      }
  
      /**
       * Add a closed rectangle contour to the path
       *
       * @param left   The left side of a rectangle to add to the path
       * @param top    The top of a rectangle to add to the path
       * @param right  The right side of a rectangle to add to the path
       * @param bottom The bottom of a rectangle to add to the path
       * @param dir    The direction to wind the rectangle's contour
       */
      public void addRect(float left, float top, float right, float bottom,
                          int dir) {
          moveTo(left, top);
  
          Direction direction = getDirection(dir);
  
          switch (direction) {
              case CW:
                  lineTo(right, top);
                  lineTo(right, bottom);
                  lineTo(left, bottom);
                  break;
              case CCW:
                  lineTo(left, bottom);
                  lineTo(right, bottom);
                  lineTo(right, top);
                  break;
          }
  
          close();
  
          resetLastPointFromPath();
      }
  
      /**
       * Offset the path by (dx,dy), returning true on success
       *
       * @param dx  The amount in the X direction to offset the entire path
       * @param dy  The amount in the Y direction to offset the entire path
       */
      public void offset(float dx, float dy) {
          GeneralPath newPath = new GeneralPath();
  
          PathIterator iterator = mPath.getPathIterator(new AffineTransform(0, 0, dx, 0, 0, dy));
  
          newPath.append(iterator, false /*connect*/);
          mPath = newPath;
      }
  
      /**
       * Transform the points in this path by matrix, and write the answer
       * into dst. If dst is null, then the the original path is modified.
       *
       * @param matrix The matrix to apply to the path
       * @param dst    The transformed path is written here. If dst is null,
       *               then the the original path is modified
       */
      public void transform(Matrix_Delegate matrix, Path_Delegate dst) {
          if (matrix.hasPerspective()) {
              assert false;
              Bridge.getLog().fidelityWarning(LayoutLog.TAG_MATRIX_AFFINE,
                      "android.graphics.Path#transform() only " +
                      "supports affine transformations.", null, null /*data*/);
          }
  
          GeneralPath newPath = new GeneralPath();
  
          PathIterator iterator = mPath.getPathIterator(matrix.getAffineTransform());
  
          newPath.append(iterator, false /*connect*/);
  
          if (dst != null) {
              dst.mPath = newPath;
          } else {
              mPath = newPath;
          }
      }
  }
  