1 /*
<lambda>null2  * Copyright 2019 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 
17 package androidx.compose.ui.graphics.vector
18 
19 import androidx.compose.ui.graphics.Path
20 import androidx.compose.ui.graphics.vector.PathNode.ArcTo
21 import androidx.compose.ui.graphics.vector.PathNode.Close
22 import androidx.compose.ui.graphics.vector.PathNode.CurveTo
23 import androidx.compose.ui.graphics.vector.PathNode.HorizontalTo
24 import androidx.compose.ui.graphics.vector.PathNode.LineTo
25 import androidx.compose.ui.graphics.vector.PathNode.MoveTo
26 import androidx.compose.ui.graphics.vector.PathNode.QuadTo
27 import androidx.compose.ui.graphics.vector.PathNode.ReflectiveCurveTo
28 import androidx.compose.ui.graphics.vector.PathNode.ReflectiveQuadTo
29 import androidx.compose.ui.graphics.vector.PathNode.RelativeArcTo
30 import androidx.compose.ui.graphics.vector.PathNode.RelativeCurveTo
31 import androidx.compose.ui.graphics.vector.PathNode.RelativeHorizontalTo
32 import androidx.compose.ui.graphics.vector.PathNode.RelativeLineTo
33 import androidx.compose.ui.graphics.vector.PathNode.RelativeMoveTo
34 import androidx.compose.ui.graphics.vector.PathNode.RelativeQuadTo
35 import androidx.compose.ui.graphics.vector.PathNode.RelativeReflectiveCurveTo
36 import androidx.compose.ui.graphics.vector.PathNode.RelativeReflectiveQuadTo
37 import androidx.compose.ui.graphics.vector.PathNode.RelativeVerticalTo
38 import androidx.compose.ui.graphics.vector.PathNode.VerticalTo
39 import androidx.compose.ui.util.fastForEach
40 import kotlin.math.PI
41 import kotlin.math.abs
42 import kotlin.math.atan2
43 import kotlin.math.ceil
44 import kotlin.math.cos
45 import kotlin.math.sin
46 import kotlin.math.sqrt
47 import kotlin.math.tan
48 
49 internal val EmptyArray = FloatArray(0)
50 
51 class PathParser {
52     private var nodes: ArrayList<PathNode>? = null
53     private var nodeData = FloatArray(64)
54 
55     /** Clears the collection of [PathNode] stored in this parser and returned by [toNodes]. */
56     fun clear() {
57         nodes?.clear()
58     }
59 
60     /**
61      * Parses the SVG path string to extract [PathNode] instances for each path instruction
62      * (`lineTo`, `moveTo`, etc.). The [PathNode] are stored in this parser's internal list of nodes
63      * which can be queried by calling [toNodes]. Calling this method replaces any existing content
64      * in the current nodes list.
65      */
66     fun parsePathString(pathData: String): PathParser {
67         var dstNodes = nodes
68         if (dstNodes == null) {
69             dstNodes = ArrayList()
70             nodes = dstNodes
71         } else {
72             dstNodes.clear()
73         }
74         pathStringToNodes(pathData, dstNodes)
75         return this
76     }
77 
78     /**
79      * Parses the path string and adds the corresponding [PathNode] instances to the specified
80      * [nodes] collection. This method returns [nodes].
81      */
82     @Suppress("ConcreteCollection")
83     fun pathStringToNodes(
84         pathData: String,
85         @Suppress("ConcreteCollection") nodes: ArrayList<PathNode> = ArrayList()
86     ): ArrayList<PathNode> {
87         var start = 0
88         var end = pathData.length
89 
90         // Holds the floats that describe the points for each command
91         var dataCount = 0
92 
93         // Trim leading and trailing tabs and spaces
94         while (start < end && pathData[start] <= ' ') start++
95         while (end > start && pathData[end - 1] <= ' ') end--
96 
97         var index = start
98         while (index < end) {
99             var c: Char
100             var command = '\u0000'
101 
102             // Look for the next command:
103             //     A character that's a lower or upper case letter, but not e or E as those can be
104             //      part of a float literal (e.g. 1.23e-3).
105             do {
106                 c = pathData[index++]
107                 val lowerChar = c.code or 0x20
108                 if ((lowerChar - 'a'.code) * (lowerChar - 'z'.code) <= 0 && lowerChar != 'e'.code) {
109                     command = c
110                     break
111                 }
112             } while (index < end)
113 
114             // We found a command
115             if (command != '\u0000') {
116                 // If the command is a close command (z or Z), we don't need to extract floats,
117                 // and can proceed to the next command instead
118                 if ((command.code or 0x20) != 'z'.code) {
119                     dataCount = 0
120 
121                     do {
122                         // Skip any whitespace
123                         while (index < end && pathData[index] <= ' ') index++
124 
125                         // Find the next float and add it to the data array if we got a valid result
126                         // An invalid result could be a malformed float, or simply that we reached
127                         // the end of the list of floats
128                         val result = nextFloat(pathData, index, end)
129                         index = result.index
130                         val value = result.floatValue
131 
132                         // If the number is not a NaN
133                         if (!value.isNaN()) {
134                             nodeData[dataCount++] = value
135                             resizeNodeData(dataCount)
136                         }
137 
138                         // Skip any commas
139                         while (index < end && pathData[index] == ',') index++
140                     } while (index < end && !value.isNaN())
141                 }
142 
143                 command.addPathNodes(nodes, nodeData, dataCount)
144             }
145         }
146 
147         return nodes
148     }
149 
150     @Suppress("NOTHING_TO_INLINE")
151     private inline fun resizeNodeData(dataCount: Int) {
152         if (dataCount >= nodeData.size) {
153             val src = nodeData
154             nodeData = FloatArray(dataCount * 2)
155             src.copyInto(nodeData, 0, 0, src.size)
156         }
157     }
158 
159     /**
160      * Adds the list of [PathNode] [nodes] to this parser's internal list of [PathNode]. The
161      * resulting list can be obtained by calling [toNodes].
162      */
163     fun addPathNodes(nodes: List<PathNode>): PathParser {
164         var dstNodes = this.nodes
165         if (dstNodes == null) {
166             dstNodes = ArrayList()
167             this.nodes = dstNodes
168         }
169         dstNodes.addAll(nodes)
170         return this
171     }
172 
173     /**
174      * Returns this parser's list of [PathNode]. Note: this function does not return a copy of the
175      * list. The caller should make a copy when appropriate.
176      */
177     fun toNodes(): List<PathNode> = nodes ?: emptyList()
178 
179     /**
180      * Converts this parser's list of [PathNode] instances into a [Path]. A new [Path] is returned
181      * every time this method is invoked.
182      */
183     fun toPath(target: Path = Path()) = nodes?.toPath(target) ?: Path()
184 }
185 
186 /**
187  * Converts this list of [PathNode] into a [Path] by adding the appropriate commands to the [target]
188  * path. If [target] is not specified, a new [Path] instance is created. This method returns
189  * [target] or the newly created [Path].
190  */
toPathnull191 fun List<PathNode>.toPath(target: Path = Path()): Path {
192     // Rewind unsets the fill type so reset it here
193     val fillType = target.fillType
194     target.rewind()
195     target.fillType = fillType
196 
197     var currentX = 0.0f
198     var currentY = 0.0f
199     var ctrlX = 0.0f
200     var ctrlY = 0.0f
201     var segmentX = 0.0f
202     var segmentY = 0.0f
203     var reflectiveCtrlX: Float
204     var reflectiveCtrlY: Float
205 
206     var previousNode = if (isEmpty()) Close else this[0]
207     fastForEach { node ->
208         when (node) {
209             is Close -> {
210                 currentX = segmentX
211                 currentY = segmentY
212                 ctrlX = segmentX
213                 ctrlY = segmentY
214                 target.close()
215             }
216             is RelativeMoveTo -> {
217                 currentX += node.dx
218                 currentY += node.dy
219                 target.relativeMoveTo(node.dx, node.dy)
220                 segmentX = currentX
221                 segmentY = currentY
222             }
223             is MoveTo -> {
224                 currentX = node.x
225                 currentY = node.y
226                 target.moveTo(node.x, node.y)
227                 segmentX = currentX
228                 segmentY = currentY
229             }
230             is RelativeLineTo -> {
231                 target.relativeLineTo(node.dx, node.dy)
232                 currentX += node.dx
233                 currentY += node.dy
234             }
235             is LineTo -> {
236                 target.lineTo(node.x, node.y)
237                 currentX = node.x
238                 currentY = node.y
239             }
240             is RelativeHorizontalTo -> {
241                 target.relativeLineTo(node.dx, 0.0f)
242                 currentX += node.dx
243             }
244             is HorizontalTo -> {
245                 target.lineTo(node.x, currentY)
246                 currentX = node.x
247             }
248             is RelativeVerticalTo -> {
249                 target.relativeLineTo(0.0f, node.dy)
250                 currentY += node.dy
251             }
252             is VerticalTo -> {
253                 target.lineTo(currentX, node.y)
254                 currentY = node.y
255             }
256             is RelativeCurveTo -> {
257                 target.relativeCubicTo(node.dx1, node.dy1, node.dx2, node.dy2, node.dx3, node.dy3)
258                 ctrlX = currentX + node.dx2
259                 ctrlY = currentY + node.dy2
260                 currentX += node.dx3
261                 currentY += node.dy3
262             }
263             is CurveTo -> {
264                 target.cubicTo(node.x1, node.y1, node.x2, node.y2, node.x3, node.y3)
265                 ctrlX = node.x2
266                 ctrlY = node.y2
267                 currentX = node.x3
268                 currentY = node.y3
269             }
270             is RelativeReflectiveCurveTo -> {
271                 if (previousNode.isCurve) {
272                     reflectiveCtrlX = currentX - ctrlX
273                     reflectiveCtrlY = currentY - ctrlY
274                 } else {
275                     reflectiveCtrlX = 0.0f
276                     reflectiveCtrlY = 0.0f
277                 }
278                 target.relativeCubicTo(
279                     reflectiveCtrlX,
280                     reflectiveCtrlY,
281                     node.dx1,
282                     node.dy1,
283                     node.dx2,
284                     node.dy2
285                 )
286                 ctrlX = currentX + node.dx1
287                 ctrlY = currentY + node.dy1
288                 currentX += node.dx2
289                 currentY += node.dy2
290             }
291             is ReflectiveCurveTo -> {
292                 if (previousNode.isCurve) {
293                     reflectiveCtrlX = 2 * currentX - ctrlX
294                     reflectiveCtrlY = 2 * currentY - ctrlY
295                 } else {
296                     reflectiveCtrlX = currentX
297                     reflectiveCtrlY = currentY
298                 }
299                 target.cubicTo(reflectiveCtrlX, reflectiveCtrlY, node.x1, node.y1, node.x2, node.y2)
300                 ctrlX = node.x1
301                 ctrlY = node.y1
302                 currentX = node.x2
303                 currentY = node.y2
304             }
305             is RelativeQuadTo -> {
306                 target.relativeQuadraticTo(node.dx1, node.dy1, node.dx2, node.dy2)
307                 ctrlX = currentX + node.dx1
308                 ctrlY = currentY + node.dy1
309                 currentX += node.dx2
310                 currentY += node.dy2
311             }
312             is QuadTo -> {
313                 target.quadraticTo(node.x1, node.y1, node.x2, node.y2)
314                 ctrlX = node.x1
315                 ctrlY = node.y1
316                 currentX = node.x2
317                 currentY = node.y2
318             }
319             is RelativeReflectiveQuadTo -> {
320                 if (previousNode.isQuad) {
321                     reflectiveCtrlX = currentX - ctrlX
322                     reflectiveCtrlY = currentY - ctrlY
323                 } else {
324                     reflectiveCtrlX = 0.0f
325                     reflectiveCtrlY = 0.0f
326                 }
327                 target.relativeQuadraticTo(reflectiveCtrlX, reflectiveCtrlY, node.dx, node.dy)
328                 ctrlX = currentX + reflectiveCtrlX
329                 ctrlY = currentY + reflectiveCtrlY
330                 currentX += node.dx
331                 currentY += node.dy
332             }
333             is ReflectiveQuadTo -> {
334                 if (previousNode.isQuad) {
335                     reflectiveCtrlX = 2 * currentX - ctrlX
336                     reflectiveCtrlY = 2 * currentY - ctrlY
337                 } else {
338                     reflectiveCtrlX = currentX
339                     reflectiveCtrlY = currentY
340                 }
341                 target.quadraticTo(reflectiveCtrlX, reflectiveCtrlY, node.x, node.y)
342                 ctrlX = reflectiveCtrlX
343                 ctrlY = reflectiveCtrlY
344                 currentX = node.x
345                 currentY = node.y
346             }
347             is RelativeArcTo -> {
348                 val arcStartX = node.arcStartDx + currentX
349                 val arcStartY = node.arcStartDy + currentY
350                 drawArc(
351                     target,
352                     currentX.toDouble(),
353                     currentY.toDouble(),
354                     arcStartX.toDouble(),
355                     arcStartY.toDouble(),
356                     node.horizontalEllipseRadius.toDouble(),
357                     node.verticalEllipseRadius.toDouble(),
358                     node.theta.toDouble(),
359                     node.isMoreThanHalf,
360                     node.isPositiveArc
361                 )
362                 currentX = arcStartX
363                 currentY = arcStartY
364                 ctrlX = currentX
365                 ctrlY = currentY
366             }
367             is ArcTo -> {
368                 drawArc(
369                     target,
370                     currentX.toDouble(),
371                     currentY.toDouble(),
372                     node.arcStartX.toDouble(),
373                     node.arcStartY.toDouble(),
374                     node.horizontalEllipseRadius.toDouble(),
375                     node.verticalEllipseRadius.toDouble(),
376                     node.theta.toDouble(),
377                     node.isMoreThanHalf,
378                     node.isPositiveArc
379                 )
380                 currentX = node.arcStartX
381                 currentY = node.arcStartY
382                 ctrlX = currentX
383                 ctrlY = currentY
384             }
385         }
386         previousNode = node
387     }
388     return target
389 }
390 
drawArcnull391 private fun drawArc(
392     p: Path,
393     x0: Double,
394     y0: Double,
395     x1: Double,
396     y1: Double,
397     a: Double,
398     b: Double,
399     theta: Double,
400     isMoreThanHalf: Boolean,
401     isPositiveArc: Boolean
402 ) {
403 
404     /* Convert rotation angle from degrees to radians */
405     val thetaD = theta.toRadians()
406     /* Pre-compute rotation matrix entries */
407     val cosTheta = cos(thetaD)
408     val sinTheta = sin(thetaD)
409     /* Transform (x0, y0) and (x1, y1) into unit space */
410     /* using (inverse) rotation, followed by (inverse) scale */
411     val x0p = (x0 * cosTheta + y0 * sinTheta) / a
412     val y0p = (-x0 * sinTheta + y0 * cosTheta) / b
413     val x1p = (x1 * cosTheta + y1 * sinTheta) / a
414     val y1p = (-x1 * sinTheta + y1 * cosTheta) / b
415 
416     /* Compute differences and averages */
417     val dx = x0p - x1p
418     val dy = y0p - y1p
419     val xm = (x0p + x1p) / 2
420     val ym = (y0p + y1p) / 2
421     /* Solve for intersecting unit circles */
422     val dsq = dx * dx + dy * dy
423     if (dsq == 0.0) {
424         return /* Points are coincident */
425     }
426     val disc = 1.0 / dsq - 1.0 / 4.0
427     if (disc < 0.0) {
428         val adjust = (sqrt(dsq) / 1.99999).toFloat()
429         drawArc(p, x0, y0, x1, y1, a * adjust, b * adjust, theta, isMoreThanHalf, isPositiveArc)
430         return /* Points are too far apart */
431     }
432     val s = sqrt(disc)
433     val sdx = s * dx
434     val sdy = s * dy
435     var cx: Double
436     var cy: Double
437     if (isMoreThanHalf == isPositiveArc) {
438         cx = xm - sdy
439         cy = ym + sdx
440     } else {
441         cx = xm + sdy
442         cy = ym - sdx
443     }
444 
445     val eta0 = atan2(y0p - cy, x0p - cx)
446 
447     val eta1 = atan2(y1p - cy, x1p - cx)
448 
449     var sweep = eta1 - eta0
450     if (isPositiveArc != (sweep >= 0)) {
451         if (sweep > 0) {
452             sweep -= 2 * PI
453         } else {
454             sweep += 2 * PI
455         }
456     }
457 
458     cx *= a
459     cy *= b
460     val tcx = cx
461     cx = cx * cosTheta - cy * sinTheta
462     cy = tcx * sinTheta + cy * cosTheta
463 
464     arcToBezier(p, cx, cy, a, b, x0, y0, thetaD, eta0, sweep)
465 }
466 
467 /**
468  * Converts an arc to cubic Bezier segments and records them in p.
469  *
470  * @param p The target for the cubic Bezier segments
471  * @param cx The x coordinate center of the ellipse
472  * @param cy The y coordinate center of the ellipse
473  * @param a The radius of the ellipse in the horizontal direction
474  * @param b The radius of the ellipse in the vertical direction
475  * @param e1x E(eta1) x coordinate of the starting point of the arc
476  * @param e1y E(eta2) y coordinate of the starting point of the arc
477  * @param theta The angle that the ellipse bounding rectangle makes with horizontal plane
478  * @param start The start angle of the arc on the ellipse
479  * @param sweep The angle (positive or negative) of the sweep of the arc on the ellipse
480  */
arcToBeziernull481 private fun arcToBezier(
482     p: Path,
483     cx: Double,
484     cy: Double,
485     a: Double,
486     b: Double,
487     e1x: Double,
488     e1y: Double,
489     theta: Double,
490     start: Double,
491     sweep: Double
492 ) {
493     var eta1x = e1x
494     var eta1y = e1y
495     // Taken from equations at: http://spaceroots.org/documents/ellipse/node8.html
496     // and http://www.spaceroots.org/documents/ellipse/node22.html
497 
498     // Maximum of 45 degrees per cubic Bezier segment
499     val numSegments = ceil(abs(sweep * 4 / PI)).toInt()
500 
501     var eta1 = start
502     val cosTheta = cos(theta)
503     val sinTheta = sin(theta)
504     val cosEta1 = cos(eta1)
505     val sinEta1 = sin(eta1)
506     var ep1x = (-a * cosTheta * sinEta1) - (b * sinTheta * cosEta1)
507     var ep1y = (-a * sinTheta * sinEta1) + (b * cosTheta * cosEta1)
508 
509     val anglePerSegment = sweep / numSegments
510     for (i in 0 until numSegments) {
511         val eta2 = eta1 + anglePerSegment
512         val sinEta2 = sin(eta2)
513         val cosEta2 = cos(eta2)
514         val e2x = cx + (a * cosTheta * cosEta2) - (b * sinTheta * sinEta2)
515         val e2y = cy + (a * sinTheta * cosEta2) + (b * cosTheta * sinEta2)
516         val ep2x = (-a * cosTheta * sinEta2) - (b * sinTheta * cosEta2)
517         val ep2y = (-a * sinTheta * sinEta2) + (b * cosTheta * cosEta2)
518         val tanDiff2 = tan((eta2 - eta1) / 2)
519         val alpha = sin(eta2 - eta1) * (sqrt(4 + 3.0 * tanDiff2 * tanDiff2) - 1) / 3
520         val q1x = eta1x + alpha * ep1x
521         val q1y = eta1y + alpha * ep1y
522         val q2x = e2x - alpha * ep2x
523         val q2y = e2y - alpha * ep2y
524 
525         // TODO (njawad) figure out if this is still necessary?
526         // Adding this no-op call to workaround a proguard related issue.
527         // p.relativeLineTo(0.0, 0.0)
528 
529         p.cubicTo(
530             q1x.toFloat(),
531             q1y.toFloat(),
532             q2x.toFloat(),
533             q2y.toFloat(),
534             e2x.toFloat(),
535             e2y.toFloat()
536         )
537         eta1 = eta2
538         eta1x = e2x
539         eta1y = e2y
540         ep1x = ep2x
541         ep1y = ep2y
542     }
543 }
544 
toRadiansnull545 @Suppress("NOTHING_TO_INLINE") private inline fun Double.toRadians(): Double = this / 180 * PI
546