blob: d3af837ffd293a9b7b9624811a255011d0141dce [file] [log] [blame]
Deepanshu Gupta491523d2015-10-06 17:56:37 -07001/*
2 * Copyright (C) 2015 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package android.util;
18
19import com.android.ide.common.rendering.api.LayoutLog;
20import com.android.layoutlib.bridge.Bridge;
21import com.android.layoutlib.bridge.impl.DelegateManager;
22import com.android.tools.layoutlib.annotations.LayoutlibDelegate;
23
24import android.annotation.NonNull;
25import android.graphics.Path_Delegate;
26
27import java.awt.geom.Path2D;
28import java.util.ArrayList;
29import java.util.Arrays;
30import java.util.logging.Level;
31import java.util.logging.Logger;
32
33/**
34 * Delegate that provides implementation for native methods in {@link android.util.PathParser}
35 * <p/>
36 * Through the layoutlib_create tool, selected methods of PathParser have been replaced by calls to
37 * methods of the same name in this delegate class.
38 *
39 * Most of the code has been taken from the implementation in
40 * {@code tools/base/sdk-common/src/main/java/com/android/ide/common/vectordrawable/PathParser.java}
41 * revision be6fe89a3b686db5a75e7e692a148699973957f3
42 */
43public class PathParser_Delegate {
44
45 private static final Logger LOGGER = Logger.getLogger("PathParser");
46
47 // ---- Builder delegate manager ----
48 private static final DelegateManager<PathParser_Delegate> sManager =
49 new DelegateManager<PathParser_Delegate>(PathParser_Delegate.class);
50
51 // ---- delegate data ----
52 @NonNull
53 private PathDataNode[] mPathDataNodes;
54
55 private PathParser_Delegate(@NonNull PathDataNode[] nodes) {
56 mPathDataNodes = nodes;
57 }
58
59 @LayoutlibDelegate
60 /*package*/ static boolean nParseStringForPath(long pathPtr, @NonNull String pathString, int
61 stringLength) {
62 Path_Delegate path_delegate = Path_Delegate.getDelegate(pathPtr);
63 if (path_delegate == null) {
64 return false;
65 }
66 assert pathString.length() == stringLength;
67 PathDataNode.nodesToPath(createNodesFromPathData(pathString), path_delegate.getJavaShape());
68 return true;
69 }
70
71 @LayoutlibDelegate
72 /*package*/ static void nCreatePathFromPathData(long outPathPtr, long pathData) {
73 Path_Delegate path_delegate = Path_Delegate.getDelegate(outPathPtr);
74 PathParser_Delegate source = sManager.getDelegate(outPathPtr);
75 if (source == null || path_delegate == null) {
76 return;
77 }
78 PathDataNode.nodesToPath(source.mPathDataNodes, path_delegate.getJavaShape());
79 }
80
81 @LayoutlibDelegate
82 /*package*/ static long nCreateEmptyPathData() {
83 PathParser_Delegate newDelegate = new PathParser_Delegate(new PathDataNode[0]);
84 return sManager.addNewDelegate(newDelegate);
85 }
86
87 @LayoutlibDelegate
88 /*package*/ static long nCreatePathData(long nativePtr) {
89 PathParser_Delegate source = sManager.getDelegate(nativePtr);
90 if (source == null) {
91 return 0;
92 }
93 PathParser_Delegate dest = new PathParser_Delegate(deepCopyNodes(source.mPathDataNodes));
94 return sManager.addNewDelegate(dest);
95 }
96
97 @LayoutlibDelegate
98 /*package*/ static long nCreatePathDataFromString(@NonNull String pathString,
99 int stringLength) {
100 assert pathString.length() == stringLength : "Inconsistent path string length.";
101 PathDataNode[] nodes = createNodesFromPathData(pathString);
102 PathParser_Delegate delegate = new PathParser_Delegate(nodes);
103 return sManager.addNewDelegate(delegate);
104
105 }
106
107 @LayoutlibDelegate
108 /*package*/ static boolean nInterpolatePathData(long outDataPtr, long fromDataPtr,
109 long toDataPtr, float fraction) {
110 PathParser_Delegate out = sManager.getDelegate(outDataPtr);
111 PathParser_Delegate from = sManager.getDelegate(fromDataPtr);
112 PathParser_Delegate to = sManager.getDelegate(toDataPtr);
113 if (out == null || from == null || to == null) {
114 return false;
115 }
116 int length = from.mPathDataNodes.length;
117 if (length != to.mPathDataNodes.length) {
118 Bridge.getLog().error(LayoutLog.TAG_BROKEN,
119 "Cannot interpolate path data with different lengths (from " + length + " to " +
120 to.mPathDataNodes.length + ").", null);
121 return false;
122 }
123 if (out.mPathDataNodes.length != length) {
124 out.mPathDataNodes = new PathDataNode[length];
125 }
126 for (int i = 0; i < length; i++) {
127 out.mPathDataNodes[i].interpolatePathDataNode(from.mPathDataNodes[i],
128 to.mPathDataNodes[i], fraction);
129 }
130 return true;
131 }
132
133 @LayoutlibDelegate
134 /*package*/ static void nFinalize(long nativePtr) {
135 sManager.removeJavaReferenceFor(nativePtr);
136 }
137
138 @LayoutlibDelegate
139 /*package*/ static boolean nCanMorph(long fromDataPtr, long toDataPtr) {
140 Bridge.getLog().fidelityWarning(LayoutLog.TAG_UNSUPPORTED, "morphing path data isn't " +
141 "supported", null, null);
142 return false;
143 }
144
145 @LayoutlibDelegate
146 /*package*/ static void nSetPathData(long outDataPtr, long fromDataPtr) {
147 PathParser_Delegate out = sManager.getDelegate(outDataPtr);
148 PathParser_Delegate from = sManager.getDelegate(fromDataPtr);
149 if (from == null || out == null) {
150 return;
151 }
152 out.mPathDataNodes = deepCopyNodes(from.mPathDataNodes);
153 }
154
155 /**
156 * @param pathData The string representing a path, the same as "d" string in svg file.
157 *
158 * @return an array of the PathDataNode.
159 */
160 @NonNull
161 private static PathDataNode[] createNodesFromPathData(@NonNull String pathData) {
162 int start = 0;
163 int end = 1;
164
165 ArrayList<PathDataNode> list = new ArrayList<PathDataNode>();
166 while (end < pathData.length()) {
167 end = nextStart(pathData, end);
168 String s = pathData.substring(start, end).trim();
169 if (s.length() > 0) {
170 float[] val = getFloats(s);
171 addNode(list, s.charAt(0), val);
172 }
173
174 start = end;
175 end++;
176 }
177 if ((end - start) == 1 && start < pathData.length()) {
178 addNode(list, pathData.charAt(start), new float[0]);
179 }
180 return list.toArray(new PathDataNode[list.size()]);
181 }
182
183 /**
184 * @param source The array of PathDataNode to be duplicated.
185 *
186 * @return a deep copy of the <code>source</code>.
187 */
188 @NonNull
189 private static PathDataNode[] deepCopyNodes(@NonNull PathDataNode[] source) {
190 PathDataNode[] copy = new PathDataNode[source.length];
191 for (int i = 0; i < source.length; i++) {
192 copy[i] = new PathDataNode(source[i]);
193 }
194 return copy;
195 }
196
197 private static int nextStart(@NonNull String s, int end) {
198 char c;
199
200 while (end < s.length()) {
201 c = s.charAt(end);
202 // Note that 'e' or 'E' are not valid path commands, but could be
203 // used for floating point numbers' scientific notation.
204 // Therefore, when searching for next command, we should ignore 'e'
205 // and 'E'.
206 if ((((c - 'A') * (c - 'Z') <= 0) || ((c - 'a') * (c - 'z') <= 0))
207 && c != 'e' && c != 'E') {
208 return end;
209 }
210 end++;
211 }
212 return end;
213 }
214
215 /**
216 * Calculate the position of the next comma or space or negative sign
217 *
218 * @param s the string to search
219 * @param start the position to start searching
220 * @param result the result of the extraction, including the position of the the starting
221 * position of next number, whether it is ending with a '-'.
222 */
223 private static void extract(@NonNull String s, int start, @NonNull ExtractFloatResult result) {
224 // Now looking for ' ', ',', '.' or '-' from the start.
225 int currentIndex = start;
226 boolean foundSeparator = false;
227 result.mEndWithNegOrDot = false;
228 boolean secondDot = false;
229 boolean isExponential = false;
230 for (; currentIndex < s.length(); currentIndex++) {
231 boolean isPrevExponential = isExponential;
232 isExponential = false;
233 char currentChar = s.charAt(currentIndex);
234 switch (currentChar) {
235 case ' ':
236 case ',':
237 foundSeparator = true;
238 break;
239 case '-':
240 // The negative sign following a 'e' or 'E' is not a separator.
241 if (currentIndex != start && !isPrevExponential) {
242 foundSeparator = true;
243 result.mEndWithNegOrDot = true;
244 }
245 break;
246 case '.':
247 if (!secondDot) {
248 secondDot = true;
249 } else {
250 // This is the second dot, and it is considered as a separator.
251 foundSeparator = true;
252 result.mEndWithNegOrDot = true;
253 }
254 break;
255 case 'e':
256 case 'E':
257 isExponential = true;
258 break;
259 }
260 if (foundSeparator) {
261 break;
262 }
263 }
264 // When there is nothing found, then we put the end position to the end
265 // of the string.
266 result.mEndPosition = currentIndex;
267 }
268
269 /**
270 * Parse the floats in the string. This is an optimized version of
271 * parseFloat(s.split(",|\\s"));
272 *
273 * @param s the string containing a command and list of floats
274 *
275 * @return array of floats
276 */
277 @NonNull
278 private static float[] getFloats(@NonNull String s) {
279 if (s.charAt(0) == 'z' || s.charAt(0) == 'Z') {
280 return new float[0];
281 }
282 try {
283 float[] results = new float[s.length()];
284 int count = 0;
285 int startPosition = 1;
286 int endPosition;
287
288 ExtractFloatResult result = new ExtractFloatResult();
289 int totalLength = s.length();
290
291 // The startPosition should always be the first character of the
292 // current number, and endPosition is the character after the current
293 // number.
294 while (startPosition < totalLength) {
295 extract(s, startPosition, result);
296 endPosition = result.mEndPosition;
297
298 if (startPosition < endPosition) {
299 results[count++] = Float.parseFloat(
300 s.substring(startPosition, endPosition));
301 }
302
303 if (result.mEndWithNegOrDot) {
304 // Keep the '-' or '.' sign with next number.
305 startPosition = endPosition;
306 } else {
307 startPosition = endPosition + 1;
308 }
309 }
310 return Arrays.copyOf(results, count);
311 } catch (NumberFormatException e) {
312 throw new RuntimeException("error in parsing \"" + s + "\"", e);
313 }
314 }
315
316
317 private static void addNode(@NonNull ArrayList<PathDataNode> list, char cmd,
318 @NonNull float[] val) {
319 list.add(new PathDataNode(cmd, val));
320 }
321
322 private static class ExtractFloatResult {
323 // We need to return the position of the next separator and whether the
324 // next float starts with a '-' or a '.'.
325 private int mEndPosition;
326 private boolean mEndWithNegOrDot;
327 }
328
329 /**
330 * Each PathDataNode represents one command in the "d" attribute of the svg file. An array of
331 * PathDataNode can represent the whole "d" attribute.
332 */
333 private static class PathDataNode {
334 private char mType;
335 @NonNull
336 private float[] mParams;
337
338 private PathDataNode(char type, @NonNull float[] params) {
339 mType = type;
340 mParams = params;
341 }
342
343 public char getType() {
344 return mType;
345 }
346
347 @NonNull
348 public float[] getParams() {
349 return mParams;
350 }
351
352 private PathDataNode(@NonNull PathDataNode n) {
353 mType = n.mType;
354 mParams = Arrays.copyOf(n.mParams, n.mParams.length);
355 }
356
357 /**
358 * Convert an array of PathDataNode to Path.
359 *
360 * @param node The source array of PathDataNode.
361 * @param path The target Path object.
362 */
363 private static void nodesToPath(@NonNull PathDataNode[] node, @NonNull Path2D path) {
364 float[] current = new float[6];
365 char previousCommand = 'm';
366 //noinspection ForLoopReplaceableByForEach
367 for (int i = 0; i < node.length; i++) {
368 addCommand(path, current, previousCommand, node[i].mType, node[i].mParams);
369 previousCommand = node[i].mType;
370 }
371 }
372
373 /**
374 * The current PathDataNode will be interpolated between the <code>nodeFrom</code> and
375 * <code>nodeTo</code> according to the <code>fraction</code>.
376 *
377 * @param nodeFrom The start value as a PathDataNode.
378 * @param nodeTo The end value as a PathDataNode
379 * @param fraction The fraction to interpolate.
380 */
381 private void interpolatePathDataNode(@NonNull PathDataNode nodeFrom,
382 @NonNull PathDataNode nodeTo, float fraction) {
383 for (int i = 0; i < nodeFrom.mParams.length; i++) {
384 mParams[i] = nodeFrom.mParams[i] * (1 - fraction)
385 + nodeTo.mParams[i] * fraction;
386 }
387 }
388
389 @SuppressWarnings("PointlessArithmeticExpression")
390 private static void addCommand(@NonNull Path2D path, float[] current, char cmd,
391 char lastCmd, @NonNull float[] val) {
392
393 int incr = 2;
394
395 float cx = current[0];
396 float cy = current[1];
397 float cpx = current[2];
398 float cpy = current[3];
399 float loopX = current[4];
400 float loopY = current[5];
401
402 switch (cmd) {
403 case 'z':
404 case 'Z':
405 path.closePath();
406 cx = loopX;
407 cy = loopY;
408 case 'm':
409 case 'M':
410 case 'l':
411 case 'L':
412 case 't':
413 case 'T':
414 incr = 2;
415 break;
416 case 'h':
417 case 'H':
418 case 'v':
419 case 'V':
420 incr = 1;
421 break;
422 case 'c':
423 case 'C':
424 incr = 6;
425 break;
426 case 's':
427 case 'S':
428 case 'q':
429 case 'Q':
430 incr = 4;
431 break;
432 case 'a':
433 case 'A':
434 incr = 7;
435 }
436
437 for (int k = 0; k < val.length; k += incr) {
438 boolean reflectCtrl;
439 float tempReflectedX, tempReflectedY;
440
441 switch (cmd) {
442 case 'm':
443 cx += val[k + 0];
444 cy += val[k + 1];
445 if (k > 0) {
446 // According to the spec, if a moveto is followed by multiple
447 // pairs of coordinates, the subsequent pairs are treated as
448 // implicit lineto commands.
449 path.lineTo(cx, cy);
450 } else {
451 path.moveTo(cx, cy);
452 loopX = cx;
453 loopY = cy;
454 }
455 break;
456 case 'M':
457 cx = val[k + 0];
458 cy = val[k + 1];
459 if (k > 0) {
460 // According to the spec, if a moveto is followed by multiple
461 // pairs of coordinates, the subsequent pairs are treated as
462 // implicit lineto commands.
463 path.lineTo(cx, cy);
464 } else {
465 path.moveTo(cx, cy);
466 loopX = cx;
467 loopY = cy;
468 }
469 break;
470 case 'l':
471 cx += val[k + 0];
472 cy += val[k + 1];
473 path.lineTo(cx, cy);
474 break;
475 case 'L':
476 cx = val[k + 0];
477 cy = val[k + 1];
478 path.lineTo(cx, cy);
479 break;
480 case 'z':
481 case 'Z':
482 path.closePath();
483 cx = loopX;
484 cy = loopY;
485 break;
486 case 'h':
487 cx += val[k + 0];
488 path.lineTo(cx, cy);
489 break;
490 case 'H':
491 path.lineTo(val[k + 0], cy);
492 cx = val[k + 0];
493 break;
494 case 'v':
495 cy += val[k + 0];
496 path.lineTo(cx, cy);
497 break;
498 case 'V':
499 path.lineTo(cx, val[k + 0]);
500 cy = val[k + 0];
501 break;
502 case 'c':
503 path.curveTo(cx + val[k + 0], cy + val[k + 1], cx + val[k + 2],
504 cy + val[k + 3], cx + val[k + 4], cy + val[k + 5]);
505 cpx = cx + val[k + 2];
506 cpy = cy + val[k + 3];
507 cx += val[k + 4];
508 cy += val[k + 5];
509 break;
510 case 'C':
511 path.curveTo(val[k + 0], val[k + 1], val[k + 2], val[k + 3],
512 val[k + 4], val[k + 5]);
513 cx = val[k + 4];
514 cy = val[k + 5];
515 cpx = val[k + 2];
516 cpy = val[k + 3];
517 break;
518 case 's':
519 reflectCtrl = (lastCmd == 'c' || lastCmd == 's' || lastCmd == 'C' ||
520 lastCmd == 'S');
521 path.curveTo(reflectCtrl ? 2 * cx - cpx : cx, reflectCtrl ? 2
522 * cy - cpy : cy, cx + val[k + 0], cy + val[k + 1], cx
523 + val[k + 2], cy + val[k + 3]);
524
525 cpx = cx + val[k + 0];
526 cpy = cy + val[k + 1];
527 cx += val[k + 2];
528 cy += val[k + 3];
529 break;
530 case 'S':
531 reflectCtrl = (lastCmd == 'c' || lastCmd == 's' || lastCmd == 'C' ||
532 lastCmd == 'S');
533 path.curveTo(reflectCtrl ? 2 * cx - cpx : cx, reflectCtrl ? 2
534 * cy - cpy : cy, val[k + 0], val[k + 1], val[k + 2],
535 val[k + 3]);
536 cpx = (val[k + 0]);
537 cpy = (val[k + 1]);
538 cx = val[k + 2];
539 cy = val[k + 3];
540 break;
541 case 'q':
542 path.quadTo(cx + val[k + 0], cy + val[k + 1], cx + val[k + 2],
543 cy + val[k + 3]);
544 cpx = cx + val[k + 0];
545 cpy = cy + val[k + 1];
546 // Note that we have to update cpx first, since cx will be updated here.
547 cx += val[k + 2];
548 cy += val[k + 3];
549 break;
550 case 'Q':
551 path.quadTo(val[k + 0], val[k + 1], val[k + 2], val[k + 3]);
552 cx = val[k + 2];
553 cy = val[k + 3];
554 cpx = val[k + 0];
555 cpy = val[k + 1];
556 break;
557 case 't':
558 reflectCtrl = (lastCmd == 'q' || lastCmd == 't' || lastCmd == 'Q' ||
559 lastCmd == 'T');
560 tempReflectedX = reflectCtrl ? 2 * cx - cpx : cx;
561 tempReflectedY = reflectCtrl ? 2 * cy - cpy : cy;
562 path.quadTo(tempReflectedX, tempReflectedY, cx + val[k + 0],
563 cy + val[k + 1]);
564 cpx = tempReflectedX;
565 cpy = tempReflectedY;
566 cx += val[k + 0];
567 cy += val[k + 1];
568 break;
569 case 'T':
570 reflectCtrl = (lastCmd == 'q' || lastCmd == 't' || lastCmd == 'Q' ||
571 lastCmd == 'T');
572 tempReflectedX = reflectCtrl ? 2 * cx - cpx : cx;
573 tempReflectedY = reflectCtrl ? 2 * cy - cpy : cy;
574 path.quadTo(tempReflectedX, tempReflectedY, val[k + 0], val[k + 1]);
575 cx = val[k + 0];
576 cy = val[k + 1];
577 cpx = tempReflectedX;
578 cpy = tempReflectedY;
579 break;
580 case 'a':
581 // (rx ry x-axis-rotation large-arc-flag sweep-flag x y)
582 drawArc(path, cx, cy, val[k + 5] + cx, val[k + 6] + cy,
583 val[k + 0], val[k + 1], val[k + 2], val[k + 3] != 0,
584 val[k + 4] != 0);
585 cx += val[k + 5];
586 cy += val[k + 6];
587 cpx = cx;
588 cpy = cy;
589
590 break;
591 case 'A':
592 drawArc(path, cx, cy, val[k + 5], val[k + 6], val[k + 0],
593 val[k + 1], val[k + 2], val[k + 3] != 0,
594 val[k + 4] != 0);
595 cx = val[k + 5];
596 cy = val[k + 6];
597 cpx = cx;
598 cpy = cy;
599 break;
600
601 }
602 lastCmd = cmd;
603 }
604 current[0] = cx;
605 current[1] = cy;
606 current[2] = cpx;
607 current[3] = cpy;
608 current[4] = loopX;
609 current[5] = loopY;
610
611 }
612
613 private static void drawArc(@NonNull Path2D p, float x0, float y0, float x1,
614 float y1, float a, float b, float theta, boolean isMoreThanHalf,
615 boolean isPositiveArc) {
616
617 LOGGER.log(Level.FINE, "(" + x0 + "," + y0 + ")-(" + x1 + "," + y1
618 + ") {" + a + " " + b + "}");
619 /* Convert rotation angle from degrees to radians */
620 double thetaD = theta * Math.PI / 180.0f;
621 /* Pre-compute rotation matrix entries */
622 double cosTheta = Math.cos(thetaD);
623 double sinTheta = Math.sin(thetaD);
624 /* Transform (x0, y0) and (x1, y1) into unit space */
625 /* using (inverse) rotation, followed by (inverse) scale */
626 double x0p = (x0 * cosTheta + y0 * sinTheta) / a;
627 double y0p = (-x0 * sinTheta + y0 * cosTheta) / b;
628 double x1p = (x1 * cosTheta + y1 * sinTheta) / a;
629 double y1p = (-x1 * sinTheta + y1 * cosTheta) / b;
630 LOGGER.log(Level.FINE, "unit space (" + x0p + "," + y0p + ")-(" + x1p
631 + "," + y1p + ")");
632 /* Compute differences and averages */
633 double dx = x0p - x1p;
634 double dy = y0p - y1p;
635 double xm = (x0p + x1p) / 2;
636 double ym = (y0p + y1p) / 2;
637 /* Solve for intersecting unit circles */
638 double dsq = dx * dx + dy * dy;
639 if (dsq == 0.0) {
640 LOGGER.log(Level.FINE, " Points are coincident");
641 return; /* Points are coincident */
642 }
643 double disc = 1.0 / dsq - 1.0 / 4.0;
644 if (disc < 0.0) {
645 LOGGER.log(Level.FINE, "Points are too far apart " + dsq);
646 float adjust = (float) (Math.sqrt(dsq) / 1.99999);
647 drawArc(p, x0, y0, x1, y1, a * adjust, b * adjust, theta,
648 isMoreThanHalf, isPositiveArc);
649 return; /* Points are too far apart */
650 }
651 double s = Math.sqrt(disc);
652 double sdx = s * dx;
653 double sdy = s * dy;
654 double cx;
655 double cy;
656 if (isMoreThanHalf == isPositiveArc) {
657 cx = xm - sdy;
658 cy = ym + sdx;
659 } else {
660 cx = xm + sdy;
661 cy = ym - sdx;
662 }
663
664 double eta0 = Math.atan2((y0p - cy), (x0p - cx));
665 LOGGER.log(Level.FINE, "eta0 = Math.atan2( " + (y0p - cy) + " , "
666 + (x0p - cx) + ") = " + Math.toDegrees(eta0));
667
668 double eta1 = Math.atan2((y1p - cy), (x1p - cx));
669 LOGGER.log(Level.FINE, "eta1 = Math.atan2( " + (y1p - cy) + " , "
670 + (x1p - cx) + ") = " + Math.toDegrees(eta1));
671 double sweep = (eta1 - eta0);
672 if (isPositiveArc != (sweep >= 0)) {
673 if (sweep > 0) {
674 sweep -= 2 * Math.PI;
675 } else {
676 sweep += 2 * Math.PI;
677 }
678 }
679
680 cx *= a;
681 cy *= b;
682 double tcx = cx;
683 cx = cx * cosTheta - cy * sinTheta;
684 cy = tcx * sinTheta + cy * cosTheta;
685 LOGGER.log(
686 Level.FINE,
687 "cx, cy, a, b, x0, y0, thetaD, eta0, sweep = " + cx + " , "
688 + cy + " , " + a + " , " + b + " , " + x0 + " , " + y0
689 + " , " + Math.toDegrees(thetaD) + " , "
690 + Math.toDegrees(eta0) + " , " + Math.toDegrees(sweep));
691
692 arcToBezier(p, cx, cy, a, b, x0, y0, thetaD, eta0, sweep);
693 }
694
695 /**
696 * Converts an arc to cubic Bezier segments and records them in p.
697 *
698 * @param p The target for the cubic Bezier segments
699 * @param cx The x coordinate center of the ellipse
700 * @param cy The y coordinate center of the ellipse
701 * @param a The radius of the ellipse in the horizontal direction
702 * @param b The radius of the ellipse in the vertical direction
703 * @param e1x E(eta1) x coordinate of the starting point of the arc
704 * @param e1y E(eta2) y coordinate of the starting point of the arc
705 * @param theta The angle that the ellipse bounding rectangle makes with the horizontal
706 * plane
707 * @param start The start angle of the arc on the ellipse
708 * @param sweep The angle (positive or negative) of the sweep of the arc on the ellipse
709 */
710 private static void arcToBezier(@NonNull Path2D p, double cx, double cy, double a,
711 double b, double e1x, double e1y, double theta, double start,
712 double sweep) {
713 // Taken from equations at:
714 // http://spaceroots.org/documents/ellipse/node8.html
715 // and http://www.spaceroots.org/documents/ellipse/node22.html
716 // Maximum of 45 degrees per cubic Bezier segment
717 int numSegments = (int) Math.ceil(Math.abs(sweep * 4 / Math.PI));
718
719
720 double eta1 = start;
721 double cosTheta = Math.cos(theta);
722 double sinTheta = Math.sin(theta);
723 double cosEta1 = Math.cos(eta1);
724 double sinEta1 = Math.sin(eta1);
725 double ep1x = (-a * cosTheta * sinEta1) - (b * sinTheta * cosEta1);
726 double ep1y = (-a * sinTheta * sinEta1) + (b * cosTheta * cosEta1);
727
728 double anglePerSegment = sweep / numSegments;
729 for (int i = 0; i < numSegments; i++) {
730 double eta2 = eta1 + anglePerSegment;
731 double sinEta2 = Math.sin(eta2);
732 double cosEta2 = Math.cos(eta2);
733 double e2x = cx + (a * cosTheta * cosEta2)
734 - (b * sinTheta * sinEta2);
735 double e2y = cy + (a * sinTheta * cosEta2)
736 + (b * cosTheta * sinEta2);
737 double ep2x = -a * cosTheta * sinEta2 - b * sinTheta * cosEta2;
738 double ep2y = -a * sinTheta * sinEta2 + b * cosTheta * cosEta2;
739 double tanDiff2 = Math.tan((eta2 - eta1) / 2);
740 double alpha = Math.sin(eta2 - eta1)
741 * (Math.sqrt(4 + (3 * tanDiff2 * tanDiff2)) - 1) / 3;
742 double q1x = e1x + alpha * ep1x;
743 double q1y = e1y + alpha * ep1y;
744 double q2x = e2x - alpha * ep2x;
745 double q2y = e2y - alpha * ep2y;
746
747 p.curveTo((float) q1x, (float) q1y, (float) q2x, (float) q2y,
748 (float) e2x, (float) e2y);
749 eta1 = eta2;
750 e1x = e2x;
751 e1y = e2y;
752 ep1x = ep2x;
753 ep1y = ep2y;
754 }
755 }
756 }
757}