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